├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .solhint.json ├── LICENSE ├── README.md ├── interface ├── .env.local ├── components │ ├── account-details │ │ ├── Copy.tsx │ │ ├── Transaction.tsx │ │ └── index.tsx │ ├── anchor.tsx │ ├── banner │ │ ├── common.tsx │ │ └── index.tsx │ ├── button.tsx │ ├── combine │ │ └── index.tsx │ ├── dropdown.tsx │ ├── footer │ │ ├── common.tsx │ │ └── index.tsx │ ├── header │ │ ├── common.tsx │ │ ├── hero.tsx │ │ └── index.tsx │ ├── icons │ │ ├── arrow-right.tsx │ │ ├── fortmatic.tsx │ │ ├── logo.tsx │ │ ├── metamask.tsx │ │ ├── social.tsx │ │ ├── tokens.tsx │ │ ├── triangle.tsx │ │ └── wallet-connect.tsx │ ├── input.tsx │ ├── manage │ │ └── index.tsx │ ├── modals │ │ ├── common.tsx │ │ ├── index.tsx │ │ └── wallet │ │ │ ├── Option.tsx │ │ │ ├── PendingView.tsx │ │ │ └── index.tsx │ ├── split │ │ └── index.tsx │ ├── tables │ │ ├── assets.tsx │ │ └── common.tsx │ ├── typography.tsx │ ├── web3-status.tsx │ └── widget.tsx ├── connectors │ ├── Fortmatic.ts │ ├── NetworkConnector.ts │ ├── fortmatic.d.ts │ └── index.ts ├── constants │ └── index.tsx ├── contexts │ ├── asset-allowances.tsx │ ├── asset-balances.tsx │ ├── banner.tsx │ ├── blockchain.tsx │ ├── full-token-prices.tsx │ ├── modal.tsx │ ├── split-addresses.tsx │ ├── tokens.tsx │ ├── transaction.tsx │ ├── web3-connection.tsx │ └── yield-balances.tsx ├── data │ ├── split_merge.json │ └── tokens.ts ├── hooks │ ├── contracts.ts │ ├── useEthToken.ts │ ├── useMounted.ts │ ├── useOnClickOutside.ts │ └── wallet.ts ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages │ ├── [...actionParams].tsx │ ├── _app.tsx │ ├── _document.tsx │ └── index.tsx ├── public │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ └── favicon.ico ├── theme │ └── index.tsx ├── tsconfig.json ├── types │ ├── app.ts │ ├── ethereum.ts │ └── split.ts ├── typings.d.ts ├── utils │ ├── address.ts │ ├── etherscan.ts │ ├── format.ts │ └── number.ts └── yarn-error.log ├── package.json ├── protocol ├── .env_example ├── contracts │ ├── CTokenPriceOracle.sol │ ├── CapitalComponentToken.sol │ ├── SplitPoolFactory.sol │ ├── SplitVault.sol │ ├── VaultControlled.sol │ ├── YieldComponentToken.sol │ ├── interfaces │ │ ├── CTokenInterface.sol │ │ └── PriceOracle.sol │ ├── lib │ │ ├── DSMath.sol │ │ ├── ERC20Base.sol │ │ └── balancer │ │ │ ├── configurable-rights-pool │ │ │ ├── contracts │ │ │ │ ├── CRPFactory.sol │ │ │ │ ├── ConfigurableRightsPool.sol │ │ │ │ ├── IBFactory.sol │ │ │ │ ├── Migrations.sol │ │ │ │ ├── PCToken.sol │ │ │ │ └── utils │ │ │ │ │ ├── BalancerOwnable.sol │ │ │ │ │ └── BalancerReentrancyGuard.sol │ │ │ ├── interfaces │ │ │ │ ├── BalancerIERC20.sol │ │ │ │ └── IConfigurableRightsPool.sol │ │ │ └── libraries │ │ │ │ ├── BalancerConstants.sol │ │ │ │ ├── BalancerSafeMath.sol │ │ │ │ ├── RightsManager.sol │ │ │ │ ├── SafeApprove.sol │ │ │ │ └── SmartPoolManager.sol │ │ │ └── core │ │ │ ├── BColor.sol │ │ │ ├── BConst.sol │ │ │ ├── BFactory.sol │ │ │ ├── BMath.sol │ │ │ ├── BNum.sol │ │ │ ├── BPool.sol │ │ │ └── BToken.sol │ └── mocks │ │ ├── ERC20Mock.sol │ │ ├── PriceOracleMock.sol │ │ └── SplitVaultMock.sol ├── deployments │ ├── index.ts │ └── types.ts ├── hardhat.config.ts ├── index.ts ├── package.json ├── tasks │ ├── add_component_set.ts │ ├── deploy_component_tokens.ts │ ├── deploy_oracle.ts │ ├── deploy_pool_factory.ts │ ├── deploy_split_pool.ts │ ├── deploy_vault.ts │ ├── index.ts │ ├── mint_test_token.ts │ └── swap_tokens.ts ├── test │ ├── capital_component_token.ts │ ├── constants.ts │ ├── split_pool_factory.ts │ ├── split_vault.ts │ ├── types.ts │ ├── utils.ts │ └── yield_component_token.ts ├── tsconfig.json ├── yarn-error.log └── yarn.lock ├── split-banner.png ├── tsconfig.json └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/interface" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "npm" 8 | directory: "/protocol" 9 | schedule: 10 | interval: "weekly" -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Build and Test 5 | 6 | on: [push] 7 | 8 | jobs: 9 | build_and_test: 10 | name: Build and Test 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [12.x] 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v2 20 | 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | 26 | - name: Cache node modules 27 | uses: actions/cache@v2 28 | env: 29 | cache-name: cached-node-modules 30 | with: 31 | path: ~/work/split/split/node_modules 32 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('yarn.lock') }} 33 | 34 | - run: yarn install --frozen-lockfile 35 | - run: yarn prettier:check 36 | - run: yarn build 37 | - run: yarn test 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | .vscode 3 | node_modules 4 | 5 | #Buidler files 6 | cache 7 | artifacts 8 | 9 | #Typechain files 10 | typechain 11 | 12 | .env 13 | #React + Next files 14 | **/.next 15 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | typechain/ 2 | node_modules/ 3 | artifacts/ 4 | cache/ 5 | .vscode/ 6 | **/.next -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "printWidth": 120, 5 | "singleQuote": false, 6 | "tabWidth": 2, 7 | "trailingComma": "all", 8 | "overrides": [ 9 | { 10 | "files": "*.sol", 11 | "options": { 12 | "tabWidth": 2 13 | } 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "code-complexity": ["error", 7], 6 | "compiler-version": ["error", "^0.7.2"], 7 | "constructor-syntax": "error", 8 | "max-line-length": ["error", 120], 9 | "not-rely-on-time": "off", 10 | "prettier/prettier": "error", 11 | "reason-string": ["warn", { "maxLength": 64 }] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |  2 | -------------------------------------------------------------------------------- /interface/.env.local: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_CHAIN_ID="4" 2 | NEXT_PUBLIC_NETWORK_URL="https://eth-rinkeby.alchemyapi.io/v2/U2ncZJ--7GNdQJY9GUTTMy9MYAJTj3OP" 3 | NEXT_PUBLIC_FORTMATIC_KEY="pk_test_C172CCBF3C04CFA8" -------------------------------------------------------------------------------- /interface/components/account-details/Copy.tsx: -------------------------------------------------------------------------------- 1 | // import React from 'react' 2 | // import styled from 'styled-components' 3 | // import useCopyClipboard from '../../hooks/useCopyClipboard' 4 | 5 | // import { LinkStyledButton } from '../../theme' 6 | // import { CheckCircle, Copy } from 'react-feather' 7 | 8 | // const CopyIcon = styled(LinkStyledButton)` 9 | // color: ${({ theme }) => theme.text3}; 10 | // flex-shrink: 0; 11 | // display: flex; 12 | // text-decoration: none; 13 | // font-size: 0.825rem; 14 | // :hover, 15 | // :active, 16 | // :focus { 17 | // text-decoration: none; 18 | // color: ${({ theme }) => theme.text2}; 19 | // } 20 | // ` 21 | // const TransactionStatusText = styled.span` 22 | // margin-left: 0.25rem; 23 | // font-size: 0.825rem; 24 | // ${({ theme }) => theme.flexRowNoWrap}; 25 | // align-items: center; 26 | // ` 27 | 28 | // export default function CopyHelper(props: { toCopy: string; children?: React.ReactNode }) { 29 | // const [isCopied, setCopied] = useCopyClipboard() 30 | 31 | // return ( 32 | // setCopied(props.toCopy)}> 33 | // {isCopied ? ( 34 | // 35 | // 36 | // Copied 37 | // 38 | // ) : ( 39 | // 40 | // 41 | // 42 | // )} 43 | // {isCopied ? '' : props.children} 44 | // 45 | // ) 46 | // } 47 | -------------------------------------------------------------------------------- /interface/components/account-details/Transaction.tsx: -------------------------------------------------------------------------------- 1 | // import React from 'react' 2 | // import styled from 'styled-components' 3 | // import { CheckCircle, Triangle } from 'react-feather' 4 | 5 | // import { useActiveWeb3React } from '../../hooks' 6 | // import { getEtherscanLink } from '../../utils' 7 | // import { ExternalLink } from '../../theme' 8 | // import { useAllTransactions } from '../../state/transactions/hooks' 9 | // import { RowFixed } from '../Row' 10 | // import Loader from '../Loader' 11 | 12 | // const TransactionWrapper = styled.div`` 13 | 14 | // const TransactionStatusText = styled.div` 15 | // margin-right: 0.5rem; 16 | // display: flex; 17 | // align-items: center; 18 | // :hover { 19 | // text-decoration: underline; 20 | // } 21 | // ` 22 | 23 | // const TransactionState = styled(ExternalLink)<{ pending: boolean; success?: boolean }>` 24 | // display: flex; 25 | // justify-content: space-between; 26 | // align-items: center; 27 | // text-decoration: none !important; 28 | // border-radius: 0.5rem; 29 | // padding: 0.25rem 0rem; 30 | // font-weight: 500; 31 | // font-size: 0.825rem; 32 | // color: ${({ theme }) => theme.primary1}; 33 | // ` 34 | 35 | // const IconWrapper = styled.div<{ pending: boolean; success?: boolean }>` 36 | // color: ${({ pending, success, theme }) => (pending ? theme.primary1 : success ? theme.green1 : theme.red1)}; 37 | // ` 38 | 39 | // export default function Transaction({ hash }: { hash: string }) { 40 | // const { chainId } = useActiveWeb3React() 41 | // const allTransactions = useAllTransactions() 42 | 43 | // const tx = allTransactions?.[hash] 44 | // const summary = tx?.summary 45 | // const pending = !tx?.receipt 46 | // const success = !pending && tx && (tx.receipt?.status === 1 || typeof tx.receipt?.status === 'undefined') 47 | 48 | // if (!chainId) return null 49 | 50 | // return ( 51 | // 52 | // 53 | // 54 | // {summary ?? hash} ↗ 55 | // 56 | // 57 | // {pending ? : success ? : } 58 | // 59 | // 60 | // 61 | // ) 62 | // } 63 | -------------------------------------------------------------------------------- /interface/components/anchor.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const PrimaryAnchor = styled.a` 4 | font-size: 16px; 5 | color: white; 6 | text-decoration: none; 7 | font-weight: bold; 8 | &:focus { 9 | outline: none; 10 | } 11 | `; 12 | 13 | export const NoStyledAnchor = styled.a` 14 | text-decoration: none; 15 | &:focus { 16 | outline: none; 17 | } 18 | `; 19 | -------------------------------------------------------------------------------- /interface/components/banner/common.tsx: -------------------------------------------------------------------------------- 1 | import { useWeb3React } from "@web3-react/core"; 2 | import { FC } from "react"; 3 | import styled from "styled-components"; 4 | import { BannerMetadata, BannerType, TxBannerMetadata } from "../../types/app"; 5 | import { getEtherscanLink } from "../../utils/etherscan"; 6 | import { PrimaryAnchor } from "../anchor"; 7 | import { FOOTER_HEIGHT } from "../footer/common"; 8 | import { H3 } from "../typography"; 9 | 10 | export const BannersWrapper = styled.div` 11 | position: fixed; 12 | right: 0; 13 | left: 0; 14 | bottom: ${FOOTER_HEIGHT}px; 15 | `; 16 | 17 | const bannerTypeToBackgroundColor = (type: BannerType) => { 18 | if (type === "success") { 19 | return "#97CB93"; 20 | } 21 | if (type === "error") { 22 | return "#D36D6D"; 23 | } 24 | return "#FFFFFF"; 25 | }; 26 | 27 | const bannerTypeToColor = (type: BannerType) => { 28 | if (type === "success") { 29 | return "#0E2991"; 30 | } 31 | if (type === "error") { 32 | return "#0E2991"; 33 | } 34 | return "#0E2991"; 35 | }; 36 | 37 | export interface BannerTypeProps { 38 | type: BannerType; 39 | } 40 | 41 | export const BannerWrapper = styled.div` 42 | width: 100%; 43 | background-color: ${props => bannerTypeToBackgroundColor(props.type)}; 44 | display: flex; 45 | align-items: center; 46 | justify-content: center; 47 | padding: 16px 0; 48 | `; 49 | 50 | const StyledH3 = styled(H3)` 51 | color: ${props => bannerTypeToColor(props.type)}; 52 | `; 53 | 54 | const StyledA = styled(PrimaryAnchor)` 55 | color: ${props => bannerTypeToColor(props.type)}; 56 | font-size: 28px; 57 | `; 58 | 59 | export const TxBanner: FC = ({ type, txHash, description }) => { 60 | const { chainId } = useWeb3React(); 61 | 62 | return ( 63 | 64 | {description} 65 | 71 | see on etherscan 72 | 73 | 74 | ); 75 | }; 76 | 77 | export const Banner: FC = ({ type, description }) => { 78 | return ( 79 | 80 | {description} 81 | 82 | ); 83 | }; 84 | -------------------------------------------------------------------------------- /interface/components/banner/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useBannerActions, useBanners } from "../../contexts/banner"; 3 | import { useMounted } from "../../hooks/useMounted"; 4 | import { BannerMetadata, TxBannerMetadata } from "../../types/app"; 5 | import { BannersWrapper, Banner, TxBanner } from "./common"; 6 | 7 | export const Banners = () => { 8 | const banners = useBanners(); 9 | const undismissedBanners = banners.filter(b => !b.dismissed); 10 | 11 | return ( 12 | 13 | {undismissedBanners.map((b: BannerMetadata, i: number) => { 14 | if (!!(b as TxBannerMetadata).txHash) { 15 | return ; 16 | } 17 | return ; 18 | // TODO key is not that great, need to change 19 | })} 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /interface/components/button.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const PrimaryButton = styled.button` 4 | border: 2px white solid; 5 | font-weight: 700; 6 | cursor: pointer; 7 | border-radius: 999px; 8 | padding: 12px 32px; 9 | color: white; 10 | letter-spacing: 0.05rem; 11 | font-size: 14px; 12 | font-weight: bold; 13 | background-color: rgba(0, 0, 0, 0); 14 | &:focus { 15 | outline: none; 16 | } 17 | &:hover:enabled, 18 | &:hover:enabled > * { 19 | background-color: #ffffff; 20 | color: #0e2991; 21 | font-style: italic; 22 | } 23 | &:disabled { 24 | opacity: 0.5; 25 | cursor: default; 26 | } 27 | `; 28 | 29 | export const SecondaryDarkButton = styled.button` 30 | border: 2px rgba(0, 0, 0, 0.05) solid; 31 | font-weight: 900; 32 | padding: 12px 32px; 33 | color: black; 34 | letter-spacing: 0.05rem; 35 | background-color: rgba(0, 0, 0, 0); 36 | font-size: 14px; 37 | &:focus:enabled { 38 | outline: none; 39 | } 40 | &:hover:enabled { 41 | color: #0e2991; 42 | border-color: #0e2991; 43 | font-style: italic; 44 | } 45 | &:disabled { 46 | opacity: 0.5; 47 | cursor: default; 48 | } 49 | `; 50 | -------------------------------------------------------------------------------- /interface/components/combine/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from "react"; 2 | import styled from "styled-components"; 3 | 4 | import { useSplitVault } from "../../hooks/contracts"; 5 | import { useFullTokens } from "../../contexts/tokens"; 6 | import { useFullTokenPrice } from "../../contexts/full-token-prices"; 7 | import { useSplitProtocolAddresses } from "../../contexts/split-addresses"; 8 | 9 | import { 10 | componentTokenAmountToFullTokenAmount, 11 | convertToBaseAmount, 12 | fullTokenAmountToComponentTokenAmount, 13 | } from "../../utils/number"; 14 | 15 | import { H1 } from "../typography"; 16 | import { Dropdown } from "../dropdown"; 17 | import { ConfirmButton, InputContainer } from "../widget"; 18 | import { TokenInput } from "../input"; 19 | import { useTransactionActions } from "../../contexts/transaction"; 20 | 21 | const CombineButton = styled(ConfirmButton)` 22 | font-size: 32px; 23 | `; 24 | 25 | const CombineContainer = styled.div` 26 | display: flex; 27 | flex-direction: column; 28 | `; 29 | 30 | export interface CombineWidgetProps {} 31 | 32 | export const CombineWidget: React.FC = () => { 33 | const { splitVault } = useSplitVault(); 34 | const tokens = useFullTokens(); 35 | const [selectedTokenIndex, setSelectedTokenIndex] = useState(0); 36 | const { addTransaction } = useTransactionActions(); 37 | const [value, setValue] = useState(""); 38 | const selectedToken = tokens[selectedTokenIndex]; 39 | const price = useFullTokenPrice(selectedToken.tokenAddress); 40 | const deployment = useSplitProtocolAddresses(); 41 | const baseAmount = convertToBaseAmount(value || "0", selectedToken.componentTokens.yieldComponentToken.decimals); 42 | 43 | const onCombineClick = useCallback(async () => { 44 | // No allowance needed for combining 45 | const tx = await splitVault.combine(baseAmount.toString(), selectedToken.tokenAddress); 46 | addTransaction(tx.hash, { 47 | fullToken: selectedToken, 48 | componentTokenAmount: baseAmount, 49 | type: "combine", 50 | }); 51 | // TODO: clear input on success???? 52 | setValue(""); 53 | }, [value, splitVault, selectedToken, deployment]); 54 | 55 | if (!tokens || !tokens.length || !price) { 56 | return Please connect your wallet.; 57 | } 58 | 59 | const dropdownItems = tokens.map(asset => ({ 60 | id: asset.tokenAddress, 61 | displayName: asset.symbol, 62 | })); 63 | 64 | const fullTokenValue = componentTokenAmountToFullTokenAmount( 65 | baseAmount, 66 | price, 67 | selectedToken.userlyingAssetMetaData.decimals, 68 | ) 69 | .toDecimalPlaces(4) 70 | .toString(); 71 | return ( 72 | 73 | 74 | combine 75 | 80 | {selectedToken.componentTokens.capitalComponentToken.symbol} 81 | and 82 | 87 | {selectedToken.componentTokens.yieldComponentToken.symbol} 88 | to get 89 | {fullTokenValue} 90 | 91 | 92 | 93 | Combine 94 | 95 | 96 | ); 97 | }; 98 | -------------------------------------------------------------------------------- /interface/components/dropdown.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styled from "styled-components"; 3 | 4 | import { H1 } from "./typography"; 5 | import { Triangle } from "./icons/triangle"; 6 | import { useIsOpenUntilOutside } from "../hooks/useOnClickOutside"; 7 | 8 | const DropdownContainer = styled.div` 9 | display: flex; 10 | align-items: center; 11 | cursor: pointer; 12 | `; 13 | 14 | const Selector = styled.div` 15 | display: flex; 16 | justify-content: space-between; 17 | align-items: center; 18 | `; 19 | 20 | const ArrowContainer = styled.div` 21 | margin-left: 10px; 22 | `; 23 | 24 | const Select = styled.div` 25 | position: absolute; 26 | border-radius: 21px; 27 | border: 2px solid white; 28 | padding: 30px; 29 | background-color: #0c2ea0; 30 | `; 31 | 32 | const Option = styled.div` 33 | font-size: 40px; 34 | color: white; 35 | margin: 20px 0px; 36 | &:hover { 37 | font-style: italic; 38 | font-weight: bold; 39 | } 40 | `; 41 | 42 | export interface DropdownItem { 43 | id: string; 44 | displayName: string; 45 | } 46 | 47 | export interface DropdownProps { 48 | selectedId: string; 49 | items: DropdownItem[]; 50 | onSelect?: (itemId: string) => void; 51 | onSelectIndex?: (index: number) => void; 52 | className?: string; 53 | } 54 | 55 | const noop = () => {}; 56 | 57 | export const Dropdown: React.FC = ({ 58 | selectedId, 59 | className, 60 | onSelect = noop, 61 | onSelectIndex = noop, 62 | items = [], 63 | }) => { 64 | const [isOpen, setIsOpen, node] = useIsOpenUntilOutside(); 65 | const selectedItem = items.find(item => item.id === selectedId); 66 | if (!selectedItem) { 67 | return null; 68 | } 69 | return ( 70 | setIsOpen(true)} className={className}> 71 | 72 | {selectedItem.displayName} 73 | 74 | 75 | 76 | 77 | {isOpen && ( 78 | 79 | {items.map((item, index) => ( 80 | { 83 | onSelect(item.id); 84 | onSelectIndex(index); 85 | setIsOpen(false); 86 | e.stopPropagation(); 87 | }} 88 | > 89 | {item.displayName} 90 | 91 | ))} 92 | 93 | )} 94 | 95 | ); 96 | }; 97 | -------------------------------------------------------------------------------- /interface/components/footer/common.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const FOOTER_HEIGHT = 90; 4 | 5 | export const FooterSpacer = styled.div` 6 | height: ${FOOTER_HEIGHT}px; 7 | width: 100%; 8 | `; 9 | 10 | export const FooterContentWrapper = styled.div` 11 | display: flex; 12 | align-items: center; 13 | justify-content: space-between; 14 | height: ${FOOTER_HEIGHT}px; 15 | padding: 0 24px; 16 | width: 100%; 17 | `; 18 | 19 | export const FooterRightContentWrapper = styled.div``; 20 | 21 | export const FooterLeftContentWrapper = styled.div``; 22 | -------------------------------------------------------------------------------- /interface/components/footer/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | import { P } from "../typography"; 5 | import { Discord, Github, Twitter } from "../icons/social"; 6 | import { FooterContentWrapper, FooterLeftContentWrapper, FooterRightContentWrapper } from "./common"; 7 | 8 | const LinksWrapper = styled.div` 9 | display: grid; 10 | grid-gap: 40px; 11 | grid-template-columns: repeat(3, 1fr); 12 | `; 13 | 14 | export const Footer: React.FC = () => { 15 | return ( 16 | 17 | 18 | © SPLIT 2020 UNAUDITED BETA – Mainnet, Rinkeby 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /interface/components/header/common.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const HEADER_HEIGHT = 90; 4 | 5 | export const HeaderSpacer = styled.div` 6 | height: ${HEADER_HEIGHT}px; 7 | width: 100%; 8 | `; 9 | 10 | export const HeaderContentWrapper = styled.div` 11 | display: flex; 12 | align-items: center; 13 | justify-content: space-between; 14 | height: ${HEADER_HEIGHT}px; 15 | padding: 0 30px; 16 | width: 100%; 17 | position: relative; 18 | `; 19 | 20 | export const HeaderRightContentWrapper = styled.div` 21 | position: relative; 22 | z-index: 1000; 23 | `; 24 | 25 | export const HeaderLeftContentWrapper = styled.div` 26 | position: relative; 27 | z-index: 1000; 28 | `; 29 | 30 | export const HeaderCenterContentWrapper = styled.div` 31 | display: flex; 32 | align-items: center; 33 | justify-content: space-around; 34 | top: 0; 35 | left: 0; 36 | right: 0; 37 | height: ${HEADER_HEIGHT}px; 38 | z-index: 999; 39 | width: 100%; 40 | `; 41 | -------------------------------------------------------------------------------- /interface/components/header/hero.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import React from "react"; 3 | import styled from "styled-components"; 4 | 5 | import { PATHS } from "../../constants"; 6 | import { PrimaryButton } from "../button"; 7 | import { LogoFull } from "../icons/logo"; 8 | 9 | import { HeaderContentWrapper, HeaderLeftContentWrapper, HeaderRightContentWrapper } from "./common"; 10 | 11 | const LogoWrapper = styled.div` 12 | cursor: pointer; 13 | `; 14 | 15 | export const HeroHeader: React.FC = () => { 16 | const router = useRouter(); 17 | const onGoToAppClick = () => { 18 | router.push(PATHS.SPLIT); 19 | }; 20 | const goToHome = () => { 21 | router.push(PATHS.ROOT); 22 | }; 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Go to app 32 | 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /interface/components/header/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useRouter } from "next/router"; 3 | import styled from "styled-components"; 4 | import { LogoSmall } from "../icons/logo"; 5 | import { 6 | HeaderContentWrapper, 7 | HeaderLeftContentWrapper, 8 | HeaderCenterContentWrapper, 9 | HeaderRightContentWrapper, 10 | } from "./common"; 11 | import Web3Status from "../web3-status"; 12 | import { PATHS } from "../../constants"; 13 | import { NoStyledAnchor } from "../anchor"; 14 | import { AppAction } from "../../types/app"; 15 | 16 | interface HeaderProps { 17 | showTabs?: boolean; 18 | currentAppAction: AppAction; 19 | onTabClick: (appAction: AppAction) => void; 20 | } 21 | 22 | interface TabButtonProps { 23 | isActive?: boolean; 24 | } 25 | 26 | const TabButton = styled.button` 27 | font-weight: ${props => (props.isActive ? 700 : 300)}; 28 | font-style: ${props => (props.isActive ? "italic" : "normal")}; 29 | padding: 12px 0px; 30 | color: white; 31 | letter-spacing: 0.1rem; 32 | background-color: rgba(0, 0, 0, 0); 33 | font-size: 36px; 34 | cursor: pointer; 35 | border: 0px solid transparent; 36 | &:focus { 37 | outline: none; 38 | } 39 | &:hover { 40 | font-weight: 700; 41 | font-style: italic; 42 | } 43 | `; 44 | 45 | const LogoWrapper = styled.div` 46 | cursor: pointer; 47 | width: 200px; 48 | `; 49 | 50 | const APP_ACTION_TO_TAB_TITLE = { 51 | [AppAction.SPLIT]: "split", 52 | [AppAction.MANAGE]: "manage", 53 | [AppAction.COMBINE]: "combine", 54 | }; 55 | 56 | export const Header: React.FC = ({ showTabs, currentAppAction, onTabClick }) => { 57 | const router = useRouter(); 58 | 59 | const onSplitIconClick = () => { 60 | router.push(PATHS.ROOT); 61 | }; 62 | 63 | return ( 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | {showTabs ? ( 73 | 74 | {Object.values(AppAction).map((appAction: AppAction) => { 75 | return ( 76 | 81 | {APP_ACTION_TO_TAB_TITLE[appAction]} 82 | 83 | ); 84 | })} 85 | 86 | ) : null} 87 | 88 | 89 | 90 | 91 | ); 92 | }; 93 | -------------------------------------------------------------------------------- /interface/components/icons/arrow-right.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface ArrowRightIconProps { 4 | className?: string; 5 | } 6 | 7 | export const ArrowRightIcon: React.FC = ({ className }) => { 8 | return ( 9 | 17 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /interface/components/icons/fortmatic.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface FortmaticIconProps { 4 | className?: string; 5 | } 6 | 7 | export const FortmaticIcon: React.FC = ({ className }) => { 8 | return ( 9 | 17 | 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /interface/components/icons/logo.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface SVGProps { 4 | width?: string; 5 | height?: string; 6 | } 7 | 8 | export const LogoFull: React.FC = ({ width = "258", height = "117" }) => ( 9 | 10 | 17 | 18 | 25 | 32 | 39 | 46 | 47 | 48 | ); 49 | 50 | export const LogoSmall: React.FC = ({ width = "23", height = "41" }) => ( 51 | 52 | 53 | 54 | 61 | 62 | 63 | 64 | ); 65 | -------------------------------------------------------------------------------- /interface/components/icons/metamask.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | interface IconProps { 4 | className?: string; 5 | } 6 | 7 | export const MetaMaskIcon: FC = ({ className }) => ( 8 | 9 | 10 | 14 | 18 | 22 | 26 | 30 | 31 | 32 | 33 | 34 | 38 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | ); 53 | -------------------------------------------------------------------------------- /interface/components/icons/social.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface SVGProps { 4 | width?: string; 5 | height?: string; 6 | } 7 | 8 | export const Discord: React.FC = ({ width = "36", height = "36" }) => ( 9 | 10 | 17 | 24 | 31 | 32 | ); 33 | 34 | export const Github: React.FC = ({ width = "36", height = "36" }) => ( 35 | 36 | 43 | 44 | ); 45 | 46 | export const Twitter: React.FC = ({ width = "36", height = "36" }) => ( 47 | 48 | 54 | 55 | ); 56 | -------------------------------------------------------------------------------- /interface/components/icons/tokens.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface SVGProps { 4 | width?: string; 5 | height?: string; 6 | } 7 | 8 | export const CapitalToken: React.FC = ({ width = "297", height = "297" }) => ( 9 | 10 | 11 | 18 | 25 | 26 | c 27 | 28 | 29 | 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | 44 | export const YieldToken: React.FC = ({ width = "297", height = "297" }) => ( 45 | 46 | 47 | 54 | 61 | 62 | y 63 | 64 | 65 | 73 | 74 | 75 | 76 | 77 | 78 | ); 79 | 80 | export const GovernanceToken: React.FC = ({ width = "297", height = "297" }) => ( 81 | 82 | 83 | 90 | 97 | 98 | g 99 | 100 | 101 | 109 | 110 | 111 | 112 | 113 | 114 | ); 115 | -------------------------------------------------------------------------------- /interface/components/icons/triangle.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface SVGProps { 4 | width?: string; 5 | height?: string; 6 | } 7 | 8 | export const Triangle: React.FC = ({ width = "20", height = "17" }) => ( 9 | 19 | 20 | 21 | ); 22 | -------------------------------------------------------------------------------- /interface/components/icons/wallet-connect.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface WalletConnectIconProps { 4 | className?: string; 5 | } 6 | 7 | export const WalletConnectIcon: React.FC = ({ className }) => { 8 | return ( 9 | 17 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /interface/components/input.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from "react"; 2 | import Decimal from "decimal.js"; 3 | import styled, { css } from "styled-components"; 4 | 5 | import { colors } from "../theme"; 6 | import { useAssetBalance } from "../contexts/asset-balances"; 7 | import { useToken } from "../contexts/tokens"; 8 | import { convertToUnitAmount } from "../utils/number"; 9 | 10 | const SplitInput = styled.input<{ isError?: boolean }>` 11 | background: transparent; 12 | border: none; 13 | border-bottom: 2px solid white; 14 | outline: none; 15 | box-shadow: none; 16 | color: white; 17 | font-size: 40px; 18 | padding: 10px 0px; 19 | min-width: 250px; 20 | ${props => 21 | props.isError && 22 | css` 23 | color: ${colors.red}; 24 | border-bottom: 2px solid ${colors.red} !important; 25 | `} 26 | &:focus { 27 | font-style: italic; 28 | font-weight: bold; 29 | border-bottom: 3px solid white; 30 | } 31 | `; 32 | 33 | const InputContainer = styled.div<{ isDisabled: boolean }>` 34 | display: flex; 35 | flex-direction: column; 36 | ${props => (props.isDisabled ? "opacity: 0.6;" : "")} 37 | `; 38 | 39 | const Message = styled.label<{ isError?: boolean }>` 40 | margin: 10px 0px; 41 | font-weight: bold; 42 | ${props => 43 | props.isError && 44 | css` 45 | color: ${colors.red} !important; 46 | `} 47 | `; 48 | 49 | export interface InputProps { 50 | value: string; 51 | onChange: (value: string) => void; 52 | message?: string; 53 | errorMessage?: string; 54 | className?: string; 55 | maxLength?: number; 56 | isDisabled?: boolean; 57 | } 58 | 59 | export const Input: React.FC = props => { 60 | const { value, onChange, message, errorMessage, className, isDisabled, maxLength = 10 } = props; 61 | const innerOnChange = useCallback( 62 | (newValue: string, oldValue: string) => { 63 | if (newValue.length > maxLength) { 64 | onChange(oldValue); 65 | return; 66 | } 67 | onChange(newValue); 68 | }, 69 | [onChange, maxLength], 70 | ); 71 | const isError = !!errorMessage; 72 | return ( 73 | 74 | innerOnChange(e.target.value, value)} 82 | /> 83 | {!isError && message && {message}} 84 | {isError && {errorMessage}} 85 | 86 | ); 87 | }; 88 | 89 | export interface TokenInputProps { 90 | tokenAddress: string; 91 | value: string; 92 | onChange: (value: string) => void; 93 | } 94 | 95 | export const TokenInput: React.FC = ({ tokenAddress, value, onChange }) => { 96 | const tokenBalance = useAssetBalance(tokenAddress); 97 | const token = useToken(tokenAddress); 98 | let errorMsg = ""; 99 | const unitTokenBalance = convertToUnitAmount(tokenBalance, token.decimals); 100 | const decimalValue = new Decimal(value || "0"); 101 | if (unitTokenBalance.lessThan(decimalValue)) { 102 | errorMsg = `you don't have enough ${token.symbol}`; 103 | } 104 | const maxString = unitTokenBalance.toString(); 105 | return ( 106 | 107 | ); 108 | }; 109 | -------------------------------------------------------------------------------- /interface/components/manage/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useMemo, useState } from "react"; 2 | import styled from "styled-components"; 3 | 4 | import { useWeb3React } from "@web3-react/core"; 5 | import { H1 } from "../typography"; 6 | import { useAssetBalances } from "../../contexts/asset-balances"; 7 | import { AssetsTable } from "../tables/assets"; 8 | 9 | const TableH1 = styled(H1)` 10 | margin-bottom: 24px; 11 | `; 12 | 13 | const ManagePageWrapper = styled.div` 14 | display: grid; 15 | grid-template-columns: 3fr 5fr; 16 | grid-gap: 72px; 17 | width: 1024px; 18 | `; 19 | 20 | const ManageColumnContainer = styled.div``; 21 | 22 | export interface ManageWidgetProps {} 23 | 24 | export const ManageWidget: React.FC = () => { 25 | const { active, error } = useWeb3React(); 26 | 27 | if (!active || error) { 28 | return Please connect your wallet.; 29 | } 30 | 31 | return ( 32 | 33 | 34 | capital 35 | 36 | 37 | 38 | yield 39 | 40 | 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /interface/components/modals/common.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled, { css } from "styled-components"; 3 | import { animated, useTransition, useSpring } from "react-spring"; 4 | import { DialogOverlay, DialogContent } from "@reach/dialog"; 5 | import { isMobile } from "react-device-detect"; 6 | import "@reach/dialog/styles.css"; 7 | import { transparentize } from "polished"; 8 | import { useGesture } from "react-use-gesture"; 9 | 10 | const AnimatedDialogOverlay = animated(DialogOverlay); 11 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 12 | const StyledDialogOverlay = styled(AnimatedDialogOverlay)` 13 | &[data-reach-dialog-overlay] { 14 | z-index: 2; 15 | overflow: hidden; 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | background-color: rgba(0, 0, 0, 0.5); 20 | } 21 | `; 22 | 23 | const AnimatedDialogContent = animated(DialogContent); 24 | // destructure to not pass custom props to Dialog DOM element 25 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 26 | const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...rest }) => ( 27 | 28 | )).attrs({ 29 | "aria-label": "dialog", 30 | })` 31 | overflow-y: ${({ mobile }) => (mobile ? "scroll" : "hidden")}; 32 | 33 | &[data-reach-dialog-content] { 34 | margin: 0 0 2rem 0; 35 | background-color: #ffffff; 36 | box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.95, "#000000")}; 37 | padding: 0px; 38 | width: 50vw; 39 | overflow-y: ${({ mobile }) => (mobile ? "scroll" : "hidden")}; 40 | overflow-x: hidden; 41 | 42 | align-self: ${({ mobile }) => (mobile ? "flex-end" : "center")}; 43 | 44 | max-width: 420px; 45 | ${({ maxHeight }) => 46 | maxHeight && 47 | css` 48 | max-height: ${maxHeight}vh; 49 | `} 50 | ${({ minHeight }) => 51 | minHeight && 52 | css` 53 | min-height: ${minHeight}vh; 54 | `} 55 | display: flex; 56 | @media (max-width: 960px) { 57 | width: 65vw; 58 | margin: 0; 59 | } 60 | @media (max-width: 720px) { 61 | width: 85vw; 62 | margin: 0; 63 | // TODO patch for mobile 64 | width: 100vw; 65 | border-radius: 20px; 66 | border-bottom-left-radius: 0; 67 | border-bottom-right-radius: 0; 68 | } 69 | } 70 | `; 71 | 72 | interface ModalProps { 73 | isOpen: boolean; 74 | onDismiss: () => void; 75 | minHeight?: number | false; 76 | maxHeight?: number; 77 | initialFocusRef?: React.RefObject; 78 | children?: React.ReactNode; 79 | } 80 | 81 | export function Modal({ isOpen, onDismiss, minHeight = false, maxHeight = 90, initialFocusRef, children }: ModalProps) { 82 | const fadeTransition = useTransition(isOpen, null, { 83 | config: { duration: 200 }, 84 | from: { opacity: 0 }, 85 | enter: { opacity: 1 }, 86 | leave: { opacity: 0 }, 87 | }); 88 | 89 | const [{ y }, set] = useSpring(() => ({ y: 0, config: { mass: 1, tension: 210, friction: 20 } })); 90 | const bind = useGesture({ 91 | onDrag: state => { 92 | set({ 93 | y: state.down ? state.movement[1] : 0, 94 | }); 95 | if (state.movement[1] > 300 || (state.velocity > 3 && state.direction[1] > 0)) { 96 | onDismiss(); 97 | } 98 | }, 99 | }); 100 | 101 | return ( 102 | <> 103 | {fadeTransition.map( 104 | ({ item, key, props }) => 105 | item && ( 106 | 107 | `translateY(${y > 0 ? y : 0}px)`) }, 112 | } 113 | : {})} 114 | aria-label="dialog content" 115 | minHeight={minHeight} 116 | maxHeight={maxHeight} 117 | mobile={isMobile} 118 | > 119 | {/* prevents the automatic focusing of inputs on mobile by the reach dialog */} 120 | {!initialFocusRef && isMobile ? : null} 121 | {children} 122 | 123 | 124 | ), 125 | )} 126 | > 127 | ); 128 | } 129 | -------------------------------------------------------------------------------- /interface/components/modals/index.tsx: -------------------------------------------------------------------------------- 1 | import WalletModal from "./wallet"; 2 | 3 | export const Modals = () => { 4 | return ( 5 | <> 6 | 7 | > 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /interface/components/modals/wallet/Option.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { PrimaryAnchor } from "../../anchor"; 4 | import { SecondaryDarkButton } from "../../button"; 5 | import { FadedDark } from "../../typography"; 6 | 7 | const InfoCard = styled(SecondaryDarkButton)` 8 | position: relative; 9 | width: 100% !important; 10 | `; 11 | 12 | const OptionCard = styled(InfoCard as any)` 13 | display: flex; 14 | flex-direction: row; 15 | align-items: center; 16 | justify-content: space-between; 17 | margin-top: 2rem; 18 | padding: 1rem; 19 | `; 20 | 21 | const OptionCardLeft = styled.div` 22 | ${({ theme }) => theme.flexColumnNoWrap}; 23 | justify-content: center; 24 | height: 100%; 25 | `; 26 | 27 | const OptionCardClickable = styled(OptionCard as any)<{ clickable?: boolean }>` 28 | margin-top: 0; 29 | &:hover { 30 | cursor: ${({ clickable }) => (clickable ? "pointer" : "")}; 31 | border: ${({ clickable, theme }) => (clickable ? `1px solid ${theme.primary1}` : ``)}; 32 | } 33 | opacity: ${({ disabled }) => (disabled ? "0.5" : "1")}; 34 | `; 35 | 36 | const GreenCircle = styled.div` 37 | ${({ theme }) => theme.flexRowNoWrap} 38 | justify-content: center; 39 | align-items: center; 40 | 41 | &:first-child { 42 | height: 8px; 43 | width: 8px; 44 | margin-right: 8px; 45 | background-color: ${({ theme }) => theme.green1}; 46 | border-radius: 50%; 47 | } 48 | `; 49 | 50 | const CircleWrapper = styled.div` 51 | color: ${({ theme }) => theme.green1}; 52 | display: flex; 53 | justify-content: center; 54 | align-items: center; 55 | `; 56 | 57 | const HeaderText = styled.div` 58 | ${({ theme }) => theme.flexRowNoWrap}; 59 | color: ${props => (props.color === "blue" ? ({ theme }) => theme.primary1 : ({ theme }) => theme.text1)}; 60 | font-size: 1rem; 61 | font-weight: 500; 62 | `; 63 | 64 | const SubHeader = styled.div` 65 | color: ${({ theme }) => theme.text1}; 66 | margin-top: 10px; 67 | font-size: 12px; 68 | `; 69 | 70 | export default function Option({ 71 | link = null, 72 | clickable = true, 73 | size, 74 | onClick = null, 75 | color, 76 | header, 77 | subheader = null, 78 | icon, 79 | active = false, 80 | id, 81 | }: { 82 | link?: string | null; 83 | clickable?: boolean; 84 | size?: number | null; 85 | onClick?: null | (() => void); 86 | color: string; 87 | header: React.ReactNode; 88 | subheader: React.ReactNode | null; 89 | icon: React.ReactNode; 90 | active?: boolean; 91 | id: string; 92 | }) { 93 | const content = ( 94 | 95 | {header} 96 | {active ? current : <>>} 97 | 98 | ); 99 | if (link) { 100 | return {content}; 101 | } 102 | 103 | return content; 104 | } 105 | -------------------------------------------------------------------------------- /interface/components/modals/wallet/PendingView.tsx: -------------------------------------------------------------------------------- 1 | import { AbstractConnector } from "@web3-react/abstract-connector"; 2 | import React from "react"; 3 | import styled from "styled-components"; 4 | import Option from "./Option"; 5 | import { SUPPORTED_WALLETS } from "../../../constants"; 6 | import { injected } from "../../../connectors"; 7 | import { P, H1, PDark } from "../../typography"; 8 | import { PrimaryButton } from "../../button"; 9 | 10 | const StyledP = styled(PDark)<{ error?: boolean }>` 11 | text-transform: uppercase; 12 | font-weight: 900; 13 | font-size: 12px; 14 | letter-spacing: 0.05rem; 15 | color: ${props => (props.error ? "white" : "black")}; 16 | `; 17 | 18 | const LargeText = styled(H1)<{ error?: boolean }>` 19 | font-size: 48px; 20 | font-weight: 900; 21 | letter-spacing: 0.05rem; 22 | color: ${props => (props.error ? "white" : "rgba(0,0,0,0.2)")}; 23 | `; 24 | 25 | const PendingSection = styled.div``; 26 | 27 | const LoadingMessage = styled.div<{ error?: boolean }>` 28 | padding: 1rem; 29 | border: 2px rgba(0, 0, 0, 0.05) solid; 30 | background-color: ${props => (props.error ? "#D36D6D" : "none")}; 31 | `; 32 | 33 | const ErrorGroup = styled.div` 34 | ${({ theme }) => theme.flexRowNoWrap}; 35 | align-items: center; 36 | justify-content: flex-start; 37 | `; 38 | 39 | const ErrorButton = styled.div` 40 | border-radius: 8px; 41 | font-size: 12px; 42 | color: ${({ theme }) => theme.text1}; 43 | background-color: ${({ theme }) => theme.bg4}; 44 | margin-left: 1rem; 45 | padding: 0.5rem; 46 | font-weight: 600; 47 | user-select: none; 48 | `; 49 | 50 | const LoadingWrapper = styled.div` 51 | padding: 0.5rem 0 1rem 0; 52 | `; 53 | 54 | export default function PendingView({ 55 | connector, 56 | error = false, 57 | setPendingError, 58 | tryActivation, 59 | }: { 60 | connector?: AbstractConnector; 61 | error?: boolean; 62 | setPendingError: (error: boolean) => void; 63 | tryActivation: (connector: AbstractConnector) => void; 64 | }) { 65 | const isMetamask = (window?.ethereum as any).isMetaMask; 66 | 67 | return ( 68 | 69 | 70 | {Object.keys(SUPPORTED_WALLETS).map(key => { 71 | const option = SUPPORTED_WALLETS[key]; 72 | if (option.connector === connector) { 73 | if (option.connector === injected) { 74 | if (isMetamask && option.name !== "MetaMask") { 75 | return null; 76 | } 77 | if (!isMetamask && option.name === "MetaMask") { 78 | return null; 79 | } 80 | } 81 | return {option.name}; 82 | } 83 | return null; 84 | })} 85 | 86 | {error ? "(✖╭╮✖)" : "Initializing..."} 87 | 88 | {error && ( 89 | { 91 | setPendingError(false); 92 | connector && tryActivation(connector); 93 | }} 94 | > 95 | Try Again 96 | 97 | )} 98 | 99 | 100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /interface/components/split/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from "react"; 2 | import styled from "styled-components"; 3 | 4 | import { useSplitVault } from "../../hooks/contracts"; 5 | import { useFullTokens } from "../../contexts/tokens"; 6 | import { useAssetAllowance } from "../../contexts/asset-allowances"; 7 | import { useTokenContract } from "../../hooks/contracts"; 8 | import { useFullTokenPrice } from "../../contexts/full-token-prices"; 9 | import { useSplitProtocolAddresses } from "../../contexts/split-addresses"; 10 | import { MAX_INT_256 } from "../../constants"; 11 | 12 | import { 13 | componentTokenAmountToFullTokenAmount, 14 | convertToBaseAmount, 15 | fullTokenAmountToComponentTokenAmount, 16 | } from "../../utils/number"; 17 | 18 | import { TokenInput } from "../input"; 19 | import { H1 } from "../typography"; 20 | import { Dropdown } from "../dropdown"; 21 | import { ConfirmButton, InputContainer } from "../widget"; 22 | import { useTransactionActions } from "../../contexts/transaction"; 23 | import { ApproveTransactionMetadata } from "../../types/app"; 24 | import Decimal from "decimal.js"; 25 | 26 | const SplitContainer = styled.div` 27 | display: flex; 28 | flex-direction: column; 29 | `; 30 | 31 | export interface SplitProps {} 32 | 33 | export const SplitWidget: React.FC = () => { 34 | const { splitVault } = useSplitVault(); 35 | const tokens = useFullTokens(); 36 | const { addTransaction } = useTransactionActions(); 37 | const [selectedTokenIndex, setSelectedTokenIndex] = useState(0); 38 | const [value, setValue] = useState(""); 39 | const selectedToken = tokens[selectedTokenIndex]; 40 | const price = useFullTokenPrice(selectedToken.tokenAddress); 41 | const allowance = useAssetAllowance(selectedToken.tokenAddress); 42 | const tokenContract = useTokenContract(selectedToken.tokenAddress); 43 | const deployment = useSplitProtocolAddresses(); 44 | const baseAmount = convertToBaseAmount(value || "0", selectedToken.decimals); 45 | 46 | const onSplitClick = useCallback(async () => { 47 | if (allowance.lessThan(baseAmount)) { 48 | const tx = await tokenContract.approve(deployment.splitVaultAddress, MAX_INT_256); 49 | addTransaction(tx.hash, { 50 | token: selectedToken, 51 | tokenAmount: new Decimal(MAX_INT_256), 52 | type: "approve", 53 | }); 54 | } 55 | const tx = await splitVault.split(baseAmount.toString(), selectedToken.tokenAddress); 56 | addTransaction(tx.hash, { 57 | fullToken: selectedToken, 58 | fullTokenAmount: baseAmount, 59 | type: "split", 60 | }); 61 | // TODO: clear input on success???? 62 | setValue(""); 63 | }, [value, splitVault, selectedToken, deployment]); 64 | 65 | if (!tokens || !tokens.length || !price) { 66 | return Please connect your wallet.; 67 | } 68 | 69 | const dropdownItems = tokens.map(asset => ({ 70 | id: asset.tokenAddress, 71 | displayName: asset.symbol, 72 | })); 73 | 74 | // The price from the price oracle is scaled by 18 decimal places. 75 | const componentTokenValue = fullTokenAmountToComponentTokenAmount( 76 | baseAmount, 77 | price, 78 | selectedToken.userlyingAssetMetaData.decimals, 79 | ) 80 | .toDecimalPlaces(4) 81 | .toString(); 82 | return ( 83 | 84 | 85 | split 86 | 87 | 88 | to get 89 | {componentTokenValue} 90 | {selectedToken.componentTokens.capitalComponentToken.symbol} 91 | and 92 | {componentTokenValue} 93 | {selectedToken.componentTokens.yieldComponentToken.symbol} 94 | 95 | 96 | Split 97 | 98 | 99 | ); 100 | }; 101 | -------------------------------------------------------------------------------- /interface/components/tables/common.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { P } from "../typography"; 3 | 4 | export const TableContainer = styled.div` 5 | width: 100%; 6 | `; 7 | export const HeaderTR = styled.div` 8 | border-bottom: 1px solid white; 9 | `; 10 | export const TR = styled.div` 11 | display: flex; 12 | align-items: center; 13 | justify-content: space-between; 14 | width: 100%; 15 | `; 16 | export const TH = styled.div``; 17 | export const TBody = styled.div``; 18 | export const THead = styled.div``; 19 | export const TCell = styled.div` 20 | padding: 12px 0; 21 | border-bottom: 1px solid white; 22 | flex-grow: 1; 23 | height: 72px; 24 | display: flex; 25 | align-items: center; 26 | `; 27 | 28 | export const TCellHeader = styled(P)` 29 | font-size: 28px; 30 | `; 31 | 32 | export const TCellLabel = styled(P)` 33 | font-size: 14px; 34 | `; 35 | -------------------------------------------------------------------------------- /interface/components/typography.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const H1 = styled.h1` 4 | color: white; 5 | padding: 0; 6 | margin: 0; 7 | font-weight: normal; 8 | font-size: 40px; 9 | `; 10 | 11 | export const H2 = styled.h2` 12 | color: white; 13 | padding: 0; 14 | font-weight: normal; 15 | margin: 0; 16 | `; 17 | 18 | export const H3 = styled.h3` 19 | color: white; 20 | padding: 0; 21 | margin: 0; 22 | font-weight: normal; 23 | font-size: 28px; 24 | `; 25 | 26 | export const H4 = styled.h4` 27 | color: white; 28 | padding: 0; 29 | font-weight: normal; 30 | margin: 0; 31 | `; 32 | 33 | export const P = styled.p` 34 | font-size: 16px; 35 | color: white; 36 | padding: 0; 37 | margin: 0; 38 | `; 39 | 40 | export const PDark = styled(P)` 41 | color: black; 42 | `; 43 | 44 | export const H3Dark = styled(H3)` 45 | color: black; 46 | `; 47 | 48 | export const Faded = styled.span` 49 | color: rgba(255, 255, 255, 0.5); 50 | `; 51 | 52 | export const FadedDark = styled.span` 53 | color: rgba(0, 0, 0, 0.5); 54 | `; 55 | -------------------------------------------------------------------------------- /interface/components/widget.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | import { PrimaryButton } from "./button"; 4 | 5 | export const ConfirmButton = styled(PrimaryButton)` 6 | cursor: pointer; 7 | margin-top: 50px; 8 | border-radius: 50%; 9 | width: 200px; 10 | height: 200px; 11 | align-self: center; 12 | font-size: 40px; 13 | `; 14 | 15 | export const InputContainer = styled.div` 16 | display: grid; 17 | grid-template-columns: 1fr 3fr 1fr; 18 | align-items: baseline; 19 | gap: 50px 12px; 20 | width: 800px; 21 | `; 22 | -------------------------------------------------------------------------------- /interface/connectors/Fortmatic.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from "../types/ethereum"; 2 | import { FortmaticConnector as FortmaticConnectorCore } from "@web3-react/fortmatic-connector"; 3 | 4 | export const OVERLAY_READY = "OVERLAY_READY"; 5 | 6 | type FormaticSupportedChains = Extract; 7 | 8 | const CHAIN_ID_NETWORK_ARGUMENT: { readonly [chainId in FormaticSupportedChains]: string | undefined } = { 9 | [ChainId.Mainnet]: undefined, 10 | [ChainId.Ropsten]: "ropsten", 11 | [ChainId.Rinkeby]: "rinkeby", 12 | [ChainId.Kovan]: "kovan", 13 | }; 14 | 15 | export class FortmaticConnector extends FortmaticConnectorCore { 16 | async activate() { 17 | if (!this.fortmatic) { 18 | const { default: Fortmatic } = await import("fortmatic"); 19 | 20 | const { apiKey, chainId } = this as any; 21 | if (chainId in CHAIN_ID_NETWORK_ARGUMENT) { 22 | this.fortmatic = new Fortmatic(apiKey, CHAIN_ID_NETWORK_ARGUMENT[chainId as FormaticSupportedChains]); 23 | } else { 24 | throw new Error(`Unsupported network ID: ${chainId}`); 25 | } 26 | } 27 | 28 | const provider = this.fortmatic.getProvider(); 29 | 30 | const pollForOverlayReady = new Promise(resolve => { 31 | const interval = setInterval(() => { 32 | if (provider.overlayReady) { 33 | clearInterval(interval); 34 | this.emit(OVERLAY_READY); 35 | resolve(); 36 | } 37 | }, 200); 38 | }); 39 | 40 | const [account] = await Promise.all([ 41 | provider.enable().then((accounts: string[]) => accounts[0]), 42 | pollForOverlayReady, 43 | ]); 44 | 45 | return { provider: this.fortmatic.getProvider(), chainId: (this as any).chainId, account }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /interface/connectors/fortmatic.d.ts: -------------------------------------------------------------------------------- 1 | declare module "formatic"; 2 | -------------------------------------------------------------------------------- /interface/connectors/index.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import { InjectedConnector } from "@web3-react/injected-connector"; 3 | import { WalletConnectConnector } from "@web3-react/walletconnect-connector"; 4 | import { WalletLinkConnector } from "@web3-react/walletlink-connector"; 5 | import { PortisConnector } from "@web3-react/portis-connector"; 6 | 7 | import { FortmaticConnector } from "./Fortmatic"; 8 | import { NetworkConnector } from "./NetworkConnector"; 9 | 10 | const NETWORK_URL = process.env.NEXT_PUBLIC_NETWORK_URL; 11 | const FORMATIC_KEY = process.env.NEXT_PUBLIC_FORTMATIC_KEY; 12 | const PORTIS_ID = process.env.NEXT_APP_PORTIS_ID; 13 | 14 | export const NETWORK_CHAIN_ID: number = parseInt(process.env.NEXT_PUBLIC_CHAIN_ID ?? "1"); 15 | 16 | if (typeof NETWORK_URL === "undefined") { 17 | throw new Error(`NEXT_PUBLIC_NETWORK_URL must be a defined environment variable`); 18 | } 19 | 20 | export const network = new NetworkConnector({ 21 | urls: { [NETWORK_CHAIN_ID]: NETWORK_URL }, 22 | }); 23 | 24 | type Web3Provider = ethers.providers.Web3Provider; 25 | let networkLibrary: Web3Provider | undefined; 26 | export function getNetworkLibrary(): Web3Provider { 27 | return (networkLibrary = networkLibrary ?? new ethers.providers.Web3Provider(network.provider as any)); 28 | } 29 | 30 | export const injected = new InjectedConnector({ 31 | supportedChainIds: [1, 3, 4, 5, 42], 32 | }); 33 | 34 | // mainnet only 35 | export const walletconnect = new WalletConnectConnector({ 36 | rpc: { 1: NETWORK_URL }, 37 | bridge: "https://bridge.walletconnect.org", 38 | qrcode: true, 39 | pollingInterval: 15000, 40 | }); 41 | 42 | // mainnet only 43 | export const fortmatic = new FortmaticConnector({ 44 | apiKey: FORMATIC_KEY ?? "", 45 | chainId: 1, 46 | }); 47 | 48 | // mainnet only 49 | export const portis = new PortisConnector({ 50 | dAppId: PORTIS_ID ?? "", 51 | networks: [1], 52 | }); 53 | 54 | // mainnet only 55 | export const walletlink = new WalletLinkConnector({ 56 | url: NETWORK_URL, 57 | appName: "Split", 58 | appLogoUrl: 59 | "https://mpng.pngfly.com/20181202/bex/kisspng-emoji-domain-unicorn-pin-badges-sticker-unicorn-tumblr-emoji-unicorn-iphoneemoji-5c046729264a77.5671679315437924251569.jpg", 60 | }); 61 | -------------------------------------------------------------------------------- /interface/constants/index.tsx: -------------------------------------------------------------------------------- 1 | import { AbstractConnector } from "@web3-react/abstract-connector"; 2 | import { MetaMaskIcon } from "../components/icons/metamask"; 3 | import { ArrowRightIcon } from "../components/icons/arrow-right"; 4 | import { WalletConnectIcon } from "../components/icons/wallet-connect"; 5 | import { FortmaticIcon } from "../components/icons/fortmatic"; 6 | 7 | import { fortmatic, injected, walletconnect } from "../connectors"; 8 | import { AppAction } from "../types/app"; 9 | import Decimal from "decimal.js"; 10 | 11 | export const MAX_INT_256 = "115792089237316195423570985008687907853269984665640564039457584007913129639935"; 12 | export const ZERO = new Decimal(0); 13 | 14 | export const PATHS = { 15 | ROOT: "/", 16 | SPLIT: "/split", 17 | }; 18 | 19 | // Only for deployed contracts. 20 | export const CHAIN_ID_NAME = { 21 | 1: "mainnet", 22 | 4: "rinkeby", 23 | }; 24 | 25 | export const APP_PARAM_TO_APP_ACTION = { 26 | split: AppAction.SPLIT, 27 | manage: AppAction.MANAGE, 28 | combine: AppAction.COMBINE, 29 | }; 30 | 31 | export interface WalletInfo { 32 | connector?: AbstractConnector; 33 | name: string; 34 | icon: React.FC; 35 | description: string; 36 | href: string | null; 37 | color: string; 38 | primary?: true; 39 | mobile?: true; 40 | mobileOnly?: true; 41 | } 42 | 43 | export const SUPPORTED_WALLETS: { [key: string]: WalletInfo } = { 44 | INJECTED: { 45 | connector: injected, 46 | name: "Injected", 47 | icon: () => , 48 | description: "Injected web3 provider.", 49 | href: null, 50 | color: "#010101", 51 | primary: true, 52 | }, 53 | METAMASK: { 54 | connector: injected, 55 | name: "MetaMask", 56 | icon: () => , 57 | description: "Easy-to-use browser extension.", 58 | href: null, 59 | color: "#E8831D", 60 | }, 61 | WALLET_CONNECT: { 62 | connector: walletconnect, 63 | name: "WalletConnect", 64 | icon: () => , 65 | description: "Connect to Trust Wallet, Rainbow Wallet and more...", 66 | href: null, 67 | color: "#4196FC", 68 | mobile: true, 69 | }, 70 | // WALLET_LINK: { 71 | // connector: walletlink, 72 | // name: 'Coinbase Wallet', 73 | // iconName: 'coinbaseWalletIcon.svg', 74 | // description: 'Use Coinbase Wallet app on mobile device', 75 | // href: null, 76 | // color: '#315CF5' 77 | // }, 78 | // COINBASE_LINK: { 79 | // name: 'Open in Coinbase Wallet', 80 | // iconName: 'coinbaseWalletIcon.svg', 81 | // description: 'Open in Coinbase Wallet app.', 82 | // href: 'https://go.cb-w.com/mtUDhEZPy1', 83 | // color: '#315CF5', 84 | // mobile: true, 85 | // mobileOnly: true 86 | // }, 87 | FORTMATIC: { 88 | connector: fortmatic, 89 | name: "Fortmatic", 90 | icon: () => , 91 | description: "Login using Fortmatic hosted wallet", 92 | href: null, 93 | color: "#6748FF", 94 | mobile: true, 95 | }, 96 | }; 97 | 98 | export const NETWORK_URL = process.env.NEXT_PUBLIC_NETWORK_URL || ""; 99 | export const CHAIN_ID = parseInt(process.env.NEXT_PUBLIC_CHAIN_ID ?? "1") || 1; 100 | -------------------------------------------------------------------------------- /interface/contexts/asset-allowances.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState, useEffect, useCallback } from "react"; 2 | import { useWeb3React } from "@web3-react/core"; 3 | import { Decimal } from "decimal.js"; 4 | import { useBlockchain } from "./blockchain"; 5 | import { useImmer } from "use-immer"; 6 | import { useTokenContracts } from "../hooks/contracts"; 7 | import { useAllTokens } from "./tokens"; 8 | import { useSplitProtocolAddresses } from "./split-addresses"; 9 | 10 | export interface AssetAllowancesProviderState { 11 | [tokenAddress: string]: Decimal | undefined | null; 12 | } 13 | 14 | export interface AssetAllowancesActionsProviderState { 15 | refreshAllowances: () => void; 16 | } 17 | 18 | const AssetAllowancesContext = React.createContext({}); 19 | 20 | const AssetAllowancesActionContext = React.createContext({ 21 | refreshAllowances: () => new Error("AssetAllowancesAction Provider not set"), 22 | }); 23 | 24 | const AssetAllowancesProvider: React.FC = ({ children }) => { 25 | const { account, chainId, library } = useWeb3React(); 26 | const { blockNum } = useBlockchain(); 27 | const protocolAddresses = useSplitProtocolAddresses(); 28 | const tokens = useAllTokens(); 29 | const tokenAddresses = useMemo(() => tokens.map(t => t.tokenAddress), [tokens]); 30 | const tokenContracts = useTokenContracts(tokenAddresses); 31 | const [assetAllowances, setAssetAllowances] = useImmer({}); 32 | const [refreshCounter, setRefreshCounter] = useState(0); 33 | 34 | useEffect(() => { 35 | if (!account) { 36 | return; 37 | } 38 | for (let tokenContract of tokenContracts) { 39 | tokenContract 40 | .allowance(account, protocolAddresses.splitVaultAddress) 41 | .then(bal => { 42 | setAssetAllowances(draft => { 43 | draft[tokenContract.address] = new Decimal(bal.toString()); 44 | }); 45 | }) 46 | .catch(_ => { 47 | setAssetAllowances(draft => { 48 | draft[tokenContract.address] = null; 49 | }); 50 | }); 51 | } 52 | }, [account, chainId, blockNum, tokenAddresses, refreshCounter]); 53 | 54 | const refreshAllowances = useCallback(() => { 55 | setRefreshCounter(refreshCounter + 1); 56 | }, [setRefreshCounter]); 57 | 58 | return ( 59 | 60 | {children} 61 | 62 | ); 63 | }; 64 | 65 | const useRefreshAllowances = (): (() => void) => { 66 | return React.useContext(AssetAllowancesActionContext).refreshAllowances; 67 | }; 68 | 69 | const useAssetAllowances = (): AssetAllowancesProviderState => { 70 | return React.useContext(AssetAllowancesContext); 71 | }; 72 | 73 | const useAssetAllowance = (tokenAddress: string): Decimal | undefined => { 74 | return React.useContext(AssetAllowancesContext)[tokenAddress]; 75 | }; 76 | 77 | export { AssetAllowancesProvider, useAssetAllowances, useAssetAllowance, useRefreshAllowances }; 78 | -------------------------------------------------------------------------------- /interface/contexts/asset-balances.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState, useEffect, useCallback } from "react"; 2 | import { useWeb3React } from "@web3-react/core"; 3 | import { JsonRpcProvider, Web3Provider } from "@ethersproject/providers"; 4 | import { Decimal } from "decimal.js"; 5 | import { useBlockchain } from "./blockchain"; 6 | import { useImmer } from "use-immer"; 7 | import { useTokenContracts } from "../hooks/contracts"; 8 | import { useAllTokens } from "./tokens"; 9 | 10 | export interface AssetBalancesProviderState { 11 | eth: Decimal | undefined | null; 12 | [tokenAddress: string]: Decimal | undefined | null; 13 | } 14 | 15 | const initialState: AssetBalancesProviderState = { 16 | eth: undefined, 17 | }; 18 | 19 | export interface AssetBalancesActionsProviderState { 20 | refreshBalances: () => void; 21 | } 22 | 23 | const AssetBalancesContext = React.createContext(initialState); 24 | 25 | const AssetBalancesActionContext = React.createContext({ 26 | refreshBalances: () => new Error("AssetBalancesActions Provider not set"), 27 | }); 28 | 29 | const AssetBalancesProvider: React.FC = ({ children }) => { 30 | const { account, chainId, library } = useWeb3React(); 31 | const { blockNum } = useBlockchain(); 32 | const tokens = useAllTokens(); 33 | const tokenAddresses = useMemo(() => tokens.map(t => t.tokenAddress), [tokens]); 34 | const tokenContracts = useTokenContracts(tokenAddresses); 35 | const [assetBalances, setAssetBalances] = useImmer(initialState); 36 | const [refreshCounter, setRefreshCounter] = useState(0); 37 | 38 | useEffect(() => { 39 | if (!account) { 40 | return; 41 | } 42 | (library as Web3Provider) 43 | .getBalance(account) 44 | .then(bal => { 45 | setAssetBalances(draft => ({ 46 | ...draft, 47 | eth: new Decimal(bal.toString()), 48 | })); 49 | }) 50 | .catch(_ => { 51 | setAssetBalances(draft => ({ 52 | ...draft, 53 | eth: null, 54 | })); 55 | }); 56 | for (let tokenContract of tokenContracts) { 57 | tokenContract 58 | .balanceOf(account) 59 | .then(bal => { 60 | setAssetBalances(draft => { 61 | draft[tokenContract.address] = new Decimal(bal.toString()); 62 | }); 63 | }) 64 | .catch(_ => { 65 | setAssetBalances(draft => { 66 | draft[tokenContract.address] = null; 67 | }); 68 | }); 69 | } 70 | }, [account, chainId, blockNum, tokenAddresses, refreshCounter]); 71 | 72 | const refreshBalances = useCallback(() => { 73 | setRefreshCounter(refreshCounter + 1); 74 | }, [setRefreshCounter]); 75 | 76 | return ( 77 | 78 | {children} 79 | 80 | ); 81 | }; 82 | 83 | const useAssetBalances = (): AssetBalancesProviderState => { 84 | return React.useContext(AssetBalancesContext); 85 | }; 86 | 87 | const useAssetBalance = (tokenAddress: string): Decimal => { 88 | return React.useContext(AssetBalancesContext)[tokenAddress] || new Decimal(0); 89 | }; 90 | 91 | const useEthBalance = (): Decimal | undefined => { 92 | return React.useContext(AssetBalancesContext).eth; 93 | }; 94 | 95 | export { AssetBalancesProvider, useAssetBalances, useAssetBalance, useEthBalance }; 96 | -------------------------------------------------------------------------------- /interface/contexts/banner.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState, useEffect, useCallback } from "react"; 2 | import { BannerMetadata, BannerType, TxBannerMetadata } from "../types/app"; 3 | import { useImmer } from "use-immer"; 4 | 5 | export interface BannerActionsProviderState { 6 | addBanner: (banner: BannerMetadata) => void; 7 | updateBanner: (index: number, changes: Partial) => void; 8 | } 9 | 10 | export type BannerProviderState = BannerMetadata[]; 11 | 12 | const BannerContext = React.createContext([]); 13 | const BannerActionsContext = React.createContext({ 14 | addBanner: () => new Error("BannerProvider not set."), 15 | updateBanner: () => new Error("BannerProvider not set."), 16 | }); 17 | 18 | const BannerProvider: React.FC = ({ children }) => { 19 | const [banners, setBanners] = useImmer([]); 20 | 21 | const addBanner = useCallback( 22 | (banner: BannerMetadata) => { 23 | setBanners(draft => { 24 | draft.push(banner); 25 | }); 26 | }, 27 | [banners, setBanners], 28 | ); 29 | 30 | const updateBanner = useCallback( 31 | (index: number, changes: Partial) => { 32 | if (index < 0 || index >= banners.length) { 33 | return; 34 | } 35 | setBanners(draft => { 36 | draft[index] = { 37 | ...draft[index], 38 | ...changes, 39 | }; 40 | }); 41 | }, 42 | [banners, setBanners], 43 | ); 44 | 45 | return ( 46 | 47 | 53 | {children} 54 | 55 | 56 | ); 57 | }; 58 | 59 | const useBanners = () => { 60 | return React.useContext(BannerContext); 61 | }; 62 | 63 | const useTxsBanners = () => { 64 | const banners = useBanners(); 65 | return banners.filter(b => !!(b as TxBannerMetadata).txHash) as TxBannerMetadata[]; 66 | }; 67 | 68 | const useTxBannerMap = () => { 69 | const txBanners = useTxsBanners(); 70 | return useMemo(() => { 71 | return txBanners.reduce((a: { [txHash: string]: TxBannerMetadata }, c: TxBannerMetadata) => { 72 | return { 73 | ...a, 74 | [c.txHash]: c, 75 | }; 76 | }, {} as { [txHash: string]: TxBannerMetadata }); 77 | }, [txBanners]); 78 | }; 79 | 80 | const useTxBanner = (txHash: string) => { 81 | const txBannerMap = useTxBannerMap(); 82 | return txBannerMap[txHash]; 83 | }; 84 | 85 | const useBannerActions = () => { 86 | return React.useContext(BannerActionsContext); 87 | }; 88 | 89 | const useTxBannerActions = () => { 90 | const bannerActions = useBannerActions(); 91 | const txBanners = useTxsBanners(); 92 | const txHashToIndexMap = useMemo(() => { 93 | return txBanners.reduce((a: { [txHash: string]: number }, c: TxBannerMetadata, i: number) => { 94 | return { 95 | ...a, 96 | [c.txHash]: i + 1, // add one to get around falsy value 97 | }; 98 | }, {} as { [txHash: string]: number }); 99 | }, [txBanners]); 100 | return useMemo( 101 | () => ({ 102 | dismissTxBanner: (txHash: string) => { 103 | if (!txHashToIndexMap[txHash]) { 104 | return; 105 | } 106 | bannerActions.updateBanner(txHashToIndexMap[txHash] - 1, { dismissed: true }); 107 | }, 108 | updateTxBanner: (txHash: string, changes: Partial) => { 109 | if (!txHashToIndexMap[txHash]) { 110 | return; 111 | } 112 | bannerActions.updateBanner(txHashToIndexMap[txHash] - 1, changes); 113 | }, 114 | addPendingTxBanner: (txHash: string, description: string) => { 115 | bannerActions.addBanner({ 116 | dismissed: false, 117 | type: "loading", 118 | description: description, 119 | txHash, 120 | } as TxBannerMetadata); 121 | }, 122 | }), 123 | [txHashToIndexMap, bannerActions], 124 | ); 125 | }; 126 | 127 | export { BannerProvider, useBanners, useTxsBanners, useTxBannerMap, useTxBanner, useBannerActions, useTxBannerActions }; 128 | -------------------------------------------------------------------------------- /interface/contexts/blockchain.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState, useEffect } from "react"; 2 | import { useWeb3React } from "@web3-react/core"; 3 | import { JsonRpcProvider, Web3Provider } from "@ethersproject/providers"; 4 | import { NETWORK_URL } from "../constants"; 5 | 6 | export interface BlockchainProviderState { 7 | blockNum: number | undefined | null; 8 | } 9 | 10 | const initialState: BlockchainProviderState = { 11 | blockNum: undefined, 12 | }; 13 | 14 | const BlockchainContext = React.createContext(initialState); 15 | 16 | const BlockchainProvider: React.FC = ({ children }) => { 17 | const { chainId } = useWeb3React(); 18 | 19 | const [blockNum, setblockNum] = useState(); 20 | 21 | useEffect(() => { 22 | if (chainId === undefined) { 23 | return; 24 | } 25 | 26 | const provider = new JsonRpcProvider(NETWORK_URL); 27 | 28 | let stale = false; 29 | 30 | // set initial value 31 | provider.getBlockNumber().then((blockNum: number) => { 32 | if (!stale) { 33 | setblockNum(blockNum); 34 | } 35 | }); 36 | 37 | provider.on("block", (blockNum: number) => { 38 | if (stale) { 39 | } 40 | setblockNum(blockNum); 41 | }); 42 | 43 | // remove listener when the component is unmounted 44 | return () => { 45 | provider.removeAllListeners("block"); 46 | setblockNum(undefined); 47 | stale = true; 48 | }; 49 | }, [chainId]); 50 | 51 | const value = useMemo(() => { 52 | return { 53 | blockNum, 54 | }; 55 | }, [blockNum]); 56 | 57 | return {children}; 58 | }; 59 | 60 | const useBlockchain = (): BlockchainProviderState => { 61 | return React.useContext(BlockchainContext); 62 | }; 63 | 64 | export { BlockchainProvider, useBlockchain }; 65 | -------------------------------------------------------------------------------- /interface/contexts/full-token-prices.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState, useEffect, useCallback } from "react"; 2 | import { useImmer } from "use-immer"; 3 | import { useWeb3React } from "@web3-react/core"; 4 | import { Decimal } from "decimal.js"; 5 | 6 | import { useCTokenPriceOracle } from "../hooks/contracts"; 7 | 8 | import { useBlockchain } from "./blockchain"; 9 | import { useFullTokens } from "./tokens"; 10 | 11 | export interface FullTokenPricesProviderState { 12 | [tokenAddress: string]: Decimal | undefined | null; 13 | } 14 | 15 | export interface FullTokenPricesActionsProviderState { 16 | refreshPrices: () => void; 17 | } 18 | 19 | const FullTokenPricesContext = React.createContext({}); 20 | 21 | const FullTokenPricesActionContext = React.createContext({ 22 | refreshPrices: () => new Error("FullTokenPricesAction Provider not set"), 23 | }); 24 | 25 | const FullTokenPricesProvider: React.FC = ({ children }) => { 26 | const { account, chainId } = useWeb3React(); 27 | const { blockNum } = useBlockchain(); 28 | const tokens = useFullTokens(); 29 | const tokenAddresses = useMemo(() => tokens.map(t => t.tokenAddress), [tokens]); 30 | const [fullTokenPrices, setFullTokenPrices] = useImmer({}); 31 | const [refreshCounter, setRefreshCounter] = useState(0); 32 | const { priceOracle } = useCTokenPriceOracle(); 33 | 34 | useEffect(() => { 35 | for (let tokenAddress of tokenAddresses) { 36 | priceOracle 37 | .getPrice(tokenAddress) 38 | .then(price => { 39 | setFullTokenPrices(draft => { 40 | draft[tokenAddress] = new Decimal(price.toString()); 41 | }); 42 | }) 43 | .catch(e => { 44 | setFullTokenPrices(draft => { 45 | draft[tokenAddress] = null; 46 | }); 47 | }); 48 | } 49 | }, [account, chainId, blockNum, tokenAddresses, priceOracle, refreshCounter]); 50 | 51 | const refreshPrices = useCallback(() => { 52 | setRefreshCounter(refreshCounter + 1); 53 | }, [setRefreshCounter]); 54 | 55 | return ( 56 | 57 | {children} 58 | 59 | ); 60 | }; 61 | 62 | const useRefreshPrices = (): (() => void) => { 63 | return React.useContext(FullTokenPricesActionContext).refreshPrices; 64 | }; 65 | 66 | const useFullTokenPrices = (): FullTokenPricesProviderState => { 67 | return React.useContext(FullTokenPricesContext); 68 | }; 69 | 70 | const useFullTokenPrice = (tokenAddress: string): Decimal | undefined => { 71 | return React.useContext(FullTokenPricesContext)[tokenAddress]; 72 | }; 73 | 74 | export { FullTokenPricesProvider, useFullTokenPrices, useFullTokenPrice, useRefreshPrices }; 75 | -------------------------------------------------------------------------------- /interface/contexts/modal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState, useEffect, useCallback } from "react"; 2 | import { useRouter } from "next/router"; 3 | import { AppModal } from "../types/app"; 4 | import { AppComponent } from "next/dist/next-server/lib/router/router"; 5 | 6 | export interface ModalStatesMap { 7 | [appModal: string]: boolean; 8 | } 9 | 10 | export interface AppModalContext { 11 | modalStates: ModalStatesMap; 12 | setModalState: (modalKey: AppModal, state: boolean) => void; 13 | } 14 | 15 | const InitialModalStates = { 16 | [AppModal.WALLET]: false, 17 | }; 18 | 19 | const AppModalContext = React.createContext({ 20 | modalStates: InitialModalStates, 21 | setModalState: () => new Error("Missing AddModalContext"), 22 | }); 23 | 24 | const AppModalProvider: React.FC = ({ children }) => { 25 | const [modalStates, setModalStates] = useState(InitialModalStates); 26 | 27 | const setModalState = (modalKey: AppModal | undefined, state: boolean) => { 28 | if (!modalKey) { 29 | return; 30 | } 31 | if (modalStates[modalKey] === undefined) { 32 | return; 33 | } 34 | const newModalStates = { 35 | ...modalStates, 36 | [modalKey]: state, 37 | }; 38 | setModalStates(newModalStates); 39 | }; 40 | 41 | return ( 42 | 48 | {children} 49 | 50 | ); 51 | }; 52 | 53 | const useAppModal = () => { 54 | return React.useContext(AppModalContext); 55 | }; 56 | 57 | const useModalState = (modalKey: AppModal) => { 58 | const { modalStates } = useAppModal(); 59 | return modalStates[modalKey]; 60 | }; 61 | 62 | const useModalStateActions = (modalKey: AppModal) => { 63 | const { setModalState } = useAppModal(); 64 | return { 65 | openModal: () => setModalState(modalKey, true), 66 | closeModal: () => setModalState(modalKey, false), 67 | }; 68 | }; 69 | 70 | export { AppModalProvider, useAppModal, useModalState, useModalStateActions }; 71 | -------------------------------------------------------------------------------- /interface/contexts/split-addresses.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState, useEffect } from "react"; 2 | import { useWeb3React } from "@web3-react/core"; 3 | import { JsonRpcProvider, Web3Provider } from "@ethersproject/providers"; 4 | import { deployments, Deployment } from "split-contracts"; 5 | import { CHAIN_ID_NAME } from "../constants"; 6 | 7 | export type SplitProtocolAddressesProviderState = Deployment | undefined; 8 | 9 | const initialState: SplitProtocolAddressesProviderState = undefined; 10 | 11 | const SplitProtocolAddressesContext = React.createContext(initialState); 12 | 13 | // TODO(dave4506) as split tokens become more diverse and dynamically added via governance, this context will need to accomodate for that 14 | const SplitProtocolAddressesProvider: React.FC = ({ children }) => { 15 | const { chainId } = useWeb3React(); 16 | 17 | const value = useMemo(() => { 18 | if (!chainId) { 19 | return deployments.mainnet; 20 | } 21 | return deployments[CHAIN_ID_NAME[chainId]]; 22 | }, [chainId]); 23 | 24 | return {children}; 25 | }; 26 | 27 | const useSplitProtocolAddresses = (): SplitProtocolAddressesProviderState => { 28 | return React.useContext(SplitProtocolAddressesContext); 29 | }; 30 | 31 | export { SplitProtocolAddressesProvider, useSplitProtocolAddresses }; 32 | -------------------------------------------------------------------------------- /interface/contexts/tokens.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react"; 2 | import { useWeb3React } from "@web3-react/core"; 3 | import { Asset, FullAsset, AssetType } from "../types/split"; 4 | import { AVAILABLE_FULL_TOKENS } from "../data/tokens"; 5 | import { ChainId } from "../types/ethereum"; 6 | 7 | export type TokensProviderState = FullAsset[] | undefined; 8 | 9 | const initialState: TokensProviderState = undefined; 10 | 11 | const FullTokensContext = React.createContext(initialState); 12 | 13 | // TODO(dave4506) as split tokens become more diverse and dynamically added via governance, this context will need to accomodate for that 14 | const TokensProvider: React.FC = ({ children }) => { 15 | const { chainId } = useWeb3React(); 16 | 17 | const tokens = useMemo(() => { 18 | if (!chainId) { 19 | // defaults to providing mainnet? TODO 20 | return AVAILABLE_FULL_TOKENS[ChainId.Mainnet]; 21 | } 22 | return AVAILABLE_FULL_TOKENS[chainId]; 23 | }, [chainId]); 24 | return {children}; 25 | }; 26 | 27 | const useFullTokens = (): TokensProviderState => { 28 | return React.useContext(FullTokensContext); 29 | }; 30 | 31 | const useFullTokensByAddress = (): { [address: string]: FullAsset } | undefined => { 32 | const tokens = useFullTokens(); 33 | const tokensMap = tokens.reduce((a, c) => ({ ...a, [c.tokenAddress]: c }), {} as { [address: string]: FullAsset }); 34 | return tokensMap; 35 | }; 36 | 37 | const useAllTokens = (): Asset[] => { 38 | const fullTokens = useFullTokens(); 39 | const allTokensMemo = useMemo(() => { 40 | const allTokens = []; 41 | for (const fullTokenAddress of Object.keys(fullTokens)) { 42 | const fullToken = fullTokens[fullTokenAddress]; 43 | const { capitalComponentToken, yieldComponentToken } = fullToken.componentTokens; 44 | allTokens.push(fullToken, capitalComponentToken, yieldComponentToken); 45 | } 46 | return allTokens; 47 | }, [fullTokens]); 48 | return allTokensMemo; 49 | }; 50 | 51 | const useTokensByAssetType = (assetType: AssetType): Asset[] | undefined => { 52 | const allTokens = useAllTokens(); 53 | const filteredTokens = useMemo(() => { 54 | return allTokens.filter(a => a.type === assetType); 55 | }, [allTokens]); 56 | return filteredTokens; 57 | }; 58 | 59 | const useAllTokensByAddress = (): { [address: string]: Asset } | undefined => { 60 | const tokens = useAllTokens(); 61 | const tokensMap = useMemo(() => { 62 | return tokens.reduce((a, c) => ({ ...a, [c.tokenAddress]: c }), {} as { [address: string]: Asset }); 63 | }, [tokens]); 64 | return tokensMap; 65 | }; 66 | 67 | const useFullToken = (tokenAddress: string): FullAsset => { 68 | const tokensMap = useFullTokensByAddress(); 69 | return tokensMap[tokenAddress]; 70 | }; 71 | 72 | const useToken = (tokenAddress: string): Asset => { 73 | return useAllTokensByAddress()[tokenAddress]; 74 | }; 75 | 76 | export { 77 | TokensProvider, 78 | useAllTokens, 79 | useFullTokens, 80 | useTokensByAssetType, 81 | useFullTokensByAddress, 82 | useFullToken, 83 | useToken, 84 | }; 85 | -------------------------------------------------------------------------------- /interface/contexts/web3-connection.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useWeb3React } from "@web3-react/core"; 3 | 4 | import { useEagerConnect, useInactiveListener } from "../hooks/wallet"; 5 | 6 | export interface Web3ConnectionContext { 7 | triedEagerConnect: boolean; 8 | } 9 | 10 | const Web3ConnectionContext = React.createContext({ 11 | triedEagerConnect: false, 12 | }); 13 | 14 | const Web3ConnectionProvider: React.FC = ({ children }) => { 15 | const { active, error } = useWeb3React(); 16 | 17 | const [triedEagerConnect, setTriedEagerConnect] = useState(false); 18 | 19 | // try to eagerly connect to an injected provider, if it exists and has granted access already 20 | const triedEager = useEagerConnect(); 21 | 22 | useEffect(() => { 23 | if (triedEager) { 24 | setTriedEagerConnect(triedEager); 25 | } 26 | }, [triedEager]); 27 | 28 | // when there's no account connected, react to logins (broadly speaking) on the injected provider, if it exists 29 | useInactiveListener(!triedEager); 30 | 31 | return {children}; 32 | }; 33 | 34 | const useWeb3Connection = () => { 35 | return React.useContext(Web3ConnectionContext); 36 | }; 37 | 38 | export { Web3ConnectionProvider, useWeb3Connection }; 39 | -------------------------------------------------------------------------------- /interface/contexts/yield-balances.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState, useEffect, useCallback } from "react"; 2 | import { useImmer } from "use-immer"; 3 | import { useWeb3React } from "@web3-react/core"; 4 | import { Decimal } from "decimal.js"; 5 | 6 | import { useYieldTokenContracts } from "../hooks/contracts"; 7 | 8 | import { useBlockchain } from "./blockchain"; 9 | import { useTokensByAssetType } from "./tokens"; 10 | 11 | export interface YieldBalancesProviderState { 12 | [tokenAddress: string]: Decimal | undefined | null; 13 | } 14 | 15 | export interface YieldBalancesActionsProviderState { 16 | refreshYieldBalances: () => void; 17 | } 18 | 19 | const YieldBalancesContext = React.createContext({}); 20 | 21 | const YieldBalancesActionContext = React.createContext({ 22 | refreshYieldBalances: () => new Error("YieldBalancesAction Provider not set"), 23 | }); 24 | 25 | const YieldBalancesProvider: React.FC = ({ children }) => { 26 | const { account, chainId } = useWeb3React(); 27 | const { blockNum } = useBlockchain(); 28 | const tokens = useTokensByAssetType("yield-split"); 29 | const tokenAddresses = useMemo(() => tokens.map(t => t.tokenAddress), [tokens]); 30 | const [yieldBalances, setYieldBalances] = useImmer({}); 31 | const [refreshCounter, setRefreshCounter] = useState(0); 32 | const tokenContracts = useYieldTokenContracts(tokenAddresses); 33 | 34 | useEffect(() => { 35 | if (!account) { 36 | return; 37 | } 38 | for (let tokenContract of tokenContracts) { 39 | tokenContract["calculatePayoutAmount(address)"](account) 40 | .then(balance => { 41 | setYieldBalances(draft => { 42 | draft[tokenContract.address] = new Decimal(balance.toString()); 43 | }); 44 | }) 45 | .catch(e => { 46 | setYieldBalances(draft => { 47 | draft[tokenContract.address] = null; 48 | }); 49 | }); 50 | } 51 | }, [account, chainId, blockNum, tokenAddresses, refreshCounter]); 52 | 53 | const refreshYieldBalances = useCallback(() => { 54 | setRefreshCounter(refreshCounter + 1); 55 | }, [setRefreshCounter]); 56 | 57 | return ( 58 | 59 | {children} 60 | 61 | ); 62 | }; 63 | 64 | const useRefreshOutstandingYield = (): (() => void) => { 65 | return React.useContext(YieldBalancesActionContext).refreshYieldBalances; 66 | }; 67 | 68 | const useYieldBalances = (): YieldBalancesProviderState => { 69 | return React.useContext(YieldBalancesContext); 70 | }; 71 | 72 | const useYieldBalance = (tokenAddress: string): Decimal | undefined => { 73 | return React.useContext(YieldBalancesContext)[tokenAddress]; 74 | }; 75 | 76 | export { 77 | YieldBalancesProvider, 78 | useYieldBalances, 79 | useYieldBalance, 80 | useRefreshOutstandingYield as useRefreshYieldBalances, 81 | }; 82 | -------------------------------------------------------------------------------- /interface/data/tokens.ts: -------------------------------------------------------------------------------- 1 | import { deployments } from "split-contracts"; 2 | 3 | import { ChainId } from "../types/ethereum"; 4 | import { Asset, FullAsset } from "../types/split"; 5 | import { CHAIN_ID_NAME } from "../constants"; 6 | 7 | // NOTE: THIS IS NOT THE WETH TOKEN, it is a metadata object for native eth 8 | export const ETH_TOKEN: Asset = { 9 | tokenAddress: "0x0000000000000000000000000000000000000000", 10 | name: "Ether", 11 | symbol: "ETH", 12 | decimals: 18, 13 | type: "full", 14 | }; 15 | 16 | const assetToFullAsset = (asset: Asset, chainId: ChainId): FullAsset => { 17 | const deployment = deployments[CHAIN_ID_NAME[chainId]]; 18 | if (!deployment) { 19 | throw new Error(`Could not find a deployment for chainId: ${chainId}`); 20 | } 21 | const componentSet = deployment.componentSets[asset.tokenAddress]; 22 | if (!componentSet) { 23 | throw new Error(`Could not find a component set for tokenAddress: ${asset.tokenAddress}`); 24 | } 25 | return { 26 | ...asset, 27 | type: "full", 28 | componentTokens: { 29 | capitalComponentToken: { 30 | tokenAddress: componentSet.capitalComponentTokenAddress, 31 | decimals: 18, 32 | symbol: `c${asset.symbol}`, 33 | name: `Capital ${asset.name}`, 34 | type: "capital-split", 35 | fullTokenAddress: asset.tokenAddress, 36 | userlyingAssetMetaData: asset, 37 | }, 38 | yieldComponentToken: { 39 | tokenAddress: componentSet.yieldComponentTokenAddress, 40 | decimals: 18, 41 | symbol: `y${asset.symbol}`, 42 | name: `Yield ${asset.name}`, 43 | type: "yield-split", 44 | fullTokenAddress: asset.tokenAddress, 45 | userlyingAssetMetaData: asset, 46 | }, 47 | }, 48 | }; 49 | }; 50 | 51 | export const AVAILABLE_FULL_TOKENS: { [chainId: number]: FullAsset[] } = { 52 | 1: [ 53 | assetToFullAsset( 54 | { 55 | tokenAddress: "0x4ddc2d193948926d02f9b1fe9e1daa0718270ed5", 56 | name: "Compound Ether", 57 | symbol: "cETH", 58 | decimals: 8, 59 | type: "full", 60 | userlyingAssetMetaData: { 61 | symbol: "ETH", 62 | name: "Ether", 63 | decimals: 18, 64 | }, 65 | }, 66 | 1, 67 | ), 68 | assetToFullAsset( 69 | { 70 | tokenAddress: "0x6c8c6b02e7b2be14d4fa6022dfd6d75921d90e4e", 71 | name: "Compound Basic Attention Token", 72 | symbol: "cBAT", 73 | decimals: 8, 74 | type: "full", 75 | userlyingAssetMetaData: { 76 | symbol: "BAT", 77 | name: "Basic Attention Token", 78 | decimals: 18, 79 | }, 80 | }, 81 | 1, 82 | ), 83 | assetToFullAsset( 84 | { 85 | tokenAddress: "0x35a18000230da775cac24873d00ff85bccded550", 86 | name: "Compound Uniswap", 87 | symbol: "cUNI", 88 | decimals: 8, 89 | type: "full", 90 | userlyingAssetMetaData: { 91 | symbol: "UNI", 92 | name: "Uniswap", 93 | decimals: 18, 94 | }, 95 | }, 96 | 1, 97 | ), 98 | ], 99 | 4: [ 100 | assetToFullAsset( 101 | { 102 | tokenAddress: "0xebf1a11532b93a529b5bc942b4baa98647913002", 103 | name: "Compound Basic Attention Token", 104 | symbol: "cBAT", 105 | decimals: 8, 106 | type: "full", 107 | userlyingAssetMetaData: { 108 | symbol: "BAT", 109 | name: "Basic Attention Token", 110 | decimals: 18, 111 | }, 112 | }, 113 | 4, 114 | ), 115 | assetToFullAsset( 116 | { 117 | tokenAddress: "0x52201ff1720134bbbbb2f6bc97bf3715490ec19b", 118 | name: "Compound ZRX", 119 | symbol: "cZRX", 120 | decimals: 8, 121 | type: "full", 122 | userlyingAssetMetaData: { 123 | symbol: "ZRX", 124 | name: "0x Protocol Token", 125 | decimals: 18, 126 | }, 127 | }, 128 | 4, 129 | ), 130 | assetToFullAsset( 131 | { 132 | tokenAddress: "0xd6801a1dffcd0a410336ef88def4320d6df1883e", 133 | name: "Compound Ether", 134 | symbol: "cETH", 135 | decimals: 8, 136 | type: "full", 137 | userlyingAssetMetaData: { 138 | symbol: "ETH", 139 | name: "Ether", 140 | decimals: 18, 141 | }, 142 | }, 143 | 4, 144 | ), 145 | assetToFullAsset( 146 | { 147 | tokenAddress: "0x5b281a6dda0b271e91ae35de655ad301c976edb1", 148 | name: "Compound USDC", 149 | symbol: "cUSDC", 150 | decimals: 8, 151 | type: "full", 152 | userlyingAssetMetaData: { 153 | symbol: "USDC", 154 | name: "USD Coin", 155 | decimals: 6, 156 | }, 157 | }, 158 | 4, 159 | ), 160 | ], 161 | }; 162 | -------------------------------------------------------------------------------- /interface/hooks/contracts.ts: -------------------------------------------------------------------------------- 1 | import { JsonRpcSigner, Web3Provider } from "@ethersproject/providers"; 2 | import { useWeb3React } from "@web3-react/core"; 3 | import { useMemo } from "react"; 4 | import { 5 | CTokenPriceOracle__factory, 6 | ERC20__factory, 7 | SplitVault__factory, 8 | YieldComponentToken__factory, 9 | } from "split-contracts"; 10 | import { useSplitProtocolAddresses } from "../contexts/split-addresses"; 11 | 12 | // account is not optional 13 | export function getSigner(library: Web3Provider, account: string): JsonRpcSigner { 14 | return library.getSigner(account).connectUnchecked(); 15 | } 16 | 17 | // account is optional 18 | export function getProviderOrSigner(library: Web3Provider, account?: string): Web3Provider | JsonRpcSigner { 19 | return account ? getSigner(library, account) : library; 20 | } 21 | 22 | export const useTokenContract = (tokenAddress: string) => { 23 | const { library, account } = useWeb3React(); 24 | return useMemo(() => { 25 | return ERC20__factory.connect(tokenAddress, getProviderOrSigner(library, account)); 26 | }, [library, account, tokenAddress]); 27 | }; 28 | 29 | export const useTokenContracts = (tokenAddresses: string[]) => { 30 | const { library, account } = useWeb3React(); 31 | return useMemo(() => { 32 | return tokenAddresses.map(ta => ERC20__factory.connect(ta, getProviderOrSigner(library, account))); 33 | }, [library, account, tokenAddresses]); 34 | }; 35 | 36 | export const useSplitVault = () => { 37 | const { library, account, active, error } = useWeb3React(); 38 | const { splitVaultAddress } = useSplitProtocolAddresses(); 39 | const splitVault = useMemo( 40 | () => SplitVault__factory.connect(splitVaultAddress, getProviderOrSigner(library, account)), 41 | [library, account, splitVaultAddress], 42 | ); 43 | return { splitVault, active, error }; 44 | }; 45 | 46 | export const useCTokenPriceOracle = () => { 47 | const { library, account, active, error } = useWeb3React(); 48 | const { priceOracleAddress } = useSplitProtocolAddresses(); 49 | const priceOracle = useMemo( 50 | () => CTokenPriceOracle__factory.connect(priceOracleAddress, getProviderOrSigner(library, account)), 51 | [library, account, priceOracleAddress], 52 | ); 53 | return { priceOracle, active, error }; 54 | }; 55 | 56 | export const useYieldTokenContracts = (tokenAddresses: string[]) => { 57 | const { library, account } = useWeb3React(); 58 | return useMemo(() => { 59 | return tokenAddresses.map(ta => YieldComponentToken__factory.connect(ta, getProviderOrSigner(library, account))); 60 | }, [library, account, tokenAddresses]); 61 | }; 62 | -------------------------------------------------------------------------------- /interface/hooks/useEthToken.ts: -------------------------------------------------------------------------------- 1 | import { ETH_TOKEN } from "../data/tokens"; 2 | 3 | export const useEthToken = () => { 4 | return ETH_TOKEN; 5 | }; 6 | -------------------------------------------------------------------------------- /interface/hooks/useMounted.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | function useMounted() { 4 | const [mounted, setMounted] = useState(false); 5 | useEffect(() => setMounted(true), []); 6 | return mounted; 7 | } 8 | 9 | export { useMounted }; 10 | -------------------------------------------------------------------------------- /interface/hooks/useOnClickOutside.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useRef } from "react"; 2 | 3 | export const useIsOpenUntilOutside = (): [ 4 | boolean, 5 | React.Dispatch>, 6 | React.MutableRefObject, 7 | ] => { 8 | const [isOpen, setIsOpen] = useState(false); 9 | const [node] = useOnClickOutside(isOpen, () => setIsOpen(false)); 10 | return [isOpen, setIsOpen, node]; 11 | }; 12 | 13 | export const useOnClickOutside = (isOpen: boolean, onClickOutside: () => void): [React.MutableRefObject] => { 14 | const node = useRef(); 15 | useEffect(() => { 16 | const handleClickOutside = (e: MouseEvent) => { 17 | if (node && (node as any).current.contains(e.target)) { 18 | // inside click 19 | return; 20 | } 21 | // outside click 22 | onClickOutside(); 23 | }; 24 | if (isOpen) { 25 | document.addEventListener("mousedown", handleClickOutside); 26 | } else { 27 | document.removeEventListener("mousedown", handleClickOutside); 28 | } 29 | 30 | return () => { 31 | document.removeEventListener("mousedown", handleClickOutside); 32 | }; 33 | }, [isOpen, onClickOutside]); 34 | 35 | return [node]; 36 | }; 37 | -------------------------------------------------------------------------------- /interface/hooks/wallet.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import { ChainId } from "../types/ethereum"; 3 | import { useWeb3React } from "@web3-react/core"; 4 | import { Web3ReactContextInterface } from "@web3-react/core/dist/types"; 5 | import { useEffect, useState } from "react"; 6 | import { isMobile } from "react-device-detect"; 7 | import { injected } from "../connectors"; 8 | import { useMountedState } from "react-use"; 9 | import { useMounted } from "./useMounted"; 10 | 11 | export const NetworkContextName = "NETWORK"; 12 | 13 | // type Web3Provider = ethers.providers.Web3Provider; 14 | // export function useActiveWeb3React(): Web3ReactContextInterface & { chainId?: ChainId } { 15 | // const context = useWeb3ReactCore(); 16 | // const contextNetwork = useWeb3ReactCore(); 17 | // return context.active ? context : contextNetwork; 18 | // } 19 | 20 | export function useEagerConnect() { 21 | const { activate, active } = useWeb3React(); // specifically using useWeb3ReactCore because of what this hook does 22 | const [tried, setTried] = useState(false); 23 | const isMounted = useMounted(); 24 | 25 | useEffect(() => { 26 | if (!isMounted) { 27 | return; 28 | } 29 | 30 | const attemptActivate = async () => { 31 | const isAuthorized = await injected.isAuthorized(); 32 | if (isAuthorized) { 33 | await activate(injected, undefined, true); 34 | setTried(true); 35 | } else { 36 | if (isMobile && window.ethereum) { 37 | activate(injected, undefined, true).catch(() => { 38 | setTried(true); 39 | }); 40 | } else { 41 | setTried(true); 42 | } 43 | } 44 | }; 45 | attemptActivate(); 46 | }, [activate, isMounted, tried]); // intentionally only running on mount (make sure it's only mounted once :)) 47 | 48 | // if the connection worked, wait until we get confirmation of that to flip the flag 49 | useEffect(() => { 50 | if (!tried && active) { 51 | setTried(true); 52 | } 53 | }, [tried, active]); 54 | 55 | return tried; 56 | } 57 | 58 | /** 59 | * Use for network and injected - logs user in 60 | * and out after checking what network theyre on 61 | */ 62 | export function useInactiveListener(suppress = false) { 63 | const { active, error, activate } = useWeb3React(); // specifically using useWeb3React because of what this hook does 64 | 65 | useEffect(() => { 66 | const ethereum = window.ethereum; 67 | 68 | if (ethereum && ethereum.on && !active && !error && !suppress) { 69 | const handleChainChanged = () => { 70 | // eat errors 71 | activate(injected, undefined, true).catch(error => { 72 | console.error("Failed to activate after chain changed", error); 73 | }); 74 | }; 75 | 76 | const handleAccountsChanged = (accounts: string[]) => { 77 | if (accounts.length > 0) { 78 | // eat errors 79 | activate(injected, undefined, true).catch(error => { 80 | console.error("Failed to activate after accounts changed", error); 81 | }); 82 | } 83 | }; 84 | 85 | ethereum.on("chainChanged", handleChainChanged); 86 | ethereum.on("accountsChanged", handleAccountsChanged); 87 | 88 | return () => { 89 | if (ethereum.removeListener) { 90 | ethereum.removeListener("chainChanged", handleChainChanged); 91 | ethereum.removeListener("accountsChanged", handleAccountsChanged); 92 | } 93 | }; 94 | } 95 | return undefined; 96 | }, [active, error, suppress, activate]); 97 | } 98 | -------------------------------------------------------------------------------- /interface/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /interface/next.config.js: -------------------------------------------------------------------------------- 1 | const withTM = require("next-transpile-modules")(["split-contracts"]); 2 | 3 | module.exports = Object.assign({}, withTM(), { target: "serverless" }); 4 | -------------------------------------------------------------------------------- /interface/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "split-interface", 3 | "version": "1.0.0", 4 | "description": "UI interface to interact with Split Protocol", 5 | "scripts": { 6 | "dev": "next dev --port 4000", 7 | "build": "next build", 8 | "start": "next start", 9 | "test": "echo 'No tests for interface.'" 10 | }, 11 | "author": "me@bydavidsun.com", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@reach/dialog": "^0.11.2", 15 | "@web3-react/core": "^6.1.1", 16 | "@web3-react/fortmatic-connector": "^6.1.6", 17 | "@web3-react/injected-connector": "^6.0.7", 18 | "@web3-react/portis-connector": "^6.1.6", 19 | "@web3-react/walletconnect-connector": "^6.1.6", 20 | "@web3-react/walletlink-connector": "^6.1.6", 21 | "decimal.js": "^10.2.1", 22 | "ethers": "^5.0.17", 23 | "immer": "^7.0.9", 24 | "lodash": "^4.17.20", 25 | "lottie-react": "^2.1.0", 26 | "next": "^10.0.2", 27 | "polished": "^4.0.3", 28 | "react": "^16.14.0", 29 | "react-device-detect": "^1.14.0", 30 | "react-dom": "^16.14.0", 31 | "react-feather": "^2.0.8", 32 | "react-spring": "^8.0.27", 33 | "react-table": "^7.6.0", 34 | "react-use": "^15.3.4", 35 | "react-use-gesture": "^7.0.16", 36 | "styled-components": "^5.2.0", 37 | "use-immer": "^0.4.1" 38 | }, 39 | "devDependencies": { 40 | "@types/lodash": "^4.14.162", 41 | "@types/node": "^14.11.8", 42 | "@types/react": "^16.9.52", 43 | "@types/styled-components": "^5.1.4", 44 | "next-transpile-modules": "^4.1.0", 45 | "typescript": "^4.1.2" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /interface/pages/[...actionParams].tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useMemo, useState } from "react"; 2 | import Head from "next/head"; 3 | import styled from "styled-components"; 4 | import findKey from "lodash/findKey"; 5 | import { useRouter } from "next/router"; 6 | import { Footer } from "../components/footer"; 7 | import { SplitWidget } from "../components/split"; 8 | import { Header } from "../components/header"; 9 | import { APP_PARAM_TO_APP_ACTION, PATHS } from "../constants"; 10 | import { HEADER_HEIGHT } from "../components/header/common"; 11 | import { FOOTER_HEIGHT } from "../components/footer/common"; 12 | import { AppAction } from "../types/app"; 13 | import { ManageWidget } from "../components/manage"; 14 | import { CombineWidget } from "../components/combine"; 15 | 16 | const MARGIN_TOP = 120; 17 | 18 | const LayoutContainer = styled.main` 19 | max-width: 1024px; 20 | margin: 0 auto; 21 | height: calc(100vh - ${HEADER_HEIGHT + FOOTER_HEIGHT + MARGIN_TOP}px); 22 | display: flex; 23 | align-items: start; 24 | justify-content: center; 25 | margin-top: ${MARGIN_TOP}px; 26 | `; 27 | 28 | const AppActionsPage: React.FC = () => { 29 | const router = useRouter(); 30 | 31 | const { query } = router; 32 | 33 | const routerProvidedParams = Array.isArray(query.actionParams) ? query.actionParams : [null]; 34 | 35 | const [appActionFromParams] = routerProvidedParams as [string | null | undefined]; 36 | 37 | const [currentAppAction, setAppAction] = useState("split" as AppAction); 38 | 39 | const setAppActionAndShallowPush = useCallback( 40 | (appAction: AppAction) => { 41 | setAppAction(appAction); 42 | const appActionParam = findKey(APP_PARAM_TO_APP_ACTION, a => a === appAction); 43 | router.push(appActionParam, undefined, { shallow: true }); 44 | }, 45 | [router, setAppAction], 46 | ); 47 | 48 | // TODO(dave4506) lift this logic into a proper routing logic with next.js + the constants 49 | useEffect(() => { 50 | if (!appActionFromParams) { 51 | return; 52 | } 53 | 54 | if (!!APP_PARAM_TO_APP_ACTION[appActionFromParams]) { 55 | setAppAction(APP_PARAM_TO_APP_ACTION[appActionFromParams]); 56 | } else { 57 | setAppActionAndShallowPush(AppAction.SPLIT); 58 | } 59 | }, [router, appActionFromParams]); 60 | 61 | const content = useMemo(() => { 62 | if (currentAppAction === AppAction.SPLIT) { 63 | return ; 64 | } 65 | if (currentAppAction === AppAction.MANAGE) { 66 | return ; 67 | } 68 | if (currentAppAction === AppAction.COMBINE) { 69 | return ; 70 | } 71 | return null; 72 | }, [currentAppAction]); 73 | 74 | return ( 75 | <> 76 | 77 | Split – {currentAppAction.toLowerCase()} 78 | 79 | 80 | {content} 81 | 82 | > 83 | ); 84 | }; 85 | 86 | export default React.memo(AppActionsPage); 87 | -------------------------------------------------------------------------------- /interface/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import App from "next/app"; 3 | import styled from "styled-components"; 4 | import { Web3ReactProvider } from "@web3-react/core"; 5 | import { ethers } from "ethers"; 6 | import { ThemedGlobalStyle } from "../theme"; 7 | import { AppModalProvider } from "../contexts/modal"; 8 | import { Web3ConnectionProvider } from "../contexts/web3-connection"; 9 | import { Modals } from "../components/modals"; 10 | import { BlockchainProvider } from "../contexts/blockchain"; 11 | import { AssetBalancesProvider } from "../contexts/asset-balances"; 12 | import { FullTokenPricesProvider } from "../contexts/full-token-prices"; 13 | import { SplitProtocolAddressesProvider } from "../contexts/split-addresses"; 14 | import { TokensProvider } from "../contexts/tokens"; 15 | import { AssetAllowancesProvider } from "../contexts/asset-allowances"; 16 | import { TransactionProvider } from "../contexts/transaction"; 17 | import { BannerProvider } from "../contexts/banner"; 18 | import { YieldBalancesProvider } from "../contexts/yield-balances"; 19 | import { Banners } from "../components/banner"; 20 | 21 | const AppWrapper = styled.div``; 22 | 23 | const getLibrary = (provider: any) => { 24 | const library = new ethers.providers.Web3Provider(provider); 25 | library.pollingInterval = 15000; 26 | return library; 27 | }; 28 | 29 | export default class SplitApp extends App { 30 | render() { 31 | const { Component, pageProps } = this.props; 32 | 33 | const { err } = this.props as any; 34 | const modifiedPageProps = { ...pageProps, err }; 35 | return ( 36 | <> 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | > 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /interface/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Document, { DocumentContext, Html, Head, Main, NextScript } from "next/document"; 3 | import { ServerStyleSheet } from "styled-components"; 4 | 5 | // Document is only rendered in the server 6 | export default class SplitDocument extends Document { 7 | static async getInitialProps(ctx: DocumentContext) { 8 | const sheet = new ServerStyleSheet(); 9 | const originalRenderPage = ctx.renderPage; 10 | 11 | try { 12 | ctx.renderPage = () => 13 | originalRenderPage({ 14 | enhanceApp: App => props => sheet.collectStyles(), 15 | }); 16 | 17 | const initialProps = await Document.getInitialProps(ctx); 18 | return { 19 | ...initialProps, 20 | styles: ( 21 | <> 22 | {initialProps.styles} 23 | {sheet.getStyleElement()} 24 | > 25 | ), 26 | }; 27 | } finally { 28 | sheet.seal(); 29 | } 30 | } 31 | 32 | render() { 33 | return ( 34 | 35 | 36 | 37 | 38 | 39 | 43 | 44 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /interface/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import Head from "next/head"; 3 | import { useLottie } from "lottie-react"; 4 | import styled from "styled-components"; 5 | import { useRouter } from "next/router"; 6 | 7 | import { CapitalToken, GovernanceToken, YieldToken } from "../components/icons/tokens"; 8 | import { H1, H3 } from "../components/typography"; 9 | import { Footer } from "../components/footer"; 10 | import { HeroHeader } from "../components/header/hero"; 11 | import { PrimaryButton } from "../components/button"; 12 | import { PATHS } from "../constants"; 13 | import splitMergeAnimation from "../data/split_merge.json"; 14 | 15 | const HeroContainer = styled.section` 16 | display: flex; 17 | flex-direction: column; 18 | `; 19 | 20 | const HeroH1 = styled(H1)` 21 | text-align: left; 22 | max-width: 600px; 23 | margin-bottom: 50px; 24 | `; 25 | 26 | const CTAButton = styled(PrimaryButton)` 27 | max-width: 250px; 28 | `; 29 | 30 | const AnimationContainer = styled.div` 31 | width: 600px; 32 | @media (max-width: 768px) { 33 | display: none; 34 | } 35 | `; 36 | 37 | const Layout = styled.main` 38 | max-width: 1400px; 39 | margin: 0 auto; 40 | `; 41 | 42 | const Section = styled.section<{ isReversed?: boolean }>` 43 | padding: 80px 40px; 44 | display: flex; 45 | flex-direction: row; 46 | flex-wrap: ${props => (props.isReversed ? "wrap-reverse" : "wrap")}; 47 | justify-content: space-around; 48 | align-items: center; 49 | `; 50 | 51 | const SectionH3 = styled(H3)` 52 | max-width: 600px; 53 | `; 54 | 55 | let speed = -1; 56 | const IndexPage: React.FC = () => { 57 | const router = useRouter(); 58 | const { View, setSpeed, play } = useLottie({ 59 | animationData: splitMergeAnimation, 60 | autoplay: false, 61 | loop: false, 62 | onComplete: () => { 63 | speed = speed * -1; 64 | setSpeed(speed); 65 | play(); 66 | }, 67 | }); 68 | useEffect(() => { 69 | play(); 70 | }, []); 71 | const onGoToAppClick = () => { 72 | router.push(PATHS.SPLIT); 73 | }; 74 | return ( 75 | <> 76 | 77 | Split 78 | 79 | 80 | 81 | 82 | 83 | 84 | Split Protocol facilitates the disaggregation of existing ERC20 tokens into various components 85 | representing different, valuable properties of the asset. 86 | 87 | Go to app 88 | 89 | {View} 90 | 91 | 92 | 93 | 94 | yieldXYZ can be minted from any income-generating token – from cDAI to YFI to UNI LP tokens 95 | – with the holder able to redeem accumulated income or receive it automatically if transferred 96 | 97 | 98 | 99 | 100 | governanceXYZ can be minted from any token with attached governance rights—such as COMP, 101 | KNC or YFI—providing the holder with full voting rights for potentially only a fraction of the full token 102 | price 103 | 104 | 105 | 106 | 107 | 108 | 109 | capitalXYZ is minted from every Split Protocol deconstruction providing growth-focused 110 | exposure 111 | 112 | 113 | 114 | 115 | > 116 | ); 117 | }; 118 | 119 | export default React.memo(IndexPage); 120 | -------------------------------------------------------------------------------- /interface/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/split-fi/split/39b13ef038fbc1cdf03bce7fc646fd6c0d02ce40/interface/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /interface/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/split-fi/split/39b13ef038fbc1cdf03bce7fc646fd6c0d02ce40/interface/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /interface/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/split-fi/split/39b13ef038fbc1cdf03bce7fc646fd6c0d02ce40/interface/public/apple-touch-icon.png -------------------------------------------------------------------------------- /interface/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/split-fi/split/39b13ef038fbc1cdf03bce7fc646fd6c0d02ce40/interface/public/favicon-16x16.png -------------------------------------------------------------------------------- /interface/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/split-fi/split/39b13ef038fbc1cdf03bce7fc646fd6c0d02ce40/interface/public/favicon-32x32.png -------------------------------------------------------------------------------- /interface/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/split-fi/split/39b13ef038fbc1cdf03bce7fc646fd6c0d02ce40/interface/public/favicon.ico -------------------------------------------------------------------------------- /interface/theme/index.tsx: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from "styled-components"; 2 | 3 | // TODO(dave4506) at some point lift styles into theme object 4 | export const LIGHT_THEME = {}; 5 | 6 | export const colors = { 7 | red: "#D36D6D", 8 | }; 9 | 10 | export const ThemedGlobalStyle = createGlobalStyle` 11 | body, html, * { 12 | box-sizing: border-box; 13 | font-family: 'Roboto', Arial; 14 | } 15 | 16 | html { 17 | color: #FFFFFF; 18 | background-color: #0E2991; 19 | } 20 | body { 21 | min-height: 100vh; 22 | margin: 0; 23 | } 24 | input[type=number]::-webkit-inner-spin-button, 25 | input[type=number]::-webkit-outer-spin-button { 26 | -webkit-appearance: none; 27 | margin: 0; 28 | } 29 | `; 30 | -------------------------------------------------------------------------------- /interface/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": false, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "types": ["react-spring"] 18 | }, 19 | "include": ["next-env.d.ts"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /interface/types/app.ts: -------------------------------------------------------------------------------- 1 | import { TransactionReceipt } from "@ethersproject/providers"; 2 | import Decimal from "decimal.js"; 3 | import { Component } from "react"; 4 | import { Asset, FullAsset } from "./split"; 5 | 6 | /** 7 | * Application state related types 8 | */ 9 | 10 | export enum AppModal { 11 | WALLET = "WALLET", 12 | } 13 | 14 | export enum AppAction { 15 | SPLIT = "SPLIT", 16 | COMBINE = "COMBINE", 17 | MANAGE = "MANAGE", 18 | } 19 | 20 | export type BannerType = "loading" | "success" | "error" | "default"; 21 | 22 | export interface BannerMetadata { 23 | description: string; 24 | type: BannerType; 25 | dismissed: boolean; 26 | } 27 | 28 | export interface TxBannerMetadata extends BannerMetadata { 29 | txHash: string; 30 | } 31 | 32 | export interface ApproveTransactionMetadata { 33 | tokenAmount: Decimal; 34 | token: Asset; 35 | type: "approve"; 36 | } 37 | 38 | export interface SplitTransactionMetadata { 39 | fullToken: FullAsset; 40 | fullTokenAmount: Decimal; 41 | type: "split"; 42 | } 43 | 44 | export interface CombineTransactionMetadata { 45 | fullToken: FullAsset; 46 | componentTokenAmount: Decimal; 47 | type: "combine"; 48 | } 49 | 50 | export interface WithdrawTransactionMetadata { 51 | withdrawTokenAmount: Decimal; 52 | withdrawToken: Asset; 53 | type: "withdraw"; 54 | } 55 | 56 | export type TransactionMetadata = 57 | | ApproveTransactionMetadata 58 | | SplitTransactionMetadata 59 | | WithdrawTransactionMetadata 60 | | CombineTransactionMetadata; 61 | 62 | export type TransactionStatus = "in-progress" | "success" | "failed"; 63 | 64 | export interface TransactionObject { 65 | chainId: number; 66 | txHash: string; 67 | status: TransactionStatus; 68 | metadata?: TransactionMetadata; 69 | receipt?: TransactionReceipt; 70 | lastBlockNumChecked?: number; 71 | } 72 | -------------------------------------------------------------------------------- /interface/types/ethereum.ts: -------------------------------------------------------------------------------- 1 | export enum ChainId { 2 | Mainnet = 1, 3 | Ropsten = 3, 4 | Rinkeby = 4, 5 | Goerli = 5, 6 | Kovan = 42, 7 | } 8 | -------------------------------------------------------------------------------- /interface/types/split.ts: -------------------------------------------------------------------------------- 1 | export type AssetType = "full" | "yield-split" | "capital-split" | "governance-split"; 2 | 3 | export interface AssetMetaData { 4 | symbol: string; 5 | name: string; 6 | decimals: number; 7 | } 8 | 9 | export interface Asset extends AssetMetaData { 10 | tokenAddress: string; 11 | type: AssetType; 12 | userlyingAssetMetaData: AssetMetaData; 13 | } 14 | 15 | export interface ComponentTokens { 16 | yieldComponentToken: ComponentToken; 17 | capitalComponentToken: ComponentToken; 18 | } 19 | 20 | export interface FullAsset extends Asset { 21 | type: Exclude; 22 | componentTokens: ComponentTokens; 23 | } 24 | 25 | export interface ComponentToken extends Asset { 26 | fullTokenAddress: string; 27 | type: Exclude; 28 | } 29 | -------------------------------------------------------------------------------- /interface/typings.d.ts: -------------------------------------------------------------------------------- 1 | // Adds ethereum to the window typings 2 | import { Web3Provider } from "@ethersproject/providers"; 3 | declare global { 4 | // Merges default window declarations with custom ones below 5 | interface Window { 6 | ethereum: Web3Provider; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /interface/utils/address.ts: -------------------------------------------------------------------------------- 1 | import { getAddress } from "@ethersproject/address"; 2 | 3 | // returns the checksummed address if the address is valid, otherwise returns false 4 | export function isAddress(value: any): string | false { 5 | try { 6 | return getAddress(value); 7 | } catch { 8 | return false; 9 | } 10 | } 11 | 12 | // shorten the checksummed version of the input address to have 0x + 4 characters at start and end 13 | export function shortenAddress(address: string, chars = 4): string { 14 | const parsed = isAddress(address); 15 | if (!parsed) { 16 | throw Error(`Invalid 'address' parameter '${address}'.`); 17 | } 18 | return `${parsed.substring(0, chars + 2)}...${parsed.substring(42 - chars)}`; 19 | } 20 | -------------------------------------------------------------------------------- /interface/utils/etherscan.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from "../types/ethereum"; 2 | 3 | const ETHERSCAN_PREFIXES: { [chainId in ChainId]: string } = { 4 | 1: "", 5 | 3: "ropsten.", 6 | 4: "rinkeby.", 7 | 5: "goerli.", 8 | 42: "kovan.", 9 | }; 10 | 11 | export function getEtherscanLink( 12 | chainId: ChainId, 13 | data: string, 14 | type: "transaction" | "token" | "address" | "block", 15 | ): string { 16 | const prefix = `https://${ETHERSCAN_PREFIXES[chainId] || ETHERSCAN_PREFIXES[1]}etherscan.io`; 17 | 18 | switch (type) { 19 | case "transaction": { 20 | return `${prefix}/tx/${data}`; 21 | } 22 | case "token": { 23 | return `${prefix}/token/${data}`; 24 | } 25 | case "block": { 26 | return `${prefix}/block/${data}`; 27 | } 28 | case "address": 29 | default: { 30 | return `${prefix}/address/${data}`; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /interface/utils/format.ts: -------------------------------------------------------------------------------- 1 | import Decimal from "decimal.js"; 2 | import { Asset } from "../types/split"; 3 | import { convertToUnitAmount } from "./number"; 4 | 5 | export interface FormattedTokenAmount { 6 | full: string; 7 | minimized: string; 8 | fullWithUnits: string; 9 | minimizedWithUnits: string; 10 | } 11 | 12 | export const formatTokenAmount = (amount: Decimal, token: Asset): FormattedTokenAmount => { 13 | const fullValue = convertToUnitAmount(amount, token.decimals); 14 | const minimizedValue = fullValue.toSignificantDigits(4); 15 | return { 16 | full: fullValue.toString(), 17 | minimized: minimizedValue.toString(), 18 | fullWithUnits: `${fullValue.toString()} ${token.symbol}`, 19 | minimizedWithUnits: `${minimizedValue.toString()} ${token.symbol}`, 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /interface/utils/number.ts: -------------------------------------------------------------------------------- 1 | import Decimal from "decimal.js"; 2 | import { Asset } from "../types/split"; 3 | 4 | export const convertToUnitAmount = (amount: Decimal, decimals: number) => { 5 | const unit = new Decimal(10).pow(decimals); 6 | return amount.dividedBy(unit); 7 | }; 8 | 9 | export const convertToBaseAmount = (amount: string, decimals: number) => { 10 | const unit = new Decimal(10).pow(decimals); 11 | return unit.mul(amount); 12 | }; 13 | 14 | // Split tokens are always 18 decimal places 15 | // cTokens are always 8 decimal places 16 | // Actual tokens vary. 17 | // The price from the price oracle is scaled by 18 decimal places. 18 | export const fullTokenAmountToComponentTokenAmount = ( 19 | baseAmount: Decimal, 20 | price: Decimal, 21 | underlyingTokenDecimals: number, 22 | ) => { 23 | const pow = 18 + underlyingTokenDecimals; 24 | const adjustment = new Decimal(10).pow(pow); 25 | return baseAmount.mul(price).div(adjustment); 26 | }; 27 | 28 | export const componentTokenAmountToFullTokenAmount = ( 29 | baseAmount: Decimal, 30 | price: Decimal, 31 | underlyingTokenDecimals: number, 32 | ) => { 33 | const pow = underlyingTokenDecimals - 8; 34 | const adjustment = new Decimal(10).pow(pow); 35 | return baseAmount.mul(adjustment).div(price); 36 | }; 37 | 38 | export interface FormattedTokenAmount { 39 | full: string; 40 | minimized: string; 41 | fullWithUnits: string; 42 | minimizedWithUnits: string; 43 | } 44 | 45 | export const formatTokenAmount = (amount: Decimal, token: Asset): FormattedTokenAmount => { 46 | const fullValue = convertToUnitAmount(amount, token.decimals); 47 | const minimizedValue = fullValue.toSignificantDigits(4); 48 | return { 49 | full: fullValue.toString(), 50 | minimized: minimizedValue.toString(), 51 | fullWithUnits: `${fullValue.toString()} ${token.symbol}`, 52 | minimizedWithUnits: `${minimizedValue.toString()} ${token.symbol}`, 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "split", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "https://github.com/split-fi/split.git", 6 | "author": "francesco@fragosti.com", 7 | "license": "AGPL-3.0", 8 | "private": true, 9 | "workspaces": [ 10 | "protocol", 11 | "interface" 12 | ], 13 | "scripts": { 14 | "build": "yarn workspaces run build", 15 | "test": "yarn workspaces run test", 16 | "prettier:fix": "prettier --config .prettierrc --write \"**/*.{js,json,md,sol,ts,tsx}\"", 17 | "prettier:check": "prettier --config .prettierrc --list-different \"**/*.{js,json,md,sol,ts,tsx}\"" 18 | }, 19 | "devDependencies": { 20 | "prettier": "^2.1.2", 21 | "prettier-plugin-solidity": "^1.0.0-alpha.59", 22 | "solhint": "^3.3.2", 23 | "solhint-plugin-prettier": "^0.0.5" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /protocol/.env_example: -------------------------------------------------------------------------------- 1 | ETH_RPC_URL_RINKEBY= 2 | ETH_RPC_URL_MAINNET= 3 | PRIVATE_KEY_RINKEBY= 4 | PRIVATE_KEY_MAINNET= 5 | ETHERSCAN_API_KEY= -------------------------------------------------------------------------------- /protocol/contracts/CTokenPriceOracle.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: AGPL-3.0 2 | pragma solidity 0.6.12; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | import "@openzeppelin/contracts/math/SafeMath.sol"; 7 | 8 | import "./interfaces/PriceOracle.sol"; 9 | import "./interfaces/CTokenInterface.sol"; 10 | 11 | contract CTokenPriceOracle is PriceOracle { 12 | using SafeMath for uint256; 13 | 14 | constructor() public {} 15 | 16 | /// @dev Get the exchange rate of one cToken to one underlying token in wads 17 | function getPrice(address cTokenAddress) external view override returns (uint256) { 18 | CTokenInterface cToken = CTokenInterface(cTokenAddress); 19 | return cToken.exchangeRateStored(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /protocol/contracts/CapitalComponentToken.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: AGPL-3.0 2 | pragma solidity 0.6.12; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | import "@openzeppelin/contracts/access/Ownable.sol"; 7 | import "@openzeppelin/contracts/math/SafeMath.sol"; 8 | 9 | import "./interfaces/PriceOracle.sol"; 10 | import "./SplitVault.sol"; 11 | import "./VaultControlled.sol"; 12 | import "./lib/DSMath.sol"; 13 | 14 | contract CapitalComponentToken is ERC20, VaultControlled { 15 | using SafeMath for uint256; 16 | 17 | address public fullToken; 18 | PriceOracle private priceOracle; 19 | 20 | constructor( 21 | string memory name, 22 | string memory symbol, 23 | address _fullToken, 24 | address priceOracleAddress, 25 | address splitVaultAddress 26 | ) public ERC20(name, symbol) VaultControlled(splitVaultAddress) { 27 | priceOracle = PriceOracle(priceOracleAddress); 28 | splitVault = SplitVault(splitVaultAddress); 29 | fullToken = _fullToken; 30 | } 31 | 32 | /// @dev Mint new capital component tokens, but compute the amount from an amount of full tokens. 33 | /// @param account address of account to mint tokens to 34 | /// @param amountOfFull amount of full tokens to use for the calculation 35 | /// @return amountMinted amount minted 36 | function mintFromFull(address account, uint256 amountOfFull) public onlyVaultOrOwner returns (uint256 amountMinted) { 37 | uint256 price = priceOracle.getPrice(fullToken); 38 | uint256 componentTokenAmount = DSMath.wmul(amountOfFull, price); 39 | _mint(account, componentTokenAmount); 40 | return componentTokenAmount; 41 | } 42 | 43 | /// @dev Mint new tokens if the contract owner 44 | /// @param account address of account to mint tokens to 45 | /// @param amount amount of tokens to mint 46 | function mint(address account, uint256 amount) public onlyOwner { 47 | _mint(account, amount); 48 | } 49 | 50 | /// @dev Burn tokens if the contract owner 51 | /// @param account address of account to burn tokens from 52 | /// @param amount amount of tokens to burn 53 | function burn(address account, uint256 amount) public onlyVaultOrOwner { 54 | _burn(account, amount); 55 | uint256 payoutAmount = calculatePayoutAmount(amount); 56 | // Call the payout function on the SplitVault contract 57 | splitVault.payout(payoutAmount, fullToken, account); 58 | } 59 | 60 | /// @dev Simplest public method for calculating the amount of fullToken due for a given amount of capital token 61 | /// @param capitalTokenAmount Amount of capital token to calculate the payout from 62 | /// @return The payout amount denoted in fullToken 63 | function calculatePayoutAmount(uint256 capitalTokenAmount) public view returns (uint256) { 64 | uint256 currPrice = priceOracle.getPrice(fullToken); 65 | return calculatePayoutAmount(capitalTokenAmount, currPrice); 66 | } 67 | 68 | /// @dev Pure function for calculating the amount of fullToken due for a given amount of capital token 69 | /// @param capitalTokenAmount Amount of capital token to calculate the payout from 70 | /// @param currPrice The current price of the fullToken with respect to the underlying 71 | /// @return The payout amount denoted in fullToken 72 | function calculatePayoutAmount(uint256 capitalTokenAmount, uint256 currPrice) public pure returns (uint256) { 73 | uint256 payoutAmount = DSMath.wdiv(capitalTokenAmount, currPrice); 74 | return payoutAmount; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /protocol/contracts/SplitPoolFactory.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: AGPL-3.0 2 | pragma solidity 0.6.12; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | 8 | import "./lib/balancer/configurable-rights-pool/contracts/ConfigurableRightsPool.sol"; 9 | import "./lib/balancer/configurable-rights-pool/contracts/CRPFactory.sol"; 10 | import "./lib/balancer/configurable-rights-pool/libraries/RightsManager.sol"; 11 | import "./SplitVault.sol"; 12 | 13 | /// @notice A wrapper around a CRPFactory to create pools configured for Split tokens. 14 | contract SplitPoolFactory is Ownable { 15 | address private _crpFactoryAddress; 16 | mapping(address => bool) private _isSplitPool; 17 | 18 | event LogNewSplitPool(address indexed caller, address indexed pool); 19 | 20 | /// @notice Create a new ConfigurableRightsPool using the underlying factory 21 | /// @param factoryAddress BFactory instance used to create the underlying pool 22 | /// @param poolParams struct containing the names, tokens, weights, balances, and swap fee 23 | /// @param rights struct of permissions, configuring this CRP instance (see above for definitions) 24 | /// @return configurable rights pool address 25 | function newSplitPool( 26 | address factoryAddress, 27 | ConfigurableRightsPool.PoolParams calldata poolParams, 28 | RightsManager.Rights calldata rights 29 | ) external onlyOwner returns (ConfigurableRightsPool) { 30 | ConfigurableRightsPool crp = CRPFactory(_crpFactoryAddress).newCrp(factoryAddress, poolParams, rights); 31 | emit LogNewSplitPool(msg.sender, address(crp)); 32 | _isSplitPool[address(crp)] = true; 33 | crp.setController(msg.sender); 34 | return crp; 35 | } 36 | 37 | /// @notice Construct a wrapper around a CRPFactory 38 | /// @param crpFactoryAddress address of the underlying Configuring Rights Pool Factory 39 | constructor(address crpFactoryAddress) public { 40 | _crpFactoryAddress = crpFactoryAddress; 41 | } 42 | 43 | /// @notice A shortcut to split tokens and to supply the splits to an underlying balancer pool 44 | /// @param crpPoolAddress address of a deployed and active Configurable Rights Pool 45 | /// @param splitVaultAddress address of the SplitVault to query for component tokens 46 | /// @param fullTokenAddress address of the token to pull and split 47 | /// @param fullTokenAmountIn amount of the token to pull in 48 | /// @param minPoolAmountOut a check to ensure that enough balancer pool tokens will be received 49 | /// @return poolAmountOut the amount of pool tokens received 50 | function joinPoolBySplitting( 51 | address crpPoolAddress, 52 | address splitVaultAddress, 53 | address fullTokenAddress, 54 | uint256 fullTokenAmountIn, 55 | uint256 minPoolAmountOut 56 | ) external returns (uint256 poolAmountOut) { 57 | // Get the full tokens from msg.sender 58 | require( 59 | IERC20(fullTokenAddress).transferFrom(msg.sender, address(this), fullTokenAmountIn), 60 | "Failed to transfer tokens to SplitPool" 61 | ); 62 | require(_isSplitPool[crpPoolAddress], "Can only join Split pools"); 63 | // Split the full tokens that are now in custody 64 | IERC20(fullTokenAddress).approve(splitVaultAddress, fullTokenAmountIn); 65 | uint256 componentTokenAmount = SplitVault(splitVaultAddress).split(fullTokenAmountIn, fullTokenAddress); 66 | 67 | // Find the component tokens and add them to the balancer pool by swapping them each in. 68 | SplitVault.ComponentSet memory componentSet = SplitVault(splitVaultAddress).getComponentSet(fullTokenAddress); 69 | IERC20(componentSet.yieldToken).approve(crpPoolAddress, componentTokenAmount); 70 | IERC20(componentSet.capitalToken).approve(crpPoolAddress, componentTokenAmount); 71 | uint256 yieldPoolAmountOut = ConfigurableRightsPool(crpPoolAddress).joinswapExternAmountIn( 72 | componentSet.yieldToken, 73 | componentTokenAmount, 74 | 0 75 | ); 76 | uint256 capitalPoolAmountOut = ConfigurableRightsPool(crpPoolAddress).joinswapExternAmountIn( 77 | componentSet.capitalToken, 78 | componentTokenAmount, 79 | 0 80 | ); 81 | uint256 amountOut = yieldPoolAmountOut + capitalPoolAmountOut; 82 | require(amountOut >= minPoolAmountOut, "ERR_LIMIT_OUT"); 83 | // Transfer the balancer pool tokens back to msg.sender 84 | require(IERC20(crpPoolAddress).transfer(msg.sender, amountOut), "Failed to transfer BP tokens back to msg.sender"); 85 | return amountOut; 86 | } 87 | 88 | /// @notice Check to see if a given address is a SplitPool 89 | /// @param addr - address to check 90 | /// @return boolean indicating whether it is a SplitPool 91 | function isSplitPool(address addr) external view returns (bool) { 92 | return _isSplitPool[addr]; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /protocol/contracts/SplitVault.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: AGPL-3.0 2 | pragma solidity 0.6.12; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | 8 | import "./CapitalComponentToken.sol"; 9 | import "./YieldComponentToken.sol"; 10 | 11 | contract SplitVault is Ownable { 12 | /* 13 | * Storage 14 | */ 15 | struct ComponentSet { 16 | address yieldToken; 17 | address capitalToken; 18 | } 19 | mapping(address => ComponentSet) public tokensToComponents; 20 | 21 | constructor() public {} 22 | 23 | /// @dev Retrieve the componentSet for a given token 24 | /// @param tokenAddress for which to fetch the associated componentSet 25 | function getComponentSet(address tokenAddress) public view returns (ComponentSet memory) { 26 | return tokensToComponents[tokenAddress]; 27 | } 28 | 29 | /// @dev Allows Split protocol governance to add support for new tokens 30 | /// @param tokenAddress the address of token to support 31 | /// @param yieldTokenAddress the corresponding yieldERC20Comp token address 32 | /// @param capitalTokenAddress the corresponding capitalERC20Comp token address 33 | function add( 34 | address tokenAddress, 35 | address yieldTokenAddress, 36 | address capitalTokenAddress 37 | ) public onlyOwner { 38 | tokensToComponents[tokenAddress] = ComponentSet({ 39 | yieldToken: yieldTokenAddress, 40 | capitalToken: capitalTokenAddress 41 | }); 42 | } 43 | 44 | /// @dev Allows Split protocol governance to remove support for new tokens 45 | /// @param tokenAddress the address of token to remove support for 46 | function remove(address tokenAddress) public onlyOwner { 47 | delete tokensToComponents[tokenAddress]; 48 | } 49 | 50 | /// @dev Allows a holder of a whitelisted Compound token to split it into it's corresponding Yield and Capital tokens 51 | /// @param amount of tokens to split 52 | /// @param tokenAddress the address of token to split 53 | /// @return amountMintedForEach amount of component tokens minted (each) 54 | function split(uint256 amount, address tokenAddress) public returns (uint256 amountMintedForEach) { 55 | ComponentSet memory componentSet = tokensToComponents[tokenAddress]; 56 | if (componentSet.yieldToken == address(0) || componentSet.capitalToken == address(0)) { 57 | revert("Attempted to split unsupported token"); 58 | } 59 | // Don't mint tokens if the transferFrom was not successful 60 | require( 61 | IERC20(tokenAddress).transferFrom(msg.sender, address(this), amount), 62 | "Failed to transfer tokens to SplitVault." 63 | ); 64 | CapitalComponentToken(componentSet.capitalToken).mintFromFull(msg.sender, amount); 65 | uint256 yieldComponentTokenAmount = YieldComponentToken(componentSet.yieldToken).mintFromFull(msg.sender, amount); 66 | emit Split(tokenAddress, amount); 67 | return yieldComponentTokenAmount; 68 | } 69 | 70 | /// @dev Allows a holder of both Yield and Capital tokens to combine them into the underlying full tokens 71 | /// @param amount of tokens to recombine 72 | /// @param tokenAddress is the address of token to recombine 73 | function combine(uint256 amount, address tokenAddress) public { 74 | ComponentSet memory componentSet = tokensToComponents[tokenAddress]; 75 | if (componentSet.yieldToken == address(0) || componentSet.capitalToken == address(0)) { 76 | revert("Attempted to recombine unsupported token"); 77 | } 78 | CapitalComponentToken(componentSet.capitalToken).burn(msg.sender, amount); 79 | YieldComponentToken(componentSet.yieldToken).burn(msg.sender, amount); 80 | // Payout is calculated and executed by the individual token contracts 81 | emit Combine(tokenAddress, amount); 82 | } 83 | 84 | /// @dev Allows component token implementation to send tokens in the vaul 85 | /// @param amount of tokens to payout 86 | /// @param tokenAddress the tokens to send 87 | /// @param recipient address of payout recipient 88 | function payout( 89 | uint256 amount, 90 | address tokenAddress, 91 | address recipient 92 | ) public { 93 | ComponentSet memory componentSet = tokensToComponents[tokenAddress]; 94 | if (componentSet.yieldToken == address(0) || componentSet.capitalToken == address(0)) { 95 | revert("Attempted to request a payout for an unsupported token"); 96 | } 97 | if (msg.sender != componentSet.yieldToken && msg.sender != componentSet.capitalToken) { 98 | revert("Payout can only be called by the corresponding yield or capital token"); 99 | } 100 | // Revert if the transfer was not successful 101 | require(IERC20(tokenAddress).transfer(recipient, amount), "Failed to transfer tokens from SplitVault."); 102 | } 103 | 104 | /// @dev Emitted when component tokens are combined into a full token 105 | event Combine(address indexed tokenAddress, uint256 amount); 106 | 107 | /// @dev Emitted when full tokens are split into component tokens 108 | event Split(address indexed tokenAddress, uint256 amount); 109 | } 110 | -------------------------------------------------------------------------------- /protocol/contracts/VaultControlled.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: AGPL-3.0 2 | pragma solidity 0.6.12; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "@openzeppelin/contracts/GSN/Context.sol"; 6 | 7 | import "./SplitVault.sol"; 8 | 9 | contract VaultControlled is Context { 10 | address private _owner; 11 | SplitVault internal splitVault; 12 | 13 | event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); 14 | 15 | constructor(address splitVaultAddress) internal { 16 | splitVault = SplitVault(splitVaultAddress); 17 | address msgSender = _msgSender(); 18 | _owner = msgSender; 19 | emit OwnershipTransferred(address(0), msgSender); 20 | } 21 | 22 | /// @dev Throws if called by any account other than the SplitVault or Owner. 23 | modifier onlyVaultOrOwner() { 24 | require(address(splitVault) == _msgSender() || _owner == _msgSender(), "Caller is not the SplitVault or Owner"); 25 | _; 26 | } 27 | 28 | /// @dev Returns the address of the current owner. 29 | function owner() public view returns (address) { 30 | return _owner; 31 | } 32 | 33 | /// @dev Throws if called by any account other than the owner. 34 | modifier onlyOwner() { 35 | require(_owner == _msgSender(), "Ownable: caller is not the owner"); 36 | _; 37 | } 38 | 39 | /// @dev Leaves the contract without owner. It will not be possible to call 40 | /// `onlyOwner` functions anymore. Can only be called by the current owner. 41 | function renounceOwnership() public virtual onlyOwner { 42 | emit OwnershipTransferred(_owner, address(0)); 43 | _owner = address(0); 44 | } 45 | 46 | /// @dev Transfers ownership of the contract to a new account (`newOwner`). 47 | /// Can only be called by the current owner. 48 | function transferOwnership(address newOwner) public virtual onlyOwner { 49 | require(newOwner != address(0), "Ownable: new owner is the zero address"); 50 | emit OwnershipTransferred(_owner, newOwner); 51 | _owner = newOwner; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /protocol/contracts/interfaces/CTokenInterface.sol: -------------------------------------------------------------------------------- 1 | // //SPDX-License-Identifier: AGPL-3.0 2 | pragma solidity 0.6.12; 3 | pragma experimental ABIEncoderV2; 4 | 5 | /** 6 | * @dev Interface of Compound cToken 7 | */ 8 | interface CTokenInterface { 9 | /** 10 | * @notice Calculates the exchange rate from the underlying to the CToken 11 | * @return Calculated exchange rate scaled by 1e18 12 | */ 13 | function exchangeRateStored() external view returns (uint256); 14 | 15 | function decimals() external view returns (uint256); 16 | 17 | function underlying() external view returns (address); 18 | } 19 | -------------------------------------------------------------------------------- /protocol/contracts/interfaces/PriceOracle.sol: -------------------------------------------------------------------------------- 1 | // //SPDX-License-Identifier: AGPL-3.0 2 | pragma solidity 0.6.12; 3 | pragma experimental ABIEncoderV2; 4 | 5 | interface PriceOracle { 6 | /// @dev Get the price of token in another token (eg. cBAT / BAT) with 18 decimal places 7 | function getPrice(address token) external view returns (uint256); 8 | } 9 | -------------------------------------------------------------------------------- /protocol/contracts/lib/DSMath.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: AGPL-3.0 2 | pragma solidity >0.4.13; 3 | 4 | library DSMath { 5 | function add(uint256 x, uint256 y) internal pure returns (uint256 z) { 6 | require((z = x + y) >= x, "ds-math-add-overflow"); 7 | } 8 | 9 | function sub(uint256 x, uint256 y) internal pure returns (uint256 z) { 10 | require((z = x - y) <= x, "ds-math-sub-underflow"); 11 | } 12 | 13 | function mul(uint256 x, uint256 y) internal pure returns (uint256 z) { 14 | require(y == 0 || (z = x * y) / y == x, "ds-math-mul-overflow"); 15 | } 16 | 17 | function min(uint256 x, uint256 y) internal pure returns (uint256 z) { 18 | return x <= y ? x : y; 19 | } 20 | 21 | function max(uint256 x, uint256 y) internal pure returns (uint256 z) { 22 | return x >= y ? x : y; 23 | } 24 | 25 | function imin(int256 x, int256 y) internal pure returns (int256 z) { 26 | return x <= y ? x : y; 27 | } 28 | 29 | function imax(int256 x, int256 y) internal pure returns (int256 z) { 30 | return x >= y ? x : y; 31 | } 32 | 33 | uint256 constant WAD = 10**18; 34 | uint256 constant RAY = 10**27; 35 | 36 | //rounds to zero if x*y < WAD / 2 37 | function wmul(uint256 x, uint256 y) internal pure returns (uint256 z) { 38 | z = add(mul(x, y), WAD / 2) / WAD; 39 | } 40 | 41 | //rounds to zero if x*y < WAD / 2 42 | function rmul(uint256 x, uint256 y) internal pure returns (uint256 z) { 43 | z = add(mul(x, y), RAY / 2) / RAY; 44 | } 45 | 46 | //rounds to zero if x*y < WAD / 2 47 | function wdiv(uint256 x, uint256 y) internal pure returns (uint256 z) { 48 | z = add(mul(x, WAD), y / 2) / y; 49 | } 50 | 51 | //rounds to zero if x*y < RAY / 2 52 | function rdiv(uint256 x, uint256 y) internal pure returns (uint256 z) { 53 | z = add(mul(x, RAY), y / 2) / y; 54 | } 55 | 56 | // This famous algorithm is called "exponentiation by squaring" 57 | // and calculates x^n with x as fixed-point and n as regular unsigned. 58 | // 59 | // It's O(log n), instead of O(n) for naive repeated multiplication. 60 | // 61 | // These facts are why it works: 62 | // 63 | // If n is even, then x^n = (x^2)^(n/2). 64 | // If n is odd, then x^n = x * x^(n-1), 65 | // and applying the equation for even x gives 66 | // x^n = x * (x^2)^((n-1) / 2). 67 | // 68 | // Also, EVM division is flooring and 69 | // floor[(n-1) / 2] = floor[n / 2]. 70 | // 71 | function rpow(uint256 x, uint256 n) internal pure returns (uint256 z) { 72 | z = n % 2 != 0 ? x : RAY; 73 | 74 | for (n /= 2; n != 0; n /= 2) { 75 | x = rmul(x, x); 76 | 77 | if (n % 2 != 0) { 78 | z = rmul(z, x); 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /protocol/contracts/lib/balancer/configurable-rights-pool/contracts/CRPFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.6.12; 3 | 4 | // Needed to handle structures externally 5 | pragma experimental ABIEncoderV2; 6 | 7 | // Imports 8 | 9 | import "./ConfigurableRightsPool.sol"; 10 | 11 | // Contracts 12 | 13 | /** 14 | * @author Balancer Labs 15 | * @title Configurable Rights Pool Factory - create parameterized smart pools 16 | * @dev Rights are held in a corresponding struct in ConfigurableRightsPool 17 | * Index values are as follows: 18 | * 0: canPauseSwapping - can setPublicSwap back to false after turning it on 19 | * by default, it is off on initialization and can only be turned on 20 | * 1: canChangeSwapFee - can setSwapFee after initialization (by default, it is fixed at create time) 21 | * 2: canChangeWeights - can bind new token weights (allowed by default in base pool) 22 | * 3: canAddRemoveTokens - can bind/unbind tokens (allowed by default in base pool) 23 | * 4: canWhitelistLPs - if set, only whitelisted addresses can join pools 24 | * (enables private pools with more than one LP) 25 | * 5: canChangeCap - can change the BSP cap (max # of pool tokens) 26 | */ 27 | contract CRPFactory { 28 | // State variables 29 | 30 | // Keep a list of all Configurable Rights Pools 31 | mapping(address => bool) private _isCrp; 32 | 33 | // Event declarations 34 | 35 | // Log the address of each new smart pool, and its creator 36 | event LogNewCrp(address indexed caller, address indexed pool); 37 | 38 | // Function declarations 39 | 40 | /** 41 | * @notice Create a new CRP 42 | * @dev emits a LogNewCRP event 43 | * @param factoryAddress - the BFactory instance used to create the underlying pool 44 | * @param poolParams - struct containing the names, tokens, weights, balances, and swap fee 45 | * @param rights - struct of permissions, configuring this CRP instance (see above for definitions) 46 | */ 47 | function newCrp( 48 | address factoryAddress, 49 | ConfigurableRightsPool.PoolParams calldata poolParams, 50 | RightsManager.Rights calldata rights 51 | ) external returns (ConfigurableRightsPool) { 52 | require(poolParams.constituentTokens.length >= BalancerConstants.MIN_ASSET_LIMIT, "ERR_TOO_FEW_TOKENS"); 53 | 54 | // Arrays must be parallel 55 | require(poolParams.tokenBalances.length == poolParams.constituentTokens.length, "ERR_START_BALANCES_MISMATCH"); 56 | require(poolParams.tokenWeights.length == poolParams.constituentTokens.length, "ERR_START_WEIGHTS_MISMATCH"); 57 | 58 | ConfigurableRightsPool crp = new ConfigurableRightsPool(factoryAddress, poolParams, rights); 59 | 60 | emit LogNewCrp(msg.sender, address(crp)); 61 | 62 | _isCrp[address(crp)] = true; 63 | // The caller is the controller of the CRP 64 | // The CRP will be the controller of the underlying Core BPool 65 | crp.setController(msg.sender); 66 | 67 | return crp; 68 | } 69 | 70 | /** 71 | * @notice Check to see if a given address is a CRP 72 | * @param addr - address to check 73 | * @return boolean indicating whether it is a CRP 74 | */ 75 | function isCrp(address addr) external view returns (bool) { 76 | return _isCrp[addr]; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /protocol/contracts/lib/balancer/configurable-rights-pool/contracts/IBFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.6.12; 3 | 4 | interface IBPool { 5 | function rebind( 6 | address token, 7 | uint256 balance, 8 | uint256 denorm 9 | ) external; 10 | 11 | function setSwapFee(uint256 swapFee) external; 12 | 13 | function setPublicSwap(bool publicSwap) external; 14 | 15 | function bind( 16 | address token, 17 | uint256 balance, 18 | uint256 denorm 19 | ) external; 20 | 21 | function unbind(address token) external; 22 | 23 | function gulp(address token) external; 24 | 25 | function isBound(address token) external view returns (bool); 26 | 27 | function getBalance(address token) external view returns (uint256); 28 | 29 | function totalSupply() external view returns (uint256); 30 | 31 | function getSwapFee() external view returns (uint256); 32 | 33 | function isPublicSwap() external view returns (bool); 34 | 35 | function getDenormalizedWeight(address token) external view returns (uint256); 36 | 37 | function getTotalDenormalizedWeight() external view returns (uint256); 38 | 39 | // solhint-disable-next-line func-name-mixedcase 40 | function EXIT_FEE() external view returns (uint256); 41 | 42 | function calcPoolOutGivenSingleIn( 43 | uint256 tokenBalanceIn, 44 | uint256 tokenWeightIn, 45 | uint256 poolSupply, 46 | uint256 totalWeight, 47 | uint256 tokenAmountIn, 48 | uint256 swapFee 49 | ) external pure returns (uint256 poolAmountOut); 50 | 51 | function calcSingleInGivenPoolOut( 52 | uint256 tokenBalanceIn, 53 | uint256 tokenWeightIn, 54 | uint256 poolSupply, 55 | uint256 totalWeight, 56 | uint256 poolAmountOut, 57 | uint256 swapFee 58 | ) external pure returns (uint256 tokenAmountIn); 59 | 60 | function calcSingleOutGivenPoolIn( 61 | uint256 tokenBalanceOut, 62 | uint256 tokenWeightOut, 63 | uint256 poolSupply, 64 | uint256 totalWeight, 65 | uint256 poolAmountIn, 66 | uint256 swapFee 67 | ) external pure returns (uint256 tokenAmountOut); 68 | 69 | function calcPoolInGivenSingleOut( 70 | uint256 tokenBalanceOut, 71 | uint256 tokenWeightOut, 72 | uint256 poolSupply, 73 | uint256 totalWeight, 74 | uint256 tokenAmountOut, 75 | uint256 swapFee 76 | ) external pure returns (uint256 poolAmountIn); 77 | 78 | function getCurrentTokens() external view returns (address[] memory tokens); 79 | } 80 | 81 | interface IBFactory { 82 | function newBPool() external returns (IBPool); 83 | 84 | function setBLabs(address b) external; 85 | 86 | function collect(IBPool pool) external; 87 | 88 | function isBPool(address b) external view returns (bool); 89 | 90 | function getBLabs() external view returns (address); 91 | } 92 | -------------------------------------------------------------------------------- /protocol/contracts/lib/balancer/configurable-rights-pool/contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.6.12; 3 | 4 | contract Migrations { 5 | address public owner; 6 | uint256 public lastCompletedMigration; 7 | 8 | constructor() public { 9 | owner = msg.sender; 10 | } 11 | 12 | modifier restricted() { 13 | if (msg.sender == owner) _; 14 | } 15 | 16 | /** 17 | * @notice set lastCompletedMigration variable 18 | * @param completed - id of the desired migration level 19 | */ 20 | function setCompleted(uint256 completed) external restricted { 21 | lastCompletedMigration = completed; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /protocol/contracts/lib/balancer/configurable-rights-pool/contracts/utils/BalancerOwnable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.6.12; 3 | 4 | /** 5 | * @dev Contract module which provides a basic access control mechanism, where 6 | * there is an account (an owner) that can be granted exclusive access to 7 | * specific functions. 8 | * 9 | * By default, the owner account will be the one that deploys the contract. This 10 | * can later be changed with {transferOwnership}. 11 | * 12 | * This module is used through inheritance. It will make available the modifier 13 | * `onlyOwner`, which can be applied to your functions to restrict their use to 14 | * the owner. 15 | */ 16 | contract BalancerOwnable { 17 | // State variables 18 | 19 | address private _owner; 20 | 21 | // Event declarations 22 | 23 | event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); 24 | 25 | // Modifiers 26 | 27 | /** 28 | * @dev Throws if called by any account other than the owner. 29 | */ 30 | modifier onlyOwner() { 31 | require(_owner == msg.sender, "ERR_NOT_CONTROLLER"); 32 | _; 33 | } 34 | 35 | // Function declarations 36 | 37 | /** 38 | * @dev Initializes the contract setting the deployer as the initial owner. 39 | */ 40 | constructor() internal { 41 | _owner = msg.sender; 42 | } 43 | 44 | /** 45 | * @notice Transfers ownership of the contract to a new account (`newOwner`). 46 | * Can only be called by the current owner 47 | * @dev external for gas optimization 48 | * @param newOwner - address of new owner 49 | */ 50 | function setController(address newOwner) external onlyOwner { 51 | require(newOwner != address(0), "ERR_ZERO_ADDRESS"); 52 | 53 | emit OwnershipTransferred(_owner, newOwner); 54 | 55 | _owner = newOwner; 56 | } 57 | 58 | /** 59 | * @notice Returns the address of the current owner 60 | * @dev external for gas optimization 61 | * @return address - of the owner (AKA controller) 62 | */ 63 | function getController() external view returns (address) { 64 | return _owner; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /protocol/contracts/lib/balancer/configurable-rights-pool/contracts/utils/BalancerReentrancyGuard.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.6.12; 3 | 4 | /** 5 | * @author Balancer Labs (and OpenZeppelin) 6 | * @title Protect against reentrant calls (and also selectively protect view functions) 7 | * @dev Contract module that helps prevent reentrant calls to a function. 8 | * 9 | * Inheriting from `ReentrancyGuard` will make the {_lock_} modifier 10 | * available, which can be applied to functions to make sure there are no nested 11 | * (reentrant) calls to them. 12 | * 13 | * Note that because there is a single `_lock_` guard, functions marked as 14 | * `_lock_` may not call one another. This can be worked around by making 15 | * those functions `private`, and then adding `external` `_lock_` entry 16 | * points to them. 17 | * 18 | * Also adds a _lockview_ modifier, which doesn't create a lock, but fails 19 | * if another _lock_ call is in progress 20 | */ 21 | contract BalancerReentrancyGuard { 22 | // Booleans are more expensive than uint256 or any type that takes up a full 23 | // word because each write operation emits an extra SLOAD to first read the 24 | // slot's contents, replace the bits taken up by the boolean, and then write 25 | // back. This is the compiler's defense against contract upgrades and 26 | // pointer aliasing, and it cannot be disabled. 27 | 28 | // The values being non-zero value makes deployment a bit more expensive, 29 | // but in exchange the refund on every call to nonReentrant will be lower in 30 | // amount. Since refunds are capped to a percentage of the total 31 | // transaction's gas, it is best to keep them low in cases like this one, to 32 | // increase the likelihood of the full refund coming into effect. 33 | uint256 private constant _NOT_ENTERED = 1; 34 | uint256 private constant _ENTERED = 2; 35 | 36 | uint256 private _status; 37 | 38 | constructor() internal { 39 | _status = _NOT_ENTERED; 40 | } 41 | 42 | /** 43 | * @dev Prevents a contract from calling itself, directly or indirectly. 44 | * Calling a `_lock_` function from another `_lock_` 45 | * function is not supported. It is possible to prevent this from happening 46 | * by making the `_lock_` function external, and make it call a 47 | * `private` function that does the actual work. 48 | */ 49 | modifier lock() { 50 | // On the first call to _lock_, _notEntered will be true 51 | require(_status != _ENTERED, "ERR_REENTRY"); 52 | 53 | // Any calls to _lock_ after this point will fail 54 | _status = _ENTERED; 55 | _; 56 | // By storing the original value once again, a refund is triggered (see 57 | // https://eips.ethereum.org/EIPS/eip-2200) 58 | _status = _NOT_ENTERED; 59 | } 60 | 61 | /** 62 | * @dev Also add a modifier that doesn't create a lock, but protects functions that 63 | * should not be called while a _lock_ function is running 64 | */ 65 | modifier viewlock() { 66 | require(_status != _ENTERED, "ERR_REENTRY_VIEW"); 67 | _; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /protocol/contracts/lib/balancer/configurable-rights-pool/interfaces/BalancerIERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.6.12; 3 | 4 | // Interface declarations 5 | 6 | /* solhint-disable func-order */ 7 | 8 | interface BalancerIERC20 { 9 | // Emitted when the allowance of a spender for an owner is set by a call to approve. 10 | // Value is the new allowance 11 | event Approval(address indexed owner, address indexed spender, uint256 value); 12 | 13 | // Emitted when value tokens are moved from one account (from) to another (to). 14 | // Note that value may be zero 15 | event Transfer(address indexed from, address indexed to, uint256 value); 16 | 17 | // Returns the amount of tokens in existence 18 | function totalSupply() external view returns (uint256); 19 | 20 | // Returns the amount of tokens owned by account 21 | function balanceOf(address account) external view returns (uint256); 22 | 23 | // Returns the remaining number of tokens that spender will be allowed to spend on behalf of owner 24 | // through transferFrom. This is zero by default 25 | // This value changes when approve or transferFrom are called 26 | function allowance(address owner, address spender) external view returns (uint256); 27 | 28 | // Sets amount as the allowance of spender over the caller’s tokens 29 | // Returns a boolean value indicating whether the operation succeeded 30 | // Emits an Approval event. 31 | function approve(address spender, uint256 amount) external returns (bool); 32 | 33 | // Moves amount tokens from the caller’s account to recipient 34 | // Returns a boolean value indicating whether the operation succeeded 35 | // Emits a Transfer event. 36 | function transfer(address recipient, uint256 amount) external returns (bool); 37 | 38 | // Moves amount tokens from sender to recipient using the allowance mechanism 39 | // Amount is then deducted from the caller’s allowance 40 | // Returns a boolean value indicating whether the operation succeeded 41 | // Emits a Transfer event 42 | function transferFrom( 43 | address sender, 44 | address recipient, 45 | uint256 amount 46 | ) external returns (bool); 47 | } 48 | -------------------------------------------------------------------------------- /protocol/contracts/lib/balancer/configurable-rights-pool/interfaces/IConfigurableRightsPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.6.12; 3 | 4 | // Interface declarations 5 | 6 | // Introduce to avoid circularity (otherwise, the CRP and SmartPoolManager include each other) 7 | // Removing circularity allows flattener tools to work, which enables Etherscan verification 8 | interface IConfigurableRightsPool { 9 | function mintPoolShareFromLib(uint256 amount) external; 10 | 11 | function pushPoolShareFromLib(address to, uint256 amount) external; 12 | 13 | function pullPoolShareFromLib(address from, uint256 amount) external; 14 | 15 | function burnPoolShareFromLib(uint256 amount) external; 16 | 17 | function totalSupply() external view returns (uint256); 18 | 19 | function getController() external view returns (address); 20 | } 21 | -------------------------------------------------------------------------------- /protocol/contracts/lib/balancer/configurable-rights-pool/libraries/BalancerConstants.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.6.12; 3 | 4 | /** 5 | * @author Balancer Labs 6 | * @title Put all the constants in one place 7 | */ 8 | 9 | library BalancerConstants { 10 | // State variables (must be constant in a library) 11 | 12 | // B "ONE" - all math is in the "realm" of 10 ** 18; 13 | // where numeric 1 = 10 ** 18 14 | uint256 public constant BONE = 10**18; 15 | uint256 public constant MIN_WEIGHT = BONE; 16 | uint256 public constant MAX_WEIGHT = BONE * 50; 17 | uint256 public constant MAX_TOTAL_WEIGHT = BONE * 50; 18 | uint256 public constant MIN_BALANCE = BONE / 10**6; 19 | uint256 public constant MAX_BALANCE = BONE * 10**12; 20 | uint256 public constant MIN_POOL_SUPPLY = BONE * 100; 21 | uint256 public constant MAX_POOL_SUPPLY = BONE * 10**9; 22 | uint256 public constant MIN_FEE = BONE / 10**6; 23 | uint256 public constant MAX_FEE = BONE / 10; 24 | // EXIT_FEE must always be zero, or ConfigurableRightsPool._pushUnderlying will fail 25 | uint256 public constant EXIT_FEE = 0; 26 | uint256 public constant MAX_IN_RATIO = BONE / 2; 27 | uint256 public constant MAX_OUT_RATIO = (BONE / 3) + 1 wei; 28 | // Must match BConst.MIN_BOUND_TOKENS and BConst.MAX_BOUND_TOKENS 29 | uint256 public constant MIN_ASSET_LIMIT = 2; 30 | uint256 public constant MAX_ASSET_LIMIT = 8; 31 | uint256 public constant MAX_UINT = uint256(-1); 32 | } 33 | -------------------------------------------------------------------------------- /protocol/contracts/lib/balancer/configurable-rights-pool/libraries/RightsManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.6.12; 3 | 4 | // Needed to handle structures externally 5 | pragma experimental ABIEncoderV2; 6 | 7 | /** 8 | * @author Balancer Labs 9 | * @title Manage Configurable Rights for the smart pool 10 | * canPauseSwapping - can setPublicSwap back to false after turning it on 11 | * by default, it is off on initialization and can only be turned on 12 | * canChangeSwapFee - can setSwapFee after initialization (by default, it is fixed at create time) 13 | * canChangeWeights - can bind new token weights (allowed by default in base pool) 14 | * canAddRemoveTokens - can bind/unbind tokens (allowed by default in base pool) 15 | * canWhitelistLPs - can limit liquidity providers to a given set of addresses 16 | * canChangeCap - can change the BSP cap (max # of pool tokens) 17 | */ 18 | library RightsManager { 19 | // Type declarations 20 | 21 | enum Permissions { PAUSE_SWAPPING, CHANGE_SWAP_FEE, CHANGE_WEIGHTS, ADD_REMOVE_TOKENS, WHITELIST_LPS, CHANGE_CAP } 22 | 23 | struct Rights { 24 | bool canPauseSwapping; 25 | bool canChangeSwapFee; 26 | bool canChangeWeights; 27 | bool canAddRemoveTokens; 28 | bool canWhitelistLPs; 29 | bool canChangeCap; 30 | } 31 | 32 | // State variables (can only be constants in a library) 33 | bool public constant DEFAULT_CAN_PAUSE_SWAPPING = false; 34 | bool public constant DEFAULT_CAN_CHANGE_SWAP_FEE = true; 35 | bool public constant DEFAULT_CAN_CHANGE_WEIGHTS = true; 36 | bool public constant DEFAULT_CAN_ADD_REMOVE_TOKENS = false; 37 | bool public constant DEFAULT_CAN_WHITELIST_LPS = false; 38 | bool public constant DEFAULT_CAN_CHANGE_CAP = false; 39 | 40 | // Functions 41 | 42 | /** 43 | * @notice create a struct from an array (or return defaults) 44 | * @dev If you pass an empty array, it will construct it using the defaults 45 | * @param a - array input 46 | * @return Rights struct 47 | */ 48 | function constructRights(bool[] calldata a) external pure returns (Rights memory) { 49 | if (a.length == 0) { 50 | return 51 | Rights( 52 | DEFAULT_CAN_PAUSE_SWAPPING, 53 | DEFAULT_CAN_CHANGE_SWAP_FEE, 54 | DEFAULT_CAN_CHANGE_WEIGHTS, 55 | DEFAULT_CAN_ADD_REMOVE_TOKENS, 56 | DEFAULT_CAN_WHITELIST_LPS, 57 | DEFAULT_CAN_CHANGE_CAP 58 | ); 59 | } else { 60 | return Rights(a[0], a[1], a[2], a[3], a[4], a[5]); 61 | } 62 | } 63 | 64 | /** 65 | * @notice Convert rights struct to an array (e.g., for events, GUI) 66 | * @dev avoids multiple calls to hasPermission 67 | * @param rights - the rights struct to convert 68 | * @return boolean array containing the rights settings 69 | */ 70 | function convertRights(Rights calldata rights) external pure returns (bool[] memory) { 71 | bool[] memory result = new bool[](6); 72 | 73 | result[0] = rights.canPauseSwapping; 74 | result[1] = rights.canChangeSwapFee; 75 | result[2] = rights.canChangeWeights; 76 | result[3] = rights.canAddRemoveTokens; 77 | result[4] = rights.canWhitelistLPs; 78 | result[5] = rights.canChangeCap; 79 | 80 | return result; 81 | } 82 | 83 | // Though it is actually simple, the number of branches triggers code-complexity 84 | /* solhint-disable code-complexity */ 85 | 86 | /** 87 | * @notice Externally check permissions using the Enum 88 | * @param self - Rights struct containing the permissions 89 | * @param permission - The permission to check 90 | * @return Boolean true if it has the permission 91 | */ 92 | function hasPermission(Rights calldata self, Permissions permission) external pure returns (bool) { 93 | if (Permissions.PAUSE_SWAPPING == permission) { 94 | return self.canPauseSwapping; 95 | } else if (Permissions.CHANGE_SWAP_FEE == permission) { 96 | return self.canChangeSwapFee; 97 | } else if (Permissions.CHANGE_WEIGHTS == permission) { 98 | return self.canChangeWeights; 99 | } else if (Permissions.ADD_REMOVE_TOKENS == permission) { 100 | return self.canAddRemoveTokens; 101 | } else if (Permissions.WHITELIST_LPS == permission) { 102 | return self.canWhitelistLPs; 103 | } else if (Permissions.CHANGE_CAP == permission) { 104 | return self.canChangeCap; 105 | } 106 | } 107 | 108 | /* solhint-enable code-complexity */ 109 | } 110 | -------------------------------------------------------------------------------- /protocol/contracts/lib/balancer/configurable-rights-pool/libraries/SafeApprove.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.6.12; 3 | 4 | // Imports 5 | 6 | import "../interfaces/BalancerIERC20.sol"; 7 | 8 | // Libraries 9 | 10 | /** 11 | * @author PieDAO (ported to Balancer Labs) 12 | * @title SafeApprove - set approval for tokens that require 0 prior approval 13 | * @dev Perhaps to address the known ERC20 race condition issue 14 | * See https://github.com/crytic/not-so-smart-contracts/tree/master/race_condition 15 | * Some tokens - notably KNC - only allow approvals to be increased from 0 16 | */ 17 | library SafeApprove { 18 | /** 19 | * @notice handle approvals of tokens that require approving from a base of 0 20 | * @param token - the token we're approving 21 | * @param spender - entity the owner (sender) is approving to spend his tokens 22 | * @param amount - number of tokens being approved 23 | */ 24 | function safeApprove( 25 | BalancerIERC20 token, 26 | address spender, 27 | uint256 amount 28 | ) internal returns (bool) { 29 | uint256 currentAllowance = token.allowance(address(this), spender); 30 | 31 | // Do nothing if allowance is already set to this value 32 | if (currentAllowance == amount) { 33 | return true; 34 | } 35 | 36 | // If approval is not zero reset it to zero first 37 | if (currentAllowance != 0) { 38 | return token.approve(spender, 0); 39 | } 40 | 41 | // do the actual approval 42 | return token.approve(spender, amount); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /protocol/contracts/lib/balancer/core/BColor.sol: -------------------------------------------------------------------------------- 1 | // This program is free software: you can redistribute it and/or modify 2 | // it under the terms of the GNU General Public License as published by 3 | // the Free Software Foundation, either version 3 of the License, or 4 | // (at your option) any later version. 5 | 6 | // This program is distributed in the hope that it will be useful, 7 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | // GNU General Public License for more details. 10 | 11 | // You should have received a copy of the GNU General Public License 12 | // along with this program. If not, see . 13 | //SPDX-License-Identifier: AGPL-3.0 14 | pragma solidity 0.6.12; 15 | 16 | abstract contract BColor { 17 | function getColor() external view virtual returns (bytes32); 18 | } 19 | 20 | contract BBronze is BColor { 21 | function getColor() external view override returns (bytes32) { 22 | return bytes32("BRONZE"); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /protocol/contracts/lib/balancer/core/BConst.sol: -------------------------------------------------------------------------------- 1 | // This program is free software: you can redistribute it and/or modify 2 | // it under the terms of the GNU General Public License as published by 3 | // the Free Software Foundation, either version 3 of the License, or 4 | // (at your option) any later version. 5 | 6 | // This program is distributed in the hope that it will be useful, 7 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | // GNU General Public License for more details. 10 | 11 | // You should have received a copy of the GNU General Public License 12 | // along with this program. If not, see . 13 | //SPDX-License-Identifier: AGPL-3.0 14 | pragma solidity 0.6.12; 15 | 16 | import "./BColor.sol"; 17 | 18 | contract BConst is BBronze { 19 | uint256 public constant BONE = 10**18; 20 | 21 | uint256 public constant MIN_BOUND_TOKENS = 2; 22 | uint256 public constant MAX_BOUND_TOKENS = 8; 23 | 24 | uint256 public constant MIN_FEE = BONE / 10**6; 25 | uint256 public constant MAX_FEE = BONE / 10; 26 | uint256 public constant EXIT_FEE = 0; 27 | 28 | uint256 public constant MIN_WEIGHT = BONE; 29 | uint256 public constant MAX_WEIGHT = BONE * 50; 30 | uint256 public constant MAX_TOTAL_WEIGHT = BONE * 50; 31 | uint256 public constant MIN_BALANCE = BONE / 10**12; 32 | 33 | uint256 public constant INIT_POOL_SUPPLY = BONE * 100; 34 | 35 | uint256 public constant MIN_BPOW_BASE = 1 wei; 36 | uint256 public constant MAX_BPOW_BASE = (2 * BONE) - 1 wei; 37 | uint256 public constant BPOW_PRECISION = BONE / 10**10; 38 | 39 | uint256 public constant MAX_IN_RATIO = BONE / 2; 40 | uint256 public constant MAX_OUT_RATIO = (BONE / 3) + 1 wei; 41 | } 42 | -------------------------------------------------------------------------------- /protocol/contracts/lib/balancer/core/BFactory.sol: -------------------------------------------------------------------------------- 1 | // This program is free software: you can redistribute it and/or modify 2 | // it under the terms of the GNU General Public License as published by 3 | // the Free Software Foundation, either version 3 of the License, or 4 | // (at your option) any later version. 5 | 6 | // This program is disstributed in the hope that it will be useful, 7 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | // GNU General Public License for more details. 10 | 11 | // You should have received a copy of the GNU General Public License 12 | // along with this program. If not, see . 13 | //SPDX-License-Identifier: AGPL-3.0 14 | pragma solidity 0.6.12; 15 | 16 | // Builds new BPools, logging their addresses and providing `isBPool(address) -> (bool)` 17 | 18 | import "./BPool.sol"; 19 | 20 | contract BFactory is BBronze { 21 | event LOG_NEW_POOL(address indexed caller, address indexed pool); 22 | 23 | event LOG_BLABS(address indexed caller, address indexed blabs); 24 | 25 | mapping(address => bool) private _isBPool; 26 | 27 | function isBPool(address b) external view returns (bool) { 28 | return _isBPool[b]; 29 | } 30 | 31 | function newBPool() external returns (BPool) { 32 | BPool bpool = new BPool(); 33 | _isBPool[address(bpool)] = true; 34 | emit LOG_NEW_POOL(msg.sender, address(bpool)); 35 | bpool.setController(msg.sender); 36 | return bpool; 37 | } 38 | 39 | address private _blabs; 40 | 41 | constructor() public { 42 | _blabs = msg.sender; 43 | } 44 | 45 | function getBLabs() external view returns (address) { 46 | return _blabs; 47 | } 48 | 49 | function setBLabs(address b) external { 50 | require(msg.sender == _blabs, "ERR_NOT_BLABS"); 51 | emit LOG_BLABS(msg.sender, b); 52 | _blabs = b; 53 | } 54 | 55 | function collect(BPool pool) external { 56 | require(msg.sender == _blabs, "ERR_NOT_BLABS"); 57 | uint256 collected = B_IERC20(pool).balanceOf(address(this)); 58 | bool xfer = pool.transfer(_blabs, collected); 59 | require(xfer, "ERR_ERC20_FAILED"); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /protocol/contracts/lib/balancer/core/BNum.sol: -------------------------------------------------------------------------------- 1 | // This program is free software: you can redistribute it and/or modify 2 | // it under the terms of the GNU General Public License as published by 3 | // the Free Software Foundation, either version 3 of the License, or 4 | // (at your option) any later version. 5 | 6 | // This program is distributed in the hope that it will be useful, 7 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | // GNU General Public License for more details. 10 | 11 | // You should have received a copy of the GNU General Public License 12 | // along with this program. If not, see . 13 | //SPDX-License-Identifier: AGPL-3.0 14 | pragma solidity 0.6.12; 15 | 16 | import "./BConst.sol"; 17 | 18 | contract BNum is BConst { 19 | function btoi(uint256 a) internal pure returns (uint256) { 20 | return a / BONE; 21 | } 22 | 23 | function bfloor(uint256 a) internal pure returns (uint256) { 24 | return btoi(a) * BONE; 25 | } 26 | 27 | function badd(uint256 a, uint256 b) internal pure returns (uint256) { 28 | uint256 c = a + b; 29 | require(c >= a, "ERR_ADD_OVERFLOW"); 30 | return c; 31 | } 32 | 33 | function bsub(uint256 a, uint256 b) internal pure returns (uint256) { 34 | (uint256 c, bool flag) = bsubSign(a, b); 35 | require(!flag, "ERR_SUB_UNDERFLOW"); 36 | return c; 37 | } 38 | 39 | function bsubSign(uint256 a, uint256 b) internal pure returns (uint256, bool) { 40 | if (a >= b) { 41 | return (a - b, false); 42 | } else { 43 | return (b - a, true); 44 | } 45 | } 46 | 47 | function bmul(uint256 a, uint256 b) internal pure returns (uint256) { 48 | uint256 c0 = a * b; 49 | require(a == 0 || c0 / a == b, "ERR_MUL_OVERFLOW"); 50 | uint256 c1 = c0 + (BONE / 2); 51 | require(c1 >= c0, "ERR_MUL_OVERFLOW"); 52 | uint256 c2 = c1 / BONE; 53 | return c2; 54 | } 55 | 56 | function bdiv(uint256 a, uint256 b) internal pure returns (uint256) { 57 | require(b != 0, "ERR_DIV_ZERO"); 58 | uint256 c0 = a * BONE; 59 | require(a == 0 || c0 / a == BONE, "ERR_DIV_INTERNAL"); // bmul overflow 60 | uint256 c1 = c0 + (b / 2); 61 | require(c1 >= c0, "ERR_DIV_INTERNAL"); // badd require 62 | uint256 c2 = c1 / b; 63 | return c2; 64 | } 65 | 66 | // DSMath.wpow 67 | function bpowi(uint256 a, uint256 n) internal pure returns (uint256) { 68 | uint256 z = n % 2 != 0 ? a : BONE; 69 | 70 | for (n /= 2; n != 0; n /= 2) { 71 | a = bmul(a, a); 72 | 73 | if (n % 2 != 0) { 74 | z = bmul(z, a); 75 | } 76 | } 77 | return z; 78 | } 79 | 80 | // Compute b^(e.w) by splitting it into (b^e)*(b^0.w). 81 | // Use `bpowi` for `b^e` and `bpowK` for k iterations 82 | // of approximation of b^0.w 83 | function bpow(uint256 base, uint256 exp) internal pure returns (uint256) { 84 | require(base >= MIN_BPOW_BASE, "ERR_BPOW_BASE_TOO_LOW"); 85 | require(base <= MAX_BPOW_BASE, "ERR_BPOW_BASE_TOO_HIGH"); 86 | 87 | uint256 whole = bfloor(exp); 88 | uint256 remain = bsub(exp, whole); 89 | 90 | uint256 wholePow = bpowi(base, btoi(whole)); 91 | 92 | if (remain == 0) { 93 | return wholePow; 94 | } 95 | 96 | uint256 partialResult = bpowApprox(base, remain, BPOW_PRECISION); 97 | return bmul(wholePow, partialResult); 98 | } 99 | 100 | function bpowApprox( 101 | uint256 base, 102 | uint256 exp, 103 | uint256 precision 104 | ) internal pure returns (uint256) { 105 | // term 0: 106 | uint256 a = exp; 107 | (uint256 x, bool xneg) = bsubSign(base, BONE); 108 | uint256 term = BONE; 109 | uint256 sum = term; 110 | bool negative = false; 111 | 112 | // term(k) = numer / denom 113 | // = (product(a - i - 1, i=1-->k) * x^k) / (k!) 114 | // each iteration, multiply previous term by (a-(k-1)) * x / k 115 | // continue until term is less than precision 116 | for (uint256 i = 1; term >= precision; i++) { 117 | uint256 bigK = i * BONE; 118 | (uint256 c, bool cneg) = bsubSign(a, bsub(bigK, BONE)); 119 | term = bmul(term, bmul(c, x)); 120 | term = bdiv(term, bigK); 121 | if (term == 0) break; 122 | 123 | if (xneg) negative = !negative; 124 | if (cneg) negative = !negative; 125 | if (negative) { 126 | sum = bsub(sum, term); 127 | } else { 128 | sum = badd(sum, term); 129 | } 130 | } 131 | 132 | return sum; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /protocol/contracts/mocks/ERC20Mock.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: AGPL-3.0 2 | pragma solidity 0.6.12; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | import "@openzeppelin/contracts/access/Ownable.sol"; 7 | 8 | contract ERC20Mock is ERC20, Ownable { 9 | constructor( 10 | string memory name, 11 | string memory symbol, 12 | uint8 decimals 13 | ) public ERC20(name, symbol) { 14 | _setupDecimals(decimals); 15 | } 16 | 17 | /// @dev Mint new tokens if the contract owner 18 | /// @param account address of account to mint tokens to 19 | /// @param amount amount of tokens to mint 20 | function mint(address account, uint256 amount) public onlyOwner { 21 | _mint(account, amount); 22 | } 23 | 24 | /// @dev Burn tokens if the contract owner 25 | /// @param account address of account to burn tokens from 26 | /// @param amount amount of tokens to burn 27 | function burn(address account, uint256 amount) public onlyOwner { 28 | _burn(account, amount); 29 | } 30 | 31 | /// @dev Burn all tokens of the account 32 | /// @param account address of account to burn tokens from 33 | function burnAll(address account) public onlyOwner { 34 | _burn(account, balanceOf(account)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /protocol/contracts/mocks/PriceOracleMock.sol: -------------------------------------------------------------------------------- 1 | // //SPDX-License-Identifier: AGPL-3.0 2 | pragma solidity 0.6.12; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "../interfaces/PriceOracle.sol"; 6 | 7 | contract PriceOracleMock is PriceOracle { 8 | constructor() public {} 9 | 10 | uint256 private price = 10**18; 11 | 12 | function getPrice(address token) external view override returns (uint256) { 13 | // make compiler happy. 14 | token; 15 | return price; 16 | } 17 | 18 | function setPrice(uint256 _price) public { 19 | price = _price; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /protocol/contracts/mocks/SplitVaultMock.sol: -------------------------------------------------------------------------------- 1 | // //SPDX-License-Identifier: AGPL-3.0 2 | pragma solidity 0.6.12; 3 | pragma experimental ABIEncoderV2; 4 | 5 | contract SplitVaultMock { 6 | constructor() public {} 7 | 8 | struct PayoutCall { 9 | uint256 amount; 10 | address tokenAddress; 11 | address recipient; 12 | } 13 | 14 | PayoutCall[] public payoutCalls; 15 | 16 | function payout( 17 | uint256 amount, 18 | address tokenAddress, 19 | address recipient 20 | ) public { 21 | payoutCalls.push(PayoutCall({ amount: amount, tokenAddress: tokenAddress, recipient: recipient })); 22 | } 23 | 24 | function getPayoutCalls() public view returns (PayoutCall[] memory) { 25 | return payoutCalls; 26 | } 27 | 28 | function reset() public { 29 | delete payoutCalls; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /protocol/deployments/index.ts: -------------------------------------------------------------------------------- 1 | import { Deployments } from "./types"; 2 | 3 | export const deployments: Deployments = { 4 | mainnet: { 5 | compAddress: "0xc00e94Cb662C3520282E6f5717214004A7f26888", 6 | usdcAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 7 | // TODO: Deploy 8 | splitPoolFactoryAddress: "", 9 | balancerPoolFactoryAddress: "0x9424B1412450D0f8Fc2255FAf6046b98213B76Bd", 10 | balancerSmartPoolFactoryAddress: "0xed52D8E202401645eDAD1c0AA21e872498ce47D0", 11 | splitVaultAddress: "0x8e31d1F69Cd5185527517F6fAc8A43edd24C93D7", 12 | priceOracleAddress: "0x09d75570c572d9D0193CAc4D4F4213a1D3c8A5bd", 13 | componentSets: { 14 | // cETH 15 | "0x4ddc2d193948926d02f9b1fe9e1daa0718270ed5": { 16 | capitalComponentTokenAddress: "0x704bD80bDdAB309fBA02736bD898Bc6a69588C63", 17 | yieldComponentTokenAddress: "0x3d9Ba05f737b9e84e90a163216E586FBBD5e48Ff", 18 | }, 19 | // cBAT 20 | "0x6c8c6b02e7b2be14d4fa6022dfd6d75921d90e4e": { 21 | capitalComponentTokenAddress: "0x15bD9367dA17D534870D958843a1d828B5Ba2414", 22 | yieldComponentTokenAddress: "0xE8B8d22B96eda2aCf98C71085375600B20bF822f", 23 | }, 24 | // cUNI 25 | "0x35a18000230da775cac24873d00ff85bccded550": { 26 | capitalComponentTokenAddress: "0xEacAA5fbE3C364fcAcEd6cD14CEd4E54703cE5D4", 27 | yieldComponentTokenAddress: "0x6b12838a3128F590887e956638e212Dd1306c3EA", 28 | }, 29 | }, 30 | splitPools: {}, 31 | libraries: { 32 | BalancerSafeMath: "0xCfE28868F6E0A24b7333D22D8943279e76aC2cdc", 33 | RightsManager: "0x0F811b1AF2B6B447B008eFF31eCceeE5A0b1d842", 34 | SmartPoolManager: "0xA3F9145CB0B50D907930840BB2dcfF4146df8Ab4", 35 | }, 36 | }, 37 | rinkeby: { 38 | // HACK: there is no COMP on Rinkeby, so using WETH instead. 39 | compAddress: "0xc778417e063141139fce010982780140aa0cd5ab", 40 | usdcAddress: "0x4DBCdF9B62e891a7cec5A2568C3F4FAF9E8Abe2b", 41 | splitPoolFactoryAddress: "0x208426c2B281aC34f7005A03f638F43C6489505e", 42 | balancerPoolFactoryAddress: "0x9C84391B443ea3a48788079a5f98e2EaD55c9309", 43 | balancerSmartPoolFactoryAddress: "0xA3F9145CB0B50D907930840BB2dcfF4146df8Ab4", 44 | splitVaultAddress: "0x17B9f2f7DE226eC18E77FEDB2c741d1B0D851bdA", 45 | priceOracleAddress: "0x9D8693092361ECCCdD30Ea2875a6054DF80D2472", 46 | componentSets: { 47 | // cBAT 48 | "0xebf1a11532b93a529b5bc942b4baa98647913002": { 49 | capitalComponentTokenAddress: "0xB5A363330c2442D3E90cE633a8C39C444D03F90E", 50 | yieldComponentTokenAddress: "0x69EF69397E9390f8dC690BE0AF01d9b9C38DeEB8", 51 | }, 52 | // cZRX 53 | "0x52201ff1720134bbbbb2f6bc97bf3715490ec19b": { 54 | capitalComponentTokenAddress: "0x14375f2c717432a8692c13d7bea2534723d4ECC4", 55 | yieldComponentTokenAddress: "0xaEFa8E3b59333227633CbF7c9a5dF6a24F1f6a05", 56 | }, 57 | // cETH 58 | "0xd6801a1dffcd0a410336ef88def4320d6df1883e": { 59 | capitalComponentTokenAddress: "0x75271A98EE3AA43482dF3c92ea4e4cEFFab53D8f", 60 | yieldComponentTokenAddress: "0xa3C203CB85DcaD41C6eaa2572DC131Dc24db1bCa", 61 | }, 62 | // cUSDC 63 | "0x5b281a6dda0b271e91ae35de655ad301c976edb1": { 64 | capitalComponentTokenAddress: "0x71d2beCA71a141adAeb7B5AD62680BAeB22e2e42", 65 | yieldComponentTokenAddress: "0x7220eD76f750554f4384600BC4C66fe808502F3b", 66 | }, 67 | }, 68 | splitPools: { 69 | // cETH 70 | "0xd6801a1dffcd0a410336ef88def4320d6df1883e": { 71 | poolAddress: "0x5CDE85734A2A07D3Eb59c8005D9035cf3C94b337", 72 | }, 73 | }, 74 | libraries: { 75 | BalancerSafeMath: "0x0F811b1AF2B6B447B008eFF31eCceeE5A0b1d842", 76 | RightsManager: "0x4aCB6685da2B5FcB29b1614E71825CE67464440b", 77 | SmartPoolManager: "0xb3a3f6826281525dd57f7BA837235E4Fa71C6248", 78 | }, 79 | }, 80 | // TODO: deploy to kovan 81 | }; 82 | -------------------------------------------------------------------------------- /protocol/deployments/types.ts: -------------------------------------------------------------------------------- 1 | export interface ComponentSet { 2 | capitalComponentTokenAddress: string; 3 | yieldComponentTokenAddress: string; 4 | } 5 | 6 | export interface SplitPool { 7 | poolAddress: string; 8 | } 9 | 10 | export type ComponentSets = { [fullToken: string]: ComponentSet }; 11 | export type SplitPools = { [fullToken: string]: SplitPool }; 12 | export interface Libraries { 13 | // name -> address 14 | [libraryName: string]: string; 15 | } 16 | 17 | export interface Deployment { 18 | balancerSmartPoolFactoryAddress: string; 19 | balancerPoolFactoryAddress: string; 20 | splitVaultAddress: string; 21 | priceOracleAddress: string; 22 | componentSets: ComponentSets; 23 | splitPoolFactoryAddress: string; 24 | usdcAddress: string; 25 | compAddress: string; 26 | splitPools: SplitPools; 27 | libraries: Libraries; 28 | } 29 | 30 | export type Deployments = { [network: string]: Deployment }; 31 | -------------------------------------------------------------------------------- /protocol/hardhat.config.ts: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | import { HardhatUserConfig } from "hardhat/config"; 3 | 4 | import "@nomiclabs/hardhat-waffle"; 5 | import "hardhat-typechain"; 6 | import "@nomiclabs/hardhat-etherscan"; 7 | 8 | import "./tasks"; 9 | 10 | const config = { 11 | solidity: { 12 | version: "0.6.12", 13 | settings: { 14 | optimizer: { 15 | enabled: true, 16 | }, 17 | }, 18 | }, 19 | typechain: { 20 | outDir: "typechain", 21 | target: "ethers-v5", 22 | }, 23 | networks: {}, 24 | etherscan: {}, 25 | }; 26 | 27 | const { 28 | ETH_RPC_URL_RINKEBY, 29 | PRIVATE_KEY_RINKEBY, 30 | ETHERSCAN_API_KEY, 31 | PRIVATE_KEY_MAINNET, 32 | ETH_RPC_URL_MAINNET, 33 | } = process.env; 34 | 35 | if (ETH_RPC_URL_RINKEBY && PRIVATE_KEY_RINKEBY) { 36 | (config.networks as any).rinkeby = { 37 | url: ETH_RPC_URL_RINKEBY, 38 | accounts: [PRIVATE_KEY_RINKEBY], 39 | }; 40 | } 41 | 42 | if (ETH_RPC_URL_MAINNET && PRIVATE_KEY_MAINNET) { 43 | (config.networks as any).mainnet = { 44 | url: ETH_RPC_URL_MAINNET, 45 | accounts: [PRIVATE_KEY_MAINNET], 46 | }; 47 | } 48 | 49 | if (ETHERSCAN_API_KEY) { 50 | (config.etherscan as any).apiKey = ETHERSCAN_API_KEY; 51 | } 52 | 53 | export default config as HardhatUserConfig; 54 | -------------------------------------------------------------------------------- /protocol/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./typechain/index"; 2 | export * from "./deployments/index"; 3 | export * from "./deployments/types"; 4 | -------------------------------------------------------------------------------- /protocol/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "split-contracts", 3 | "version": "1.0.0", 4 | "main": "index.ts", 5 | "author": "francesco@fragosti.com", 6 | "license": "AGPL-3.0", 7 | "devDependencies": { 8 | "@nomiclabs/hardhat-ethers": "^2.0.0", 9 | "@nomiclabs/hardhat-etherscan": "^2.0.1", 10 | "@nomiclabs/hardhat-waffle": "^2.0.0", 11 | "@openzeppelin/contracts": "^3.2.0", 12 | "@typechain/ethers-v5": "^4.0.0", 13 | "@types/chai": "^4.2.13", 14 | "@types/mocha": "^8.0.3", 15 | "@types/node": "^14.11.5", 16 | "axios": "^0.21.0", 17 | "chai": "^4.2.0", 18 | "dotenv": "^8.2.0", 19 | "ethereum-waffle": "^3.0.0", 20 | "ethers": "^5.0.0", 21 | "hardhat": "^2.0.5", 22 | "hardhat-typechain": "^0.3.3", 23 | "ts-generator": "^0.1.1", 24 | "ts-node": "^9.0.0", 25 | "typechain": "^4.0.0", 26 | "typescript": "^4.1.2" 27 | }, 28 | "scripts": { 29 | "clean": "hardhat clean", 30 | "deploy:vault": "hardhat deploy_vault", 31 | "deploy:oracle": "hardhat deploy_oracle", 32 | "deploy:components": "hardhat deploy_component_tokens", 33 | "deploy:pool_factory": "hardhat deploy_pool_factory", 34 | "deploy:split_pool": "hardhat deploy_split_pool", 35 | "add:component_set": "hardhat add_component_set", 36 | "mint:test_token": "hardhat mint_test_token", 37 | "swap:tokens": "hardhat swap_tokens", 38 | "verify:contract": "hardhat verify", 39 | "build": "hardhat compile", 40 | "test": "hardhat test" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /protocol/tasks/add_component_set.ts: -------------------------------------------------------------------------------- 1 | import { task } from "hardhat/config"; 2 | 3 | import { deployments } from "../deployments"; 4 | 5 | interface Args { 6 | cTokenAddress: string; 7 | splitVaultAddress?: string; 8 | capitalComponentTokenAddress?: string; 9 | yieldComponentTokenAddress?: string; 10 | } 11 | 12 | task("add_component_set", "adds a component set to the SplitVault") 13 | .addParam("cTokenAddress", "address of the cToken") 14 | .addOptionalParam("splitVaultAddress", "address of the SplitVault") 15 | .addOptionalParam("capitalComponentTokenAddress", "address of the deployed capital component token") 16 | .addOptionalParam("yieldComponentTokenAddress", "address of the deployed yield component token") 17 | .setAction(async (args: Args, bre) => { 18 | await bre.run("compile"); 19 | const deployment = deployments[bre.network.name]; 20 | const componentSet = deployment.componentSets[args.cTokenAddress] || {}; 21 | const { splitVaultAddress, cTokenAddress, capitalComponentTokenAddress, yieldComponentTokenAddress } = { 22 | ...args, 23 | splitVaultAddress: args.splitVaultAddress || deployment.splitVaultAddress, 24 | capitalComponentTokenAddress: args.capitalComponentTokenAddress || componentSet.capitalComponentTokenAddress, 25 | yieldComponentTokenAddress: args.yieldComponentTokenAddress || componentSet.yieldComponentTokenAddress, 26 | }; 27 | if (!capitalComponentTokenAddress || !yieldComponentTokenAddress) { 28 | console.warn( 29 | "Could not find values for capitalComponentTokenAddress or yieldComponentTokenAddress. Add to deployments or provide as parameters.", 30 | ); 31 | return; 32 | } 33 | 34 | console.log("SplitVault address used: ", splitVaultAddress); 35 | console.log("cTokenAddress address used: ", cTokenAddress); 36 | console.log("YieldComponentToken address used: ", yieldComponentTokenAddress); 37 | console.log("CapitalComponentToken address used: ", capitalComponentTokenAddress); 38 | 39 | const SplitVaultFactory = await bre.ethers.getContractFactory("SplitVault"); 40 | const splitVault = SplitVaultFactory.attach(splitVaultAddress); 41 | 42 | const txn = await splitVault.add(cTokenAddress, yieldComponentTokenAddress, capitalComponentTokenAddress); 43 | 44 | console.log("Added ComponentSet to SplitVault. Txn:", txn.hash); 45 | }); 46 | -------------------------------------------------------------------------------- /protocol/tasks/deploy_component_tokens.ts: -------------------------------------------------------------------------------- 1 | import { task } from "hardhat/config"; 2 | 3 | import { deployments } from "../deployments"; 4 | 5 | interface Args { 6 | cTokenAddress: string; 7 | splitVaultAddress?: string; 8 | priceOracleAddress?: string; 9 | name?: string; 10 | symbol?: string; 11 | } 12 | 13 | task("deploy_component_tokens", "deploys a pair of component tokens for a cToken") 14 | .addParam("cTokenAddress", "address of the cToken") 15 | .addOptionalParam("splitVaultAddress", "address of the deployed splitVault") 16 | .addOptionalParam("priceOracleAddress", "address of the cToken price oracle") 17 | .addOptionalParam("name", "name of the cToken") 18 | .addOptionalParam("symbol", "symbol of the cToken") 19 | .setAction(async (args: Args, bre) => { 20 | await bre.run("compile"); 21 | const deployment = deployments[bre.network.name]; 22 | const { cTokenAddress, splitVaultAddress, priceOracleAddress, name, symbol } = { 23 | ...args, 24 | splitVaultAddress: args.splitVaultAddress || deployment.splitVaultAddress, 25 | priceOracleAddress: args.priceOracleAddress || deployment.priceOracleAddress, 26 | }; 27 | if (!priceOracleAddress || !splitVaultAddress) { 28 | console.warn( 29 | `Could not find deployments of PriceOracle or SplitVault on ${bre.network.name}, so must provide optional parameters`, 30 | ); 31 | return; 32 | } 33 | 34 | console.log("Oracle address used: ", priceOracleAddress); 35 | console.log("SplitVault address used: ", splitVaultAddress); 36 | 37 | const Erc20Factory = await bre.ethers.getContractFactory("ERC20"); 38 | const erc20 = Erc20Factory.attach(cTokenAddress); 39 | let cTokenName = name; 40 | let cTokenSymbol = symbol; 41 | if (!cTokenName || !cTokenSymbol) { 42 | try { 43 | cTokenName = await erc20.name(); 44 | cTokenSymbol = await erc20.symbol(); 45 | console.log(`Token name and symbol not provided, so using ${cTokenName}: ${cTokenSymbol}`); 46 | } catch { 47 | console.warn("Failed to fetch name and symbol from ERC20 contract. Must provide optional parameters."); 48 | return; 49 | } 50 | } 51 | 52 | const CapitalComponentTokenFactory = await bre.ethers.getContractFactory("CapitalComponentToken"); 53 | const capitalComponentToken = await CapitalComponentTokenFactory.deploy( 54 | `Capital ${cTokenName}`, 55 | `c${cTokenSymbol}`, 56 | cTokenAddress, 57 | priceOracleAddress, 58 | splitVaultAddress, 59 | ); 60 | await capitalComponentToken.deployed(); 61 | 62 | const YieldComponentTokenFactory = await bre.ethers.getContractFactory("YieldComponentToken"); 63 | const yieldComponentToken = await YieldComponentTokenFactory.deploy( 64 | `Yield ${cTokenName}`, 65 | `y${cTokenSymbol}`, 66 | cTokenAddress, 67 | priceOracleAddress, 68 | splitVaultAddress, 69 | ); 70 | await yieldComponentToken.deployed(); 71 | 72 | console.log(`Used ${cTokenName}: ${cTokenSymbol} as full token`); 73 | console.log("CapitalComponentToken deployed to: ", capitalComponentToken.address); 74 | console.log("YieldComponentToken deployed to :", yieldComponentToken.address); 75 | }); 76 | -------------------------------------------------------------------------------- /protocol/tasks/deploy_oracle.ts: -------------------------------------------------------------------------------- 1 | import { task } from "hardhat/config"; 2 | 3 | task("deploy_oracle", "deploys the CTokenPriceOracle", async (_args, bre) => { 4 | await bre.run("compile"); 5 | 6 | const CTokenPriceOracle = await bre.ethers.getContractFactory("CTokenPriceOracle"); 7 | const priceOracle = await CTokenPriceOracle.deploy(); 8 | 9 | await priceOracle.deployed(); 10 | console.log("CTokenPriceOracle deployed to:", priceOracle.address); 11 | }); 12 | -------------------------------------------------------------------------------- /protocol/tasks/deploy_pool_factory.ts: -------------------------------------------------------------------------------- 1 | import { task } from "hardhat/config"; 2 | 3 | import { deployments } from "../deployments"; 4 | 5 | task("deploy_pool_factory", "deploys a SplitPoolFactory", async (_args, bre) => { 6 | await bre.run("compile"); 7 | const deployment = deployments[bre.network.name]; 8 | const { balancerSmartPoolFactoryAddress: balancerSmartPoolFactory } = deployment; 9 | const SplitPoolFactory = await bre.ethers.getContractFactory("SplitPoolFactory"); 10 | const splitPoolFactory = await SplitPoolFactory.deploy(balancerSmartPoolFactory); 11 | 12 | await splitPoolFactory.deployed(); 13 | console.log("SplitPoolFactory deployed to:", splitPoolFactory.address); 14 | }); 15 | -------------------------------------------------------------------------------- /protocol/tasks/deploy_vault.ts: -------------------------------------------------------------------------------- 1 | import { task } from "hardhat/config"; 2 | 3 | task("deploy_vault", "deploys the SplitVault", async (_args, bre) => { 4 | await bre.run("compile"); 5 | const SplitVault = await bre.ethers.getContractFactory("SplitVault"); 6 | const splitVault = await SplitVault.deploy(); 7 | 8 | await splitVault.deployed(); 9 | console.log("SplitVault deployed to:", splitVault.address); 10 | }); 11 | -------------------------------------------------------------------------------- /protocol/tasks/index.ts: -------------------------------------------------------------------------------- 1 | import "./deploy_vault"; 2 | import "./deploy_oracle"; 3 | import "./deploy_component_tokens"; 4 | import "./add_component_set"; 5 | import "./mint_test_token"; 6 | import "./deploy_pool_factory"; 7 | import "./deploy_split_pool"; 8 | import "./swap_tokens"; 9 | -------------------------------------------------------------------------------- /protocol/tasks/mint_test_token.ts: -------------------------------------------------------------------------------- 1 | import { task } from "hardhat/config"; 2 | 3 | type TokenAddresses = { [symbol: string]: string }; 4 | 5 | interface Args { 6 | tokenSymbol: string; 7 | allocateTo?: string; 8 | amount?: string; 9 | } 10 | 11 | const networkToToken: { [network: string]: TokenAddresses } = { 12 | rinkeby: { 13 | USDC: "0x4DBCdF9B62e891a7cec5A2568C3F4FAF9E8Abe2b", 14 | BAT: "0xbF7A7169562078c96f0eC1A8aFD6aE50f12e5A99", 15 | ZRX: "0xddea378A6dDC8AfeC82C36E9b0078826bf9e68B6", 16 | }, 17 | }; 18 | 19 | const abi = [ 20 | { 21 | constant: false, 22 | inputs: [ 23 | { 24 | name: "_owner", 25 | type: "address", 26 | }, 27 | { 28 | name: "value", 29 | type: "uint256", 30 | }, 31 | ], 32 | name: "allocateTo", 33 | outputs: [], 34 | payable: false, 35 | stateMutability: "nonpayable", 36 | type: "function", 37 | signature: "0x08bca566", 38 | }, 39 | ]; 40 | 41 | task("mint_test_token", "mints tokens on testnets") 42 | .addParam("tokenSymbol", "The token to mint") 43 | .addOptionalParam("allocateTo", "The address to allocate the tokens to") 44 | .addOptionalParam("amount", "How much to mint") 45 | .setAction(async (args: Args, bre) => { 46 | const tokens = networkToToken[bre.network.name]; 47 | if (!tokens) { 48 | console.warn(`Could not find token addresses for network ${bre.network}`); 49 | } 50 | const address = tokens[args.tokenSymbol]; 51 | if (!address) { 52 | console.warn(`Could not find token address for token symbol ${args.tokenSymbol}`); 53 | } 54 | const [signer] = await bre.ethers.getSigners(); 55 | const faucet = new bre.ethers.Contract(address, abi, signer); 56 | const txn = await faucet.allocateTo(args.allocateTo || (await signer.getAddress()), args.amount || "100000000000"); 57 | console.log(`Allocation tx: ${txn.hash}`); 58 | }); 59 | -------------------------------------------------------------------------------- /protocol/tasks/swap_tokens.ts: -------------------------------------------------------------------------------- 1 | import { task } from "hardhat/config"; 2 | 3 | import { deployments } from "../deployments"; 4 | 5 | interface Args { 6 | cTokenAddress: string; 7 | tokenInAddress: string; 8 | tokenOutAddress: string; 9 | tokenInAmount: string; 10 | } 11 | 12 | task("swap_tokens", "swap tokens using a balancer pool") 13 | .addParam("cTokenAddress", "address of the cToken (to find the corresponding pool)") 14 | .addParam("tokenOutAddress", "address of the token to sell") 15 | .addParam("tokenInAddress", "address of the token to buy") 16 | .addParam("tokenInAmount", "amount of token to sell") 17 | .setAction(async (args: Args, bre) => { 18 | await bre.run("compile"); 19 | const deployment = deployments[bre.network.name]; 20 | const { poolAddress } = deployment.splitPools[args.cTokenAddress]; 21 | 22 | const ConfigurableRightsPoolFactory = await bre.ethers.getContractFactory("ConfigurableRightsPool", { 23 | libraries: deployment.libraries, 24 | }); 25 | const configurableRightsPool = ConfigurableRightsPoolFactory.attach(poolAddress); 26 | 27 | const ERC20Factory = await bre.ethers.getContractFactory("ERC20"); 28 | const erc20 = ERC20Factory.attach(args.tokenInAddress); 29 | 30 | const bPoolAddress = await configurableRightsPool.bPool(); 31 | await erc20.approve(bPoolAddress, bre.ethers.constants.MaxUint256); 32 | 33 | const BPoolFactory = await bre.ethers.getContractFactory("BPool"); 34 | const bPool = BPoolFactory.attach(bPoolAddress); 35 | 36 | const txn = await bPool.swapExactAmountIn( 37 | args.tokenInAddress, 38 | args.tokenInAmount, 39 | args.tokenOutAddress, 40 | "0", 41 | bre.ethers.constants.MaxUint256, 42 | { 43 | gasLimit: "10000000", 44 | }, 45 | ); 46 | console.log(`Swapping ${args.tokenInAmount} ${args.tokenInAddress} for ${args.tokenOutAddress}. Txn: ${txn.hash}`); 47 | }); 48 | -------------------------------------------------------------------------------- /protocol/test/constants.ts: -------------------------------------------------------------------------------- 1 | export const NULL_ADDRESS = "0x0000000000000000000000000000000000000000"; 2 | export const ACCOUNT_1 = "0xc783df8a850f42e7F7e57013759C285caa701eB6"; 3 | export const WAD = "1000000000000000000"; 4 | export const C_TOKEN_DECIMALS = 8; 5 | -------------------------------------------------------------------------------- /protocol/test/types.ts: -------------------------------------------------------------------------------- 1 | export interface ComponentTokenDependencyAddresses { 2 | fullTokenAddress: string; 3 | oracleAddress: string; 4 | splitVaultAddress: string; 5 | } 6 | -------------------------------------------------------------------------------- /protocol/test/utils.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | 3 | import { SplitVault } from "../typechain/SplitVault"; 4 | import { PriceOracleMock } from "../typechain/PriceOracleMock"; 5 | import { ERC20Mock } from "../typechain/ERC20Mock"; 6 | import { CapitalComponentToken } from "../typechain/CapitalComponentToken"; 7 | import { YieldComponentToken } from "../typechain/YieldComponentToken"; 8 | 9 | import { ComponentTokenDependencyAddresses } from "./types"; 10 | 11 | export const getDeployedCapitalComponentToken = async ( 12 | name: string, 13 | symbol: string, 14 | addresses: ComponentTokenDependencyAddresses, 15 | ) => { 16 | const CapitalComponentTokenFactory = await ethers.getContractFactory("CapitalComponentToken"); 17 | const capitalComponentToken = (await CapitalComponentTokenFactory.deploy( 18 | name, 19 | symbol, 20 | addresses.fullTokenAddress, 21 | addresses.oracleAddress, 22 | addresses.splitVaultAddress, 23 | )) as CapitalComponentToken; 24 | await capitalComponentToken.deployed(); 25 | return capitalComponentToken; 26 | }; 27 | 28 | export const getYieldName = (name: string) => { 29 | return `${name} Yield Component`; 30 | }; 31 | 32 | export const getYieldSymbol = (symbol: string) => { 33 | return `yc${symbol}`; 34 | }; 35 | 36 | export const getDeployedYieldComponentToken = async ( 37 | name: string, 38 | symbol: string, 39 | addresses: ComponentTokenDependencyAddresses, 40 | ) => { 41 | const YieldComponentTokenFactory = await ethers.getContractFactory("YieldComponentToken"); 42 | const yieldComponentToken = (await YieldComponentTokenFactory.deploy( 43 | getYieldName(name), 44 | getYieldSymbol(symbol), 45 | addresses.fullTokenAddress, 46 | addresses.oracleAddress, 47 | addresses.splitVaultAddress, 48 | )) as YieldComponentToken; 49 | 50 | await yieldComponentToken.deployed(); 51 | 52 | return yieldComponentToken; 53 | }; 54 | 55 | export interface SplitVaultDependencyAddresses { 56 | fullTokenAddress: string; 57 | oracleAddress: string; 58 | } 59 | 60 | export interface VaultAndComponentTokens { 61 | splitVault: SplitVault; 62 | yieldComponentToken: YieldComponentToken; 63 | capitalComponentToken: CapitalComponentToken; 64 | } 65 | 66 | export const getDeployedContracts = async ( 67 | addresses: SplitVaultDependencyAddresses, 68 | ): Promise => { 69 | const SplitVaultFactory = await ethers.getContractFactory("SplitVault"); 70 | const splitVault = (await SplitVaultFactory.deploy()) as SplitVault; 71 | await splitVault.deployed(); 72 | const deployedAddresses: ComponentTokenDependencyAddresses = { 73 | ...addresses, 74 | splitVaultAddress: splitVault.address, 75 | }; 76 | const name = "X Token"; 77 | const symbol = "XXX"; 78 | const yieldComponentToken = await getDeployedYieldComponentToken(name, symbol, deployedAddresses); 79 | const capitalComponentToken = await getDeployedCapitalComponentToken(name, symbol, deployedAddresses); 80 | return { 81 | splitVault, 82 | yieldComponentToken, 83 | capitalComponentToken, 84 | }; 85 | }; 86 | 87 | export const getPriceOracle = async () => { 88 | const PriceOracleMockFactory = await ethers.getContractFactory("PriceOracleMock"); 89 | const priceOracle = (await PriceOracleMockFactory.deploy()) as PriceOracleMock; 90 | await priceOracle.deployed(); 91 | return priceOracle; 92 | }; 93 | 94 | export const getErc20 = async (decimals: number) => { 95 | const Erc20Factory = await ethers.getContractFactory("ERC20Mock"); 96 | const erc20 = (await Erc20Factory.deploy("X Token", "XXX", decimals)) as ERC20Mock; 97 | await erc20.deployed(); 98 | return erc20; 99 | }; 100 | -------------------------------------------------------------------------------- /protocol/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "target": "es2018", 5 | "module": "commonjs", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "outDir": "dist" 9 | }, 10 | "include": ["./scripts", "./test"], 11 | "files": ["./hardhat.config.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /split-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/split-fi/split/39b13ef038fbc1cdf03bce7fc646fd6c0d02ce40/split-banner.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitReturns": true, 4 | "pretty": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | // These settings are required for TypeScript project references 8 | "declaration": true, 9 | "declarationMap": true, 10 | "sourceMap": true 11 | }, 12 | "references": [{ "path:": "./protocol" }, { "path": "./interface" }] 13 | } 14 | --------------------------------------------------------------------------------
© SPLIT 2020 UNAUDITED BETA – Mainnet, Rinkeby