├── .eslintrc.json ├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc ├── .solcover.js ├── .solhint.json ├── .soliumignore ├── .soliumrc.json ├── @types └── types.d.ts ├── LICENSE.md ├── contracts ├── Math64x64.sol ├── Pool.sol ├── YieldMath.sol ├── extensions │ ├── PoolView.sol │ └── YieldMathExtensions.sol └── mocks │ ├── BaseMock.sol │ ├── DaiMock.sol │ ├── FYTokenMock.sol │ ├── USDCMock.sol │ ├── WETH9Mock.sol │ └── YieldMathWrapper.sol ├── deploy ├── PoolFactory.js ├── SafeERC20Namer.js └── YieldMath.js ├── deployments └── kovan │ ├── .chainId │ ├── DaiMock.json │ └── solcInputs │ └── 3fc54a857697ac0057f90f3bd4be8df3.json ├── hardhat.config.ts ├── package.json ├── src ├── constants.ts ├── index.ts └── yieldspace.ts ├── test ├── 011_yield_math_base.ts ├── 012_yield_math_reverts.ts ├── 013_yield_math_surface.ts ├── 014_yield_math_curve.ts ├── 031_pool_mint.ts ├── 032_pool_trade.ts ├── 033_pool_usdc.ts ├── 034_pool_mintWithBase.ts ├── 035_pool_twar.ts ├── 036_pool_extensions.ts └── shared │ ├── fixtures.ts │ ├── helpers.ts │ └── poolEstimator.ts ├── tsconfig-publish.json ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2020": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "airbnb-base" 8 | ], 9 | "parserOptions": { 10 | "ecmaVersion": 11, 11 | "sourceType": "module" 12 | }, 13 | "rules": { 14 | } 15 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v2 16 | with: 17 | node-version: 12.x 18 | registry-url: 'https://registry.npmjs.org' 19 | 20 | - id: yarn-cache 21 | run: echo "::set-output name=dir::$(yarn cache dir)" 22 | 23 | - uses: actions/cache@v1 24 | with: 25 | path: ${{ steps.yarn-cache.outputs.dir }} 26 | key: yarn-${{ hashFiles('**/yarn.lock') }} 27 | restore-keys: | 28 | yarn- 29 | - run: yarn 30 | env: 31 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 32 | - run: yarn lint:ts 33 | # - run: yarn lint:sol 34 | 35 | test: 36 | runs-on: ubuntu-latest 37 | 38 | steps: 39 | - uses: actions/checkout@v2 40 | - uses: actions/setup-node@v2 41 | with: 42 | node-version: 12.x 43 | registry-url: 'https://registry.npmjs.org' 44 | 45 | - id: yarn-cache 46 | run: echo "::set-output name=dir::$(yarn cache dir)" 47 | 48 | - uses: actions/cache@v1 49 | with: 50 | path: ${{ steps.yarn-cache.outputs.dir }} 51 | key: yarn-${{ hashFiles('**/yarn.lock') }} 52 | restore-keys: | 53 | yarn- 54 | - run: yarn 55 | env: 56 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 57 | - run: yarn test 58 | 59 | migrations: 60 | runs-on: ubuntu-latest 61 | 62 | steps: 63 | - uses: actions/checkout@v2 64 | - uses: actions/setup-node@v2 65 | with: 66 | node-version: 12.x 67 | registry-url: 'https://registry.npmjs.org' 68 | 69 | - id: yarn-cache 70 | run: echo "::set-output name=dir::$(yarn cache dir)" 71 | 72 | - uses: actions/cache@v1 73 | with: 74 | path: ${{ steps.yarn-cache.outputs.dir }} 75 | key: yarn-${{ hashFiles('**/yarn.lock') }} 76 | restore-keys: | 77 | yarn- 78 | - run: yarn 79 | env: 80 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 81 | - run: yarn test:deploy 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Temp & IDE files 2 | .vscode/ 3 | *~ 4 | *.swp 5 | *.swo 6 | 7 | # Packaging 8 | *.tar.gz 9 | *.tgz 10 | package/ 11 | 12 | # Buidler files 13 | cache 14 | artifacts 15 | node_modules/ 16 | abi/ 17 | output/ 18 | typechain/ 19 | deployments/ 20 | 21 | # Secret files 22 | .etherscanKey 23 | .infuraKey 24 | .mythxKey 25 | .secret 26 | build/ 27 | 28 | # Solidity-coverage 29 | coverage/ 30 | .coverage_contracts/ 31 | coverage.json 32 | 33 | # Yarn 34 | yarn-error.log 35 | 36 | # NPM 37 | package-lock.json 38 | 39 | # Crytic 40 | crytic-export/ 41 | 42 | # Typescript 43 | dist/ 44 | 45 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "printWidth": 120 5 | } 6 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | skipFiles: [ 3 | 'Migrations.sol', 4 | 'chai', 5 | 'maker', 6 | 'mocks' 7 | ] 8 | }; -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": [], 4 | "rules": { 5 | "avoid-throw": "off", 6 | "avoid-suicide": "error", 7 | "avoid-sha3": "warn", 8 | "compiler-version": "off", 9 | "no-empty-blocks": "off" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.soliumignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yieldprotocol/yieldspace-v2-DEPRECATED/17b18e26de37e52e504b82f2883cf90249b7a9f5/.soliumignore -------------------------------------------------------------------------------- /.soliumrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solium:all", 3 | "plugins": [ 4 | "security" 5 | ], 6 | "rules": { 7 | "error-reason": "off", 8 | "indentation": [ 9 | "error", 10 | 4 11 | ], 12 | "lbrace": "off", 13 | "linebreak-style": [ 14 | "error", 15 | "unix" 16 | ], 17 | "max-len": [ 18 | "error", 19 | 119 20 | ], 21 | "no-constant": [ 22 | "error" 23 | ], 24 | "no-empty-blocks": "off", 25 | "quotes": [ 26 | "error", 27 | "double" 28 | ], 29 | "uppercase": "off", 30 | "visibility-first": "error", 31 | "security/enforce-explicit-visibility": [ 32 | "error" 33 | ], 34 | "security/no-block-members": [ 35 | "warning" 36 | ], 37 | "security/no-inline-assembly": [ 38 | "warning" 39 | ], 40 | "imports-on-top": "warning", 41 | "variable-declarations": "warning", 42 | "array-declarations": "warning", 43 | "operator-whitespace": "warning", 44 | "function-whitespace": "warning", 45 | "semicolon-whitespace": "warning", 46 | "comma-whitespace": "warning", 47 | "conditionals-whitespace": "warning", 48 | "arg-overflow": "off" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /@types/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module Chai { 2 | interface Assertion { 3 | bignumber: Assertion; 4 | } 5 | } 6 | 7 | declare module 'ganache-time-traveler' { 8 | export function takeSnapshot(): Promise; 9 | export function revertToSnapshot(id: string): Promise; 10 | export function advanceTime(time: number): Promise; 11 | export function advanceBlock(): Promise; 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Business Source License 1.1 2 | 3 | License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved. 4 | "Business Source License" is a trademark of MariaDB Corporation Ab. 5 | 6 | ----------------------------------------------------------------------------- 7 | 8 | Parameters 9 | 10 | Licensor: Yield Inc. 11 | 12 | Licensed Work: Yield Protocol v2 13 | The Licensed Work is (c) 2021 Yield Inc. 14 | 15 | Additional Use Grant: Any uses listed and defined at 16 | v2-yieldspace-license-grants.yieldprotocol.eth 17 | 18 | Change Date: The earlier of 2023-06-01 or a date specified at 19 | v2-yieldspace-license-date.yieldprotocol.eth 20 | 21 | Change License: GNU General Public License v2.0 or later 22 | 23 | ----------------------------------------------------------------------------- 24 | 25 | Terms 26 | 27 | The Licensor hereby grants you the right to copy, modify, create derivative 28 | works, redistribute, and make non-production use of the Licensed Work. The 29 | Licensor may make an Additional Use Grant, above, permitting limited 30 | production use. 31 | 32 | Effective on the Change Date, or the fourth anniversary of the first publicly 33 | available distribution of a specific version of the Licensed Work under this 34 | License, whichever comes first, the Licensor hereby grants you rights under 35 | the terms of the Change License, and the rights granted in the paragraph 36 | above terminate. 37 | 38 | If your use of the Licensed Work does not comply with the requirements 39 | currently in effect as described in this License, you must purchase a 40 | commercial license from the Licensor, its affiliated entities, or authorized 41 | resellers, or you must refrain from using the Licensed Work. 42 | 43 | All copies of the original and modified Licensed Work, and derivative works 44 | of the Licensed Work, are subject to this License. This License applies 45 | separately for each version of the Licensed Work and the Change Date may vary 46 | for each version of the Licensed Work released by Licensor. 47 | 48 | You must conspicuously display this License on each original or modified copy 49 | of the Licensed Work. If you receive the Licensed Work in original or 50 | modified form from a third party, the terms and conditions set forth in this 51 | License apply to your use of that work. 52 | 53 | Any use of the Licensed Work in violation of this License will automatically 54 | terminate your rights under this License for the current and all other 55 | versions of the Licensed Work. 56 | 57 | This License does not grant you any right in any trademark or logo of 58 | Licensor or its affiliates (provided that you may use a trademark or logo of 59 | Licensor as expressly required by this License). 60 | 61 | TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON 62 | AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, 63 | EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF 64 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND 65 | TITLE. 66 | 67 | MariaDB hereby grants you permission to use this License’s text to license 68 | your works, and to refer to it using the trademark "Business Source License", 69 | as long as you comply with the Covenants of Licensor below. 70 | 71 | ----------------------------------------------------------------------------- 72 | 73 | Covenants of Licensor 74 | 75 | In consideration of the right to use this License’s text and the "Business 76 | Source License" name and trademark, Licensor covenants to MariaDB, and to all 77 | other recipients of the licensed work to be provided by Licensor: 78 | 79 | 1. To specify as the Change License the GPL Version 2.0 or any later version, 80 | or a license that is compatible with GPL Version 2.0 or a later version, 81 | where "compatible" means that software provided under the Change License can 82 | be included in a program with software provided under GPL Version 2.0 or a 83 | later version. Licensor may specify additional Change Licenses without 84 | limitation. 85 | 86 | 2. To either: (a) specify an additional grant of rights to use that does not 87 | impose any additional restriction on the right granted in this License, as 88 | the Additional Use Grant; or (b) insert the text "None". 89 | 90 | 3. To specify a Change Date. 91 | 92 | 4. Not to modify this License in any other way. 93 | 94 | ----------------------------------------------------------------------------- 95 | 96 | Notice 97 | 98 | The Business Source License (this document, or the "License") is not an Open 99 | Source license. However, the Licensed Work will eventually be made available 100 | under an Open Source License, as stated in this License. 101 | -------------------------------------------------------------------------------- /contracts/Math64x64.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | /* 3 | * Math 64.64 Smart Contract Library. Copyright © 2019 by Consulting. 4 | * Author: Mikhail Vladimirov 5 | */ 6 | pragma solidity 0.8.14; 7 | 8 | /** 9 | * Smart contract library of mathematical functions operating with signed 10 | * 64.64-bit fixed point numbers. Signed 64.64-bit fixed point number is 11 | * basically a simple fraction whose numerator is signed 128-bit integer and 12 | * denominator is 2^64. As long as denominator is always the same, there is no 13 | * need to store it, thus in Solidity signed 64.64-bit fixed point numbers are 14 | * represented by int128 type holding only the numerator. 15 | */ 16 | library Math64x64 { 17 | /* 18 | * Minimum value signed 64.64-bit fixed point number may have. 19 | */ 20 | int128 private constant MIN_64x64 = -0x80000000000000000000000000000000; 21 | 22 | /* 23 | * Maximum value signed 64.64-bit fixed point number may have. 24 | */ 25 | int128 private constant MAX_64x64 = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; 26 | 27 | /** 28 | * Convert signed 256-bit integer number into signed 64.64-bit fixed point 29 | * number. Revert on overflow. 30 | * 31 | * @param x signed 256-bit integer number 32 | * @return signed 64.64-bit fixed point number 33 | */ 34 | function fromInt (int256 x) internal pure returns (int128) { 35 | unchecked { 36 | require (x >= -0x8000000000000000 && x <= 0x7FFFFFFFFFFFFFFF); 37 | return int128 (x << 64); 38 | } 39 | } 40 | 41 | /** 42 | * Convert signed 64.64 fixed point number into signed 64-bit integer number 43 | * rounding down. 44 | * 45 | * @param x signed 64.64-bit fixed point number 46 | * @return signed 64-bit integer number 47 | */ 48 | function toInt (int128 x) internal pure returns (int64) { 49 | unchecked { 50 | return int64 (x >> 64); 51 | } 52 | } 53 | 54 | /** 55 | * Convert unsigned 256-bit integer number into signed 64.64-bit fixed point 56 | * number. Revert on overflow. 57 | * 58 | * @param x unsigned 256-bit integer number 59 | * @return signed 64.64-bit fixed point number 60 | */ 61 | function fromUInt (uint256 x) internal pure returns (int128) { 62 | unchecked { 63 | require (x <= 0x7FFFFFFFFFFFFFFF); 64 | return int128 (int256 (x << 64)); 65 | } 66 | } 67 | 68 | /** 69 | * Convert signed 64.64 fixed point number into unsigned 64-bit integer 70 | * number rounding down. Revert on underflow. 71 | * 72 | * @param x signed 64.64-bit fixed point number 73 | * @return unsigned 64-bit integer number 74 | */ 75 | function toUInt (int128 x) internal pure returns (uint64) { 76 | unchecked { 77 | require (x >= 0); 78 | return uint64 (uint128 (x >> 64)); 79 | } 80 | } 81 | 82 | /** 83 | * Convert signed 128.128 fixed point number into signed 64.64-bit fixed point 84 | * number rounding down. Revert on overflow. 85 | * 86 | * @param x signed 128.128-bin fixed point number 87 | * @return signed 64.64-bit fixed point number 88 | */ 89 | function from128x128 (int256 x) internal pure returns (int128) { 90 | unchecked { 91 | int256 result = x >> 64; 92 | require (result >= MIN_64x64 && result <= MAX_64x64); 93 | return int128 (result); 94 | } 95 | } 96 | 97 | /** 98 | * Convert signed 64.64 fixed point number into signed 128.128 fixed point 99 | * number. 100 | * 101 | * @param x signed 64.64-bit fixed point number 102 | * @return signed 128.128 fixed point number 103 | */ 104 | function to128x128 (int128 x) internal pure returns (int256) { 105 | unchecked { 106 | return int256 (x) << 64; 107 | } 108 | } 109 | 110 | /** 111 | * Calculate x + y. Revert on overflow. 112 | * 113 | * @param x signed 64.64-bit fixed point number 114 | * @param y signed 64.64-bit fixed point number 115 | * @return signed 64.64-bit fixed point number 116 | */ 117 | function add (int128 x, int128 y) internal pure returns (int128) { 118 | unchecked { 119 | int256 result = int256(x) + y; 120 | require (result >= MIN_64x64 && result <= MAX_64x64); 121 | return int128 (result); 122 | } 123 | } 124 | 125 | /** 126 | * Calculate x - y. Revert on overflow. 127 | * 128 | * @param x signed 64.64-bit fixed point number 129 | * @param y signed 64.64-bit fixed point number 130 | * @return signed 64.64-bit fixed point number 131 | */ 132 | function sub (int128 x, int128 y) internal pure returns (int128) { 133 | unchecked { 134 | int256 result = int256(x) - y; 135 | require (result >= MIN_64x64 && result <= MAX_64x64); 136 | return int128 (result); 137 | } 138 | } 139 | 140 | /** 141 | * Calculate x * y rounding down. Revert on overflow. 142 | * 143 | * @param x signed 64.64-bit fixed point number 144 | * @param y signed 64.64-bit fixed point number 145 | * @return signed 64.64-bit fixed point number 146 | */ 147 | function mul (int128 x, int128 y) internal pure returns (int128) { 148 | unchecked { 149 | int256 result = int256(x) * y >> 64; 150 | require (result >= MIN_64x64 && result <= MAX_64x64); 151 | return int128 (result); 152 | } 153 | } 154 | 155 | /** 156 | * Calculate x * y rounding towards zero, where x is signed 64.64 fixed point 157 | * number and y is signed 256-bit integer number. Revert on overflow. 158 | * 159 | * @param x signed 64.64 fixed point number 160 | * @param y signed 256-bit integer number 161 | * @return signed 256-bit integer number 162 | */ 163 | function muli (int128 x, int256 y) internal pure returns (int256) { 164 | unchecked { 165 | if (x == MIN_64x64) { 166 | require (y >= -0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF && 167 | y <= 0x1000000000000000000000000000000000000000000000000); 168 | return -y << 63; 169 | } else { 170 | bool negativeResult = false; 171 | if (x < 0) { 172 | x = -x; 173 | negativeResult = true; 174 | } 175 | if (y < 0) { 176 | y = -y; // We rely on overflow behavior here 177 | negativeResult = !negativeResult; 178 | } 179 | uint256 absoluteResult = mulu (x, uint256 (y)); 180 | if (negativeResult) { 181 | require (absoluteResult <= 182 | 0x8000000000000000000000000000000000000000000000000000000000000000); 183 | return -int256 (absoluteResult); // We rely on overflow behavior here 184 | } else { 185 | require (absoluteResult <= 186 | 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF); 187 | return int256 (absoluteResult); 188 | } 189 | } 190 | } 191 | } 192 | 193 | /** 194 | * Calculate x * y rounding down, where x is signed 64.64 fixed point number 195 | * and y is unsigned 256-bit integer number. Revert on overflow. 196 | * 197 | * @param x signed 64.64 fixed point number 198 | * @param y unsigned 256-bit integer number 199 | * @return unsigned 256-bit integer number 200 | */ 201 | function mulu (int128 x, uint256 y) internal pure returns (uint256) { 202 | unchecked { 203 | if (y == 0) return 0; 204 | 205 | require (x >= 0); 206 | 207 | uint256 lo = (uint256 (int256 (x)) * (y & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)) >> 64; 208 | uint256 hi = uint256 (int256 (x)) * (y >> 128); 209 | 210 | require (hi <= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF); 211 | hi <<= 64; 212 | 213 | require (hi <= 214 | 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF - lo); 215 | return hi + lo; 216 | } 217 | } 218 | 219 | /** 220 | * Calculate x / y rounding towards zero. Revert on overflow or when y is 221 | * zero. 222 | * 223 | * @param x signed 64.64-bit fixed point number 224 | * @param y signed 64.64-bit fixed point number 225 | * @return signed 64.64-bit fixed point number 226 | */ 227 | function div (int128 x, int128 y) internal pure returns (int128) { 228 | unchecked { 229 | require (y != 0); 230 | int256 result = (int256 (x) << 64) / y; 231 | require (result >= MIN_64x64 && result <= MAX_64x64); 232 | return int128 (result); 233 | } 234 | } 235 | 236 | /** 237 | * Calculate x / y rounding towards zero, where x and y are signed 256-bit 238 | * integer numbers. Revert on overflow or when y is zero. 239 | * 240 | * @param x signed 256-bit integer number 241 | * @param y signed 256-bit integer number 242 | * @return signed 64.64-bit fixed point number 243 | */ 244 | function divi (int256 x, int256 y) internal pure returns (int128) { 245 | unchecked { 246 | require (y != 0); 247 | 248 | bool negativeResult = false; 249 | if (x < 0) { 250 | x = -x; // We rely on overflow behavior here 251 | negativeResult = true; 252 | } 253 | if (y < 0) { 254 | y = -y; // We rely on overflow behavior here 255 | negativeResult = !negativeResult; 256 | } 257 | uint128 absoluteResult = divuu (uint256 (x), uint256 (y)); 258 | if (negativeResult) { 259 | require (absoluteResult <= 0x80000000000000000000000000000000); 260 | return -int128 (absoluteResult); // We rely on overflow behavior here 261 | } else { 262 | require (absoluteResult <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF); 263 | return int128 (absoluteResult); // We rely on overflow behavior here 264 | } 265 | } 266 | } 267 | 268 | /** 269 | * Calculate x / y rounding towards zero, where x and y are unsigned 256-bit 270 | * integer numbers. Revert on overflow or when y is zero. 271 | * 272 | * @param x unsigned 256-bit integer number 273 | * @param y unsigned 256-bit integer number 274 | * @return signed 64.64-bit fixed point number 275 | */ 276 | function divu (uint256 x, uint256 y) internal pure returns (int128) { 277 | unchecked { 278 | require (y != 0); 279 | uint128 result = divuu (x, y); 280 | require (result <= uint128 (MAX_64x64)); 281 | return int128 (result); 282 | } 283 | } 284 | 285 | /** 286 | * Calculate -x. Revert on overflow. 287 | * 288 | * @param x signed 64.64-bit fixed point number 289 | * @return signed 64.64-bit fixed point number 290 | */ 291 | function neg (int128 x) internal pure returns (int128) { 292 | unchecked { 293 | require (x != MIN_64x64); 294 | return -x; 295 | } 296 | } 297 | 298 | /** 299 | * Calculate |x|. Revert on overflow. 300 | * 301 | * @param x signed 64.64-bit fixed point number 302 | * @return signed 64.64-bit fixed point number 303 | */ 304 | function abs (int128 x) internal pure returns (int128) { 305 | unchecked { 306 | require (x != MIN_64x64); 307 | return x < 0 ? -x : x; 308 | } 309 | } 310 | 311 | /** 312 | * Calculate 1 / x rounding towards zero. Revert on overflow or when x is 313 | * zero. 314 | * 315 | * @param x signed 64.64-bit fixed point number 316 | * @return signed 64.64-bit fixed point number 317 | */ 318 | function inv (int128 x) internal pure returns (int128) { 319 | unchecked { 320 | require (x != 0); 321 | int256 result = int256 (0x100000000000000000000000000000000) / x; 322 | require (result >= MIN_64x64 && result <= MAX_64x64); 323 | return int128 (result); 324 | } 325 | } 326 | 327 | /** 328 | * Calculate arithmetics average of x and y, i.e. (x + y) / 2 rounding down. 329 | * 330 | * @param x signed 64.64-bit fixed point number 331 | * @param y signed 64.64-bit fixed point number 332 | * @return signed 64.64-bit fixed point number 333 | */ 334 | function avg (int128 x, int128 y) internal pure returns (int128) { 335 | unchecked { 336 | return int128 ((int256 (x) + int256 (y)) >> 1); 337 | } 338 | } 339 | 340 | /** 341 | * Calculate geometric average of x and y, i.e. sqrt (x * y) rounding down. 342 | * Revert on overflow or in case x * y is negative. 343 | * 344 | * @param x signed 64.64-bit fixed point number 345 | * @param y signed 64.64-bit fixed point number 346 | * @return signed 64.64-bit fixed point number 347 | */ 348 | function gavg (int128 x, int128 y) internal pure returns (int128) { 349 | unchecked { 350 | int256 m = int256 (x) * int256 (y); 351 | require (m >= 0); 352 | require (m < 353 | 0x4000000000000000000000000000000000000000000000000000000000000000); 354 | return int128 (sqrtu (uint256 (m))); 355 | } 356 | } 357 | 358 | /** 359 | * Calculate x^y assuming 0^0 is 1, where x is signed 64.64 fixed point number 360 | * and y is unsigned 256-bit integer number. Revert on overflow. 361 | * 362 | * @param x signed 64.64-bit fixed point number 363 | * @param y uint256 value 364 | * @return signed 64.64-bit fixed point number 365 | */ 366 | function pow (int128 x, uint256 y) internal pure returns (int128) { 367 | unchecked { 368 | bool negative = x < 0 && y & 1 == 1; 369 | 370 | uint256 absX = uint128 (x < 0 ? -x : x); 371 | uint256 absResult; 372 | absResult = 0x100000000000000000000000000000000; 373 | 374 | if (absX <= 0x10000000000000000) { 375 | absX <<= 63; 376 | while (y != 0) { 377 | if (y & 0x1 != 0) { 378 | absResult = absResult * absX >> 127; 379 | } 380 | absX = absX * absX >> 127; 381 | 382 | if (y & 0x2 != 0) { 383 | absResult = absResult * absX >> 127; 384 | } 385 | absX = absX * absX >> 127; 386 | 387 | if (y & 0x4 != 0) { 388 | absResult = absResult * absX >> 127; 389 | } 390 | absX = absX * absX >> 127; 391 | 392 | if (y & 0x8 != 0) { 393 | absResult = absResult * absX >> 127; 394 | } 395 | absX = absX * absX >> 127; 396 | 397 | y >>= 4; 398 | } 399 | 400 | absResult >>= 64; 401 | } else { 402 | uint256 absXShift = 63; 403 | if (absX < 0x1000000000000000000000000) { absX <<= 32; absXShift -= 32; } 404 | if (absX < 0x10000000000000000000000000000) { absX <<= 16; absXShift -= 16; } 405 | if (absX < 0x1000000000000000000000000000000) { absX <<= 8; absXShift -= 8; } 406 | if (absX < 0x10000000000000000000000000000000) { absX <<= 4; absXShift -= 4; } 407 | if (absX < 0x40000000000000000000000000000000) { absX <<= 2; absXShift -= 2; } 408 | if (absX < 0x80000000000000000000000000000000) { absX <<= 1; absXShift -= 1; } 409 | 410 | uint256 resultShift = 0; 411 | while (y != 0) { 412 | require (absXShift < 64); 413 | 414 | if (y & 0x1 != 0) { 415 | absResult = absResult * absX >> 127; 416 | resultShift += absXShift; 417 | if (absResult > 0x100000000000000000000000000000000) { 418 | absResult >>= 1; 419 | resultShift += 1; 420 | } 421 | } 422 | absX = absX * absX >> 127; 423 | absXShift <<= 1; 424 | if (absX >= 0x100000000000000000000000000000000) { 425 | absX >>= 1; 426 | absXShift += 1; 427 | } 428 | 429 | y >>= 1; 430 | } 431 | 432 | require (resultShift < 64); 433 | absResult >>= 64 - resultShift; 434 | } 435 | int256 result = negative ? -int256 (absResult) : int256 (absResult); 436 | require (result >= MIN_64x64 && result <= MAX_64x64); 437 | return int128 (result); 438 | } 439 | } 440 | 441 | /** 442 | * Calculate sqrt (x) rounding down. Revert if x < 0. 443 | * 444 | * @param x signed 64.64-bit fixed point number 445 | * @return signed 64.64-bit fixed point number 446 | */ 447 | function sqrt (int128 x) internal pure returns (int128) { 448 | unchecked { 449 | require (x >= 0); 450 | return int128 (sqrtu (uint256 (int256 (x)) << 64)); 451 | } 452 | } 453 | 454 | /** 455 | * Calculate binary logarithm of x. Revert if x <= 0. 456 | * 457 | * @param x signed 64.64-bit fixed point number 458 | * @return signed 64.64-bit fixed point number 459 | */ 460 | function log_2 (int128 x) internal pure returns (int128) { 461 | unchecked { 462 | require (x > 0); 463 | 464 | int256 msb = 0; 465 | int256 xc = x; 466 | if (xc >= 0x10000000000000000) { xc >>= 64; msb += 64; } 467 | if (xc >= 0x100000000) { xc >>= 32; msb += 32; } 468 | if (xc >= 0x10000) { xc >>= 16; msb += 16; } 469 | if (xc >= 0x100) { xc >>= 8; msb += 8; } 470 | if (xc >= 0x10) { xc >>= 4; msb += 4; } 471 | if (xc >= 0x4) { xc >>= 2; msb += 2; } 472 | if (xc >= 0x2) msb += 1; // No need to shift xc anymore 473 | 474 | int256 result = msb - 64 << 64; 475 | uint256 ux = uint256 (int256 (x)) << uint256 (127 - msb); 476 | for (int256 bit = 0x8000000000000000; bit > 0; bit >>= 1) { 477 | ux *= ux; 478 | uint256 b = ux >> 255; 479 | ux >>= 127 + b; 480 | result += bit * int256 (b); 481 | } 482 | 483 | return int128 (result); 484 | } 485 | } 486 | 487 | /** 488 | * Calculate natural logarithm of x. Revert if x <= 0. 489 | * 490 | * @param x signed 64.64-bit fixed point number 491 | * @return signed 64.64-bit fixed point number 492 | */ 493 | function ln (int128 x) internal pure returns (int128) { 494 | unchecked { 495 | require (x > 0); 496 | 497 | return int128 (int256 ( 498 | uint256 (int256 (log_2 (x))) * 0xB17217F7D1CF79ABC9E3B39803F2F6AF >> 128)); 499 | } 500 | } 501 | 502 | /** 503 | * Calculate binary exponent of x. Revert on overflow. 504 | * 505 | * @param x signed 64.64-bit fixed point number 506 | * @return signed 64.64-bit fixed point number 507 | */ 508 | function exp_2 (int128 x) internal pure returns (int128) { 509 | unchecked { 510 | require (x < 0x400000000000000000); // Overflow 511 | 512 | if (x < -0x400000000000000000) return 0; // Underflow 513 | 514 | uint256 result = 0x80000000000000000000000000000000; 515 | 516 | if (x & 0x8000000000000000 > 0) 517 | result = result * 0x16A09E667F3BCC908B2FB1366EA957D3E >> 128; 518 | if (x & 0x4000000000000000 > 0) 519 | result = result * 0x1306FE0A31B7152DE8D5A46305C85EDEC >> 128; 520 | if (x & 0x2000000000000000 > 0) 521 | result = result * 0x1172B83C7D517ADCDF7C8C50EB14A791F >> 128; 522 | if (x & 0x1000000000000000 > 0) 523 | result = result * 0x10B5586CF9890F6298B92B71842A98363 >> 128; 524 | if (x & 0x800000000000000 > 0) 525 | result = result * 0x1059B0D31585743AE7C548EB68CA417FD >> 128; 526 | if (x & 0x400000000000000 > 0) 527 | result = result * 0x102C9A3E778060EE6F7CACA4F7A29BDE8 >> 128; 528 | if (x & 0x200000000000000 > 0) 529 | result = result * 0x10163DA9FB33356D84A66AE336DCDFA3F >> 128; 530 | if (x & 0x100000000000000 > 0) 531 | result = result * 0x100B1AFA5ABCBED6129AB13EC11DC9543 >> 128; 532 | if (x & 0x80000000000000 > 0) 533 | result = result * 0x10058C86DA1C09EA1FF19D294CF2F679B >> 128; 534 | if (x & 0x40000000000000 > 0) 535 | result = result * 0x1002C605E2E8CEC506D21BFC89A23A00F >> 128; 536 | if (x & 0x20000000000000 > 0) 537 | result = result * 0x100162F3904051FA128BCA9C55C31E5DF >> 128; 538 | if (x & 0x10000000000000 > 0) 539 | result = result * 0x1000B175EFFDC76BA38E31671CA939725 >> 128; 540 | if (x & 0x8000000000000 > 0) 541 | result = result * 0x100058BA01FB9F96D6CACD4B180917C3D >> 128; 542 | if (x & 0x4000000000000 > 0) 543 | result = result * 0x10002C5CC37DA9491D0985C348C68E7B3 >> 128; 544 | if (x & 0x2000000000000 > 0) 545 | result = result * 0x1000162E525EE054754457D5995292026 >> 128; 546 | if (x & 0x1000000000000 > 0) 547 | result = result * 0x10000B17255775C040618BF4A4ADE83FC >> 128; 548 | if (x & 0x800000000000 > 0) 549 | result = result * 0x1000058B91B5BC9AE2EED81E9B7D4CFAB >> 128; 550 | if (x & 0x400000000000 > 0) 551 | result = result * 0x100002C5C89D5EC6CA4D7C8ACC017B7C9 >> 128; 552 | if (x & 0x200000000000 > 0) 553 | result = result * 0x10000162E43F4F831060E02D839A9D16D >> 128; 554 | if (x & 0x100000000000 > 0) 555 | result = result * 0x100000B1721BCFC99D9F890EA06911763 >> 128; 556 | if (x & 0x80000000000 > 0) 557 | result = result * 0x10000058B90CF1E6D97F9CA14DBCC1628 >> 128; 558 | if (x & 0x40000000000 > 0) 559 | result = result * 0x1000002C5C863B73F016468F6BAC5CA2B >> 128; 560 | if (x & 0x20000000000 > 0) 561 | result = result * 0x100000162E430E5A18F6119E3C02282A5 >> 128; 562 | if (x & 0x10000000000 > 0) 563 | result = result * 0x1000000B1721835514B86E6D96EFD1BFE >> 128; 564 | if (x & 0x8000000000 > 0) 565 | result = result * 0x100000058B90C0B48C6BE5DF846C5B2EF >> 128; 566 | if (x & 0x4000000000 > 0) 567 | result = result * 0x10000002C5C8601CC6B9E94213C72737A >> 128; 568 | if (x & 0x2000000000 > 0) 569 | result = result * 0x1000000162E42FFF037DF38AA2B219F06 >> 128; 570 | if (x & 0x1000000000 > 0) 571 | result = result * 0x10000000B17217FBA9C739AA5819F44F9 >> 128; 572 | if (x & 0x800000000 > 0) 573 | result = result * 0x1000000058B90BFCDEE5ACD3C1CEDC823 >> 128; 574 | if (x & 0x400000000 > 0) 575 | result = result * 0x100000002C5C85FE31F35A6A30DA1BE50 >> 128; 576 | if (x & 0x200000000 > 0) 577 | result = result * 0x10000000162E42FF0999CE3541B9FFFCF >> 128; 578 | if (x & 0x100000000 > 0) 579 | result = result * 0x100000000B17217F80F4EF5AADDA45554 >> 128; 580 | if (x & 0x80000000 > 0) 581 | result = result * 0x10000000058B90BFBF8479BD5A81B51AD >> 128; 582 | if (x & 0x40000000 > 0) 583 | result = result * 0x1000000002C5C85FDF84BD62AE30A74CC >> 128; 584 | if (x & 0x20000000 > 0) 585 | result = result * 0x100000000162E42FEFB2FED257559BDAA >> 128; 586 | if (x & 0x10000000 > 0) 587 | result = result * 0x1000000000B17217F7D5A7716BBA4A9AE >> 128; 588 | if (x & 0x8000000 > 0) 589 | result = result * 0x100000000058B90BFBE9DDBAC5E109CCE >> 128; 590 | if (x & 0x4000000 > 0) 591 | result = result * 0x10000000002C5C85FDF4B15DE6F17EB0D >> 128; 592 | if (x & 0x2000000 > 0) 593 | result = result * 0x1000000000162E42FEFA494F1478FDE05 >> 128; 594 | if (x & 0x1000000 > 0) 595 | result = result * 0x10000000000B17217F7D20CF927C8E94C >> 128; 596 | if (x & 0x800000 > 0) 597 | result = result * 0x1000000000058B90BFBE8F71CB4E4B33D >> 128; 598 | if (x & 0x400000 > 0) 599 | result = result * 0x100000000002C5C85FDF477B662B26945 >> 128; 600 | if (x & 0x200000 > 0) 601 | result = result * 0x10000000000162E42FEFA3AE53369388C >> 128; 602 | if (x & 0x100000 > 0) 603 | result = result * 0x100000000000B17217F7D1D351A389D40 >> 128; 604 | if (x & 0x80000 > 0) 605 | result = result * 0x10000000000058B90BFBE8E8B2D3D4EDE >> 128; 606 | if (x & 0x40000 > 0) 607 | result = result * 0x1000000000002C5C85FDF4741BEA6E77E >> 128; 608 | if (x & 0x20000 > 0) 609 | result = result * 0x100000000000162E42FEFA39FE95583C2 >> 128; 610 | if (x & 0x10000 > 0) 611 | result = result * 0x1000000000000B17217F7D1CFB72B45E1 >> 128; 612 | if (x & 0x8000 > 0) 613 | result = result * 0x100000000000058B90BFBE8E7CC35C3F0 >> 128; 614 | if (x & 0x4000 > 0) 615 | result = result * 0x10000000000002C5C85FDF473E242EA38 >> 128; 616 | if (x & 0x2000 > 0) 617 | result = result * 0x1000000000000162E42FEFA39F02B772C >> 128; 618 | if (x & 0x1000 > 0) 619 | result = result * 0x10000000000000B17217F7D1CF7D83C1A >> 128; 620 | if (x & 0x800 > 0) 621 | result = result * 0x1000000000000058B90BFBE8E7BDCBE2E >> 128; 622 | if (x & 0x400 > 0) 623 | result = result * 0x100000000000002C5C85FDF473DEA871F >> 128; 624 | if (x & 0x200 > 0) 625 | result = result * 0x10000000000000162E42FEFA39EF44D91 >> 128; 626 | if (x & 0x100 > 0) 627 | result = result * 0x100000000000000B17217F7D1CF79E949 >> 128; 628 | if (x & 0x80 > 0) 629 | result = result * 0x10000000000000058B90BFBE8E7BCE544 >> 128; 630 | if (x & 0x40 > 0) 631 | result = result * 0x1000000000000002C5C85FDF473DE6ECA >> 128; 632 | if (x & 0x20 > 0) 633 | result = result * 0x100000000000000162E42FEFA39EF366F >> 128; 634 | if (x & 0x10 > 0) 635 | result = result * 0x1000000000000000B17217F7D1CF79AFA >> 128; 636 | if (x & 0x8 > 0) 637 | result = result * 0x100000000000000058B90BFBE8E7BCD6D >> 128; 638 | if (x & 0x4 > 0) 639 | result = result * 0x10000000000000002C5C85FDF473DE6B2 >> 128; 640 | if (x & 0x2 > 0) 641 | result = result * 0x1000000000000000162E42FEFA39EF358 >> 128; 642 | if (x & 0x1 > 0) 643 | result = result * 0x10000000000000000B17217F7D1CF79AB >> 128; 644 | 645 | result >>= uint256 (int256 (63 - (x >> 64))); 646 | require (result <= uint256 (int256 (MAX_64x64))); 647 | 648 | return int128 (int256 (result)); 649 | } 650 | } 651 | 652 | /** 653 | * Calculate natural exponent of x. Revert on overflow. 654 | * 655 | * @param x signed 64.64-bit fixed point number 656 | * @return signed 64.64-bit fixed point number 657 | */ 658 | function exp (int128 x) internal pure returns (int128) { 659 | unchecked { 660 | require (x < 0x400000000000000000); // Overflow 661 | 662 | if (x < -0x400000000000000000) return 0; // Underflow 663 | 664 | return exp_2 ( 665 | int128 (int256 (x) * 0x171547652B82FE1777D0FFDA0D23A7D12 >> 128)); 666 | } 667 | } 668 | 669 | /** 670 | * Calculate x / y rounding towards zero, where x and y are unsigned 256-bit 671 | * integer numbers. Revert on overflow or when y is zero. 672 | * 673 | * @param x unsigned 256-bit integer number 674 | * @param y unsigned 256-bit integer number 675 | * @return unsigned 64.64-bit fixed point number 676 | */ 677 | function divuu (uint256 x, uint256 y) private pure returns (uint128) { 678 | unchecked { 679 | require (y != 0); 680 | 681 | uint256 result; 682 | 683 | if (x <= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) 684 | result = (x << 64) / y; 685 | else { 686 | uint256 msb = 192; 687 | uint256 xc = x >> 192; 688 | if (xc >= 0x100000000) { xc >>= 32; msb += 32; } 689 | if (xc >= 0x10000) { xc >>= 16; msb += 16; } 690 | if (xc >= 0x100) { xc >>= 8; msb += 8; } 691 | if (xc >= 0x10) { xc >>= 4; msb += 4; } 692 | if (xc >= 0x4) { xc >>= 2; msb += 2; } 693 | if (xc >= 0x2) msb += 1; // No need to shift xc anymore 694 | 695 | result = (x << 255 - msb) / ((y - 1 >> msb - 191) + 1); 696 | require (result <= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF); 697 | 698 | uint256 hi = result * (y >> 128); 699 | uint256 lo = result * (y & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF); 700 | 701 | uint256 xh = x >> 192; 702 | uint256 xl = x << 64; 703 | 704 | if (xl < lo) xh -= 1; 705 | xl -= lo; // We rely on overflow behavior here 706 | lo = hi << 128; 707 | if (xl < lo) xh -= 1; 708 | xl -= lo; // We rely on overflow behavior here 709 | 710 | assert (xh == hi >> 128); 711 | 712 | result += xl / y; 713 | } 714 | 715 | require (result <= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF); 716 | return uint128 (result); 717 | } 718 | } 719 | 720 | /** 721 | * Calculate sqrt (x) rounding down, where x is unsigned 256-bit integer 722 | * number. 723 | * 724 | * @param x unsigned 256-bit integer number 725 | * @return unsigned 128-bit integer number 726 | */ 727 | function sqrtu (uint256 x) private pure returns (uint128) { 728 | unchecked { 729 | if (x == 0) return 0; 730 | else { 731 | uint256 xx = x; 732 | uint256 r = 1; 733 | if (xx >= 0x100000000000000000000000000000000) { xx >>= 128; r <<= 64; } 734 | if (xx >= 0x10000000000000000) { xx >>= 64; r <<= 32; } 735 | if (xx >= 0x100000000) { xx >>= 32; r <<= 16; } 736 | if (xx >= 0x10000) { xx >>= 16; r <<= 8; } 737 | if (xx >= 0x100) { xx >>= 8; r <<= 4; } 738 | if (xx >= 0x10) { xx >>= 4; r <<= 2; } 739 | if (xx >= 0x8) { r <<= 1; } 740 | r = (r + x / r) >> 1; 741 | r = (r + x / r) >> 1; 742 | r = (r + x / r) >> 1; 743 | r = (r + x / r) >> 1; 744 | r = (r + x / r) >> 1; 745 | r = (r + x / r) >> 1; 746 | r = (r + x / r) >> 1; // Seven iterations should be enough 747 | uint256 r1 = x / r; 748 | return uint128 (r < r1 ? r : r1); 749 | } 750 | } 751 | } 752 | } -------------------------------------------------------------------------------- /contracts/Pool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.14; 3 | 4 | import "@yield-protocol/utils-v2/contracts/token/IERC20.sol"; 5 | import "@yield-protocol/utils-v2/contracts/token/IERC20Metadata.sol"; 6 | import "@yield-protocol/utils-v2/contracts/token/ERC20Permit.sol"; 7 | import "@yield-protocol/utils-v2/contracts/token/MinimalTransferHelper.sol"; 8 | import "@yield-protocol/utils-v2/contracts/cast/CastU256U128.sol"; 9 | import "@yield-protocol/utils-v2/contracts/cast/CastU256U112.sol"; 10 | import "@yield-protocol/utils-v2/contracts/cast/CastU256I256.sol"; 11 | import "@yield-protocol/utils-v2/contracts/cast/CastU128U112.sol"; 12 | import "@yield-protocol/utils-v2/contracts/cast/CastU128I128.sol"; 13 | import "@yield-protocol/yieldspace-interfaces/IPool.sol"; 14 | import "@yield-protocol/vault-interfaces/src/IFYToken.sol"; 15 | import "./YieldMath.sol"; 16 | 17 | 18 | /// @dev The Pool contract exchanges base for fyToken at a price defined by a specific formula. 19 | contract Pool is IPool, ERC20Permit { 20 | using CastU256U128 for uint256; 21 | using CastU256U112 for uint256; 22 | using CastU256I256 for uint256; 23 | using CastU128U112 for uint128; 24 | using CastU128I128 for uint128; 25 | using MinimalTransferHelper for IERC20; 26 | 27 | event Trade(uint32 maturity, address indexed from, address indexed to, int256 bases, int256 fyTokens); 28 | event Liquidity(uint32 maturity, address indexed from, address indexed to, address indexed fyTokenTo, int256 bases, int256 fyTokens, int256 poolTokens); 29 | event Sync(uint112 baseCached, uint112 fyTokenCached, uint256 cumulativeBalancesRatio); 30 | 31 | int128 public immutable override ts; // 1 / Seconds in 10 years, in 64.64 32 | int128 public immutable override g1; // To be used when selling base to the pool 33 | int128 public immutable override g2; // To be used when selling fyToken to the pool 34 | uint32 public immutable override maturity; 35 | uint96 public immutable override scaleFactor; // Scale up to 18 low decimal tokens to get the right precision in YieldMath 36 | 37 | IERC20 public immutable override base; 38 | IFYToken public immutable override fyToken; 39 | 40 | uint112 private baseCached; // uses single storage slot, accessible via getCache 41 | uint112 private fyTokenCached; // uses single storage slot, accessible via getCache 42 | uint32 private blockTimestampLast; // uses single storage slot, accessible via getCache 43 | 44 | uint256 public override cumulativeBalancesRatio; // Fixed point factor with 27 decimals (ray) 45 | 46 | /// @dev Deploy a Pool. 47 | /// Make sure that the fyToken follows ERC20 standards with regards to name, symbol and decimals 48 | constructor(IERC20 base_, IFYToken fyToken_, int128 ts_, int128 g1_, int128 g2_) 49 | ERC20Permit( 50 | string(abi.encodePacked(IERC20Metadata(address(fyToken_)).name(), " LP")), 51 | string(abi.encodePacked(IERC20Metadata(address(fyToken_)).symbol(), "LP")), 52 | IERC20Metadata(address(fyToken_)).decimals() 53 | ) 54 | { 55 | fyToken = fyToken_; 56 | base = base_; 57 | 58 | uint256 maturity_ = fyToken_.maturity(); 59 | require (maturity_ <= type(uint32).max, "Pool: Maturity too far in the future"); 60 | maturity = uint32(maturity_); 61 | 62 | ts = ts_; 63 | g1 = g1_; 64 | g2 = g2_; 65 | 66 | scaleFactor = uint96(10 ** (18 - uint96(decimals))); 67 | } 68 | 69 | /// @dev Trading can only be done before maturity 70 | modifier beforeMaturity() { 71 | require( 72 | block.timestamp < maturity, 73 | "Pool: Too late" 74 | ); 75 | _; 76 | } 77 | 78 | // ---- Balances management ---- 79 | 80 | /// @dev Updates the cache to match the actual balances. 81 | function sync() external { 82 | _update(_getBaseBalance(), _getFYTokenBalance(), baseCached, fyTokenCached); 83 | } 84 | 85 | /// @dev Returns the cached balances & last updated timestamp. 86 | /// @return Cached base token balance. 87 | /// @return Cached virtual FY token balance. 88 | /// @return Timestamp that balances were last cached. 89 | function getCache() 90 | external view override 91 | returns (uint112, uint112, uint32) 92 | { 93 | return (baseCached, fyTokenCached, blockTimestampLast); 94 | } 95 | 96 | /// @dev Returns the "virtual" fyToken balance, which is the real balance plus the pool token supply. 97 | function getFYTokenBalance() 98 | public view override 99 | returns(uint112) 100 | { 101 | return _getFYTokenBalance(); 102 | } 103 | 104 | /// @dev Returns the base balance 105 | function getBaseBalance() 106 | public view override 107 | returns(uint112) 108 | { 109 | return _getBaseBalance(); 110 | } 111 | 112 | /// @dev Returns the "virtual" fyToken balance, which is the real balance plus the pool token supply. 113 | function _getFYTokenBalance() 114 | internal view 115 | returns(uint112) 116 | { 117 | return (fyToken.balanceOf(address(this)) + _totalSupply).u112(); 118 | } 119 | 120 | /// @dev Returns the base balance 121 | function _getBaseBalance() 122 | internal view 123 | returns(uint112) 124 | { 125 | return base.balanceOf(address(this)).u112(); 126 | } 127 | 128 | /// @dev Retrieve any base tokens not accounted for in the cache 129 | function retrieveBase(address to) 130 | external override 131 | returns(uint128 retrieved) 132 | { 133 | retrieved = _getBaseBalance() - baseCached; // Cache can never be above balances 134 | base.safeTransfer(to, retrieved); 135 | // Now the current balances match the cache, so no need to update the TWAR 136 | } 137 | 138 | /// @dev Retrieve any fyTokens not accounted for in the cache 139 | function retrieveFYToken(address to) 140 | external override 141 | returns(uint128 retrieved) 142 | { 143 | retrieved = _getFYTokenBalance() - fyTokenCached; // Cache can never be above balances 144 | IERC20(address(fyToken)).safeTransfer(to, retrieved); 145 | // Now the balances match the cache, so no need to update the TWAR 146 | } 147 | 148 | /// @dev Update cache and, on the first call per block, ratio accumulators 149 | function _update(uint128 baseBalance, uint128 fyBalance, uint112 _baseCached, uint112 _fyTokenCached) private { 150 | uint32 blockTimestamp = uint32(block.timestamp); 151 | uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired 152 | if (timeElapsed > 0 && _baseCached != 0 && _fyTokenCached != 0) { 153 | // We multiply by 1e27 here so that r = t * y/x is a fixed point factor with 27 decimals 154 | uint256 scaledFYTokenCached = uint256(_fyTokenCached) * 1e27; 155 | cumulativeBalancesRatio += scaledFYTokenCached * timeElapsed / _baseCached; 156 | } 157 | baseCached = baseBalance.u112(); 158 | fyTokenCached = fyBalance.u112(); 159 | blockTimestampLast = blockTimestamp; 160 | emit Sync(baseCached, fyTokenCached, cumulativeBalancesRatio); 161 | } 162 | 163 | // ---- Liquidity ---- 164 | 165 | /// @dev Mint liquidity tokens in exchange for adding base and fyToken 166 | /// The amount of liquidity tokens to mint is calculated from the amount of unaccounted for fyToken in this contract. 167 | /// A proportional amount of base tokens need to be present in this contract, also unaccounted for. 168 | /// @param to Wallet receiving the minted liquidity tokens. 169 | /// @param remainder Wallet receiving any surplus base. 170 | /// @param minRatio Minimum ratio of base to fyToken in the pool. 171 | /// @param maxRatio Maximum ratio of base to fyToken in the pool. 172 | /// @return The amount of liquidity tokens minted. 173 | function mint(address to, address remainder, uint256 minRatio, uint256 maxRatio) 174 | external override 175 | returns (uint256, uint256, uint256) 176 | { 177 | return _mintInternal(to, remainder, 0, minRatio, maxRatio); 178 | } 179 | 180 | /// @dev Mint liquidity tokens in exchange for adding only base 181 | /// The amount of liquidity tokens is calculated from the amount of fyToken to buy from the pool, 182 | /// plus the amount of unaccounted for fyToken in this contract. 183 | /// The base tokens need to be present in this contract, unaccounted for. 184 | /// @param to Wallet receiving the minted liquidity tokens. 185 | /// @param remainder Wallet receiving any surplus base. 186 | /// @param fyTokenToBuy Amount of `fyToken` being bought in the Pool, from this we calculate how much base it will be taken in. 187 | /// @param minRatio Minimum ratio of base to fyToken in the pool. 188 | /// @param maxRatio Maximum ratio of base to fyToken in the pool. 189 | /// @return The amount of liquidity tokens minted. 190 | function mintWithBase(address to, address remainder, uint256 fyTokenToBuy, uint256 minRatio, uint256 maxRatio) 191 | external override 192 | returns (uint256, uint256, uint256) 193 | { 194 | return _mintInternal(to, remainder, fyTokenToBuy, minRatio, maxRatio); 195 | } 196 | 197 | /// @dev Mint liquidity tokens, with an optional internal trade to buy fyToken beforehand. 198 | /// The amount of liquidity tokens is calculated from the amount of fyToken to buy from the pool, 199 | /// plus the amount of unaccounted for fyToken in this contract. 200 | /// The base tokens need to be present in this contract, unaccounted for. 201 | /// @param to Wallet receiving the minted liquidity tokens. 202 | /// @param remainder Wallet receiving any surplus base. 203 | /// @param fyTokenToBuy Amount of `fyToken` being bought in the Pool, from this we calculate how much base it will be taken in. 204 | /// @param minRatio Minimum ratio of base to fyToken in the pool. 205 | /// @param maxRatio Minimum ratio of base to fyToken in the pool. 206 | function _mintInternal(address to, address remainder, uint256 fyTokenToBuy, uint256 minRatio, uint256 maxRatio) 207 | internal 208 | returns (uint256 baseIn, uint256 fyTokenIn, uint256 tokensMinted) 209 | { 210 | // Gather data 211 | uint256 supply = _totalSupply; 212 | (uint112 _baseCached, uint112 _fyTokenCached) = 213 | (baseCached, fyTokenCached); 214 | uint256 _realFYTokenCached = _fyTokenCached - supply; // The fyToken cache includes the virtual fyToken, equal to the supply 215 | uint256 baseBalance = base.balanceOf(address(this)); 216 | uint256 fyTokenBalance = fyToken.balanceOf(address(this)); 217 | uint256 baseAvailable = baseBalance - _baseCached; 218 | 219 | // Check the burn wasn't sandwiched 220 | require ( 221 | _realFYTokenCached == 0 || ( 222 | uint256(_baseCached) * 1e18 / _realFYTokenCached >= minRatio && 223 | uint256(_baseCached) * 1e18 / _realFYTokenCached <= maxRatio 224 | ), 225 | "Pool: Reserves ratio changed" 226 | ); 227 | 228 | // Calculate token amounts 229 | if (supply == 0) { // Initialize at 1 pool token minted per base token supplied 230 | baseIn = baseAvailable; 231 | tokensMinted = baseIn; 232 | } else if (_realFYTokenCached == 0) { // Edge case, no fyToken in the Pool after initialization 233 | baseIn = baseAvailable; 234 | tokensMinted = supply * baseIn / _baseCached; 235 | } else { 236 | // There is an optional virtual trade before the mint 237 | uint256 baseToSell; 238 | if (fyTokenToBuy > 0) { 239 | baseToSell = _buyFYTokenPreview( 240 | fyTokenToBuy.u128(), 241 | _baseCached, 242 | _fyTokenCached 243 | ); 244 | } 245 | 246 | // We use all the available fyTokens, plus a virtual trade if it happened, surplus is in base tokens 247 | fyTokenIn = fyTokenBalance - _realFYTokenCached; 248 | tokensMinted = (supply * (fyTokenToBuy + fyTokenIn)) / (_realFYTokenCached - fyTokenToBuy); 249 | baseIn = baseToSell + ((_baseCached + baseToSell) * tokensMinted) / supply; 250 | require(baseAvailable >= baseIn, "Pool: Not enough base token in"); 251 | } 252 | 253 | // Update TWAR 254 | _update( 255 | (_baseCached + baseIn).u128(), 256 | (_fyTokenCached + fyTokenIn + tokensMinted).u128(), // Account for the "virtual" fyToken from the new minted LP tokens 257 | _baseCached, 258 | _fyTokenCached 259 | ); 260 | 261 | // Execute mint 262 | _mint(to, tokensMinted); 263 | 264 | // Return any unused base 265 | if (baseAvailable - baseIn > 0) base.safeTransfer(remainder, baseAvailable - baseIn); 266 | 267 | emit Liquidity(maturity, msg.sender, to, address(0), -(baseIn.i256()), -(fyTokenIn.i256()), tokensMinted.i256()); 268 | } 269 | 270 | /// @dev Burn liquidity tokens in exchange for base and fyToken. 271 | /// The liquidity tokens need to be in this contract. 272 | /// @param baseTo Wallet receiving the base. 273 | /// @param fyTokenTo Wallet receiving the fyToken. 274 | /// @param minRatio Minimum ratio of base to fyToken in the pool. 275 | /// @param maxRatio Maximum ratio of base to fyToken in the pool. 276 | /// @return The amount of tokens burned and returned (tokensBurned, bases, fyTokens). 277 | function burn(address baseTo, address fyTokenTo, uint256 minRatio, uint256 maxRatio) 278 | external override 279 | returns (uint256, uint256, uint256) 280 | { 281 | return _burnInternal(baseTo, fyTokenTo, false, minRatio, maxRatio); 282 | } 283 | 284 | /// @dev Burn liquidity tokens in exchange for base. 285 | /// The liquidity provider needs to have called `pool.approve`. 286 | /// @param to Wallet receiving the base and fyToken. 287 | /// @param minRatio Minimum ratio of base to fyToken in the pool. 288 | /// @param maxRatio Minimum ratio of base to fyToken in the pool. 289 | /// @return tokensBurned The amount of lp tokens burned. 290 | /// @return baseOut The amount of base tokens returned. 291 | function burnForBase(address to, uint256 minRatio, uint256 maxRatio) 292 | external override 293 | returns (uint256 tokensBurned, uint256 baseOut) 294 | { 295 | (tokensBurned, baseOut, ) = _burnInternal(to, address(0), true, minRatio, maxRatio); 296 | } 297 | 298 | 299 | /// @dev Burn liquidity tokens in exchange for base. 300 | /// The liquidity provider needs to have called `pool.approve`. 301 | /// @param baseTo Wallet receiving the base. 302 | /// @param fyTokenTo Wallet receiving the fyToken. 303 | /// @param tradeToBase Whether the resulting fyToken should be traded for base tokens. 304 | /// @param minRatio Minimum ratio of base to fyToken in the pool. 305 | /// @param maxRatio Minimum ratio of base to fyToken in the pool. 306 | /// @return tokensBurned The amount of pool tokens burned. 307 | /// @return tokenOut The amount of base tokens returned. 308 | /// @return fyTokenOut The amount of fyTokens returned. 309 | function _burnInternal(address baseTo, address fyTokenTo, bool tradeToBase, uint256 minRatio, uint256 maxRatio) 310 | internal 311 | returns (uint256 tokensBurned, uint256 tokenOut, uint256 fyTokenOut) 312 | { 313 | 314 | // Gather data 315 | tokensBurned = _balanceOf[address(this)]; 316 | uint256 supply = _totalSupply; 317 | (uint112 _baseCached, uint112 _fyTokenCached) = 318 | (baseCached, fyTokenCached); 319 | uint256 _realFYTokenCached = _fyTokenCached - supply; // The fyToken cache includes the virtual fyToken, equal to the supply 320 | 321 | // Check the burn wasn't sandwiched 322 | require ( 323 | _realFYTokenCached == 0 || ( 324 | uint256(_baseCached) * 1e18 / _realFYTokenCached >= minRatio && 325 | uint256(_baseCached) * 1e18 / _realFYTokenCached <= maxRatio 326 | ), 327 | "Pool: Reserves ratio changed" 328 | ); 329 | 330 | // Calculate trade 331 | tokenOut = (tokensBurned * _baseCached) / supply; 332 | fyTokenOut = (tokensBurned * _realFYTokenCached) / supply; 333 | 334 | if (tradeToBase) { 335 | tokenOut += YieldMath.baseOutForFYTokenIn( // This is a virtual sell 336 | (_baseCached - tokenOut.u128()) * scaleFactor, // Cache, minus virtual burn 337 | (_fyTokenCached - fyTokenOut.u128()) * scaleFactor, // Cache, minus virtual burn 338 | fyTokenOut.u128() * scaleFactor, // Sell the virtual fyToken obtained 339 | maturity - uint32(block.timestamp), // This can't be called after maturity 340 | ts, 341 | g2 342 | ) / scaleFactor; 343 | fyTokenOut = 0; 344 | } 345 | 346 | // Update TWAR 347 | _update( 348 | (_baseCached - tokenOut).u128(), 349 | (_fyTokenCached - fyTokenOut - tokensBurned).u128(), 350 | _baseCached, 351 | _fyTokenCached 352 | ); 353 | 354 | // Transfer assets 355 | _burn(address(this), tokensBurned); 356 | base.safeTransfer(baseTo, tokenOut); 357 | if (fyTokenOut > 0) IERC20(address(fyToken)).safeTransfer(fyTokenTo, fyTokenOut); 358 | 359 | emit Liquidity(maturity, msg.sender, baseTo, fyTokenTo, tokenOut.i256(), fyTokenOut.i256(), -(tokensBurned.i256())); 360 | } 361 | 362 | // ---- Trading ---- 363 | 364 | /// @dev Sell base for fyToken. 365 | /// The trader needs to have transferred the amount of base to sell to the pool before in the same transaction. 366 | /// @param to Wallet receiving the fyToken being bought 367 | /// @param min Minimm accepted amount of fyToken 368 | /// @return Amount of fyToken that will be deposited on `to` wallet 369 | function sellBase(address to, uint128 min) 370 | external override 371 | returns(uint128) 372 | { 373 | // Calculate trade 374 | (uint112 _baseCached, uint112 _fyTokenCached) = 375 | (baseCached, fyTokenCached); 376 | uint112 _baseBalance = _getBaseBalance(); 377 | uint112 _fyTokenBalance = _getFYTokenBalance(); 378 | uint128 baseIn = _baseBalance - _baseCached; 379 | uint128 fyTokenOut = _sellBasePreview( 380 | baseIn, 381 | _baseCached, 382 | _fyTokenBalance 383 | ); 384 | 385 | // Slippage check 386 | require( 387 | fyTokenOut >= min, 388 | "Pool: Not enough fyToken obtained" 389 | ); 390 | 391 | // Update TWAR 392 | _update( 393 | _baseBalance, 394 | _fyTokenBalance - fyTokenOut, 395 | _baseCached, 396 | _fyTokenCached 397 | ); 398 | 399 | // Transfer assets 400 | IERC20(address(fyToken)).safeTransfer(to, fyTokenOut); 401 | 402 | emit Trade(maturity, msg.sender, to, -(baseIn.i128()), fyTokenOut.i128()); 403 | return fyTokenOut; 404 | } 405 | 406 | /// @dev Returns how much fyToken would be obtained by selling `baseIn` base 407 | /// @param baseIn Amount of base hypothetically sold. 408 | /// @return Amount of fyToken hypothetically bought. 409 | function sellBasePreview(uint128 baseIn) 410 | external view override 411 | returns(uint128) 412 | { 413 | (uint112 _baseCached, uint112 _fyTokenCached) = 414 | (baseCached, fyTokenCached); 415 | return _sellBasePreview(baseIn, _baseCached, _fyTokenCached); 416 | } 417 | 418 | /// @dev Returns how much fyToken would be obtained by selling `baseIn` base 419 | function _sellBasePreview( 420 | uint128 baseIn, 421 | uint112 baseBalance, 422 | uint112 fyTokenBalance 423 | ) 424 | private view 425 | beforeMaturity 426 | returns(uint128) 427 | { 428 | uint128 fyTokenOut = YieldMath.fyTokenOutForBaseIn( 429 | baseBalance * scaleFactor, 430 | fyTokenBalance * scaleFactor, 431 | baseIn * scaleFactor, 432 | maturity - uint32(block.timestamp), // This can't be called after maturity 433 | ts, 434 | g1 435 | ) / scaleFactor; 436 | 437 | require( 438 | fyTokenBalance - fyTokenOut >= baseBalance + baseIn, 439 | "Pool: fyToken balance too low" 440 | ); 441 | 442 | return fyTokenOut; 443 | } 444 | 445 | /// @dev Buy base for fyToken 446 | /// The trader needs to have called `fyToken.approve` 447 | /// @param to Wallet receiving the base being bought 448 | /// @param tokenOut Amount of base being bought that will be deposited in `to` wallet 449 | /// @param max Maximum amount of fyToken that will be paid for the trade 450 | /// @return Amount of fyToken that will be taken from caller 451 | function buyBase(address to, uint128 tokenOut, uint128 max) 452 | external override 453 | returns(uint128) 454 | { 455 | // Calculate trade 456 | uint128 fyTokenBalance = _getFYTokenBalance(); 457 | (uint112 _baseCached, uint112 _fyTokenCached) = 458 | (baseCached, fyTokenCached); 459 | uint128 fyTokenIn = _buyBasePreview( 460 | tokenOut, 461 | _baseCached, 462 | _fyTokenCached 463 | ); 464 | require( 465 | fyTokenBalance - _fyTokenCached >= fyTokenIn, 466 | "Pool: Not enough fyToken in" 467 | ); 468 | 469 | // Slippage check 470 | require( 471 | fyTokenIn <= max, 472 | "Pool: Too much fyToken in" 473 | ); 474 | 475 | // Update TWAR 476 | _update( 477 | _baseCached - tokenOut, 478 | _fyTokenCached + fyTokenIn, 479 | _baseCached, 480 | _fyTokenCached 481 | ); 482 | 483 | // Transfer assets 484 | base.safeTransfer(to, tokenOut); 485 | 486 | emit Trade(maturity, msg.sender, to, tokenOut.i128(), -(fyTokenIn.i128())); 487 | return fyTokenIn; 488 | } 489 | 490 | /// @dev Returns how much fyToken would be required to buy `tokenOut` base. 491 | /// @param tokenOut Amount of base hypothetically desired. 492 | /// @return Amount of fyToken hypothetically required. 493 | function buyBasePreview(uint128 tokenOut) 494 | external view override 495 | returns(uint128) 496 | { 497 | (uint112 _baseCached, uint112 _fyTokenCached) = 498 | (baseCached, fyTokenCached); 499 | return _buyBasePreview(tokenOut, _baseCached, _fyTokenCached); 500 | } 501 | 502 | /// @dev Returns how much fyToken would be required to buy `tokenOut` base. 503 | function _buyBasePreview( 504 | uint128 tokenOut, 505 | uint112 baseBalance, 506 | uint112 fyTokenBalance 507 | ) 508 | private view 509 | beforeMaturity 510 | returns(uint128) 511 | { 512 | return YieldMath.fyTokenInForBaseOut( 513 | baseBalance * scaleFactor, 514 | fyTokenBalance * scaleFactor, 515 | tokenOut * scaleFactor, 516 | maturity - uint32(block.timestamp), // This can't be called after maturity 517 | ts, 518 | g2 519 | ) / scaleFactor; 520 | } 521 | 522 | /// @dev Sell fyToken for base 523 | /// The trader needs to have transferred the amount of fyToken to sell to the pool before in the same transaction. 524 | /// @param to Wallet receiving the base being bought 525 | /// @param min Minimm accepted amount of base 526 | /// @return Amount of base that will be deposited on `to` wallet 527 | function sellFYToken(address to, uint128 min) 528 | external override 529 | returns(uint128) 530 | { 531 | // Calculate trade 532 | (uint112 _baseCached, uint112 _fyTokenCached) = 533 | (baseCached, fyTokenCached); 534 | uint112 _fyTokenBalance = _getFYTokenBalance(); 535 | uint112 _baseBalance = _getBaseBalance(); 536 | uint128 fyTokenIn = _fyTokenBalance - _fyTokenCached; 537 | uint128 baseOut = _sellFYTokenPreview( 538 | fyTokenIn, 539 | _baseCached, 540 | _fyTokenCached 541 | ); 542 | 543 | // Slippage check 544 | require( 545 | baseOut >= min, 546 | "Pool: Not enough base obtained" 547 | ); 548 | 549 | // Update TWAR 550 | _update( 551 | _baseBalance - baseOut, 552 | _fyTokenBalance, 553 | _baseCached, 554 | _fyTokenCached 555 | ); 556 | 557 | // Transfer assets 558 | base.safeTransfer(to, baseOut); 559 | 560 | emit Trade(maturity, msg.sender, to, baseOut.i128(), -(fyTokenIn.i128())); 561 | return baseOut; 562 | } 563 | 564 | /// @dev Returns how much base would be obtained by selling `fyTokenIn` fyToken. 565 | /// @param fyTokenIn Amount of fyToken hypothetically sold. 566 | /// @return Amount of base hypothetically bought. 567 | function sellFYTokenPreview(uint128 fyTokenIn) 568 | external view override 569 | returns(uint128) 570 | { 571 | (uint112 _baseCached, uint112 _fyTokenCached) = 572 | (baseCached, fyTokenCached); 573 | return _sellFYTokenPreview(fyTokenIn, _baseCached, _fyTokenCached); 574 | } 575 | 576 | /// @dev Returns how much base would be obtained by selling `fyTokenIn` fyToken. 577 | function _sellFYTokenPreview( 578 | uint128 fyTokenIn, 579 | uint112 baseBalance, 580 | uint112 fyTokenBalance 581 | ) 582 | private view 583 | beforeMaturity 584 | returns(uint128) 585 | { 586 | return YieldMath.baseOutForFYTokenIn( 587 | baseBalance * scaleFactor, 588 | fyTokenBalance * scaleFactor, 589 | fyTokenIn * scaleFactor, 590 | maturity - uint32(block.timestamp), // This can't be called after maturity 591 | ts, 592 | g2 593 | ) / scaleFactor; 594 | } 595 | 596 | /// @dev Buy fyToken for base 597 | /// The trader needs to have called `base.approve` 598 | /// @param to Wallet receiving the fyToken being bought 599 | /// @param fyTokenOut Amount of fyToken being bought that will be deposited in `to` wallet 600 | /// @param max Maximum amount of base token that will be paid for the trade 601 | /// @return Amount of base that will be taken from caller's wallet 602 | function buyFYToken(address to, uint128 fyTokenOut, uint128 max) 603 | external override 604 | returns(uint128) 605 | { 606 | // Calculate trade 607 | uint128 baseBalance = _getBaseBalance(); 608 | (uint112 _baseCached, uint112 _fyTokenCached) = 609 | (baseCached, fyTokenCached); 610 | uint128 baseIn = _buyFYTokenPreview( 611 | fyTokenOut, 612 | _baseCached, 613 | _fyTokenCached 614 | ); 615 | require( 616 | baseBalance - _baseCached >= baseIn, 617 | "Pool: Not enough base token in" 618 | ); 619 | 620 | // Slippage check 621 | require( 622 | baseIn <= max, 623 | "Pool: Too much base token in" 624 | ); 625 | 626 | // Update TWAR 627 | _update( 628 | _baseCached + baseIn, 629 | _fyTokenCached - fyTokenOut, 630 | _baseCached, 631 | _fyTokenCached 632 | ); 633 | 634 | // Transfer assets 635 | IERC20(address(fyToken)).safeTransfer(to, fyTokenOut); 636 | 637 | emit Trade(maturity, msg.sender, to, -(baseIn.i128()), fyTokenOut.i128()); 638 | return baseIn; 639 | } 640 | 641 | /// @dev Returns how much base would be required to buy `fyTokenOut` fyToken. 642 | /// @param fyTokenOut Amount of fyToken hypothetically desired. 643 | /// @return Amount of base hypothetically required. 644 | function buyFYTokenPreview(uint128 fyTokenOut) 645 | external view override 646 | returns(uint128) 647 | { 648 | (uint112 _baseCached, uint112 _fyTokenCached) = 649 | (baseCached, fyTokenCached); 650 | return _buyFYTokenPreview(fyTokenOut, _baseCached, _fyTokenCached); 651 | } 652 | 653 | /// @dev Returns how much base would be required to buy `fyTokenOut` fyToken. 654 | function _buyFYTokenPreview( 655 | uint128 fyTokenOut, 656 | uint128 baseBalance, 657 | uint128 fyTokenBalance 658 | ) 659 | private view 660 | beforeMaturity 661 | returns(uint128) 662 | { 663 | uint128 baseIn = YieldMath.baseInForFYTokenOut( 664 | baseBalance * scaleFactor, 665 | fyTokenBalance * scaleFactor, 666 | fyTokenOut * scaleFactor, 667 | maturity - uint32(block.timestamp), // This can't be called after maturity 668 | ts, 669 | g1 670 | ) / scaleFactor; 671 | 672 | require( 673 | fyTokenBalance - fyTokenOut >= baseBalance + baseIn, 674 | "Pool: fyToken balance too low" 675 | ); 676 | 677 | return baseIn; 678 | } 679 | } 680 | -------------------------------------------------------------------------------- /contracts/extensions/PoolView.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.14; 3 | 4 | import "./YieldMathExtensions.sol"; 5 | 6 | 7 | contract PoolView { 8 | using YieldMathExtensions for IPool; 9 | 10 | /// @dev Calculate the invariant for this pool 11 | function invariant(IPool pool) external view returns (uint128) { 12 | return pool.invariant(); 13 | } 14 | 15 | function maxFYTokenOut(IPool pool) external view returns (uint128) { 16 | return pool.maxFYTokenOut(); 17 | } 18 | 19 | function maxFYTokenIn(IPool pool) external view returns (uint128) { 20 | return pool.maxFYTokenIn(); 21 | } 22 | 23 | function maxBaseIn(IPool pool) external view returns (uint128) { 24 | return pool.maxBaseIn(); 25 | } 26 | 27 | function maxBaseOut(IPool pool) external view returns (uint128) { 28 | return pool.maxBaseOut(); 29 | } 30 | 31 | function retrievableBase(IPool pool) external view returns (uint256) { 32 | (uint112 baseCached,,) = pool.getCache(); 33 | return pool.base().balanceOf(address(pool)) - baseCached; 34 | } 35 | 36 | function retrievableFYToken(IPool pool) external view returns (uint256) { 37 | (, uint112 fyTokenCached,) = pool.getCache(); 38 | return pool.fyToken().balanceOf(address(pool)) + pool.totalSupply() - fyTokenCached; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /contracts/extensions/YieldMathExtensions.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.14; 3 | 4 | import "@yield-protocol/yieldspace-interfaces/IPool.sol"; 5 | import "../YieldMath.sol"; 6 | 7 | 8 | library YieldMathExtensions { 9 | 10 | /// @dev Calculate the invariant for this pool 11 | function invariant(IPool pool) external view returns (uint128) { 12 | uint32 maturity = pool.maturity(); 13 | uint32 timeToMaturity = (maturity > uint32(block.timestamp)) ? maturity - uint32(block.timestamp) : 0; 14 | return YieldMath.invariant( 15 | pool.getBaseBalance(), 16 | pool.getFYTokenBalance(), 17 | pool.totalSupply(), 18 | timeToMaturity, 19 | pool.ts() 20 | ); 21 | } 22 | 23 | /// @dev max amount of fyTokens that can be bought from the pool 24 | function maxFYTokenOut(IPool pool) external view returns (uint128) { 25 | (uint112 _baseCached, uint112 _fyTokenCached,) = pool.getCache(); 26 | uint96 scaleFactor = pool.scaleFactor(); 27 | return YieldMath.maxFYTokenOut( 28 | _baseCached * scaleFactor, 29 | _fyTokenCached * scaleFactor, 30 | pool.maturity() - uint32(block.timestamp), 31 | pool.ts(), 32 | pool.g1() 33 | ) / scaleFactor; 34 | } 35 | 36 | /// @dev max amount of fyTokens that can be sold into the pool 37 | function maxFYTokenIn(IPool pool) external view returns (uint128) { 38 | (uint112 _baseCached, uint112 _fyTokenCached,) = pool.getCache(); 39 | uint96 scaleFactor = pool.scaleFactor(); 40 | return YieldMath.maxFYTokenIn( 41 | _baseCached * scaleFactor, 42 | _fyTokenCached * scaleFactor, 43 | pool.maturity() - uint32(block.timestamp), 44 | pool.ts(), 45 | pool.g2() 46 | ) / scaleFactor; 47 | } 48 | 49 | /// @dev max amount of Base that can be sold to the pool 50 | function maxBaseIn(IPool pool) external view returns (uint128) { 51 | (uint112 _baseCached, uint112 _fyTokenCached,) = pool.getCache(); 52 | uint96 scaleFactor = pool.scaleFactor(); 53 | return YieldMath.maxBaseIn( 54 | _baseCached * scaleFactor, 55 | _fyTokenCached * scaleFactor, 56 | pool.maturity() - uint32(block.timestamp), 57 | pool.ts(), 58 | pool.g1() 59 | ) / scaleFactor; 60 | } 61 | 62 | /// @dev max amount of Base that can be bought from the pool 63 | function maxBaseOut(IPool pool) external view returns (uint128) { 64 | (uint112 _baseCached, uint112 _fyTokenCached,) = pool.getCache(); 65 | uint96 scaleFactor = pool.scaleFactor(); 66 | return YieldMath.maxBaseOut( 67 | _baseCached * scaleFactor, 68 | _fyTokenCached * scaleFactor, 69 | pool.maturity() - uint32(block.timestamp), 70 | pool.ts(), 71 | pool.g2() 72 | ) / scaleFactor; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /contracts/mocks/BaseMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.14; 3 | 4 | import "@yield-protocol/utils-v2/contracts/token/ERC20Permit.sol"; 5 | 6 | 7 | contract BaseMock is ERC20Permit("Base", "BASE", 18) { 8 | function mint(address to, uint256 amount) public { 9 | _mint(to, amount); 10 | } 11 | } -------------------------------------------------------------------------------- /contracts/mocks/DaiMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.14; 3 | import "@yield-protocol/utils-v2/contracts/token/ERC20.sol"; 4 | 5 | 6 | contract DaiMock is ERC20 { 7 | 8 | mapping (address => uint) public nonces; 9 | 10 | // --- EIP712 niceties --- 11 | bytes32 public DOMAIN_SEPARATOR; 12 | bytes32 public constant PERMIT_TYPEHASH = keccak256("Permit(address holder,address spender,uint256 nonce,uint256 expiry,bool allowed)"); 13 | string public constant version = "1"; 14 | 15 | constructor( 16 | string memory name, 17 | string memory symbol 18 | ) ERC20(name, symbol, 18) { 19 | uint256 chainId; 20 | assembly { 21 | chainId := chainid() 22 | } 23 | 24 | DOMAIN_SEPARATOR = keccak256(abi.encode( 25 | keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), 26 | keccak256(bytes(name)), 27 | keccak256(bytes(version)), 28 | chainId, 29 | address(this) 30 | )); 31 | } 32 | 33 | /// @dev Give tokens to whoever asks for them. 34 | function mint(address to, uint256 amount) public virtual { 35 | _mint(to, amount); 36 | } 37 | 38 | // --- Approve by signature --- 39 | function permit(address holder, address spender, uint256 nonce, uint256 expiry, 40 | bool allowed, uint8 v, bytes32 r, bytes32 s) external 41 | { 42 | bytes32 digest = 43 | keccak256(abi.encodePacked( 44 | "\x19\x01", 45 | DOMAIN_SEPARATOR, 46 | keccak256(abi.encode(PERMIT_TYPEHASH, 47 | holder, 48 | spender, 49 | nonce, 50 | expiry, 51 | allowed)) 52 | )); 53 | 54 | require(holder != address(0), "Dai/invalid-address-0"); 55 | require(holder == ecrecover(digest, v, r, s), "Dai/invalid-permit"); 56 | require(expiry == 0 || block.timestamp <= expiry, "Dai/permit-expired"); 57 | require(nonce == nonces[holder]++, "Dai/invalid-nonce"); 58 | uint wad = allowed ? type(uint256).max : 0; 59 | _allowance[holder][spender] = wad; 60 | emit Approval(holder, spender, wad); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /contracts/mocks/FYTokenMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.14; 3 | 4 | import "./BaseMock.sol"; 5 | import "@yield-protocol/utils-v2/contracts/token/ERC20Permit.sol"; 6 | 7 | contract FYTokenMock is ERC20Permit { 8 | BaseMock public base; 9 | uint32 public maturity; 10 | 11 | constructor (BaseMock base_, uint32 maturity_) 12 | ERC20Permit( 13 | "Test", 14 | "TST", 15 | IERC20Metadata(address(base_)).decimals() 16 | ) { 17 | base = base_; 18 | maturity = maturity_; 19 | } 20 | 21 | function mint(address to, uint256 amount) public { 22 | _mint(to, amount); 23 | } 24 | 25 | function burn(address from, uint256 amount) public { 26 | _burn(from, amount); 27 | } 28 | 29 | function redeem(address from, address to, uint256 amount) public { 30 | _burn(from, amount); 31 | base.mint(to, amount); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /contracts/mocks/USDCMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.14; 3 | import "@yield-protocol/utils-v2/contracts/token/ERC20Permit.sol"; 4 | 5 | 6 | contract USDCMock is ERC20Permit { 7 | 8 | constructor( 9 | string memory name, 10 | string memory symbol 11 | ) ERC20Permit(name, symbol, 6) { } 12 | 13 | function version() public pure override returns(string memory) { return "2"; } 14 | 15 | /// @dev Give tokens to whoever asks for them. 16 | function mint(address to, uint256 amount) public virtual { 17 | _mint(to, amount); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /contracts/mocks/WETH9Mock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.14; 3 | 4 | import "@yield-protocol/utils-v2/contracts/token/ERC20.sol"; 5 | 6 | 7 | contract WETH9Mock is ERC20 { 8 | event Deposit(address indexed dst, uint wad); 9 | event Withdrawal(address indexed src, uint wad); 10 | 11 | constructor () ERC20("Wrapped Ether", "WETH", 18) { } 12 | 13 | receive() external payable { 14 | deposit(); 15 | } 16 | 17 | function deposit() public payable { 18 | _balanceOf[msg.sender] += msg.value; 19 | emit Deposit(msg.sender, msg.value); 20 | } 21 | 22 | function withdraw(uint wad) public { 23 | require(_balanceOf[msg.sender] >= wad, "WETH9: Insufficient balance"); 24 | _balanceOf[msg.sender] -= wad; 25 | payable(msg.sender).transfer(wad); 26 | emit Withdrawal(msg.sender, wad); 27 | } 28 | 29 | function totalSupply() public view override returns (uint) { 30 | return address(this).balance; 31 | } 32 | } -------------------------------------------------------------------------------- /contracts/mocks/YieldMathWrapper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.14; 3 | 4 | import "../YieldMath.sol"; 5 | 6 | /** 7 | * Wrapper for the Yield Math Smart Contract Library. 8 | */ 9 | contract YieldMathWrapper { 10 | /** 11 | * Calculate the amount of fyToken a user would get for given amount of Base. 12 | * 13 | * @param baseReserves Base reserves amount 14 | * @param fyTokenReserves fyToken reserves amount 15 | * @param baseAmount Base amount to be traded 16 | * @param timeTillMaturity time till maturity in seconds 17 | * @param ts time till maturity coefficient, multiplied by 2^64 18 | * @param g fee coefficient, multiplied by 2^64 19 | * @return the amount of fyToken a user would get for given amount of Base 20 | */ 21 | function fyTokenOutForBaseIn( 22 | uint128 baseReserves, uint128 fyTokenReserves, uint128 baseAmount, 23 | uint128 timeTillMaturity, int128 ts, int128 g) 24 | public pure returns(uint128) { 25 | return YieldMath.fyTokenOutForBaseIn( 26 | baseReserves, fyTokenReserves, baseAmount, timeTillMaturity, ts, g 27 | ); 28 | } 29 | 30 | /** 31 | * Calculate the amount of Base a user would get for certain amount of fyToken. 32 | * 33 | * @param baseReserves Base reserves amount 34 | * @param fyTokenReserves fyToken reserves amount 35 | * @param fyTokenAmount fyToken amount to be traded 36 | * @param timeTillMaturity time till maturity in seconds 37 | * @param ts time till maturity coefficient, multiplied by 2^64 38 | * @param g fee coefficient, multiplied by 2^64 39 | * @return the amount of Base a user would get for given amount of fyToken 40 | */ 41 | function baseOutForFYTokenIn( 42 | uint128 baseReserves, uint128 fyTokenReserves, uint128 fyTokenAmount, 43 | uint128 timeTillMaturity, int128 ts, int128 g) 44 | public pure returns(uint128) { 45 | return YieldMath.baseOutForFYTokenIn( 46 | baseReserves, fyTokenReserves, fyTokenAmount, timeTillMaturity, ts, g 47 | ); 48 | } 49 | 50 | /** 51 | * Calculate the amount of fyToken a user could sell for given amount of Base. 52 | * 53 | * @param baseReserves Base reserves amount 54 | * @param fyTokenReserves fyToken reserves amount 55 | * @param baseAmount Base amount to be traded 56 | * @param timeTillMaturity time till maturity in seconds 57 | * @param ts time till maturity coefficient, multiplied by 2^64 58 | * @param g fee coefficient, multiplied by 2^64 59 | * @return the amount of fyToken a user could sell for given amount of Base 60 | */ 61 | function fyTokenInForBaseOut( 62 | uint128 baseReserves, uint128 fyTokenReserves, uint128 baseAmount, 63 | uint128 timeTillMaturity, int128 ts, int128 g) 64 | public pure returns(uint128) { 65 | return YieldMath.fyTokenInForBaseOut( 66 | baseReserves, fyTokenReserves, baseAmount, timeTillMaturity, ts, g 67 | ); 68 | } 69 | 70 | /** 71 | * Calculate the amount of Base a user would have to pay for certain amount of 72 | * fyToken. 73 | * 74 | * @param baseReserves Base reserves amount 75 | * @param fyTokenReserves fyToken reserves amount 76 | * @param fyTokenAmount fyToken amount to be traded 77 | * @param timeTillMaturity time till maturity in seconds 78 | * @param ts time till maturity coefficient, multiplied by 2^64 79 | * @param g fee coefficient, multiplied by 2^64 80 | * @return the amount of Base a user would have to pay for given amount of 81 | * fyToken 82 | */ 83 | function baseInForFYTokenOut( 84 | uint128 baseReserves, uint128 fyTokenReserves, uint128 fyTokenAmount, 85 | uint128 timeTillMaturity, int128 ts, int128 g) 86 | public pure returns(uint128) { 87 | return YieldMath.baseInForFYTokenOut( 88 | baseReserves, fyTokenReserves, fyTokenAmount, timeTillMaturity, ts, g 89 | ); 90 | } 91 | 92 | /** 93 | * Raise given number x into power specified as a simple fraction y/z and then 94 | * multiply the result by the normalization factor 2^(128 *(1 - y/z)). 95 | * Revert if z is zero, or if both x and y are zeros. 96 | * 97 | * @param x number to raise into given power y/z 98 | * @param y numerator of the power to raise x into 99 | * @param z denominator of the power to raise x into 100 | * @return x raised into power y/z and then multiplied by 2^(128 *(1 - y/z)) 101 | */ 102 | function pow(uint128 x, uint128 y, uint128 z) 103 | public pure returns(bool, uint256) { 104 | return( 105 | true, 106 | Exp64x64.pow(x, y, z)); 107 | } 108 | 109 | /** 110 | * Calculate base 2 logarithm of an unsigned 128-bit integer number. Revert 111 | * in case x is zero. 112 | * 113 | * @param x number to calculate 2-base logarithm of 114 | * @return 2-base logarithm of x, multiplied by 2^121 115 | */ 116 | function log_2(uint128 x) 117 | public pure returns(bool, uint128) { 118 | return( 119 | true, 120 | Exp64x64.log_2(x)); 121 | } 122 | 123 | /** 124 | * Calculate 2 raised into given power. 125 | * 126 | * @param x power to raise 2 into, multiplied by 2^121 127 | * @return 2 raised into given power 128 | */ 129 | function pow_2(uint128 x) 130 | public pure returns(bool, uint128) { 131 | return( 132 | true, 133 | Exp64x64.pow_2(x)); 134 | } 135 | } -------------------------------------------------------------------------------- /deploy/PoolFactory.js: -------------------------------------------------------------------------------- 1 | const func = async function ({ deployments, getNamedAccounts, getChainId }) { 2 | const { deploy, execute, get, read } = deployments; 3 | const { deployer } = await getNamedAccounts(); 4 | const chainId = await getChainId() 5 | 6 | safeERC20NamerAddress = (await get('SafeERC20Namer')).address; 7 | yieldMathAddress = (await get('YieldMath')).address; 8 | 9 | const poolFactory = await deploy('PoolFactory', { 10 | from: deployer, 11 | deterministicDeployment: true, 12 | libraries: { 13 | YieldMath: yieldMathAddress, 14 | SafeERC20Namer: safeERC20NamerAddress 15 | } 16 | }) 17 | console.log(`Deployed PoolFactory to ${poolFactory.address}`); 18 | }; 19 | 20 | module.exports = func; 21 | module.exports.tags = ["PoolFactory"]; 22 | module.exports.dependencies = ["YieldMath", "SafeERC20Namer"] -------------------------------------------------------------------------------- /deploy/SafeERC20Namer.js: -------------------------------------------------------------------------------- 1 | const func = async function ({ deployments, getNamedAccounts, getChainId }) { 2 | const { deploy, execute, get, read } = deployments; 3 | const { deployer } = await getNamedAccounts(); 4 | const chainId = await getChainId() 5 | 6 | const safeERC20Namer = await deploy('SafeERC20Namer', { 7 | from: deployer, 8 | deterministicDeployment: true, 9 | }) 10 | console.log(`Deployed SafeERC20Namer to ${safeERC20Namer.address}`); 11 | }; 12 | 13 | module.exports = func; 14 | module.exports.tags = ["SafeERC20Namer"]; 15 | -------------------------------------------------------------------------------- /deploy/YieldMath.js: -------------------------------------------------------------------------------- 1 | const func = async function ({ deployments, getNamedAccounts, getChainId }) { 2 | const { deploy, execute, get, read } = deployments; 3 | const { deployer } = await getNamedAccounts(); 4 | const chainId = await getChainId() 5 | 6 | const yieldMath = await deploy('YieldMath', { 7 | from: deployer, 8 | deterministicDeployment: true, 9 | }) 10 | console.log(`Deployed YieldMath to ${yieldMath.address}`); 11 | }; 12 | 13 | module.exports = func; 14 | module.exports.tags = ["YieldMath"]; 15 | -------------------------------------------------------------------------------- /deployments/kovan/.chainId: -------------------------------------------------------------------------------- 1 | 42 -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import * as path from 'path' 3 | 4 | import '@nomiclabs/hardhat-waffle' 5 | import '@nomiclabs/hardhat-etherscan' 6 | import 'hardhat-abi-exporter' 7 | import 'hardhat-contract-sizer' 8 | import 'hardhat-gas-reporter' 9 | import 'hardhat-typechain' 10 | import 'solidity-coverage' 11 | import 'hardhat-deploy' 12 | import { task } from 'hardhat/config' 13 | import { TASK_TEST } from 'hardhat/builtin-tasks/task-names' 14 | import { TaskArguments, HardhatRuntimeEnvironment, RunSuperFunction } from 'hardhat/types' 15 | 16 | // REQUIRED TO ENSURE METADATA IS SAVED IN DEPLOYMENTS (because solidity-coverage disable it otherwise) 17 | /* import { 18 | TASK_COMPILE_GET_COMPILER_INPUT 19 | } from "hardhat/builtin-tasks/task-names" 20 | task(TASK_COMPILE_GET_COMPILER_INPUT).setAction(async (_, bre, runSuper) => { 21 | const input = await runSuper() 22 | input.settings.metadata.useLiteralContent = bre.network.name !== "coverage" 23 | return input 24 | }) */ 25 | 26 | task("lint:collisions", "Checks all contracts for function signatures collisions with ROOT (0x00000000) and LOCK (0xffffffff)", 27 | async (taskArguments, hre) => { 28 | let ROOT = "0x00000000" 29 | let LOCK = "0xffffffff" 30 | const abiPath = path.join(__dirname, 'abi') 31 | for (let contract of fs.readdirSync(abiPath)) { 32 | const iface = new hre.ethers.utils.Interface(require(abiPath + "/" + contract)) 33 | for (let func in iface.functions) { 34 | const sig = iface.getSighash(func) 35 | if (sig == ROOT) { 36 | console.error("Function " + func + " of contract " + contract.slice(0, contract.length - 5) + " has a role-colliding signature with ROOT.") 37 | } 38 | if (sig == LOCK) { 39 | console.error("Function " + func + " of contract " + contract.slice(0, contract.length - 5) + " has a role-colliding signature with LOCK.") 40 | } 41 | } 42 | } 43 | console.log("No collisions, check passed.") 44 | } 45 | ) 46 | 47 | // This hook enables debugging during test runs by appending the `--debug` flag to the test command (`yarn test --debug`) and using `debugLog()` (instead of `console.log()`). 48 | task(TASK_TEST).addFlag("debug", "Enable debugging logs").setAction( 49 | async (args: TaskArguments, hre: HardhatRuntimeEnvironment, runSuper: RunSuperFunction) => { 50 | process.env.DEBUG = args?.["debug"] 51 | delete args?.["debug"] 52 | await runSuper(args); 53 | } 54 | ) 55 | 56 | 57 | function nodeUrl(network: any) { 58 | let infuraKey 59 | try { 60 | infuraKey = fs.readFileSync(path.resolve(__dirname, '.infuraKey')).toString().trim() 61 | } catch(e) { 62 | infuraKey = '' 63 | } 64 | return `https://${network}.infura.io/v3/${infuraKey}` 65 | } 66 | 67 | let mnemonic = process.env.MNEMONIC 68 | if (!mnemonic) { 69 | try { 70 | mnemonic = fs.readFileSync(path.resolve(__dirname, '.secret')).toString().trim() 71 | } catch(e){} 72 | } 73 | const accounts = mnemonic ? { 74 | mnemonic, 75 | }: undefined 76 | 77 | let etherscanKey = process.env.ETHERSCANKEY 78 | if (!etherscanKey) { 79 | try { 80 | etherscanKey = fs.readFileSync(path.resolve(__dirname, '.etherscanKey')).toString().trim() 81 | } catch(e){} 82 | } 83 | 84 | module.exports = { 85 | solidity: { 86 | version: '0.8.14', 87 | settings: { 88 | optimizer: { 89 | enabled: true, 90 | runs: 1000, 91 | } 92 | } 93 | }, 94 | typechain: { 95 | outDir: 'typechain', 96 | target: 'ethers-v5', 97 | }, 98 | abiExporter: { 99 | path: './abi', 100 | clear: true, 101 | flat: true, 102 | spacing: 2 103 | }, 104 | contractSizer: { 105 | alphaSort: true, 106 | runOnCompile: false, 107 | disambiguatePaths: false, 108 | }, 109 | gasReporter: { 110 | enabled: true, 111 | }, 112 | defaultNetwork: 'hardhat', 113 | namedAccounts: { 114 | deployer: 0, 115 | owner: 1, 116 | other: 2, 117 | }, 118 | networks: { 119 | kovan: { 120 | accounts, 121 | url: nodeUrl('kovan') 122 | }, 123 | goerli: { 124 | accounts, 125 | url: nodeUrl('goerli'), 126 | }, 127 | rinkeby: { 128 | accounts, 129 | url: nodeUrl('rinkeby') 130 | }, 131 | ropsten: { 132 | accounts, 133 | url: nodeUrl('ropsten') 134 | }, 135 | mainnet: { 136 | accounts, 137 | url: nodeUrl('mainnet') 138 | }, 139 | coverage: { 140 | url: 'http://127.0.0.1:8555', 141 | }, 142 | }, 143 | etherscan: { 144 | apiKey: etherscanKey 145 | }, 146 | } 147 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yield-protocol/yieldspace-v2", 3 | "version": "0.12.2", 4 | "description": "YieldSpace v2 contracts", 5 | "author": "Yield Inc.", 6 | "files": [ 7 | "contracts/*.sol", 8 | "contracts/extensions/*.sol", 9 | "dist" 10 | ], 11 | "main": "dist/index.js", 12 | "typings": "dist/index.d.ts", 13 | "scripts": { 14 | "build": "rm -rf artifacts typechain && hardhat compile", 15 | "test": "hardhat test", 16 | "test:parallel": "mocha --require hardhat/register --recursive --parallel --exit --extension ts", 17 | "test:deploy": "hardhat deploy --tags DeployTest", 18 | "coverage": "hardhat coverage", 19 | "lint:collisions": "yarn build && hardhat lint:collisions", 20 | "lint:sol": "yarn lint:collisions && solhint -f table contracts/*.sol", 21 | "lint:ts": "prettier ./test/*.ts --check", 22 | "lint:ts:fix": "prettier ./test/*.ts --write", 23 | "prepublishOnly": "npx tsdx build --tsconfig ./tsconfig-publish.json" 24 | }, 25 | "devDependencies": { 26 | "@ethersproject/providers": "^5.0.15", 27 | "@nomiclabs/hardhat-ethers": "^2.0.1", 28 | "@nomiclabs/hardhat-etherscan": "^2.1.0", 29 | "@nomiclabs/hardhat-waffle": "^2.0.1", 30 | "@truffle/hdwallet-provider": "^1.0.40", 31 | "@types/mocha": "^8.0.0", 32 | "@yield-protocol/utils-v2": "^2.4.1", 33 | "@yield-protocol/vault-interfaces": "^2.4.1", 34 | "@yield-protocol/yieldspace-interfaces": "^2.3.2", 35 | "chai": "4.2.0", 36 | "chai-bignumber": "3.0.0", 37 | "dss-interfaces": "0.1.1", 38 | "erc3156": "^0.4.8", 39 | "ethereumjs-util": "^7.0.9", 40 | "ethereum-waffle": "^3.2.2", 41 | "ethers": "^5.0.31", 42 | "hardhat": "^2.2.0", 43 | "hardhat-abi-exporter": "^2.0.3", 44 | "hardhat-contract-sizer": "^2.0.3", 45 | "hardhat-deploy": "^0.7.0-beta.44", 46 | "hardhat-gas-reporter": "^1.0.3", 47 | "hardhat-typechain": "^0.3.4", 48 | "mathjs": "^7.3.0", 49 | "mocha": "^8.3.2", 50 | "prettier": "^2.0.5", 51 | "solhint": "^3.3.3", 52 | "solidity-coverage": "^0.7.14", 53 | "ts-node": "^8.10.2", 54 | "tsdx": "^0.14.1", 55 | "typechain": "^4.0.3", 56 | "typechain-target-ethers-v5": "^5.0.1", 57 | "typescript": "^3.9.7" 58 | }, 59 | "repository": { 60 | "url": "git+https://github.com/yieldprotocol/yieldspace-v2.git", 61 | "type": "git" 62 | }, 63 | "bugs": { 64 | "url": "https://github.com/yieldprotocol/yieldspace-v2/issues" 65 | }, 66 | "license": "GPL-3.0-or-later", 67 | "homepage": "https://github.com/yieldprotocol/yieldspace-v2#readme" 68 | } -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from 'ethers' 2 | 3 | export const ONE64 = BigNumber.from('18446744073709551616') // In 64.64 format 4 | export const secondsInOneYear = BigNumber.from(31557600) 5 | export const secondsInTenYears = secondsInOneYear.mul(10) // Seconds in 10 years 6 | export const ts = ONE64.div(secondsInTenYears) 7 | 8 | export const g0 = ONE64 // No fees 9 | export const g1 = ONE64.mul(950).div(1000) // Sell base to the pool 10 | export const g2 = ONE64.mul(1000).div(950) // Sell fyToken to the pool -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * as constants from "./constants" -------------------------------------------------------------------------------- /src/yieldspace.ts: -------------------------------------------------------------------------------- 1 | import { ethers, BigNumber } from 'ethers'; 2 | import { secondsInTenYears } from './constants' 3 | import { Decimal } from 'decimal.js'; 4 | 5 | Decimal.set({ precision: 64 }); 6 | 7 | /* constants exposed for export */ 8 | export const ZERO_DEC: Decimal = new Decimal(0); 9 | export const ONE_DEC: Decimal = new Decimal(1); 10 | export const TWO_DEC: Decimal = new Decimal(2); 11 | export const SECONDS_PER_YEAR: number = (365 * 24 * 60 * 60); 12 | 13 | /* locally used constants */ 14 | const ZERO = ZERO_DEC; 15 | const ONE = ONE_DEC; 16 | const TWO = TWO_DEC; 17 | const ts = new Decimal(1 / secondsInTenYears.toNumber()); // inv of seconds in 4 years 18 | const g1 = new Decimal(950 / 1000); 19 | const g2 = new Decimal(1000 / 950); 20 | const precisionFee = new Decimal(1000000000000); 21 | 22 | /** ************************* 23 | Support functions 24 | *************************** */ 25 | 26 | /** 27 | * @param { BigNumber | string } multiplicant 28 | * @param { BigNumber | string } multiplier 29 | * @param { string } precisionDifference // Difference between multiplicant and multiplier precision (eg. wei vs ray '1e-27' ) 30 | * @returns { string } in decimal precision of the multiplicant 31 | */ 32 | export const mulDecimal = ( 33 | multiplicant: BigNumber | string, 34 | multiplier: BigNumber | string, 35 | precisionDifference: string = '1', // DEFAULT = 1 (same precision) 36 | ): string => { 37 | const multiplicant_ = new Decimal(multiplicant.toString()); 38 | const multiplier_ = new Decimal(multiplier.toString()); 39 | const _preDif = new Decimal(precisionDifference.toString()); 40 | const _normalisedMul = multiplier_.mul(_preDif); 41 | return multiplicant_.mul(_normalisedMul).toFixed(); 42 | }; 43 | 44 | /** 45 | * @param { BigNumber | string } numerator 46 | * @param { BigNumber | string } divisor 47 | * @param { BigNumber | string } precisionDifference // Difference between multiplicant and mulitplier precision (eg. wei vs ray '1e-27' ) 48 | * @returns { string } in decimal precision of the numerator 49 | */ 50 | export const divDecimal = ( 51 | numerator: BigNumber | string, 52 | divisor: BigNumber | string, 53 | precisionDifference: string = '1', // DEFAULT = 1 (same precision) 54 | ): string => { 55 | const numerator_ = new Decimal(numerator.toString()); 56 | const divisor_ = new Decimal(divisor.toString()); 57 | const _preDif = new Decimal(precisionDifference.toString()); 58 | const _normalisedDiv = divisor_.mul(_preDif); 59 | return numerator_.div(_normalisedDiv).toFixed(); 60 | }; 61 | 62 | /** 63 | * @param { BigNumber | string } value 64 | * @returns { string } 65 | */ 66 | export const floorDecimal = (value: BigNumber | string): string => Decimal.floor(value.toString()).toFixed(); 67 | 68 | /** 69 | * @param { Decimal } value 70 | * @returns { BigNumber } 71 | */ 72 | export const toBn = (value: Decimal): BigNumber => BigNumber.from(floorDecimal(value.toFixed())); 73 | 74 | /** 75 | * @param { BigNumber | string } to unix time 76 | * @param { BigNumber | string } from unix time *optional* default: now 77 | * @returns { string } as number seconds 'from' -> 'to' 78 | */ 79 | export const secondsToFrom = ( 80 | to: BigNumber | string, 81 | from: BigNumber | string = BigNumber.from(Math.round(new Date().getTime() / 1000)), // OPTIONAL: FROM defaults to current time if omitted 82 | ) : string => { 83 | const to_ = ethers.BigNumber.isBigNumber(to) ? to : BigNumber.from(to); 84 | const from_ = ethers.BigNumber.isBigNumber(from) ? from : BigNumber.from(from); 85 | return to_.sub(from_).toString(); 86 | }; 87 | 88 | /** ************************* 89 | YieldSpace functions 90 | *************************** */ 91 | 92 | /** 93 | * @param { BigNumber | string } baseBalance 94 | * @param { BigNumber | string } fyTokenBalance 95 | * @param { BigNumber | string } totalSupply 96 | * @param { BigNumber | string } base 97 | * @returns {[BigNumber, BigNumber]} 98 | * 99 | * https://www.desmos.com/calculator/mllhtohxfx 100 | */ 101 | export function mint( 102 | baseBalance: BigNumber | string, 103 | fyTokenBalance: BigNumber | string, 104 | totalSupply: BigNumber | string, 105 | mainTokenIn: BigNumber | string, 106 | ) : [ BigNumber, BigNumber ] { 107 | const baseBalance_ = new Decimal(baseBalance.toString()); 108 | const fyTokenBalance_ = new Decimal(fyTokenBalance.toString()); 109 | const supply_ = new Decimal(totalSupply.toString()); 110 | const mainTokenIn_ = new Decimal(mainTokenIn.toString()); 111 | 112 | let minted: Decimal 113 | let secTokenIn: Decimal 114 | minted = (supply_.mul(mainTokenIn_)).div(fyTokenBalance_); 115 | secTokenIn = (baseBalance_.mul(minted)).div(supply_); 116 | return [toBn(minted), toBn(secTokenIn)]; 117 | } 118 | 119 | /** 120 | * @param { BigNumber | string } baseBalance 121 | * @param { BigNumber | string } fyTokenBalance 122 | * @param { BigNumber | string } totalSupply 123 | * @param lpTokens { BigNumber | string } 124 | * @returns {[BigNumber, BigNumber]} 125 | * 126 | * https://www.desmos.com/calculator/ubsalzunpo 127 | */ 128 | export function burn( 129 | baseBalance: BigNumber | string, 130 | fyTokenBalance: BigNumber | string, 131 | totalSupply: BigNumber | string, 132 | lpTokens: BigNumber | string, 133 | ): [ BigNumber, BigNumber ] { 134 | const Z = new Decimal(baseBalance.toString()); 135 | const Y = new Decimal(fyTokenBalance.toString()); 136 | const S = new Decimal(totalSupply.toString()); 137 | const x = new Decimal(lpTokens.toString()); 138 | const z = (x.mul(Z)).div(S); 139 | const y = (x.mul(Y)).div(S); 140 | return [toBn(z), toBn(y)]; 141 | } 142 | 143 | /** 144 | * @param { BigNumber | string } baseBalance 145 | * @param { BigNumber | string } fyTokenBalanceVirtual 146 | * @param { BigNumber | string } fyTokenBalanceReal 147 | * @param { BigNumber | string } totalSupply 148 | * @param { BigNumber | string } fyToken 149 | * @param { BigNumber | string } timeTillMaturity 150 | * @param { BigNumber | string } scaleFactor 151 | * @returns {[BigNumber, BigNumber]} 152 | */ 153 | export function mintWithBase( 154 | baseBalance: BigNumber|string, 155 | fyTokenBalanceVirtual: BigNumber|string, 156 | fyTokenBalanceReal: BigNumber|string, 157 | supply: BigNumber|string, 158 | fyToken: BigNumber|string, 159 | timeTillMaturity: BigNumber|string, 160 | scaleFactor: BigNumber|string, 161 | ): [BigNumber, BigNumber] { 162 | const Z = new Decimal(baseBalance.toString()); 163 | const YR = new Decimal(fyTokenBalanceReal.toString()); 164 | const S = new Decimal(supply.toString()); 165 | const y = new Decimal(fyToken.toString()); 166 | 167 | // buyFyToken: 168 | /* console.log(` 169 | base reserves: ${baseBalance} 170 | fyToken virtual reserves: ${fyTokenBalanceVirtual} 171 | fyTokenOut: ${fyToken} 172 | timeTillMaturity: ${timeTillMaturity} 173 | scaleFactor: ${scaleFactor} 174 | `) */ 175 | const z1 = new Decimal(buyFYToken(baseBalance, fyTokenBalanceVirtual, fyToken, timeTillMaturity, scaleFactor).toString()); 176 | const Z2 = Z.add(z1) // Base reserves after the trade 177 | const YR2 = YR.sub(y) // FYToken reserves after the trade 178 | 179 | // Mint specifying how much fyToken to take in. Reverse of `mint`. 180 | const [m, z2] = mint( 181 | Z2.floor().toFixed(), 182 | YR2.floor().toFixed(), 183 | supply, 184 | fyToken, 185 | ) 186 | 187 | /* console.log(` 188 | Z_1: ${Z2.floor().toFixed()} 189 | Y_1: ${YR2.floor().toFixed()} 190 | z_1: ${z1} 191 | y_1: ${fyToken} 192 | `) */ 193 | return [m, toBn(z1).add(z2)]; 194 | } 195 | 196 | /** 197 | * @param { BigNumber | string } baseBalance 198 | * @param { BigNumber | string } fyTokenBalanceVirtual 199 | * @param { BigNumber | string } fyTokenBalanceReal 200 | * @param { BigNumber | string } totalSupply 201 | * @param { BigNumber | string } lpTokens 202 | * @param { BigNumber | string } timeTillMaturity 203 | * @param { BigNumber | string } scaleFactor 204 | * @returns { BigNumber } 205 | */ 206 | export function burnForBase( 207 | baseBalance: BigNumber, 208 | fyTokenBalanceVirtual: BigNumber, 209 | fyTokenBalanceReal: BigNumber, 210 | supply: BigNumber, 211 | lpTokens: BigNumber, 212 | timeTillMaturity: BigNumber, 213 | scaleFactor: BigNumber|string, 214 | ): BigNumber { 215 | // Burn FyToken 216 | const [z1, y] = burn(baseBalance, fyTokenBalanceReal, supply, lpTokens); 217 | // Sell FyToken for base 218 | const z2 = sellFYToken(baseBalance, fyTokenBalanceVirtual, y, timeTillMaturity, scaleFactor); 219 | const z1D = new Decimal(z1.toString()); 220 | const z2D = new Decimal(z2.toString()); 221 | return toBn(z1D.add(z2D)); 222 | } 223 | 224 | /** 225 | * @param { BigNumber | string } baseBalance 226 | * @param { BigNumber | string } fyTokenBalance 227 | * @param { BigNumber | string } base 228 | * @param { BigNumber | string } timeTillMaturity 229 | * @param { BigNumber | string } scaleFactor 230 | * @param { boolean } withNoFee 231 | * @returns { BigNumber } 232 | */ 233 | export function sellBase( 234 | baseBalance: BigNumber | string, 235 | fyTokenBalance: BigNumber | string, 236 | base: BigNumber | string, 237 | timeTillMaturity: BigNumber | string, 238 | scaleFactor: BigNumber|string, 239 | withNoFee: boolean = false, // optional: default === false 240 | ): BigNumber { 241 | const scaleFactor_ = new Decimal(scaleFactor.toString()); 242 | const baseBalance_ = (new Decimal(baseBalance.toString())).mul(scaleFactor_); 243 | const fyTokenBalance_ = (new Decimal(fyTokenBalance.toString())).mul(scaleFactor_); 244 | const timeTillMaturity_ = new Decimal(timeTillMaturity.toString()); 245 | const x = (new Decimal(base.toString())).mul(scaleFactor_); 246 | 247 | const g = withNoFee ? ONE : g1; 248 | const t = ts.mul(timeTillMaturity_); 249 | const a = ONE.sub(g.mul(t)); 250 | const invA = ONE.div(a); 251 | 252 | const Za = baseBalance_.pow(a); 253 | const Ya = fyTokenBalance_.pow(a); 254 | const Zxa = (baseBalance_.add(x)).pow(a); 255 | const sum = (Za.add(Ya)).sub(Zxa); 256 | const y = fyTokenBalance_.sub(sum.pow(invA)); 257 | const yFee = y.sub(precisionFee); 258 | 259 | return toBn(yFee.div(scaleFactor_)); 260 | } 261 | 262 | /** 263 | * @param { BigNumber | string } baseBalance 264 | * @param { BigNumber | string } fyTokenBalance 265 | * @param { BigNumber | string } fyToken 266 | * @param { BigNumber | string } timeTillMaturity 267 | * @param { BigNumber | string } scaleFactor 268 | * @param { boolean } withNoFee 269 | * @returns { BigNumber } 270 | */ 271 | export function sellFYToken( 272 | baseBalance: BigNumber | string, 273 | fyTokenBalance: BigNumber | string, 274 | fyToken: BigNumber | string, 275 | timeTillMaturity: BigNumber | string, 276 | scaleFactor: BigNumber|string, 277 | withNoFee: boolean = false, // optional: default === false 278 | ): BigNumber { 279 | const scaleFactor_ = new Decimal(scaleFactor.toString()); 280 | const baseBalance_ = (new Decimal(baseBalance.toString())).mul(scaleFactor_); 281 | const fyTokenBalance_ = (new Decimal(fyTokenBalance.toString())).mul(scaleFactor_); 282 | const timeTillMaturity_ = new Decimal(timeTillMaturity.toString()); 283 | const fyDai_ = (new Decimal(fyToken.toString())).mul(scaleFactor_); 284 | 285 | const g = withNoFee ? ONE : g2; 286 | const t = ts.mul(timeTillMaturity_); 287 | const a = ONE.sub(g.mul(t)); 288 | const invA = ONE.div(a); 289 | 290 | const Za = baseBalance_.pow(a); 291 | const Ya = fyTokenBalance_.pow(a); 292 | const Yxa = (fyTokenBalance_.add(fyDai_)).pow(a); 293 | const sum = Za.add(Ya.sub(Yxa)); 294 | const y = baseBalance_.sub(sum.pow(invA)); 295 | const yFee = y.sub(precisionFee); 296 | 297 | return toBn(yFee.div(scaleFactor_)); 298 | } 299 | 300 | /** 301 | * @param { BigNumber | string } baseBalance 302 | * @param { BigNumber | string } fyTokenBalance 303 | * @param { BigNumber | string } base 304 | * @param { BigNumber | string } timeTillMaturity 305 | * @param { BigNumber | string } scaleFactor 306 | * @param { boolean } withNoFee 307 | * @returns { BigNumber } 308 | */ 309 | export function buyBase( 310 | baseBalance: BigNumber | string, 311 | fyTokenBalance: BigNumber | string, 312 | base: BigNumber | string, 313 | timeTillMaturity: BigNumber | string, 314 | scaleFactor: BigNumber|string, 315 | withNoFee: boolean = false, // optional: default === false 316 | ): BigNumber { 317 | const scaleFactor_ = new Decimal(scaleFactor.toString()); 318 | const baseBalance_ = (new Decimal(baseBalance.toString())).mul(scaleFactor_); 319 | const fyTokenBalance_ = (new Decimal(fyTokenBalance.toString())).mul(scaleFactor_); 320 | const timeTillMaturity_ = new Decimal(timeTillMaturity.toString()); 321 | const x = (new Decimal(base.toString())).mul(scaleFactor_); 322 | 323 | const g = withNoFee ? ONE : g2; 324 | const t = ts.mul(timeTillMaturity_); 325 | const a = ONE.sub(g.mul(t)); 326 | const invA = ONE.div(a); 327 | 328 | const Za = baseBalance_.pow(a); 329 | const Ya = fyTokenBalance_.pow(a); 330 | const Zxa = (baseBalance_.sub(x)).pow(a); 331 | const sum = (Za.add(Ya)).sub(Zxa); 332 | const y = (sum.pow(invA)).sub(fyTokenBalance_); 333 | const yFee = y.add(precisionFee); 334 | 335 | return toBn(yFee.div(scaleFactor_)); 336 | } 337 | 338 | /** 339 | * @param { BigNumber | string } baseBalance 340 | * @param { BigNumber | string } fyTokenBalance 341 | * @param { BigNumber | string } fyToken 342 | * @param { BigNumber | string } timeTillMaturity 343 | * @param { BigNumber | string } scaleFactor 344 | * @param { boolean } withNoFee 345 | * @returns { BigNumber } 346 | */ 347 | export function buyFYToken( 348 | baseBalance: BigNumber | string, 349 | fyTokenBalance: BigNumber | string, 350 | fyToken: BigNumber | string, 351 | timeTillMaturity: BigNumber | string, 352 | scaleFactor: BigNumber|string, 353 | withNoFee: boolean = false, // optional: default === false 354 | ): BigNumber { 355 | const scaleFactor_ = new Decimal(scaleFactor.toString()); 356 | const baseBalance_ = (new Decimal(baseBalance.toString())).mul(scaleFactor_); 357 | const fyTokenBalance_ = (new Decimal(fyTokenBalance.toString())).mul(scaleFactor_); 358 | const timeTillMaturity_ = new Decimal(timeTillMaturity.toString()); 359 | const fyDai_ = (new Decimal(fyToken.toString())).mul(scaleFactor_); 360 | 361 | const g = withNoFee ? ONE : g1; 362 | const t = ts.mul(timeTillMaturity_); 363 | const a = ONE.sub(g.mul(t)); 364 | const invA = ONE.div(a); 365 | 366 | const Za = baseBalance_.pow(a); 367 | const Ya = fyTokenBalance_.pow(a); 368 | const Yxa = (fyTokenBalance_.sub(fyDai_)).pow(a); 369 | const sum = Za.add(Ya.sub(Yxa)); 370 | const y = (sum.pow(invA)).sub(baseBalance_); 371 | const yFee = y.add(precisionFee); 372 | 373 | return toBn(yFee.div(scaleFactor_)); 374 | } 375 | 376 | /** 377 | * @param { BigNumber | string } baseBalance 378 | * @param { BigNumber | string } fyTokenBalance 379 | * @param { BigNumber | string } fyToken 380 | * @param { BigNumber | string } timeTillMaturity 381 | * @param { BigNumber | string } scaleFactor 382 | * @returns { BigNumber } 383 | */ 384 | export function getFee( 385 | baseBalance: BigNumber | string, 386 | fyTokenBalance: BigNumber | string, 387 | fyToken: BigNumber | string, 388 | timeTillMaturity: BigNumber | string, 389 | scaleFactor: BigNumber|string, 390 | ): BigNumber { 391 | let fee_: Decimal = ZERO; 392 | const fyDai_: BigNumber = BigNumber.isBigNumber(fyToken) ? fyToken : BigNumber.from(fyToken); 393 | 394 | if (fyDai_.gte(ethers.constants.Zero)) { 395 | const daiWithFee: BigNumber = buyFYToken(baseBalance, fyTokenBalance, fyToken, timeTillMaturity, scaleFactor); 396 | const daiWithoutFee: BigNumber = buyFYToken(baseBalance, fyTokenBalance, fyToken, timeTillMaturity, scaleFactor, true); 397 | fee_ = (new Decimal(daiWithFee.toString())).sub(new Decimal(daiWithoutFee.toString())); 398 | } else { 399 | const daiWithFee:BigNumber = sellFYToken(baseBalance, fyTokenBalance, fyDai_.mul(BigNumber.from('-1')), timeTillMaturity, scaleFactor); 400 | const daiWithoutFee:BigNumber = sellFYToken(baseBalance, fyTokenBalance, fyDai_.mul(BigNumber.from('-1')), timeTillMaturity, scaleFactor, true); 401 | fee_ = (new Decimal(daiWithoutFee.toString())).sub(new Decimal(daiWithFee.toString())); 402 | } 403 | return toBn(fee_); 404 | } 405 | 406 | export function fyDaiForMint( 407 | baseBalance: BigNumber |string, 408 | fyDaiRealBalance: BigNumber|string, 409 | fyDaiVirtualBalance: BigNumber|string, 410 | base: BigNumber|string, 411 | timeTillMaturity: BigNumber|string, 412 | ): string { 413 | const baseBalance_ = new Decimal(baseBalance.toString()); 414 | const fyDaiRealBalance_ = new Decimal(fyDaiRealBalance.toString()); 415 | const timeTillMaturity_ = new Decimal(timeTillMaturity.toString()); 416 | const x = new Decimal(base.toString()); 417 | let min = ZERO; 418 | let max = x.mul(TWO); 419 | let yOut = Decimal.floor((min.add(max)).div(TWO)); 420 | let zIn: Decimal 421 | 422 | let i = 0; 423 | while (true) { 424 | if (i++ > 100) throw 'Not converging' 425 | 426 | zIn = new Decimal( 427 | buyFYToken( 428 | baseBalance, 429 | fyDaiVirtualBalance, 430 | BigNumber.from(yOut.toFixed(0)), 431 | timeTillMaturity_.toString(), 432 | BigNumber.from('1') 433 | ).toString(), 434 | ); 435 | 436 | const Z_1 = baseBalance_.add(zIn); // New base balance 437 | const z_1 = x.sub(zIn) // My remaining base 438 | const Y_1 = fyDaiRealBalance_.sub(yOut); // New fyToken balance 439 | const y_1 = yOut // My fyToken 440 | const pz = z_1.div(z_1.add(y_1)); // base proportion in my assets 441 | const PZ = Z_1.div(Z_1.add(Y_1)); // base proportion in the balances 442 | 443 | // Targeting between 0.001% and 0.002% slippage (surplus) 444 | // Lower both if getting "Not enough base in" errors. That means that 445 | // the calculation that was done off-chain was stale when the actual mint happened. 446 | // It might be reasonable to set `minTarget` to half the slippage, and `maxTarget` 447 | // to the slippage. That would also mean that the algorithm would aim to waste at 448 | // least half the slippage allowed. 449 | // For large trades, it would make sense to append a `retrieveBase` action at the 450 | // end of the batch. 451 | const minTarget = new Decimal(1.00001) 452 | const maxTarget = new Decimal(1.00002) 453 | 454 | // The base proportion in my assets needs to be higher than but very close to the 455 | // base proportion in the balances, to make sure all the fyToken is used. 456 | // eslint-disable-next-line no-plusplus 457 | if ((PZ.mul(maxTarget) > pz && PZ.mul(minTarget) < pz)) { 458 | break; // Too many iterations, or found the result 459 | } else if (PZ.mul(maxTarget) <= pz) { 460 | min = yOut; 461 | yOut = (yOut.add(max)).div(TWO); // bought too little fyToken, buy some more 462 | } else { 463 | max = yOut; 464 | yOut = (yOut.add(min)).div(TWO); // bought too much fyToken, buy a bit less 465 | } 466 | } 467 | 468 | /* console.log(` 469 | base reserves: ${baseBalance} 470 | fyToken virtual reserves: ${fyDaiVirtualBalance} 471 | fyTokenOut: ${BigNumber.from(yOut.toFixed(0))} 472 | timeTillMaturity: ${timeTillMaturity_} 473 | scaleFactor: ${BigNumber.from('1')} 474 | `) 475 | const Z_1 = baseBalance_.add(zIn); // New base balance 476 | const z_1 = x.sub(zIn) // My remaining base 477 | const Y_1 = fyDaiRealBalance_.sub(yOut); // New fyToken balance 478 | const y_1 = yOut // My fyToken 479 | const pz = z_1.div(z_1.add(y_1)); // base proportion in my assets 480 | const PZ = Z_1.div(Z_1.add(Y_1)); // base proportion in the balances 481 | console.log(` 482 | Z_1: ${Z_1.floor().toFixed()} 483 | Y_1: ${Y_1.floor().toFixed()} 484 | z_1: ${z_1.floor().toFixed()} 485 | y_1: ${y_1.floor().toFixed()} 486 | PZ: ${PZ} 487 | pz: ${pz} 488 | i: ${i} 489 | `) */ 490 | return Decimal.floor(yOut).toFixed(); 491 | } 492 | 493 | /** 494 | * Split a certain amount of X liquidity into its two componetnts (eg. base and fyToken) 495 | * @param { BigNumber } xBalance // eg. base balance 496 | * @param { BigNumber } yBalance // eg. fyToken balance 497 | * @param {BigNumber} xAmount // amount to split in wei 498 | * @returns [ BigNumber, BigNumber ] returns an array of [base, fyToken] 499 | */ 500 | export const splitLiquidity = ( 501 | xBalance: BigNumber | string, 502 | yBalance: BigNumber | string, 503 | xAmount: BigNumber | string, 504 | ) : [string, string] => { 505 | const xBalance_ = new Decimal(xBalance.toString()); 506 | const yBalance_ = new Decimal(yBalance.toString()); 507 | const xAmount_ = new Decimal(xAmount.toString()); 508 | const xPortion = (xAmount_.mul(xBalance_)).div(yBalance_.add(xBalance_)); 509 | const yPortion = xAmount_.sub(xPortion); 510 | return [xPortion.toFixed(), yPortion.toFixed()]; 511 | }; 512 | 513 | /** 514 | * Calculate Slippage 515 | * @param { BigNumber } value 516 | * @param { BigNumber } slippage optional: defaults to 0.005 (0.5%) 517 | * @param { number } minimise optional: whether the resutl should be a minimum or maximum (default max) 518 | * @returns { string } human readable string 519 | */ 520 | export const calculateSlippage = ( 521 | value: BigNumber | string, 522 | slippage: BigNumber | string = '0.005', 523 | minimise:boolean = false, 524 | ): string => { 525 | const value_ = new Decimal(value.toString()); 526 | const _slippageAmount = floorDecimal(mulDecimal(value, slippage)); 527 | if (minimise) { 528 | return value_.sub(_slippageAmount).toFixed(); 529 | } 530 | return value_.add(_slippageAmount).toFixed(); 531 | }; 532 | 533 | /** 534 | * Calculate Annualised Yield Rate 535 | * @param { BigNumber | string } rate // current [base] price per unit y[base] 536 | * @param { BigNumber | string } amount // y[base] amount at maturity 537 | * @param { number } maturity // date of maturity 538 | * @param { number } fromDate // ***optional*** start date - defaults to now() 539 | * @returns { string | undefined } human readable string 540 | */ 541 | export const calculateAPR = ( 542 | tradeValue: BigNumber | string, 543 | amount: BigNumber | string, 544 | maturity: number, 545 | fromDate: number = (Math.round(new Date().getTime() / 1000)), // if not provided, defaults to current time. 546 | ): string | undefined => { 547 | const tradeValue_ = new Decimal(tradeValue.toString()); 548 | const amount_ = new Decimal(amount.toString()); 549 | 550 | if ( 551 | maturity > Math.round(new Date().getTime() / 1000) 552 | ) { 553 | const secsToMaturity = maturity - fromDate; 554 | const propOfYear = new Decimal(secsToMaturity / SECONDS_PER_YEAR); 555 | const priceRatio = amount_.div(tradeValue_); 556 | const powRatio = ONE.div(propOfYear); 557 | const apr = (priceRatio.pow(powRatio)).sub(ONE); 558 | 559 | if (apr.gt(ZERO) && apr.lt(100)) { 560 | return apr.mul(100).toFixed(); 561 | } 562 | return undefined; 563 | } 564 | return undefined; 565 | }; 566 | 567 | /** 568 | * Calculates the collateralization ratio 569 | * based on the collat amount and value and debt value. 570 | * @param { BigNumber | string } collateralAmount amount of collateral ( in wei) 571 | * @param { BigNumber | string } collateralPrice price of collateral (in USD) 572 | * @param { BigNumber | string } debtValue value of base debt (in USD) 573 | * @param {boolean} asPercent OPTIONAL: flag to return ratio as a percentage 574 | * @returns { string | undefined } 575 | */ 576 | export const collateralizationRatio = ( 577 | collateralAmount: BigNumber | string, 578 | collateralPrice: BigNumber | string, 579 | debtValue: BigNumber | string, 580 | asPercent: boolean = false, // OPTIONAL: flag to return as percentage 581 | ): string | undefined => { 582 | if ( 583 | ethers.BigNumber.isBigNumber(debtValue) ? debtValue.isZero() : debtValue === '0' 584 | ) { 585 | return undefined; 586 | } 587 | const _colVal = mulDecimal(collateralAmount, collateralPrice); 588 | const _ratio = divDecimal(_colVal, debtValue); 589 | 590 | if (asPercent) { 591 | return mulDecimal('100', _ratio); 592 | } 593 | return _ratio; 594 | }; 595 | 596 | /** 597 | * Calcualtes the amount (base, or other variant) that can be borrowed based on 598 | * an amount of collateral (ETH, or other), and collateral price. 599 | * 600 | * @param {BigNumber | string} collateralAmount amount of collateral 601 | * @param {BigNumber | string} collateralPrice price of unit collateral (in currency x) 602 | * @param {BigNumber | string} debtValue value of debt (in currency x) 603 | * @param {BigNumber | string} liquidationRatio OPTIONAL: 1.5 (150%) as default 604 | * 605 | * @returns {string} 606 | */ 607 | export const borrowingPower = ( 608 | collateralAmount: BigNumber | string, 609 | collateralPrice: BigNumber | string, 610 | debtValue: BigNumber | string, 611 | liquidationRatio: string = '1.5', // OPTIONAL: 150% as default 612 | ): string => { 613 | const collateralValue = mulDecimal(collateralAmount, collateralPrice); 614 | const maxSafeDebtValue_ = new Decimal(divDecimal(collateralValue, liquidationRatio)); 615 | const debtValue_ = new Decimal(debtValue.toString()); 616 | const _max = debtValue_.lt(maxSafeDebtValue_) ? maxSafeDebtValue_.sub(debtValue_) : new Decimal('0'); 617 | return _max.toFixed(0); 618 | }; 619 | -------------------------------------------------------------------------------- /test/011_yield_math_base.ts: -------------------------------------------------------------------------------- 1 | import { debugLog } from './shared/helpers' 2 | 3 | import { YieldMathWrapper } from '../typechain/YieldMathWrapper' 4 | import { YieldMath } from '../typechain/YieldMath' 5 | 6 | import { BigNumber } from 'ethers' 7 | 8 | import { ethers } from 'hardhat' 9 | 10 | /** 11 | * Throws given message unless given condition is true. 12 | * 13 | * @param message message to throw unless given condition is true 14 | * @param condition condition to check 15 | */ 16 | function assert(message: string, condition: boolean) { 17 | if (!condition) throw message 18 | } 19 | 20 | describe('YieldMath - Base', async function () { 21 | this.timeout(0) 22 | let yieldMathLibrary: YieldMath 23 | let yieldMath: YieldMathWrapper 24 | 25 | before(async () => { 26 | const YieldMathFactory = await ethers.getContractFactory('YieldMath') 27 | yieldMathLibrary = ((await YieldMathFactory.deploy()) as unknown) as YieldMath 28 | await yieldMathLibrary.deployed() 29 | 30 | const YieldMathWrapperFactory = await ethers.getContractFactory('YieldMathWrapper', { 31 | libraries: { 32 | YieldMath: yieldMathLibrary.address, 33 | }, 34 | }) 35 | 36 | yieldMath = ((await YieldMathWrapperFactory.deploy()) as unknown) as YieldMathWrapper 37 | await yieldMath.deployed() 38 | }) 39 | 40 | describe('Test pure math functions', async () => { 41 | it('Test `log_2` function', async () => { 42 | var xValues = [ 43 | '0x0', 44 | '0x1', 45 | '0x2', 46 | '0xFEDCBA9876543210', 47 | '0xFFFFFFFFFFFFFFFF', 48 | '0x10000000000000000', 49 | '0xFFFFFFFFFFFFFFFFFFFFFFFF', 50 | '0x1000000000000000000000000', 51 | '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 52 | '0x10000000000000000000000000000', 53 | '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 54 | '0x1000000000000000000000000000000', 55 | '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 56 | '0x10000000000000000000000000000000', 57 | '0x3FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 58 | '0x40000000000000000000000000000000', 59 | '0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 60 | '0x80000000000000000000000000000000', 61 | '0xFEDCBA9876543210FEDCBA9876543210', 62 | '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 63 | ] 64 | 65 | for (var i = 0; i < xValues.length; i++) { 66 | var xValue = xValues[i] 67 | var x = BigNumber.from(xValue) 68 | var result 69 | try { 70 | result = await yieldMath.log_2(x.toString()) 71 | } catch (e) { 72 | result = [false, undefined] 73 | } 74 | if (!x.eq(BigNumber.from('0x0'))) { 75 | assert('log_2 (' + xValue + ')[0]', result[0] as boolean) 76 | assert( 77 | 'log_2 (' + xValue + ')[1]', 78 | Math.abs( 79 | Math.log(Number(x)) / Math.LN2 - 80 | Number(result[1]) / Number(BigNumber.from('0x2000000000000000000000000000000')) 81 | ) < 0.00000000001 82 | ) 83 | } else { 84 | assert('!log_2 (' + xValue + ')[0]', !result[0]) 85 | } 86 | } 87 | }) 88 | 89 | it('Test `pow_2` function', async () => { 90 | var xValues = [ 91 | '0x0', 92 | '0x1', 93 | '0x2', 94 | '0x1FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 95 | '0x2000000000000000000000000000000', 96 | '0x2000000000000000000000000000001', 97 | '0x20123456789ABCDEF0123456789ABCD', 98 | '0x3FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 99 | '0x40000000000000000000000000000000', 100 | '0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 101 | '0x80000000000000000000000000000000', 102 | '0xFEDCBA9876543210FEDCBA9876543210', 103 | '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 104 | ] 105 | 106 | for (var i = 0; i < xValues.length; i++) { 107 | var xValue = xValues[i] 108 | debugLog(' pow_2 (' + xValue + ')') 109 | var x = BigNumber.from(xValue) 110 | var result 111 | try { 112 | result = await yieldMath.pow_2(x) 113 | } catch (e) { 114 | result = [false, undefined] 115 | } 116 | assert('pow_2 (' + xValue + ')[0]', result[0] as boolean) 117 | var expected = Math.pow(2, Number(x) / Number(BigNumber.from('0x2000000000000000000000000000000'))) 118 | assert( 119 | 'pow_2 (' + xValue + ')[1]', 120 | Math.abs(expected - Number(result[1])) <= Math.max(1.0000000000001, expected / 1000000000000.0) 121 | ) 122 | } 123 | }) 124 | 125 | it('Test `pow` function', async () => { 126 | var xValues = ['0x0', '0x1', '0x2', '0xFEDCBA9876543210', '0xFEDCBA9876543210FEDCBA9876543210'] 127 | var yzValues = [ 128 | ['0x0', '0x0'], 129 | ['0x1', '0x0'], 130 | ['0x0', '0x1'], 131 | ['0x1', '0x1'], 132 | ['0x2', '0x1'], 133 | ['0x3', '0x1'], 134 | ['0x7F', '0x1'], 135 | ['0xFEDCBA987', '0x1'], 136 | ['0xFEDCBA9876543210FEDCBA9876543210', '0x1'], 137 | ['0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', '0x1'], 138 | ['0x1', '0x2'], 139 | ['0x1', '0x3'], 140 | ['0x1', '0x7F'], 141 | ['0x1', '0xFEDCBA9876543210'], 142 | ['0x1', '0xFEDCBA9876543210FEDCBA9876543210'], 143 | ['0x1', '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'], 144 | ] 145 | 146 | for (var i = 0; i < xValues.length; i++) { 147 | var xValue = xValues[i] 148 | for (var j = 0; j < yzValues.length; j++) { 149 | var yValue = yzValues[j][0] 150 | var zValue = yzValues[j][1] 151 | debugLog(' pow (' + xValue + ', ' + yValue + ', ' + zValue + ')') 152 | var x = BigNumber.from(xValue) 153 | var y = BigNumber.from(yValue) 154 | var z = BigNumber.from(zValue) 155 | var result 156 | try { 157 | result = await yieldMath.pow(x, y, z) 158 | } catch (e) { 159 | result = [false, undefined] 160 | } 161 | 162 | if (!z.eq(BigNumber.from('0x0')) && (!x.eq(BigNumber.from('0x0')) || !y.eq(BigNumber.from('0x0')))) { 163 | assert('pow (' + xValue + ', ' + yValue + ', ' + zValue + ')[0]', result[0] as boolean) 164 | var expectedLog = 165 | (Math.log(Number(x)) * Number(y)) / Number(z) + 128 * (1.0 - Number(y) / Number(z)) * Math.LN2 166 | if (expectedLog < 0.0) expectedLog = -1.0 167 | if (x.eq(BigNumber.from('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'))) expectedLog = 128 * Math.LN2 168 | var resultLog = Math.log(Number(result[1])) 169 | if (resultLog < 0.0) resultLog = -1.0 170 | assert( 171 | 'pow (' + xValue + ', ' + yValue + ', ' + zValue + ')[1]', 172 | Math.abs(expectedLog - resultLog) <= 0.000000001 173 | ) 174 | } else { 175 | assert('!pow (' + xValue + ', ' + yValue + ', ' + zValue + ')[0]', !result[0]) 176 | } 177 | } 178 | } 179 | }) 180 | }) 181 | }) 182 | -------------------------------------------------------------------------------- /test/012_yield_math_reverts.ts: -------------------------------------------------------------------------------- 1 | import { YieldMathWrapper } from '../typechain/YieldMathWrapper' 2 | import { YieldMath } from '../typechain/YieldMath' 3 | 4 | import { BigNumber } from 'ethers' 5 | 6 | import { constants } from '@yield-protocol/utils-v2' 7 | const { WAD, MAX128 } = constants 8 | const MAX = MAX128 9 | 10 | import { secondsInOneYear, secondsInTenYears, ts, g0 } from '../src/constants' 11 | 12 | import { ethers } from 'hardhat' 13 | import { expect } from 'chai' 14 | 15 | describe('YieldMath - Reverts', async function () { 16 | this.timeout(0) 17 | let yieldMathLibrary: YieldMath 18 | let yieldMath: YieldMathWrapper 19 | 20 | before(async () => { 21 | const YieldMathFactory = await ethers.getContractFactory('YieldMath') 22 | yieldMathLibrary = ((await YieldMathFactory.deploy()) as unknown) as YieldMath 23 | await yieldMathLibrary.deployed() 24 | 25 | const YieldMathWrapperFactory = await ethers.getContractFactory('YieldMathWrapper', { 26 | libraries: { 27 | YieldMath: yieldMathLibrary.address, 28 | }, 29 | }) 30 | 31 | yieldMath = ((await YieldMathWrapperFactory.deploy()) as unknown) as YieldMathWrapper 32 | await yieldMath.deployed() 33 | }) 34 | 35 | describe('fyTokenOutForBaseIn reverts', () => { 36 | beforeEach(async () => {}) 37 | 38 | // If time to maturity is higher than 1/ts, multiplied or divided by g, we are too far from maturity. 39 | it('Too far from maturity', async () => { 40 | await expect( 41 | yieldMath.fyTokenOutForBaseIn( 42 | WAD.mul(10), 43 | WAD.mul(10), 44 | WAD, 45 | secondsInTenYears.add(BigNumber.from(60 * 60)), 46 | ts, 47 | g0 48 | ) 49 | ).to.be.revertedWith('YieldMath: Too far from maturity') 50 | }) 51 | 52 | // If the base in, added to the base balance, exceed 2**128, we will have too much base to operate 53 | it('Too much base in', async () => { 54 | await expect(yieldMath.fyTokenOutForBaseIn(MAX, WAD.mul(10), WAD, secondsInOneYear, ts, g0)).to.be.revertedWith( 55 | 'YieldMath: Too much base in' 56 | ) 57 | }) 58 | 59 | // If the fyToken to be obtained exceeds the fyToken balance, the trade reverts 60 | it('Insufficient fyToken balance', async () => { 61 | await expect( 62 | yieldMath.fyTokenOutForBaseIn(WAD, WAD.mul(10), WAD.mul(20), secondsInOneYear, ts, g0) 63 | ).to.be.revertedWith('YieldMath: Insufficient fyToken reserves') 64 | }) 65 | 66 | /* it("Rounding induced error", async () => { 67 | await expect( 68 | yieldMath.fyTokenOutForBaseIn(WAD, WAD, 0, secondsInOneYear, ts, g0) 69 | ).to.be.revertedWith( 70 | 'YieldMath: Rounding induced error' 71 | ) 72 | }) */ 73 | }) 74 | 75 | describe('baseOutForFYTokenIn reverts', () => { 76 | beforeEach(async () => {}) 77 | 78 | // If time to maturity is higher than 1/ts, multiplied or divided by g, we are too far from maturity. 79 | it('Too far from maturity', async () => { 80 | await expect( 81 | yieldMath.baseOutForFYTokenIn( 82 | WAD.mul(10), 83 | WAD.mul(10), 84 | WAD, 85 | secondsInTenYears.add(BigNumber.from(60 * 60)), 86 | ts, 87 | g0 88 | ) 89 | ).to.be.revertedWith('YieldMath: Too far from maturity') 90 | }) 91 | 92 | // If the fyToken in, added to the fyToken balance, exceed 2**128, we will have too much fyToken to operate 93 | it('Too much fyToken in', async () => { 94 | await expect(yieldMath.baseOutForFYTokenIn(WAD.mul(10), MAX, WAD, secondsInOneYear, ts, g0)).to.be.revertedWith( 95 | 'YieldMath: Too much fyToken in' 96 | ) 97 | }) 98 | 99 | // If the base to be obtained exceeds the base balance, the trade reverts 100 | it('Insufficient base balance', async () => { 101 | await expect( 102 | yieldMath.baseOutForFYTokenIn(WAD.mul(10), WAD, WAD.mul(20), secondsInOneYear, ts, g0) 103 | ).to.be.revertedWith('YieldMath: Insufficient base reserves') 104 | }) 105 | 106 | /* it("Rounding induced error", async () => { 107 | await expect( 108 | yieldMath.baseOutForFYTokenIn(MAX, WAD, WAD, 1, ts, g0) 109 | ).to.be.revertedWith( 110 | 'YieldMath: Rounding induced error' 111 | ) 112 | }) */ 113 | }) 114 | 115 | describe('fyTokenInForBaseOut reverts', () => { 116 | beforeEach(async () => {}) 117 | 118 | // If time to maturity is higher than 1/ts, multiplied or divided by g, we are too far from maturity. 119 | it('Too far from maturity', async () => { 120 | await expect( 121 | yieldMath.fyTokenInForBaseOut( 122 | WAD.mul(10), 123 | WAD.mul(10), 124 | WAD, 125 | secondsInTenYears.add(BigNumber.from(60 * 60)), 126 | ts, 127 | g0 128 | ) 129 | ).to.be.revertedWith('YieldMath: Too far from maturity') 130 | }) 131 | 132 | it('Too much base out', async () => { 133 | await expect( 134 | yieldMath.fyTokenInForBaseOut(WAD.mul(2), WAD, WAD.mul(3), secondsInOneYear, ts, g0) 135 | ).to.be.revertedWith('YieldMath: Too much base out') 136 | }) 137 | 138 | // If the base to be obtained exceeds the base balance, the trade reverts 139 | it('Resulting fyToken balance too high', async () => { 140 | await expect( 141 | yieldMath.fyTokenInForBaseOut(WAD.mul(10), MAX, WAD, secondsInOneYear.mul(4), ts, g0), 142 | 'YieldMath: Resulting fyToken balance too high' 143 | ).to.be.revertedWith('YieldMath: Resulting fyToken reserves too high') 144 | }) 145 | 146 | it('Rounding induced error', async () => { 147 | await expect(yieldMath.fyTokenInForBaseOut(WAD.mul(10), MAX, WAD, 1, ts, g0)).to.be.revertedWith( 148 | 'YieldMath: Rounding induced error' 149 | ) 150 | }) 151 | }) 152 | 153 | describe('baseInForFYBaseOut reverts', () => { 154 | beforeEach(async () => {}) 155 | 156 | // If time to maturity is higher than 1/ts, multiplied or divided by g, we are too far from maturity. 157 | it('Too far from maturity', async () => { 158 | await expect( 159 | yieldMath.baseInForFYTokenOut( 160 | WAD.mul(10), 161 | WAD.mul(10), 162 | WAD, 163 | secondsInTenYears.add(BigNumber.from(60 * 60)), 164 | ts, 165 | g0 166 | ) 167 | ).to.be.revertedWith('YieldMath: Too far from maturity') 168 | }) 169 | 170 | it('Too much fyToken out', async () => { 171 | await expect(yieldMath.baseInForFYTokenOut(WAD, WAD, WAD.mul(2), secondsInOneYear, ts, g0)).to.be.revertedWith( 172 | 'YieldMath: Too much fyToken out' 173 | ) 174 | }) 175 | 176 | // If the base to be traded in makes the base balance to go over 2**128, the trade reverts 177 | it('Resulting base balance too high', async () => { 178 | await expect( 179 | yieldMath.baseInForFYTokenOut(MAX, WAD.mul(10), WAD, secondsInOneYear.mul(4), ts, g0) 180 | ).to.be.revertedWith('YieldMath: Resulting base reserves too high') 181 | }) 182 | 183 | it('Rounding induced error', async () => { 184 | await expect( 185 | yieldMath.baseInForFYTokenOut(MAX, WAD, WAD, 1, ts, g0) // Why does it revert? No idea. 186 | ).to.be.revertedWith('YieldMath: Rounding induced error') 187 | }) 188 | }) 189 | }) 190 | -------------------------------------------------------------------------------- /test/013_yield_math_surface.ts: -------------------------------------------------------------------------------- 1 | import { debugLog } from './shared/helpers' 2 | 3 | import { YieldMathWrapper } from '../typechain/YieldMathWrapper' 4 | import { YieldMath } from '../typechain/YieldMath' 5 | 6 | import { BigNumber } from 'ethers' 7 | 8 | import { ethers } from 'hardhat' 9 | import { expect } from 'chai' 10 | 11 | import { ts, g1, g2 } from '../src/constants' 12 | 13 | import { sellBase, sellFYToken, buyBase, buyFYToken } from '../src/yieldspace' 14 | 15 | const PRECISION = BigNumber.from('100000000000000') // 1e14 16 | 17 | function almostEqual(x: BigNumber, y: BigNumber, p: BigNumber) { 18 | // Check that abs(x - y) < p: 19 | const diff = x.gt(y) ? BigNumber.from(x).sub(y) : BigNumber.from(y).sub(x) // Not sure why I have to convert x and y to BigNumber 20 | expect(diff.div(p)).to.eq(0) // Hack to avoid silly conversions. BigNumber truncates decimals off. 21 | } 22 | 23 | describe('YieldMath - Surface', async function () { 24 | this.timeout(0) 25 | let yieldMathLibrary: YieldMath 26 | let yieldMath: YieldMathWrapper 27 | 28 | const baseBalances = [ 29 | // BigNumber.from('100000000000000000000000'), 30 | // BigNumber.from('1000000000000000000000000'), 31 | BigNumber.from('10000000000000000000000000'), 32 | BigNumber.from('100000000000000000000000000'), 33 | BigNumber.from('1000000000000000000000000000'), 34 | ] 35 | const fyTokenBalanceDeltas = [ 36 | // BigNumber.from('10000000000000000000'), 37 | // BigNumber.from('1000000000000000000000'), 38 | BigNumber.from('100000000000000000000000'), 39 | BigNumber.from('10000000000000000000000000'), 40 | BigNumber.from('1000000000000000000000000000'), 41 | ] 42 | const tradeSizes = [ 43 | // BigNumber.from('1000000000000000000'), 44 | // BigNumber.from('10000000000000000000'), 45 | BigNumber.from('100000000000000000000'), 46 | BigNumber.from('1000000000000000000000'), 47 | BigNumber.from('10000000000000000000000'), 48 | ] 49 | const timesTillMaturity = [ 50 | // BigNumber.from('4'), 51 | // BigNumber.from('40'), 52 | BigNumber.from('4000'), 53 | BigNumber.from('400000'), 54 | BigNumber.from('40000000'), 55 | ] 56 | 57 | const scaleFactor = BigNumber.from('1') 58 | 59 | before(async () => { 60 | const YieldMathFactory = await ethers.getContractFactory('YieldMath') 61 | yieldMathLibrary = ((await YieldMathFactory.deploy()) as unknown) as YieldMath 62 | await yieldMathLibrary.deployed() 63 | 64 | const YieldMathWrapperFactory = await ethers.getContractFactory('YieldMathWrapper', { 65 | libraries: { 66 | YieldMath: yieldMathLibrary.address, 67 | }, 68 | }) 69 | 70 | yieldMath = ((await YieldMathWrapperFactory.deploy()) as unknown) as YieldMathWrapper 71 | await yieldMath.deployed() 72 | }) 73 | 74 | describe('Test scenarios', async () => { 75 | it('Compare a lattice of on-chain vs off-chain yieldspace trades', async function () { 76 | this.timeout(0) 77 | 78 | for (var baseBalance of baseBalances) { 79 | for (var fyTokenBalanceDelta of fyTokenBalanceDeltas) { 80 | for (var tradeSize of tradeSizes) { 81 | for (var timeTillMaturity of timesTillMaturity) { 82 | debugLog(`baseBalance, fyTokenBalanceDelta, tradeSize, timeTillMaturity`) 83 | debugLog(`${baseBalance}, ${fyTokenBalanceDelta}, ${tradeSize}, ${timeTillMaturity}`) 84 | const fyTokenBalance = baseBalance.add(fyTokenBalanceDelta) 85 | let offChain, onChain 86 | offChain = sellFYToken(baseBalance, fyTokenBalance, tradeSize, timeTillMaturity, scaleFactor) 87 | onChain = await yieldMath.baseOutForFYTokenIn( 88 | baseBalance, 89 | fyTokenBalance, 90 | tradeSize, 91 | timeTillMaturity, 92 | ts, 93 | g2 94 | ) 95 | debugLog(`offChain sellFYToken: ${offChain}`) 96 | debugLog(`onChain sellFYToken: ${onChain}`) 97 | almostEqual(onChain, offChain, PRECISION) 98 | 99 | offChain = sellBase(baseBalance, fyTokenBalance, tradeSize, timeTillMaturity, scaleFactor) 100 | onChain = await yieldMath.fyTokenOutForBaseIn( 101 | baseBalance, 102 | fyTokenBalance, 103 | tradeSize, 104 | timeTillMaturity, 105 | ts, 106 | g1 107 | ) 108 | debugLog(`offChain sellBase: ${offChain}`) 109 | debugLog(`onChain sellBase: ${onChain}`) 110 | almostEqual(onChain, offChain, PRECISION) 111 | 112 | offChain = buyBase(baseBalance, fyTokenBalance, tradeSize, timeTillMaturity, scaleFactor) 113 | onChain = await yieldMath.fyTokenInForBaseOut( 114 | baseBalance, 115 | fyTokenBalance, 116 | tradeSize, 117 | timeTillMaturity, 118 | ts, 119 | g2 120 | ) 121 | debugLog(`offChain buyBase: ${offChain}`) 122 | debugLog(`onChain buyBase: ${onChain}`) 123 | almostEqual(onChain, offChain, PRECISION) 124 | 125 | offChain = buyFYToken(baseBalance, fyTokenBalance, tradeSize, timeTillMaturity, scaleFactor) 126 | onChain = await yieldMath.baseInForFYTokenOut( 127 | baseBalance, 128 | fyTokenBalance, 129 | tradeSize, 130 | timeTillMaturity, 131 | ts, 132 | g1 133 | ) 134 | debugLog(`offChain buyFYToken: ${offChain}`) 135 | debugLog(`onChain buyFYToken: ${onChain}`) 136 | almostEqual(onChain, offChain, PRECISION) 137 | 138 | debugLog() 139 | } 140 | } 141 | } 142 | } 143 | }) 144 | }) 145 | }) 146 | -------------------------------------------------------------------------------- /test/014_yield_math_curve.ts: -------------------------------------------------------------------------------- 1 | import { debugLog } from './shared/helpers' 2 | 3 | import { YieldMathWrapper } from '../typechain/YieldMathWrapper' 4 | import { YieldMath } from '../typechain/YieldMath' 5 | 6 | import { BigNumber } from 'ethers' 7 | 8 | import { ONE64, ts, g1, g2 } from '../src/constants' 9 | 10 | import { ethers } from 'hardhat' 11 | import { solidity } from 'ethereum-waffle' 12 | import { expect, use } from 'chai' 13 | use(solidity) 14 | 15 | const PRECISION = BigNumber.from('100000000000000') // 1e14 16 | 17 | function almostEqual(x: BigNumber, y: BigNumber, p: BigNumber) { 18 | // Check that abs(x - y) < p: 19 | const diff = x.gt(y) ? BigNumber.from(x).sub(y) : BigNumber.from(y).sub(x) // Not sure why I have to convert x and y to BigNumber 20 | expect(diff.div(p)).to.eq(0) // Hack to avoid silly conversions. BigNumber truncates decimals off. 21 | } 22 | 23 | describe('YieldMath - Curve', async function () { 24 | this.timeout(0) 25 | let yieldMathLibrary: YieldMath 26 | let yieldMath: YieldMathWrapper 27 | 28 | const values = [ 29 | ['10000000000000000000000', '1000000000000000000000', '10000000000000000000', '1000000'], 30 | ['100000000000000000000000000', '10000000000000000000000000', '1000000000000000000000', '1000000'], 31 | ['1000000000000000000000000000000', '100000000000000000000000000000', '100000000000000000000000', '1000000'], 32 | ] 33 | const timeTillMaturity = ['0', '40', '4000', '400000', '40000000'] 34 | 35 | before(async () => { 36 | const YieldMathFactory = await ethers.getContractFactory('YieldMath') 37 | yieldMathLibrary = ((await YieldMathFactory.deploy()) as unknown) as YieldMath 38 | await yieldMathLibrary.deployed() 39 | 40 | const YieldMathWrapperFactory = await ethers.getContractFactory('YieldMathWrapper', { 41 | libraries: { 42 | YieldMath: yieldMathLibrary.address, 43 | }, 44 | }) 45 | 46 | yieldMath = ((await YieldMathWrapperFactory.deploy()) as unknown) as YieldMathWrapper 47 | await yieldMath.deployed() 48 | }) 49 | 50 | describe('Test trading functions', async () => { 51 | it('A higher g means more fyToken out with `fyTokenOutForBaseIn`', async () => { 52 | for (var i = 0; i < values.length; i++) { 53 | var baseBalanceValue = values[i][0] 54 | var fyTokenBalanceValue = values[i][1] 55 | var baseAmountValue = values[i][2] 56 | var timeTillMaturityValue = values[i][3] 57 | 58 | var baseBalance = BigNumber.from(baseBalanceValue) 59 | var fyTokenBalance = BigNumber.from(fyTokenBalanceValue) 60 | var baseAmount = BigNumber.from(baseAmountValue) 61 | var timeTillMaturity = BigNumber.from(timeTillMaturityValue) 62 | var g = [ 63 | ['9', '10'], 64 | ['95', '100'], 65 | ['950', '1000'], 66 | ] 67 | var result = BigNumber.from('0') 68 | var previousResult = BigNumber.from('0') 69 | for (var j = 0; j < g.length; j++) { 70 | var g_ = BigNumber.from(g[j][0]).mul(ONE64).div(BigNumber.from(g[j][1])) 71 | result = await yieldMath.fyTokenOutForBaseIn( 72 | baseBalance, 73 | fyTokenBalance, 74 | baseAmount, 75 | timeTillMaturity, 76 | ts, 77 | g_ 78 | ) 79 | } 80 | 81 | expect(result).to.be.gt(previousResult) 82 | previousResult = result 83 | } 84 | }) 85 | 86 | it('As we approach maturity, price grows to 1 for `fyTokenOutForBaseIn`', async () => { 87 | for (var i = 0; i < values.length; i++) { 88 | debugLog('') 89 | var baseBalanceValue = values[i][0] 90 | var fyTokenBalanceValue = values[i][1] 91 | var baseAmountValue = values[i][2] 92 | 93 | var baseBalance = BigNumber.from(baseBalanceValue) 94 | var fyTokenBalance = BigNumber.from(fyTokenBalanceValue) 95 | var baseAmount = BigNumber.from(baseAmountValue) 96 | 97 | const flatFee = BigNumber.from('1000000000000') 98 | const maximum = baseAmount.sub(flatFee) 99 | var result = maximum 100 | var previousResult = maximum 101 | for (var j = 0; j < timeTillMaturity.length; j++) { 102 | var t = timeTillMaturity[j] 103 | 104 | result = await yieldMath.fyTokenOutForBaseIn(baseBalance, fyTokenBalance, baseAmount, t, ts, g1) 105 | 106 | debugLog(' ' + result.toString()) 107 | if (j == 0) { 108 | // Test that when we are very close to maturity, price is very close to 1 minus flat fee. 109 | almostEqual(result, maximum, PRECISION) 110 | } else { 111 | // Easier to test prices diverging from 1 112 | expect(result).to.be.lt(previousResult) 113 | } 114 | previousResult = result 115 | } 116 | } 117 | }) 118 | 119 | it('A lower g means more Base out with `baseOutForFYTokenIn`', async () => { 120 | for (var i = 0; i < values.length; i++) { 121 | var baseBalanceValue = values[i][0] 122 | var fyTokenBalanceValue = values[i][1] 123 | var baseAmountValue = values[i][2] 124 | var timeTillMaturityValue = values[i][3] 125 | 126 | var baseBalance = BigNumber.from(baseBalanceValue) 127 | var fyTokenBalance = BigNumber.from(fyTokenBalanceValue) 128 | var baseAmount = BigNumber.from(baseAmountValue) 129 | var timeTillMaturity = BigNumber.from(timeTillMaturityValue) 130 | 131 | var g = [ 132 | ['950', '1000'], 133 | ['95', '100'], 134 | ['9', '10'], 135 | ] 136 | var result = BigNumber.from('0') 137 | var previousResult = BigNumber.from('0') 138 | for (var j = 0; j < g.length; j++) { 139 | var g_ = BigNumber.from(g[j][0]).mul(ONE64).div(BigNumber.from(g[j][1])) 140 | result = await yieldMath.baseOutForFYTokenIn( 141 | baseBalance, 142 | fyTokenBalance, 143 | baseAmount, 144 | timeTillMaturity, 145 | ts, 146 | g_ 147 | ) 148 | } 149 | 150 | expect(result).to.be.gt(previousResult) 151 | previousResult = result 152 | } 153 | }) 154 | 155 | it('As we approach maturity, price drops to 1 for `baseOutForFYTokenIn`', async () => { 156 | for (var i = 0; i < values.length; i++) { 157 | debugLog('') 158 | var baseBalanceValue = values[i][0] 159 | var fyTokenBalanceValue = values[i][1] 160 | var baseAmountValue = values[i][2] 161 | 162 | var baseBalance = BigNumber.from(baseBalanceValue) 163 | var fyTokenBalance = BigNumber.from(fyTokenBalanceValue) 164 | var baseAmount = BigNumber.from(baseAmountValue) 165 | 166 | const flatFee = BigNumber.from('1000000000000') 167 | const minimum = baseAmount.sub(flatFee) 168 | var result = minimum 169 | var previousResult = minimum 170 | for (var j = 0; j < timeTillMaturity.length; j++) { 171 | var t = timeTillMaturity[j] 172 | result = await yieldMath.baseOutForFYTokenIn(baseBalance, fyTokenBalance, baseAmount, t, ts, g2) 173 | 174 | debugLog(' ' + result.toString()) 175 | if (j == 0) { 176 | // Test that when we are very close to maturity, price is very close to 1 minus flat fee. 177 | almostEqual(result, minimum, PRECISION) 178 | } else { 179 | // Easier to test prices diverging from 1 180 | expect(result).to.be.gt(previousResult) 181 | } 182 | previousResult = result 183 | } 184 | } 185 | }) 186 | 187 | it('A higher g means more fyToken in with `fyTokenInForBaseOut`', async () => { 188 | for (var i = 0; i < values.length; i++) { 189 | var baseBalanceValue = values[i][0] 190 | var fyTokenBalanceValue = values[i][1] 191 | var baseAmountValue = values[i][2] 192 | var timeTillMaturityValue = values[i][3] 193 | 194 | var baseBalance = BigNumber.from(baseBalanceValue) 195 | var fyTokenBalance = BigNumber.from(fyTokenBalanceValue) 196 | var baseAmount = BigNumber.from(baseAmountValue) 197 | var timeTillMaturity = BigNumber.from(timeTillMaturityValue) 198 | 199 | var g = [ 200 | ['9', '10'], 201 | ['95', '100'], 202 | ['950', '1000'], 203 | ] 204 | var result = BigNumber.from('0') 205 | var previousResult = BigNumber.from('0') 206 | for (var j = 0; j < g.length; j++) { 207 | var g_ = BigNumber.from(g[j][0]).mul(ONE64).div(BigNumber.from(g[j][1])) 208 | result = await yieldMath.fyTokenInForBaseOut( 209 | baseBalance, 210 | fyTokenBalance, 211 | baseAmount, 212 | timeTillMaturity, 213 | ts, 214 | g_ 215 | ) 216 | } 217 | 218 | expect(result).to.be.gt(previousResult) 219 | previousResult = result 220 | } 221 | }) 222 | 223 | it('As we approach maturity, price grows to 1 for `fyTokenInForBaseOut`', async () => { 224 | for (var i = 0; i < values.length; i++) { 225 | debugLog('') 226 | var baseBalanceValue = values[i][0] 227 | var fyTokenBalanceValue = values[i][1] 228 | var baseAmountValue = values[i][2] 229 | 230 | var baseBalance = BigNumber.from(baseBalanceValue) 231 | var fyTokenBalance = BigNumber.from(fyTokenBalanceValue) 232 | var baseAmount = BigNumber.from(baseAmountValue) 233 | 234 | const flatFee = BigNumber.from('1000000000000') 235 | const maximum = baseAmount.add(flatFee) 236 | var result = maximum 237 | var previousResult = maximum 238 | for (var j = 0; j < timeTillMaturity.length; j++) { 239 | var t = timeTillMaturity[j] 240 | result = await yieldMath.fyTokenInForBaseOut(baseBalance, fyTokenBalance, baseAmount, t, ts, g2) 241 | 242 | debugLog(' ' + result.toString()) 243 | if (j == 0) { 244 | // Test that when we are very close to maturity, price is very close to 1 plus flat fee. 245 | almostEqual(result, maximum, PRECISION) 246 | } else { 247 | // Easier to test prices diverging from 1 248 | expect(result).to.be.lt(previousResult) 249 | } 250 | previousResult = result 251 | } 252 | } 253 | }) 254 | 255 | it('A lower g means more Base in with `baseInForFYTokenOut`', async () => { 256 | for (var i = 0; i < values.length; i++) { 257 | var baseBalanceValue = values[i][0] 258 | var fyTokenBalanceValue = values[i][1] 259 | var baseAmountValue = values[i][2] 260 | var timeTillMaturityValue = values[i][3] 261 | 262 | var baseBalance = BigNumber.from(baseBalanceValue) 263 | var fyTokenBalance = BigNumber.from(fyTokenBalanceValue) 264 | var baseAmount = BigNumber.from(baseAmountValue) 265 | var timeTillMaturity = BigNumber.from(timeTillMaturityValue) 266 | 267 | var g = [ 268 | ['950', '1000'], 269 | ['95', '100'], 270 | ['9', '10'], 271 | ] 272 | var result = BigNumber.from('0') 273 | var previousResult = BigNumber.from('0') 274 | for (var j = 0; j < g.length; j++) { 275 | var g_ = BigNumber.from(g[j][0]).mul(ONE64).div(BigNumber.from(g[j][1])) 276 | result = await yieldMath.baseInForFYTokenOut( 277 | baseBalance, 278 | fyTokenBalance, 279 | baseAmount, 280 | timeTillMaturity, 281 | ts, 282 | g_ 283 | ) 284 | } 285 | 286 | expect(result).to.be.gt(previousResult) 287 | previousResult = result 288 | } 289 | }) 290 | 291 | it('As we approach maturity, price drops to 1 for `baseInForFYTokenOut`', async () => { 292 | for (var i = 0; i < values.length; i++) { 293 | debugLog('') 294 | var baseBalanceValue = values[i][0] 295 | var fyTokenBalanceValue = values[i][1] 296 | var baseAmountValue = values[i][2] 297 | 298 | var baseBalance = BigNumber.from(baseBalanceValue) 299 | var fyTokenBalance = BigNumber.from(fyTokenBalanceValue) 300 | var baseAmount = BigNumber.from(baseAmountValue) 301 | 302 | const flatFee = BigNumber.from('1000000000000') 303 | const minimum = baseAmount.add(flatFee) 304 | var result = minimum 305 | var previousResult = minimum 306 | for (var j = 0; j < timeTillMaturity.length; j++) { 307 | var t = timeTillMaturity[j] 308 | result = await yieldMath.baseInForFYTokenOut(baseBalance, fyTokenBalance, baseAmount, t, ts, g1) 309 | 310 | debugLog(' ' + result.toString()) 311 | if (j == 0) { 312 | // Test that when we are very close to maturity, price is very close to 1 plus flat fee. 313 | almostEqual(result, minimum, PRECISION) 314 | } else { 315 | // Easier to test prices diverging from 1 316 | expect(result).to.be.gt(previousResult) 317 | } 318 | previousResult = result 319 | } 320 | } 321 | }) 322 | }) 323 | }) 324 | -------------------------------------------------------------------------------- /test/031_pool_mint.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address' 2 | 3 | import { constants } from '@yield-protocol/utils-v2' 4 | const { WAD, MAX256 } = constants 5 | const MAX = MAX256 6 | 7 | import { Pool } from '../typechain/Pool' 8 | import { BaseMock as Base } from '../typechain/BaseMock' 9 | import { FYTokenMock as FYToken } from '../typechain/FYTokenMock' 10 | import { YieldSpaceEnvironment } from './shared/fixtures' 11 | 12 | import { PoolEstimator } from './shared/poolEstimator' 13 | import { BigNumber } from 'ethers' 14 | 15 | import { ethers, waffle } from 'hardhat' 16 | import { expect } from 'chai' 17 | const { loadFixture } = waffle 18 | 19 | const ZERO_ADDRESS = '0x' + '00'.repeat(20) 20 | 21 | function almostEqual(x: BigNumber, y: BigNumber, p: BigNumber) { 22 | // Check that abs(x - y) < p: 23 | const diff = x.gt(y) ? BigNumber.from(x).sub(y) : BigNumber.from(y).sub(x) // Not sure why I have to convert x and y to BigNumber 24 | expect(diff.div(p)).to.eq(0) // Hack to avoid silly conversions. BigNumber truncates decimals off. 25 | } 26 | 27 | describe('Pool - mint', async function () { 28 | this.timeout(0) 29 | 30 | // These values impact the pool results 31 | const bases = WAD.mul(1000000) 32 | const fyTokens = bases 33 | const initialBase = bases 34 | const OVERRIDES = { gasLimit: 1_000_000 } 35 | 36 | let ownerAcc: SignerWithAddress 37 | let user1Acc: SignerWithAddress 38 | let user2Acc: SignerWithAddress 39 | let user3Acc: SignerWithAddress 40 | let owner: string 41 | let user1: string 42 | let user2: string 43 | let user3: string 44 | 45 | let yieldSpace: YieldSpaceEnvironment 46 | 47 | let pool: Pool 48 | let poolEstimator: PoolEstimator 49 | 50 | let base: Base 51 | let fyToken: FYToken 52 | let maturity: BigNumber 53 | 54 | const baseId = ethers.utils.hexlify(ethers.utils.randomBytes(6)) 55 | const maturityId = '3M' 56 | const fyTokenId = baseId + '-' + maturityId 57 | 58 | async function fixture() { 59 | return await YieldSpaceEnvironment.setup(ownerAcc, [baseId], [maturityId], BigNumber.from('0')) 60 | } 61 | 62 | before(async () => { 63 | const signers = await ethers.getSigners() 64 | ownerAcc = signers[0] 65 | owner = ownerAcc.address 66 | user1Acc = signers[1] 67 | user1 = user1Acc.address 68 | user2Acc = signers[2] 69 | user2 = user2Acc.address 70 | user3Acc = signers[3] 71 | user3 = user3Acc.address 72 | }) 73 | 74 | beforeEach(async () => { 75 | yieldSpace = await loadFixture(fixture) 76 | base = yieldSpace.bases.get(baseId) as Base 77 | fyToken = yieldSpace.fyTokens.get(fyTokenId) as FYToken 78 | 79 | // Deploy a fresh pool so that we can test initialization 80 | pool = ((yieldSpace.pools.get(baseId) as Map).get(fyTokenId) as Pool).connect(user1Acc) 81 | poolEstimator = await PoolEstimator.setup(pool) 82 | 83 | maturity = BigNumber.from(await fyToken.maturity()) 84 | }) 85 | 86 | it('adds initial liquidity', async () => { 87 | await base.mint(pool.address, initialBase) 88 | 89 | await expect(pool.mint(user2, user2, 0, MAX)) 90 | .to.emit(pool, 'Liquidity') 91 | .withArgs(maturity, user1, user2, ZERO_ADDRESS, initialBase.mul(-1), 0, initialBase) 92 | 93 | expect(await pool.balanceOf(user2)).to.equal(initialBase, 'User2 should have ' + initialBase + ' liquidity tokens') 94 | 95 | expect((await pool.getCache())[0]).to.equal(await pool.getBaseBalance()) 96 | expect((await pool.getCache())[1]).to.equal(await pool.getFYTokenBalance()) 97 | }) 98 | 99 | it('adds liquidity with zero fyToken', async () => { 100 | await base.mint(pool.address, initialBase) 101 | await pool.mint(ZERO_ADDRESS, ZERO_ADDRESS, 0, MAX) 102 | 103 | // After initializing, donate base and sync to simulate having reached zero fyToken through trading 104 | await base.mint(pool.address, initialBase) 105 | await pool.sync() 106 | 107 | await base.mint(pool.address, initialBase) 108 | await expect(pool.mint(user2, user2, 0, MAX)) 109 | .to.emit(pool, 'Liquidity') 110 | .withArgs(maturity, user1, user2, ZERO_ADDRESS, initialBase.mul(-1), 0, initialBase.div(2)) 111 | 112 | // The user got as minted tokens half of the amount he supplied as base, because supply doesn't equal base in the pool anymore 113 | expect(await pool.balanceOf(user2)).to.equal( 114 | initialBase.div(2), 115 | 'User2 should have ' + initialBase.div(2) + ' liquidity tokens' 116 | ) 117 | 118 | expect((await pool.getCache())[0]).to.equal(await pool.getBaseBalance()) 119 | expect((await pool.getCache())[1]).to.equal(await pool.getFYTokenBalance()) 120 | }) 121 | 122 | it('syncs balances after donations', async () => { 123 | await base.mint(pool.address, initialBase) 124 | await fyToken.mint(pool.address, initialBase.div(9)) 125 | 126 | await expect(pool.sync()).to.emit(pool, 'Sync') 127 | 128 | expect((await pool.getCache())[0]).to.equal(await pool.getBaseBalance()) 129 | expect((await pool.getCache())[1]).to.equal(await pool.getFYTokenBalance()) 130 | }) 131 | 132 | describe('with initial liquidity', () => { 133 | beforeEach(async () => { 134 | await base.mint(pool.address, initialBase) 135 | await pool.mint(user1, user2, 0, MAX) 136 | 137 | const additionalFYToken = initialBase.div(9) 138 | // Skew the balances without using trading functions 139 | await fyToken.mint(pool.address, additionalFYToken) 140 | await pool.sync() 141 | }) 142 | 143 | it('mints liquidity tokens, returning base surplus', async () => { 144 | const fyTokenIn = WAD 145 | 146 | const [expectedMinted, expectedBaseIn] = await poolEstimator.mint(fyTokenIn) 147 | 148 | const baseTokensBefore = await base.balanceOf(user2) 149 | const poolTokensBefore = await pool.balanceOf(user2) 150 | 151 | await base.mint(pool.address, expectedBaseIn.add(WAD)) 152 | await fyToken.mint(pool.address, fyTokenIn) 153 | await expect(pool.mint(user2, user2, 0, MAX)) 154 | .to.emit(pool, 'Liquidity') 155 | .withArgs(maturity, user1, user2, ZERO_ADDRESS, expectedBaseIn.mul(-1), fyTokenIn.mul(-1), expectedMinted) 156 | 157 | const minted = (await pool.balanceOf(user2)).sub(poolTokensBefore) 158 | 159 | almostEqual(minted, expectedMinted, fyTokenIn.div(10000)) 160 | almostEqual(await base.balanceOf(user2), WAD, fyTokenIn.div(10000)) 161 | expect((await pool.getCache())[0]).to.equal(await pool.getBaseBalance()) 162 | expect((await pool.getCache())[1]).to.equal(await pool.getFYTokenBalance()) 163 | }) 164 | 165 | it('mints liquidity tokens with base only', async () => { 166 | const fyTokenToBuy = WAD.div(1000) 167 | 168 | const [expectedMinted, expectedBaseIn] = await poolEstimator.mintWithBase(fyTokenToBuy) 169 | 170 | const poolTokensBefore = await pool.balanceOf(user2) 171 | const poolSupplyBefore = await pool.totalSupply() 172 | const baseCachedBefore = (await pool.getCache())[0] 173 | const fyTokenCachedBefore = (await pool.getCache())[1] 174 | 175 | await base.mint(pool.address, expectedBaseIn) 176 | 177 | await expect(pool.mintWithBase(user2, user2, fyTokenToBuy, 0, MAX, OVERRIDES)) 178 | .to.emit(pool, 'Liquidity') 179 | .withArgs( 180 | maturity, 181 | user1, 182 | user2, 183 | ZERO_ADDRESS, 184 | (await pool.getCache())[0].sub(baseCachedBefore).mul(-1), 185 | 0, 186 | (await pool.totalSupply()).sub(poolSupplyBefore) 187 | ) 188 | .to.emit(base, 'Transfer') 189 | .withArgs(pool.address, user2, await base.balanceOf(user2)) // Surplus base is given to the receiver of LP tokens 190 | 191 | const baseIn = (await pool.getCache())[0].sub(baseCachedBefore) 192 | const minted = (await pool.balanceOf(user2)).sub(poolTokensBefore) 193 | 194 | almostEqual(minted, expectedMinted, minted.div(10000)) 195 | 196 | almostEqual(baseIn, expectedBaseIn, baseIn.div(10000)) 197 | expect((await pool.getCache())[0]).to.equal(baseCachedBefore.add(baseIn)) 198 | expect((await pool.getCache())[1]).to.equal(fyTokenCachedBefore.add(minted)) 199 | }) 200 | 201 | it("doesn't mint if ratio drops", async () => { 202 | const fyTokenToBuy = WAD.div(1000) 203 | await base.mint(pool.address, WAD) 204 | const minRatio = WAD.mul(await base.balanceOf(pool.address)).div(await fyToken.balanceOf(pool.address)) 205 | await fyToken.mint(pool.address, WAD) 206 | await pool.sync() 207 | await expect(pool.mintWithBase(user2, user2, fyTokenToBuy, minRatio, MAX, OVERRIDES)).to.be.revertedWith( 208 | 'Pool: Reserves ratio changed' 209 | ) 210 | }) 211 | 212 | it("doesn't mint if ratio rises", async () => { 213 | const fyTokenToBuy = WAD.div(1000) 214 | await base.mint(pool.address, WAD) 215 | const maxRatio = WAD.mul(await base.balanceOf(pool.address)).div(await fyToken.balanceOf(pool.address)) 216 | await base.mint(pool.address, WAD) 217 | await pool.sync() 218 | await expect(pool.mintWithBase(user2, user2, fyTokenToBuy, 0, maxRatio, OVERRIDES)).to.be.revertedWith( 219 | 'Pool: Reserves ratio changed' 220 | ) 221 | }) 222 | 223 | it('burns liquidity tokens', async () => { 224 | const baseBalance = await base.balanceOf(pool.address) 225 | const fyTokenBalance = await fyToken.balanceOf(pool.address) 226 | const lpTokensIn = WAD 227 | 228 | const [expectedBaseOut, expectedFYTokenOut] = await poolEstimator.burn(lpTokensIn) 229 | 230 | await pool.transfer(pool.address, lpTokensIn) 231 | await expect(pool.burn(user2, user3, 0, MAX)) 232 | .to.emit(pool, 'Liquidity') 233 | .withArgs( 234 | maturity, 235 | user1, 236 | user2, 237 | user3, 238 | baseBalance.sub(await base.balanceOf(pool.address)), 239 | fyTokenBalance.sub(await fyToken.balanceOf(pool.address)), 240 | lpTokensIn.mul(-1) 241 | ) 242 | 243 | const baseOut = baseBalance.sub(await base.balanceOf(pool.address)) 244 | const fyTokenOut = fyTokenBalance.sub(await fyToken.balanceOf(pool.address)) 245 | 246 | almostEqual(baseOut, expectedBaseOut, baseOut.div(10000)) 247 | almostEqual(fyTokenOut, expectedFYTokenOut, fyTokenOut.div(10000)) 248 | expect((await pool.getCache())[0]).to.equal(await pool.getBaseBalance()) 249 | expect((await pool.getCache())[1]).to.equal(await pool.getFYTokenBalance()) 250 | 251 | expect(await base.balanceOf(user2)).to.equal(baseOut) 252 | expect(await fyToken.balanceOf(user3)).to.equal(fyTokenOut) 253 | }) 254 | 255 | it('burns liquidity tokens to Base', async () => { 256 | const baseBalance = await base.balanceOf(pool.address) 257 | const lpTokensIn = WAD.mul(2) 258 | 259 | const expectedBaseOut = await poolEstimator.burnForBase(lpTokensIn) 260 | 261 | await pool.transfer(pool.address, lpTokensIn) 262 | await expect(pool.burnForBase(user2, 0, MAX, OVERRIDES)) 263 | .to.emit(pool, 'Liquidity') 264 | .withArgs( 265 | maturity, 266 | user1, 267 | user2, 268 | ZERO_ADDRESS, 269 | baseBalance.sub(await base.balanceOf(pool.address)), 270 | 0, 271 | lpTokensIn.mul(-1) 272 | ) 273 | 274 | const baseOut = baseBalance.sub(await base.balanceOf(pool.address)) 275 | 276 | almostEqual(baseOut, expectedBaseOut, baseOut.div(10000)) 277 | expect((await pool.getCache())[0]).to.equal(await pool.getBaseBalance()) 278 | expect((await pool.getCache())[1]).to.equal(await pool.getFYTokenBalance()) 279 | }) 280 | 281 | it("doesn't burn if ratio drops", async () => { 282 | const lpTokensIn = WAD.mul(2) 283 | await pool.transfer(pool.address, lpTokensIn) 284 | const minRatio = WAD.mul(await base.balanceOf(pool.address)).div(await fyToken.balanceOf(pool.address)) 285 | await fyToken.mint(pool.address, WAD) 286 | await pool.sync() 287 | await expect(pool.burnForBase(user2, minRatio, MAX, OVERRIDES)).to.be.revertedWith('Pool: Reserves ratio changed') 288 | }) 289 | 290 | it("doesn't burn if ratio rises", async () => { 291 | const lpTokensIn = WAD.mul(2) 292 | await pool.transfer(pool.address, lpTokensIn) 293 | const maxRatio = WAD.mul(await base.balanceOf(pool.address)).div(await fyToken.balanceOf(pool.address)) 294 | await base.mint(pool.address, WAD) 295 | await pool.sync() 296 | await expect(pool.burnForBase(user2, 0, maxRatio, OVERRIDES)).to.be.revertedWith('Pool: Reserves ratio changed') 297 | }) 298 | }) 299 | }) 300 | -------------------------------------------------------------------------------- /test/032_pool_trade.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address' 2 | 3 | import { constants } from '@yield-protocol/utils-v2' 4 | const { WAD, MAX128 } = constants 5 | const MAX = MAX128 6 | 7 | import { PoolEstimator } from './shared/poolEstimator' 8 | import { Pool } from '../typechain/Pool' 9 | import { BaseMock as Base } from '../typechain/BaseMock' 10 | import { FYTokenMock as FYToken } from '../typechain/FYTokenMock' 11 | import { YieldSpaceEnvironment } from './shared/fixtures' 12 | 13 | import { BigNumber } from 'ethers' 14 | 15 | import { ethers, waffle } from 'hardhat' 16 | import { expect } from 'chai' 17 | const { loadFixture } = waffle 18 | 19 | function almostEqual(x: BigNumber, y: BigNumber, p: BigNumber) { 20 | // Check that abs(x - y) < p: 21 | const diff = x.gt(y) ? BigNumber.from(x).sub(y) : BigNumber.from(y).sub(x) // Not sure why I have to convert x and y to BigNumber 22 | expect(diff.div(p)).to.eq(0) // Hack to avoid silly conversions. BigNumber truncates decimals off. 23 | } 24 | 25 | describe('Pool - trade', async function () { 26 | this.timeout(0) 27 | 28 | const bases = WAD.mul(1000000) 29 | const fyTokens = bases 30 | const initialBase = bases 31 | const OVERRIDES = { gasLimit: 1_000_000 } 32 | 33 | let ownerAcc: SignerWithAddress 34 | let user1Acc: SignerWithAddress 35 | let user2Acc: SignerWithAddress 36 | let owner: string 37 | let user1: string 38 | let user2: string 39 | 40 | let yieldSpace: YieldSpaceEnvironment 41 | let poolEstimator: PoolEstimator 42 | let pool: Pool 43 | let base: Base 44 | let fyToken: FYToken 45 | let maturity: BigNumber 46 | 47 | const baseId = ethers.utils.hexlify(ethers.utils.randomBytes(6)) 48 | const maturityId = '3M' 49 | const fyTokenId = baseId + '-' + maturityId 50 | 51 | async function fixture() { 52 | return await YieldSpaceEnvironment.setup(ownerAcc, [baseId], [maturityId], initialBase) 53 | } 54 | 55 | before(async () => { 56 | const signers = await ethers.getSigners() 57 | ownerAcc = signers[0] 58 | owner = ownerAcc.address 59 | user1Acc = signers[1] 60 | user1 = user1Acc.address 61 | user2Acc = signers[2] 62 | user2 = user2Acc.address 63 | }) 64 | 65 | beforeEach(async () => { 66 | yieldSpace = await loadFixture(fixture) 67 | base = yieldSpace.bases.get(baseId) as Base 68 | fyToken = yieldSpace.fyTokens.get(fyTokenId) as FYToken 69 | pool = ((yieldSpace.pools.get(baseId) as Map).get(fyTokenId) as Pool).connect(user1Acc) 70 | poolEstimator = await PoolEstimator.setup(pool) 71 | maturity = BigNumber.from(await pool.maturity()) 72 | }) 73 | 74 | it('sells fyToken', async () => { 75 | const fyTokenIn = WAD 76 | const userBaseBefore = await base.balanceOf(user2) 77 | 78 | // Transfer fyToken for sale to the pool 79 | await fyToken.mint(pool.address, fyTokenIn) 80 | 81 | const baseOutPreview = await pool.sellFYTokenPreview(fyTokenIn) 82 | const expectedBaseOut = await poolEstimator.sellFYToken() 83 | 84 | await expect(pool.sellFYToken(user2, 0)) 85 | .to.emit(pool, 'Trade') 86 | .withArgs(maturity, user1, user2, await base.balanceOf(user2), fyTokenIn.mul(-1)) 87 | 88 | const baseOut = (await base.balanceOf(user2)).sub(userBaseBefore) 89 | 90 | almostEqual(baseOut, expectedBaseOut, fyTokenIn.div(1000000)) 91 | almostEqual(baseOutPreview, expectedBaseOut, fyTokenIn.div(1000000)) 92 | expect((await pool.getCache())[0]).to.equal(await pool.getBaseBalance()) 93 | expect((await pool.getCache())[1]).to.equal(await pool.getFYTokenBalance()) 94 | }) 95 | 96 | it('does not sell fyToken beyond slippage', async () => { 97 | const fyTokenIn = WAD 98 | 99 | await fyToken.mint(pool.address, fyTokenIn) 100 | await expect(pool.sellFYToken(user2, MAX, OVERRIDES)).to.be.revertedWith('Pool: Not enough base obtained') 101 | }) 102 | 103 | it('donates base and sells fyToken', async () => { 104 | const baseDonation = WAD 105 | const fyTokenIn = WAD 106 | 107 | await base.mint(pool.address, baseDonation) 108 | await fyToken.mint(pool.address, fyTokenIn) 109 | 110 | await pool.sellFYToken(user2, 0) 111 | 112 | expect((await pool.getCache())[0]).to.equal(await pool.getBaseBalance()) 113 | expect((await pool.getCache())[1]).to.equal(await pool.getFYTokenBalance()) 114 | }) 115 | 116 | it('buys base', async () => { 117 | const fyTokenCachedBefore = (await pool.getCache())[1] 118 | const userBaseBefore = await base.balanceOf(user2) 119 | const baseOut = WAD 120 | 121 | const fyTokenInPreview = await pool.buyBasePreview(baseOut) 122 | const expectedFYTokenIn = await poolEstimator.buyBase(baseOut) 123 | 124 | await fyToken.mint(pool.address, fyTokens) 125 | 126 | await expect(pool.buyBase(user2, baseOut, MAX, OVERRIDES)) 127 | .to.emit(pool, 'Trade') 128 | .withArgs(maturity, user1, user2, baseOut, (await pool.getCache())[1].sub(fyTokenCachedBefore).mul(-1)) 129 | 130 | const fyTokenCachedCurrent = (await pool.getCache())[1] 131 | const fyTokenIn = fyTokenCachedCurrent.sub(fyTokenCachedBefore) 132 | const fyTokenChange = (await pool.getFYTokenBalance()).sub(fyTokenCachedCurrent) 133 | 134 | expect(await base.balanceOf(user2)).to.equal( 135 | userBaseBefore.add(baseOut), 136 | 'Receiver account should have 1 base token' 137 | ) 138 | 139 | almostEqual(fyTokenIn, expectedFYTokenIn, baseOut.div(1000000)) 140 | almostEqual(fyTokenInPreview, expectedFYTokenIn, baseOut.div(1000000)) 141 | expect((await pool.getCache())[0]).to.equal(await pool.getBaseBalance()) 142 | expect((await pool.getCache())[1].add(fyTokenChange)).to.equal(await pool.getFYTokenBalance()) 143 | }) 144 | 145 | it('does not buy base beyond slippage', async () => { 146 | const baseOut = WAD 147 | 148 | await fyToken.mint(pool.address, fyTokens) 149 | await expect(pool.buyBase(user2, baseOut, 0, OVERRIDES)).to.be.revertedWith('Pool: Too much fyToken in') 150 | }) 151 | 152 | it('buys base and retrieves change', async () => { 153 | const userBaseBefore = await base.balanceOf(user2) 154 | const baseOut = WAD 155 | 156 | const expectedFYTokenIn = await poolEstimator.buyBase(baseOut) 157 | 158 | await fyToken.mint(pool.address, fyTokens) 159 | 160 | await pool.buyBase(user2, baseOut, MAX, OVERRIDES) 161 | 162 | const fyTokenCachedCurrent = (await pool.getCache())[1] 163 | const fyTokenChange = (await pool.getFYTokenBalance()).sub(fyTokenCachedCurrent) 164 | 165 | expect(await base.balanceOf(user2)).to.equal( 166 | userBaseBefore.add(baseOut), 167 | 'Receiver account should have 1 base token' 168 | ) 169 | almostEqual(fyTokenChange, fyTokens.sub(expectedFYTokenIn), baseOut.div(1000000)) 170 | 171 | expect((await pool.getCache())[0]).to.equal(await pool.getBaseBalance()) 172 | expect((await pool.getCache())[1].add(fyTokenChange)).to.equal(await pool.getFYTokenBalance()) 173 | 174 | await expect(pool.retrieveFYToken(user1)).to.emit(fyToken, 'Transfer').withArgs(pool.address, user1, fyTokenChange) 175 | 176 | expect(await fyToken.balanceOf(user1)).to.equal(fyTokenChange) 177 | }) 178 | 179 | it('donates fyToken and buys base', async () => { 180 | const baseBalances = await pool.getBaseBalance() 181 | const fyTokenBalances = await pool.getFYTokenBalance() 182 | const fyTokenCachedBefore = (await pool.getCache())[1] 183 | 184 | const baseOut = WAD 185 | const fyTokenDonation = WAD 186 | 187 | await fyToken.mint(pool.address, fyTokens.add(fyTokenDonation)) 188 | 189 | await pool.buyBase(user2, baseOut, MAX, OVERRIDES) 190 | 191 | const fyTokenCachedCurrent = (await pool.getCache())[1] 192 | const fyTokenIn = fyTokenCachedCurrent.sub(fyTokenCachedBefore) 193 | 194 | expect((await pool.getCache())[0]).to.equal(baseBalances.sub(baseOut)) 195 | expect((await pool.getCache())[1]).to.equal(fyTokenBalances.add(fyTokenIn)) 196 | }) 197 | 198 | describe('with extra fyToken', () => { 199 | beforeEach(async () => { 200 | const additionalFYToken = WAD.mul(30) 201 | await fyToken.mint(owner, additionalFYToken) 202 | await fyToken.transfer(pool.address, additionalFYToken) 203 | await pool.sellFYToken(owner, 0) 204 | }) 205 | 206 | it('sells base', async () => { 207 | const baseIn = WAD 208 | const userFYTokenBefore = await fyToken.balanceOf(user2) 209 | 210 | // Transfer base for sale to the pool 211 | await base.mint(pool.address, baseIn) 212 | 213 | const fyTokenOutPreview = await pool.sellBasePreview(baseIn) 214 | const expectedFYTokenOut = await poolEstimator.sellBase() 215 | 216 | await expect(pool.sellBase(user2, 0, OVERRIDES)) 217 | .to.emit(pool, 'Trade') 218 | .withArgs(maturity, user1, user2, baseIn.mul(-1), await fyToken.balanceOf(user2)) 219 | 220 | const fyTokenOut = (await fyToken.balanceOf(user2)).sub(userFYTokenBefore) 221 | 222 | expect(await base.balanceOf(user1)).to.equal(0, "'From' wallet should have no base tokens") 223 | 224 | almostEqual(fyTokenOut, expectedFYTokenOut, baseIn.div(1000000)) 225 | almostEqual(fyTokenOutPreview, expectedFYTokenOut, baseIn.div(1000000)) 226 | expect((await pool.getCache())[0]).to.equal(await pool.getBaseBalance()) 227 | expect((await pool.getCache())[1]).to.equal(await pool.getFYTokenBalance()) 228 | }) 229 | 230 | it('does not sell base beyond slippage', async () => { 231 | const baseIn = WAD 232 | 233 | await base.mint(pool.address, baseIn) 234 | await expect(pool.sellBase(user2, MAX, OVERRIDES)).to.be.revertedWith('Pool: Not enough fyToken obtained') 235 | }) 236 | 237 | it('donates fyToken and sells base', async () => { 238 | const baseIn = WAD 239 | const fyTokenDonation = WAD 240 | 241 | await fyToken.mint(pool.address, fyTokenDonation) 242 | await base.mint(pool.address, baseIn) 243 | 244 | await pool.sellBase(user2, 0, OVERRIDES) 245 | 246 | expect((await pool.getCache())[0]).to.equal(await pool.getBaseBalance()) 247 | expect((await pool.getCache())[1]).to.equal(await pool.getFYTokenBalance()) 248 | }) 249 | 250 | it('buys fyToken', async () => { 251 | const baseCachedBefore = (await pool.getCache())[0] 252 | const userFYTokenBefore = await fyToken.balanceOf(user2) 253 | const fyTokenOut = WAD 254 | 255 | const baseInPreview = await pool.buyFYTokenPreview(fyTokenOut) 256 | const expectedBaseIn = await poolEstimator.buyFYToken(fyTokenOut) 257 | 258 | await base.mint(pool.address, bases) 259 | 260 | await expect(pool.buyFYToken(user2, fyTokenOut, MAX, OVERRIDES)) 261 | .to.emit(pool, 'Trade') 262 | .withArgs(maturity, user1, user2, (await pool.getCache())[0].sub(baseCachedBefore).mul(-1), fyTokenOut) 263 | 264 | const baseCachedCurrent = (await pool.getCache())[0] 265 | const baseIn = baseCachedCurrent.sub(baseCachedBefore) 266 | const baseChange = (await pool.getBaseBalance()).sub(baseCachedCurrent) 267 | 268 | expect(await fyToken.balanceOf(user2)).to.equal( 269 | userFYTokenBefore.add(fyTokenOut), 270 | "'User2' wallet should have 1 fyToken token" 271 | ) 272 | 273 | almostEqual(baseIn, expectedBaseIn, baseIn.div(1000000)) 274 | almostEqual(baseInPreview, expectedBaseIn, baseIn.div(1000000)) 275 | expect((await pool.getCache())[0].add(baseChange)).to.equal(await pool.getBaseBalance()) 276 | expect((await pool.getCache())[1]).to.equal(await pool.getFYTokenBalance()) 277 | }) 278 | 279 | it('does not buy fyToken beyond slippage', async () => { 280 | const fyTokenOut = WAD 281 | 282 | await base.mint(pool.address, bases) 283 | await expect(pool.buyFYToken(user2, fyTokenOut, 0, OVERRIDES)).to.be.revertedWith('Pool: Too much base token in') 284 | }) 285 | 286 | it('buys fyToken and retrieves change', async () => { 287 | const fyTokenOut = WAD 288 | 289 | await base.mint(pool.address, bases) 290 | 291 | await pool.buyFYToken(user2, fyTokenOut, MAX, OVERRIDES) 292 | 293 | const baseCachedCurrent = (await pool.getCache())[0] 294 | const baseChange = (await pool.getBaseBalance()).sub(baseCachedCurrent) 295 | 296 | expect((await pool.getCache())[0].add(baseChange)).to.equal(await pool.getBaseBalance()) 297 | expect((await pool.getCache())[1]).to.equal(await pool.getFYTokenBalance()) 298 | 299 | await expect(pool.retrieveBase(user1)).to.emit(base, 'Transfer').withArgs(pool.address, user1, baseChange) 300 | 301 | expect(await base.balanceOf(user1)).to.equal(baseChange) 302 | }) 303 | 304 | it('donates base and buys fyToken', async () => { 305 | const baseBalances = await pool.getBaseBalance() 306 | const fyTokenBalances = await pool.getFYTokenBalance() 307 | const baseCachedBefore = (await pool.getCache())[0] 308 | 309 | const fyTokenOut = WAD 310 | const baseDonation = WAD 311 | 312 | await base.mint(pool.address, bases.add(baseDonation)) 313 | 314 | await pool.buyFYToken(user2, fyTokenOut, MAX, OVERRIDES) 315 | 316 | const baseCachedCurrent = (await pool.getCache())[0] 317 | const baseIn = baseCachedCurrent.sub(baseCachedBefore) 318 | 319 | expect((await pool.getCache())[0]).to.equal(baseBalances.add(baseIn)) 320 | expect((await pool.getCache())[1]).to.equal(fyTokenBalances.sub(fyTokenOut)) 321 | }) 322 | 323 | describe('once mature', () => { 324 | beforeEach(async () => { 325 | await ethers.provider.send('evm_mine', [await pool.maturity()]) 326 | }) 327 | 328 | it("doesn't allow sellBase", async () => { 329 | await expect(pool.sellBasePreview(WAD)).to.be.revertedWith('Pool: Too late') 330 | await expect(pool.sellBase(user1, 0)).to.be.revertedWith('Pool: Too late') 331 | }) 332 | 333 | it("doesn't allow buyBase", async () => { 334 | await expect(pool.buyBasePreview(WAD)).to.be.revertedWith('Pool: Too late') 335 | await expect(pool.buyBase(user1, WAD, MAX)).to.be.revertedWith('Pool: Too late') 336 | }) 337 | 338 | it("doesn't allow sellFYToken", async () => { 339 | await expect(pool.sellFYTokenPreview(WAD)).to.be.revertedWith('Pool: Too late') 340 | await expect(pool.sellFYToken(user1, 0)).to.be.revertedWith('Pool: Too late') 341 | }) 342 | 343 | it("doesn't allow buyFYToken", async () => { 344 | await expect(pool.buyFYTokenPreview(WAD)).to.be.revertedWith('Pool: Too late') 345 | await expect(pool.buyFYToken(user1, WAD, MAX)).to.be.revertedWith('Pool: Too late') 346 | }) 347 | }) 348 | }) 349 | }) 350 | -------------------------------------------------------------------------------- /test/033_pool_usdc.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address' 2 | 3 | import { constants } from '@yield-protocol/utils-v2' 4 | const { MAX128, USDC } = constants 5 | const MAX = MAX128 6 | 7 | import { PoolEstimator } from './shared/poolEstimator' 8 | import { Pool } from '../typechain' 9 | import { BaseMock as Base } from '../typechain/BaseMock' 10 | import { FYTokenMock as FYToken } from '../typechain/FYTokenMock' 11 | import { YieldSpaceEnvironment } from './shared/fixtures' 12 | 13 | import { BigNumber } from 'ethers' 14 | 15 | import { ethers, waffle } from 'hardhat' 16 | import { expect } from 'chai' 17 | const { loadFixture } = waffle 18 | 19 | const ZERO_ADDRESS = '0x' + '00'.repeat(20) 20 | 21 | function almostEqual(x: BigNumber, y: BigNumber, p: BigNumber) { 22 | // Check that abs(x - y) < p: 23 | const diff = x.gt(y) ? BigNumber.from(x).sub(y) : BigNumber.from(y).sub(x) // Not sure why I have to convert x and y to BigNumber 24 | expect(diff.div(p)).to.eq(0) // Hack to avoid silly conversions. BigNumber truncates decimals off. 25 | } 26 | 27 | describe('Pool - usdc', async function () { 28 | this.timeout(0) 29 | 30 | const oneUSDC = BigNumber.from(1000000) 31 | const bases = oneUSDC.mul(1000000) 32 | const fyTokens = bases 33 | const initialBase = bases 34 | const OVERRIDES = { gasLimit: 1_000_000 } 35 | 36 | let ownerAcc: SignerWithAddress 37 | let user1Acc: SignerWithAddress 38 | let user2Acc: SignerWithAddress 39 | let owner: string 40 | let user1: string 41 | let user2: string 42 | 43 | let yieldSpace: YieldSpaceEnvironment 44 | 45 | let pool: Pool 46 | let poolEstimator: PoolEstimator 47 | 48 | let base: Base 49 | let fyToken: FYToken 50 | let maturity: BigNumber 51 | 52 | const baseId = USDC 53 | const maturityId = '3M' 54 | const fyTokenId = baseId + '-' + maturityId 55 | 56 | async function fixture() { 57 | return await YieldSpaceEnvironment.setup(ownerAcc, [], [maturityId], BigNumber.from('0')) 58 | } 59 | 60 | before(async () => { 61 | const signers = await ethers.getSigners() 62 | ownerAcc = signers[0] 63 | owner = ownerAcc.address 64 | user1Acc = signers[1] 65 | user1 = user1Acc.address 66 | user2Acc = signers[2] 67 | user2 = user2Acc.address 68 | }) 69 | 70 | beforeEach(async () => { 71 | yieldSpace = await loadFixture(fixture) 72 | base = yieldSpace.bases.get(baseId) as Base 73 | fyToken = yieldSpace.fyTokens.get(fyTokenId) as FYToken 74 | pool = (yieldSpace.pools.get(baseId) as Map).get(fyTokenId) as Pool 75 | poolEstimator = await PoolEstimator.setup(pool) 76 | maturity = BigNumber.from(await fyToken.maturity()) 77 | 78 | await base.mint(pool.address, initialBase) 79 | await pool.connect(user1Acc).mint(user1, user1, 0, MAX) 80 | }) 81 | 82 | it('sells fyToken', async () => { 83 | const fyTokenIn = oneUSDC 84 | const baseBefore = await base.balanceOf(user2) 85 | 86 | await fyToken.mint(pool.address, fyTokenIn) 87 | 88 | const baseOutPreview = await pool.connect(user1Acc).sellFYTokenPreview(fyTokenIn) 89 | const expectedBaseOut = await poolEstimator.sellFYToken() 90 | 91 | await expect(pool.connect(user1Acc).sellFYToken(user2, 0)) 92 | .to.emit(pool, 'Trade') 93 | .withArgs(maturity, user1, user2, await base.balanceOf(user2), fyTokenIn.mul(-1)) 94 | 95 | expect(await fyToken.balanceOf(user1)).to.equal(0, "'From' wallet should have no fyToken tokens") 96 | 97 | const baseOut = (await base.balanceOf(user2)).sub(baseBefore) 98 | 99 | almostEqual(baseOut, expectedBaseOut, fyTokenIn.div(1000000)) 100 | almostEqual(baseOutPreview, expectedBaseOut, fyTokenIn.div(1000000)) 101 | expect((await pool.getCache())[0]).to.equal(await pool.getBaseBalance()) 102 | expect((await pool.getCache())[1]).to.equal(await pool.getFYTokenBalance()) 103 | }) 104 | 105 | it('buys base', async () => { 106 | const fyTokenCachedBefore = (await pool.getCache())[1] 107 | const baseOut = oneUSDC 108 | const baseBefore = await base.balanceOf(user2) 109 | 110 | const fyTokenInPreview = await pool.connect(user1Acc).buyBasePreview(baseOut) 111 | const expectedFYTokenIn = await poolEstimator.buyBase(baseOut) 112 | 113 | await fyToken.mint(pool.address, fyTokenInPreview) 114 | 115 | await expect(pool.connect(user1Acc).buyBase(user2, baseOut, MAX, OVERRIDES)) 116 | .to.emit(pool, 'Trade') 117 | .withArgs(maturity, user1, user2, baseOut, (await pool.getCache())[1].sub(fyTokenCachedBefore).mul(-1)) 118 | 119 | const fyTokenCachedCurrent = (await pool.getCache())[1] 120 | const fyTokenIn = fyTokenCachedCurrent.sub(fyTokenCachedBefore) 121 | const fyTokenChange = (await pool.getFYTokenBalance()).sub(fyTokenCachedCurrent) 122 | 123 | expect(await base.balanceOf(user2)).to.equal(baseOut.add(baseBefore), 'Receiver account should have 1 base token') 124 | 125 | almostEqual(fyTokenIn, expectedFYTokenIn, BigNumber.from(1)) 126 | 127 | almostEqual(fyTokenInPreview, expectedFYTokenIn, BigNumber.from(1)) 128 | expect((await pool.getCache())[0]).to.equal(await pool.getBaseBalance()) 129 | expect((await pool.getCache())[1].add(fyTokenChange)).to.equal(await pool.getFYTokenBalance()) 130 | }) 131 | 132 | describe('with extra fyToken balance', () => { 133 | beforeEach(async () => { 134 | const additionalFYTokenBalance = oneUSDC.mul(30) 135 | await fyToken.mint(pool.address, additionalFYTokenBalance) 136 | await pool.sellFYToken(owner, 0) 137 | }) 138 | 139 | it('mints liquidity tokens', async () => { 140 | const fyTokenIn = oneUSDC 141 | const [expectedMinted, expectedBaseIn] = await poolEstimator.mint(fyTokenIn) 142 | const poolTokensBefore = await pool.balanceOf(user2) 143 | 144 | await base.mint(pool.address, expectedBaseIn.add(oneUSDC)) 145 | await fyToken.mint(pool.address, fyTokenIn) 146 | await expect(pool.connect(user1Acc).mint(user2, user2, 0, MAX)) 147 | .to.emit(pool, 'Liquidity') 148 | .withArgs( 149 | maturity, 150 | user1, 151 | user2, 152 | ZERO_ADDRESS, 153 | expectedBaseIn.sub(1).mul(-1), 154 | fyTokenIn.mul(-1), 155 | expectedMinted 156 | ) 157 | 158 | const minted = (await pool.balanceOf(user2)).sub(poolTokensBefore) 159 | 160 | almostEqual(minted, expectedMinted, fyTokenIn.div(10000)) 161 | expect((await pool.getCache())[0]).to.equal(await pool.getBaseBalance()) 162 | expect((await pool.getCache())[1]).to.equal(await pool.getFYTokenBalance()) 163 | }) 164 | 165 | it('burns liquidity tokens', async () => { 166 | const baseBalance = await base.balanceOf(pool.address) 167 | const fyTokenBalance = await fyToken.balanceOf(pool.address) 168 | const lpTokensIn = oneUSDC 169 | 170 | const [expectedBaseOut, expectedFYTokenOut] = await poolEstimator.burn(lpTokensIn) 171 | 172 | await pool.connect(user1Acc).transfer(pool.address, lpTokensIn) 173 | await expect(pool.connect(user1Acc).burn(user2, user2, 0, MAX)) 174 | .to.emit(pool, 'Liquidity') 175 | .withArgs( 176 | maturity, 177 | user1, 178 | user2, 179 | user2, 180 | baseBalance.sub(await base.balanceOf(pool.address)), 181 | fyTokenBalance.sub(await fyToken.balanceOf(pool.address)), 182 | lpTokensIn.mul(-1) 183 | ) 184 | 185 | const baseOut = baseBalance.sub(await base.balanceOf(pool.address)) 186 | const fyTokenOut = fyTokenBalance.sub(await fyToken.balanceOf(pool.address)) 187 | 188 | almostEqual(baseOut, expectedBaseOut, BigNumber.from(1)) 189 | almostEqual(fyTokenOut, expectedFYTokenOut, BigNumber.from(1)) 190 | expect((await pool.getCache())[0]).to.equal(await pool.getBaseBalance()) 191 | expect((await pool.getCache())[1]).to.equal(await pool.getFYTokenBalance()) 192 | }) 193 | 194 | it('sells base', async () => { 195 | const baseIn = oneUSDC 196 | const userFYTokenBefore = await fyToken.balanceOf(user2) 197 | 198 | await base.mint(pool.address, baseIn) 199 | 200 | const fyTokenOutPreview = await pool.sellBasePreview(baseIn) 201 | const expectedFYTokenOut = await poolEstimator.sellBase() 202 | 203 | await expect(pool.connect(user1Acc).sellBase(user2, 0, OVERRIDES)) 204 | .to.emit(pool, 'Trade') 205 | .withArgs(maturity, user1, user2, baseIn.mul(-1), await fyToken.balanceOf(user2)) 206 | 207 | const fyTokenOut = (await fyToken.balanceOf(user2)).sub(userFYTokenBefore) 208 | 209 | almostEqual(fyTokenOut, expectedFYTokenOut, BigNumber.from(1)) 210 | almostEqual(fyTokenOutPreview, expectedFYTokenOut, BigNumber.from(1)) 211 | expect((await pool.getCache())[0]).to.equal(await pool.getBaseBalance()) 212 | expect((await pool.getCache())[1]).to.equal(await pool.getFYTokenBalance()) 213 | }) 214 | 215 | it('buys fyToken', async () => { 216 | const baseCachedBefore = (await pool.getCache())[0] 217 | const fyTokenOut = oneUSDC 218 | const userFYTokenBefore = await fyToken.balanceOf(user2) 219 | 220 | const baseInPreview = await pool.buyFYTokenPreview(fyTokenOut) 221 | const expectedBaseIn = await poolEstimator.buyFYToken(fyTokenOut) 222 | 223 | await base.mint(pool.address, baseInPreview) 224 | 225 | await expect(pool.connect(user1Acc).buyFYToken(user2, fyTokenOut, MAX, OVERRIDES)) 226 | .to.emit(pool, 'Trade') 227 | .withArgs(maturity, user1, user2, (await pool.getCache())[0].sub(baseCachedBefore).mul(-1), fyTokenOut) 228 | 229 | const baseCachedCurrent = (await pool.getCache())[0] 230 | const baseIn = baseCachedCurrent.sub(baseCachedBefore) 231 | const baseChange = (await pool.getBaseBalance()).sub(baseCachedCurrent) 232 | 233 | expect(await fyToken.balanceOf(user2)).to.equal( 234 | fyTokenOut.add(userFYTokenBefore), 235 | "'User2' wallet should have 1 fyToken token" 236 | ) 237 | 238 | almostEqual(baseIn, expectedBaseIn, BigNumber.from(1)) 239 | almostEqual(baseInPreview, expectedBaseIn, BigNumber.from(1)) 240 | expect((await pool.getCache())[0].add(baseChange)).to.equal(await pool.getBaseBalance()) 241 | expect((await pool.getCache())[1]).to.equal(await pool.getFYTokenBalance()) 242 | }) 243 | }) 244 | }) 245 | -------------------------------------------------------------------------------- /test/034_pool_mintWithBase.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address' 2 | 3 | import { constants } from '@yield-protocol/utils-v2' 4 | const { WAD, MAX128 } = constants 5 | const MAX = MAX128 6 | 7 | import { PoolEstimator } from './shared/poolEstimator' 8 | import { Pool } from '../typechain/Pool' 9 | import { BaseMock as Base } from '../typechain/BaseMock' 10 | import { FYTokenMock as FYToken } from '../typechain/FYTokenMock' 11 | import { YieldSpaceEnvironment } from './shared/fixtures' 12 | import { fyDaiForMint } from '../src/yieldspace' 13 | 14 | import { BigNumber } from 'ethers' 15 | 16 | import { ethers, waffle } from 'hardhat' 17 | import { expect } from 'chai' 18 | const { loadFixture } = waffle 19 | 20 | function almostEqual(x: BigNumber, y: BigNumber, p: BigNumber) { 21 | // Check that abs(x - y) < p: 22 | const diff = x.gt(y) ? BigNumber.from(x).sub(y) : BigNumber.from(y).sub(x) // Not sure why I have to convert x and y to BigNumber 23 | expect(diff.div(p)).to.eq(0) // Hack to avoid silly conversions. BigNumber truncates decimals off. 24 | } 25 | 26 | describe('Pool - mintWithBase', async function () { 27 | this.timeout(0) 28 | 29 | const initialBase = BigNumber.from(0) 30 | const OVERRIDES = { gasLimit: 1_000_000 } 31 | 32 | let ownerAcc: SignerWithAddress 33 | let user1Acc: SignerWithAddress 34 | let user2Acc: SignerWithAddress 35 | let owner: string 36 | let user1: string 37 | let user2: string 38 | 39 | let yieldSpace: YieldSpaceEnvironment 40 | let poolEstimator: PoolEstimator 41 | let pool: Pool 42 | let base: Base 43 | let fyToken: FYToken 44 | let maturity: BigNumber 45 | 46 | const baseId = ethers.utils.hexlify(ethers.utils.randomBytes(6)) 47 | const maturityId = '3M' 48 | const fyTokenId = baseId + '-' + maturityId 49 | 50 | const poolSupplies = [ 51 | BigNumber.from('10000000000000000000'), 52 | // BigNumber.from('1000000000000000000000'), 53 | // BigNumber.from('100000000000000000000000'), 54 | // BigNumber.from('10000000000000000000000000'), 55 | // BigNumber.from('1000000000000000000000000000'), 56 | ] 57 | const baseReserves = [ 58 | // Multiplier on the supply 59 | BigNumber.from('1'), 60 | BigNumber.from('10'), 61 | BigNumber.from('100'), 62 | ] 63 | const fyTokenVirtualReservesDeltas = [ 64 | // Multiplier on the supply 65 | BigNumber.from('1'), 66 | BigNumber.from('10'), 67 | BigNumber.from('100'), 68 | ] 69 | const tradeSizes = [ 70 | // Divisor on the reserves delta 71 | BigNumber.from('1'), 72 | BigNumber.from('2'), 73 | BigNumber.from('10'), 74 | BigNumber.from('100'), 75 | ] 76 | const timesTillMaturity = [30, 30000, 30000000] 77 | 78 | const scaleFactor = BigNumber.from('1') 79 | 80 | async function fixture() { 81 | return await YieldSpaceEnvironment.setup(ownerAcc, [baseId], [maturityId], initialBase) 82 | } 83 | 84 | before(async () => { 85 | const signers = await ethers.getSigners() 86 | ownerAcc = signers[0] 87 | owner = ownerAcc.address 88 | user1Acc = signers[1] 89 | user1 = user1Acc.address 90 | user2Acc = signers[2] 91 | user2 = user2Acc.address 92 | }) 93 | 94 | beforeEach(async () => { 95 | yieldSpace = await loadFixture(fixture) 96 | base = yieldSpace.bases.get(baseId) as Base 97 | fyToken = yieldSpace.fyTokens.get(fyTokenId) as FYToken 98 | pool = ((yieldSpace.pools.get(baseId) as Map).get(fyTokenId) as Pool).connect(user1Acc) 99 | poolEstimator = await PoolEstimator.setup(pool) 100 | maturity = BigNumber.from(await pool.maturity()) 101 | }) 102 | 103 | it('mintWithBase', async () => { 104 | for (var poolSupply of poolSupplies) { 105 | for (var baseReserveMultiplier of baseReserves) { 106 | for (var fyTokenVirtualReservesDeltaMultiplier of fyTokenVirtualReservesDeltas) { 107 | for (var tradeSizeDivisor of tradeSizes) { 108 | for (var timeTillMaturity of timesTillMaturity) { 109 | const snapshotId = await ethers.provider.send('evm_snapshot', []) 110 | await ethers.provider.send('evm_mine', [maturity.toNumber() - timeTillMaturity]) 111 | 112 | const baseReserve = baseReserveMultiplier.mul(poolSupply) 113 | const reservesDelta = fyTokenVirtualReservesDeltaMultiplier.mul(poolSupply) 114 | const fyTokenVirtualReserve = reservesDelta.add(baseReserve) 115 | const fyTokenRealReserve = fyTokenVirtualReserve.sub(poolSupply) 116 | const trade = reservesDelta.div(tradeSizeDivisor) 117 | 118 | // Initialize to supply 119 | await base.mint(pool.address, poolSupply) 120 | await pool.mint(owner, owner, 0, MAX) 121 | 122 | // Donate to reserves 123 | const baseDonation = baseReserve.sub(poolSupply) 124 | await base.mint(pool.address, baseDonation) 125 | await fyToken.mint(pool.address, fyTokenRealReserve) 126 | await pool.sync() 127 | 128 | let fyTokenToBuy: BigNumber 129 | try { 130 | fyTokenToBuy = BigNumber.from( 131 | fyDaiForMint( 132 | baseReserve, 133 | fyTokenRealReserve, 134 | fyTokenVirtualReserve, 135 | trade, 136 | BigNumber.from(timeTillMaturity) 137 | ) 138 | ) 139 | } catch (e) { 140 | // A number of trades will revert, in very unusual conditions such as very unbalanced trades, or seconds to maturity. That's fine. 141 | /* console.log(` 142 | Aborted trade: 143 | supply: ${await pool.totalSupply()} 144 | baseReserves: ${await pool.getBaseBalance()} 145 | fyTokenReserves: ${await fyToken.balanceOf(pool.address)} 146 | fyTokenVirtual: ${await pool.getFYTokenBalance()} 147 | trade: ${trade} 148 | timeTillMaturity: ${timeTillMaturity} 149 | `) */ 150 | await ethers.provider.send('evm_revert', [snapshotId]) 151 | continue 152 | } 153 | /* console.log(` 154 | supply: ${await pool.totalSupply()} 155 | baseReserves: ${await pool.getBaseBalance()} 156 | fyTokenReserves: ${await fyToken.balanceOf(pool.address)} 157 | fyTokenVirtual: ${await pool.getFYTokenBalance()} 158 | trade: ${trade} 159 | timeTillMaturity: ${timeTillMaturity} 160 | fyTokenToBuy: ${fyTokenToBuy.toString()} 161 | baseSold (off): ${await poolEstimator.buyFYToken(fyTokenToBuy)} 162 | baseUsed: ${(await poolEstimator.mintWithBase(fyTokenToBuy))[1]} 163 | `) */ 164 | 165 | await base.mint(pool.address, trade) 166 | /* console.log(` 167 | baseSold (on): ${await pool.buyFYTokenPreview(fyTokenToBuy)} 168 | `) */ 169 | const result = await pool.callStatic.mintWithBase(owner, owner, fyTokenToBuy, 0, MAX, OVERRIDES) 170 | /* console.log(` 171 | baseIn: ${result[0]} 172 | surplus: ${trade.sub(result[0])} 173 | `) */ 174 | // TODO: Verify that the surplus is below a 0.005% of the trade (fyDaiForMint targets 0.001% to 0.002%) 175 | 176 | await ethers.provider.send('evm_revert', [snapshotId]) 177 | } 178 | } 179 | } 180 | } 181 | } 182 | }) 183 | }) 184 | -------------------------------------------------------------------------------- /test/035_pool_twar.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address' 2 | 3 | import { constants } from '@yield-protocol/utils-v2' 4 | const { WAD, MAX128 } = constants 5 | const MAX = MAX128 6 | 7 | import { Pool } from '../typechain/Pool' 8 | import { BaseMock as Base } from '../typechain/BaseMock' 9 | import { YieldSpaceEnvironment } from './shared/fixtures' 10 | 11 | import { BigNumber } from 'ethers' 12 | 13 | import { ethers, waffle } from 'hardhat' 14 | import { expect } from 'chai' 15 | const { loadFixture } = waffle 16 | 17 | async function currentTimestamp() { 18 | return (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp 19 | } 20 | 21 | function almostEqual(x: BigNumber, y: BigNumber, p: BigNumber) { 22 | // Check that abs(x - y) < p: 23 | const diff = x.gt(y) ? BigNumber.from(x).sub(y) : BigNumber.from(y).sub(x) // Not sure why I have to convert x and y to BigNumber 24 | expect(diff.div(p)).to.eq(0) // Hack to avoid silly conversions. BigNumber truncates decimals off. 25 | } 26 | 27 | describe('Pool - TWAR', async function () { 28 | this.timeout(0) 29 | 30 | // These values impact the pool results 31 | const bases = BigNumber.from('1000000000000000000000000') 32 | const initialBase = bases 33 | 34 | let ownerAcc: SignerWithAddress 35 | let user1Acc: SignerWithAddress 36 | let owner: string 37 | let user1: string 38 | 39 | let yieldSpace: YieldSpaceEnvironment 40 | 41 | let pool: Pool 42 | let poolFromUser1: Pool 43 | 44 | let base: Base 45 | let baseFromUser1: Base 46 | 47 | const baseId = ethers.utils.hexlify(ethers.utils.randomBytes(6)) 48 | const maturityId = '3M' 49 | const fyTokenId = baseId + '-' + maturityId 50 | 51 | async function fixture() { 52 | return await YieldSpaceEnvironment.setup(ownerAcc, [baseId], [maturityId], BigNumber.from('0')) 53 | } 54 | 55 | before(async () => { 56 | const signers = await ethers.getSigners() 57 | ownerAcc = signers[0] 58 | owner = ownerAcc.address 59 | user1Acc = signers[1] 60 | user1 = user1Acc.address 61 | }) 62 | 63 | beforeEach(async () => { 64 | yieldSpace = await loadFixture(fixture) 65 | base = yieldSpace.bases.get(baseId) as Base 66 | baseFromUser1 = base.connect(user1Acc) 67 | 68 | // Deploy a fresh pool so that we can test initialization 69 | pool = (yieldSpace.pools.get(baseId) as Map).get(fyTokenId) as Pool 70 | poolFromUser1 = pool.connect(user1Acc) 71 | 72 | await base.mint(pool.address, initialBase) 73 | await poolFromUser1.mint(user1, user1, 0, MAX) 74 | }) 75 | 76 | it('calculates the TWAR price', async () => { 77 | const cumulativePrice1 = await pool.cumulativeBalancesRatio() 78 | expect(cumulativePrice1).to.equal(0, 'Price should start at 0') 79 | const timestamp1 = (await pool.getCache())[2] 80 | 81 | await ethers.provider.send('evm_mine', [(await currentTimestamp()) + 120]) 82 | 83 | await pool.sync() 84 | 85 | const balancedRatio = BigNumber.from('10').pow(BigNumber.from('27')) 86 | 87 | const cumulativeRatio2 = await pool.cumulativeBalancesRatio() 88 | const timestamp2 = (await pool.getCache())[2] 89 | const ratio2 = cumulativeRatio2.div(BigNumber.from(timestamp2 - timestamp1)) 90 | almostEqual(ratio2, balancedRatio, BigNumber.from('10000000000')) 91 | 92 | await ethers.provider.send('evm_mine', [(await currentTimestamp()) + 120]) 93 | 94 | await pool.sync() 95 | 96 | const cumulativeRatio3 = await pool.cumulativeBalancesRatio() 97 | const timestamp3 = (await pool.getCache())[2] 98 | const ratio3 = cumulativeRatio3.sub(cumulativeRatio2).div(BigNumber.from(timestamp3 - timestamp2)) 99 | almostEqual(ratio3, balancedRatio, BigNumber.from('10000000000')) 100 | }) 101 | }) 102 | -------------------------------------------------------------------------------- /test/036_pool_extensions.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address' 2 | 3 | import { constants } from '@yield-protocol/utils-v2' 4 | 5 | import { PoolEstimator } from './shared/poolEstimator' 6 | import { Pool, PoolView, YieldMath__factory, PoolView__factory } from '../typechain' 7 | import { BaseMock as Base } from '../typechain/BaseMock' 8 | import { FYTokenMock as FYToken } from '../typechain/FYTokenMock' 9 | import { YieldSpaceEnvironment } from './shared/fixtures' 10 | 11 | import { BigNumber, utils } from 'ethers' 12 | import { ethers, waffle } from 'hardhat' 13 | import { expect } from 'chai' 14 | 15 | const { MAX128, USDC } = constants 16 | const MAX = MAX128 17 | 18 | const { parseUnits } = utils 19 | 20 | const { loadFixture } = waffle 21 | 22 | describe('YieldMathExtensions - allowances', async function () { 23 | this.timeout(0) 24 | 25 | const oneUSDC = BigNumber.from(parseUnits('1', 6)) 26 | const bases = oneUSDC.mul(1_000_000) 27 | const OVERRIDES = { gasLimit: 1_000_000 } 28 | 29 | let ownerAcc: SignerWithAddress 30 | let user1Acc: SignerWithAddress 31 | let user2Acc: SignerWithAddress 32 | let owner: string 33 | let user1: string 34 | let user2: string 35 | 36 | let yieldSpace: YieldSpaceEnvironment 37 | 38 | let pool: Pool 39 | let poolEstimator: PoolEstimator 40 | let poolView: PoolView 41 | 42 | let base: Base 43 | let fyToken: FYToken 44 | let maturity: BigNumber 45 | 46 | const baseId = USDC 47 | const maturityId = '3M' 48 | const fyTokenId = baseId + '-' + maturityId 49 | 50 | async function fixture() { 51 | return await YieldSpaceEnvironment.setup(ownerAcc, [], [maturityId], BigNumber.from('0')) 52 | } 53 | 54 | before(async () => { 55 | const signers = await ethers.getSigners() 56 | ownerAcc = signers[0] 57 | owner = ownerAcc.address 58 | user1Acc = signers[1] 59 | user1 = user1Acc.address 60 | user2Acc = signers[2] 61 | user2 = user2Acc.address 62 | }) 63 | 64 | beforeEach(async () => { 65 | yieldSpace = await loadFixture(fixture) 66 | base = yieldSpace.bases.get(baseId) as Base 67 | fyToken = yieldSpace.fyTokens.get(fyTokenId) as FYToken 68 | pool = (yieldSpace.pools.get(baseId) as Map).get(fyTokenId) as Pool 69 | poolEstimator = await PoolEstimator.setup(pool) 70 | maturity = BigNumber.from(await fyToken.maturity()) 71 | 72 | await base.mint(pool.address, bases) 73 | await pool.connect(user1Acc).mint(user1, user1, 0, MAX) 74 | 75 | const yieldMathLibrary = await ((await ethers.getContractFactory('YieldMath')) as YieldMath__factory).deploy() 76 | await yieldMathLibrary.deployed() 77 | const yieldMathExtensionsLibrary = await ((await ethers.getContractFactory('YieldMathExtensions', { 78 | libraries: { 79 | YieldMath: yieldMathLibrary.address, 80 | }, 81 | })) as YieldMath__factory).deploy() 82 | await yieldMathExtensionsLibrary.deployed() 83 | poolView = await ((await ethers.getContractFactory('PoolView', { 84 | libraries: { 85 | YieldMathExtensions: yieldMathExtensionsLibrary.address, 86 | }, 87 | })) as PoolView__factory).deploy() 88 | await poolView.deployed() 89 | }) 90 | 91 | it('computes the invariant', async () => { 92 | let invariant = await poolView.invariant(pool.address) 93 | await base.mint(pool.address, oneUSDC) 94 | await pool.sync() 95 | 96 | // By expecting that the second invariant reading is greater than the first we know it's computed 97 | // To make it better, we should include it in the off-chain yieldmath library 98 | expect(await poolView.invariant(pool.address)).gt(invariant) 99 | }) 100 | 101 | it('computes the retrievable base', async () => { 102 | const retrievableBaseBefore = await poolView.retrievableBase(pool.address) 103 | await base.mint(pool.address, oneUSDC) 104 | expect(await poolView.retrievableBase(pool.address)).to.equal(retrievableBaseBefore.add(oneUSDC)) 105 | }) 106 | 107 | it('computes the retrievable fyToken', async () => { 108 | const retrievableFYTokenBefore = await poolView.retrievableFYToken(pool.address) 109 | await fyToken.mint(pool.address, oneUSDC) 110 | expect(await poolView.retrievableFYToken(pool.address)).to.equal(retrievableFYTokenBefore.add(oneUSDC)) 111 | }) 112 | 113 | it('computes the pool allowances after fyToken sale', async () => { 114 | //given 115 | const maxFYTokenInBefore = await poolView.maxFYTokenIn(pool.address) 116 | const maxFYTokenOutBefore = await poolView.maxFYTokenOut(pool.address) 117 | const maxBaseInBefore = await poolView.maxBaseIn(pool.address) 118 | const maxBaseOutBefore = await poolView.maxBaseOut(pool.address) 119 | const fyTokenIn = oneUSDC 120 | 121 | expect(maxFYTokenInBefore).to.be.gt(0) 122 | expect(maxBaseOutBefore).to.be.gt(0) 123 | expect(maxFYTokenOutBefore).to.be.eq(0) 124 | expect(maxBaseInBefore).to.be.eq(0) 125 | const baseBefore = await pool.getBaseBalance() 126 | expect(await pool.sellFYTokenPreview(maxFYTokenInBefore)).to.be.lt(baseBefore) 127 | expect(maxBaseOutBefore).to.be.lt(baseBefore) 128 | 129 | //when 130 | await fyToken.mint(pool.address, fyTokenIn) 131 | await expect(pool.connect(user1Acc).sellFYToken(user2, 0)) 132 | .to.emit(pool, 'Trade') 133 | .withArgs(maturity, user1, user2, await base.balanceOf(user2), fyTokenIn.mul(-1)) 134 | 135 | //then 136 | const maxFYTokenIn = await poolView.maxFYTokenIn(pool.address) 137 | const maxFYTokenOut = await poolView.maxFYTokenOut(pool.address) 138 | const maxBaseIn = await poolView.maxBaseIn(pool.address) 139 | const maxBaseOut = await poolView.maxBaseOut(pool.address) 140 | 141 | expect(maxFYTokenInBefore).to.be.gt(maxFYTokenIn) 142 | expect(maxFYTokenOutBefore).to.be.lt(maxFYTokenOut) 143 | expect(maxBaseInBefore).to.be.lt(maxBaseIn) 144 | expect(maxBaseOutBefore).to.be.gt(maxBaseOut) 145 | 146 | expect(await pool.buyFYTokenPreview(maxFYTokenOut)).to.be.gt(0) 147 | expect(await pool.sellBasePreview(maxBaseIn)).to.be.gt(0) 148 | const baseAfter = await pool.getBaseBalance() 149 | expect(await pool.sellFYTokenPreview(maxFYTokenIn)).to.be.lt(baseAfter) 150 | expect(maxBaseOut).to.be.lt(baseAfter) 151 | 152 | // YieldMath is not 100%, so some times is 1 wei off, but on the safe side 153 | await expect(pool.buyFYTokenPreview(maxFYTokenOut.add(2))).to.be.revertedWith('Pool: fyToken balance too low') 154 | await expect(pool.sellBasePreview(maxBaseIn.add(2))).to.be.revertedWith('Pool: fyToken balance too low') 155 | }) 156 | 157 | it('computes the pool allowances after base purchase', async () => { 158 | //given 159 | const maxFYTokenInBefore = await poolView.maxFYTokenIn(pool.address) 160 | const maxFYTokenOutBefore = await poolView.maxFYTokenOut(pool.address) 161 | const maxBaseInBefore = await poolView.maxBaseIn(pool.address) 162 | const maxBaseOutBefore = await poolView.maxBaseOut(pool.address) 163 | const fyTokenCachedBefore = (await pool.getCache())[1] 164 | const baseOut = oneUSDC 165 | 166 | //when 167 | await fyToken.mint(pool.address, await pool.connect(user1Acc).buyBasePreview(baseOut)) 168 | await expect(pool.connect(user1Acc).buyBase(user2, baseOut, MAX, OVERRIDES)) 169 | .to.emit(pool, 'Trade') 170 | .withArgs(maturity, user1, user2, baseOut, (await pool.getCache())[1].sub(fyTokenCachedBefore).mul(-1)) 171 | 172 | //then 173 | const maxFYTokenIn = await poolView.maxFYTokenIn(pool.address) 174 | const maxFYTokenOut = await poolView.maxFYTokenOut(pool.address) 175 | const maxBaseIn = await poolView.maxBaseIn(pool.address) 176 | const maxBaseOut = await poolView.maxBaseOut(pool.address) 177 | 178 | expect(maxFYTokenInBefore).to.be.gt(maxFYTokenIn) 179 | expect(maxFYTokenOutBefore).to.be.lt(maxFYTokenOut) 180 | expect(maxBaseInBefore).to.be.lt(maxBaseIn) 181 | expect(maxBaseOutBefore).to.be.gt(maxBaseOut) 182 | 183 | expect(await pool.buyFYTokenPreview(maxFYTokenOut)).to.be.gt(0) 184 | expect(await pool.sellBasePreview(maxBaseIn)).to.be.gt(0) 185 | const baseAfter = await pool.getBaseBalance() 186 | expect(await pool.sellFYTokenPreview(maxFYTokenIn)).to.be.lt(baseAfter) 187 | expect(maxBaseOut).to.be.lt(baseAfter) 188 | 189 | // YieldMath is not 100%, so some times is 1 wei off, but on the safe side 190 | await expect(pool.buyFYTokenPreview(maxFYTokenOut.add(2))).to.be.revertedWith('Pool: fyToken balance too low') 191 | await expect(pool.sellBasePreview(maxBaseIn.add(2))).to.be.revertedWith('Pool: fyToken balance too low') 192 | }) 193 | 194 | describe('with extra fyToken balance', () => { 195 | beforeEach(async () => { 196 | const additionalFYTokenBalance = oneUSDC.mul(30) 197 | await fyToken.mint(pool.address, additionalFYTokenBalance) 198 | await pool.sellFYToken(owner, 0) 199 | }) 200 | 201 | it('computes the pool allowances after base sale', async () => { 202 | //given 203 | const maxFYTokenInBefore = await poolView.maxFYTokenIn(pool.address) 204 | const maxFYTokenOutBefore = await poolView.maxFYTokenOut(pool.address) 205 | const maxBaseInBefore = await poolView.maxBaseIn(pool.address) 206 | const maxBaseOutBefore = await poolView.maxBaseOut(pool.address) 207 | const baseIn = oneUSDC 208 | 209 | //when 210 | await base.mint(pool.address, baseIn) 211 | await expect(pool.connect(user1Acc).sellBase(user2, 0, OVERRIDES)) 212 | .to.emit(pool, 'Trade') 213 | .withArgs(maturity, user1, user2, baseIn.mul(-1), await fyToken.balanceOf(user2)) 214 | 215 | //then 216 | const maxFYTokenIn = await poolView.maxFYTokenIn(pool.address) 217 | const maxFYTokenOut = await poolView.maxFYTokenOut(pool.address) 218 | const maxBaseIn = await poolView.maxBaseIn(pool.address) 219 | const maxBaseOut = await poolView.maxBaseOut(pool.address) 220 | 221 | expect(maxFYTokenInBefore).to.be.lt(maxFYTokenIn) 222 | expect(maxFYTokenOutBefore).to.be.gt(maxFYTokenOut) 223 | expect(maxBaseInBefore).to.be.gt(maxBaseIn) 224 | expect(maxBaseOutBefore).to.be.lt(maxBaseOut) 225 | 226 | expect(await pool.buyFYTokenPreview(maxFYTokenOut)).to.be.gt(0) 227 | expect(await pool.sellBasePreview(maxBaseIn)).to.be.gt(0) 228 | const baseAfter = await pool.getBaseBalance() 229 | expect(await pool.sellFYTokenPreview(maxFYTokenIn)).to.be.lt(baseAfter) 230 | expect(maxBaseOut).to.be.lt(baseAfter) 231 | 232 | // YieldMath is not 100%, so some times is 1 wei off, but on the safe side 233 | await expect(pool.buyFYTokenPreview(maxFYTokenOut.add(2))).to.be.revertedWith('Pool: fyToken balance too low') 234 | await expect(pool.sellBasePreview(maxBaseIn.add(2))).to.be.revertedWith('Pool: fyToken balance too low') 235 | }) 236 | 237 | it('computes the pool allowances after fyToken purchase', async () => { 238 | //given 239 | const maxFYTokenInBefore = await poolView.maxFYTokenIn(pool.address) 240 | const maxFYTokenOutBefore = await poolView.maxFYTokenOut(pool.address) 241 | const maxBaseInBefore = await poolView.maxBaseIn(pool.address) 242 | const maxBaseOutBefore = await poolView.maxBaseOut(pool.address) 243 | const baseCachedBefore = (await pool.getCache())[0] 244 | const fyTokenOut = oneUSDC 245 | 246 | //when 247 | await base.mint(pool.address, await pool.buyFYTokenPreview(fyTokenOut)) 248 | await expect(pool.connect(user1Acc).buyFYToken(user2, fyTokenOut, MAX, OVERRIDES)) 249 | .to.emit(pool, 'Trade') 250 | .withArgs(maturity, user1, user2, (await pool.getCache())[0].sub(baseCachedBefore).mul(-1), fyTokenOut) 251 | 252 | //then 253 | const maxFYTokenIn = await poolView.maxFYTokenIn(pool.address) 254 | const maxFYTokenOut = await poolView.maxFYTokenOut(pool.address) 255 | const maxBaseIn = await poolView.maxBaseIn(pool.address) 256 | const maxBaseOut = await poolView.maxBaseOut(pool.address) 257 | 258 | expect(maxFYTokenInBefore).to.be.lt(maxFYTokenIn) 259 | expect(maxFYTokenOutBefore).to.be.gt(maxFYTokenOut) 260 | expect(maxBaseInBefore).to.be.gt(maxBaseIn) 261 | expect(maxBaseOutBefore).to.be.lt(maxBaseOut) 262 | 263 | expect(await pool.buyFYTokenPreview(maxFYTokenOut)).to.be.gt(0) 264 | expect(await pool.sellBasePreview(maxBaseIn)).to.be.gt(0) 265 | const baseAfter = await pool.getBaseBalance() 266 | expect(await pool.sellFYTokenPreview(maxFYTokenIn)).to.be.lt(baseAfter) 267 | expect(maxBaseOut).to.be.lt(baseAfter) 268 | 269 | // YieldMath is not 100%, so some times is 1 wei off, but on the safe side 270 | await expect(pool.buyFYTokenPreview(maxFYTokenOut.add(2))).to.be.revertedWith('Pool: fyToken balance too low') 271 | await expect(pool.sellBasePreview(maxBaseIn.add(2))).to.be.revertedWith('Pool: fyToken balance too low') 272 | }) 273 | }) 274 | }) 275 | -------------------------------------------------------------------------------- /test/shared/fixtures.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address' 2 | import { BaseProvider } from '@ethersproject/providers' 3 | 4 | import { constants, id } from '@yield-protocol/utils-v2' 5 | const { DAI, ETH, USDC, THREE_MONTHS, MAX256 } = constants 6 | const MAX = MAX256 7 | 8 | import { YieldMath } from '../../typechain/YieldMath' 9 | import { Pool } from '../../typechain/Pool' 10 | import { BaseMock as ERC20 } from '../../typechain/BaseMock' 11 | import { FYTokenMock as FYToken } from '../../typechain/FYTokenMock' 12 | import { ethers } from 'hardhat' 13 | import { BigNumber } from 'ethers' 14 | import { ts, g1, g2 } from '../../src/constants' 15 | 16 | export class YieldSpaceEnvironment { 17 | owner: SignerWithAddress 18 | bases: Map 19 | fyTokens: Map 20 | pools: Map> 21 | 22 | constructor( 23 | owner: SignerWithAddress, 24 | bases: Map, 25 | fyTokens: Map, 26 | pools: Map> 27 | ) { 28 | this.owner = owner 29 | this.bases = bases 30 | this.fyTokens = fyTokens 31 | this.pools = pools 32 | } 33 | 34 | // Set up a test environment with pools according to the cartesian product of the base ids and the fyToken ids 35 | public static async setup( 36 | owner: SignerWithAddress, 37 | baseIds: Array, 38 | maturityIds: Array, 39 | initialBase: BigNumber 40 | ) { 41 | const ownerAdd = await owner.getAddress() 42 | 43 | let yieldMathLibrary: YieldMath 44 | 45 | const WETH9Factory = await ethers.getContractFactory('WETH9Mock') 46 | const weth9 = (((await WETH9Factory.deploy()) as unknown) as unknown) as ERC20 47 | await weth9.deployed() 48 | 49 | const DaiFactory = await ethers.getContractFactory('DaiMock') 50 | const dai = (((await DaiFactory.deploy('DAI', 'DAI')) as unknown) as unknown) as ERC20 51 | await dai.deployed() 52 | 53 | const USDCFactory = await ethers.getContractFactory('USDCMock') 54 | const usdc = (((await USDCFactory.deploy('USDC', 'USDC')) as unknown) as unknown) as ERC20 55 | await usdc.deployed() 56 | 57 | const BaseFactory = await ethers.getContractFactory('BaseMock') 58 | const FYTokenFactory = await ethers.getContractFactory('FYTokenMock') 59 | const YieldMathFactory = await ethers.getContractFactory('YieldMath') 60 | yieldMathLibrary = ((await YieldMathFactory.deploy()) as unknown) as YieldMath 61 | await yieldMathLibrary.deployed() 62 | 63 | const PoolFactory = await ethers.getContractFactory('Pool', { 64 | libraries: { 65 | YieldMath: yieldMathLibrary.address, 66 | }, 67 | }) 68 | 69 | const initialFYToken = initialBase.div(9) 70 | const bases: Map = new Map() 71 | const fyTokens: Map = new Map() 72 | const pools: Map> = new Map() 73 | const now = (await ethers.provider.getBlock('latest')).timestamp 74 | let count: number = 1 75 | 76 | // deploy bases 77 | for (let baseId of baseIds) { 78 | const base = ((await BaseFactory.deploy()) as unknown) as ERC20 79 | await base.deployed() 80 | bases.set(baseId, base) 81 | } 82 | 83 | // add WETH to bases 84 | bases.set(ETH, weth9) 85 | baseIds.unshift(ETH) 86 | 87 | // add Dai to bases 88 | bases.set(DAI, dai) 89 | baseIds.unshift(DAI) 90 | 91 | // add USDC to bases 92 | bases.set(USDC, usdc) 93 | baseIds.unshift(USDC) 94 | 95 | for (let baseId of baseIds) { 96 | const base = bases.get(baseId) as ERC20 97 | const fyTokenPoolPairs: Map = new Map() 98 | pools.set(baseId, fyTokenPoolPairs) 99 | 100 | for (let maturityId of maturityIds) { 101 | const fyTokenId = baseId + '-' + maturityId 102 | 103 | // deploy fyToken 104 | const maturity = now + THREE_MONTHS * count++ // We are just assuming that the maturities are '3M', '6M', '9M' and so on 105 | const fyToken = ((await FYTokenFactory.deploy(base.address, maturity)) as unknown) as FYToken 106 | await fyToken.deployed() 107 | fyTokens.set(fyTokenId, fyToken) 108 | 109 | // deploy base/fyToken pool 110 | const pool = ((await PoolFactory.deploy(base.address, fyToken.address, ts, g1, g2)) as unknown) as Pool 111 | fyTokenPoolPairs.set(fyTokenId, pool) 112 | 113 | // init pool 114 | if (initialBase !== BigNumber.from(0)) { 115 | if (baseId === ETH) { 116 | break // TODO: Fix when we can give `initialBase` ether to the deployer 117 | await weth9.deposit({ value: initialBase }) 118 | await weth9.transfer(pool.address, initialBase) 119 | } else { 120 | await base.mint(pool.address, initialBase) 121 | } 122 | await pool.mint(ownerAdd, ownerAdd, 0, MAX) 123 | 124 | // skew pool to 5% interest rate 125 | await fyToken.mint(pool.address, initialFYToken) 126 | await pool.sync() 127 | } 128 | } 129 | } 130 | 131 | return new YieldSpaceEnvironment(owner, bases, fyTokens, pools) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /test/shared/helpers.ts: -------------------------------------------------------------------------------- 1 | export const DEBUG: boolean = 2 | (process.env.DEBUG == undefined ? 'false' : process.env.DEBUG).toLowerCase() == 'true' ? true : false 3 | 4 | export const debugLog: (str?: string) => Promise = async (str = '\n') => { 5 | DEBUG && console.log(str) 6 | } 7 | -------------------------------------------------------------------------------- /test/shared/poolEstimator.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, BigNumberish } from 'ethers' 2 | import { IERC20 } from '../../typechain/IERC20' 3 | import { Pool } from '../../typechain/Pool' 4 | import { mint, burn, sellBase, sellFYToken, buyBase, buyFYToken, mintWithBase, burnForBase } from '../../src/yieldspace' 5 | import { ethers } from 'hardhat' 6 | 7 | async function currentTimestamp() { 8 | return (await ethers.provider.getBlock('latest')).timestamp 9 | } 10 | 11 | export class PoolEstimator { 12 | pool: Pool 13 | base: IERC20 14 | fyToken: IERC20 15 | 16 | constructor(pool: Pool, base: IERC20, fyToken: IERC20) { 17 | this.pool = pool 18 | this.base = base 19 | this.fyToken = fyToken 20 | } 21 | 22 | public static async setup(pool: Pool): Promise { 23 | const base = (await ethers.getContractAt('IERC20', await pool.base())) as IERC20 24 | const fyToken = (await ethers.getContractAt('IERC20', await pool.fyToken())) as IERC20 25 | return new PoolEstimator(pool, base, fyToken) 26 | } 27 | 28 | public async sellBase(): Promise { 29 | return sellBase( 30 | await this.pool.getBaseBalance(), 31 | await this.pool.getFYTokenBalance(), 32 | (await this.pool.getBaseBalance()).sub((await this.pool.getCache())[0]), 33 | BigNumber.from(await this.pool.maturity()).sub(await currentTimestamp()), 34 | await this.pool.scaleFactor() 35 | ) 36 | } 37 | 38 | public async sellFYToken(): Promise { 39 | return sellFYToken( 40 | await this.pool.getBaseBalance(), 41 | await this.pool.getFYTokenBalance(), 42 | (await this.pool.getFYTokenBalance()).sub((await this.pool.getCache())[1]), 43 | BigNumber.from(await this.pool.maturity()).sub(await currentTimestamp()), 44 | await this.pool.scaleFactor() 45 | ) 46 | } 47 | 48 | public async buyBase(tokenOut: BigNumberish): Promise { 49 | return buyBase( 50 | await this.pool.getBaseBalance(), 51 | await this.pool.getFYTokenBalance(), 52 | BigNumber.from(tokenOut), 53 | BigNumber.from(await this.pool.maturity()).sub(await currentTimestamp()), 54 | await this.pool.scaleFactor() 55 | ) 56 | } 57 | 58 | public async buyFYToken(tokenOut: BigNumberish): Promise { 59 | return buyFYToken( 60 | await this.pool.getBaseBalance(), 61 | await this.pool.getFYTokenBalance(), 62 | BigNumber.from(tokenOut), 63 | BigNumber.from(await this.pool.maturity()).sub(await currentTimestamp()), 64 | await this.pool.scaleFactor() 65 | ) 66 | } 67 | 68 | public async mint(input: BigNumber): Promise<[BigNumber, BigNumber]> { 69 | return mint( 70 | await this.base.balanceOf(this.pool.address), 71 | await this.fyToken.balanceOf(this.pool.address), 72 | await this.pool.totalSupply(), 73 | input 74 | ) 75 | } 76 | 77 | public async burn(lpTokens: BigNumber): Promise<[BigNumber, BigNumber]> { 78 | return burn( 79 | await this.base.balanceOf(this.pool.address), 80 | await this.fyToken.balanceOf(this.pool.address), 81 | await this.pool.totalSupply(), 82 | lpTokens 83 | ) 84 | } 85 | 86 | public async mintWithBase(fyToken: BigNumber): Promise<[BigNumber, BigNumber]> { 87 | return mintWithBase( 88 | await this.base.balanceOf(this.pool.address), 89 | await this.pool.getFYTokenBalance(), 90 | await this.fyToken.balanceOf(this.pool.address), 91 | await this.pool.totalSupply(), 92 | fyToken, 93 | BigNumber.from(await this.pool.maturity()).sub(await currentTimestamp()), 94 | await this.pool.scaleFactor() 95 | ) 96 | } 97 | 98 | public async burnForBase(lpTokens: BigNumber): Promise { 99 | return burnForBase( 100 | await this.base.balanceOf(this.pool.address), 101 | await this.pool.getFYTokenBalance(), 102 | await this.fyToken.balanceOf(this.pool.address), 103 | await this.pool.totalSupply(), 104 | lpTokens, 105 | BigNumber.from(await this.pool.maturity()).sub(await currentTimestamp()), 106 | await this.pool.scaleFactor() 107 | ) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /tsconfig-publish.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "es2015", 5 | "importHelpers": true, 6 | "declaration": true, 7 | "sourceMap": true, 8 | "rootDir": "./src", 9 | "baseUrl": "./", 10 | "paths": { 11 | "*": ["src/*", "node_modules/*"] 12 | } 13 | }, 14 | "include": [ 15 | "src/**/*.ts" 16 | ] 17 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "resolveJsonModule": true 8 | }, 9 | "files": [ 10 | "./hardhat.config.ts", 11 | "./node_modules/@nomiclabs/hardhat-ethers/src/type-extensions.d.ts", 12 | "./node_modules/@nomiclabs/hardhat-waffle/src/type-extensions.d.ts", 13 | "./node_modules/truffle-typings/index.d.ts", 14 | "./@types/types.d.ts" 15 | ] 16 | } 17 | --------------------------------------------------------------------------------