├── .solhintignore ├── .npmignore ├── .eslintignore ├── .solcover.js ├── .mocharc.json ├── .prettierignore ├── .husky └── pre-commit ├── web3 ├── public │ ├── favicon.ico │ ├── yieldbox.png │ └── background.png ├── tsconfig.node.json ├── vite.config.ts ├── data-web3.ts ├── env.d.ts ├── index.html ├── components │ ├── USDAmount.vue │ ├── TokenAmount.vue │ ├── ExplorerAddress.vue │ ├── AddressInput.vue │ ├── Web3Button.vue │ ├── TokenAmountInput.vue │ ├── Menu.vue │ ├── YieldBoxModal.vue │ └── Web3Modal.vue ├── tsconfig.json ├── classes │ ├── types │ │ ├── index.ts │ │ ├── common.ts │ │ ├── factories │ │ │ ├── IGnosisSafe__factory.ts │ │ │ ├── IUniswapV2Factory__factory.ts │ │ │ └── IERC20__factory.ts │ │ └── IGnosisSafe.ts │ ├── CoinGeckoAPI.ts │ ├── YieldBox.ts │ └── Account.ts ├── App.vue ├── pages │ ├── Swap.vue │ ├── Escrow.vue │ ├── Lending.vue │ ├── Tokenizer.vue │ └── YieldBoxBalances.vue └── main.ts ├── .vscode └── settings.json ├── workbench ├── public │ └── favicon.ico ├── tsconfig.node.json ├── vite.config.ts ├── env.d.ts ├── index.html ├── tsconfig.json ├── App.vue ├── pages │ ├── Home.vue │ ├── Address.vue │ └── Block.vue ├── components │ ├── AddressLink.vue │ ├── Ago.vue │ └── Menu.vue ├── classes │ ├── FactoryInterface.ts │ └── HardhatProvider.ts ├── data-workbench.ts └── main.ts ├── sdk ├── index.ts ├── Network.ts └── NetworkConnector.ts ├── .env.example ├── .solhint.json ├── contracts ├── interfaces │ ├── IWrappedNative.sol │ ├── IYieldBox.sol │ └── IStrategy.sol ├── mocks │ ├── ExternalFunctionMock.sol │ ├── ERC20Mock.sol │ ├── MaliciousMasterContractMock.sol │ ├── MasterContractMock.sol │ ├── ERC1155Mock.sol │ ├── YieldBoxRebaseMock.sol │ ├── WETH9Mock.sol │ ├── RevertingERC20Mock.sol │ ├── SushiBarMock.sol │ ├── MasterContractFullCycleMock.sol │ ├── ERC1155ReceiverMock.sol │ ├── ERC1155StrategyMock.sol │ ├── ERC20StrategyMock.sol │ └── ReturnFalseERC20Mock.sol ├── samples │ ├── YieldApp.sol │ ├── Tokenizer.sol │ ├── Escrow.sol │ ├── helloworld.sol │ ├── lending │ │ ├── ISwapper.sol │ │ └── IOracle.sol │ └── YieldSwap.sol ├── ERC721TokenReceiver.sol ├── enums │ └── YieldBoxTokenType.sol ├── ERC1155TokenReceiver.sol ├── BoringMath.sol ├── strategies │ ├── ERC20WithoutStrategy.sol │ ├── SushiStakingSimpleStrategy.sol │ ├── SushiStakingBufferStrategy.sol │ ├── BaseStrategy.sol │ └── BaseBufferStrategy.sol ├── YieldBoxRebase.sol ├── AssetRegister.sol ├── ERC1155.sol └── YieldBoxURIBuilder.sol ├── tsconfig.json ├── .gitignore ├── .eslintrc.js ├── deploy ├── Salary.ts └── YieldBox.ts ├── scripts └── deploy.ts ├── test ├── ERC1155TokenReceiver.ts ├── samples │ └── helloworld.ts ├── AssetRegister.ts ├── YieldBoxRebase.ts └── YieldBoxURIBuilder.ts ├── package.json ├── README.md ├── .prettierrc.js └── hardhat.config.ts /.solhintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | hardhat.config.ts 2 | scripts 3 | test 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | artifacts 3 | cache 4 | coverage 5 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | skipFiles: ["mocks/", "interfaces/"], 3 | } 4 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": "ts-node/register/files", 3 | "timeout": 20000 4 | } 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | artifacts 3 | cache 4 | coverage* 5 | gasReporterOutput.json 6 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx pretty-quick --staged 5 | -------------------------------------------------------------------------------- /web3/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boringcrypto/YieldBox/HEAD/web3/public/favicon.ico -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "solidity.compileUsingRemoteVersion": "v0.8.9+commit.e5eed63a" 3 | } 4 | -------------------------------------------------------------------------------- /web3/public/yieldbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boringcrypto/YieldBox/HEAD/web3/public/yieldbox.png -------------------------------------------------------------------------------- /web3/public/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boringcrypto/YieldBox/HEAD/web3/public/background.png -------------------------------------------------------------------------------- /workbench/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boringcrypto/YieldBox/HEAD/workbench/public/favicon.ico -------------------------------------------------------------------------------- /sdk/index.ts: -------------------------------------------------------------------------------- 1 | export enum TokenType { 2 | Native = 0, 3 | ERC20 = 1, 4 | ERC721 = 2, 5 | ERC1155 = 3, 6 | } 7 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | ALCHEMY_API_KEY= 2 | HARDHAT_NETWORK=hardhat 3 | HARDHAT_MAX_MEMORY=4096 4 | HARDHAT_SHOW_STACK_TRACES=true 5 | HARDHAT_VERBOSE=true -------------------------------------------------------------------------------- /web3/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "compiler-version": ["error", "^0.8.0"], 5 | "func-visibility": ["warn", { "ignoreConstructors": true }] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /workbench/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /web3/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite" 2 | import vue from "@vitejs/plugin-vue" 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | root: "web3", 7 | plugins: [vue()], 8 | }) 9 | -------------------------------------------------------------------------------- /workbench/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite" 2 | import vue from "@vitejs/plugin-vue" 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | root: "workbench", 7 | plugins: [vue()], 8 | server: { 9 | port: 2504, 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /web3/data-web3.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "vue" 2 | import Web3 from "../sdk/Web3" 3 | import { Account } from "./classes/Account" 4 | 5 | export default reactive({ 6 | title: "YieldBox", 7 | name: "YieldBox", 8 | web3: new Web3(), 9 | account: null as Account | null, 10 | }) 11 | -------------------------------------------------------------------------------- /contracts/interfaces/IWrappedNative.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.9; 3 | import "@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol"; 4 | 5 | interface IWrappedNative is IERC20 { 6 | function deposit() external payable; 7 | 8 | function withdraw(uint256) external; 9 | } 10 | -------------------------------------------------------------------------------- /web3/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module "*.vue" { 4 | import type { DefineComponent } from "vue" 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } 9 | -------------------------------------------------------------------------------- /contracts/mocks/ExternalFunctionMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.9; 3 | 4 | contract ExternalFunctionMock { 5 | event Result(uint256 output); 6 | 7 | function sum(uint256 a, uint256 b) external returns (uint256 c) { 8 | c = a + b; 9 | emit Result(c); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /workbench/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module "*.vue" { 4 | import type { DefineComponent } from "vue" 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "outDir": "dist", 8 | "declaration": true 9 | }, 10 | "include": ["./scripts", "./test", "./typechain"], 11 | "files": ["./hardhat.config.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /contracts/samples/YieldApp.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.9; 3 | pragma experimental ABIEncoderV2; 4 | import "../YieldBox.sol"; 5 | 6 | contract YieldApp { 7 | using YieldBoxRebase for uint256; 8 | 9 | YieldBox public yieldBox; 10 | 11 | constructor(YieldBox _yieldBox) { 12 | yieldBox = _yieldBox; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | artifacts/ 2 | build/ 3 | cache/ 4 | .cache/ 5 | coverage/ 6 | node_modules/ 7 | package-lock.json 8 | .coverage_artifacts/ 9 | .coverage_contracts/ 10 | coverage/ 11 | coverage.json 12 | .env 13 | yarn-error.log 14 | .last_confs/* 15 | .certora_config/* 16 | .certora_*.json 17 | deployments/localhost 18 | typechain-types 19 | workbench/contracts.json 20 | -------------------------------------------------------------------------------- /contracts/mocks/ERC20Mock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.9; 3 | import "@boringcrypto/boring-solidity/contracts/ERC20.sol"; 4 | 5 | contract ERC20Mock is ERC20 { 6 | uint256 public override totalSupply; 7 | 8 | constructor(uint256 _initialAmount) { 9 | // Give the creator all initial tokens 10 | balanceOf[msg.sender] = _initialAmount; 11 | // Update total supply 12 | totalSupply = _initialAmount; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /web3/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | YieldBox 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /web3/components/USDAmount.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /workbench/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Boring WorkBench 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /contracts/mocks/MaliciousMasterContractMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.9; 3 | import "@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol"; 4 | import "../YieldBox.sol"; 5 | 6 | contract MaliciousMasterContractMock is IMasterContract { 7 | function init(bytes calldata) external payable override { 8 | return; 9 | } 10 | 11 | function attack(YieldBox yieldBox) public { 12 | yieldBox.setApprovalForAll(address(this), true); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: false, 4 | es2021: true, 5 | mocha: true, 6 | node: true, 7 | }, 8 | plugins: ["@typescript-eslint"], 9 | extends: ["standard", "plugin:prettier/recommended", "plugin:node/recommended"], 10 | parser: "@typescript-eslint/parser", 11 | parserOptions: { 12 | ecmaVersion: 12, 13 | }, 14 | rules: { 15 | "node/no-unsupported-features/es-syntax": ["error", { ignores: ["modules"] }], 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /contracts/ERC721TokenReceiver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.9; 3 | 4 | import "@boringcrypto/boring-solidity/contracts/interfaces/IERC721TokenReceiver.sol"; 5 | 6 | contract ERC721TokenReceiver is IERC721TokenReceiver { 7 | function onERC721Received( 8 | address, 9 | address, 10 | uint256, 11 | bytes calldata 12 | ) external pure override returns (bytes4) { 13 | return 0x150b7a02; //bytes4(keccak256("onERC721Received(address,address,uint256,bytes)")) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /web3/components/TokenAmount.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /workbench/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "lib": ["esnext", "dom"] 13 | }, 14 | "include": ["**/*.ts", "**/*.d.ts", "**/*.tsx", "**/*.vue"], 15 | "references": [{ "path": "./tsconfig.node.json" }] 16 | } 17 | -------------------------------------------------------------------------------- /web3/components/ExplorerAddress.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /contracts/enums/YieldBoxTokenType.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.9; 3 | 4 | /// @title TokenType 5 | /// @author BoringCrypto (@Boring_Crypto) 6 | /// @notice The YieldBox can hold different types of tokens: 7 | /// Native: These are ERC1155 tokens native to YieldBox. Protocols using YieldBox should use these is possible when simple token creation is needed. 8 | /// ERC20: ERC20 tokens (including rebasing tokens) can be added to the YieldBox. 9 | /// ERC1155: ERC1155 tokens are also supported. This can also be used to add YieldBox Native tokens to strategies since they are ERC1155 tokens. 10 | enum TokenType { 11 | Native, 12 | ERC20, 13 | ERC721, 14 | ERC1155, 15 | None 16 | } 17 | -------------------------------------------------------------------------------- /workbench/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /contracts/mocks/MasterContractMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | 3 | pragma solidity 0.8.9; 4 | 5 | import "@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol"; 6 | import "../YieldBox.sol"; 7 | 8 | contract MasterContractMock is IMasterContract { 9 | YieldBox public immutable yieldBox; 10 | 11 | constructor(YieldBox _yieldBox) { 12 | yieldBox = _yieldBox; 13 | } 14 | 15 | function deposit(uint256 id, uint256 amount) public { 16 | yieldBox.depositAsset(id, msg.sender, address(this), 0, amount); 17 | } 18 | 19 | function setApproval() public { 20 | yieldBox.setApprovalForAll(msg.sender, true); 21 | } 22 | 23 | function init(bytes calldata) external payable override { 24 | return; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /web3/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "lib": ["esnext", "dom"] 13 | }, 14 | "include": [ 15 | "**/*.ts", 16 | "**/*.d.ts", 17 | "**/*.tsx", 18 | "**/*.vue", 19 | "../sdk/NetworkConnectors.ts", 20 | "../sdk/types/factories/Multicall2__factory.ts", 21 | "../sdk/types/Multicall2.ts", 22 | "../sdk/NetworkConnector.ts", 23 | "../sdk/Network.ts", 24 | "../sdk/Web3.ts" 25 | ], 26 | "references": [{ "path": "./tsconfig.node.json" }] 27 | } 28 | -------------------------------------------------------------------------------- /deploy/Salary.ts: -------------------------------------------------------------------------------- 1 | import "hardhat-deploy" 2 | import { DeployFunction } from "hardhat-deploy/types" 3 | import { HardhatRuntimeEnvironment } from "hardhat/types" 4 | 5 | const deployFunction: DeployFunction = async function ({ deployments, getChainId, getNamedAccounts }: HardhatRuntimeEnvironment) { 6 | console.log("Running PointList deploy script") 7 | const { deploy } = deployments 8 | const chainId = parseInt(await getChainId()) 9 | const { deployer } = await getNamedAccounts() 10 | 11 | await deploy("Salary", { 12 | from: deployer, 13 | args: [(await deployments.get("YieldBox")).address], 14 | log: true, 15 | deterministicDeployment: false, 16 | }) 17 | 18 | deployments.run 19 | } 20 | 21 | deployFunction.dependencies = ["YieldBox"] 22 | deployFunction.tags = ["Salary"] 23 | 24 | export default deployFunction 25 | -------------------------------------------------------------------------------- /scripts/deploy.ts: -------------------------------------------------------------------------------- 1 | // We require the Hardhat Runtime Environment explicitly here. This is optional 2 | // but useful for running the script in a standalone fashion through `node 27 | 28 | 31 | -------------------------------------------------------------------------------- /web3/App.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /workbench/pages/Home.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /workbench/components/AddressLink.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 32 | -------------------------------------------------------------------------------- /sdk/Network.ts: -------------------------------------------------------------------------------- 1 | export enum Network { 2 | NONE = 0, 3 | ETHEREUM = 1, 4 | ROPSTEN = 3, 5 | KOVAN = 42, 6 | RINKEBY = 4, 7 | GOERLI = 5, 8 | OPTIMISM = 10, 9 | BINANCE = 56, 10 | OKEX_TEST = 65, 11 | OKEX = 66, 12 | BINANCE_TEST = 98, 13 | FUSE = 122, 14 | POLYGON = 137, 15 | POLYGON_TEST = 80001, 16 | XDAI = 100, 17 | HUOBI = 128, 18 | HUOBI_TEST = 256, 19 | ENERGY_WEB_CHAIN = 246, 20 | ENERGY_WEB_CHAIN_TEST = 73799, 21 | ARBITRUM = 42161, 22 | ARBITRUM_TEST = 421611, 23 | AVALANCHE = 43114, 24 | AVALANCHE_TEST = 43113, 25 | TOMO = 88, 26 | TOMO_TEST = 89, 27 | FANTOM = 250, 28 | FANTOM_TEST = 4002, 29 | MOONBEAM = 1284, 30 | MOONBEAM_KUSAMA = 1285, 31 | MOONBEAM_TEST = 1287, 32 | HARDHAT = 31337, 33 | CELO = 42220, 34 | HARMONY = 1666600000, 35 | HARMONY_TEST = 1666700000, 36 | PALM = 11297108109, 37 | TELOS = 40, 38 | BOBA = 288, 39 | AURORA = 1313161554, 40 | AURORA_TEST = 1313161555, 41 | AURORA_BETA = 1313161556, 42 | UBIQ = 8, 43 | UBIQ_TEST = 9, 44 | CRONOS = 25, 45 | KLAYTN = 8217, 46 | METIS = 1088, 47 | METIS_TEST = 588, 48 | } 49 | -------------------------------------------------------------------------------- /contracts/samples/Tokenizer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.9; 3 | import "../YieldBox.sol"; 4 | 5 | contract Tokenizer { 6 | YieldBox public yieldBox; 7 | 8 | constructor(YieldBox _yieldBox) { 9 | yieldBox = _yieldBox; 10 | } 11 | 12 | mapping(uint256 => uint256) tokenizedAsset; 13 | 14 | function deposit(uint256 sourceAsset, uint256 share) public { 15 | uint256 assetId = tokenizedAsset[sourceAsset]; 16 | if (assetId == 0) { 17 | yieldBox.createToken( 18 | string(string.concat("Tokenized ", bytes(yieldBox.name(sourceAsset)))), 19 | string(string.concat("t", bytes(yieldBox.symbol(sourceAsset)))), 20 | 18, 21 | "" 22 | ); 23 | } 24 | yieldBox.transfer(msg.sender, address(this), sourceAsset, share); 25 | yieldBox.mint(assetId, msg.sender, share * 1e18); 26 | } 27 | 28 | function withdraw(uint256 sourceAsset, uint256 share) public { 29 | uint256 assetId = tokenizedAsset[sourceAsset]; 30 | yieldBox.burn(assetId, msg.sender, share * 1e18); 31 | yieldBox.transfer(address(this), msg.sender, sourceAsset, share); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /contracts/strategies/ERC20WithoutStrategy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.9; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol"; 6 | import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; 7 | import "../enums/YieldBoxTokenType.sol"; 8 | import "../BoringMath.sol"; 9 | import "./BaseStrategy.sol"; 10 | 11 | // solhint-disable const-name-snakecase 12 | // solhint-disable no-empty-blocks 13 | 14 | contract ERC20WithoutStrategy is BaseERC20Strategy { 15 | using BoringERC20 for IERC20; 16 | 17 | constructor(IYieldBox _yieldBox, IERC20 token) BaseERC20Strategy(_yieldBox, address(token)) {} 18 | 19 | string public constant override name = "No strategy"; 20 | string public constant override description = "No strategy"; 21 | 22 | function _currentBalance() internal view override returns (uint256 amount) { 23 | return IERC20(contractAddress).safeBalanceOf(address(this)); 24 | } 25 | 26 | function _deposited(uint256 amount) internal override {} 27 | 28 | function _withdraw(address to, uint256 amount) internal override { 29 | IERC20(contractAddress).safeTransfer(to, amount); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /web3/classes/types/common.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | import type { Listener } from "@ethersproject/providers" 5 | import type { Event, EventFilter } from "ethers" 6 | 7 | export interface TypedEvent = any, TArgsObject = any> extends Event { 8 | args: TArgsArray & TArgsObject 9 | } 10 | 11 | export interface TypedEventFilter<_TEvent extends TypedEvent> extends EventFilter {} 12 | 13 | export interface TypedListener { 14 | (...listenerArg: [...__TypechainArgsArray, TEvent]): void 15 | } 16 | 17 | type __TypechainArgsArray = T extends TypedEvent ? U : never 18 | 19 | export interface OnEvent { 20 | (eventFilter: TypedEventFilter, listener: TypedListener): TRes 21 | (eventName: string, listener: Listener): TRes 22 | } 23 | 24 | export type MinEthersFactory = { 25 | deploy(...a: ARGS[]): Promise 26 | } 27 | 28 | export type GetContractTypeFromFactory = F extends MinEthersFactory ? C : never 29 | 30 | export type GetARGsTypeFromFactory = F extends MinEthersFactory ? Parameters : never 31 | -------------------------------------------------------------------------------- /contracts/samples/Escrow.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.9; 3 | import "../YieldBox.sol"; 4 | 5 | struct Offer { 6 | address owner; 7 | uint256 assetFrom; 8 | uint256 assetTo; 9 | uint256 shareFrom; 10 | uint256 shareTo; 11 | bool closed; 12 | } 13 | 14 | contract Escrow { 15 | YieldBox public yieldBox; 16 | 17 | constructor(YieldBox _yieldBox) { 18 | yieldBox = _yieldBox; 19 | } 20 | 21 | Offer[] public offers; 22 | 23 | function make( 24 | uint256 assetFrom, 25 | uint256 assetTo, 26 | uint256 shareFrom, 27 | uint256 shareTo 28 | ) public { 29 | offers.push(Offer(msg.sender, assetFrom, assetTo, shareFrom, shareTo, false)); 30 | } 31 | 32 | function take(uint256 offerId) public { 33 | Offer memory offer = offers[offerId]; 34 | yieldBox.transfer(msg.sender, offer.owner, offer.assetFrom, offer.shareFrom); 35 | yieldBox.transfer(offer.owner, msg.sender, offer.assetTo, offer.shareTo); 36 | offers[offerId].closed = true; 37 | } 38 | 39 | function cancel(uint256 offerId) public { 40 | require(offers[offerId].owner == msg.sender); 41 | offers[offerId].closed = true; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /deploy/YieldBox.ts: -------------------------------------------------------------------------------- 1 | import "hardhat-deploy" 2 | import "@nomiclabs/hardhat-ethers" 3 | import { ethers } from "hardhat" 4 | import { DeployFunction } from "hardhat-deploy/types" 5 | import { HardhatRuntimeEnvironment } from "hardhat/types" 6 | 7 | const deployFunction: DeployFunction = async function ({ deployments, getChainId, getNamedAccounts }: HardhatRuntimeEnvironment) { 8 | console.log("Running PointList deploy script") 9 | const { deploy } = deployments 10 | const chainId = parseInt(await getChainId()) 11 | const { deployer } = await getNamedAccounts() 12 | 13 | await ethers.provider.send("evm_setNextBlockTimestamp", [Date.now() / 1000]) 14 | 15 | const uriBuilder = await deploy("YieldBoxURIBuilder", { 16 | from: deployer, 17 | args: [], 18 | log: true, 19 | deterministicDeployment: false, 20 | skipIfAlreadyDeployed: false, 21 | }) 22 | 23 | await deploy("YieldBox", { 24 | from: deployer, 25 | args: ["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", uriBuilder.address], 26 | log: true, 27 | deterministicDeployment: false, 28 | }) 29 | } 30 | 31 | deployFunction.dependencies = [] 32 | deployFunction.tags = ["YieldBox"] 33 | 34 | export default deployFunction 35 | -------------------------------------------------------------------------------- /workbench/components/Ago.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 39 | -------------------------------------------------------------------------------- /web3/classes/CoinGeckoAPI.ts: -------------------------------------------------------------------------------- 1 | import Decimal from "decimal.js-light" 2 | import { NetworkConnector } from "../../sdk/NetworkConnector" 3 | import { ERC20Token, SLPToken, Token, tokens } from "./TokenManager" 4 | 5 | export class CoinGecko { 6 | async getPrices(connector: NetworkConnector, _tokens: Token[]) { 7 | const addresses = _tokens.filter((token) => token.constructor === Token).map((token) => token.address) 8 | while (addresses.length) { 9 | try { 10 | const url = 11 | "https://api.coingecko.com/api/v3/simple/token_price/" + 12 | connector.coinGeckoId + 13 | "?contract_addresses=" + 14 | addresses.splice(0, 100).join(",") + 15 | "&vs_currencies=usd&include_market_cap=false&include_24hr_vol=false&include_24hr_change=false&include_last_updated_at=false" 16 | 17 | const result = await (await fetch(url)).json() 18 | for (const price of Object.entries(result)) { 19 | const token = tokens.get(connector.chainId, price[0]) 20 | ;(token.details as ERC20Token).price = new Decimal((price[1] as any).usd as string) 21 | } 22 | } catch (e) { 23 | console.log(e) 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /web3/classes/types/factories/IGnosisSafe__factory.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | 5 | import { Contract, Signer, utils } from "ethers" 6 | import { Provider } from "@ethersproject/providers" 7 | import type { IGnosisSafe, IGnosisSafeInterface } from "../IGnosisSafe" 8 | 9 | const _abi = [ 10 | { 11 | inputs: [], 12 | name: "getOwners", 13 | outputs: [ 14 | { 15 | internalType: "address[]", 16 | name: "", 17 | type: "address[]", 18 | }, 19 | ], 20 | stateMutability: "view", 21 | type: "function", 22 | }, 23 | { 24 | inputs: [], 25 | name: "getThreshold", 26 | outputs: [ 27 | { 28 | internalType: "uint256", 29 | name: "", 30 | type: "uint256", 31 | }, 32 | ], 33 | stateMutability: "view", 34 | type: "function", 35 | }, 36 | ] 37 | 38 | export class IGnosisSafe__factory { 39 | static readonly abi = _abi 40 | static createInterface(): IGnosisSafeInterface { 41 | return new utils.Interface(_abi) as IGnosisSafeInterface 42 | } 43 | static connect(address: string, signerOrProvider: Signer | Provider): IGnosisSafe { 44 | return new Contract(address, _abi, signerOrProvider) as IGnosisSafe 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/ERC1155TokenReceiver.ts: -------------------------------------------------------------------------------- 1 | import chai, { assert, expect } from "chai" 2 | import { solidity } from "ethereum-waffle" 3 | import { ethers } from "hardhat" 4 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers" 5 | import { ERC1155TokenReceiver, ERC1155TokenReceiver__factory } from "../typechain-types" 6 | chai.use(solidity) 7 | 8 | describe("ERC1155TokenReceiver", () => { 9 | let deployer: SignerWithAddress, alice: SignerWithAddress, bob: SignerWithAddress, carol: SignerWithAddress 10 | let Deployer: string, Alice: string, Bob: string, Carol: string 11 | const Zero = ethers.constants.AddressZero 12 | let receiver: ERC1155TokenReceiver 13 | 14 | beforeEach(async () => { 15 | ;({ deployer, alice, bob, carol } = await ethers.getNamedSigners()) 16 | Deployer = deployer.address 17 | Alice = alice.address 18 | Bob = bob.address 19 | Carol = carol.address 20 | receiver = await new ERC1155TokenReceiver__factory(deployer).deploy() 21 | await receiver.deployed() 22 | }) 23 | 24 | it("Deploy ERC1155TokenReceiver", async function () { 25 | assert.equal((await receiver.deployTransaction.wait()).status, 1) 26 | }) 27 | 28 | it("responds correctly to onERC1155Received", async function () { 29 | expect(await receiver.onERC1155Received(Alice, Bob, 12, 123, "0x1234")).equals("0xf23a6e61") 30 | }) 31 | 32 | it("responds correctly to onERC1155BatchReceived", async function () { 33 | expect(await receiver.onERC1155BatchReceived(Alice, Bob, [12], [123], "0x1234")).equals("0xbc197c81") 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "UNLICENSED", 3 | "devDependencies": { 4 | "@boringcrypto/hardhat-framework": "boringcrypto/hardhat-framework#fb3efb3f2f6db3ca798acd5fd3454e62b169a856", 5 | "concurrently": "^7.0.0", 6 | "husky": "^7.0.0" 7 | }, 8 | "scripts": { 9 | "compile": "hardhat compile", 10 | "test": "hardhat test", 11 | "coverage": "hardhat coverage && open-cli ./coverage/index.html", 12 | "prettier": "prettier --write *.js *.ts *.json test/**/*.ts contracts/**/*.sol", 13 | "flat": "hardhat flat .\\contracts\\YieldBox.sol --output .\\flat\\YieldBoxFlat.sol", 14 | "dev": "hardhat compile && concurrently \"hardhat node --export ./workbench/contracts.json --watch\" \"vite --config web3/vite.config.ts\" \"vite --config workbench/vite.config.ts\"", 15 | "build": "vue-tsc --noEmit && vite build", 16 | "preview": "vite preview", 17 | "prepare": "husky install", 18 | "workbench": "vite --config workbench/vite.config.ts" 19 | }, 20 | "dependencies": { 21 | "@boringcrypto/boring-solidity": "boringcrypto/BoringSolidity#c343377d4633f8a7f320993679d9f2b04c87b066", 22 | "@openzeppelin/contracts": "^4.5.0", 23 | "@popperjs/core": "^2.11.2", 24 | "@uniswap/token-lists": "^1.0.0-beta.28", 25 | "axios": "^0.26.1", 26 | "bootstrap": "^5.1.3", 27 | "bootstrap-icons": "^1.8.1", 28 | "bootstrap-vue-3": "^0.1.7", 29 | "bootswatch": "^5.1.3", 30 | "decimal.js-light": "^2.5.1", 31 | "solc": "0.8.9", 32 | "vue-router": "^4.0.13" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /workbench/classes/FactoryInterface.ts: -------------------------------------------------------------------------------- 1 | import { Contract, ContractFactory, Overrides, Signer, utils, BytesLike } from "ethers" 2 | import { Provider, TransactionRequest } from "@ethersproject/providers" 3 | import { FunctionFragment, Result } from "ethers/lib/utils" 4 | 5 | export interface IContract extends utils.Interface { 6 | contractName: "Contract" 7 | functions: { [name: string]: FunctionFragment } 8 | 9 | encodeFunctionData(functionFragment: string, values: any[]): string 10 | 11 | decodeFunctionResult(functionFragment: string, data: BytesLike): Result 12 | 13 | events: {} 14 | } 15 | 16 | export class BaseFactory extends ContractFactory { 17 | deploy(overrides?: Overrides & { from?: string | Promise }): Promise { 18 | return super.deploy(overrides || {}) as Promise 19 | } 20 | getDeployTransaction(overrides?: Overrides & { from?: string | Promise }): TransactionRequest { 21 | return super.getDeployTransaction(overrides || {}) 22 | } 23 | attach(address: string): Contract { 24 | return super.attach(address) as Contract 25 | } 26 | connect(signer: Signer): BaseFactory { 27 | return super.connect(signer) as BaseFactory 28 | } 29 | static readonly contractName = "Contract" 30 | public readonly contractName = "Contract" 31 | static readonly bytecode = "" 32 | static readonly abi = "" 33 | static createInterface(): IContract { 34 | return new utils.Interface(this.abi) as IContract 35 | } 36 | static connect(address: string, signerOrProvider: Signer | Provider): Contract { 37 | return new Contract(address, this.abi, signerOrProvider) as Contract 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /contracts/samples/helloworld.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.9; 3 | import "../YieldBox.sol"; 4 | 5 | // An example a contract that stores tokens in the YieldBox. 6 | // PS. This isn't good code, just kept it simple to illustrate usage. 7 | contract HelloWorld { 8 | YieldBox public immutable yieldBox; 9 | uint256 public immutable assetId; 10 | 11 | constructor(YieldBox _yieldBox, IERC20 token) { 12 | yieldBox = _yieldBox; 13 | assetId = _yieldBox.registerAsset(TokenType.ERC20, address(token), IStrategy(address(0)), 0); 14 | } 15 | 16 | mapping(address => uint256) public yieldBoxShares; 17 | 18 | // Deposits an amount of token into the YieldBox. YieldBox shares are given to the HelloWorld contract and 19 | // assigned to the user in yieldBoxShares. 20 | // Don't deposit twice, you'll lose the first deposit ;) 21 | function deposit(uint256 amount) public { 22 | uint256 shares; 23 | (, shares) = yieldBox.depositAsset(assetId, msg.sender, address(this), amount, 0); 24 | yieldBoxShares[msg.sender] += shares; 25 | } 26 | 27 | // This will return the current value in amount of the YieldBox shares. 28 | // Through a strategy, the value can go up over time, although in this example no strategy is selected. 29 | function balance() public view returns (uint256 amount) { 30 | return yieldBox.toAmount(assetId, yieldBoxShares[msg.sender], false); 31 | } 32 | 33 | // Withdraw all shares from the YieldBox and receive the token. 34 | function withdraw() public { 35 | yieldBox.withdraw(assetId, address(this), msg.sender, 0, yieldBoxShares[msg.sender]); 36 | yieldBoxShares[msg.sender] = 0; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /workbench/pages/Address.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /web3/components/Web3Button.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 47 | -------------------------------------------------------------------------------- /contracts/samples/lending/ISwapper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.9; 3 | import "@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol"; 4 | 5 | interface ISwapper { 6 | /// @notice Withdraws 'amountFrom' of token 'from' from the BentoBox account for this swapper. 7 | /// Swaps it for at least 'amountToMin' of token 'to'. 8 | /// Transfers the swapped tokens of 'to' into the BentoBox using a plain ERC20 transfer. 9 | /// Returns the amount of tokens 'to' transferred to BentoBox. 10 | /// (The BentoBox skim function will be used by the caller to get the swapped funds). 11 | function swap( 12 | uint256 fromAssetId, 13 | uint256 toAssetId, 14 | address recipient, 15 | uint256 shareToMin, 16 | uint256 shareFrom 17 | ) external returns (uint256 extraShare, uint256 shareReturned); 18 | 19 | /// @notice Calculates the amount of token 'from' needed to complete the swap (amountFrom), 20 | /// this should be less than or equal to amountFromMax. 21 | /// Withdraws 'amountFrom' of token 'from' from the BentoBox account for this swapper. 22 | /// Swaps it for exactly 'exactAmountTo' of token 'to'. 23 | /// Transfers the swapped tokens of 'to' into the BentoBox using a plain ERC20 transfer. 24 | /// Transfers allocated, but unused 'from' tokens within the BentoBox to 'refundTo' (amountFromMax - amountFrom). 25 | /// Returns the amount of 'from' tokens withdrawn from BentoBox (amountFrom). 26 | /// (The BentoBox skim function will be used by the caller to get the swapped funds). 27 | function swapExact( 28 | uint256 fromAssetId, 29 | uint256 toAssetId, 30 | address recipient, 31 | address refundTo, 32 | uint256 shareFromSupplied, 33 | uint256 shareToExact 34 | ) external returns (uint256 shareUsed, uint256 shareReturned); 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YieldBox 2 | 3 | ### Introduction Video 4 | 5 | [![YIΞLDBOX - @BoringCrypto at @ETHDubaiConf 6 | ](https://img.youtube.com/vi/JbMQg6-pH8E/0.jpg)](https://www.youtube.com/watch?v=JbMQg6-pH8E) 7 | 8 | 9 | ### Local development 10 | 11 | Clone the repo and run `yarn` 12 | 13 | To start hardhat, the sample UI and workbench: 14 | 15 | `yarn dev` 16 | 17 | There may be a few errors while things recompile. Be sure to add your Alchemy key to .env and it's recommended to use VSCode with the plugins suggested below. 18 | 19 | ### Setting up your .env 20 | 21 | You can include your environment variables in a `.env` file in the root of your repo. Alternatively you can set an actual environment variable called `DOTENV_PATH` to point to a central `.env` file to be used. This way you can use the same environment settings accross multiple projects. 22 | 23 | Some useful settings: 24 | 25 | ``` 26 | ALCHEMY_API_KEY= 27 | COINMARKETCAP_API_KEY= 28 | HARDHAT_NETWORK=hardhat 29 | HARDHAT_MAX_MEMORY=4096 30 | HARDHAT_SHOW_STACK_TRACES=true 31 | HARDHAT_VERBOSE=true 32 | ``` 33 | 34 | ### Recommended VSCode extentions 35 | 36 | - solidity - Juan Blanco 37 | - Mocha Test Explorer - Holger Benl 38 | - Vue Language Features (Volar) - Johnson Chu 39 | 40 | ### Want to help out? 41 | 42 | Contact BoringCrypto on Twitter (@Boring_Crypto) or on Discord (BoringCrypto#3523). 43 | 44 | To move YieldBox along, help is needed with: 45 | 46 | - Documentation (setting up GitBook and building out docs) 47 | - Sample apps - Several sample contracts have been created, they need a UI, testing and documentation 48 | - Salary - stream with optional cliff 49 | - Escrow - 50 | - Lending - isolated lending contract 51 | - Tokenizer - tokenize anything, most useful for NFTs 52 | - Swap - AMM based off Uni V2 53 | - Building strategies (you're free to add a fee to your strategy) 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 145, 3 | semi: false, 4 | trailingComma: "es5", 5 | tabWidth: 4, 6 | endOfLine: "lf", 7 | singleQuote: false, 8 | bracketSpacing: true, 9 | overrides: [ 10 | { 11 | files: "*.vue", 12 | options: { 13 | bracketSpacing: true, 14 | printWidth: 145, 15 | tabWidth: 4, 16 | useTabs: false, 17 | singleQuote: false, 18 | explicitTypes: "always", 19 | endOfLine: "lf", 20 | semi: false, 21 | }, 22 | }, 23 | { 24 | files: "*.sol", 25 | options: { 26 | bracketSpacing: true, 27 | printWidth: 145, 28 | tabWidth: 4, 29 | useTabs: false, 30 | singleQuote: false, 31 | explicitTypes: "always", 32 | endOfLine: "lf", 33 | }, 34 | }, 35 | { 36 | files: "*.js", 37 | options: { 38 | printWidth: 145, 39 | semi: false, 40 | trailingComma: "es5", 41 | tabWidth: 4, 42 | endOfLine: "lf", 43 | }, 44 | }, 45 | { 46 | files: "*.ts", 47 | options: { 48 | printWidth: 145, 49 | semi: false, 50 | trailingComma: "es5", 51 | tabWidth: 4, 52 | endOfLine: "lf", 53 | }, 54 | }, 55 | { 56 | files: "*.json", 57 | options: { 58 | printWidth: 145, 59 | semi: false, 60 | trailingComma: "es5", 61 | tabWidth: 4, 62 | endOfLine: "lf", 63 | }, 64 | }, 65 | ], 66 | } 67 | -------------------------------------------------------------------------------- /web3/pages/Swap.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /web3/pages/Escrow.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /web3/pages/Lending.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /web3/pages/Tokenizer.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /workbench/components/Menu.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 47 | -------------------------------------------------------------------------------- /contracts/strategies/SushiStakingSimpleStrategy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.9; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol"; 6 | import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; 7 | import "../enums/YieldBoxTokenType.sol"; 8 | import "../BoringMath.sol"; 9 | import "./BaseStrategy.sol"; 10 | 11 | // solhint-disable const-name-snakecase 12 | // solhint-disable no-empty-blocks 13 | 14 | interface ISushiBar is IERC20 { 15 | function enter(uint256 amount) external; 16 | 17 | function leave(uint256 share) external; 18 | } 19 | 20 | contract SushiStakingStrategy is BaseERC20Strategy { 21 | using BoringERC20 for IERC20; 22 | using BoringERC20 for ISushiBar; 23 | using BoringMath for uint256; 24 | 25 | constructor(IYieldBox _yieldBox) BaseERC20Strategy(_yieldBox, address(sushi)) {} 26 | 27 | string public constant override name = "xSUSHI"; 28 | string public constant override description = "Stakes SUSHI into the SushiBar for xSushi"; 29 | 30 | IERC20 private constant sushi = IERC20(0x6B3595068778DD592e39A122f4f5a5cF09C90fE2); 31 | ISushiBar private constant sushiBar = ISushiBar(0x8798249c2E607446EfB7Ad49eC89dD1865Ff4272); 32 | 33 | function _currentBalance() internal view override returns (uint256 amount) { 34 | uint256 sushiBalance = sushi.safeBalanceOf(address(this)); 35 | uint256 sushiInBar = sushi.safeBalanceOf(address(sushiBar)); 36 | uint256 xSushiBalance = sushiBar.safeBalanceOf(address(this)); 37 | return sushiBalance + xSushiBalance.muldiv(sushiInBar, sushiBar.safeTotalSupply(), false); 38 | } 39 | 40 | function _deposited(uint256 amount) internal override { 41 | sushiBar.enter(amount); 42 | } 43 | 44 | function _withdraw(address to, uint256 amount) internal override { 45 | uint256 totalSushi = sushi.safeBalanceOf(address(sushiBar)); 46 | uint256 totalxSushi = sushiBar.safeTotalSupply(); 47 | 48 | sushiBar.leave(amount.muldiv(totalxSushi, totalSushi, true)); 49 | sushi.safeTransfer(to, amount); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /workbench/data-workbench.ts: -------------------------------------------------------------------------------- 1 | import { reactive, markRaw } from "vue" 2 | import { ethers } from "ethers" 3 | import Contracts from "./contracts.json" 4 | 5 | export interface IAddressInfo { 6 | address: string 7 | type: "wallet" | "contract" | "miner" | "zero" 8 | name: string 9 | object: ethers.Wallet | ethers.Contract | null 10 | } 11 | 12 | export default reactive({ 13 | title: "Boring WorkBench", 14 | name: "Boring WorkBench", 15 | addresses: {} as { [address: string]: IAddressInfo }, 16 | names: {} as { [name: string]: IAddressInfo }, 17 | contracts: {} as { [address: string]: ethers.Contract }, 18 | 19 | lookupAddress: function (name: string) { 20 | if (name) { 21 | return this.addresses[name.toLowerCase()] 22 | } 23 | return null 24 | }, 25 | 26 | lookupName: function (name: string) { 27 | return this.names[name.toLowerCase()] 28 | }, 29 | 30 | addNamedAddress: function (address: IAddressInfo) { 31 | this.addresses[address.address.toLowerCase()] = address 32 | this.names[address.name.toLowerCase()] = address 33 | }, 34 | 35 | setup: function () { 36 | this.addNamedAddress({ 37 | type: "zero", 38 | address: "0x0000000000000000000000000000000000000000", 39 | name: "Zero", 40 | object: null, 41 | }) 42 | 43 | this.addNamedAddress({ 44 | address: "0xC014BA5EC014ba5ec014Ba5EC014ba5Ec014bA5E", 45 | type: "miner", 46 | name: "Hardhat Miner", 47 | object: null, 48 | }) 49 | 50 | for (let name of Object.keys(Contracts.contracts)) { 51 | const contractInfo = Contracts.contracts[name as keyof typeof Contracts.contracts] 52 | const contract = new ethers.Contract(contractInfo.address, contractInfo.abi) 53 | this.contracts[contract.address] = contract 54 | this.addNamedAddress({ 55 | type: "contract", 56 | address: contract.address, 57 | name: name, 58 | object: markRaw(contract), 59 | }) 60 | } 61 | }, 62 | }) 63 | -------------------------------------------------------------------------------- /contracts/mocks/YieldBoxRebaseMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.9; 3 | pragma experimental ABIEncoderV2; 4 | import "../YieldBox.sol"; 5 | 6 | contract YieldBoxRebaseMock { 7 | using YieldBoxRebase for uint256; 8 | 9 | uint256 public totalAmount; 10 | uint256 public totalShares; 11 | 12 | function toShare(uint256 amount, bool roundUp) public view returns (uint256 share) { 13 | share = amount._toShares(totalShares, totalAmount, roundUp); 14 | } 15 | 16 | function toAmount(uint256 share, bool roundUp) public view returns (uint256 amount) { 17 | amount = share._toAmount(totalShares, totalAmount, roundUp); 18 | } 19 | 20 | function deposit(uint256 share, uint256 amount) public returns (uint256 shareOut, uint256 amountOut) { 21 | if (share == 0) { 22 | // value of the share may be lower than the amount due to rounding, that's ok 23 | share = amount._toShares(totalShares, totalAmount, false); 24 | } else { 25 | // amount may be lower than the value of share due to rounding, in that case, add 1 to amount (Always round up) 26 | amount = share._toAmount(totalShares, totalAmount, true); 27 | } 28 | totalAmount += amount; 29 | totalShares += share; 30 | return (share, amount); 31 | } 32 | 33 | function withdraw(uint256 share, uint256 amount) public returns (uint256 shareOut, uint256 amountOut) { 34 | if (share == 0) { 35 | // value of the share paid could be lower than the amount paid due to rounding, in that case, add a share (Always round up) 36 | share = amount._toShares(totalShares, totalAmount, true); 37 | } else { 38 | // amount may be lower than the value of share due to rounding, that's ok 39 | amount = share._toAmount(totalShares, totalAmount, false); 40 | } 41 | 42 | totalAmount -= amount; 43 | totalShares -= share; 44 | return (share, amount); 45 | } 46 | 47 | function gain(uint256 amount) public { 48 | totalAmount += amount; 49 | } 50 | 51 | function lose(uint256 amount) public { 52 | totalAmount -= amount; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /contracts/strategies/SushiStakingBufferStrategy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.9; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol"; 6 | import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; 7 | import "../enums/YieldBoxTokenType.sol"; 8 | import "./BaseBufferStrategy.sol"; 9 | 10 | // solhint-disable const-name-snakecase 11 | // solhint-disable no-empty-blocks 12 | 13 | interface ISushiBar is IERC20 { 14 | function enter(uint256 amount) external; 15 | 16 | function leave(uint256 share) external; 17 | } 18 | 19 | contract SushiStakingBufferStrategy is BaseERC20BufferStrategy { 20 | using BoringMath for uint256; 21 | using BoringERC20 for IERC20; 22 | using BoringERC20 for ISushiBar; 23 | 24 | constructor(IYieldBox _yieldBox) BaseERC20BufferStrategy(_yieldBox, address(sushi)) {} 25 | 26 | string public constant override name = "xSUSHI-Buffered"; 27 | string public constant override description = "Stakes SUSHI into the SushiBar for xSushi with a buffer"; 28 | 29 | IERC20 private constant sushi = IERC20(0x6B3595068778DD592e39A122f4f5a5cF09C90fE2); 30 | ISushiBar private constant sushiBar = ISushiBar(0x8798249c2E607446EfB7Ad49eC89dD1865Ff4272); 31 | 32 | uint256 private _balance; 33 | 34 | function _balanceInvested() internal view override returns (uint256 amount) { 35 | uint256 sushiInBar = sushi.safeBalanceOf(address(sushiBar)); 36 | uint256 xSushiBalance = sushiBar.safeBalanceOf(address(this)); 37 | return xSushiBalance.muldiv(sushiInBar, sushiBar.safeTotalSupply(), false); 38 | } 39 | 40 | function _invest(uint256 amount) internal override { 41 | sushiBar.enter(amount); 42 | } 43 | 44 | function _divestAll() internal override { 45 | sushiBar.leave(sushiBar.balanceOf(address(this))); 46 | } 47 | 48 | function _divest(uint256 amount) internal override { 49 | uint256 totalShares = sushiBar.totalSupply(); 50 | uint256 totalSushi = sushi.balanceOf(address(sushiBar)); 51 | 52 | uint256 shares = (amount * totalShares) / totalSushi; 53 | 54 | sushiBar.leave(shares); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /contracts/mocks/WETH9Mock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity 0.8.9; 3 | 4 | contract WETH9Mock { 5 | string public name = "Wrapped Ether"; 6 | string public symbol = "WETH"; 7 | uint8 public decimals = 18; 8 | 9 | event Approval(address indexed src, address indexed guy, uint256 wad); 10 | event Transfer(address indexed src, address indexed dst, uint256 wad); 11 | event Deposit(address indexed dst, uint256 wad); 12 | event Withdrawal(address indexed src, uint256 wad); 13 | 14 | mapping(address => uint256) public balanceOf; 15 | mapping(address => mapping(address => uint256)) public allowance; 16 | 17 | /*fallback () external payable { 18 | deposit(); 19 | }*/ 20 | function deposit() public payable { 21 | balanceOf[msg.sender] += msg.value; 22 | emit Deposit(msg.sender, msg.value); 23 | } 24 | 25 | function withdraw(uint256 wad) public { 26 | require(balanceOf[msg.sender] >= wad, "WETH9: Error"); 27 | balanceOf[msg.sender] -= wad; 28 | bool success; 29 | (success, ) = msg.sender.call{ value: wad }(""); 30 | emit Withdrawal(msg.sender, wad); 31 | } 32 | 33 | function totalSupply() public view returns (uint256) { 34 | return address(this).balance; 35 | } 36 | 37 | function approve(address guy, uint256 wad) public returns (bool) { 38 | allowance[msg.sender][guy] = wad; 39 | emit Approval(msg.sender, guy, wad); 40 | return true; 41 | } 42 | 43 | function transfer(address dst, uint256 wad) public returns (bool) { 44 | return transferFrom(msg.sender, dst, wad); 45 | } 46 | 47 | function transferFrom( 48 | address src, 49 | address dst, 50 | uint256 wad 51 | ) public returns (bool) { 52 | require(balanceOf[src] >= wad, "WETH9: Error"); 53 | 54 | if (src != msg.sender && allowance[src][msg.sender] != type(uint256).max) { 55 | require(allowance[src][msg.sender] >= wad, "WETH9: Error"); 56 | allowance[src][msg.sender] -= wad; 57 | } 58 | 59 | balanceOf[src] -= wad; 60 | balanceOf[dst] += wad; 61 | 62 | emit Transfer(src, dst, wad); 63 | 64 | return true; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /contracts/interfaces/IYieldBox.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.9; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "../enums/YieldBoxTokenType.sol"; 6 | 7 | interface IYieldBox { 8 | function wrappedNative() external view returns (address wrappedNative); 9 | 10 | function assets(uint256 assetId) 11 | external 12 | view 13 | returns ( 14 | TokenType tokenType, 15 | address contractAddress, 16 | address strategy, 17 | uint256 tokenId 18 | ); 19 | 20 | function nativeTokens(uint256 assetId) 21 | external 22 | view 23 | returns ( 24 | string memory name, 25 | string memory symbol, 26 | uint8 decimals 27 | ); 28 | 29 | function owner(uint256 assetId) external view returns (address owner); 30 | 31 | function totalSupply(uint256 assetId) external view returns (uint256 totalSupply); 32 | 33 | function depositAsset( 34 | uint256 assetId, 35 | address from, 36 | address to, 37 | uint256 amount, 38 | uint256 share 39 | ) external returns (uint256 amountOut, uint256 shareOut); 40 | 41 | function withdraw( 42 | uint256 assetId, 43 | address from, 44 | address to, 45 | uint256 amount, 46 | uint256 share 47 | ) external returns (uint256 amountOut, uint256 shareOut); 48 | 49 | function transfer( 50 | address from, 51 | address to, 52 | uint256 assetId, 53 | uint256 share 54 | ) external; 55 | 56 | function batchTransfer( 57 | address from, 58 | address to, 59 | uint256[] calldata assetIds_, 60 | uint256[] calldata shares_ 61 | ) external; 62 | 63 | function transferMultiple( 64 | address from, 65 | address[] calldata tos, 66 | uint256 assetId, 67 | uint256[] calldata shares 68 | ) external; 69 | 70 | function toShare( 71 | uint256 assetId, 72 | uint256 amount, 73 | bool roundUp 74 | ) external view returns (uint256 share); 75 | 76 | function toAmount( 77 | uint256 assetId, 78 | uint256 share, 79 | bool roundUp 80 | ) external view returns (uint256 amount); 81 | } 82 | -------------------------------------------------------------------------------- /contracts/mocks/RevertingERC20Mock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.9; 3 | 4 | // RevertingERC20 reverts on errors 5 | contract RevertingERC20Mock { 6 | string public symbol; 7 | string public name; 8 | uint8 public immutable decimals; 9 | uint256 public totalSupply; 10 | mapping(address => uint256) public balanceOf; 11 | mapping(address => mapping(address => uint256)) public allowance; 12 | 13 | event Transfer(address indexed _from, address indexed _to, uint256 _value); 14 | event Approval(address indexed _owner, address indexed _spender, uint256 _value); 15 | 16 | constructor( 17 | string memory name_, 18 | string memory symbol_, 19 | uint8 decimals_, 20 | uint256 supply 21 | ) { 22 | name = name_; 23 | symbol = symbol_; 24 | decimals = decimals_; 25 | totalSupply = supply; 26 | balanceOf[msg.sender] = supply; 27 | emit Transfer(address(0), msg.sender, supply); 28 | } 29 | 30 | function transfer(address to, uint256 amount) public returns (bool success) { 31 | require(balanceOf[msg.sender] >= amount, "TokenB: balance too low"); 32 | require(amount >= 0, "TokenB: amount should be > 0"); 33 | require(balanceOf[to] + amount >= balanceOf[to], "TokenB: overflow detected"); 34 | balanceOf[msg.sender] -= amount; 35 | balanceOf[to] += amount; 36 | emit Transfer(msg.sender, to, amount); 37 | return true; 38 | } 39 | 40 | function transferFrom( 41 | address from, 42 | address to, 43 | uint256 amount 44 | ) public returns (bool success) { 45 | require(balanceOf[from] >= amount, "TokenB: balance too low"); 46 | require(allowance[from][msg.sender] >= amount, "TokenB: allowance too low"); 47 | require(amount >= 0, "TokenB: amount should be >= 0"); 48 | require(balanceOf[to] + amount >= balanceOf[to], "TokenB: overflow detected"); 49 | balanceOf[from] -= amount; 50 | allowance[from][msg.sender] -= amount; 51 | balanceOf[to] += amount; 52 | emit Transfer(from, to, amount); 53 | return true; 54 | } 55 | 56 | function approve(address spender, uint256 amount) public returns (bool success) { 57 | allowance[msg.sender][spender] = amount; 58 | emit Approval(msg.sender, spender, amount); 59 | return true; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /contracts/strategies/BaseStrategy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.9; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol"; 6 | import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; 7 | import "../enums/YieldBoxTokenType.sol"; 8 | import "../interfaces/IStrategy.sol"; 9 | 10 | // solhint-disable const-name-snakecase 11 | // solhint-disable no-empty-blocks 12 | 13 | abstract contract BaseStrategy is IStrategy { 14 | IYieldBox public immutable yieldBox; 15 | 16 | constructor(IYieldBox _yieldBox) { 17 | yieldBox = _yieldBox; 18 | } 19 | 20 | function _currentBalance() internal view virtual returns (uint256 amount); 21 | 22 | function currentBalance() public view virtual returns (uint256 amount) { 23 | return _currentBalance(); 24 | } 25 | 26 | function withdrawable() external view virtual returns (uint256 amount) { 27 | return _currentBalance(); 28 | } 29 | 30 | function cheapWithdrawable() external view virtual returns (uint256 amount) { 31 | return _currentBalance(); 32 | } 33 | 34 | function _deposited(uint256 amount) internal virtual; 35 | 36 | function deposited(uint256 amount) external { 37 | require(msg.sender == address(yieldBox), "Not YieldBox"); 38 | _deposited(amount); 39 | } 40 | 41 | function _withdraw(address to, uint256 amount) internal virtual; 42 | 43 | function withdraw(address to, uint256 amount) external { 44 | require(msg.sender == address(yieldBox), "Not YieldBox"); 45 | _withdraw(to, amount); 46 | } 47 | } 48 | 49 | abstract contract BaseERC20Strategy is BaseStrategy { 50 | TokenType public constant tokenType = TokenType.ERC20; 51 | uint256 public constant tokenId = 0; 52 | address public immutable contractAddress; 53 | 54 | constructor(IYieldBox _yieldBox, address _contractAddress) BaseStrategy(_yieldBox) { 55 | contractAddress = _contractAddress; 56 | } 57 | } 58 | 59 | abstract contract BaseERC1155Strategy is BaseStrategy { 60 | TokenType public constant tokenType = TokenType.ERC1155; 61 | uint256 public immutable tokenId; 62 | address public immutable contractAddress; 63 | 64 | constructor( 65 | IYieldBox _yieldBox, 66 | address _contractAddress, 67 | uint256 _tokenId 68 | ) BaseStrategy(_yieldBox) { 69 | contractAddress = _contractAddress; 70 | tokenId = _tokenId; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /web3/pages/YieldBoxBalances.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /web3/components/TokenAmountInput.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 65 | 66 | 79 | -------------------------------------------------------------------------------- /web3/classes/YieldBox.ts: -------------------------------------------------------------------------------- 1 | import { Network } from "../../sdk/Network" 2 | import { reactive, ref } from "vue" 3 | import { ethers, BigNumber } from "ethers" 4 | import Web3 from "../../sdk/Web3" 5 | import { connectors } from "../../sdk/NetworkConnectors" 6 | import { NetworkConnector } from "../../sdk/NetworkConnector" 7 | import Decimal from "decimal.js-light" 8 | import { YieldBox as YieldBoxContract, YieldBox__factory } from "../../typechain-types" 9 | import app from "../data-web3" 10 | import { Token, tokens } from "./TokenManager" 11 | import { TokenType } from "../../sdk" 12 | 13 | export { TokenType } 14 | 15 | export class Asset { 16 | network: Network 17 | assetId: number 18 | tokenType: TokenType 19 | contractAddress: string 20 | strategyAddress: string 21 | tokenId: BigNumber 22 | loaded = false 23 | token?: Token 24 | name?: string 25 | symbol?: string 26 | 27 | constructor(network: Network, assetId: number, tokenType: TokenType, contractAddress: string, strategyAddress: string, tokenId: BigNumber) { 28 | this.network = network 29 | this.assetId = assetId 30 | this.tokenType = tokenType 31 | this.contractAddress = contractAddress 32 | this.strategyAddress = strategyAddress 33 | this.tokenId = tokenId 34 | } 35 | } 36 | 37 | export class YieldBox { 38 | network: Network 39 | yieldBox: YieldBoxContract 40 | assets = reactive([] as Asset[]) 41 | 42 | constructor(network: Network, address: string) { 43 | this.network = network 44 | this.yieldBox = new YieldBox__factory(app.web3.provider!.getSigner()).attach(address) 45 | } 46 | 47 | async loadAssets() { 48 | const len = (await this.yieldBox.assetCount()).toNumber() 49 | 50 | const connector = new connectors[app.web3.chainId]() 51 | for (let i = this.assets.length; i < len; i++) { 52 | connector.queue(this.yieldBox.populateTransaction.assets(i), this.yieldBox.interface, (result) => { 53 | const asset = new Asset(this.network, i, result[0], result[1], result[2], result[3]) 54 | this.assets[i] = asset 55 | if (asset.tokenType == TokenType.ERC20) { 56 | asset.token = tokens.get(this.network, asset.contractAddress) 57 | } 58 | }) 59 | connector.queue(this.yieldBox.populateTransaction.name(i), this.yieldBox.interface, (result) => (this.assets[i].name = result)) 60 | connector.queue(this.yieldBox.populateTransaction.symbol(i), this.yieldBox.interface, (result) => (this.assets[i].symbol = result)) 61 | } 62 | await connector.call(100) 63 | await tokens.loadInfo() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /contracts/mocks/SushiBarMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.9; 3 | import "@boringcrypto/boring-solidity/contracts/ERC20.sol"; 4 | 5 | // solhint-disable const-name-snakecase 6 | 7 | // SushiBar is the coolest bar in town. You come in with some Sushi, and leave with more! The longer you stay, the more Sushi you get. 8 | // This contract handles swapping to and from xSushi, SushiSwap's staking token. 9 | contract SushiBarMock is ERC20 { 10 | ERC20 public sushi; 11 | uint256 public override totalSupply; 12 | string public constant name = "SushiBar"; 13 | string public constant symbol = "xSushi"; 14 | 15 | // Define the Sushi token contract 16 | constructor(ERC20 _sushi) { 17 | sushi = _sushi; 18 | } 19 | 20 | // Enter the bar. Pay some SUSHIs. Earn some shares. 21 | // Locks Sushi and mints xSushi 22 | function enter(uint256 _amount) public { 23 | // Gets the amount of Sushi locked in the contract 24 | uint256 totalSushi = sushi.balanceOf(address(this)); 25 | // Gets the amount of xSushi in existence 26 | uint256 totalShares = totalSupply; 27 | // If no xSushi exists, mint it 1:1 to the amount put in 28 | if (totalShares == 0 || totalSushi == 0) { 29 | _mint(msg.sender, _amount); 30 | } 31 | // Calculate and mint the amount of xSushi the Sushi is worth. The ratio will change overtime, 32 | // as xSushi is burned/minted and Sushi deposited + gained from fees / withdrawn. 33 | else { 34 | uint256 what = (_amount * totalShares) / totalSushi; 35 | _mint(msg.sender, what); 36 | } 37 | // Lock the Sushi in the contract 38 | sushi.transferFrom(msg.sender, address(this), _amount); 39 | } 40 | 41 | // Leave the bar. Claim back your SUSHIs. 42 | // Unclocks the staked + gained Sushi and burns xSushi 43 | function leave(uint256 _share) public { 44 | // Gets the amount of xSushi in existence 45 | uint256 totalShares = totalSupply; 46 | // Calculates the amount of Sushi the xSushi is worth 47 | uint256 what = (_share * sushi.balanceOf(address(this))) / totalShares; 48 | _burn(msg.sender, _share); 49 | sushi.transfer(msg.sender, what); 50 | } 51 | 52 | function _mint(address account, uint256 amount) internal { 53 | totalSupply += amount; 54 | balanceOf[account] += amount; 55 | emit Transfer(address(0), account, amount); 56 | } 57 | 58 | function _burn(address account, uint256 amount) internal { 59 | balanceOf[account] -= amount; 60 | totalSupply -= amount; 61 | emit Transfer(account, address(0), amount); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /contracts/mocks/MasterContractFullCycleMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | 3 | pragma solidity 0.8.9; 4 | 5 | import "@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol"; 6 | import "../YieldBox.sol"; 7 | 8 | contract MasterContractFullCycleMock is IMasterContract { 9 | YieldBox public immutable yieldBox; 10 | address public deployer; 11 | address public token; 12 | address public erc1155; 13 | IStrategy public tokenStrategy; 14 | IStrategy public erc1155Strategy; 15 | IStrategy public ethStrategy; 16 | IStrategy private constant ZERO = IStrategy(address(0)); 17 | 18 | constructor(YieldBox _yieldBox) { 19 | yieldBox = _yieldBox; 20 | } 21 | 22 | function init(bytes calldata data) external payable override { 23 | (deployer, token, erc1155, tokenStrategy, erc1155Strategy, ethStrategy) = abi.decode( 24 | data, 25 | (address, address, address, IStrategy, IStrategy, IStrategy) 26 | ); 27 | return; 28 | } 29 | 30 | function run() public payable { 31 | yieldBox.deposit(TokenType.ERC20, token, ZERO, 0, deployer, deployer, 1000, 0); 32 | yieldBox.deposit(TokenType.ERC20, token, ZERO, 0, deployer, deployer, 0, 1000_00000000); 33 | yieldBox.withdraw(2, deployer, deployer, 1000, 0); 34 | yieldBox.withdraw(2, deployer, deployer, 0, 1000_00000000); 35 | 36 | yieldBox.deposit(TokenType.ERC1155, erc1155, ZERO, 42, deployer, deployer, 1000, 0); 37 | yieldBox.deposit(TokenType.ERC1155, erc1155, ZERO, 42, deployer, deployer, 0, 1000_00000000); 38 | yieldBox.withdraw(3, deployer, deployer, 1000, 0); 39 | yieldBox.withdraw(3, deployer, deployer, 0, 1000_00000000); 40 | 41 | yieldBox.depositETH{ value: 1000 }(ZERO, deployer, 1000); 42 | yieldBox.withdraw(4, deployer, deployer, 1000, 0); 43 | 44 | yieldBox.deposit(TokenType.ERC20, token, tokenStrategy, 0, deployer, deployer, 1000, 0); 45 | yieldBox.deposit(TokenType.ERC20, token, tokenStrategy, 0, deployer, deployer, 0, 1000_00000000); 46 | yieldBox.withdraw(5, deployer, deployer, 1000, 0); 47 | yieldBox.withdraw(5, deployer, deployer, 0, 1000_00000000); 48 | 49 | yieldBox.deposit(TokenType.ERC1155, erc1155, erc1155Strategy, 42, deployer, deployer, 1000, 0); 50 | yieldBox.deposit(TokenType.ERC1155, erc1155, erc1155Strategy, 42, deployer, deployer, 0, 1000_00000000); 51 | yieldBox.withdraw(6, deployer, deployer, 1000, 0); 52 | yieldBox.withdraw(6, deployer, deployer, 0, 1000_00000000); 53 | 54 | yieldBox.depositETH{ value: 1000 }(ethStrategy, deployer, 1000); 55 | yieldBox.withdraw(7, deployer, deployer, 1000, 0); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /contracts/YieldBoxRebase.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.9; 4 | pragma experimental ABIEncoderV2; 5 | import "./interfaces/IStrategy.sol"; 6 | import "@boringcrypto/boring-solidity/contracts/interfaces/IERC1155.sol"; 7 | import "@boringcrypto/boring-solidity/contracts/libraries/Base64.sol"; 8 | import "@boringcrypto/boring-solidity/contracts/libraries/BoringAddress.sol"; 9 | import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; 10 | import "@boringcrypto/boring-solidity/contracts/Domain.sol"; 11 | import "./ERC1155TokenReceiver.sol"; 12 | import "./ERC1155.sol"; 13 | import "@boringcrypto/boring-solidity/contracts/BoringBatchable.sol"; 14 | import "@boringcrypto/boring-solidity/contracts/BoringFactory.sol"; 15 | 16 | library YieldBoxRebase { 17 | /// @notice Calculates the base value in relationship to `elastic` and `total`. 18 | function _toShares( 19 | uint256 amount, 20 | uint256 totalShares_, 21 | uint256 totalAmount, 22 | bool roundUp 23 | ) internal pure returns (uint256 share) { 24 | // To prevent reseting the ratio due to withdrawal of all shares, we start with 25 | // 1 amount/1e8 shares already burned. This also starts with a 1 : 1e8 ratio which 26 | // functions like 8 decimal fixed point math. This prevents ratio attacks or inaccuracy 27 | // due to 'gifting' or rebasing tokens. (Up to a certain degree) 28 | totalAmount++; 29 | totalShares_ += 1e8; 30 | 31 | // Calculte the shares using te current amount to share ratio 32 | share = (amount * totalShares_) / totalAmount; 33 | 34 | // Default is to round down (Solidity), round up if required 35 | if (roundUp && (share * totalAmount) / totalShares_ < amount) { 36 | share++; 37 | } 38 | } 39 | 40 | /// @notice Calculates the elastic value in relationship to `base` and `total`. 41 | function _toAmount( 42 | uint256 share, 43 | uint256 totalShares_, 44 | uint256 totalAmount, 45 | bool roundUp 46 | ) internal pure returns (uint256 amount) { 47 | // To prevent reseting the ratio due to withdrawal of all shares, we start with 48 | // 1 amount/1e8 shares already burned. This also starts with a 1 : 1e8 ratio which 49 | // functions like 8 decimal fixed point math. This prevents ratio attacks or inaccuracy 50 | // due to 'gifting' or rebasing tokens. (Up to a certain degree) 51 | totalAmount++; 52 | totalShares_ += 1e8; 53 | 54 | // Calculte the amount using te current amount to share ratio 55 | amount = (share * totalAmount) / totalShares_; 56 | 57 | // Default is to round down (Solidity), round up if required 58 | if (roundUp && (amount * totalShares_) / totalAmount < share) { 59 | amount++; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /web3/classes/types/IGnosisSafe.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | import { BaseContract, BigNumber, BytesLike, CallOverrides, PopulatedTransaction, Signer, utils } from "ethers" 5 | import { FunctionFragment, Result } from "@ethersproject/abi" 6 | import { Listener, Provider } from "@ethersproject/providers" 7 | import { TypedEventFilter, TypedEvent, TypedListener, OnEvent } from "./common" 8 | 9 | export interface IGnosisSafeInterface extends utils.Interface { 10 | contractName: "IGnosisSafe" 11 | functions: { 12 | "getOwners()": FunctionFragment 13 | "getThreshold()": FunctionFragment 14 | } 15 | 16 | encodeFunctionData(functionFragment: "getOwners", values?: undefined): string 17 | encodeFunctionData(functionFragment: "getThreshold", values?: undefined): string 18 | 19 | decodeFunctionResult(functionFragment: "getOwners", data: BytesLike): Result 20 | decodeFunctionResult(functionFragment: "getThreshold", data: BytesLike): Result 21 | 22 | events: {} 23 | } 24 | 25 | export interface IGnosisSafe extends BaseContract { 26 | contractName: "IGnosisSafe" 27 | connect(signerOrProvider: Signer | Provider | string): this 28 | attach(addressOrName: string): this 29 | deployed(): Promise 30 | 31 | interface: IGnosisSafeInterface 32 | 33 | queryFilter( 34 | event: TypedEventFilter, 35 | fromBlockOrBlockhash?: string | number | undefined, 36 | toBlock?: string | number | undefined 37 | ): Promise> 38 | 39 | listeners(eventFilter?: TypedEventFilter): Array> 40 | listeners(eventName?: string): Array 41 | removeAllListeners(eventFilter: TypedEventFilter): this 42 | removeAllListeners(eventName?: string): this 43 | off: OnEvent 44 | on: OnEvent 45 | once: OnEvent 46 | removeListener: OnEvent 47 | 48 | functions: { 49 | getOwners(overrides?: CallOverrides): Promise<[string[]]> 50 | 51 | getThreshold(overrides?: CallOverrides): Promise<[BigNumber]> 52 | } 53 | 54 | getOwners(overrides?: CallOverrides): Promise 55 | 56 | getThreshold(overrides?: CallOverrides): Promise 57 | 58 | callStatic: { 59 | getOwners(overrides?: CallOverrides): Promise 60 | 61 | getThreshold(overrides?: CallOverrides): Promise 62 | } 63 | 64 | filters: {} 65 | 66 | estimateGas: { 67 | getOwners(overrides?: CallOverrides): Promise 68 | 69 | getThreshold(overrides?: CallOverrides): Promise 70 | } 71 | 72 | populateTransaction: { 73 | getOwners(overrides?: CallOverrides): Promise 74 | 75 | getThreshold(overrides?: CallOverrides): Promise 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /contracts/samples/lending/IOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.9; 3 | 4 | interface IOracle { 5 | /// @notice Get the latest exchange rate. 6 | /// @param data Usually abi encoded, implementation specific data that contains information and arguments to & about the oracle. 7 | /// For example: 8 | /// (string memory collateralSymbol, string memory assetSymbol, uint256 division) = abi.decode(data, (string, string, uint256)); 9 | /// @return success if no valid (recent) rate is available, return false else true. 10 | /// @return rate The rate of the requested asset / pair / pool. 11 | function get(bytes calldata data) external returns (bool success, uint256 rate); 12 | 13 | /// @notice Check the last exchange rate without any state changes. 14 | /// @param data Usually abi encoded, implementation specific data that contains information and arguments to & about the oracle. 15 | /// For example: 16 | /// (string memory collateralSymbol, string memory assetSymbol, uint256 division) = abi.decode(data, (string, string, uint256)); 17 | /// @return success if no valid (recent) rate is available, return false else true. 18 | /// @return rate The rate of the requested asset / pair / pool. 19 | function peek(bytes calldata data) external view returns (bool success, uint256 rate); 20 | 21 | /// @notice Check the current spot exchange rate without any state changes. For oracles like TWAP this will be different from peek(). 22 | /// @param data Usually abi encoded, implementation specific data that contains information and arguments to & about the oracle. 23 | /// For example: 24 | /// (string memory collateralSymbol, string memory assetSymbol, uint256 division) = abi.decode(data, (string, string, uint256)); 25 | /// @return rate The rate of the requested asset / pair / pool. 26 | function peekSpot(bytes calldata data) external view returns (uint256 rate); 27 | 28 | /// @notice Returns a human readable (short) name about this oracle. 29 | /// @param data Usually abi encoded, implementation specific data that contains information and arguments to & about the oracle. 30 | /// For example: 31 | /// (string memory collateralSymbol, string memory assetSymbol, uint256 division) = abi.decode(data, (string, string, uint256)); 32 | /// @return (string) A human readable symbol name about this oracle. 33 | function symbol(bytes calldata data) external view returns (string memory); 34 | 35 | /// @notice Returns a human readable name about this oracle. 36 | /// @param data Usually abi encoded, implementation specific data that contains information and arguments to & about the oracle. 37 | /// For example: 38 | /// (string memory collateralSymbol, string memory assetSymbol, uint256 division) = abi.decode(data, (string, string, uint256)); 39 | /// @return (string) A human readable name about this oracle. 40 | function name(bytes calldata data) external view returns (string memory); 41 | } 42 | -------------------------------------------------------------------------------- /contracts/mocks/ERC1155ReceiverMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.9; 3 | import "../ERC1155.sol"; 4 | 5 | contract ERC1155ReceiverMock is IERC1155TokenReceiver { 6 | address public sender; 7 | address public operator; 8 | address public from; 9 | uint256 public id; 10 | uint256[] public ids; 11 | uint256 public value; 12 | uint256[] public values; 13 | bytes public data; 14 | 15 | uint256 public fromBalance; 16 | 17 | function onERC1155Received( 18 | address _operator, 19 | address _from, 20 | uint256 _id, 21 | uint256 _value, 22 | bytes calldata _data 23 | ) external override returns (bytes4) { 24 | sender = msg.sender; 25 | operator = _operator; 26 | from = _from; 27 | id = _id; 28 | value = _value; 29 | data = _data; 30 | fromBalance = ERC1155(sender).balanceOf(from, id); 31 | 32 | return bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")); 33 | } 34 | 35 | function onERC1155BatchReceived( 36 | address _operator, 37 | address _from, 38 | uint256[] calldata _ids, 39 | uint256[] calldata _values, 40 | bytes calldata _data 41 | ) external override returns (bytes4) { 42 | sender = msg.sender; 43 | operator = _operator; 44 | from = _from; 45 | ids = _ids; 46 | values = _values; 47 | data = _data; 48 | if (ids.length > 0) { 49 | fromBalance = ERC1155(sender).balanceOf(from, ids[0]); 50 | } 51 | 52 | return bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)")); 53 | } 54 | 55 | function returnToken() external { 56 | ERC1155(sender).safeTransferFrom(address(this), from, id, value, ""); 57 | } 58 | 59 | function returnTokens() external { 60 | ERC1155(sender).safeBatchTransferFrom(address(this), from, ids, values, ""); 61 | } 62 | } 63 | 64 | contract ERC1155BrokenReceiverMock is IERC1155TokenReceiver { 65 | function onERC1155Received( 66 | address, 67 | address, 68 | uint256, 69 | uint256, 70 | bytes calldata 71 | ) external pure override returns (bytes4) { 72 | return bytes4(keccak256("wrong")); 73 | } 74 | 75 | function onERC1155BatchReceived( 76 | address, 77 | address, 78 | uint256[] calldata, 79 | uint256[] calldata, 80 | bytes calldata 81 | ) external pure override returns (bytes4) { 82 | return bytes4(keccak256("wrong")); 83 | } 84 | } 85 | 86 | contract ERC1155RevertingReceiverMock is IERC1155TokenReceiver { 87 | function onERC1155Received( 88 | address, 89 | address, 90 | uint256, 91 | uint256, 92 | bytes calldata 93 | ) external pure override returns (bytes4) { 94 | revert("Oops"); 95 | } 96 | 97 | function onERC1155BatchReceived( 98 | address, 99 | address, 100 | uint256[] calldata, 101 | uint256[] calldata, 102 | bytes calldata 103 | ) external pure override returns (bytes4) { 104 | revert("Oops"); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /web3/components/Menu.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 63 | 64 | 69 | -------------------------------------------------------------------------------- /test/samples/helloworld.ts: -------------------------------------------------------------------------------- 1 | import chai, { assert, expect } from "chai" 2 | import { solidity } from "ethereum-waffle" 3 | import { ethers } from "hardhat" 4 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers" 5 | import { 6 | ERC1155TokenReceiver, 7 | ERC1155TokenReceiver__factory, 8 | ERC20Mock, 9 | ERC20Mock__factory, 10 | ERC20WithSupply__factory, 11 | HelloWorld, 12 | HelloWorld__factory, 13 | NativeTokenFactory__factory, 14 | WETH9Mock__factory, 15 | YieldBox, 16 | YieldBoxURIBuilder__factory, 17 | YieldBox__factory, 18 | } from "../../typechain-types" 19 | chai.use(solidity) 20 | 21 | describe("Sample: Hello World", () => { 22 | let deployer: SignerWithAddress, alice: SignerWithAddress, bob: SignerWithAddress, carol: SignerWithAddress 23 | let Deployer: string, Alice: string, Bob: string, Carol: string 24 | const Zero = ethers.constants.AddressZero 25 | let hello: HelloWorld 26 | let yieldBox: YieldBox 27 | let token: ERC20Mock 28 | 29 | beforeEach(async () => { 30 | ;({ deployer, alice, bob, carol } = await ethers.getNamedSigners()) 31 | Deployer = deployer.address 32 | Alice = alice.address 33 | Bob = bob.address 34 | Carol = carol.address 35 | 36 | const weth = await new WETH9Mock__factory(deployer).deploy() 37 | await weth.deployed() 38 | 39 | const uriBuilder = await new YieldBoxURIBuilder__factory(deployer).deploy() 40 | await uriBuilder.deployed() 41 | 42 | yieldBox = await new YieldBox__factory(deployer).deploy(weth.address, uriBuilder.address) 43 | await yieldBox.deployed() 44 | 45 | token = await new ERC20Mock__factory(deployer).deploy(10000) 46 | await token.deployed() 47 | 48 | hello = await new HelloWorld__factory(deployer).deploy(yieldBox.address, token.address) 49 | await hello.deployed() 50 | 51 | await token.approve(yieldBox.address, 5000) 52 | 53 | await yieldBox.setApprovalForAll(hello.address, true) 54 | }) 55 | 56 | it("Deploy HelloWorld", async function () { 57 | assert.equal((await hello.deployTransaction.wait()).status, 1) 58 | }) 59 | 60 | it("deposit", async function () { 61 | await hello.deposit(1000) 62 | 63 | expect(await hello.balance()).equals(1000) 64 | expect(await yieldBox.balanceOf(hello.address, await hello.assetId())).equals(100000000000) 65 | }) 66 | 67 | it("withdraw", async function () { 68 | expect(await token.balanceOf(Deployer)).equals(10000) 69 | 70 | await hello.deposit(1000) 71 | await hello.withdraw() 72 | 73 | expect(await token.balanceOf(Deployer)).equals(10000) 74 | }) 75 | 76 | it("multiple deposits and withdraw", async function () { 77 | await hello.deposit(1000) 78 | await hello.deposit(1000) 79 | await hello.deposit(1000) 80 | await hello.deposit(1000) 81 | await hello.deposit(1000) 82 | 83 | expect(await hello.balance()).equals(5000) 84 | expect(await yieldBox.balanceOf(hello.address, await hello.assetId())).equals(500000000000) 85 | 86 | await hello.withdraw() 87 | 88 | expect(await hello.balance()).equals(0) 89 | expect(await yieldBox.balanceOf(hello.address, await hello.assetId())).equals(0) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /web3/components/YieldBoxModal.vue: -------------------------------------------------------------------------------- 1 | 94 | 95 | 115 | -------------------------------------------------------------------------------- /contracts/mocks/ERC1155StrategyMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.9; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "@boringcrypto/boring-solidity/contracts/interfaces/IERC1155.sol"; 6 | import "../enums/YieldBoxTokenType.sol"; 7 | import "../interfaces/IStrategy.sol"; 8 | import "../ERC1155TokenReceiver.sol"; 9 | 10 | // solhint-disable const-name-snakecase 11 | // solhint-disable no-empty-blocks 12 | 13 | contract ERC1155StrategyMock is IStrategy, ERC1155TokenReceiver { 14 | string public constant override name = "ERC1155StrategyMock"; 15 | string public constant override description = "Mock Strategy for testing"; 16 | 17 | TokenType public constant override tokenType = TokenType.ERC1155; 18 | address public immutable override contractAddress; 19 | uint256 public immutable override tokenId; 20 | 21 | IYieldBox public immutable yieldBox; 22 | 23 | constructor( 24 | IYieldBox yieldBox_, 25 | address token, 26 | uint256 tokenId_ 27 | ) { 28 | yieldBox = yieldBox_; 29 | contractAddress = token; 30 | tokenId = tokenId_; 31 | } 32 | 33 | /// Returns the total value the strategy holds (principle + gain) expressed in asset token amount. 34 | /// This should be cheap in gas to retrieve. Can return a bit less than the actual, but shouldn't return more. 35 | /// The gas cost of this function will be paid on any deposit or withdrawal onto and out of the YieldBox 36 | /// that uses this strategy. Also, anytime a protocol converts between shares and amount, this gets called. 37 | function currentBalance() public view override returns (uint256 amount) { 38 | return IERC1155(contractAddress).balanceOf(address(this), tokenId); 39 | } 40 | 41 | /// Returns the maximum amount that can be withdrawn 42 | function withdrawable() external view override returns (uint256 amount) { 43 | return IERC1155(contractAddress).balanceOf(address(this), tokenId); 44 | } 45 | 46 | /// Returns the maximum amount that can be withdrawn for a low gas fee 47 | /// When more than this amount is withdrawn it will trigger divesting from the actual strategy 48 | /// which will incur higher gas costs 49 | function cheapWithdrawable() external view override returns (uint256 amount) { 50 | return IERC1155(contractAddress).balanceOf(address(this), tokenId); 51 | } 52 | 53 | /// Is called by YieldBox to signal funds have been added, the strategy may choose to act on this 54 | /// When a large enough deposit is made, this should trigger the strategy to invest into the actual 55 | /// strategy. This function should normally NOT be used to invest on each call as that would be costly 56 | /// for small deposits. 57 | /// Only accept this call from the YieldBox 58 | function deposited(uint256 amount) external override {} 59 | 60 | /// Is called by the YieldBox to ask the strategy to withdraw to the user 61 | /// When a strategy keeps a little reserve for cheap withdrawals and the requested withdrawal goes over this amount, 62 | /// the strategy should divest enough from the strategy to complete the withdrawal and rebalance the reserve. 63 | /// Only accept this call from the YieldBox 64 | function withdraw(address to, uint256 amount) external override { 65 | IERC1155(contractAddress).safeTransferFrom(address(this), to, tokenId, amount, ""); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /contracts/mocks/ERC20StrategyMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.9; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol"; 6 | import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; 7 | import "../enums/YieldBoxTokenType.sol"; 8 | import "../interfaces/IStrategy.sol"; 9 | import "../interfaces/IYieldBox.sol"; 10 | 11 | // solhint-disable const-name-snakecase 12 | // solhint-disable no-empty-blocks 13 | 14 | contract ERC20StrategyMock is IStrategy { 15 | using BoringERC20 for IERC20; 16 | 17 | string public constant override name = "ERC20StrategyMock"; 18 | string public constant override description = "Mock Strategy for testing"; 19 | 20 | TokenType public constant override tokenType = TokenType.ERC20; 21 | address public immutable override contractAddress; 22 | uint256 public constant override tokenId = 0; 23 | 24 | IYieldBox public immutable override yieldBox; 25 | 26 | constructor(IYieldBox yieldBox_, address token) { 27 | yieldBox = yieldBox_; 28 | contractAddress = token; 29 | } 30 | 31 | /// Returns the total value the strategy holds (principle + gain) expressed in asset token amount. 32 | /// This should be cheap in gas to retrieve. Can return a bit less than the actual, but shouldn't return more. 33 | /// The gas cost of this function will be paid on any deposit or withdrawal onto and out of the YieldBox 34 | /// that uses this strategy. Also, anytime a protocol converts between shares and amount, this gets called. 35 | function currentBalance() public view override returns (uint256 amount) { 36 | return IERC20(contractAddress).balanceOf(address(this)); 37 | } 38 | 39 | /// Returns the maximum amount that can be withdrawn 40 | function withdrawable() external view override returns (uint256 amount) { 41 | return IERC20(contractAddress).balanceOf(address(this)); 42 | } 43 | 44 | /// Returns the maximum amount that can be withdrawn for a low gas fee 45 | /// When more than this amount is withdrawn it will trigger divesting from the actual strategy 46 | /// which will incur higher gas costs 47 | function cheapWithdrawable() external view override returns (uint256 amount) { 48 | return IERC20(contractAddress).balanceOf(address(this)); 49 | } 50 | 51 | /// Is called by YieldBox to signal funds have been added, the strategy may choose to act on this 52 | /// When a large enough deposit is made, this should trigger the strategy to invest into the actual 53 | /// strategy. This function should normally NOT be used to invest on each call as that would be costly 54 | /// for small deposits. 55 | /// Only accept this call from the YieldBox 56 | function deposited(uint256 amount) external override {} 57 | 58 | /// Is called by the YieldBox to ask the strategy to withdraw to the user 59 | /// When a strategy keeps a little reserve for cheap withdrawals and the requested withdrawal goes over this amount, 60 | /// the strategy should divest enough from the strategy to complete the withdrawal and rebalance the reserve. 61 | /// Only accept this call from the YieldBox 62 | function withdraw(address to, uint256 amount) external override { 63 | if (contractAddress == yieldBox.wrappedNative()) {} else { 64 | IERC20(contractAddress).safeTransfer(to, amount); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | if (process.env.DOTENV_PATH) { 2 | console.log("Using custom .env path:", process.env.DOTENV_PATH) 3 | require("dotenv").config({ path: process.env.DOTENV_PATH }) 4 | } else { 5 | require("dotenv").config() 6 | } 7 | 8 | import { HardhatUserConfig, task } from "hardhat/config" 9 | import "@nomiclabs/hardhat-ethers" 10 | import "@nomiclabs/hardhat-etherscan" 11 | import "@nomiclabs/hardhat-waffle" 12 | import "@typechain/hardhat" 13 | import "hardhat-gas-reporter" 14 | import "hardhat-deploy" 15 | import "solidity-coverage" 16 | import "@boringcrypto/hardhat-framework" 17 | import { ethers, BigNumber } from "ethers" 18 | import requestSync from "sync-request" 19 | 20 | // This is a sample Hardhat task. To learn how to create your own go to 21 | // https://hardhat.org/guides/create-task.html 22 | task("accounts", "Prints the list of accounts", async (taskArgs, hre) => { 23 | const accounts = await hre.ethers.getSigners() 24 | 25 | for (const account of accounts) { 26 | console.log(account.address) 27 | } 28 | 29 | console.log(await hre.getNamedAccounts()) 30 | }) 31 | 32 | // You need to export an object to set up your config 33 | // Go to https://hardhat.org/config/ to learn more 34 | 35 | const last_block = 36 | process.env.ALCHEMY_API_KEY && false 37 | ? BigNumber.from( 38 | JSON.parse( 39 | requestSync("GET", "https://api.etherscan.io/api?module=proxy&action=eth_blockNumber&apikey=YourApiKeyToken").body as string 40 | ).result 41 | ) 42 | : BigNumber.from(14333352) 43 | 44 | console.log( 45 | process.env.ALCHEMY_API_KEY 46 | ? "Forking from block " + (last_block.toNumber() - 6).toString() 47 | : "Please add your Alchemy key to the ALCHEMY_API_KEY environment variable or to .env" 48 | ) 49 | 50 | const config: HardhatUserConfig = { 51 | solidity: { 52 | version: "0.8.9", 53 | settings: { 54 | optimizer: { 55 | enabled: true, 56 | runs: 50000, 57 | }, 58 | }, 59 | }, 60 | networks: { 61 | hardhat: Object.assign( 62 | { 63 | live: false, 64 | blockGasLimit: 30_000_000, 65 | allowUnlimitedContractSize: true, 66 | }, 67 | process.env.ALCHEMY_API_KEY 68 | ? { 69 | forking: { 70 | url: `https://eth-mainnet.alchemyapi.io/v2/${process.env.ALCHEMY_API_KEY}`, 71 | blockNumber: last_block.toNumber() - 6, 72 | }, 73 | } 74 | : {} 75 | ), 76 | }, 77 | namedAccounts: { 78 | deployer: { default: 0 }, 79 | alice: { default: 1 }, 80 | bob: { default: 2 }, 81 | carol: { default: 3 }, 82 | dave: { default: 4 }, 83 | eve: { default: 5 }, 84 | frank: { default: 6 }, 85 | grace: { default: 7 }, 86 | }, 87 | gasReporter: { 88 | enabled: true, 89 | currency: "USD", 90 | outputFile: "gas_report.txt", 91 | noColors: true, 92 | showMethodSig: true, 93 | coinmarketcap: process.env.COINMARKETCAP_API_KEY, 94 | }, 95 | etherscan: { 96 | apiKey: process.env.ETHERSCAN_API_KEY, 97 | }, 98 | } 99 | 100 | export default config 101 | -------------------------------------------------------------------------------- /web3/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp, reactive } from "vue" 2 | import { createRouter, createWebHashHistory } from "vue-router" 3 | import { BigNumber } from "ethers" 4 | import BootstrapVue from "bootstrap-vue-3" 5 | 6 | import "bootswatch/dist/litera/bootstrap.min.css" 7 | import "bootstrap-vue-3/dist/bootstrap-vue-3.css" 8 | 9 | import App from "./App.vue" 10 | import Home from "./pages/Home.vue" 11 | import Escrow from "./pages/Escrow.vue" 12 | import Lending from "./pages/Lending.vue" 13 | import Salary from "./pages/Salary.vue" 14 | import Swap from "./pages/Swap.vue" 15 | import Tokenizer from "./pages/Tokenizer.vue" 16 | import YieldBoxBalances from "./pages/YieldBoxBalances.vue" 17 | 18 | import Data from "./data-web3" 19 | import Decimal from "decimal.js-light" 20 | import { Token } from "./classes/TokenManager" 21 | import { Account } from "./classes/Account" 22 | 23 | Decimal.config({ precision: 36 }) 24 | Decimal.config({ toExpNeg: -1000 }) 25 | Decimal.config({ toExpPos: 1000 }) 26 | 27 | // this is just for debugging 28 | declare global { 29 | interface Window { 30 | data: any 31 | } 32 | } 33 | 34 | declare module "decimal.js-light" { 35 | interface Decimal { 36 | toInt: (decimals: number) => BigNumber 37 | } 38 | } 39 | 40 | Decimal.prototype.toInt = function (decimals: number) { 41 | return BigNumber.from( 42 | this.times(new Decimal("10").pow(new Decimal(decimals.toString()))) 43 | .todp(0) 44 | .toString() 45 | ) 46 | } 47 | 48 | declare module "ethers" { 49 | interface BigNumber { 50 | toDec: (decimals?: number) => Decimal 51 | toDisplay: (token?: Token) => string 52 | } 53 | } 54 | 55 | BigNumber.prototype.toDec = function (decimals?: number) { 56 | return new Decimal(this.toString()).dividedBy(new Decimal(10).toPower((decimals || 0).toString())) 57 | } 58 | BigNumber.prototype.toDisplay = function (token?: Token) { 59 | const decimal = this?.toDec(token?.decimals || 0) || new Decimal(0) 60 | if (decimal.gt(10000)) { 61 | return decimal.toFixed(0) 62 | } else { 63 | return decimal.toSignificantDigits(4).toString() 64 | } 65 | } 66 | 67 | const BigNumberMax = (...args: BigNumber[]) => args.reduce((m, e) => (e > m ? e : m)) 68 | const BigNumberMin = (...args: BigNumber[]) => args.reduce((m, e) => (e < m ? e : m)) 69 | 70 | declare module "@vue/runtime-core" { 71 | interface ComponentCustomProperties { 72 | app: typeof Data 73 | } 74 | } 75 | 76 | async function main() { 77 | const app = createApp(App) 78 | Data.web3.onAccountChanged = (address) => { 79 | Data.account = new Account(address) 80 | } 81 | await Data.web3.setup() 82 | window.data = Data 83 | app.config.globalProperties.app = reactive(Data) 84 | app.provide("app", app.config.globalProperties.app) 85 | 86 | app.use( 87 | createRouter({ 88 | history: createWebHashHistory(), 89 | routes: [ 90 | { path: "/", component: Home }, 91 | { path: "/escrow", component: Escrow }, 92 | { path: "/salary", component: Salary }, 93 | { path: "/swap", component: Swap }, 94 | { path: "/tokenizer", component: Tokenizer }, 95 | { path: "/lending", component: Lending }, 96 | { path: "/yieldbox/balances/:address", component: YieldBoxBalances }, 97 | ], 98 | }) 99 | ) 100 | app.use(BootstrapVue) 101 | app.mount("#app") 102 | } 103 | 104 | main() 105 | -------------------------------------------------------------------------------- /contracts/AssetRegister.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.9; 3 | import "./interfaces/IStrategy.sol"; 4 | import "@boringcrypto/boring-solidity/contracts/libraries/BoringAddress.sol"; 5 | import "./ERC1155.sol"; 6 | 7 | // An asset is a token + a strategy 8 | struct Asset { 9 | TokenType tokenType; 10 | address contractAddress; 11 | IStrategy strategy; 12 | uint256 tokenId; 13 | } 14 | 15 | contract AssetRegister is ERC1155 { 16 | using BoringAddress for address; 17 | 18 | event AssetRegistered( 19 | TokenType indexed tokenType, 20 | address indexed contractAddress, 21 | IStrategy strategy, 22 | uint256 indexed tokenId, 23 | uint256 assetId 24 | ); 25 | 26 | // ids start at 1 so that id 0 means it's not yet registered 27 | mapping(TokenType => mapping(address => mapping(IStrategy => mapping(uint256 => uint256)))) public ids; 28 | Asset[] public assets; 29 | 30 | constructor() { 31 | assets.push(Asset(TokenType.None, address(0), NO_STRATEGY, 0)); 32 | } 33 | 34 | function assetCount() public view returns (uint256) { 35 | return assets.length; 36 | } 37 | 38 | function _registerAsset( 39 | TokenType tokenType, 40 | address contractAddress, 41 | IStrategy strategy, 42 | uint256 tokenId 43 | ) internal returns (uint256 assetId) { 44 | // Checks 45 | assetId = ids[tokenType][contractAddress][strategy][tokenId]; 46 | 47 | // If assetId is 0, this is a new asset that needs to be registered 48 | if (assetId == 0) { 49 | // Only do these checks if a new asset needs to be created 50 | require(tokenId == 0 || tokenType != TokenType.ERC20, "YieldBox: No tokenId for ERC20"); 51 | require( 52 | tokenType == TokenType.Native || 53 | (tokenType == strategy.tokenType() && contractAddress == strategy.contractAddress() && tokenId == strategy.tokenId()), 54 | "YieldBox: Strategy mismatch" 55 | ); 56 | // If a new token gets added, the isContract checks that this is a deployed contract. Needed for security. 57 | // Prevents getting shares for a future token whose address is known in advance. For instance a token that will be deployed with CREATE2 in the future or while the contract creation is 58 | // in the mempool 59 | require((tokenType == TokenType.Native && contractAddress == address(0)) || contractAddress.isContract(), "YieldBox: Not a token"); 60 | 61 | // Effects 62 | assetId = assets.length; 63 | assets.push(Asset(tokenType, contractAddress, strategy, tokenId)); 64 | ids[tokenType][contractAddress][strategy][tokenId] = assetId; 65 | 66 | // The actual URI isn't emitted here as per EIP1155, because that would make this call super expensive. 67 | emit URI("", assetId); 68 | emit AssetRegistered(tokenType, contractAddress, strategy, tokenId, assetId); 69 | } 70 | } 71 | 72 | function registerAsset( 73 | TokenType tokenType, 74 | address contractAddress, 75 | IStrategy strategy, 76 | uint256 tokenId 77 | ) public returns (uint256 assetId) { 78 | // Native assets can only be added internally by the NativeTokenFactory 79 | require( 80 | tokenType == TokenType.ERC20 || tokenType == TokenType.ERC721 || tokenType == TokenType.ERC1155, 81 | "AssetManager: cannot add Native" 82 | ); 83 | assetId = _registerAsset(tokenType, contractAddress, strategy, tokenId); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /web3/components/Web3Modal.vue: -------------------------------------------------------------------------------- 1 | 99 | 100 | 113 | -------------------------------------------------------------------------------- /workbench/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp, reactive, ref, Ref } from "vue" 2 | import { createRouter, createWebHashHistory } from "vue-router" 3 | import { BigNumber } from "ethers" 4 | import "bootstrap-icons/font/bootstrap-icons.css" 5 | import BootstrapVue from "bootstrap-vue-3" 6 | 7 | import "bootswatch/dist/litera/bootstrap.min.css" 8 | import "bootstrap-vue-3/dist/bootstrap-vue-3.css" 9 | 10 | import App from "./App.vue" 11 | import Home from "./pages/Home.vue" 12 | import Block from "./pages/Block.vue" 13 | import Address from "./pages/Address.vue" 14 | 15 | import Data from "./data-workbench" 16 | import Decimal from "decimal.js-light" 17 | import { Token } from "../web3/classes/TokenManager" 18 | import { HardhatProvider, hardhat } from "./classes/HardhatProvider" 19 | 20 | Decimal.config({ precision: 36 }) 21 | Decimal.config({ toExpNeg: -1000 }) 22 | Decimal.config({ toExpPos: 1000 }) 23 | 24 | // this is just for debugging 25 | declare global { 26 | interface Window { 27 | data: any 28 | } 29 | } 30 | 31 | declare module "decimal.js-light" { 32 | interface Decimal { 33 | toInt: (decimals: number) => BigNumber 34 | } 35 | } 36 | 37 | Decimal.prototype.toInt = function (decimals: number) { 38 | return BigNumber.from( 39 | this.times(new Decimal("10").pow(new Decimal(decimals.toString()))) 40 | .todp(0) 41 | .toString() 42 | ) 43 | } 44 | 45 | declare module "ethers" { 46 | interface BigNumber { 47 | toDec: (decimals?: number) => Decimal 48 | toDisplay: (token?: Token) => string 49 | } 50 | } 51 | 52 | BigNumber.prototype.toDec = function (decimals?: number) { 53 | return new Decimal(this.toString()).dividedBy(new Decimal(10).toPower((decimals || 0).toString())) 54 | } 55 | BigNumber.prototype.toDisplay = function (token?: Token) { 56 | const decimal = this?.toDec(token?.decimals || 0) || new Decimal(0) 57 | if (decimal.gt(10000)) { 58 | return decimal.toFixed(0) 59 | } else { 60 | return decimal.toSignificantDigits(4).toString() 61 | } 62 | } 63 | 64 | const BigNumberMax = (...args: BigNumber[]) => args.reduce((m, e) => (e > m ? e : m)) 65 | const BigNumberMin = (...args: BigNumber[]) => args.reduce((m, e) => (e < m ? e : m)) 66 | 67 | declare module "@vue/runtime-core" { 68 | interface ComponentCustomProperties { 69 | app: typeof Data 70 | hardhat: HardhatProvider 71 | now: Ref 72 | } 73 | } 74 | 75 | function setupNow() { 76 | let now: Ref = ref(Date.now()) 77 | window.setInterval(() => (now.value = Date.now()), 1000) 78 | return now 79 | } 80 | 81 | async function main() { 82 | Data.setup() 83 | hardhat.accounts.forEach((account) => { 84 | Data.addNamedAddress({ 85 | address: account.address, 86 | type: "wallet", 87 | name: account.name, 88 | object: account, 89 | }) 90 | }) 91 | 92 | const app = createApp(App) 93 | window.data = Data 94 | app.config.globalProperties.app = reactive(Data) 95 | app.config.globalProperties.hardhat = hardhat 96 | app.config.globalProperties.now = setupNow() 97 | app.provide("app", app.config.globalProperties.app) 98 | app.provide("hardhat", app.config.globalProperties.hardhat) 99 | app.provide("now", app.config.globalProperties.now) 100 | 101 | app.use( 102 | createRouter({ 103 | history: createWebHashHistory(), 104 | routes: [ 105 | { path: "/", component: Home }, 106 | { path: "/block/:number", component: Block }, 107 | { path: "/address/:address", component: Address }, 108 | ], 109 | }) 110 | ) 111 | app.use(BootstrapVue) 112 | app.mount("#app") 113 | } 114 | 115 | main() 116 | -------------------------------------------------------------------------------- /contracts/mocks/ReturnFalseERC20Mock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.9; 3 | 4 | // solhint-disable no-inline-assembly 5 | // solhint-disable not-rely-on-time 6 | 7 | // ReturnFalseERC20 does not revert on errors, it just returns false 8 | contract ReturnFalseERC20Mock { 9 | string public symbol; 10 | string public name; 11 | uint8 public immutable decimals; 12 | uint256 public totalSupply; 13 | mapping(address => uint256) public balanceOf; 14 | mapping(address => mapping(address => uint256)) public allowance; 15 | mapping(address => uint256) public nonces; 16 | 17 | event Transfer(address indexed _from, address indexed _to, uint256 _value); 18 | event Approval(address indexed _owner, address indexed _spender, uint256 _value); 19 | 20 | constructor( 21 | string memory name_, 22 | string memory symbol_, 23 | uint8 decimals_, 24 | uint256 supply 25 | ) { 26 | name = name_; 27 | symbol = symbol_; 28 | decimals = decimals_; 29 | totalSupply = supply; 30 | balanceOf[msg.sender] = supply; 31 | } 32 | 33 | function transfer(address to, uint256 amount) public returns (bool success) { 34 | if (balanceOf[msg.sender] >= amount && balanceOf[to] + amount >= balanceOf[to]) { 35 | balanceOf[msg.sender] -= amount; 36 | balanceOf[to] += amount; 37 | emit Transfer(msg.sender, to, amount); 38 | return true; 39 | } else { 40 | return false; 41 | } 42 | } 43 | 44 | function transferFrom( 45 | address from, 46 | address to, 47 | uint256 amount 48 | ) public returns (bool success) { 49 | if (balanceOf[from] >= amount && allowance[from][msg.sender] >= amount && balanceOf[to] + amount >= balanceOf[to]) { 50 | balanceOf[from] -= amount; 51 | allowance[from][msg.sender] -= amount; 52 | balanceOf[to] += amount; 53 | emit Transfer(from, to, amount); 54 | return true; 55 | } else { 56 | return false; 57 | } 58 | } 59 | 60 | function approve(address spender, uint256 amount) public returns (bool success) { 61 | allowance[msg.sender][spender] = amount; 62 | emit Approval(msg.sender, spender, amount); 63 | return true; 64 | } 65 | 66 | // solhint-disable-next-line func-name-mixedcase 67 | function DOMAIN_SEPARATOR() public view returns (bytes32) { 68 | uint256 chainId; 69 | assembly { 70 | chainId := chainid() 71 | } 72 | return keccak256(abi.encode(keccak256("EIP712Domain(uint256 chainId,address verifyingContract)"), chainId, address(this))); 73 | } 74 | 75 | function permit( 76 | address owner, 77 | address spender, 78 | uint256 value, 79 | uint256 deadline, 80 | uint8 v, 81 | bytes32 r, 82 | bytes32 s 83 | ) external { 84 | require(block.timestamp < deadline, "ReturnFalseERC20: Expired"); 85 | bytes32 digest = keccak256( 86 | abi.encodePacked( 87 | "\x19\x01", 88 | DOMAIN_SEPARATOR(), 89 | keccak256( 90 | abi.encode( 91 | 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9, 92 | owner, 93 | spender, 94 | value, 95 | nonces[owner]++, 96 | deadline 97 | ) 98 | ) 99 | ) 100 | ); 101 | address recoveredAddress = ecrecover(digest, v, r, s); 102 | require(recoveredAddress == owner, "ReturnFalseERC20: Invalid Sig"); 103 | allowance[owner][spender] = value; 104 | emit Approval(owner, spender, value); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /test/AssetRegister.ts: -------------------------------------------------------------------------------- 1 | import chai, { assert, expect } from "chai" 2 | import { solidity } from "ethereum-waffle" 3 | import { ethers } from "hardhat" 4 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers" 5 | import { 6 | AssetRegister, 7 | AssetRegister__factory, 8 | BoringFactory__factory, 9 | SushiStakingStrategy, 10 | SushiStakingStrategy__factory, 11 | } from "../typechain-types" 12 | import { TokenType } from "../sdk" 13 | chai.use(solidity) 14 | 15 | describe("AssetRegister", () => { 16 | let deployer: SignerWithAddress, alice: SignerWithAddress, bob: SignerWithAddress, carol: SignerWithAddress 17 | let Deployer: string, Alice: string, Bob: string, Carol: string 18 | const Zero = ethers.constants.AddressZero 19 | let register: AssetRegister 20 | let sushiStrategy: SushiStakingStrategy 21 | const sushi = "0x6B3595068778DD592e39A122f4f5a5cF09C90fE2" 22 | const rarible = "0xd07dc4262BCDbf85190C01c996b4C06a461d2430" 23 | 24 | beforeEach(async () => { 25 | ;({ deployer, alice, bob, carol } = await ethers.getNamedSigners()) 26 | Deployer = deployer.address 27 | Alice = alice.address 28 | Bob = bob.address 29 | Carol = carol.address 30 | register = await new AssetRegister__factory(deployer).deploy() 31 | await register.deployed() 32 | 33 | sushiStrategy = await new SushiStakingStrategy__factory(deployer).deploy(Zero) 34 | await sushiStrategy.deployed() 35 | }) 36 | 37 | it("Deploy ERC1155TokenReceiver", async function () { 38 | assert.equal((await register.deployTransaction.wait()).status, 1) 39 | }) 40 | 41 | it("can register an asset", async function () { 42 | await expect(register.registerAsset(TokenType.ERC20, sushi, Zero, 0)).to.emit(register, "URI").withArgs("", 1) 43 | await expect(register.registerAsset(TokenType.ERC1155, rarible, Zero, 628973)).to.emit(register, "URI").withArgs("", 2) 44 | await expect(register.registerAsset(TokenType.ERC20, sushi, Zero, 0)).to.not.emit(register, "URI") 45 | await expect(register.registerAsset(TokenType.ERC1155, rarible, Zero, 628973)).to.not.emit(register, "URI") 46 | 47 | await expect(register.registerAsset(TokenType.ERC20, sushi, sushiStrategy.address, 0)).to.emit(register, "URI").withArgs("", 3) 48 | 49 | expect(await register.ids(TokenType.ERC1155, rarible, Zero, 628973)).equals(2) 50 | 51 | let asset = await register.assets(2) 52 | expect(asset.tokenType).equals(TokenType.ERC1155) 53 | expect(asset.contractAddress).equals(rarible) 54 | expect(asset.strategy).equals(Zero) 55 | expect(asset.tokenId).equals(628973) 56 | 57 | asset = await register.assets(3) 58 | expect(asset.tokenType).equals(TokenType.ERC20) 59 | expect(asset.contractAddress).equals(sushi) 60 | expect(asset.strategy).equals(sushiStrategy.address) 61 | expect(asset.tokenId).equals(0) 62 | }) 63 | 64 | it("cannot register a Native asset", async function () { 65 | await expect(register.registerAsset(TokenType.Native, Zero, Zero, 0)).to.be.revertedWith("AssetManager: cannot add Native") 66 | }) 67 | 68 | it("cannot add an ERC20 token with a tokenId", async function () { 69 | await expect(register.registerAsset(TokenType.ERC20, sushi, sushiStrategy.address, 1)).to.be.revertedWith("No tokenId for ERC20") 70 | }) 71 | 72 | it("cannot register an asset with a mismatching strategy", async function () { 73 | await expect(register.registerAsset(TokenType.ERC1155, rarible, sushiStrategy.address, 628973)).to.be.revertedWith("Strategy mismatch") 74 | }) 75 | 76 | it("cannot use an EOA as contractAddress", async function () { 77 | await expect(register.registerAsset(TokenType.ERC20, Alice, Zero, 0)).to.be.revertedWith("Not a token") 78 | await expect(register.registerAsset(TokenType.ERC1155, Bob, Zero, 0)).to.be.revertedWith("Not a token") 79 | await expect(register.registerAsset(TokenType.ERC20, Zero, Zero, 0)).to.be.revertedWith("Not a token") 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /contracts/interfaces/IStrategy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.9; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "../enums/YieldBoxTokenType.sol"; 6 | import "./IYieldBox.sol"; 7 | 8 | interface IStrategy { 9 | /// Each strategy only works with a single asset. This should help make implementations simpler and more readable. 10 | /// To safe gas a proxy pattern (YieldBox factory) could be used to deploy the same strategy for multiple tokens. 11 | 12 | /// It is recommended that strategies keep a small amount of funds uninvested (like 5%) to handle small withdrawals 13 | /// and deposits without triggering costly investing/divesting logic. 14 | 15 | /// ######################### 16 | /// ### Basic Information ### 17 | /// ######################### 18 | 19 | /// Returns the address of the yieldBox that this strategy is for 20 | function yieldBox() external view returns (IYieldBox yieldBox_); 21 | 22 | /// Returns a name for this strategy 23 | function name() external view returns (string memory name_); 24 | 25 | /// Returns a description for this strategy 26 | function description() external view returns (string memory description_); 27 | 28 | /// ####################### 29 | /// ### Supported Token ### 30 | /// ####################### 31 | 32 | /// Returns the standard that this strategy works with 33 | function tokenType() external view returns (TokenType tokenType_); 34 | 35 | /// Returns the contract address that this strategy works with 36 | function contractAddress() external view returns (address contractAddress_); 37 | 38 | /// Returns the tokenId that this strategy works with (for EIP1155) 39 | /// This is always 0 for EIP20 tokens 40 | function tokenId() external view returns (uint256 tokenId_); 41 | 42 | /// ########################### 43 | /// ### Balance Information ### 44 | /// ########################### 45 | 46 | /// Returns the total value the strategy holds (principle + gain) expressed in asset token amount. 47 | /// This should be cheap in gas to retrieve. Can return a bit less than the actual, but MUST NOT return more. 48 | /// The gas cost of this function will be paid on any deposit or withdrawal onto and out of the YieldBox 49 | /// that uses this strategy. Also, anytime a protocol converts between shares and amount, this gets called. 50 | function currentBalance() external view returns (uint256 amount); 51 | 52 | /// Returns the maximum amount that can be withdrawn 53 | function withdrawable() external view returns (uint256 amount); 54 | 55 | /// Returns the maximum amount that can be withdrawn for a low gas fee 56 | /// When more than this amount is withdrawn it will trigger divesting from the actual strategy 57 | /// which will incur higher gas costs 58 | function cheapWithdrawable() external view returns (uint256 amount); 59 | 60 | /// ########################## 61 | /// ### YieldBox Functions ### 62 | /// ########################## 63 | 64 | /// Is called by YieldBox to signal funds have been added, the strategy may choose to act on this 65 | /// When a large enough deposit is made, this should trigger the strategy to invest into the actual 66 | /// strategy. This function should normally NOT be used to invest on each call as that would be costly 67 | /// for small deposits. 68 | /// If the strategy handles native tokens (ETH) it will receive it directly (not wrapped). It will be 69 | /// up to the strategy to wrap it if needed. 70 | /// Only accept this call from the YieldBox 71 | function deposited(uint256 amount) external; 72 | 73 | /// Is called by the YieldBox to ask the strategy to withdraw to the user 74 | /// When a strategy keeps a little reserve for cheap withdrawals and the requested withdrawal goes over this amount, 75 | /// the strategy should divest enough from the strategy to complete the withdrawal and rebalance the reserve. 76 | /// If the strategy handles native tokens (ETH) it should send this, not a wrapped version. 77 | /// With some strategies it might be hard to withdraw exactly the correct amount. 78 | /// Only accept this call from the YieldBox 79 | function withdraw(address to, uint256 amount) external; 80 | } 81 | 82 | IStrategy constant NO_STRATEGY = IStrategy(address(0)); 83 | -------------------------------------------------------------------------------- /workbench/classes/HardhatProvider.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, ethers } from "ethers" 2 | import { BlockWithTransactions } from "@ethersproject/abstract-provider" 3 | import { computed, markRaw, reactive } from "vue" 4 | 5 | type HardhatTransaction = { 6 | hash: string 7 | type: number 8 | accessList: any 9 | blockHash: string 10 | blockNumber: number 11 | transactionIndex: number 12 | confirmation: number 13 | from: string 14 | gasPrice: BigNumber 15 | maxPriorityFeePerGas?: BigNumber 16 | maxFeePerGas?: BigNumber 17 | gasLimit: BigNumber 18 | to?: string 19 | value: BigNumber 20 | nonce: number 21 | data: string 22 | r: string 23 | s: string 24 | v: number 25 | chainId: number 26 | creates?: string 27 | } 28 | 29 | class NamedWallet extends ethers.Wallet { 30 | name = "" as string 31 | } 32 | 33 | class HardhatProvider { 34 | provider: ethers.providers.JsonRpcProvider 35 | deployer: NamedWallet 36 | alice: NamedWallet 37 | bob: NamedWallet 38 | carol: NamedWallet 39 | dirk: NamedWallet 40 | erin: NamedWallet 41 | fred: NamedWallet 42 | accounts: NamedWallet[] 43 | named_accounts 44 | block = reactive({ 45 | number: 0, 46 | }) 47 | blocks: { [number: number]: BlockWithTransactions } = reactive({}) 48 | txs: { [txhash: string]: ethers.providers.TransactionResponse } = reactive({}) 49 | 50 | constructor() { 51 | const mnemonic = "test test test test test test test test test test test junk" 52 | this.provider = markRaw(new ethers.providers.JsonRpcProvider("http://127.0.0.1:8545/")) 53 | this.deployer = ethers.Wallet.fromMnemonic(mnemonic, "m/44'/60'/0'/0/0").connect(this.provider) as NamedWallet 54 | this.deployer.name = "Deployer" 55 | this.alice = ethers.Wallet.fromMnemonic(mnemonic, "m/44'/60'/0'/0/1").connect(this.provider) as NamedWallet 56 | this.alice.name = "Alice" 57 | this.bob = ethers.Wallet.fromMnemonic(mnemonic, "m/44'/60'/0'/0/2").connect(this.provider) as NamedWallet 58 | this.bob.name = "Bob" 59 | this.carol = ethers.Wallet.fromMnemonic(mnemonic, "m/44'/60'/0'/0/3").connect(this.provider) as NamedWallet 60 | this.carol.name = "Carol" 61 | this.dirk = ethers.Wallet.fromMnemonic(mnemonic, "m/44'/60'/0'/0/4").connect(this.provider) as NamedWallet 62 | this.dirk.name = "Dirk" 63 | this.erin = ethers.Wallet.fromMnemonic(mnemonic, "m/44'/60'/0'/0/5").connect(this.provider) as NamedWallet 64 | this.erin.name = "Erin" 65 | this.fred = ethers.Wallet.fromMnemonic(mnemonic, "m/44'/60'/0'/0/6").connect(this.provider) as NamedWallet 66 | this.fred.name = "Fred" 67 | this.accounts = [this.deployer, this.alice, this.bob, this.carol, this.dirk, this.erin, this.fred] 68 | this.named_accounts = { 69 | deployer: this.deployer, 70 | alice: this.alice, 71 | bob: this.bob, 72 | carol: this.carol, 73 | dirk: this.dirk, 74 | erin: this.erin, 75 | fred: this.fred, 76 | } 77 | 78 | this.provider.on("block", async (number: number) => { 79 | await this.getBlock(number) 80 | this.block.number = number 81 | }) 82 | } 83 | 84 | async getBlock(number: string | number) { 85 | if (typeof number == "string") { 86 | number = parseInt(number) 87 | } 88 | let block = this.blocks[number] 89 | if (!block) { 90 | block = await this.provider.getBlockWithTransactions(number) 91 | this.blocks[number] = block 92 | for (const hash in block.transactions) { 93 | const tx = block.transactions[hash] 94 | this.txs[tx.hash] = tx 95 | } 96 | } 97 | return block 98 | } 99 | 100 | getAccount(name: string) { 101 | return this.named_accounts[name as "alice"] 102 | } 103 | } 104 | 105 | const hardhat = reactive(new HardhatProvider()) 106 | 107 | type WalletName = keyof typeof hardhat.named_accounts 108 | 109 | interface CallError extends Error { 110 | error: string 111 | } 112 | 113 | export { HardhatProvider, HardhatTransaction, hardhat, WalletName, CallError } 114 | -------------------------------------------------------------------------------- /contracts/strategies/BaseBufferStrategy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.9; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol"; 6 | import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; 7 | import "../BoringMath.sol"; 8 | import "../enums/YieldBoxTokenType.sol"; 9 | import "../interfaces/IStrategy.sol"; 10 | 11 | // solhint-disable const-name-snakecase 12 | // solhint-disable no-empty-blocks 13 | 14 | abstract contract BaseBufferStrategy is IStrategy { 15 | using BoringMath for uint256; 16 | 17 | IYieldBox public immutable yieldBox; 18 | 19 | constructor(IYieldBox _yieldBox) { 20 | yieldBox = _yieldBox; 21 | } 22 | 23 | uint256 public constant MAX_RESERVE_PERCENT = 10e18; 24 | uint256 public constant TARGET_RESERVE_PERCENT = 5e18; 25 | 26 | // Implemented by base strategies for token type 27 | function _reserve() internal view virtual returns (uint256 amount); 28 | 29 | function _transfer(address to, uint256 amount) internal virtual; 30 | 31 | // Implemented by strategy 32 | function _balanceInvested() internal view virtual returns (uint256 amount); 33 | 34 | function _invest(uint256 amount) internal virtual; 35 | 36 | function _divestAll() internal virtual; 37 | 38 | function _divest(uint256 amount) internal virtual; 39 | 40 | function currentBalance() public view override returns (uint256 amount) { 41 | return _reserve() + _balanceInvested(); 42 | } 43 | 44 | function withdrawable() external view override returns (uint256 amount) { 45 | return _reserve() + _balanceInvested(); 46 | } 47 | 48 | function cheapWithdrawable() external view override returns (uint256 amount) { 49 | return _reserve(); 50 | } 51 | 52 | /// Is called by YieldBox to signal funds have been added, the strategy may choose to act on this 53 | /// When a large enough deposit is made, this should trigger the strategy to invest into the actual 54 | /// strategy. This function should normally NOT be used to invest on each call as that would be costly 55 | /// for small deposits. 56 | /// Only accept this call from the YieldBox 57 | function deposited(uint256) public override { 58 | require(msg.sender == address(yieldBox), "Not YieldBox"); 59 | 60 | uint256 balance = _balanceInvested(); 61 | uint256 reserve = _reserve(); 62 | 63 | // Get the size of the reserve in % (1e18 based) 64 | uint256 reservePercent = (reserve * 100e18) / (balance + reserve); 65 | 66 | // Check if the reserve is too large, if so invest it 67 | if (reservePercent > MAX_RESERVE_PERCENT) { 68 | _invest(balance.muldiv(reservePercent - TARGET_RESERVE_PERCENT, 100e18, false)); 69 | } 70 | } 71 | 72 | /// Is called by the YieldBox to ask the strategy to withdraw to the user 73 | /// When a strategy keeps a little reserve for cheap withdrawals and the requested withdrawal goes over this amount, 74 | /// the strategy should divest enough from the strategy to complete the withdrawal and rebalance the reserve. 75 | /// Only accept this call from the YieldBox 76 | function withdraw(address to, uint256 amount) public override { 77 | require(msg.sender == address(yieldBox), "Not YieldBox"); 78 | 79 | uint256 balance = _balanceInvested(); 80 | uint256 reserve = _reserve(); 81 | 82 | if (reserve < amount) { 83 | if (balance + reserve == amount) { 84 | _divestAll(); 85 | _transfer(to, _reserve()); 86 | } else { 87 | _divest(balance - (balance + reserve - amount).muldiv(TARGET_RESERVE_PERCENT, 100e18, false)); 88 | _transfer(to, amount); 89 | } 90 | } 91 | } 92 | } 93 | 94 | abstract contract BaseERC20BufferStrategy is BaseBufferStrategy { 95 | using BoringERC20 for IERC20; 96 | 97 | TokenType public constant tokenType = TokenType.ERC20; 98 | uint256 public constant tokenId = 0; 99 | address public immutable contractAddress; 100 | 101 | constructor(IYieldBox _yieldBox, address _contractAddress) BaseBufferStrategy(_yieldBox) { 102 | contractAddress = _contractAddress; 103 | } 104 | 105 | function _reserve() internal view override returns (uint256 amount) { 106 | return IERC20(contractAddress).safeBalanceOf(address(this)); 107 | } 108 | 109 | function _transfer(address to, uint256 amount) internal override { 110 | IERC20(contractAddress).safeTransfer(to, amount); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /web3/classes/Account.ts: -------------------------------------------------------------------------------- 1 | import { Network } from "../../sdk/Network" 2 | import { reactive, computed, ComputedRef } from "vue" 3 | import { ethers, BigNumber } from "ethers" 4 | import Web3 from "../../sdk/Web3" 5 | import { connectors } from "../../sdk/NetworkConnectors" 6 | import { IERC20__factory, IUniswapV2Pair__factory } from "./types" 7 | import { NetworkConnector } from "../../sdk/NetworkConnector" 8 | import Decimal from "decimal.js-light" 9 | import { SLPToken, Token, tokens } from "./TokenManager" 10 | import { Asset, TokenType, YieldBox } from "./YieldBox" 11 | 12 | export class Account { 13 | address: string 14 | balances: { [key: string]: BigNumber } = reactive({}) 15 | tokens: Token[] = reactive([]) 16 | assets: Asset[] = reactive([]) 17 | 18 | constructor(address: string) { 19 | this.address = address 20 | } 21 | 22 | balance(token: Token | undefined | null) { 23 | return token && token.address ? this.balances[token.network.toString() + token.address] || BigNumber.from(0) : BigNumber.from(0) 24 | } 25 | 26 | assetBalance(asset: Asset | undefined | null) { 27 | return asset ? this.balances[asset.network.toString() + "|" + asset.assetId.toString()] || BigNumber.from(0) : BigNumber.from(0) 28 | } 29 | 30 | value(token: Token) { 31 | return token.value(this.balances[token.network.toString() + token.address]) 32 | } 33 | 34 | assetValue(asset: Asset) { 35 | return asset.token?.value(this.balances[asset.network.toString() + "|" + asset.assetId.toString()]) || new Decimal(0) 36 | } 37 | 38 | get SLPTokens(): Token[] { 39 | return this.tokens.filter((token) => token.details instanceof SLPToken) 40 | } 41 | 42 | async loadNetworkBalances(network: Network) { 43 | console.log("Getting token balances", network, tokens.tokens[network]) 44 | const connector = new connectors[network]() 45 | const IERC20 = IERC20__factory.createInterface() 46 | Object.values(tokens.tokens[network] || []).forEach((token) => { 47 | connector.queue( 48 | IERC20__factory.connect(token.address, connector.provider).populateTransaction.balanceOf(this.address), 49 | IERC20, 50 | (result) => { 51 | const balance = result as BigNumber 52 | const key = token.network.toString() + token.address 53 | if (!balance.isZero() && !this.balances[key]) { 54 | this.tokens.push(token) 55 | this.balances[key] = balance 56 | } else if (this.balances[key]) { 57 | if (balance.isZero() && this.tokens.indexOf(token) !== -1) { 58 | this.tokens.splice(this.tokens.indexOf(token), 1) 59 | } 60 | if (balance.isZero()) { 61 | delete this.balances[key] 62 | } else { 63 | this.balances[key] = balance 64 | } 65 | } 66 | } 67 | ) 68 | }) 69 | await connector.call(100) 70 | } 71 | 72 | async loadYieldBoxBalances(yieldBox: YieldBox) { 73 | console.log("Getting yieldBox balances", yieldBox.network) 74 | const connector = new connectors[yieldBox.network]() 75 | yieldBox.assets.forEach((asset) => { 76 | if (asset.tokenType == TokenType.ERC20) { 77 | connector.queue( 78 | yieldBox.yieldBox.populateTransaction.amountOf(this.address, asset.assetId), 79 | yieldBox.yieldBox.interface, 80 | (result) => { 81 | const balance = result as BigNumber 82 | const key = yieldBox.network.toString() + "|" + asset.assetId.toString() 83 | if (!balance.isZero() && !this.balances[key]) { 84 | this.assets.push(asset) 85 | this.balances[key] = balance 86 | } else if (this.balances[key]) { 87 | if (balance.isZero() && this.assets.indexOf(asset) != -1) { 88 | this.assets.splice(this.assets.indexOf(asset), 1) 89 | } 90 | if (balance.isZero()) { 91 | delete this.balances[key] 92 | } else { 93 | this.balances[key] = balance 94 | } 95 | } 96 | } 97 | ) 98 | } 99 | }) 100 | await connector.call(100) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /workbench/pages/Block.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 118 | -------------------------------------------------------------------------------- /test/YieldBoxRebase.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from "chai" 2 | import { solidity } from "ethereum-waffle" 3 | import { ethers } from "hardhat" 4 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers" 5 | import { YieldBoxRebaseMock, YieldBoxRebaseMock__factory } from "../typechain-types" 6 | chai.use(solidity) 7 | 8 | describe("YieldBoxRebase", () => { 9 | let deployer: SignerWithAddress, alice: SignerWithAddress, bob: SignerWithAddress, carol: SignerWithAddress 10 | let Deployer: string, Alice: string, Bob: string, Carol: string 11 | const Zero = ethers.constants.AddressZero 12 | let rebase: YieldBoxRebaseMock 13 | 14 | beforeEach(async () => { 15 | ;({ deployer, alice, bob, carol } = await ethers.getNamedSigners()) 16 | Deployer = deployer.address 17 | Alice = alice.address 18 | Bob = bob.address 19 | Carol = carol.address 20 | rebase = await new YieldBoxRebaseMock__factory(deployer).deploy() 21 | await rebase.deployed() 22 | }) 23 | 24 | it("Deploy YieldBoxRebaseMock", async function () { 25 | expect((await rebase.deployTransaction.wait()).status).equals(1) 26 | }) 27 | 28 | it("performs basic deposit and withdraw", async function () { 29 | await rebase.deposit(100000000, 0) 30 | expect(await rebase.toAmount(100000000, false)).equals(1) 31 | expect(await rebase.toShare(1, false)).equals(100000000) 32 | await rebase.deposit(0, 1) 33 | expect(await rebase.totalAmount()).equals(2) 34 | expect(await rebase.totalShares()).equals(200000000) 35 | 36 | await rebase.withdraw(100000000, 0) 37 | await rebase.withdraw(0, 1) 38 | expect(await rebase.toAmount(100000000, false)).equals(1) 39 | expect(await rebase.toShare(1, false)).equals(100000000) 40 | expect(await rebase.totalAmount()).equals(0) 41 | expect(await rebase.totalShares()).equals(0) 42 | }) 43 | 44 | it("handles gain", async function () { 45 | await rebase.deposit(0, 1000) 46 | 47 | await rebase.gain(1000) // Amount doubles, shares stay same (of course) 48 | 49 | expect(await rebase.totalAmount()).equals(2000) 50 | expect(await rebase.totalShares()).equals(100000000000) 51 | 52 | expect(await rebase.toAmount(100000000000, false)).equals(1999) 53 | expect(await rebase.toAmount(100000000000, true)).equals(2000) 54 | 55 | expect(await rebase.toShare(1000, false)).equals(50024987506) 56 | 57 | await rebase.withdraw(100000000000, 0) 58 | expect(await rebase.toAmount(100000000000, false)).equals(2000) 59 | expect(await rebase.toAmount(100000000000, true)).equals(2000) 60 | expect(await rebase.toShare(1000, false)).equals(50000000000) 61 | expect(await rebase.totalAmount()).equals(1) 62 | expect(await rebase.totalShares()).equals(0) 63 | }) 64 | 65 | it("handles rounding", async function () { 66 | await rebase.deposit(0, 1000) 67 | expect(await rebase.toAmount(100000000000, false)).equals(1000) 68 | expect(await rebase.toAmount(100000000000, true)).equals(1000) 69 | 70 | expect(await rebase.toShare(1000, false)).equals(100000000000) 71 | expect(await rebase.toShare(1000, true)).equals(100000000000) 72 | 73 | await rebase.gain(1) 74 | 75 | expect(await rebase.toAmount(100000000000, false)).equals(1000) 76 | expect(await rebase.toAmount(100000000000, true)).equals(1001) 77 | 78 | expect(await rebase.toShare(1000, false)).equals(99900199600) 79 | expect(await rebase.toShare(1000, true)).equals(99900199601) 80 | }) 81 | 82 | it("withstand cheap minipulation attacks", async function () { 83 | // Let's assume WBTC with 8 decimals 84 | for (let i = 0; i < 10; i++) { 85 | // Deposit the minimum 86 | await rebase.deposit(0, 1) 87 | 88 | // We add 1 WBTC for this attack 89 | await rebase.gain(100000000) 90 | 91 | // We withdraw fully 92 | await rebase.withdraw(await rebase.totalShares(), 0) 93 | } 94 | 95 | // The attacker has lost 9.5 WBTC 96 | expect(await rebase.totalAmount()).equals(950000008) 97 | expect(await rebase.totalShares()).equals(0) 98 | 99 | // Now a user deposits 0.1 WBTC 100 | await rebase.deposit(0, 10000000) 101 | 102 | // If the user withdraws, they will receive 9999994 sat... a rounding loss of 6 sat 103 | expect(await rebase.toAmount(await rebase.totalShares(), false)).equals(9999994) 104 | 105 | // The user withdraws 106 | await rebase.withdraw(await rebase.totalShares(), 0) 107 | 108 | // Now a user deposits 9 sat 109 | await rebase.deposit(0, 9) 110 | 111 | // But receives no shares at all. This is the maximum loss. At deposit of 10 sat, the user will receive 9 sat back due to rounding. 112 | expect(await rebase.toAmount(await rebase.totalShares(), false)).equals(0) 113 | }) 114 | }) 115 | -------------------------------------------------------------------------------- /contracts/ERC1155.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.9; 3 | 4 | import "@boringcrypto/boring-solidity/contracts/interfaces/IERC1155.sol"; 5 | import "@boringcrypto/boring-solidity/contracts/interfaces/IERC1155TokenReceiver.sol"; 6 | import "@boringcrypto/boring-solidity/contracts/libraries/BoringAddress.sol"; 7 | 8 | // Written by OreNoMochi (https://github.com/OreNoMochii), BoringCrypto 9 | 10 | contract ERC1155 is IERC1155 { 11 | using BoringAddress for address; 12 | 13 | // mappings 14 | mapping(address => mapping(address => bool)) public override isApprovedForAll; // map of operator approval 15 | mapping(address => mapping(uint256 => uint256)) public override balanceOf; // map of tokens owned by 16 | mapping(uint256 => uint256) public totalSupply; // totalSupply per token 17 | 18 | function supportsInterface(bytes4 interfaceID) public pure override returns (bool) { 19 | return 20 | interfaceID == this.supportsInterface.selector || // EIP-165 21 | interfaceID == 0xd9b67a26 || // ERC-1155 22 | interfaceID == 0x0e89341c; // EIP-1155 Metadata 23 | } 24 | 25 | function balanceOfBatch(address[] calldata owners, uint256[] calldata ids) external view override returns (uint256[] memory balances) { 26 | uint256 len = owners.length; 27 | require(len == ids.length, "ERC1155: Length mismatch"); 28 | 29 | balances = new uint256[](len); 30 | 31 | for (uint256 i = 0; i < len; i++) { 32 | balances[i] = balanceOf[owners[i]][ids[i]]; 33 | } 34 | } 35 | 36 | function _mint( 37 | address to, 38 | uint256 id, 39 | uint256 value 40 | ) internal { 41 | require(to != address(0), "No 0 address"); 42 | 43 | balanceOf[to][id] += value; 44 | totalSupply[id] += value; 45 | 46 | emit TransferSingle(msg.sender, address(0), to, id, value); 47 | } 48 | 49 | function _burn( 50 | address from, 51 | uint256 id, 52 | uint256 value 53 | ) internal { 54 | require(from != address(0), "No 0 address"); 55 | 56 | balanceOf[from][id] -= value; 57 | totalSupply[id] -= value; 58 | 59 | emit TransferSingle(msg.sender, from, address(0), id, value); 60 | } 61 | 62 | function _transferSingle( 63 | address from, 64 | address to, 65 | uint256 id, 66 | uint256 value 67 | ) internal { 68 | require(to != address(0), "No 0 address"); 69 | 70 | balanceOf[from][id] -= value; 71 | balanceOf[to][id] += value; 72 | 73 | emit TransferSingle(msg.sender, from, to, id, value); 74 | } 75 | 76 | function _transferBatch( 77 | address from, 78 | address to, 79 | uint256[] calldata ids, 80 | uint256[] calldata values 81 | ) internal { 82 | require(to != address(0), "No 0 address"); 83 | 84 | uint256 len = ids.length; 85 | for (uint256 i = 0; i < len; i++) { 86 | uint256 id = ids[i]; 87 | uint256 value = values[i]; 88 | balanceOf[from][id] -= value; 89 | balanceOf[to][id] += value; 90 | } 91 | 92 | emit TransferBatch(msg.sender, from, to, ids, values); 93 | } 94 | 95 | function _requireTransferAllowed(address from) internal view virtual { 96 | require(from == msg.sender || isApprovedForAll[from][msg.sender] == true, "Transfer not allowed"); 97 | } 98 | 99 | function safeTransferFrom( 100 | address from, 101 | address to, 102 | uint256 id, 103 | uint256 value, 104 | bytes calldata data 105 | ) external override { 106 | _requireTransferAllowed(from); 107 | 108 | _transferSingle(from, to, id, value); 109 | 110 | if (to.isContract()) { 111 | require( 112 | IERC1155TokenReceiver(to).onERC1155Received(msg.sender, from, id, value, data) == 113 | bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")), 114 | "Wrong return value" 115 | ); 116 | } 117 | } 118 | 119 | function safeBatchTransferFrom( 120 | address from, 121 | address to, 122 | uint256[] calldata ids, 123 | uint256[] calldata values, 124 | bytes calldata data 125 | ) external override { 126 | require(ids.length == values.length, "ERC1155: Length mismatch"); 127 | _requireTransferAllowed(from); 128 | 129 | _transferBatch(from, to, ids, values); 130 | 131 | if (to.isContract()) { 132 | require( 133 | IERC1155TokenReceiver(to).onERC1155BatchReceived(msg.sender, from, ids, values, data) == 134 | bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)")), 135 | "Wrong return value" 136 | ); 137 | } 138 | } 139 | 140 | function setApprovalForAll(address operator, bool approved) external virtual override { 141 | isApprovedForAll[msg.sender][operator] = approved; 142 | 143 | emit ApprovalForAll(msg.sender, operator, approved); 144 | } 145 | 146 | function uri( 147 | uint256 /*assetId*/ 148 | ) external view virtual returns (string memory) { 149 | return ""; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /sdk/NetworkConnector.ts: -------------------------------------------------------------------------------- 1 | import { ethers, PopulatedTransaction, providers, ContractInterface, utils } from "ethers" 2 | import { Multicall2__factory } from "./types/factories/Multicall2__factory" 3 | import { Multicall2 } from "./types/Multicall2" 4 | import { Network } from "./Network" 5 | 6 | interface AddEthereumChainParameter { 7 | chainId: string // A 0x-prefixed hexadecimal string 8 | chainName: string 9 | nativeCurrency: { 10 | name: string 11 | symbol: string // 2-6 characters long 12 | decimals: number 13 | } 14 | rpcUrls: string[] 15 | blockExplorerUrls?: string[] 16 | iconUrls?: string[] // Currently ignored. 17 | } 18 | 19 | type MulticallCallback = (result: any, transaction: PopulatedTransaction) => void 20 | type MulticallFailCallback = (transaction: PopulatedTransaction) => void 21 | 22 | type MulticallItem = { 23 | transactionPromise: Promise 24 | transaction?: PopulatedTransaction 25 | callback?: MulticallCallback 26 | failcallback?: MulticallFailCallback 27 | contractInterface?: utils.Interface 28 | } 29 | 30 | export class NetworkConnector { 31 | provider: providers.Provider 32 | static get chainId(): Network { 33 | return Network.NONE 34 | } 35 | static get chainName(): string { 36 | return "None" 37 | } 38 | static get nativeCurrency(): { name: string; symbol: string; decimals: number } { 39 | return { name: "None", symbol: "NONE", decimals: 18 } 40 | } 41 | static get rpcUrls(): string[] { 42 | return [] 43 | } 44 | static get blockExplorerUrls(): string[] { 45 | return [] 46 | } 47 | static get multiCallAddress(): string { 48 | return "" 49 | } 50 | static get chainParams(): AddEthereumChainParameter { 51 | return { 52 | chainId: ethers.utils.hexStripZeros(ethers.utils.hexlify(this.chainId)), 53 | chainName: this.chainName, 54 | nativeCurrency: this.nativeCurrency, 55 | rpcUrls: this.rpcUrls, 56 | blockExplorerUrls: this.blockExplorerUrls, 57 | } 58 | } 59 | 60 | static get coinGeckoId(): string { 61 | return "" 62 | } 63 | 64 | // Add the static values to each instance 65 | get type() { 66 | return this.constructor as typeof NetworkConnector 67 | } 68 | get chainId() { 69 | return this.type.chainId 70 | } 71 | get chainName() { 72 | return this.type.chainName 73 | } 74 | get nativeCurrency() { 75 | return this.type.nativeCurrency 76 | } 77 | get rpcUrls() { 78 | return this.type.rpcUrls 79 | } 80 | get blockExplorerUrls() { 81 | return this.type.blockExplorerUrls 82 | } 83 | get multiCallAddress() { 84 | return this.type.multiCallAddress 85 | } 86 | get coinGeckoId() { 87 | return this.type.coinGeckoId 88 | } 89 | 90 | constructor(provider?: providers.Provider | null) { 91 | if (provider) { 92 | // Use provided provider (for instance injected MetaMask web3) 93 | this.provider = provider 94 | } else { 95 | // or create one using the RPC Url 96 | this.provider = new providers.StaticJsonRpcProvider({ 97 | url: this.rpcUrls[0], 98 | }) 99 | } 100 | } 101 | 102 | items: MulticallItem[] = [] 103 | 104 | queue( 105 | transactionPromise: Promise, 106 | contractInterface?: utils.Interface, 107 | callback?: MulticallCallback, 108 | failcallback?: MulticallFailCallback 109 | ) { 110 | this.items.push({ transactionPromise, contractInterface, callback, failcallback }) 111 | } 112 | 113 | async call(batchSize: number = 0) { 114 | const results: any[] = [] 115 | 116 | while (this.items.length) { 117 | const contract = Multicall2__factory.connect(this.multiCallAddress, this.provider) 118 | 119 | const batch = this.items.splice(0, batchSize || this.items.length) 120 | for (let i in batch) { 121 | batch[i].transaction = await batch[i].transactionPromise 122 | } 123 | 124 | const calls: Multicall2.CallStruct[] = batch.map((item) => { 125 | return { 126 | target: item.transaction!.to!, 127 | callData: item.transaction!.data!, 128 | } 129 | }) 130 | const callResult = await contract.callStatic.tryAggregate(false, calls) 131 | batch.forEach((item, i) => { 132 | if (callResult[i].success) { 133 | let result: any = callResult[i].returnData 134 | if (item.contractInterface) { 135 | try { 136 | result = item.contractInterface.decodeFunctionResult( 137 | item.contractInterface.parseTransaction({ data: item.transaction?.data || "" }).name, 138 | callResult[i].returnData 139 | ) 140 | 141 | if (item.callback) { 142 | item.callback(result.length === 1 ? result[0] : result, item.transaction!) 143 | } 144 | } catch (e) { 145 | console.log(e) 146 | } 147 | } 148 | 149 | results.push(result.length === 1 ? result[0] : result) 150 | } else { 151 | if (item.failcallback) { 152 | item.failcallback(item.transaction!) 153 | } 154 | console.log("Fail") 155 | results.push(new Error("Failed")) 156 | } 157 | }) 158 | } 159 | return results 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /test/YieldBoxURIBuilder.ts: -------------------------------------------------------------------------------- 1 | import chai, { assert, expect } from "chai" 2 | import { solidity } from "ethereum-waffle" 3 | import { ethers } from "hardhat" 4 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers" 5 | import { WETH9Mock__factory, YieldBox, YieldBoxURIBuilder, YieldBoxURIBuilder__factory, YieldBox__factory } from "../typechain-types" 6 | import { TokenType } from "../sdk" 7 | chai.use(solidity) 8 | 9 | describe("YieldBoxURIBuilder", () => { 10 | let deployer: SignerWithAddress, alice: SignerWithAddress, bob: SignerWithAddress, carol: SignerWithAddress 11 | let Deployer: string, Alice: string, Bob: string, Carol: string 12 | const Zero = ethers.constants.AddressZero 13 | let uriBuilder: YieldBoxURIBuilder 14 | let yieldBox: YieldBox 15 | const sushi = "0x6B3595068778DD592e39A122f4f5a5cF09C90fE2" 16 | const rarible = "0xd07dc4262BCDbf85190C01c996b4C06a461d2430" 17 | 18 | beforeEach(async () => { 19 | ;({ deployer, alice, bob, carol } = await ethers.getNamedSigners()) 20 | Deployer = deployer.address 21 | Alice = alice.address 22 | Bob = bob.address 23 | Carol = carol.address 24 | 25 | const weth = await new WETH9Mock__factory(deployer).deploy() 26 | await weth.deployed() 27 | 28 | uriBuilder = await new YieldBoxURIBuilder__factory(deployer).deploy() 29 | await uriBuilder.deployed() 30 | 31 | yieldBox = await new YieldBox__factory(deployer).deploy(weth.address, uriBuilder.address) 32 | await yieldBox.deployed() 33 | }) 34 | 35 | it("Deploy YieldBoxURIBuilder", async function () { 36 | assert.equal((await uriBuilder.deployTransaction.wait()).status, 1) 37 | }) 38 | 39 | it("Creates URI for Native tokens", async function () { 40 | await yieldBox.createToken("Boring Token", "BORING", 18, "") 41 | const asset = await yieldBox.assets(1) 42 | const nativeToken = await yieldBox.nativeTokens(1) 43 | 44 | const uri = await uriBuilder.uri( 45 | await yieldBox.assets(1), 46 | await yieldBox.nativeTokens(1), 47 | await yieldBox.totalSupply(1), 48 | await yieldBox.owner(1) 49 | ) 50 | expect(uri.startsWith("data:application/json;base64,")).to.be.true 51 | const base64 = uri.substring(29) 52 | const json = Buffer.from(base64, "base64").toString("utf-8") 53 | const data = JSON.parse(json) 54 | expect(data.properties.tokenType).equals("Native") 55 | expect(data.name).equals("Boring Token") 56 | expect(data.symbol).equals("BORING") 57 | expect(data.decimals).equals(18) 58 | expect(data.properties.strategy).equals(Zero) 59 | expect(data.properties.totalSupply).equals(0) 60 | expect(data.properties.fixedSupply).equals(false) 61 | }) 62 | 63 | it("Creates URI for Native token with fixed supply", async function () { 64 | await yieldBox.createToken("Boring Token", "BORING", 18, "") 65 | await yieldBox.mint(1, Alice, 1000) 66 | await yieldBox.transferOwnership(1, Zero, true, true) 67 | 68 | const uri = await uriBuilder.uri( 69 | await yieldBox.assets(1), 70 | await yieldBox.nativeTokens(1), 71 | await yieldBox.totalSupply(1), 72 | await yieldBox.owner(1) 73 | ) 74 | expect(uri.startsWith("data:application/json;base64,")).to.be.true 75 | const base64 = uri.substring(29) 76 | const json = Buffer.from(base64, "base64").toString("utf-8") 77 | const data = JSON.parse(json) 78 | expect(data.properties.tokenType).equals("Native") 79 | expect(data.name).equals("Boring Token") 80 | expect(data.symbol).equals("BORING") 81 | expect(data.decimals).equals(18) 82 | expect(data.properties.strategy).equals(Zero) 83 | expect(data.properties.totalSupply).equals(1000) 84 | expect(data.properties.fixedSupply).equals(true) 85 | }) 86 | 87 | it("Creates URI for ERC20 token", async function () { 88 | await yieldBox.registerAsset(TokenType.ERC20, sushi, Zero, 0) 89 | 90 | const uri = await uriBuilder.uri( 91 | await yieldBox.assets(1), 92 | await yieldBox.nativeTokens(1), 93 | await yieldBox.totalSupply(1), 94 | await yieldBox.owner(1) 95 | ) 96 | expect(uri.startsWith("data:application/json;base64,")).to.be.true 97 | const base64 = uri.substring(29) 98 | const json = Buffer.from(base64, "base64").toString("utf-8") 99 | const data = JSON.parse(json) 100 | expect(data.properties.tokenType).equals("ERC20") 101 | expect(data.name).equals("SushiToken") 102 | expect(data.symbol).equals("SUSHI") 103 | expect(data.decimals).equals(18) 104 | expect(data.properties.strategy).equals(Zero) 105 | expect(data.properties.tokenAddress).equals(sushi.toLowerCase()) 106 | }) 107 | 108 | it("Creates URI for ERC1155 token", async function () { 109 | await yieldBox.registerAsset(TokenType.ERC1155, rarible, Zero, 50) 110 | 111 | const uri = await uriBuilder.uri( 112 | await yieldBox.assets(1), 113 | await yieldBox.nativeTokens(1), 114 | await yieldBox.totalSupply(1), 115 | await yieldBox.owner(1) 116 | ) 117 | expect(uri.startsWith("data:application/json;base64,")).to.be.true 118 | const base64 = uri.substring(29) 119 | const json = Buffer.from(base64, "base64").toString("utf-8") 120 | const data = JSON.parse(json) 121 | expect(data.properties.tokenType).equals("ERC1155") 122 | expect(data.name).equals("ERC1155:0xd07dc4262bcdbf85190c01c996b4c06a461d2430/50") 123 | expect(data.symbol).equals("ERC1155") 124 | expect(data.properties.strategy).equals(Zero) 125 | expect(data.properties.tokenAddress).equals(rarible.toLowerCase()) 126 | expect(data.properties.tokenId).equals(50) 127 | }) 128 | }) 129 | -------------------------------------------------------------------------------- /web3/classes/types/factories/IUniswapV2Factory__factory.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | 5 | import { Contract, Signer, utils } from "ethers" 6 | import { Provider } from "@ethersproject/providers" 7 | import type { IUniswapV2Factory, IUniswapV2FactoryInterface } from "../IUniswapV2Factory" 8 | 9 | const _abi = [ 10 | { 11 | anonymous: false, 12 | inputs: [ 13 | { 14 | indexed: true, 15 | internalType: "address", 16 | name: "token0", 17 | type: "address", 18 | }, 19 | { 20 | indexed: true, 21 | internalType: "address", 22 | name: "token1", 23 | type: "address", 24 | }, 25 | { 26 | indexed: false, 27 | internalType: "address", 28 | name: "pair", 29 | type: "address", 30 | }, 31 | { 32 | indexed: false, 33 | internalType: "uint256", 34 | name: "", 35 | type: "uint256", 36 | }, 37 | ], 38 | name: "PairCreated", 39 | type: "event", 40 | }, 41 | { 42 | inputs: [ 43 | { 44 | internalType: "uint256", 45 | name: "", 46 | type: "uint256", 47 | }, 48 | ], 49 | name: "allPairs", 50 | outputs: [ 51 | { 52 | internalType: "address", 53 | name: "pair", 54 | type: "address", 55 | }, 56 | ], 57 | stateMutability: "view", 58 | type: "function", 59 | }, 60 | { 61 | inputs: [], 62 | name: "allPairsLength", 63 | outputs: [ 64 | { 65 | internalType: "uint256", 66 | name: "", 67 | type: "uint256", 68 | }, 69 | ], 70 | stateMutability: "view", 71 | type: "function", 72 | }, 73 | { 74 | inputs: [ 75 | { 76 | internalType: "address", 77 | name: "tokenA", 78 | type: "address", 79 | }, 80 | { 81 | internalType: "address", 82 | name: "tokenB", 83 | type: "address", 84 | }, 85 | ], 86 | name: "createPair", 87 | outputs: [ 88 | { 89 | internalType: "address", 90 | name: "pair", 91 | type: "address", 92 | }, 93 | ], 94 | stateMutability: "nonpayable", 95 | type: "function", 96 | }, 97 | { 98 | inputs: [], 99 | name: "feeTo", 100 | outputs: [ 101 | { 102 | internalType: "address", 103 | name: "", 104 | type: "address", 105 | }, 106 | ], 107 | stateMutability: "view", 108 | type: "function", 109 | }, 110 | { 111 | inputs: [], 112 | name: "feeToSetter", 113 | outputs: [ 114 | { 115 | internalType: "address", 116 | name: "", 117 | type: "address", 118 | }, 119 | ], 120 | stateMutability: "view", 121 | type: "function", 122 | }, 123 | { 124 | inputs: [ 125 | { 126 | internalType: "address", 127 | name: "tokenA", 128 | type: "address", 129 | }, 130 | { 131 | internalType: "address", 132 | name: "tokenB", 133 | type: "address", 134 | }, 135 | ], 136 | name: "getPair", 137 | outputs: [ 138 | { 139 | internalType: "address", 140 | name: "pair", 141 | type: "address", 142 | }, 143 | ], 144 | stateMutability: "view", 145 | type: "function", 146 | }, 147 | { 148 | inputs: [], 149 | name: "migrator", 150 | outputs: [ 151 | { 152 | internalType: "address", 153 | name: "", 154 | type: "address", 155 | }, 156 | ], 157 | stateMutability: "view", 158 | type: "function", 159 | }, 160 | { 161 | inputs: [ 162 | { 163 | internalType: "address", 164 | name: "", 165 | type: "address", 166 | }, 167 | ], 168 | name: "setFeeTo", 169 | outputs: [], 170 | stateMutability: "nonpayable", 171 | type: "function", 172 | }, 173 | { 174 | inputs: [ 175 | { 176 | internalType: "address", 177 | name: "", 178 | type: "address", 179 | }, 180 | ], 181 | name: "setFeeToSetter", 182 | outputs: [], 183 | stateMutability: "nonpayable", 184 | type: "function", 185 | }, 186 | { 187 | inputs: [ 188 | { 189 | internalType: "address", 190 | name: "", 191 | type: "address", 192 | }, 193 | ], 194 | name: "setMigrator", 195 | outputs: [], 196 | stateMutability: "nonpayable", 197 | type: "function", 198 | }, 199 | ] 200 | 201 | export class IUniswapV2Factory__factory { 202 | static readonly abi = _abi 203 | static createInterface(): IUniswapV2FactoryInterface { 204 | return new utils.Interface(_abi) as IUniswapV2FactoryInterface 205 | } 206 | static connect(address: string, signerOrProvider: Signer | Provider): IUniswapV2Factory { 207 | return new Contract(address, _abi, signerOrProvider) as IUniswapV2Factory 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /contracts/YieldBoxURIBuilder.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.9; 3 | import "@openzeppelin/contracts/utils/Strings.sol"; 4 | import "@boringcrypto/boring-solidity/contracts/libraries/Base64.sol"; 5 | import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; 6 | import "./interfaces/IYieldBox.sol"; 7 | import "./NativeTokenFactory.sol"; 8 | 9 | // solhint-disable quotes 10 | 11 | contract YieldBoxURIBuilder { 12 | using BoringERC20 for IERC20; 13 | using Strings for uint256; 14 | using Base64 for bytes; 15 | 16 | struct AssetDetails { 17 | string tokenType; 18 | string name; 19 | string symbol; 20 | uint256 decimals; 21 | } 22 | 23 | function name(Asset calldata asset, string calldata nativeName) external view returns (string memory) { 24 | if (asset.strategy == NO_STRATEGY) { 25 | return nativeName; 26 | } else { 27 | if (asset.tokenType == TokenType.ERC20) { 28 | IERC20 token = IERC20(asset.contractAddress); 29 | return string(abi.encodePacked(token.safeName(), " (", asset.strategy.name(), ")")); 30 | } else if (asset.tokenType == TokenType.ERC1155) { 31 | return 32 | string( 33 | abi.encodePacked( 34 | string( 35 | abi.encodePacked( 36 | "ERC1155:", 37 | uint256(uint160(asset.contractAddress)).toHexString(20), 38 | "/", 39 | asset.tokenId.toString() 40 | ) 41 | ), 42 | " (", 43 | asset.strategy.name(), 44 | ")" 45 | ) 46 | ); 47 | } else { 48 | return string(abi.encodePacked(nativeName, " (", asset.strategy.name(), ")")); 49 | } 50 | } 51 | } 52 | 53 | function symbol(Asset calldata asset, string calldata nativeSymbol) external view returns (string memory) { 54 | if (asset.strategy == NO_STRATEGY) { 55 | return nativeSymbol; 56 | } else { 57 | if (asset.tokenType == TokenType.ERC20) { 58 | IERC20 token = IERC20(asset.contractAddress); 59 | return string(abi.encodePacked(token.safeSymbol(), " (", asset.strategy.name(), ")")); 60 | } else if (asset.tokenType == TokenType.ERC1155) { 61 | return string(abi.encodePacked("ERC1155", " (", asset.strategy.name(), ")")); 62 | } else { 63 | return string(abi.encodePacked(nativeSymbol, " (", asset.strategy.name(), ")")); 64 | } 65 | } 66 | } 67 | 68 | function decimals(Asset calldata asset, uint8 nativeDecimals) external view returns (uint8) { 69 | if (asset.tokenType == TokenType.ERC1155) { 70 | return 0; 71 | } else if (asset.tokenType == TokenType.ERC20) { 72 | IERC20 token = IERC20(asset.contractAddress); 73 | return token.safeDecimals(); 74 | } else { 75 | return nativeDecimals; 76 | } 77 | } 78 | 79 | function uri( 80 | Asset calldata asset, 81 | NativeToken calldata nativeToken, 82 | uint256 totalSupply, 83 | address owner 84 | ) external view returns (string memory) { 85 | AssetDetails memory details; 86 | if (asset.tokenType == TokenType.ERC1155) { 87 | // Contracts can't retrieve URIs, so the details are out of reach 88 | details.tokenType = "ERC1155"; 89 | details.name = string( 90 | abi.encodePacked("ERC1155:", uint256(uint160(asset.contractAddress)).toHexString(20), "/", asset.tokenId.toString()) 91 | ); 92 | details.symbol = "ERC1155"; 93 | } else if (asset.tokenType == TokenType.ERC20) { 94 | IERC20 token = IERC20(asset.contractAddress); 95 | details = AssetDetails("ERC20", token.safeName(), token.safeSymbol(), token.safeDecimals()); 96 | } else { 97 | // Native 98 | details.tokenType = "Native"; 99 | details.name = nativeToken.name; 100 | details.symbol = nativeToken.symbol; 101 | details.decimals = nativeToken.decimals; 102 | } 103 | 104 | string memory properties = string( 105 | asset.tokenType != TokenType.Native 106 | ? abi.encodePacked(',"tokenAddress":"', uint256(uint160(asset.contractAddress)).toHexString(20), '"') 107 | : abi.encodePacked(',"totalSupply":', totalSupply.toString(), ',"fixedSupply":', owner == address(0) ? "true" : "false") 108 | ); 109 | 110 | return 111 | string( 112 | abi.encodePacked( 113 | "data:application/json;base64,", 114 | abi 115 | .encodePacked( 116 | '{"name":"', 117 | details.name, 118 | '","symbol":"', 119 | details.symbol, 120 | '"', 121 | asset.tokenType == TokenType.ERC1155 ? "" : ',"decimals":', 122 | asset.tokenType == TokenType.ERC1155 ? "" : details.decimals.toString(), 123 | ',"properties":{"strategy":"', 124 | uint256(uint160(address(asset.strategy))).toHexString(20), 125 | '","tokenType":"', 126 | details.tokenType, 127 | '"', 128 | properties, 129 | asset.tokenType == TokenType.ERC1155 ? string(abi.encodePacked(',"tokenId":', asset.tokenId.toString())) : "", 130 | "}}" 131 | ) 132 | .encode() 133 | ) 134 | ); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /contracts/samples/YieldSwap.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3 2 | // Uniswap V2 for YieldBox (https://github.com/Uniswap/v2-core) 3 | pragma solidity 0.8.9; 4 | import "../YieldBox.sol"; 5 | 6 | struct Pair { 7 | uint128 reserve0; 8 | uint128 reserve1; 9 | uint32 asset0; 10 | uint32 asset1; 11 | uint32 lpAssetId; 12 | uint256 kLast; 13 | } 14 | 15 | library Math { 16 | function min(uint256 x, uint256 y) internal pure returns (uint256 z) { 17 | z = x < y ? x : y; 18 | } 19 | 20 | // babylonian method (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method) 21 | function sqrt(uint256 y) internal pure returns (uint256 z) { 22 | if (y > 3) { 23 | z = y; 24 | uint256 x = y / 2 + 1; 25 | while (x < z) { 26 | z = x; 27 | x = (y / x + x) / 2; 28 | } 29 | } else if (y != 0) { 30 | z = 1; 31 | } 32 | } 33 | } 34 | 35 | contract YieldSwap { 36 | using BoringMath for uint256; 37 | 38 | YieldBox public yieldBox; 39 | 40 | constructor(YieldBox _yieldBox) { 41 | yieldBox = _yieldBox; 42 | } 43 | 44 | uint256 public constant MINIMUM_LIQUIDITY = 10**3; 45 | 46 | Pair[] public pairs; 47 | mapping(uint256 => mapping(uint256 => uint256)) public pairLookup; 48 | 49 | event Mint(address indexed sender, uint256 amount0, uint256 amount1); 50 | event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to); 51 | event Swap(address indexed sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, address indexed to); 52 | event Sync(uint112 reserve0, uint112 reserve1); 53 | 54 | function create(uint32 asset0, uint32 asset1) public returns (uint256 pairId) { 55 | if (asset0 > asset1) { 56 | (asset0, asset1) = (asset1, asset0); 57 | } 58 | 59 | uint32 lpAssetId = yieldBox.createToken("YieldBox LP Token", "YLP", 18, ""); 60 | pairId = pairs.length; 61 | pairLookup[asset0][asset1] = pairId; 62 | pairs.push(Pair(0, 0, asset0, asset1, lpAssetId, 0)); 63 | } 64 | 65 | function mint(uint256 pairId, address to) external returns (uint256 liquidity) { 66 | Pair storage pair = pairs[pairId]; 67 | 68 | uint256 balance0 = yieldBox.balanceOf(address(this), pair.asset0); 69 | uint256 balance1 = yieldBox.balanceOf(address(this), pair.asset1); 70 | uint256 amount0 = balance0 - pair.reserve0; 71 | uint256 amount1 = balance1 - pair.reserve1; 72 | 73 | uint256 _totalSupply = yieldBox.totalSupply(pair.lpAssetId); 74 | if (_totalSupply == 0) { 75 | liquidity = Math.sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY; 76 | yieldBox.mint(pair.lpAssetId, address(0), MINIMUM_LIQUIDITY); 77 | } else { 78 | liquidity = Math.min((amount0 * _totalSupply) / pair.reserve0, (amount1 * _totalSupply) / pair.reserve1); 79 | } 80 | require(liquidity > 0, "YieldSwap: Not enough mint"); 81 | yieldBox.mint(pair.lpAssetId, to, liquidity); 82 | 83 | pair.reserve0 = balance0.to128(); 84 | pair.reserve1 = balance1.to128(); 85 | } 86 | 87 | function burn(uint256 pairId, address to) external returns (uint256 share0, uint256 share1) { 88 | Pair storage pair = pairs[pairId]; 89 | 90 | uint256 balance0 = yieldBox.balanceOf(address(this), pair.asset0); 91 | uint256 balance1 = yieldBox.balanceOf(address(this), pair.asset1); 92 | uint256 liquidity = yieldBox.balanceOf(address(this), pair.lpAssetId); 93 | 94 | uint256 _totalSupply = yieldBox.totalSupply(pair.lpAssetId); 95 | share0 = (liquidity * balance0) / _totalSupply; // using balances ensures pro-rata distribution 96 | share1 = (liquidity * balance1) / _totalSupply; // using balances ensures pro-rata distribution 97 | require(share0 > 0 && share1 > 0, "YieldSwap: Not enough"); 98 | yieldBox.burn(pair.lpAssetId, address(this), liquidity); 99 | yieldBox.transfer(address(this), to, pair.asset0, share0); 100 | yieldBox.transfer(address(this), to, pair.asset1, share1); 101 | 102 | pair.reserve0 = yieldBox.balanceOf(address(this), pair.asset0).to128(); 103 | pair.reserve1 = yieldBox.balanceOf(address(this), pair.asset1).to128(); 104 | } 105 | 106 | function swap( 107 | uint256 pairId, 108 | uint256 share0Out, 109 | uint256 share1Out, 110 | address to 111 | ) external { 112 | Pair storage pair = pairs[pairId]; 113 | 114 | require(share0Out > 0 || share1Out > 0, "YieldSwap: Output too low"); 115 | require(share0Out < pair.reserve0 && share1Out < pair.reserve1, "YieldSwap: Liquidity too low"); 116 | 117 | yieldBox.transfer(address(this), to, pair.asset0, share0Out); 118 | yieldBox.transfer(address(this), to, pair.asset1, share1Out); 119 | 120 | uint256 balance0 = yieldBox.balanceOf(address(this), pair.asset0); 121 | uint256 balance1 = yieldBox.balanceOf(address(this), pair.asset1); 122 | 123 | uint256 share0In = balance0 > pair.reserve0 - share0Out ? balance0 - (pair.reserve0 - share0Out) : 0; 124 | uint256 share1In = balance1 > pair.reserve1 - share1Out ? balance1 - (pair.reserve1 - share1Out) : 0; 125 | require(share0In > 0 || share1In > 0, "YieldSwap: No input"); 126 | require(balance0 * balance1 >= pair.reserve0 * pair.reserve1, "YieldSwap: K"); 127 | 128 | pair.reserve0 = balance0.to128(); 129 | pair.reserve1 = balance1.to128(); 130 | } 131 | 132 | // force balances to match reserves 133 | function skim(uint256 pairId, address to) external { 134 | Pair storage pair = pairs[pairId]; 135 | 136 | yieldBox.transfer(address(this), to, pair.asset0, yieldBox.balanceOf(address(this), pair.asset0) - pair.reserve0); 137 | yieldBox.transfer(address(this), to, pair.asset1, yieldBox.balanceOf(address(this), pair.asset1) - pair.reserve1); 138 | } 139 | 140 | // force reserves to match balances 141 | function sync(uint256 pairId) external { 142 | Pair storage pair = pairs[pairId]; 143 | 144 | pair.reserve0 = yieldBox.balanceOf(address(this), pair.asset0).to128(); 145 | pair.reserve1 = yieldBox.balanceOf(address(this), pair.asset1).to128(); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /web3/classes/types/factories/IERC20__factory.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | 5 | import { Contract, Signer, utils } from "ethers" 6 | import { Provider } from "@ethersproject/providers" 7 | import type { IERC20, IERC20Interface } from "../IERC20" 8 | 9 | const _abi = [ 10 | { 11 | anonymous: false, 12 | inputs: [ 13 | { 14 | indexed: true, 15 | internalType: "address", 16 | name: "owner", 17 | type: "address", 18 | }, 19 | { 20 | indexed: true, 21 | internalType: "address", 22 | name: "spender", 23 | type: "address", 24 | }, 25 | { 26 | indexed: false, 27 | internalType: "uint256", 28 | name: "value", 29 | type: "uint256", 30 | }, 31 | ], 32 | name: "Approval", 33 | type: "event", 34 | }, 35 | { 36 | anonymous: false, 37 | inputs: [ 38 | { 39 | indexed: true, 40 | internalType: "address", 41 | name: "from", 42 | type: "address", 43 | }, 44 | { 45 | indexed: true, 46 | internalType: "address", 47 | name: "to", 48 | type: "address", 49 | }, 50 | { 51 | indexed: false, 52 | internalType: "uint256", 53 | name: "value", 54 | type: "uint256", 55 | }, 56 | ], 57 | name: "Transfer", 58 | type: "event", 59 | }, 60 | { 61 | inputs: [ 62 | { 63 | internalType: "address", 64 | name: "owner", 65 | type: "address", 66 | }, 67 | { 68 | internalType: "address", 69 | name: "spender", 70 | type: "address", 71 | }, 72 | ], 73 | name: "allowance", 74 | outputs: [ 75 | { 76 | internalType: "uint256", 77 | name: "", 78 | type: "uint256", 79 | }, 80 | ], 81 | stateMutability: "view", 82 | type: "function", 83 | }, 84 | { 85 | inputs: [ 86 | { 87 | internalType: "address", 88 | name: "spender", 89 | type: "address", 90 | }, 91 | { 92 | internalType: "uint256", 93 | name: "amount", 94 | type: "uint256", 95 | }, 96 | ], 97 | name: "approve", 98 | outputs: [ 99 | { 100 | internalType: "bool", 101 | name: "", 102 | type: "bool", 103 | }, 104 | ], 105 | stateMutability: "nonpayable", 106 | type: "function", 107 | }, 108 | { 109 | inputs: [ 110 | { 111 | internalType: "address", 112 | name: "account", 113 | type: "address", 114 | }, 115 | ], 116 | name: "balanceOf", 117 | outputs: [ 118 | { 119 | internalType: "uint256", 120 | name: "", 121 | type: "uint256", 122 | }, 123 | ], 124 | stateMutability: "view", 125 | type: "function", 126 | }, 127 | { 128 | inputs: [], 129 | name: "decimals", 130 | outputs: [ 131 | { 132 | internalType: "uint8", 133 | name: "", 134 | type: "uint8", 135 | }, 136 | ], 137 | stateMutability: "view", 138 | type: "function", 139 | }, 140 | { 141 | inputs: [], 142 | name: "name", 143 | outputs: [ 144 | { 145 | internalType: "string", 146 | name: "", 147 | type: "string", 148 | }, 149 | ], 150 | stateMutability: "view", 151 | type: "function", 152 | }, 153 | { 154 | inputs: [ 155 | { 156 | internalType: "address", 157 | name: "owner", 158 | type: "address", 159 | }, 160 | { 161 | internalType: "address", 162 | name: "spender", 163 | type: "address", 164 | }, 165 | { 166 | internalType: "uint256", 167 | name: "value", 168 | type: "uint256", 169 | }, 170 | { 171 | internalType: "uint256", 172 | name: "deadline", 173 | type: "uint256", 174 | }, 175 | { 176 | internalType: "uint8", 177 | name: "v", 178 | type: "uint8", 179 | }, 180 | { 181 | internalType: "bytes32", 182 | name: "r", 183 | type: "bytes32", 184 | }, 185 | { 186 | internalType: "bytes32", 187 | name: "s", 188 | type: "bytes32", 189 | }, 190 | ], 191 | name: "permit", 192 | outputs: [], 193 | stateMutability: "nonpayable", 194 | type: "function", 195 | }, 196 | { 197 | inputs: [], 198 | name: "symbol", 199 | outputs: [ 200 | { 201 | internalType: "string", 202 | name: "", 203 | type: "string", 204 | }, 205 | ], 206 | stateMutability: "view", 207 | type: "function", 208 | }, 209 | { 210 | inputs: [], 211 | name: "totalSupply", 212 | outputs: [ 213 | { 214 | internalType: "uint256", 215 | name: "", 216 | type: "uint256", 217 | }, 218 | ], 219 | stateMutability: "view", 220 | type: "function", 221 | }, 222 | ] 223 | 224 | export class IERC20__factory { 225 | static readonly abi = _abi 226 | static createInterface(): IERC20Interface { 227 | return new utils.Interface(_abi) as IERC20Interface 228 | } 229 | static connect(address: string, signerOrProvider: Signer | Provider): IERC20 { 230 | return new Contract(address, _abi, signerOrProvider) as IERC20 231 | } 232 | } 233 | --------------------------------------------------------------------------------