├── .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 |
14 | ${{ display }}
15 |
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 |
14 | {{ display }} {{ " " + token?.symbol }}
15 |
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 |
14 |
15 |
16 | {{ address }}
17 |
18 |
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 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Loading...
17 |
18 |
19 | Unaudited and just for testing. @Boring_crypto.
20 |
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 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/web3/App.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Loading...
24 |
25 |
26 | Unaudited and just for testing. @Boring_crypto.
27 |
28 | {{ info.description }} - {{ info.status }}
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/workbench/pages/Home.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 | Mainnet forking is not enabled. To enable mainnet forking, create a (free) account on
12 |
Alchemy . Then get an API key for mainnet and add copy the .env.example file to
13 | .env and add your API key there.
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | {{ contract.address }}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/workbench/components/AddressLink.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 👷
8 | {{ info.name }}
9 |
10 | {{ address }}
11 |
12 |
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 |
2 | {{ days }}d
3 | {{ hours }}h
4 | {{ minutes }}m
5 | {{ seconds }}s
6 | ago
7 |
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 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | 👷
33 | {{ info.name }}
34 |
35 | {{ address }}
36 |
37 | {{ address }}
38 |
39 | This is a named wallet
40 |
41 | ETH Balance {{ detail.ethBalance.toString() }}
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/web3/components/Web3Button.vue:
--------------------------------------------------------------------------------
1 |
37 |
38 |
39 |
40 | ...
41 | Connect
42 | Switch
43 | Allow
44 |
45 |
46 |
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 | [](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 |
50 |
51 |
Swap
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/web3/pages/Escrow.vue:
--------------------------------------------------------------------------------
1 |
48 |
49 |
50 |
51 |
Escrow
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/web3/pages/Lending.vue:
--------------------------------------------------------------------------------
1 |
48 |
49 |
50 |
51 |
Lending
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/web3/pages/Tokenizer.vue:
--------------------------------------------------------------------------------
1 |
48 |
49 |
50 |
51 |
Tokenizer
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/workbench/components/Menu.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
45 |
46 |
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 |
44 |
45 |
YieldBox
46 | {{ address }}
47 |
48 |
49 | Token
50 | Balance
51 | Value
52 |
53 |
54 |
55 |
56 |
57 | {{ asset.token.symbol }}
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/web3/components/TokenAmountInput.vue:
--------------------------------------------------------------------------------
1 |
58 |
59 |
60 |
61 |
62 | Max
63 |
64 |
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 |
6 |
7 |
8 |
9 |
10 |
11 |
20 |
21 |
22 |
23 |
47 |
48 |
49 | Switch to Hardhat!
50 |
51 | {{ app.web3.connector?.chainName }}
52 |
53 | Connect
54 |
55 |
56 | {{ app.web3.address }}
57 |
58 |
59 |
60 |
61 |
62 |
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 |
96 |
97 |
98 |
99 | Cancel
100 |
101 |
109 | {{ okTitle }}
110 |
111 |
112 |
113 |
114 |
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 |
101 |
102 |
103 |
104 | Cancel
105 |
106 |
107 | {{ okTitle }}
108 |
109 |
110 |
111 |
112 |
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 |
39 |
40 |
Block #{{ block.number }}
41 |
42 |
43 |
44 |
45 | Timestamp
46 |
47 | ({{
48 | new Intl.DateTimeFormat(undefined, { timeStyle: "short", dateStyle: "medium" }).format(block.timestamp * 1000)
49 | }})
50 |
51 |
52 |
53 | Transactions
54 | {{ block.transactions.length }} transaction{{ block.transactions.length == 1 ? "" : "s" }}
55 |
56 |
57 | Mined by
58 |
59 |
60 |
61 | Gas Used
62 |
63 | {{ block.gasUsed.toString() }}
64 | ({{ BigNumber.from(block.gasUsed).mul(10000).div(block.gasLimit).toDec(2).toString() }}%)
65 |
66 |
67 |
68 | Base Fee Per Gas
69 | {{ BigNumber.from(block.baseFeePerGas).toDec(9).toString() }} Gwei
70 |
71 |
72 | Extra Data
73 | {{ block.extraData }}
74 |
75 |
76 | Hash
77 | {{ block.hash }}
78 |
79 |
80 | Parent Hash
81 | {{ block.parentHash }}
82 |
83 |
84 | Nonce
85 | {{ block.nonce }}
86 |
87 |
88 |
89 |
90 |
Transactions
91 |
92 |
93 |
94 |
95 | Created contract
96 |
97 |
98 |
99 |
100 | Index: {{ tx.transactionIndex }}
101 | Hash: {{ tx.hash }}
102 | Type: {{ tx.type }}
103 | From:
104 | To:
105 | Creates:
106 | Nonce: {{ tx.nonce }}
107 | Gas Limit: {{ tx.gasLimit.toString() }}
108 | Gas Price: {{ tx.gasPrice.toString() }}
109 | Value: {{ tx.value.toString() }}
110 | Max Priority Fee: {{ tx.maxPriorityFeePerGas?.toString() }}
111 | Max Fee: {{ tx.maxFeePerGas?.toString() }}
112 | Data: {{ tx.data }}
113 |
114 |
115 |
116 |
117 |
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 |
--------------------------------------------------------------------------------