├── .eslintignore ├── .eslintrc.js ├── .eslintrc.yaml ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── nightly.yml │ └── test.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .solcover.js ├── .solhint.json ├── .solhintignore ├── .yarn └── releases │ └── yarn-1.22.22.cjs ├── .yarnrc ├── LICENSE ├── README.md ├── contracts ├── GeyserRegistry.sol ├── ITokenGeyser.sol ├── ITokenPool.sol ├── TokenGeyser.sol ├── TokenPool.sol ├── _external │ └── ampleforth.sol ├── _mocks │ └── MockErc20.sol └── _utils │ └── SafeMathCompatibility.sol ├── deployments └── mainnet.yaml ├── hardhat.config.ts ├── package.json ├── tasks └── .DS_Store ├── test ├── helper.ts ├── registry.ts ├── staking.ts ├── token_pool.ts ├── token_unlock.ts └── unstake.ts ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | artifacts 3 | cache 4 | coverage 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: false, 4 | es2021: true, 5 | mocha: true, 6 | node: true, 7 | }, 8 | plugins: ["@typescript-eslint", "no-only-tests", "unused-imports"], 9 | extends: ["standard", "plugin:prettier/recommended", "plugin:node/recommended"], 10 | parser: "@typescript-eslint/parser", 11 | parserOptions: { 12 | ecmaVersion: 12, 13 | warnOnUnsupportedTypeScriptVersion: false, 14 | }, 15 | rules: { 16 | "node/no-unsupported-features/es-syntax": ["error", { ignores: ["modules"] }], 17 | "node/no-missing-import": [ 18 | "error", 19 | { 20 | tryExtensions: [".ts", ".js", ".json"], 21 | }, 22 | ], 23 | "node/no-unpublished-import": [ 24 | "error", 25 | { 26 | allowModules: [ 27 | "hardhat", 28 | "ethers", 29 | "@openzeppelin/upgrades-core", 30 | "chai", 31 | "@nomicfoundation/hardhat-ethers", 32 | "@nomicfoundation/hardhat-chai-matchers", 33 | "@nomicfoundation/hardhat-verify", 34 | "@nomicfoundation/hardhat-toolbox", 35 | "@openzeppelin/hardhat-upgrades", 36 | "solidity-coverage", 37 | "hardhat-gas-reporter", 38 | "dotenv", 39 | ], 40 | }, 41 | ], 42 | "no-only-tests/no-only-tests": "error", 43 | "unused-imports/no-unused-imports": "error", 44 | "unused-imports/no-unused-vars": ["warn", { vars: "all" }], 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /.eslintrc.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ampleforth/token-geyser/7541e2c999a448bcb23bdd4791fd613a1325ceff/.eslintrc.yaml -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yml: -------------------------------------------------------------------------------- 1 | name: Nightly 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | jobs: 8 | test: 9 | runs-on: ${{ matrix.os }} 10 | 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | node-version: [20.x] 15 | os: [ubuntu-latest] 16 | 17 | steps: 18 | - name: Setup Repo 19 | uses: actions/checkout@v4 20 | 21 | - name: Uses node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | 26 | - name: Install 27 | run: yarn install --immutable 28 | 29 | - name: Seutp 30 | run: yarn compile 31 | 32 | - name: Lint 33 | run: yarn lint 34 | 35 | - name: Test 36 | run: yarn coverage 37 | 38 | - name: spot-contracts report coverage 39 | uses: coverallsapp/github-action@v2.3.4 40 | with: 41 | github-token: ${{ secrets.GITHUB_TOKEN }} 42 | path-to-lcov: "./coverage/lcov.info" -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | test: 11 | runs-on: ${{ matrix.os }} 12 | 13 | strategy: 14 | matrix: 15 | node-version: [20.x] 16 | os: [ubuntu-latest] 17 | 18 | steps: 19 | - name: Setup Repo 20 | uses: actions/checkout@v4 21 | 22 | - name: Uses node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | 27 | - name: Install 28 | run: yarn install --immutable 29 | 30 | - name: Seutp 31 | run: yarn compile 32 | 33 | - name: Lint 34 | run: yarn lint 35 | 36 | - name: Test 37 | run: yarn test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | 4 | # Hardhat files 5 | /cache 6 | /artifacts 7 | 8 | # TypeChain files 9 | /typechain 10 | /typechain-types 11 | 12 | # solidity-coverage files 13 | /coverage 14 | /coverage.json 15 | 16 | 17 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # folders 2 | artifacts/ 3 | build/ 4 | cache/ 5 | coverage/ 6 | dist/ 7 | lib/ 8 | node_modules/ 9 | typechain/ 10 | 11 | # files 12 | coverage.json 13 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "endOfLine":"auto", 5 | "printWidth": 90, 6 | "singleQuote": false, 7 | "tabWidth": 2, 8 | "trailingComma": "all", 9 | "overrides": [ 10 | { 11 | "files": "*.sol", 12 | "options": { 13 | "tabWidth": 4 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | skipFiles: ["_test", "_interfaces", "_external"], 3 | }; 4 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "compiler-version": ["warn", "^0.8.0"], 5 | "func-visibility": ["warn", { "ignoreConstructors": true }], 6 | "reason-string": ["warn", { "maxLength": 64 }], 7 | "not-rely-on-time": "off", 8 | "max-states-count": ["warn", 17], 9 | "custom-errors": "off" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | yarn-path ".yarn/releases/yarn-1.22.22.cjs" 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Token Geyser 2 | 3 | [![Build Status](https://travis-ci.com/ampleforth/token-geyser.svg?token=o34Gqy9mFp6fX3Y6jzyy&branch=master)](https://travis-ci.com/ampleforth/token-geyser)  [![Coverage Status](https://coveralls.io/repos/github/ampleforth/token-geyser/badge.svg?branch=master&t=LdZfUk)](https://coveralls.io/github/ampleforth/token-geyser?branch=master) 4 | 5 | A smart-contract based mechanism to distribute tokens over time, inspired loosely by Compound and Uniswap. 6 | 7 | Implementation of [Continuous Vesting Token Distribution](https://github.com/ampleforth/RFCs/blob/master/RFCs/rfc-1.md) 8 | 9 | The official Geyser contract addresses are (by target): 10 | 11 | - UniswapV2 [ETH/AMPL](https://uniswap.exchange/swap?outputCurrency=0xd46ba6d942050d489dbd938a2c909a5d5039a161) Pool: [0xD36132E0c1141B26E62733e018f12Eb38A7b7678](https://etherscan.io/address/0xd36132e0c1141b26e62733e018f12eb38a7b7678) 12 | 13 | ## Table of Contents 14 | 15 | - [Install](#install) 16 | - [Testing](#testing) 17 | - [Contribute](#contribute) 18 | - [License](#license) 19 | 20 | ## Install 21 | 22 | ```bash 23 | # Install project dependencies 24 | npm install 25 | 26 | # Install ethereum local blockchain(s) and associated dependencies 27 | npx setup-local-chains 28 | ``` 29 | 30 | ## Testing 31 | 32 | ```bash 33 | # You can use the following command to start a local blockchain instance 34 | npx start-chain [ganacheUnitTest|gethUnitTest] 35 | 36 | # Run all unit tests 37 | npm test 38 | 39 | # Run unit tests in isolation 40 | npx mocha test/staking.js --exit 41 | ``` 42 | 43 | ## Contribute 44 | 45 | To report bugs within this package, please create an issue in this repository. 46 | When submitting code ensure that it is free of lint errors and has 100% test coverage. 47 | 48 | ```bash 49 | # Lint code 50 | npm run lint 51 | 52 | # View code coverage 53 | npm run coverage 54 | ``` 55 | 56 | ## License 57 | 58 | [GNU General Public License v3.0 (c) 2020 Fragments, Inc.](./LICENSE) 59 | -------------------------------------------------------------------------------- /contracts/GeyserRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.8.24; 3 | 4 | import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; 5 | 6 | /// @title GeyserRegistry 7 | contract GeyserRegistry is Ownable { 8 | mapping(address => bool) public geysers; 9 | 10 | event InstanceAdded(address instance); 11 | event InstanceRemoved(address instance); 12 | 13 | constructor() Ownable(msg.sender) {} 14 | 15 | function register(address instance) external onlyOwner { 16 | require(!geysers[instance], "GeyserRegistry: Geyser already registered"); 17 | geysers[instance] = true; 18 | emit InstanceAdded(instance); 19 | } 20 | 21 | function deregister(address instance) external onlyOwner { 22 | require(geysers[instance], "GeyserRegistry: Geyser not registered"); 23 | delete geysers[instance]; 24 | emit InstanceRemoved(instance); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /contracts/ITokenGeyser.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.24; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | /** 7 | * @title Geyser staking interface 8 | */ 9 | interface ITokenGeyser { 10 | function stake(uint256 amount) external; 11 | function unstake(uint256 amount) external returns (uint256); 12 | function totalStakedBy(address addr) external view returns (uint256); 13 | function totalStaked() external view returns (uint256); 14 | function totalLocked() external view returns (uint256); 15 | function totalUnlocked() external view returns (uint256); 16 | function stakingToken() external view returns (IERC20); 17 | function distributionToken() external view returns (IERC20); 18 | } 19 | -------------------------------------------------------------------------------- /contracts/ITokenPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.24; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | /** 7 | * @title Token pool interface 8 | */ 9 | interface ITokenPool { 10 | function init(IERC20 token_) external; 11 | function token() external view returns (IERC20); 12 | function balance() external view returns (uint256); 13 | function transfer(address to, uint256 value) external; 14 | function rescueFunds(address tokenToRescue, address to, uint256 amount) external; 15 | } 16 | -------------------------------------------------------------------------------- /contracts/TokenGeyser.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.24; 3 | 4 | import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; 5 | import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; 6 | import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; 7 | import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; 8 | import { Clones } from "@openzeppelin/contracts/proxy/Clones.sol"; 9 | import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 10 | import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; 11 | import { SafeMathCompatibility } from "./_utils/SafeMathCompatibility.sol"; 12 | import { ITokenPool } from "./ITokenPool.sol"; 13 | 14 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 15 | import { ITokenGeyser } from "./ITokenGeyser.sol"; 16 | 17 | /** 18 | * @title Token Geyser 19 | * @dev A smart-contract based mechanism to distribute tokens over time, inspired loosely by 20 | * Compound and Uniswap. 21 | * 22 | * Distribution tokens are added to a locked pool in the contract and become unlocked over time 23 | * according to a once-configurable unlock schedule. Once unlocked, they are available to be 24 | * claimed by users. 25 | * 26 | * A user may deposit tokens to accrue ownership share over the unlocked pool. This owner share 27 | * is a function of the number of tokens deposited as well as the length of time deposited. 28 | * Specifically, a user's share of the currently-unlocked pool equals their "deposit-seconds" 29 | * divided by the global "deposit-seconds". This aligns the new token distribution with long 30 | * term supporters of the project, addressing one of the major drawbacks of simple airdrops. 31 | * 32 | * More background and motivation available at: 33 | * https://github.com/ampleforth/RFCs/blob/master/RFCs/rfc-1.md 34 | */ 35 | contract TokenGeyser is 36 | ITokenGeyser, 37 | OwnableUpgradeable, 38 | PausableUpgradeable, 39 | ReentrancyGuardUpgradeable 40 | { 41 | using SafeMathCompatibility for uint256; 42 | using Math for uint256; 43 | using SafeERC20 for IERC20; 44 | using Math for uint256; 45 | 46 | //------------------------------------------------------------------------- 47 | // Events 48 | 49 | event Staked(address indexed user, uint256 amount, uint256 total); 50 | event Unstaked(address indexed user, uint256 amount, uint256 total); 51 | event TokensClaimed(address indexed user, uint256 amount); 52 | event TokensLocked(uint256 amount, uint256 durationSec, uint256 total); 53 | // amount: Unlocked tokens, total: Total locked tokens 54 | event TokensUnlocked(uint256 amount, uint256 total); 55 | 56 | //------------------------------------------------------------------------- 57 | // Storage 58 | 59 | ITokenPool public stakingPool; 60 | ITokenPool public unlockedPool; 61 | ITokenPool public lockedPool; 62 | 63 | // 64 | // Time-bonus params 65 | // 66 | uint256 public constant BONUS_DECIMALS = 2; 67 | uint256 public constant BONUS_HUNDRED_PERC = 10 ** BONUS_DECIMALS; 68 | uint256 public startBonus; 69 | uint256 public bonusPeriodSec; 70 | 71 | // 72 | // Global accounting state 73 | // 74 | uint256 public totalLockedShares; 75 | uint256 public totalStakingShares; 76 | uint256 public totalStakingShareSeconds; 77 | uint256 public lastAccountingTimestampSec; 78 | uint256 public maxUnlockSchedules; 79 | uint256 public initialSharesPerToken; 80 | 81 | // 82 | // User accounting state 83 | // 84 | // Represents a single stake for a user. A user may have multiple. 85 | struct Stake { 86 | uint256 stakingShares; 87 | uint256 timestampSec; 88 | } 89 | 90 | // Caches aggregated values from the User->Stake[] map to save computation. 91 | // If lastAccountingTimestampSec is 0, there's no entry for that user. 92 | struct UserTotals { 93 | uint256 stakingShares; 94 | uint256 stakingShareSeconds; 95 | uint256 lastAccountingTimestampSec; 96 | } 97 | 98 | // Aggregated staking values per user 99 | mapping(address => UserTotals) public userTotals; 100 | 101 | // The collection of stakes for each user. Ordered by timestamp, earliest to latest. 102 | mapping(address => Stake[]) public userStakes; 103 | 104 | // 105 | // Locked/Unlocked Accounting state 106 | // 107 | struct UnlockSchedule { 108 | uint256 initialLockedShares; 109 | uint256 unlockedShares; 110 | uint256 lastUnlockTimestampSec; 111 | uint256 endAtSec; 112 | uint256 durationSec; 113 | } 114 | 115 | UnlockSchedule[] public unlockSchedules; 116 | 117 | //------------------------------------------------------------------------- 118 | // Construction 119 | 120 | /// @custom:oz-upgrades-unsafe-allow constructor 121 | constructor() { 122 | _disableInitializers(); 123 | } 124 | 125 | /** 126 | * @param stakingToken_ The token users deposit as stake. 127 | * @param distributionToken_ The token users receive as they unstake. 128 | * @param maxUnlockSchedules_ Max number of unlock stages, to guard against hitting gas limit. 129 | * @param startBonus_ Starting time bonus, BONUS_DECIMALS fixed point. 130 | * e.g. 25% means user gets 25% of max distribution tokens. 131 | * @param bonusPeriodSec_ Length of time for bonus to increase linearly to max. 132 | * @param initialSharesPerToken_ Number of shares to mint per staking token on first stake. 133 | */ 134 | function init( 135 | address tokenPoolImpl, 136 | IERC20 stakingToken_, 137 | IERC20 distributionToken_, 138 | uint256 maxUnlockSchedules_, 139 | uint256 startBonus_, 140 | uint256 bonusPeriodSec_, 141 | uint256 initialSharesPerToken_ 142 | ) public initializer { 143 | __Ownable_init(msg.sender); 144 | __Pausable_init(); 145 | __ReentrancyGuard_init(); 146 | 147 | // The start bonus must be some fraction of the max. (i.e. <= 100%) 148 | require(startBonus_ <= 10 ** BONUS_DECIMALS, "TokenGeyser: start bonus too high"); 149 | // If no period is desired, instead set startBonus = 100% 150 | // and bonusPeriod to a small value like 1 sec. 151 | require(bonusPeriodSec_ != 0, "TokenGeyser: bonus period is zero"); 152 | require(initialSharesPerToken_ > 0, "TokenGeyser: initialSharesPerToken is zero"); 153 | 154 | stakingPool = ITokenPool(Clones.clone(tokenPoolImpl)); 155 | stakingPool.init(stakingToken_); 156 | 157 | unlockedPool = ITokenPool(Clones.clone(tokenPoolImpl)); 158 | unlockedPool.init(distributionToken_); 159 | 160 | lockedPool = ITokenPool(Clones.clone(tokenPoolImpl)); 161 | lockedPool.init(distributionToken_); 162 | 163 | startBonus = startBonus_; 164 | bonusPeriodSec = bonusPeriodSec_; 165 | 166 | totalLockedShares = 0; 167 | totalStakingShares = 0; 168 | totalStakingShareSeconds = 0; 169 | lastAccountingTimestampSec = block.timestamp; 170 | maxUnlockSchedules = maxUnlockSchedules_; 171 | initialSharesPerToken = initialSharesPerToken_; 172 | } 173 | 174 | //------------------------------------------------------------------------- 175 | // External and public methods 176 | 177 | /** 178 | * @return The token users deposit as stake. 179 | */ 180 | function stakingToken() public view override returns (IERC20) { 181 | return stakingPool.token(); 182 | } 183 | 184 | /** 185 | * @return The token users receive as they unstake. 186 | */ 187 | function distributionToken() public view override returns (IERC20) { 188 | assert(unlockedPool.token() == lockedPool.token()); 189 | return unlockedPool.token(); 190 | } 191 | 192 | /** 193 | * @notice Transfers amount of deposit tokens from the user. 194 | * @param amount Number of deposit tokens to stake. 195 | */ 196 | function stake(uint256 amount) external nonReentrant whenNotPaused { 197 | require(amount > 0, "TokenGeyser: stake amount is zero"); 198 | require( 199 | totalStakingShares == 0 || totalStaked() > 0, 200 | "TokenGeyser: Staking shares exist, but no staking tokens do" 201 | ); 202 | 203 | uint256 mintedStakingShares = computeStakingShares(amount); 204 | require(mintedStakingShares > 0, "TokenGeyser: Stake amount is too small"); 205 | 206 | _updateAccounting(); 207 | 208 | // 1. User Accounting 209 | UserTotals storage user = userTotals[msg.sender]; 210 | user.stakingShares = user.stakingShares.add(mintedStakingShares); 211 | user.lastAccountingTimestampSec = block.timestamp; 212 | 213 | Stake memory newStake = Stake(mintedStakingShares, block.timestamp); 214 | userStakes[msg.sender].push(newStake); 215 | 216 | // 2. Global Accounting 217 | totalStakingShares = totalStakingShares.add(mintedStakingShares); 218 | // Already set in _updateAccounting() 219 | // lastAccountingTimestampSec = block.timestamp; 220 | 221 | // interactions 222 | stakingPool.token().safeTransferFrom(msg.sender, address(stakingPool), amount); 223 | emit Staked(msg.sender, amount, totalStakedBy(msg.sender)); 224 | } 225 | 226 | /** 227 | * @notice Unstakes a certain amount of previously deposited tokens. User also receives their 228 | * allotted number of distribution tokens. 229 | * @param amount Number of deposit tokens to unstake / withdraw. 230 | */ 231 | function unstake( 232 | uint256 amount 233 | ) external nonReentrant whenNotPaused returns (uint256) { 234 | _updateAccounting(); 235 | 236 | // checks 237 | require(amount > 0, "TokenGeyser: unstake amount is zero"); 238 | require( 239 | totalStakedBy(msg.sender) >= amount, 240 | "TokenGeyser: unstake amount is greater than total user stakes" 241 | ); 242 | uint256 stakingSharesToBurn = totalStakingShares.mul(amount).div(totalStaked()); 243 | require( 244 | stakingSharesToBurn > 0, 245 | "TokenGeyser: Unable to unstake amount this small" 246 | ); 247 | 248 | // 1. User Accounting 249 | UserTotals storage user = userTotals[msg.sender]; 250 | Stake[] storage accountStakes = userStakes[msg.sender]; 251 | 252 | // Redeem from most recent stake and go backwards in time. 253 | uint256 stakingShareSecondsToBurn = 0; 254 | uint256 sharesLeftToBurn = stakingSharesToBurn; 255 | uint256 rewardAmount = 0; 256 | uint256 totalUnlocked_ = totalUnlocked(); 257 | while (sharesLeftToBurn > 0) { 258 | Stake storage lastStake = accountStakes[accountStakes.length - 1]; 259 | uint256 stakeTimeSec = block.timestamp.sub(lastStake.timestampSec); 260 | uint256 newStakingShareSecondsToBurn = 0; 261 | if (lastStake.stakingShares <= sharesLeftToBurn) { 262 | // fully redeem a past stake 263 | newStakingShareSecondsToBurn = lastStake.stakingShares.mul(stakeTimeSec); 264 | rewardAmount = computeNewReward( 265 | rewardAmount, 266 | newStakingShareSecondsToBurn, 267 | totalStakingShareSeconds, 268 | stakeTimeSec, 269 | totalUnlocked_ 270 | ); 271 | stakingShareSecondsToBurn = stakingShareSecondsToBurn.add( 272 | newStakingShareSecondsToBurn 273 | ); 274 | sharesLeftToBurn = sharesLeftToBurn.sub(lastStake.stakingShares); 275 | accountStakes.pop(); 276 | } else { 277 | // partially redeem a past stake 278 | newStakingShareSecondsToBurn = sharesLeftToBurn.mul(stakeTimeSec); 279 | rewardAmount = computeNewReward( 280 | rewardAmount, 281 | newStakingShareSecondsToBurn, 282 | totalStakingShareSeconds, 283 | stakeTimeSec, 284 | totalUnlocked_ 285 | ); 286 | stakingShareSecondsToBurn = stakingShareSecondsToBurn.add( 287 | newStakingShareSecondsToBurn 288 | ); 289 | lastStake.stakingShares = lastStake.stakingShares.sub(sharesLeftToBurn); 290 | sharesLeftToBurn = 0; 291 | } 292 | } 293 | user.stakingShareSeconds = user.stakingShareSeconds.sub( 294 | stakingShareSecondsToBurn 295 | ); 296 | user.stakingShares = user.stakingShares.sub(stakingSharesToBurn); 297 | // Already set in updateAccounting 298 | // user.lastAccountingTimestampSec = block.timestamp; 299 | 300 | // 2. Global Accounting 301 | totalStakingShareSeconds = totalStakingShareSeconds.sub( 302 | stakingShareSecondsToBurn 303 | ); 304 | totalStakingShares = totalStakingShares.sub(stakingSharesToBurn); 305 | // Already set in updateAccounting 306 | // lastAccountingTimestampSec = block.timestamp; 307 | 308 | // interactions 309 | stakingPool.transfer(msg.sender, amount); 310 | unlockedPool.transfer(msg.sender, rewardAmount); 311 | 312 | emit Unstaked(msg.sender, amount, totalStakedBy(msg.sender)); 313 | emit TokensClaimed(msg.sender, rewardAmount); 314 | 315 | require( 316 | totalStakingShares == 0 || totalStaked() > 0, 317 | "TokenGeyser: Staking shares exist, but no staking tokens do" 318 | ); 319 | return rewardAmount; 320 | } 321 | 322 | /** 323 | * @notice Applies an additional time-bonus to a distribution amount. This is necessary to 324 | * encourage long-term deposits instead of constant unstake/restakes. 325 | * The bonus-multiplier is the result of a linear function that starts at startBonus and 326 | * ends at 100% over bonusPeriodSec, then stays at 100% thereafter. 327 | * @param currentRewardTokens The current number of distribution tokens already alotted for this 328 | * unstake op. Any bonuses are already applied. 329 | * @param stakingShareSeconds The stakingShare-seconds that are being burned for new 330 | * distribution tokens. 331 | * @param totalStakingShareSeconds_ The total stakingShare-seconds. 332 | * @param stakeTimeSec Length of time for which the tokens were staked. Needed to calculate 333 | * the time-bonus. 334 | * @param totalUnlocked_ The reward tokens currently unlocked. 335 | * @return Updated amount of distribution tokens to award, with any bonus included on the 336 | * newly added tokens. 337 | */ 338 | function computeNewReward( 339 | uint256 currentRewardTokens, 340 | uint256 stakingShareSeconds, 341 | uint256 totalStakingShareSeconds_, 342 | uint256 stakeTimeSec, 343 | uint256 totalUnlocked_ 344 | ) public view returns (uint256) { 345 | uint256 newRewardTokens = (totalStakingShareSeconds_ > 0) 346 | ? totalUnlocked_.mul(stakingShareSeconds).div(totalStakingShareSeconds_) 347 | : 0; 348 | 349 | if (stakeTimeSec >= bonusPeriodSec) { 350 | return currentRewardTokens.add(newRewardTokens); 351 | } 352 | 353 | uint256 bonusedReward = startBonus 354 | .add(BONUS_HUNDRED_PERC.sub(startBonus).mul(stakeTimeSec).div(bonusPeriodSec)) 355 | .mul(newRewardTokens) 356 | .div(BONUS_HUNDRED_PERC); 357 | return currentRewardTokens.add(bonusedReward); 358 | } 359 | 360 | /** 361 | * @param addr The user to look up staking information for. 362 | * @return The number of staking tokens deposited for address. 363 | */ 364 | function totalStakedBy(address addr) public view returns (uint256) { 365 | return 366 | totalStakingShares > 0 367 | ? totalStaked().mulDiv( 368 | userTotals[addr].stakingShares, 369 | totalStakingShares, 370 | Math.Rounding.Ceil 371 | ) 372 | : 0; 373 | } 374 | 375 | /** 376 | * @return The total number of deposit tokens staked globally, by all users. 377 | */ 378 | function totalStaked() public view returns (uint256) { 379 | return stakingPool.balance(); 380 | } 381 | 382 | /** 383 | * @notice A globally callable function to update the accounting state of the system. 384 | * Global state and state for the caller are updated. 385 | */ 386 | function updateAccounting() external nonReentrant whenNotPaused { 387 | _updateAccounting(); 388 | } 389 | 390 | /** 391 | * @return Total number of locked distribution tokens. 392 | */ 393 | function totalLocked() public view override returns (uint256) { 394 | return lockedPool.balance(); 395 | } 396 | 397 | /** 398 | * @return Total number of unlocked distribution tokens. 399 | */ 400 | function totalUnlocked() public view override returns (uint256) { 401 | return unlockedPool.balance(); 402 | } 403 | 404 | /** 405 | * @return Number of unlock schedules. 406 | */ 407 | function unlockScheduleCount() external view returns (uint256) { 408 | return unlockSchedules.length; 409 | } 410 | 411 | /** 412 | * @param amount The amounted of tokens staked. 413 | * @return Total number staking shares minted to the user. 414 | */ 415 | function computeStakingShares(uint256 amount) public view returns (uint256) { 416 | return 417 | (totalStakingShares > 0) 418 | ? totalStakingShares.mul(amount).div(totalStaked()) 419 | : amount.mul(initialSharesPerToken); 420 | } 421 | 422 | /** 423 | * @return durationSec The amount of time in seconds when all the reward tokens unlock. 424 | */ 425 | function unlockDuration() external view returns (uint256 durationSec) { 426 | durationSec = 0; 427 | for (uint256 s = 0; s < unlockSchedules.length; s++) { 428 | durationSec = Math.max( 429 | (block.timestamp < unlockSchedules[s].endAtSec) 430 | ? unlockSchedules[s].endAtSec - block.timestamp 431 | : 0, 432 | durationSec 433 | ); 434 | } 435 | } 436 | 437 | /** 438 | * @notice Computes rewards and pool stats after `durationSec` has elapsed. 439 | * @param durationSec The amount of time in seconds the user continues to participate in the program. 440 | * @param addr The beneficiary wallet address. 441 | * @param additionalStake Any additional stake the user makes at the current block. 442 | * @return [0] Total rewards locked. 443 | * @return [1] Total rewards unlocked. 444 | * @return [2] Amount staked by the user. 445 | * @return [3] Total amount staked by all users. 446 | * @return [4] Total rewards unlocked. 447 | * @return [5] Timestamp after `durationSec`. 448 | */ 449 | function previewRewards( 450 | uint256 durationSec, 451 | address addr, 452 | uint256 additionalStake 453 | ) external view returns (uint256, uint256, uint256, uint256, uint256, uint256) { 454 | uint256 endTimestampSec = block.timestamp.add(durationSec); 455 | 456 | // Compute unlock schedule 457 | uint256 unlockedTokens = 0; 458 | { 459 | uint256 unlockedShares = 0; 460 | for (uint256 s = 0; s < unlockSchedules.length; s++) { 461 | UnlockSchedule memory schedule = unlockSchedules[s]; 462 | uint256 unlockedScheduleShares = (endTimestampSec >= schedule.endAtSec) 463 | ? schedule.initialLockedShares.sub(schedule.unlockedShares) 464 | : endTimestampSec 465 | .sub(schedule.lastUnlockTimestampSec) 466 | .mul(schedule.initialLockedShares) 467 | .div(schedule.durationSec); 468 | unlockedShares = unlockedShares.add(unlockedScheduleShares); 469 | } 470 | unlockedTokens = (totalLockedShares > 0) 471 | ? unlockedShares.mul(totalLocked()).div(totalLockedShares) 472 | : 0; 473 | } 474 | uint256 totalLocked_ = totalLocked().sub(unlockedTokens); 475 | uint256 totalUnlocked_ = totalUnlocked().add(unlockedTokens); 476 | 477 | // Compute new accounting state 478 | uint256 userStake = totalStakedBy(addr).add(additionalStake); 479 | uint256 totalStaked_ = totalStaked().add(additionalStake); 480 | 481 | // Compute user's final stake share and rewards 482 | uint256 rewardAmount = 0; 483 | { 484 | uint256 additionalStakingShareSeconds = durationSec.mul( 485 | computeStakingShares(additionalStake) 486 | ); 487 | 488 | uint256 newStakingShareSeconds = block 489 | .timestamp 490 | .sub(lastAccountingTimestampSec) 491 | .add(durationSec) 492 | .mul(totalStakingShares); 493 | uint256 totalStakingShareSeconds_ = totalStakingShareSeconds 494 | .add(newStakingShareSeconds) 495 | .add(additionalStakingShareSeconds); 496 | 497 | Stake[] memory accountStakes = userStakes[addr]; 498 | for (uint256 s = 0; s < accountStakes.length; s++) { 499 | Stake memory stake_ = accountStakes[s]; 500 | uint256 stakeDurationSec = endTimestampSec.sub(stake_.timestampSec); 501 | rewardAmount = computeNewReward( 502 | rewardAmount, 503 | stake_.stakingShares.mul(stakeDurationSec), 504 | totalStakingShareSeconds_, 505 | durationSec, 506 | totalUnlocked_ 507 | ); 508 | } 509 | rewardAmount = computeNewReward( 510 | rewardAmount, 511 | additionalStakingShareSeconds, 512 | totalStakingShareSeconds_, 513 | durationSec, 514 | totalUnlocked_ 515 | ); 516 | } 517 | 518 | return ( 519 | totalLocked_, 520 | totalUnlocked_, 521 | userStake, 522 | totalStaked_, 523 | rewardAmount, 524 | endTimestampSec 525 | ); 526 | } 527 | 528 | //------------------------------------------------------------------------- 529 | // Admin only methods 530 | 531 | /// @notice Pauses all user interactions. 532 | function pause() external onlyOwner { 533 | _pause(); 534 | } 535 | 536 | /// @notice Unpauses all user interactions. 537 | function unpause() external onlyOwner { 538 | _unpause(); 539 | } 540 | 541 | /** 542 | * @dev This function allows the contract owner to add more locked distribution tokens, along 543 | * with the associated "unlock schedule". These locked tokens immediately begin unlocking 544 | * linearly over the duration of durationSec time frame. 545 | * @param amount Number of distribution tokens to lock. These are transferred from the caller. 546 | * @param durationSec Length of time to linear unlock the tokens. 547 | */ 548 | function lockTokens(uint256 amount, uint256 durationSec) external onlyOwner { 549 | require( 550 | unlockSchedules.length < maxUnlockSchedules, 551 | "TokenGeyser: reached maximum unlock schedules" 552 | ); 553 | 554 | // Update lockedTokens amount before using it in computations after. 555 | _updateAccounting(); 556 | 557 | uint256 lockedTokens = totalLocked(); 558 | uint256 mintedLockedShares = (lockedTokens > 0) 559 | ? totalLockedShares.mul(amount).div(lockedTokens) 560 | : amount.mul(initialSharesPerToken); 561 | 562 | UnlockSchedule memory schedule; 563 | schedule.initialLockedShares = mintedLockedShares; 564 | schedule.lastUnlockTimestampSec = block.timestamp; 565 | schedule.endAtSec = block.timestamp.add(durationSec); 566 | schedule.durationSec = durationSec; 567 | unlockSchedules.push(schedule); 568 | 569 | totalLockedShares = totalLockedShares.add(mintedLockedShares); 570 | 571 | lockedPool.token().safeTransferFrom(msg.sender, address(lockedPool), amount); 572 | emit TokensLocked(amount, durationSec, totalLocked()); 573 | } 574 | 575 | /** 576 | * @dev Lets the owner rescue funds air-dropped to the staking pool. 577 | * @param tokenToRescue Address of the token to be rescued. 578 | * @param to Address to which the rescued funds are to be sent. 579 | * @param amount Amount of tokens to be rescued. 580 | */ 581 | function rescueFundsFromStakingPool( 582 | address tokenToRescue, 583 | address to, 584 | uint256 amount 585 | ) external onlyOwner { 586 | stakingPool.rescueFunds(tokenToRescue, to, amount); 587 | } 588 | 589 | //------------------------------------------------------------------------- 590 | // Private methods 591 | 592 | /** 593 | * @dev Updates time-dependent global storage state. 594 | */ 595 | function _updateAccounting() private { 596 | _unlockTokens(); 597 | 598 | // Global accounting 599 | uint256 newStakingShareSeconds = block 600 | .timestamp 601 | .sub(lastAccountingTimestampSec) 602 | .mul(totalStakingShares); 603 | totalStakingShareSeconds = totalStakingShareSeconds.add(newStakingShareSeconds); 604 | lastAccountingTimestampSec = block.timestamp; 605 | 606 | // User Accounting 607 | UserTotals storage user = userTotals[msg.sender]; 608 | uint256 newUserStakingShareSeconds = block 609 | .timestamp 610 | .sub(user.lastAccountingTimestampSec) 611 | .mul(user.stakingShares); 612 | user.stakingShareSeconds = user.stakingShareSeconds.add( 613 | newUserStakingShareSeconds 614 | ); 615 | user.lastAccountingTimestampSec = block.timestamp; 616 | } 617 | 618 | /** 619 | * @dev Unlocks distribution tokens based on reward schedule. 620 | */ 621 | function _unlockTokens() private returns (uint256) { 622 | uint256 unlockedTokens = 0; 623 | uint256 lockedTokens = totalLocked(); 624 | 625 | if (totalLockedShares == 0) { 626 | unlockedTokens = lockedTokens; 627 | } else { 628 | uint256 unlockedShares = 0; 629 | for (uint256 s = 0; s < unlockSchedules.length; s++) { 630 | unlockedShares = unlockedShares.add(_unlockScheduleShares(s)); 631 | } 632 | unlockedTokens = unlockedShares.mul(lockedTokens).div(totalLockedShares); 633 | totalLockedShares = totalLockedShares.sub(unlockedShares); 634 | } 635 | 636 | if (unlockedTokens > 0) { 637 | lockedPool.transfer(address(unlockedPool), unlockedTokens); 638 | emit TokensUnlocked(unlockedTokens, totalLocked()); 639 | } 640 | 641 | return unlockedTokens; 642 | } 643 | 644 | /** 645 | * @dev Returns the number of unlock-able shares from a given schedule. The returned value 646 | * depends on the time since the last unlock. This function updates schedule accounting, 647 | * but does not actually transfer any tokens. 648 | * @param s Index of the unlock schedule. 649 | * @return The number of unlocked shares. 650 | */ 651 | function _unlockScheduleShares(uint256 s) private returns (uint256) { 652 | UnlockSchedule storage schedule = unlockSchedules[s]; 653 | 654 | if (schedule.unlockedShares >= schedule.initialLockedShares) { 655 | return 0; 656 | } 657 | 658 | uint256 sharesToUnlock = 0; 659 | // Special case to handle any leftover dust from integer division 660 | if (block.timestamp >= schedule.endAtSec) { 661 | sharesToUnlock = (schedule.initialLockedShares.sub(schedule.unlockedShares)); 662 | schedule.lastUnlockTimestampSec = schedule.endAtSec; 663 | } else { 664 | sharesToUnlock = block 665 | .timestamp 666 | .sub(schedule.lastUnlockTimestampSec) 667 | .mul(schedule.initialLockedShares) 668 | .div(schedule.durationSec); 669 | schedule.lastUnlockTimestampSec = block.timestamp; 670 | } 671 | 672 | schedule.unlockedShares = schedule.unlockedShares.add(sharesToUnlock); 673 | return sharesToUnlock; 674 | } 675 | } 676 | -------------------------------------------------------------------------------- /contracts/TokenPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.24; 3 | 4 | import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; 5 | import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 6 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | import { ITokenPool } from "./ITokenPool.sol"; 8 | 9 | /** 10 | * @title A simple holder of tokens. 11 | * This is a simple contract to hold tokens. It's useful in the case where a separate contract 12 | * needs to hold multiple distinct pools of the same token. 13 | */ 14 | contract TokenPool is ITokenPool, OwnableUpgradeable { 15 | using SafeERC20 for IERC20; 16 | IERC20 public token; 17 | 18 | /// @custom:oz-upgrades-unsafe-allow constructor 19 | constructor() { 20 | _disableInitializers(); 21 | } 22 | 23 | function init(IERC20 token_) public initializer { 24 | __Ownable_init(msg.sender); 25 | token = token_; 26 | } 27 | 28 | function balance() public view override returns (uint256) { 29 | return token.balanceOf(address(this)); 30 | } 31 | 32 | function transfer(address to, uint256 value) external override onlyOwner { 33 | token.safeTransfer(to, value); 34 | } 35 | 36 | function rescueFunds( 37 | address tokenToRescue, 38 | address to, 39 | uint256 amount 40 | ) external override onlyOwner { 41 | require( 42 | address(token) != tokenToRescue, 43 | "TokenPool: Cannot claim token held by the contract" 44 | ); 45 | 46 | IERC20(tokenToRescue).safeTransfer(to, amount); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /contracts/_external/ampleforth.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | // solhint-disable-next-line no-unused-import 5 | import { UFragments } from "ampleforth-contracts/contracts/UFragments.sol"; 6 | -------------------------------------------------------------------------------- /contracts/_mocks/MockErc20.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.24; 2 | 3 | import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; 4 | 5 | contract MockERC20 is ERC20Upgradeable { 6 | constructor(uint256 _totalSupply) { 7 | _mint(msg.sender, _totalSupply); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /contracts/_utils/SafeMathCompatibility.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | // NOTE: Adding an intermediate library to support older version of safemath. 5 | library SafeMathCompatibility { 6 | function add(uint256 a, uint256 b) internal pure returns (uint256) { 7 | return a + b; 8 | } 9 | 10 | function sub(uint256 a, uint256 b) internal pure returns (uint256) { 11 | return a - b; 12 | } 13 | 14 | function mul(uint256 a, uint256 b) internal pure returns (uint256) { 15 | return a * b; 16 | } 17 | 18 | function div(uint256 a, uint256 b) internal pure returns (uint256) { 19 | // solhint-disable-next-line custom-errors 20 | require(b > 0, "SafeMath: division by zero"); 21 | return a / b; 22 | } 23 | 24 | function mod(uint256 a, uint256 b) internal pure returns (uint256) { 25 | // solhint-disable-next-line custom-errors 26 | require(b > 0, "SafeMath: modulo by zero"); 27 | return a % b; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /deployments/mainnet.yaml: -------------------------------------------------------------------------------- 1 | - poolRef: "UNI-ETHAMPL-V2 (Pilot)" 2 | stakingToken: '0xc5be99A02C6857f9Eac67BbCE58DF5572498F40c' 3 | distToken: '0xD46bA6D942050d489DBd938a2C909A5d5039A161' 4 | deployment: '0xD36132E0c1141B26E62733e018f12Eb38A7b7678' 5 | deploymentParams: "'0xc5be99A02C6857f9Eac67BbCE58DF5572498F40c', '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 10000, 33, 5184000, 1000000" 6 | onwer: '0x6723B7641c8Ac48a61F5f505aB1E9C03Bb44a301' 7 | 8 | - poolRef: "UNI-ETHAMPL-V2 (Beehive V1)" 9 | stakingToken: '0xc5be99A02C6857f9Eac67BbCE58DF5572498F40c' 10 | distToken: '0xD46bA6D942050d489DBd938a2C909A5d5039A161' 11 | deployment: '0x0eEf70ab0638A763acb5178Dd3C62E49767fd940' 12 | deploymentParams: "'0xc5be99A02C6857f9Eac67BbCE58DF5572498F40c', '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 10000, 33, 5184000, 1000000" 13 | onwer: '0x6723B7641c8Ac48a61F5f505aB1E9C03Bb44a301' 14 | 15 | - poolRef: "MOON-LINK-AMPL (Enceladus)" 16 | stakingToken: '0xd0a38cb0a67fa4ea83cac1f441e5aedfa9ba1155' 17 | distToken: '0xD46bA6D942050d489DBd938a2C909A5d5039A161' 18 | deployment: '0x4A26862D65cd3a316Ce40d27Fb5778aD923e5d4F' 19 | deploymentParams: "'0xd0a38cb0a67fa4ea83cac1f441e5aedfa9ba1155', '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 10000, 33, 5184000, 1000000" 20 | onwer: '0x6723B7641c8Ac48a61F5f505aB1E9C03Bb44a301' 21 | 22 | - poolRef: "MOON-BAL-AMPL (Enceladus)" 23 | stakingToken: '0xb1a75c05a04a95565451d5b98e793de566ffd0b3' 24 | distToken: '0xD46bA6D942050d489DBd938a2C909A5d5039A161' 25 | deployment: '0x106175d6cf20780b06944993185f7478ed046d11' 26 | deploymentParams: "'0xb1a75c05a04a95565451d5b98e793de566ffd0b3', '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 10000, 33, 5184000, 1000000" 27 | onwer: '0x6723B7641c8Ac48a61F5f505aB1E9C03Bb44a301' 28 | 29 | - poolRef: "MOON-LEND-AMPL (Enceladus)" 30 | stakingToken: '0xce61d71ef076688e8575f87614d946d82e9222bd' 31 | distToken: '0xD46bA6D942050d489DBd938a2C909A5d5039A161' 32 | deployment: '0x0142Ed98ECc63DAEc2A6CB90b73D6D405D6d9b83' 33 | deploymentParams: "'0xce61d71ef076688e8575f87614d946d82e9222bd', '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 10000, 33, 5184000, 1000000" 34 | onwer: '0x6723B7641c8Ac48a61F5f505aB1E9C03Bb44a301' 35 | 36 | - poolRef: "MOON-COMP-AMPL (Enceladus)" 37 | stakingToken: '0x2bc2fc7285f777406b5a29d52d58796f39184493' 38 | distToken: '0xD46bA6D942050d489DBd938a2C909A5d5039A161' 39 | deployment: '0x77f94Eb69B56792bBff41df00a56d35ebe39ce33' 40 | deploymentParams: "'0x2bc2fc7285f777406b5a29d52d58796f39184493', '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 10000, 33, 5184000, 1000000" 41 | onwer: '0x6723B7641c8Ac48a61F5f505aB1E9C03Bb44a301' 42 | 43 | - poolRef: "MOON-MKR-AMPL (Enceladus)" 44 | stakingToken: '0x84e632c0a8d02178d382a47679f8cbb37d28bfc1' 45 | distToken: '0xD46bA6D942050d489DBd938a2C909A5d5039A161' 46 | deployment: '0x946fca8Ab96cF5e3ee2476FA29736987D59F1c76' 47 | deploymentParams: "'0x84e632c0a8d02178d382a47679f8cbb37d28bfc1', '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 10000, 33, 5184000, 1000000" 48 | onwer: '0x6723B7641c8Ac48a61F5f505aB1E9C03Bb44a301' 49 | 50 | - poolRef: "MOON-CRV-AMPL (Enceladus)" 51 | stakingToken: '0x2e1c3b37e9c2e1ea1855b6b3fe886ae0f361757f' 52 | distToken: '0xD46bA6D942050d489DBd938a2C909A5d5039A161' 53 | deployment: '0x999f88aB581c5D3Fc3806b90E8A97E6D84E23500' 54 | deploymentParams: "'0x2e1c3b37e9c2e1ea1855b6b3fe886ae0f361757f', '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 10000, 33, 5184000, 1000000" 55 | onwer: '0x6723B7641c8Ac48a61F5f505aB1E9C03Bb44a301' 56 | 57 | - poolRef: "MOON-BZRX-AMPL (Enceladus)" 58 | stakingToken: '0xcaaf31704f6a19b32f590e21ad7acdbf7d4bb9fc' 59 | distToken: '0xD46bA6D942050d489DBd938a2C909A5d5039A161' 60 | deployment: '0xbeFfA3eCBcd244C979285BC4466CBe5899F89918' 61 | deploymentParams: "'0xcaaf31704f6a19b32f590e21ad7acdbf7d4bb9fc', '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 10000, 33, 5184000, 1000000" 62 | onwer: '0x6723B7641c8Ac48a61F5f505aB1E9C03Bb44a301' 63 | 64 | - poolRef: "MOON-YFI-AMPL (Enceladus)" 65 | stakingToken: '0x8626cb2422fff2742b6ce74730e40c958f7cfccc' 66 | distToken: '0xD46bA6D942050d489DBd938a2C909A5d5039A161' 67 | deployment: '0x8b0f1B4Abb263B5329B02e1b8c42b9E8F539f917' 68 | deploymentParams: "'0x8626cb2422fff2742b6ce74730e40c958f7cfccc', '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 10000, 33, 5184000, 1000000" 69 | onwer: '0x6723B7641c8Ac48a61F5f505aB1E9C03Bb44a301' 70 | 71 | - poolRef: "MOON-NMR-AMPL (Enceladus)" 72 | stakingToken: '0xc915e0e55911e4fdfefb6182e5fce0f47c5878f2' 73 | distToken: '0xD46bA6D942050d489DBd938a2C909A5d5039A161' 74 | deployment: '0xB5FAF2fC869f3AE44F192Cc0d28B471eCD4455cc' 75 | deploymentParams: "'0xc915e0e55911e4fdfefb6182e5fce0f47c5878f2', '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 10000, 33, 5184000, 1000000" 76 | onwer: '0x6723B7641c8Ac48a61F5f505aB1E9C03Bb44a301' 77 | 78 | - poolRef: "MOON-AMPL-y{USD} (Enceladus)" 79 | stakingToken: '0x57acfaf4a6d3cf64c37bcd9b66dbc1be36f8d121' 80 | distToken: '0xD46bA6D942050d489DBd938a2C909A5d5039A161' 81 | deployment: '0x79Fbe448A81f130410Cc3D66E89aE4a47598526e' 82 | deploymentParams: "'0x57acfaf4a6d3cf64c37bcd9b66dbc1be36f8d121', '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 10000, 33, 5184000, 1000000" 83 | onwer: '0x6723B7641c8Ac48a61F5f505aB1E9C03Bb44a301' 84 | 85 | - poolRef: "UNI-ETHAMPL-V2 (Beehive V2)" 86 | stakingToken: '0xc5be99A02C6857f9Eac67BbCE58DF5572498F40c' 87 | distToken: '0xD46bA6D942050d489DBd938a2C909A5d5039A161' 88 | deployment: '0x23796Bc856ed786dCC505984fd538f91dAD3194A' 89 | deploymentParams: "'0xc5be99A02C6857f9Eac67BbCE58DF5572498F40c', '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 10000, 33, 5184000, 1000000" 90 | onwer: '0x6723B7641c8Ac48a61F5f505aB1E9C03Bb44a301' 91 | 92 | - poolRef: "SUSHI-ETHAMPL (Pescadero V1)" 93 | stakingToken: '0xcb2286d9471cc185281c4f763d34a962ed212962' 94 | distToken: '0xD46bA6D942050d489DBd938a2C909A5d5039A161' 95 | deployment: '0xC88Ba3885995cE2714C14816a69a09880e1E518C' 96 | deploymentParams: "'0xcb2286d9471cc185281c4f763d34a962ed212962', '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 10000, 33, 2592000, 1000000" 97 | onwer: '0x6723B7641c8Ac48a61F5f505aB1E9C03Bb44a301' 98 | 99 | - poolRef: "BAL-SMART-AMPL-USDC (Old Faithful V1)" 100 | stakingToken: '0x49f2beff98ce62999792ec98d0ee4ad790e7786f' 101 | distToken: '0xD46bA6D942050d489DBd938a2C909A5d5039A161' 102 | deployment: '0x42d3c21DF4a26C06d7084f6319aCBF9195a583C1' 103 | deploymentParams: "'0x49f2beff98ce62999792ec98d0ee4ad790e7786f', '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 10000, 33, 2592000, 1000000" 104 | onwer: '0x6723B7641c8Ac48a61F5f505aB1E9C03Bb44a301' 105 | 106 | - poolRef: "UNI-ETHAMPL-V2 (Beehive V3)" 107 | stakingToken: '0xc5be99A02C6857f9Eac67BbCE58DF5572498F40c' 108 | distToken: '0xD46bA6D942050d489DBd938a2C909A5d5039A161' 109 | deployment: '0x075Bb66A472AB2BBB8c215629C77E8ee128CC2Fc' 110 | deploymentParams: "'0xc5be99A02C6857f9Eac67BbCE58DF5572498F40c', '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 10000, 33, 2592000, 1000000" 111 | onwer: '0x6723B7641c8Ac48a61F5f505aB1E9C03Bb44a301' 112 | 113 | - poolRef: "WBTC-WETH-AMPL-BPT (Trinity V1)" 114 | stakingToken: '0xa751a143f8fe0a108800bfb915585e4255c2fe80' 115 | distToken: '0xD46bA6D942050d489DBd938a2C909A5d5039A161' 116 | deployment: '0xcF98862a8eC1271c9019D47715565a0Bf3a761B8' 117 | deploymentParams: "'0xa751a143f8fe0a108800bfb915585e4255c2fe80', '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 10000, 33, 2592000, 1000000" 118 | onwer: '0x6723B7641c8Ac48a61F5f505aB1E9C03Bb44a301' 119 | 120 | 121 | # YET TO BE deployed :) 122 | # - poolRef: "MOON-WBTC-AMPL (Enceladus)" 123 | # stakingToken: '0x39dfb8e735e491d4d1d8f0fe7c174a68dd4ec240' 124 | # distToken: '0xD46bA6D942050d489DBd938a2C909A5d5039A161' 125 | # deployment: '' 126 | # deploymentParams: "'0x39dfb8e735e491d4d1d8f0fe7c174a68dd4ec240', '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 10000, 33, 5184000, 1000000" 127 | # onwer: '0x6723B7641c8Ac48a61F5f505aB1E9C03Bb44a301' 128 | 129 | # FAILED 130 | # - poolRef: "MOON-BZRX-AMPL (Enceladus)" 131 | # stakingToken: '0xcaaf31704f6a19b32f590e21ad7acdbf7d4bb9fc' 132 | # distToken: '0xD46bA6D942050d489DBd938a2C909A5d5039A161' 133 | # deployment: '0x6282f43eb6d935363a9b3de10275046c90cea417' 134 | # deploymentParams: "'0xcaaf31704f6a19b32f590e21ad7acdbf7d4bb9fc', '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 10000, 33, 5184000, 1000000" 135 | # onwer: '0x6723B7641c8Ac48a61F5f505aB1E9C03Bb44a301' 136 | 137 | # Discarded 138 | # - poolRef: "UNI-ETHAMPL-V2 (Beehive V3)" 139 | # stakingToken: '0xc5be99A02C6857f9Eac67BbCE58DF5572498F40c' 140 | # distToken: '0xD46bA6D942050d489DBd938a2C909A5d5039A161' 141 | # deployment: '0x09ea24a19b413f0f818566641db796032071bb6c' 142 | # deploymentParams: "'0xc5be99A02C6857f9Eac67BbCE58DF5572498F40c', '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 10000, 33, 5184000, 1000000" 143 | # onwer: '0x6723B7641c8Ac48a61F5f505aB1E9C03Bb44a301' 144 | 145 | # - poolRef: "SUSHI-ETHAMPL (Pescadero V1)" 146 | # stakingToken: '0xcb2286d9471cc185281c4f763d34a962ed212962' 147 | # distToken: '0xD46bA6D942050d489DBd938a2C909A5d5039A161' 148 | # deployment: '0x9780f5A0e52e98d6448971a13a2365CAe572aD77' 149 | # deploymentParams: "'0xcb2286d9471cc185281c4f763d34a962ed212962', '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 10000, 33, 5184000, 1000000" 150 | # onwer: '0x6723B7641c8Ac48a61F5f505aB1E9C03Bb44a301' 151 | 152 | # - poolRef: "BAL-SMART-AMPL-USDC (Old Faithful V1)" 153 | # stakingToken: '0x49f2beff98ce62999792ec98d0ee4ad790e7786f' 154 | # distToken: '0xD46bA6D942050d489DBd938a2C909A5d5039A161' 155 | # deployment: '0x88Ab9735Aa084b59FEB1fe2ADc1518EC6D2E32d6' 156 | # deploymentParams: "'0x49f2beff98ce62999792ec98d0ee4ad790e7786f', '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 10000, 33, 5184000, 1000000" 157 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from "hardhat/config"; 2 | import { Wallet } from "ethers"; 3 | 4 | import "@nomicfoundation/hardhat-ethers"; 5 | import "@nomicfoundation/hardhat-chai-matchers"; 6 | import "@nomicfoundation/hardhat-verify"; 7 | import "@openzeppelin/hardhat-upgrades"; 8 | import "solidity-coverage"; 9 | import "hardhat-gas-reporter"; 10 | 11 | // Loads custom tasks 12 | // import "./tasks/deploy"; 13 | 14 | // Loads env variables from .env file 15 | import * as dotenv from "dotenv"; 16 | dotenv.config(); 17 | 18 | export default { 19 | networks: { 20 | hardhat: { 21 | initialBaseFeePerGas: 0, 22 | accounts: { 23 | mnemonic: Wallet.createRandom().mnemonic.phrase, 24 | }, 25 | }, 26 | ganache: { 27 | url: "http://127.0.0.1:8545", 28 | chainId: 1337, 29 | }, 30 | sepolia: { 31 | url: `https://eth-sepolia.g.alchemy.com/v2/${process.env.ALCHEMY_SECRET}`, 32 | accounts: { 33 | mnemonic: process.env.PROD_MNEMONIC || Wallet.createRandom().mnemonic.phrase, 34 | }, 35 | gasMultiplier: 1.01, 36 | }, 37 | mainnet: { 38 | url: `https://eth-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_SECRET}`, 39 | accounts: { 40 | mnemonic: process.env.PROD_MNEMONIC || Wallet.createRandom().mnemonic.phrase, 41 | }, 42 | gasMultiplier: 1.005, 43 | }, 44 | }, 45 | solidity: { 46 | compilers: [ 47 | { 48 | version: "0.7.6", 49 | settings: { 50 | optimizer: { 51 | enabled: true, 52 | runs: 750, 53 | }, 54 | }, 55 | }, 56 | { 57 | version: "0.8.3", 58 | }, 59 | { 60 | version: "0.8.4", 61 | }, 62 | { 63 | version: "0.8.24", 64 | settings: { 65 | optimizer: { 66 | enabled: true, 67 | runs: 750, 68 | }, 69 | viaIR: true, 70 | }, 71 | }, 72 | ], 73 | }, 74 | gasReporter: { 75 | currency: "USD", 76 | enabled: !!process.env.REPORT_GAS, 77 | excludeContracts: ["_test/"], 78 | coinmarketcap: process.env.COINMARKETCAP_API_KEY, 79 | L1Etherscan: process.env.ETHERSCAN_API_KEY, 80 | }, 81 | etherscan: { 82 | apiKey: process.env.ETHERSCAN_API_KEY, 83 | }, 84 | mocha: { 85 | bail: false, 86 | timeout: 100000000, 87 | }, 88 | } as HardhatUserConfig; 89 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ampleforthorg/token-geyser", 3 | "description": "A smart-contract based mechanism to distribute tokens over time, inspired loosely by Compound and Uniswap.", 4 | "keywords": [ 5 | "ethereum", 6 | "smart-contracts", 7 | "solidity" 8 | ], 9 | "homepage": "https://github.com/ampleforth/continuous-vesting-distribution#readme", 10 | "bugs": { 11 | "url": "https://github.com/ampleforth/continuous-vesting-distribution/issues" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/ampleforth/continuous-vesting-distribution.git" 16 | }, 17 | "license": "GPL-3.0", 18 | "author": "dev-support@ampleforth.org", 19 | "scripts": { 20 | "compile": "yarn hardhat compile", 21 | "coverage": "yarn hardhat coverage --testfiles 'test/*.ts'", 22 | "lint": "yarn run lint:sol && yarn run lint:ts && yarn run prettier:list-different", 23 | "lint:fix": "yarn run prettier && yarn run lint:sol:fix && yarn run lint:ts:fix", 24 | "lint:sol": "solhint --config ./.solhint.json --max-warnings 0 \"contracts/**/*.sol\"", 25 | "lint:sol:fix": "solhint --config ./.solhint.json --fix --max-warnings 1 \"contracts/**/*.sol\"", 26 | "lint:ts": "eslint --config ./.eslintrc.yaml --ignore-path ./.eslintignore --ext .js,.ts .", 27 | "lint:ts:fix": "eslint --config ./.eslintrc.yaml --fix --ignore-path ./.eslintignore --ext .js,.ts .", 28 | "prettier": "prettier --config .prettierrc --write \"**/*.{js,json,md,sol,ts}\"", 29 | "prettier:list-different": "prettier --config .prettierrc --list-different \"**/*.{js,json,md,sol,ts}\"", 30 | "profile": "REPORT_GAS=true yarn hardhat test test/*.ts", 31 | "test": "yarn hardhat test test/*.ts" 32 | }, 33 | "dependencies": { 34 | "@openzeppelin/contracts": "5.1.0", 35 | "@openzeppelin/contracts-upgradeable": "5.1.0", 36 | "ampleforth-contracts": "https://github.com/ampleforth/ampleforth-contracts#master" 37 | }, 38 | "devDependencies": { 39 | "@ethersproject/abi": "^5.6.4", 40 | "@ethersproject/abstract-provider": "^5.7.0", 41 | "@ethersproject/abstract-signer": "^5.7.0", 42 | "@ethersproject/bytes": "^5.6.1", 43 | "@ethersproject/providers": "^5.6.8", 44 | "@nomicfoundation/hardhat-chai-matchers": "latest", 45 | "@nomicfoundation/hardhat-ethers": "^3.0.0", 46 | "@nomicfoundation/hardhat-ignition-ethers": "^0.15.0", 47 | "@nomicfoundation/hardhat-network-helpers": "^1.0.0", 48 | "@nomicfoundation/hardhat-toolbox": "latest", 49 | "@nomicfoundation/hardhat-verify": "latest", 50 | "@nomiclabs/hardhat-waffle": "^2.0.6", 51 | "@openzeppelin/hardhat-upgrades": "^3.0.4", 52 | "@openzeppelin/upgrades-core": "latest", 53 | "@typechain/ethers-v6": "^0.5.1", 54 | "@typechain/hardhat": "^6.1.2", 55 | "@types/chai": "^4.3.1", 56 | "@types/mocha": "^9.1.1", 57 | "@types/node": "^18.6.1", 58 | "@types/sinon-chai": "^3.2.12", 59 | "@typescript-eslint/eslint-plugin": "^5.0.0", 60 | "@typescript-eslint/parser": "^5.0.0", 61 | "chai": "^4.3.6", 62 | "dotenv": "^16.0.1", 63 | "eslint": "^8.20.0", 64 | "eslint-config-prettier": "^8.5.0", 65 | "eslint-config-standard": "^17.0.0", 66 | "eslint-plugin-import": "^2.26.0", 67 | "eslint-plugin-n": "^15.2.4", 68 | "eslint-plugin-no-only-tests": "^3.1.0", 69 | "eslint-plugin-node": "^11.1.0", 70 | "eslint-plugin-prettier": "^4.2.1", 71 | "eslint-plugin-promise": "^6.0.0", 72 | "eslint-plugin-unused-imports": "^3.0.0", 73 | "ethereum-waffle": "latest", 74 | "ethers": "^6.6.0", 75 | "ethers-v5": "npm:ethers@^5.7.0", 76 | "ganache-cli": "latest", 77 | "hardhat": "^2.22.8", 78 | "hardhat-gas-reporter": "latest", 79 | "lodash": "^4.17.21", 80 | "prettier": "^2.7.1", 81 | "prettier-plugin-solidity": "^1.0.0-dev.23", 82 | "solhint": "^3.3.7", 83 | "solidity-coverage": "^0.8.5", 84 | "ts-node": "^10.9.1", 85 | "typechain": "^8.1.0", 86 | "typescript": "^4.7.4" 87 | }, 88 | "engines": { 89 | "node": ">=20" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tasks/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ampleforth/token-geyser/7541e2c999a448bcb23bdd4791fd613a1325ceff/tasks/.DS_Store -------------------------------------------------------------------------------- /test/helper.ts: -------------------------------------------------------------------------------- 1 | import hre, { ethers, upgrades } from "hardhat"; 2 | import { expect } from "chai"; 3 | 4 | const AMPL_DECIMALS = 9; 5 | 6 | function $AMPL(x: number) { 7 | return ethers.parseUnits(x.toFixed(AMPL_DECIMALS), AMPL_DECIMALS); 8 | } 9 | 10 | // Perc has to be a whole number 11 | async function invokeRebase(ampl, perc) { 12 | await ampl.rebase(1, ((await ampl.totalSupply()) * BigInt(perc)) / 100n); 13 | } 14 | 15 | function checkAmplAprox(x, y) { 16 | checkAprox(x, $AMPL(y), BigInt(10 ** 7)); 17 | } 18 | 19 | function checkSharesAprox(x, y) { 20 | checkAprox(x, y, BigInt(10 ** 12)); 21 | } 22 | 23 | function checkAprox(x, y, delta_) { 24 | const delta = BigInt(delta_); 25 | const upper = BigInt(y) + delta; 26 | const lower = BigInt(y) - delta; 27 | expect(x).to.gte(lower).to.lte(upper); 28 | } 29 | 30 | export const TimeHelpers = { 31 | secondsFromNow: async (secondsFromNow: number): Promise => { 32 | return (await TimeHelpers.currentTime()) + secondsFromNow; 33 | }, 34 | 35 | moveClock: async (seconds: number): Promise => { 36 | await hre.network.provider.send("evm_increaseTime", [seconds]); 37 | }, 38 | 39 | advanceBlock: async () => { 40 | await hre.network.provider.send("evm_mine"); 41 | }, 42 | 43 | increaseTime: async (seconds: number): Promise => { 44 | await hre.network.provider.send("evm_increaseTime", [seconds]); 45 | await hre.network.provider.send("evm_mine"); 46 | }, 47 | 48 | setNextBlockTimestamp: async (timestamp: number): Promise => { 49 | await ethers.provider.send("evm_setNextBlockTimestamp", [timestamp]); 50 | await hre.network.provider.send("evm_mine"); 51 | }, 52 | 53 | currentTime: async (): Promise => { 54 | const res = await hre.network.provider.send("eth_getBlockByNumber", [ 55 | "latest", 56 | false, 57 | ]); 58 | const timestamp = parseInt(res.timestamp, 16); 59 | return timestamp; 60 | }, 61 | }; 62 | 63 | async function printMethodOutput(r) { 64 | console.log(r.logs); 65 | } 66 | 67 | async function printStatus(dist) { 68 | console.log("Total Locked: ", await dist.totalLocked.staticCall().toString()); 69 | console.log("Total UnLocked: ", await dist.totalUnlocked.staticCall().toString()); 70 | const c = (await dist.unlockScheduleCount.staticCall()).toNumber(); 71 | console.log(await dist.unlockScheduleCount.staticCall().toString()); 72 | 73 | for (let i = 0; i < c; i++) { 74 | console.log(await dist.unlockSchedules.staticCall(i).toString()); 75 | } 76 | // await dist.totalLocked.staticCall() 77 | // await dist.totalUnlocked.staticCall() 78 | // await dist.unlockScheduleCount.staticCall() 79 | // dist.updateAccounting.staticCall() // and all the logs 80 | // dist.unlockSchedules.staticCall(1) 81 | } 82 | 83 | async function deployGeyser(owner, params) { 84 | const TokenGeyser = await ethers.getContractFactory("TokenGeyser"); 85 | const dist = await upgrades.deployProxy(TokenGeyser.connect(owner), params, { 86 | initializer: "init(address,address,address,uint256,uint256,uint256,uint256)", 87 | }); 88 | return dist; 89 | } 90 | 91 | module.exports = { 92 | checkAprox, 93 | checkAmplAprox, 94 | checkSharesAprox, 95 | invokeRebase, 96 | $AMPL, 97 | TimeHelpers, 98 | printMethodOutput, 99 | printStatus, 100 | deployGeyser, 101 | }; 102 | -------------------------------------------------------------------------------- /test/registry.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { expect } from "chai"; 3 | import { Contract } from "ethers"; 4 | 5 | // Generated by chat-gpt 6 | describe("GeyserRegistry Contract", () => { 7 | let GeyserRegistry: Contract; 8 | let deployer: any; 9 | let nonOwner: any; 10 | let geyserAddress: string; 11 | let unregisteredAddress: string; 12 | 13 | beforeEach(async () => { 14 | [deployer, nonOwner] = await ethers.getSigners(); 15 | geyserAddress = ethers.Wallet.createRandom().address; // Mock address 16 | unregisteredAddress = ethers.Wallet.createRandom().address; // Mock address 17 | 18 | const GeyserRegistryFactory = await ethers.getContractFactory( 19 | "GeyserRegistry", 20 | deployer, 21 | ); 22 | GeyserRegistry = await GeyserRegistryFactory.deploy(); 23 | }); 24 | 25 | describe("Deployment", () => { 26 | it("should deploy with the correct owner", async () => { 27 | const owner = await GeyserRegistry.owner(); 28 | expect(owner).to.equal(deployer.address); 29 | }); 30 | 31 | it("should have no geysers initially", async () => { 32 | const isGeyser = await GeyserRegistry.geysers(geyserAddress); 33 | expect(isGeyser).to.equal(false); 34 | }); 35 | }); 36 | 37 | describe("Registering a Geyser", () => { 38 | it("should allow the owner to register a geyser", async () => { 39 | const tx = await GeyserRegistry.register(geyserAddress); 40 | await tx.wait(); 41 | 42 | const isGeyser = await GeyserRegistry.geysers(geyserAddress); 43 | expect(isGeyser).to.equal(true); 44 | }); 45 | 46 | it("should emit InstanceAdded when a geyser is registered", async () => { 47 | await expect(GeyserRegistry.register(geyserAddress)) 48 | .to.emit(GeyserRegistry, "InstanceAdded") 49 | .withArgs(geyserAddress); 50 | }); 51 | 52 | it("should revert if a non-owner tries to register", async () => { 53 | await expect( 54 | GeyserRegistry.connect(nonOwner).register(geyserAddress), 55 | ).to.be.revertedWithCustomError(GeyserRegistry, "OwnableUnauthorizedAccount"); 56 | }); 57 | 58 | it("should revert if the geyser is already registered", async () => { 59 | await GeyserRegistry.register(geyserAddress); 60 | await expect(GeyserRegistry.register(geyserAddress)).to.be.revertedWith( 61 | "GeyserRegistry: Geyser already registered", 62 | ); 63 | }); 64 | }); 65 | 66 | describe("Deregistering a Geyser", () => { 67 | beforeEach(async function () { 68 | await GeyserRegistry.register(geyserAddress); 69 | }); 70 | 71 | it("should allow the owner to deregister a geyser", async () => { 72 | const tx = await GeyserRegistry.deregister(geyserAddress); 73 | await tx.wait(); 74 | 75 | const isGeyser = await GeyserRegistry.geysers(geyserAddress); 76 | expect(isGeyser).to.equal(false); 77 | }); 78 | 79 | it("should emit InstanceRemoved when a geyser is deregistered", async () => { 80 | await expect(GeyserRegistry.deregister(geyserAddress)) 81 | .to.emit(GeyserRegistry, "InstanceRemoved") 82 | .withArgs(geyserAddress); 83 | }); 84 | 85 | it("should revert if a non-owner tries to deregister", async () => { 86 | await expect( 87 | GeyserRegistry.connect(nonOwner).deregister(geyserAddress), 88 | ).to.be.revertedWithCustomError(GeyserRegistry, "OwnableUnauthorizedAccount"); 89 | }); 90 | 91 | it("should revert if the geyser is not registered", async () => { 92 | await expect(GeyserRegistry.deregister(unregisteredAddress)).to.be.revertedWith( 93 | "GeyserRegistry: Geyser not registered", 94 | ); 95 | }); 96 | }); 97 | 98 | describe("Edge Cases", () => { 99 | it("should not allow re-registering the same geyser address", async () => { 100 | await GeyserRegistry.register(geyserAddress); 101 | await expect(GeyserRegistry.register(geyserAddress)).to.be.revertedWith( 102 | "GeyserRegistry: Geyser already registered", 103 | ); 104 | }); 105 | 106 | it("should revert when deregistering an unregistered address", async () => { 107 | await expect(GeyserRegistry.deregister(unregisteredAddress)).to.be.revertedWith( 108 | "GeyserRegistry: Geyser not registered", 109 | ); 110 | }); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /test/staking.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { expect } from "chai"; 3 | import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; 4 | import { $AMPL, invokeRebase, deployGeyser } from "../test/helper"; 5 | import { SignerWithAddress } from "ethers"; 6 | 7 | let ampl: any, 8 | tokenPoolImpl: any, 9 | dist: any, 10 | owner: SignerWithAddress, 11 | anotherAccount: SignerWithAddress; 12 | const InitialSharesPerToken = BigInt(10 ** 6); 13 | 14 | describe("staking", function () { 15 | async function setupContracts() { 16 | [owner, anotherAccount] = await ethers.getSigners(); 17 | 18 | const AmpleforthErc20 = await ethers.getContractFactory("UFragments"); 19 | ampl = await AmpleforthErc20.deploy(); 20 | await ampl.initialize(await owner.getAddress()); 21 | await ampl.setMonetaryPolicy(await owner.getAddress()); 22 | 23 | const TokenPool = await ethers.getContractFactory("TokenPool"); 24 | const tokenPoolImpl = await TokenPool.deploy(); 25 | 26 | dist = await deployGeyser(owner, [ 27 | tokenPoolImpl.target, 28 | ampl.target, 29 | ampl.target, 30 | 10, 31 | 50, 32 | 86400, 33 | InitialSharesPerToken, 34 | ]); 35 | 36 | return { ampl, tokenPoolImpl, dist, owner, anotherAccount }; 37 | } 38 | 39 | beforeEach(async function () { 40 | ({ ampl, tokenPoolImpl, dist, owner, anotherAccount } = await loadFixture( 41 | setupContracts, 42 | )); 43 | }); 44 | 45 | describe("when start bonus too high", function () { 46 | it("should fail to construct", async function () { 47 | await expect( 48 | deployGeyser(owner, [ 49 | tokenPoolImpl.target, 50 | ampl.target, 51 | ampl.target, 52 | 10, 53 | 101, 54 | 86400, 55 | InitialSharesPerToken, 56 | ]), 57 | ).to.be.revertedWith("TokenGeyser: start bonus too high"); 58 | }); 59 | }); 60 | 61 | describe("when bonus period is 0", function () { 62 | it("should fail to construct", async function () { 63 | await expect( 64 | deployGeyser(owner, [ 65 | tokenPoolImpl.target, 66 | ampl.target, 67 | ampl.target, 68 | 10, 69 | 50, 70 | 0, 71 | InitialSharesPerToken, 72 | ]), 73 | ).to.be.revertedWith("TokenGeyser: bonus period is zero"); 74 | }); 75 | }); 76 | 77 | describe("#pause", function () { 78 | describe("when triggered by non-owner", function () { 79 | it("should revert", async function () { 80 | await dist.transferOwnership(anotherAccount); 81 | await expect(dist.pause()).to.be.revertedWithCustomError( 82 | dist, 83 | "OwnableUnauthorizedAccount", 84 | ); 85 | }); 86 | }); 87 | 88 | describe("when already paused", function () { 89 | it("should revert", async function () { 90 | await dist.pause(); 91 | await expect(dist.pause()).to.be.revertedWithCustomError(dist, "EnforcedPause"); 92 | }); 93 | }); 94 | 95 | describe("when valid", function () { 96 | it("should pause", async function () { 97 | await dist.pause(); 98 | expect(await dist.paused()).to.eq(true); 99 | }); 100 | }); 101 | }); 102 | 103 | describe("#unpause", function () { 104 | describe("when triggered by non-owner", function () { 105 | it("should revert", async function () { 106 | await dist.pause(); 107 | await dist.transferOwnership(anotherAccount); 108 | await expect(dist.unpause()).to.be.revertedWithCustomError( 109 | dist, 110 | "OwnableUnauthorizedAccount", 111 | ); 112 | }); 113 | }); 114 | 115 | describe("when not paused", function () { 116 | it("should revert", async function () { 117 | await expect(dist.unpause()).to.be.revertedWithCustomError(dist, "ExpectedPause"); 118 | }); 119 | }); 120 | 121 | describe("when valid", function () { 122 | it("should unpause", async function () { 123 | await dist.pause(); 124 | await dist.unpause(); 125 | expect(await dist.paused()).to.eq(false); 126 | }); 127 | }); 128 | }); 129 | 130 | describe("owner", function () { 131 | it("should return the owner", async function () { 132 | expect(await dist.owner()).to.equal(await owner.getAddress()); 133 | }); 134 | }); 135 | 136 | describe("stakingToken", function () { 137 | it("should return the staking token", async function () { 138 | expect(await dist.stakingToken()).to.equal(ampl.target); 139 | }); 140 | }); 141 | 142 | describe("stake", function () { 143 | describe("when the amount is 0", function () { 144 | it("should fail", async function () { 145 | await ampl.approve(dist.target, $AMPL(1000)); 146 | await expect(dist.stake($AMPL(0))).to.be.revertedWith( 147 | "TokenGeyser: stake amount is zero", 148 | ); 149 | }); 150 | }); 151 | 152 | describe("when token transfer has not been approved", function () { 153 | it("should fail", async function () { 154 | await expect(dist.stake($AMPL(100))).to.be.reverted; 155 | }); 156 | }); 157 | 158 | describe("when totalStaked=0", function () { 159 | beforeEach(async function () { 160 | expect(await dist.totalStaked()).to.equal($AMPL(0)); 161 | await ampl.approve(dist.target, $AMPL(100)); 162 | }); 163 | it("should update the total staked", async function () { 164 | await dist.stake($AMPL(100)); 165 | expect(await dist.totalStaked()).to.equal($AMPL(100)); 166 | expect(await dist.totalStakedBy(await owner.getAddress())).to.equal($AMPL(100)); 167 | expect(await dist.totalStakingShares()).to.equal( 168 | $AMPL(100) * InitialSharesPerToken, 169 | ); 170 | }); 171 | it("should log Staked", async function () { 172 | const tx = await dist.stake($AMPL(100)); 173 | await expect(tx) 174 | .to.emit(dist, "Staked") 175 | .withArgs(await owner.getAddress(), $AMPL(100), $AMPL(100)); 176 | }); 177 | }); 178 | 179 | describe("when totalStaked>0", function () { 180 | beforeEach(async function () { 181 | expect(await dist.totalStaked()).to.equal($AMPL(0)); 182 | await ampl.transfer(await anotherAccount.getAddress(), $AMPL(50)); 183 | await ampl.connect(anotherAccount).approve(dist.target, $AMPL(50)); 184 | await dist.connect(anotherAccount).stake($AMPL(50)); 185 | await ampl.approve(dist.target, $AMPL(150)); 186 | await dist.stake($AMPL(150)); 187 | }); 188 | it("should update the total staked", async function () { 189 | expect(await dist.totalStaked()).to.equal($AMPL(200)); 190 | expect(await dist.totalStakedBy(await anotherAccount.getAddress())).to.equal( 191 | $AMPL(50), 192 | ); 193 | expect(await dist.totalStakedBy(await owner.getAddress())).to.equal($AMPL(150)); 194 | expect(await dist.totalStakingShares()).to.equal( 195 | $AMPL(200) * InitialSharesPerToken, 196 | ); 197 | }); 198 | }); 199 | 200 | describe("when totalStaked>0, rebase increases supply", function () { 201 | beforeEach(async function () { 202 | expect(await dist.totalStaked()).to.equal($AMPL(0)); 203 | await ampl.transfer(await anotherAccount.getAddress(), $AMPL(50)); 204 | await ampl.connect(anotherAccount).approve(dist.target, $AMPL(50)); 205 | await dist.connect(anotherAccount).stake($AMPL(50)); 206 | await ampl.approve(dist.target, $AMPL(150)); 207 | await invokeRebase(ampl, 100); 208 | expect(await dist.totalStaked()).to.equal($AMPL(100)); 209 | await dist.stake($AMPL(150)); 210 | }); 211 | it("should updated the total staked shares", async function () { 212 | expect(await dist.totalStaked()).to.equal($AMPL(250)); 213 | expect(await dist.totalStakedBy(await anotherAccount.getAddress())).to.equal( 214 | $AMPL(100), 215 | ); 216 | expect(await dist.totalStakedBy(await owner.getAddress())).to.equal($AMPL(150)); 217 | expect(await dist.totalStakingShares()).to.equal( 218 | $AMPL(125) * InitialSharesPerToken, 219 | ); 220 | }); 221 | }); 222 | 223 | describe("when totalStaked>0, when rebase increases supply", function () { 224 | beforeEach(async function () { 225 | await ampl.approve(dist.target, $AMPL(51)); 226 | await dist.stake($AMPL(50)); 227 | }); 228 | it("should fail if there are too few mintedStakingShares", async function () { 229 | await invokeRebase(ampl, 100n * InitialSharesPerToken); 230 | await expect(dist.stake(1)).to.be.revertedWith( 231 | "TokenGeyser: Stake amount is too small", 232 | ); 233 | }); 234 | }); 235 | 236 | describe("when totalStaked>0, rebase decreases supply", function () { 237 | beforeEach(async function () { 238 | expect(await dist.totalStaked()).to.equal($AMPL(0)); 239 | await ampl.transfer(await anotherAccount.getAddress(), $AMPL(50)); 240 | await ampl.connect(anotherAccount).approve(dist.target, $AMPL(50)); 241 | await dist.connect(anotherAccount).stake($AMPL(50)); 242 | await ampl.approve(dist.target, $AMPL(150)); 243 | await invokeRebase(ampl, -50); 244 | expect(await dist.totalStaked()).to.equal($AMPL(25)); 245 | await dist.stake($AMPL(150)); 246 | }); 247 | it("should updated the total staked shares", async function () { 248 | expect(await dist.totalStaked()).to.equal($AMPL(175)); 249 | expect(await dist.totalStakedBy(await anotherAccount.getAddress())).to.equal( 250 | $AMPL(25), 251 | ); 252 | expect(await dist.totalStakedBy(await owner.getAddress())).to.equal($AMPL(150)); 253 | expect(await dist.totalStakingShares()).to.equal( 254 | $AMPL(350) * InitialSharesPerToken, 255 | ); 256 | }); 257 | }); 258 | }); 259 | }); 260 | -------------------------------------------------------------------------------- /test/token_pool.ts: -------------------------------------------------------------------------------- 1 | import { ethers, upgrades } from "hardhat"; 2 | import { expect } from "chai"; 3 | import { SignerWithAddress } from "ethers"; 4 | import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; 5 | 6 | let owner: SignerWithAddress, anotherAccount: SignerWithAddress; 7 | 8 | describe("TokenPool", function () { 9 | async function setupContracts() { 10 | [owner, anotherAccount] = await ethers.getSigners(); 11 | 12 | const MockERC20 = await ethers.getContractFactory("MockERC20"); 13 | const token = await MockERC20.deploy(1000); 14 | const otherToken = await MockERC20.deploy(2000); 15 | 16 | const TokenPool = await ethers.getContractFactory("TokenPool"); 17 | const tokenPool = await upgrades.deployProxy( 18 | TokenPool.connect(owner), 19 | [token.target], 20 | { 21 | initializer: "init(address)", 22 | }, 23 | ); 24 | 25 | return { token, otherToken, tokenPool, owner, anotherAccount }; 26 | } 27 | 28 | describe("balance", function () { 29 | it("should return the balance of the token pool", async function () { 30 | const { token, tokenPool, owner } = await loadFixture(setupContracts); 31 | 32 | await token.transfer(tokenPool.target, 123); 33 | expect(await tokenPool.balance()).to.equal(123); 34 | await tokenPool.transfer(await owner.getAddress(), 99); 35 | expect(await tokenPool.balance()).to.equal(24); 36 | await tokenPool.transfer(await owner.getAddress(), 24); 37 | expect(await tokenPool.balance()).to.equal(0); 38 | }); 39 | }); 40 | 41 | describe("transfer", function () { 42 | it("should let the owner transfer funds out", async function () { 43 | const { token, tokenPool, anotherAccount } = await loadFixture(setupContracts); 44 | 45 | await token.transfer(tokenPool.target, 1000); 46 | 47 | expect(await tokenPool.balance()).to.equal(1000); 48 | expect(await token.balanceOf(await anotherAccount.getAddress())).to.equal(0); 49 | 50 | await tokenPool.transfer(await anotherAccount.getAddress(), 1000); 51 | 52 | expect(await tokenPool.balance()).to.equal(0); 53 | expect(await token.balanceOf(await anotherAccount.getAddress())).to.equal(1000); 54 | }); 55 | 56 | it("should NOT let other users transfer funds out", async function () { 57 | const { token, tokenPool, anotherAccount } = await loadFixture(setupContracts); 58 | 59 | await token.transfer(tokenPool.target, 1000); 60 | await expect( 61 | tokenPool 62 | .connect(anotherAccount) 63 | .transfer(await anotherAccount.getAddress(), 1000), 64 | ).to.be.revertedWithCustomError(tokenPool, "OwnableUnauthorizedAccount"); 65 | }); 66 | }); 67 | 68 | describe("rescueFunds", function () { 69 | it("should let owner users claim excess funds completely", async function () { 70 | const { token, otherToken, tokenPool, anotherAccount } = await loadFixture( 71 | setupContracts, 72 | ); 73 | 74 | await token.transfer(tokenPool.target, 1000); 75 | await otherToken.transfer(tokenPool.target, 2000); 76 | 77 | await tokenPool.rescueFunds( 78 | otherToken.target, 79 | await anotherAccount.getAddress(), 80 | 2000, 81 | ); 82 | 83 | expect(await tokenPool.balance()).to.equal(1000); 84 | expect(await token.balanceOf(await anotherAccount.getAddress())).to.equal(0); 85 | expect(await otherToken.balanceOf(tokenPool.target)).to.equal(0); 86 | expect(await otherToken.balanceOf(await anotherAccount.getAddress())).to.equal( 87 | 2000, 88 | ); 89 | }); 90 | 91 | it("should let owner users claim excess funds partially", async function () { 92 | const { token, otherToken, tokenPool, anotherAccount } = await loadFixture( 93 | setupContracts, 94 | ); 95 | 96 | await token.transfer(tokenPool.target, 1000); 97 | await otherToken.transfer(tokenPool.target, 2000); 98 | 99 | await tokenPool.rescueFunds( 100 | otherToken.target, 101 | await anotherAccount.getAddress(), 102 | 777, 103 | ); 104 | 105 | expect(await tokenPool.balance()).to.equal(1000); 106 | expect(await token.balanceOf(await anotherAccount.getAddress())).to.equal(0); 107 | expect(await otherToken.balanceOf(tokenPool.target)).to.equal(1223); 108 | expect(await otherToken.balanceOf(await anotherAccount.getAddress())).to.equal(777); 109 | }); 110 | 111 | it("should NOT let owner claim more than available excess funds", async function () { 112 | const { otherToken, tokenPool, anotherAccount } = await loadFixture(setupContracts); 113 | 114 | await otherToken.transfer(tokenPool.target, 2000); 115 | 116 | await expect( 117 | tokenPool.rescueFunds(otherToken.target, await anotherAccount.getAddress(), 2001), 118 | ).to.be.revertedWithCustomError(otherToken, "ERC20InsufficientBalance"); 119 | }); 120 | 121 | it("should NOT let owner users claim held funds", async function () { 122 | const { token, tokenPool, anotherAccount } = await loadFixture(setupContracts); 123 | 124 | await token.transfer(tokenPool.target, 1000); 125 | 126 | await expect( 127 | tokenPool.rescueFunds(token.target, await anotherAccount.getAddress(), 1000), 128 | ).to.be.revertedWith("TokenPool: Cannot claim token held by the contract"); 129 | }); 130 | 131 | it("should NOT let other users claim excess funds", async function () { 132 | const { otherToken, tokenPool, anotherAccount } = await loadFixture(setupContracts); 133 | 134 | await otherToken.transfer(tokenPool.target, 2000); 135 | 136 | await expect( 137 | tokenPool 138 | .connect(anotherAccount) 139 | .rescueFunds(otherToken.target, await anotherAccount.getAddress(), 2000), 140 | ).to.be.revertedWithCustomError(tokenPool, "OwnableUnauthorizedAccount"); 141 | }); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /test/token_unlock.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { expect } from "chai"; 3 | import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; 4 | import { 5 | TimeHelpers, 6 | $AMPL, 7 | invokeRebase, 8 | checkAprox, 9 | checkAmplAprox, 10 | checkSharesAprox, 11 | deployGeyser, 12 | } from "../test/helper"; 13 | import { SignerWithAddress } from "ethers"; 14 | 15 | let ampl: any, 16 | tokenPoolImpl: any, 17 | dist: any, 18 | owner: SignerWithAddress, 19 | anotherAccount: SignerWithAddress; 20 | const InitialSharesPerToken = BigInt(10 ** 6); 21 | const ONE_YEAR = 365 * 24 * 3600; 22 | const START_BONUS = 50; 23 | const BONUS_PERIOD = 86400; 24 | 25 | async function setupContracts() { 26 | [owner, anotherAccount] = await ethers.getSigners(); 27 | 28 | const AmpleforthErc20 = await ethers.getContractFactory("UFragments"); 29 | ampl = await AmpleforthErc20.deploy(); 30 | await ampl.initialize(await owner.getAddress()); 31 | await ampl.setMonetaryPolicy(await owner.getAddress()); 32 | 33 | const TokenPool = await ethers.getContractFactory("TokenPool"); 34 | const tokenPoolImpl = await TokenPool.deploy(); 35 | 36 | dist = await deployGeyser(owner, [ 37 | tokenPoolImpl.target, 38 | ampl.target, 39 | ampl.target, 40 | 10, 41 | START_BONUS, 42 | BONUS_PERIOD, 43 | InitialSharesPerToken, 44 | ]); 45 | 46 | return { ampl, tokenPoolImpl, dist, owner, anotherAccount }; 47 | } 48 | 49 | async function checkAvailableToUnlock(dist, v) { 50 | const u = await dist.totalUnlocked.staticCall(); 51 | const from = await owner.getAddress(); 52 | const r = await dist.previewRewards.staticCall(0, from, 0); 53 | // console.log( 54 | // "Total unlocked: ", 55 | // u.toString(), 56 | // "total unlocked after: ", 57 | // r[1].toString(), 58 | // ); 59 | checkAmplAprox(r[1] - u, v); 60 | } 61 | 62 | describe("LockedPool", function () { 63 | beforeEach("setup contracts", async function () { 64 | ({ ampl, tokenPoolImpl, dist, owner, anotherAccount } = await loadFixture( 65 | setupContracts, 66 | )); 67 | }); 68 | 69 | describe("distributionToken", function () { 70 | it("should return the staking token", async function () { 71 | expect(await dist.distributionToken.staticCall()).to.equal(ampl.target); 72 | }); 73 | }); 74 | 75 | describe("lockTokens", function () { 76 | describe("when not approved", function () { 77 | it("should fail", async function () { 78 | const d = await deployGeyser(owner, [ 79 | tokenPoolImpl.target, 80 | ampl.target, 81 | ampl.target, 82 | 5n, 83 | START_BONUS, 84 | BONUS_PERIOD, 85 | InitialSharesPerToken, 86 | ]); 87 | await expect(d.lockTokens($AMPL(10), ONE_YEAR)).to.be.reverted; 88 | }); 89 | }); 90 | 91 | describe("when number of unlock schedules exceeds the maxUnlockSchedules", function () { 92 | it("should fail", async function () { 93 | const d = await deployGeyser(owner, [ 94 | tokenPoolImpl.target, 95 | ampl.target, 96 | ampl.target, 97 | 5n, 98 | START_BONUS, 99 | BONUS_PERIOD, 100 | InitialSharesPerToken, 101 | ]); 102 | await ampl.approve(d.target, $AMPL(100)); 103 | for (let i = 0; i < 5; i++) { 104 | await d.lockTokens($AMPL(10), ONE_YEAR); 105 | } 106 | await expect(d.lockTokens($AMPL(10), ONE_YEAR)).to.be.revertedWith( 107 | "TokenGeyser: reached maximum unlock schedules", 108 | ); 109 | }); 110 | }); 111 | 112 | describe("when totalLocked=0", function () { 113 | beforeEach(async function () { 114 | checkAmplAprox(await dist.totalLocked(), 0); 115 | await ampl.approve(dist.target, $AMPL(100)); 116 | }); 117 | it("should updated the locked pool balance", async function () { 118 | await dist.lockTokens($AMPL(100), ONE_YEAR); 119 | checkAmplAprox(await dist.totalLocked(), 100); 120 | }); 121 | it("should create a schedule", async function () { 122 | await dist.lockTokens($AMPL(100), ONE_YEAR); 123 | const s = await dist.unlockSchedules(0); 124 | expect(s.initialLockedShares).to.equal( 125 | $AMPL(100) * BigInt(InitialSharesPerToken), 126 | ); 127 | expect(s.unlockedShares).to.equal($AMPL(0)); 128 | expect(s.lastUnlockTimestampSec + s.durationSec).to.equal(s.endAtSec); 129 | expect(s.durationSec).to.equal(ONE_YEAR); 130 | expect(await dist.unlockScheduleCount()).to.equal(1); 131 | }); 132 | it("should log TokensLocked", async function () { 133 | const r = await dist.lockTokens($AMPL(100), ONE_YEAR); 134 | await expect(r) 135 | .to.emit(dist, "TokensLocked") 136 | .withArgs($AMPL(100), ONE_YEAR, $AMPL(100)); 137 | }); 138 | it("should be protected", async function () { 139 | await ampl.approve(dist.target, $AMPL(100)); 140 | await expect( 141 | dist.connect(anotherAccount).lockTokens($AMPL(50), ONE_YEAR), 142 | ).to.be.revertedWithCustomError(dist, "OwnableUnauthorizedAccount"); 143 | await dist.lockTokens($AMPL(50), ONE_YEAR); 144 | }); 145 | }); 146 | 147 | describe("when totalLocked>0", function () { 148 | beforeEach(async function () { 149 | await ampl.approve(dist.target, $AMPL(150)); 150 | await dist.lockTokens($AMPL(100), ONE_YEAR); 151 | checkAmplAprox(await dist.totalLocked(), 100); 152 | await TimeHelpers.increaseTime(ONE_YEAR / 10); 153 | }); 154 | it("should update the locked and unlocked pool balance", async function () { 155 | await dist.lockTokens($AMPL(50), ONE_YEAR); 156 | checkAmplAprox(await dist.totalLocked(), 100 * 0.9 + 50); 157 | }); 158 | it("should log TokensUnlocked and TokensLocked", async function () { 159 | const r = await dist.lockTokens($AMPL(50), ONE_YEAR); 160 | const txR = await r.wait(); 161 | let l = txR.logs.find(l => l.fragment?.name === "TokensUnlocked"); 162 | checkAmplAprox(l.args.amount, 100 * 0.1); 163 | checkAmplAprox(l.args.total, 100 * 0.9); 164 | 165 | l = txR.logs.find(l => l.fragment?.name === "TokensLocked"); 166 | checkAmplAprox(l.args.amount, 50); 167 | checkAmplAprox(l.args.total, 100 * 0.9 + 50); 168 | expect(l.args.durationSec).to.eq(ONE_YEAR); 169 | }); 170 | it("should create a schedule", async function () { 171 | await dist.lockTokens($AMPL(50), ONE_YEAR); 172 | const s = await dist.unlockSchedules(1); 173 | // struct UnlockSchedule { 174 | // 0 uint256 initialLockedShares; 175 | // 1 uint256 unlockedShares; 176 | // 2 uint256 lastUnlockTimestampSec; 177 | // 3 uint256 endAtSec; 178 | // 4 uint256 durationSec; 179 | // } 180 | checkSharesAprox(s[0], $AMPL(50) * BigInt(InitialSharesPerToken)); 181 | checkSharesAprox(s[1], 0n); 182 | expect(s[2] + s[4]).to.equal(s[3]); 183 | expect(s[4]).to.equal(ONE_YEAR); 184 | expect(await dist.unlockScheduleCount()).to.equal(2); 185 | }); 186 | }); 187 | 188 | describe("when totalLocked>0", function () { 189 | beforeEach(async function () { 190 | await ampl.approve(dist.target, $AMPL(150)); 191 | await dist.lockTokens($AMPL(100), ONE_YEAR); 192 | checkAmplAprox(await dist.totalLocked.staticCall(), 100); 193 | await TimeHelpers.increaseTime(ONE_YEAR / 10); 194 | }); 195 | it("should updated the locked and unlocked pool balance", async function () { 196 | await dist.lockTokens($AMPL(50), ONE_YEAR); 197 | checkAmplAprox(await dist.totalLocked.staticCall(), 100 * 0.9 + 50); 198 | }); 199 | it("should log TokensUnlocked and TokensLocked", async function () { 200 | const r = await dist.lockTokens($AMPL(50), ONE_YEAR); 201 | const txR = await r.wait(); 202 | let l = txR.logs.find(l => l.fragment?.name === "TokensUnlocked"); 203 | checkAmplAprox(l.args.amount, 100 * 0.1); 204 | checkAmplAprox(l.args.total, 100 * 0.9); 205 | 206 | l = txR.logs.find(l => l.fragment?.name === "TokensLocked"); 207 | checkAmplAprox(l.args.amount, 50); 208 | checkAmplAprox(l.args.total, 100 * 0.9 + 50); 209 | expect(l.args.durationSec).to.eq(ONE_YEAR); 210 | }); 211 | it("should create a schedule", async function () { 212 | await dist.lockTokens($AMPL(50), ONE_YEAR); 213 | const s = await dist.unlockSchedules.staticCall(1); 214 | checkSharesAprox(s[0], $AMPL(50) * BigInt(InitialSharesPerToken)); 215 | checkSharesAprox(s[1], 0n); 216 | expect(s[2] + s[4]).to.equal(s[3]); 217 | expect(s[4]).to.equal(ONE_YEAR); 218 | expect(await dist.unlockScheduleCount()).to.equal(2); 219 | }); 220 | }); 221 | 222 | describe("when totalLocked>0, rebase increases supply", function () { 223 | beforeEach(async function () { 224 | await ampl.approve(dist.target, $AMPL(150)); 225 | await dist.lockTokens($AMPL(100), ONE_YEAR); 226 | checkAmplAprox(await dist.totalLocked.staticCall(), 100); 227 | await TimeHelpers.increaseTime(ONE_YEAR / 10); 228 | await invokeRebase(ampl, 100); 229 | }); 230 | it("should update the locked pool balance", async function () { 231 | await dist.lockTokens($AMPL(50), ONE_YEAR); 232 | checkAmplAprox(await dist.totalLocked.staticCall(), 50 + 200 * 0.9); 233 | }); 234 | it("should log TokensUnlocked and TokensLocked", async function () { 235 | const r = await dist.lockTokens($AMPL(50), ONE_YEAR); 236 | const txR = await r.wait(); 237 | let l = txR.logs.find(l => l.fragment?.name === "TokensUnlocked"); 238 | checkAmplAprox(l.args.amount, 200 * 0.1); 239 | checkAmplAprox(l.args.total, 200 * 0.9); 240 | 241 | l = txR.logs.find(l => l.fragment?.name === "TokensLocked"); 242 | checkAmplAprox(l.args.amount, 50); 243 | checkAmplAprox(l.args.total, 50 + 200 * 0.9); 244 | expect(l.args.durationSec).to.eq(ONE_YEAR); 245 | }); 246 | it("should create a schedule", async function () { 247 | await dist.lockTokens($AMPL(50), ONE_YEAR); 248 | const s = await dist.unlockSchedules.staticCall(1); 249 | checkSharesAprox(s[0], $AMPL(25) * BigInt(InitialSharesPerToken)); 250 | checkSharesAprox(s[1], 0n); 251 | expect(s[2] + s[4]).to.equal(s[3]); 252 | expect(s[4]).to.equal(ONE_YEAR); 253 | expect(await dist.unlockScheduleCount()).to.equal(2); 254 | }); 255 | }); 256 | 257 | describe("when totalLocked>0, rebase decreases supply", function () { 258 | beforeEach(async function () { 259 | await ampl.approve(dist.target, $AMPL(150)); 260 | await dist.lockTokens($AMPL(100), ONE_YEAR); 261 | checkAmplAprox(await dist.totalLocked.staticCall(), 100); 262 | await TimeHelpers.increaseTime(ONE_YEAR / 10); 263 | await invokeRebase(ampl, -50); 264 | }); 265 | it("should updated the locked pool balance", async function () { 266 | await dist.lockTokens($AMPL(50), ONE_YEAR); 267 | checkAmplAprox(await dist.totalLocked.staticCall(), 0.9 * 50 + 50); 268 | }); 269 | it("should log TokensUnlocked and TokensLocked", async function () { 270 | const r = await dist.lockTokens($AMPL(50), ONE_YEAR); 271 | const txR = await r.wait(); 272 | 273 | let l = txR.logs.find(l => l.fragment?.name === "TokensUnlocked"); 274 | checkAmplAprox(l.args.amount, 50 * 0.1); 275 | checkAmplAprox(l.args.total, 50 * 0.9); 276 | 277 | l = txR.logs.find(l => l.fragment?.name === "TokensLocked"); 278 | checkAmplAprox(l.args.amount, 50); 279 | checkAmplAprox(l.args.total, 50 + 50 * 0.9); 280 | expect(l.args.durationSec).to.eq(ONE_YEAR); 281 | }); 282 | it("should create a schedule", async function () { 283 | await dist.lockTokens($AMPL(50), ONE_YEAR); 284 | const s = await dist.unlockSchedules.staticCall(1); 285 | checkSharesAprox(s[0], $AMPL(100) * BigInt(InitialSharesPerToken)); 286 | checkSharesAprox(s[1], 0n); 287 | expect(s[2] + s[4]).to.equal(s[3]); 288 | expect(s[4]).to.equal(ONE_YEAR); 289 | expect(await dist.unlockScheduleCount()).to.equal(2); 290 | }); 291 | }); 292 | }); 293 | 294 | describe("unlockTokens", function () { 295 | describe("single schedule", function () { 296 | describe("after waiting for 1/2 the duration", function () { 297 | beforeEach(async function () { 298 | await ampl.approve(dist.target, $AMPL(100)); 299 | await dist.lockTokens($AMPL(100), ONE_YEAR); 300 | await TimeHelpers.increaseTime(ONE_YEAR / 2); 301 | }); 302 | 303 | describe("when supply is unchanged", function () { 304 | it("should unlock 1/2 the tokens", async function () { 305 | expect(await dist.totalLocked()).to.eq($AMPL(100)); 306 | expect(await dist.totalUnlocked()).to.eq($AMPL(0)); 307 | await checkAvailableToUnlock(dist, 50); 308 | }); 309 | it("should transfer tokens to unlocked pool", async function () { 310 | await dist.updateAccounting(); 311 | checkAmplAprox(await dist.totalLocked(), 50); 312 | checkAmplAprox(await dist.totalUnlocked(), 50); 313 | await checkAvailableToUnlock(dist, 0); 314 | }); 315 | it("should log TokensUnlocked and update state", async function () { 316 | const r = await dist.updateAccounting(); 317 | const receipt = await r.wait(); 318 | const event = receipt.events?.find(event => event.event === "TokensUnlocked"); 319 | if (event && event.args) { 320 | checkAmplAprox(event.args.amount, 50); 321 | checkAmplAprox(event.args.total, 50); 322 | } 323 | const s = await dist.unlockSchedules(0); 324 | expect(s[0]).to.eq($AMPL(100) * InitialSharesPerToken); 325 | checkSharesAprox(s[1], $AMPL(50) * InitialSharesPerToken); 326 | }); 327 | }); 328 | 329 | describe("when rebase increases supply", function () { 330 | beforeEach(async function () { 331 | await invokeRebase(ampl, 100); 332 | }); 333 | it("should unlock 1/2 the tokens", async function () { 334 | expect(await dist.totalLocked()).to.eq($AMPL(200)); 335 | expect(await dist.totalUnlocked()).to.eq($AMPL(0)); 336 | await checkAvailableToUnlock(dist, 100); 337 | }); 338 | it("should transfer tokens to unlocked pool", async function () { 339 | await dist.updateAccounting(); 340 | checkAmplAprox(await dist.totalLocked(), 100); 341 | checkAmplAprox(await dist.totalUnlocked(), 100); 342 | await checkAvailableToUnlock(dist, 0); 343 | }); 344 | }); 345 | 346 | describe("when rebase decreases supply", function () { 347 | beforeEach(async function () { 348 | await invokeRebase(ampl, -50); 349 | }); 350 | it("should unlock 1/2 the tokens", async function () { 351 | expect(await dist.totalLocked()).to.eq($AMPL(50)); 352 | await checkAvailableToUnlock(dist, 25); 353 | }); 354 | it("should transfer tokens to unlocked pool", async function () { 355 | expect(await dist.totalLocked()).to.eq($AMPL(50)); 356 | expect(await dist.totalUnlocked()).to.eq($AMPL(0)); 357 | await dist.updateAccounting(); 358 | checkAmplAprox(await dist.totalLocked(), 25); 359 | checkAmplAprox(await dist.totalUnlocked(), 25); 360 | await checkAvailableToUnlock(dist, 0); 361 | }); 362 | }); 363 | }); 364 | 365 | describe("after waiting > the duration", function () { 366 | beforeEach(async function () { 367 | await ampl.approve(dist.target, $AMPL(100)); 368 | await dist.lockTokens($AMPL(100), ONE_YEAR); 369 | await TimeHelpers.increaseTime(2 * ONE_YEAR); 370 | }); 371 | it("should unlock all the tokens", async function () { 372 | await checkAvailableToUnlock(dist, 100); 373 | }); 374 | it("should transfer tokens to unlocked pool", async function () { 375 | expect(await dist.totalLocked()).to.eq($AMPL(100)); 376 | expect(await dist.totalUnlocked()).to.eq($AMPL(0)); 377 | await dist.updateAccounting(); 378 | expect(await dist.totalLocked()).to.eq($AMPL(0)); 379 | checkAmplAprox(await dist.totalUnlocked(), 100); 380 | await checkAvailableToUnlock(dist, 0); 381 | }); 382 | it("should log TokensUnlocked and update state", async function () { 383 | const r = await dist.updateAccounting(); 384 | const receipt = await r.wait(); 385 | const event = receipt.events?.find(event => event.event === "TokensUnlocked"); 386 | if (event && event.args) { 387 | checkAmplAprox(event.args.amount, 50); 388 | checkAmplAprox(event.args.total, 50); 389 | } 390 | const s = await dist.unlockSchedules(0); 391 | expect(s[0]).to.eq($AMPL(100) * InitialSharesPerToken); 392 | checkSharesAprox(s[1], $AMPL(100) * InitialSharesPerToken); 393 | }); 394 | }); 395 | 396 | describe("dust tokens due to division underflow", function () { 397 | beforeEach(async function () { 398 | await ampl.approve(dist.target, $AMPL(100)); 399 | await dist.lockTokens($AMPL(1), 10 * ONE_YEAR); 400 | }); 401 | it("should unlock all tokens", async function () { 402 | await TimeHelpers.increaseTime(10 * ONE_YEAR - 60); 403 | const r1 = await dist.updateAccounting(); 404 | const receipt1 = await r1.wait(); 405 | const l1 = receipt1.events?.find(event => event.event === "TokensUnlocked"); 406 | await TimeHelpers.increaseTime(65); 407 | const r2 = await dist.updateAccounting(); 408 | const receipt2 = await r2.wait(); 409 | const l2 = receipt2.events?.find(event => event.event === "TokensUnlocked"); 410 | if (l1 && l2 && l1.args && l2.args) { 411 | expect(l1.args.amount.add(l2.args.amount)).to.eq($AMPL(1)); 412 | } 413 | }); 414 | }); 415 | }); 416 | 417 | describe("multi schedule", function () { 418 | beforeEach(async function () { 419 | await ampl.approve(dist.target, $AMPL(200)); 420 | await dist.lockTokens($AMPL(100), ONE_YEAR); 421 | await TimeHelpers.increaseTime(ONE_YEAR / 2); 422 | await dist.lockTokens($AMPL(100), ONE_YEAR); 423 | await TimeHelpers.increaseTime(ONE_YEAR / 10); 424 | }); 425 | 426 | it("should return the remaining unlock value", async function () { 427 | checkAmplAprox(await dist.totalLocked(), 150); 428 | checkAmplAprox(await dist.totalUnlocked(), 50); 429 | await checkAvailableToUnlock(dist, 20); 430 | }); 431 | 432 | it("should transfer tokens to unlocked pool", async function () { 433 | await dist.updateAccounting(); 434 | checkAmplAprox(await dist.totalLocked(), 130); 435 | checkAmplAprox(await dist.totalUnlocked(), 70); 436 | await checkAvailableToUnlock(dist, 0); 437 | }); 438 | 439 | it("should log TokensUnlocked and update state", async function () { 440 | const r = await dist.updateAccounting(); 441 | const receipt = await r.wait(); 442 | const l = receipt.events?.find(event => event.event === "TokensUnlocked"); 443 | if (l?.args) { 444 | checkAmplAprox(l.args.amount, 20); 445 | checkAmplAprox(l.args.total, 130); 446 | } 447 | 448 | const s1 = await dist.unlockSchedules(0); 449 | checkSharesAprox(s1[0], $AMPL(100) * InitialSharesPerToken); 450 | checkSharesAprox(s1[1], $AMPL(60) * InitialSharesPerToken); 451 | const s2 = await dist.unlockSchedules(1); 452 | checkSharesAprox(s2[0], $AMPL(100) * InitialSharesPerToken); 453 | checkSharesAprox(s2[1], $AMPL(10) * InitialSharesPerToken); 454 | }); 455 | 456 | it("should continue linear the unlock", async function () { 457 | await dist.updateAccounting(); 458 | await TimeHelpers.increaseTime(ONE_YEAR / 5); 459 | await dist.updateAccounting(); 460 | 461 | checkAmplAprox(await dist.totalLocked(), 90); 462 | checkAmplAprox(await dist.totalUnlocked(), 110); 463 | await checkAvailableToUnlock(dist, 0); 464 | 465 | await TimeHelpers.increaseTime(ONE_YEAR / 5); 466 | await dist.updateAccounting(); 467 | 468 | checkAmplAprox(await dist.totalLocked(), 50); 469 | checkAmplAprox(await dist.totalUnlocked(), 150); 470 | await checkAvailableToUnlock(dist, 0); 471 | }); 472 | }); 473 | }); 474 | 475 | describe("previewRewards", function () { 476 | let _r, _t; 477 | beforeEach(async function () { 478 | await ampl.transfer(anotherAccount.getAddress(), $AMPL(1000)); 479 | _r = await dist.previewRewards(0, await owner.getAddress(), 0); 480 | _t = await TimeHelpers.currentTime(); 481 | await ampl.approve(dist.target, $AMPL(300)); 482 | await dist.stake($AMPL(100)); 483 | await dist.lockTokens($AMPL(100), ONE_YEAR); 484 | checkAprox(await dist.unlockDuration(), ONE_YEAR, 86400); 485 | await TimeHelpers.increaseTime(ONE_YEAR / 2); 486 | await dist.lockTokens($AMPL(100), ONE_YEAR); 487 | checkAprox(await dist.unlockDuration(), ONE_YEAR, 86400); 488 | await ampl.connect(anotherAccount).approve(dist.target, $AMPL(200)); 489 | await dist.connect(anotherAccount).stake($AMPL(200)); 490 | await TimeHelpers.increaseTime(ONE_YEAR / 10); 491 | checkAprox(await dist.unlockDuration(), (ONE_YEAR * 9) / 10, 86400); 492 | }); 493 | 494 | describe("when user history does exist", async function () { 495 | describe("current state, without additional stake", function () { 496 | it("should return the system state", async function () { 497 | const r = await dist.previewRewards(0, await owner.getAddress(), 0); 498 | const t = await TimeHelpers.currentTime(); 499 | checkAmplAprox(r[0], 130); 500 | checkAmplAprox(r[1], 70); 501 | expect(r[2]).to.eq($AMPL(100)); 502 | expect(r[3]).to.eq($AMPL(300)); 503 | checkAmplAprox(r[4], 26.25); 504 | const timeElapsed = t - _t; 505 | expect(r[5] - _r[5]) 506 | .to.gte(timeElapsed - 1) 507 | .to.lte(timeElapsed + 1); 508 | await expect(await dist.unstake($AMPL(100))) 509 | .to.emit(dist, "TokensClaimed") 510 | .withArgs(await owner.getAddress(), "52500015952"); 511 | }); 512 | }); 513 | 514 | describe("current state, with additional stake", function () { 515 | it("should return the system state", async function () { 516 | const r = await dist.previewRewards(0, await owner.getAddress(), $AMPL(100)); 517 | const t = await TimeHelpers.currentTime(); 518 | checkAmplAprox(r[0], 130); 519 | checkAmplAprox(r[1], 70); 520 | expect(r[2]).to.eq($AMPL(200)); 521 | expect(r[3]).to.eq($AMPL(400)); 522 | checkAmplAprox(r[4], 26.25); 523 | const timeElapsed = t - _t; 524 | expect(r[5] - _r[5]) 525 | .to.gte(timeElapsed - 1) 526 | .to.lte(timeElapsed + 1); 527 | await ampl.approve(dist.target, $AMPL(100)); 528 | await dist.stake($AMPL(100)); 529 | await expect(await dist.unstake($AMPL(200))) 530 | .to.emit(dist, "TokensClaimed") 531 | .withArgs(await owner.getAddress(), "52500017834"); 532 | }); 533 | }); 534 | 535 | describe("after 3 months, without additional stake", function () { 536 | it("should return the system state", async function () { 537 | const r = await dist.previewRewards(ONE_YEAR / 4, await owner.getAddress(), 0); 538 | const t = await TimeHelpers.currentTime(); 539 | checkAmplAprox(r[0], 80); 540 | checkAmplAprox(r[1], 120); 541 | expect(r[2]).to.eq($AMPL(100)); 542 | expect(r[3]).to.eq($AMPL(300)); 543 | checkAmplAprox(r[4], 65.8); 544 | const timeElapsed = t - _t + ONE_YEAR / 4; 545 | expect(r[5] - _r[5]) 546 | .to.gte(timeElapsed - 1) 547 | .to.lte(timeElapsed + 1); 548 | 549 | await TimeHelpers.increaseTime(ONE_YEAR / 4); 550 | await expect(await dist.unstake($AMPL(100))) 551 | .to.emit(dist, "TokensClaimed") 552 | .withArgs(await owner.getAddress(), "65806466635"); 553 | }); 554 | }); 555 | 556 | describe("after 3 months, with additional stake", function () { 557 | it("should return the system state", async function () { 558 | const r = await dist.previewRewards( 559 | ONE_YEAR / 4, 560 | await owner.getAddress(), 561 | $AMPL(100), 562 | ); 563 | const t = await TimeHelpers.currentTime(); 564 | checkAmplAprox(r[0], 80); 565 | checkAmplAprox(r[1], 120); 566 | expect(r[2]).to.eq($AMPL(200)); 567 | expect(r[3]).to.eq($AMPL(400)); 568 | checkAmplAprox(r[4], 73.3333); 569 | const timeElapsed = t - _t + ONE_YEAR / 4; 570 | expect(r[5] - _r[5]) 571 | .to.gte(timeElapsed - 1) 572 | .to.lte(timeElapsed + 1); 573 | await ampl.approve(dist.target, $AMPL(100)); 574 | await dist.stake($AMPL(100)); 575 | await TimeHelpers.increaseTime(ONE_YEAR / 4); 576 | checkAprox(await dist.unlockDuration(), 0.65 * ONE_YEAR, 86400); 577 | await expect(await dist.unstake($AMPL(200))) 578 | .to.emit(dist, "TokensClaimed") 579 | .withArgs(await owner.getAddress(), "73333353473"); 580 | await TimeHelpers.increaseTime(ONE_YEAR * 10); 581 | await expect(await dist.connect(anotherAccount).unstake($AMPL(200))) 582 | .to.emit(dist, "TokensClaimed") 583 | .withArgs(await anotherAccount.getAddress(), "126666646527"); 584 | expect(await dist.unlockDuration()).to.eq(0); 585 | }); 586 | }); 587 | }); 588 | 589 | describe("when user history does not exist", async function () { 590 | describe("current state, with no additional stake", function () { 591 | it("should return the system state", async function () { 592 | const r = await dist.previewRewards(0, ethers.ZeroAddress, 0); 593 | const t = await TimeHelpers.currentTime(); 594 | checkAmplAprox(r[0], 130); 595 | checkAmplAprox(r[1], 70); 596 | expect(r[2]).to.eq(0n); 597 | expect(r[3]).to.eq($AMPL(300)); 598 | checkAmplAprox(r[4], 0); 599 | const timeElapsed = t - _t; 600 | expect(r[5] - _r[5]) 601 | .to.gte(timeElapsed - 1) 602 | .to.lte(timeElapsed + 1); 603 | }); 604 | }); 605 | 606 | describe("current state, with additional stake", function () { 607 | it("should return the system state", async function () { 608 | const r = await dist.previewRewards(0, ethers.ZeroAddress, $AMPL(100)); 609 | const t = await TimeHelpers.currentTime(); 610 | checkAmplAprox(r[0], 130); 611 | checkAmplAprox(r[1], 70); 612 | expect(r[2]).to.eq($AMPL(100)); 613 | expect(r[3]).to.eq($AMPL(400)); 614 | checkAmplAprox(r[4], 0); 615 | const timeElapsed = t - _t; 616 | expect(r[5] - _r[5]) 617 | .to.gte(timeElapsed - 1) 618 | .to.lte(timeElapsed + 1); 619 | }); 620 | }); 621 | 622 | describe("after 3 months, without additional stake", function () { 623 | it("should return the system state", async function () { 624 | const r = await dist.previewRewards(ONE_YEAR / 4, ethers.ZeroAddress, 0); 625 | const t = await TimeHelpers.currentTime(); 626 | checkAmplAprox(r[0], 79.99); 627 | checkAmplAprox(r[1], 120); 628 | expect(r[2]).to.eq(0n); 629 | expect(r[3]).to.eq($AMPL(300)); 630 | checkAmplAprox(r[4], 0); 631 | const timeElapsed = t - _t + ONE_YEAR / 4; 632 | expect(r[5] - _r[5]) 633 | .to.gte(timeElapsed - 1) 634 | .to.lte(timeElapsed + 1); 635 | }); 636 | }); 637 | 638 | describe("after 3 months, with additional stake", function () { 639 | it("should return the system state", async function () { 640 | const r = await dist.previewRewards( 641 | ONE_YEAR / 4, 642 | ethers.ZeroAddress, 643 | $AMPL(100), 644 | ); 645 | const t = await TimeHelpers.currentTime(); 646 | checkAmplAprox(r[0], 79.99); 647 | checkAmplAprox(r[1], 120); 648 | expect(r[2]).to.eq($AMPL(100)); 649 | expect(r[3]).to.eq($AMPL(400)); 650 | checkAmplAprox(r[4], 16.666); 651 | const timeElapsed = t - _t + ONE_YEAR / 4; 652 | expect(r[5] - _r[5]) 653 | .to.gte(timeElapsed - 1) 654 | .to.lte(timeElapsed + 1); 655 | }); 656 | }); 657 | }); 658 | }); 659 | }); 660 | -------------------------------------------------------------------------------- /test/unstake.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { expect } from "chai"; 3 | import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; 4 | import { 5 | $AMPL, 6 | invokeRebase, 7 | checkAmplAprox, 8 | TimeHelpers, 9 | deployGeyser, 10 | } from "../test/helper"; 11 | import { SignerWithAddress } from "ethers"; 12 | 13 | let ampl: any, dist: any, owner: SignerWithAddress, anotherAccount: SignerWithAddress; 14 | const InitialSharesPerToken = 10 ** 6; 15 | const ONE_YEAR = 1 * 365 * 24 * 3600; 16 | 17 | async function setupContracts() { 18 | [owner, anotherAccount] = await ethers.getSigners(); 19 | 20 | const AmpleforthErc20 = await ethers.getContractFactory("UFragments"); 21 | ampl = await AmpleforthErc20.deploy(); 22 | await ampl.initialize(await owner.getAddress()); 23 | await ampl.setMonetaryPolicy(await owner.getAddress()); 24 | 25 | const TokenPool = await ethers.getContractFactory("TokenPool"); 26 | const tokenPoolImpl = await TokenPool.deploy(); 27 | 28 | const startBonus = 50; // 50% 29 | const bonusPeriod = 86400; // 1 Day 30 | dist = await deployGeyser(owner, [ 31 | tokenPoolImpl.target, 32 | ampl.target, 33 | ampl.target, 34 | 10, 35 | startBonus, 36 | bonusPeriod, 37 | InitialSharesPerToken, 38 | ]); 39 | 40 | await ampl.transfer(await anotherAccount.getAddress(), $AMPL(50000)); 41 | await ampl.connect(anotherAccount).approve(dist.target, $AMPL(50000)); 42 | await ampl.connect(owner).approve(dist.target, $AMPL(50000)); 43 | 44 | return { ampl, dist, owner, anotherAccount }; 45 | } 46 | 47 | async function totalRewardsFor(account) { 48 | const r = await dist.previewRewards.staticCall(0, await account.getAddress(), 0); 49 | return r[4]; 50 | } 51 | 52 | async function expectEvent(tx, name, params) { 53 | const txR = await tx.wait(); 54 | const event = txR.logs?.find(event => event.fragment?.name === name); 55 | expect(event.args).to.deep.equal(params); 56 | } 57 | 58 | describe("unstaking", function () { 59 | beforeEach("setup contracts", async function () { 60 | ({ ampl, dist, owner, anotherAccount } = await loadFixture(setupContracts)); 61 | }); 62 | 63 | describe("unstake", function () { 64 | describe("when amount is 0", function () { 65 | it("should fail", async function () { 66 | await dist.connect(anotherAccount).stake($AMPL(50)); 67 | await expect(dist.connect(anotherAccount).unstake($AMPL(0))).to.be.revertedWith( 68 | "TokenGeyser: unstake amount is zero", 69 | ); 70 | }); 71 | }); 72 | 73 | describe("when rebase increases supply", function () { 74 | beforeEach(async function () { 75 | await dist.connect(anotherAccount).stake($AMPL(50)); 76 | await TimeHelpers.increaseTime(1); 77 | }); 78 | it("should fail if user tries to unstake more than his balance", async function () { 79 | await invokeRebase(ampl, +50); 80 | await expect(dist.connect(anotherAccount).unstake($AMPL(85))).to.be.revertedWith( 81 | "TokenGeyser: unstake amount is greater than total user stakes", 82 | ); 83 | }); 84 | it("should NOT fail if user tries to unstake his balance", async function () { 85 | await invokeRebase(ampl, +50); 86 | await dist.connect(anotherAccount).unstake($AMPL(75)); 87 | }); 88 | it("should fail if there are too few stakingSharesToBurn", async function () { 89 | await invokeRebase(ampl, 100 * InitialSharesPerToken); 90 | await expect(dist.connect(anotherAccount).unstake(1)).to.be.revertedWith( 91 | "TokenGeyser: Unable to unstake amount this small", 92 | ); 93 | }); 94 | }); 95 | 96 | describe("when rebase decreases supply", function () { 97 | beforeEach(async function () { 98 | await dist.connect(anotherAccount).stake($AMPL(50)); 99 | await TimeHelpers.increaseTime(1); 100 | }); 101 | it("should fail if user tries to unstake more than his balance", async function () { 102 | await invokeRebase(ampl, -50); 103 | await expect(dist.connect(anotherAccount).unstake($AMPL(50))).to.be.revertedWith( 104 | "TokenGeyser: unstake amount is greater than total user stakes", 105 | ); 106 | }); 107 | it("should NOT fail if user tries to unstake his balance", async function () { 108 | await invokeRebase(ampl, -50); 109 | await dist.connect(anotherAccount).unstake($AMPL(25)); 110 | }); 111 | }); 112 | 113 | describe("when single user stakes once", function () { 114 | // 100 ampls locked for 1 year, user stakes 50 ampls for 1 year 115 | // user is eligible for 100% of the reward, 116 | // unstakes 30 ampls, gets 60% of the reward (60 ampl) 117 | // user's final balance is 90 ampl, (20 remains staked), eligible rewards (40 ampl) 118 | beforeEach(async function () { 119 | await dist.lockTokens($AMPL(100), ONE_YEAR); 120 | await dist.connect(anotherAccount).stake($AMPL(50)); 121 | await TimeHelpers.increaseTime(ONE_YEAR); 122 | await dist.connect(anotherAccount).updateAccounting(); 123 | checkAmplAprox(await totalRewardsFor(anotherAccount), 50); 124 | }); 125 | it("should update the total staked and rewards", async function () { 126 | await dist.connect(anotherAccount).unstake($AMPL(30)); 127 | expect(await dist.totalStaked.staticCall()).to.eq($AMPL(20)); 128 | expect( 129 | await dist.totalStakedBy.staticCall(await anotherAccount.getAddress()), 130 | ).to.eq($AMPL(20)); 131 | checkAmplAprox(await totalRewardsFor(anotherAccount), 20); 132 | }); 133 | it("should transfer back staked tokens + rewards", async function () { 134 | const _b = await ampl.balanceOf.staticCall(await anotherAccount.getAddress()); 135 | await dist.connect(anotherAccount).unstake($AMPL(30)); 136 | const b = await ampl.balanceOf.staticCall(await anotherAccount.getAddress()); 137 | checkAmplAprox(b - _b, 90); 138 | }); 139 | it("should log Unstaked", async function () { 140 | const r = await dist.connect(anotherAccount).unstake($AMPL(30)); 141 | await expectEvent(r, "Unstaked", [ 142 | await anotherAccount.getAddress(), 143 | $AMPL(30), 144 | $AMPL(20), 145 | ]); 146 | }); 147 | it("should log TokensClaimed", async function () { 148 | const r = await dist.connect(anotherAccount).unstake($AMPL(30)); 149 | await expectEvent(r, "TokensClaimed", [ 150 | await anotherAccount.getAddress(), 151 | $AMPL(60), 152 | ]); 153 | }); 154 | }); 155 | 156 | describe("when single user unstake early with early bonus", function () { 157 | // Start bonus = 50%, Bonus Period = 1 Day. 158 | // 1000 ampls locked for 1 hour, so all will be unlocked by test-time. 159 | // user stakes 500 ampls for 12 hours, half the period. 160 | // user is eligible for 75% of the max reward, 161 | // unstakes 250 ampls, gets .5 * .75 * 1000 ampls 162 | // user's final balance is 625 ampl, (250 remains staked), eligible rewards (375 ampl) 163 | const ONE_HOUR = 3600; 164 | beforeEach(async function () { 165 | await dist.lockTokens($AMPL(1000), ONE_HOUR); 166 | 167 | await dist.connect(anotherAccount).stake($AMPL(500)); 168 | await TimeHelpers.increaseTime(12 * ONE_HOUR); 169 | await dist.connect(anotherAccount).updateAccounting(); 170 | checkAmplAprox(await totalRewardsFor(anotherAccount), 500); 171 | }); 172 | it("should update the total staked and rewards", async function () { 173 | await dist.connect(anotherAccount).unstake($AMPL(250)); 174 | expect(await dist.totalStaked.staticCall()).to.eq($AMPL(250)); 175 | expect( 176 | await dist.totalStakedBy.staticCall(await anotherAccount.getAddress()), 177 | ).to.eq($AMPL(250)); 178 | checkAmplAprox(await totalRewardsFor(anotherAccount), 312.5); // (.5 * .75 * 1000) + 250 179 | }); 180 | it("should transfer back staked tokens + rewards", async function () { 181 | const _b = await ampl.balanceOf.staticCall(await anotherAccount.getAddress()); 182 | await dist.connect(anotherAccount).unstake($AMPL(250)); 183 | const b = await ampl.balanceOf.staticCall(await anotherAccount.getAddress()); 184 | checkAmplAprox(b - _b, 625); 185 | }); 186 | it("should log Unstaked", async function () { 187 | const r = await dist.connect(anotherAccount).unstake($AMPL(250)); 188 | await expectEvent(r, "Unstaked", [ 189 | await anotherAccount.getAddress(), 190 | $AMPL(250), 191 | $AMPL(250), 192 | ]); 193 | }); 194 | it("should log TokensClaimed", async function () { 195 | const r = await dist.connect(anotherAccount).unstake($AMPL(250)); 196 | await expectEvent(r, "TokensClaimed", [ 197 | await anotherAccount.getAddress(), 198 | $AMPL(375), // .5 * .75 * 1000 199 | ]); 200 | }); 201 | }); 202 | 203 | describe("when single user stakes many times", function () { 204 | // 100 ampls locked for 1 year, 205 | // user stakes 50 ampls for 1/2 year, 50 ampls for 1/4 year, [50 ampls unlocked in this time ] 206 | // unstakes 30 ampls, gets 20% of the unlocked reward (10 ampl) ~ [30 * 0.25 / (50*0.25+50*0.5) * 50] 207 | // user's final balance is 40 ampl 208 | beforeEach(async function () { 209 | await dist.lockTokens($AMPL(100), ONE_YEAR); 210 | 211 | await TimeHelpers.increaseTime(ONE_YEAR / 100); 212 | await dist.connect(anotherAccount).stake($AMPL(50)); 213 | 214 | await TimeHelpers.increaseTime(ONE_YEAR / 4); 215 | await dist.connect(anotherAccount).stake($AMPL(50)); 216 | await TimeHelpers.increaseTime(ONE_YEAR / 4); 217 | await dist.connect(anotherAccount).updateAccounting(); 218 | }); 219 | it("checkTotalRewards", async function () { 220 | checkAmplAprox(await totalRewardsFor(anotherAccount), 25.5); 221 | }); 222 | it("should update the total staked and rewards", async function () { 223 | await dist.connect(anotherAccount).unstake($AMPL(30)); 224 | expect(await dist.totalStaked.staticCall()).to.eq($AMPL(70)); 225 | expect( 226 | await dist.totalStakedBy.staticCall(await anotherAccount.getAddress()), 227 | ).to.eq($AMPL(70)); 228 | checkAmplAprox(await totalRewardsFor(anotherAccount), 20.4); 229 | }); 230 | it("should transfer back staked tokens + rewards", async function () { 231 | const _b = await ampl.balanceOf.staticCall(await anotherAccount.getAddress()); 232 | await dist.connect(anotherAccount).unstake($AMPL(30)); 233 | const b = await ampl.balanceOf.staticCall(await anotherAccount.getAddress()); 234 | checkAmplAprox(b - _b, 40.2); 235 | }); 236 | }); 237 | 238 | describe("when single user performs unstake many times", function () { 239 | // 100 ampls locked for 1 year, 240 | // user stakes 10 ampls, waits 1 year, stakes 10 ampls, waits 1 year, 241 | // unstakes 5 ampl, unstakes 5 ampl, unstakes 5 ampl 242 | // 3rd unstake should be worth twice the first one 243 | beforeEach(async function () { 244 | await dist.lockTokens($AMPL(100), ONE_YEAR); 245 | 246 | await dist.connect(anotherAccount).stake($AMPL(10)); 247 | await TimeHelpers.increaseTime(ONE_YEAR); 248 | await dist.connect(anotherAccount).stake($AMPL(10)); 249 | await TimeHelpers.increaseTime(ONE_YEAR); 250 | await dist.connect(anotherAccount).updateAccounting(); 251 | checkAmplAprox(await totalRewardsFor(anotherAccount), 50); 252 | }); 253 | 254 | it("should use updated user accounting", async function () { 255 | const r1 = await dist.connect(anotherAccount).unstake($AMPL(5)); 256 | await expectEvent(r1, "TokensClaimed", [ 257 | await anotherAccount.getAddress(), 258 | 16666666842n, 259 | ]); 260 | const claim1 = 16666666842n; 261 | const r2 = await dist.connect(anotherAccount).unstake($AMPL(5)); 262 | await expectEvent(r2, "TokensClaimed", [ 263 | await anotherAccount.getAddress(), 264 | 16666667054n, 265 | ]); 266 | const r3 = await dist.connect(anotherAccount).unstake($AMPL(5)); 267 | await expectEvent(r3, "TokensClaimed", [ 268 | await anotherAccount.getAddress(), 269 | 33333333052n, 270 | ]); 271 | const claim3 = 33333333052n; 272 | const ratio = (claim3 * 100n) / claim1; 273 | expect(ratio).gte(199n).lt(201); 274 | }); 275 | }); 276 | 277 | describe("when multiple users stake once", function () { 278 | // 100 ampls locked for 1 year, 279 | // userA stakes 50 ampls for 3/4 year, userb stakes 50 ampl for 1/2 year, total unlocked 75 ampl 280 | // userA unstakes 30 ampls, gets 36% of the unlocked reward (27 ampl) ~ [30 * 0.75 / (50*0.75+50*0.5) * 75] 281 | // user's final balance is 57 ampl 282 | beforeEach(async function () { 283 | await dist.lockTokens($AMPL(100), ONE_YEAR); 284 | 285 | await TimeHelpers.increaseTime(ONE_YEAR / 100); 286 | await dist.connect(anotherAccount).stake($AMPL(50)); 287 | 288 | await TimeHelpers.increaseTime(ONE_YEAR / 4); 289 | await dist.stake($AMPL(50)); 290 | await TimeHelpers.increaseTime(ONE_YEAR / 2); 291 | await dist.connect(anotherAccount).updateAccounting(); 292 | await dist.updateAccounting(); 293 | expect(await dist.totalStaked.staticCall()).to.eq($AMPL(100)); 294 | checkAmplAprox(await totalRewardsFor(anotherAccount), 22.8); 295 | checkAmplAprox(await totalRewardsFor(owner), 15.2); 296 | }); 297 | it("checkTotalRewards", async function () { 298 | expect(await dist.totalStaked.staticCall()).to.eq($AMPL(100)); 299 | checkAmplAprox(await totalRewardsFor(anotherAccount), 22.8); 300 | checkAmplAprox(await totalRewardsFor(owner), 15.2); 301 | }); 302 | it("should update the total staked and rewards", async function () { 303 | await dist.connect(anotherAccount).unstake($AMPL(30)); 304 | expect(await dist.totalStaked.staticCall()).to.eq($AMPL(70)); 305 | expect( 306 | await dist.totalStakedBy.staticCall(await anotherAccount.getAddress()), 307 | ).to.eq($AMPL(20)); 308 | expect(await dist.totalStakedBy.staticCall(await owner.getAddress())).to.eq( 309 | $AMPL(50), 310 | ); 311 | checkAmplAprox(await totalRewardsFor(anotherAccount), 9.12); 312 | checkAmplAprox(await totalRewardsFor(owner), 15.2); 313 | }); 314 | it("should transfer back staked tokens + rewards", async function () { 315 | const _b = await ampl.balanceOf.staticCall(await anotherAccount.getAddress()); 316 | await dist.connect(anotherAccount).unstake($AMPL(30)); 317 | const b = await ampl.balanceOf.staticCall(await anotherAccount.getAddress()); 318 | checkAmplAprox(b - _b, 57.36); 319 | }); 320 | }); 321 | 322 | describe("when multiple users stake many times", function () { 323 | // 10000 ampls locked for 1 year, 324 | // userA stakes 5000 ampls for 3/4 year, and 5000 ampls for 1/4 year 325 | // userb stakes 5000 ampls for 1/2 year and 3000 ampls for 1/4 year 326 | // userA unstakes 10000 ampls, gets 60.60% of the unlocked reward (4545 ampl) 327 | // ~ [5000*0.75+5000*0.25 / (5000*0.75+5000*0.25+5000*0.5+3000*0.25) * 7500] 328 | // user's final balance is 14545 ampl 329 | // userb unstakes 8000 ampls, gets the 10955 ampl 330 | const rewardsAnotherAccount = 50000.0 / 11.0; 331 | const rewardsOwner = 32500.0 / 11.0; 332 | beforeEach(async function () { 333 | await dist.lockTokens($AMPL(10000), ONE_YEAR); 334 | await dist.connect(anotherAccount).stake($AMPL(5000)); 335 | 336 | await TimeHelpers.increaseTime(ONE_YEAR / 4); 337 | await dist.stake($AMPL(5000)); 338 | await TimeHelpers.increaseTime(ONE_YEAR / 4); 339 | await dist.connect(anotherAccount).stake($AMPL(5000)); 340 | await dist.stake($AMPL(3000)); 341 | await TimeHelpers.increaseTime(ONE_YEAR / 4); 342 | await dist.connect(anotherAccount).updateAccounting(); 343 | await dist.updateAccounting(); 344 | expect(await dist.totalStaked.staticCall()).to.eq($AMPL(18000)); 345 | checkAmplAprox(await totalRewardsFor(anotherAccount), rewardsAnotherAccount / 2); 346 | checkAmplAprox(await totalRewardsFor(owner), rewardsOwner / 2); 347 | }); 348 | it("should update the total staked and rewards", async function () { 349 | await dist.connect(anotherAccount).unstake($AMPL(10000)); 350 | expect(await dist.totalStaked.staticCall()).to.eq($AMPL(8000)); 351 | expect(await dist.totalStakedBy.staticCall(ethers.ZeroAddress)).to.eq($AMPL(0)); 352 | expect(await dist.totalStakedBy.staticCall(await owner.getAddress())).to.eq( 353 | $AMPL(8000), 354 | ); 355 | checkAmplAprox(await totalRewardsFor(anotherAccount), 0); 356 | checkAmplAprox(await totalRewardsFor(owner), rewardsOwner / 2); 357 | await dist.unstake($AMPL(8000)); 358 | expect(await dist.totalStaked.staticCall()).to.eq($AMPL(0)); 359 | expect( 360 | await dist.totalStakedBy.staticCall(await anotherAccount.getAddress()), 361 | ).to.eq($AMPL(0)); 362 | expect(await dist.totalStakedBy.staticCall(await owner.getAddress())).to.eq( 363 | $AMPL(0), 364 | ); 365 | checkAmplAprox(await totalRewardsFor(anotherAccount), 0); 366 | checkAmplAprox(await totalRewardsFor(owner), 0); 367 | }); 368 | it("should transfer back staked tokens + rewards", async function () { 369 | const b1 = await ampl.balanceOf.staticCall(await anotherAccount.getAddress()); 370 | await dist.connect(anotherAccount).unstake($AMPL(10000)); 371 | const b2 = await ampl.balanceOf.staticCall(await anotherAccount.getAddress()); 372 | checkAmplAprox(b2 - b1, 10000 + rewardsAnotherAccount); 373 | const b3 = await ampl.balanceOf.staticCall(await owner.getAddress()); 374 | await dist.unstake($AMPL(8000)); 375 | const b4 = await ampl.balanceOf.staticCall(await owner.getAddress()); 376 | checkAmplAprox(b4 - b3, 8000 + rewardsOwner); 377 | }); 378 | }); 379 | }); 380 | 381 | describe("unstakeQuery", function () { 382 | // 100 ampls locked for 1 year, user stakes 50 ampls for 1 year 383 | // user is eligible for 100% of the reward, 384 | // unstakes 30 ampls, gets 60% of the reward (60 ampl) 385 | beforeEach(async function () { 386 | await dist.lockTokens($AMPL(100), ONE_YEAR); 387 | await dist.connect(anotherAccount).stake($AMPL(50)); 388 | await TimeHelpers.increaseTime(ONE_YEAR); 389 | await dist.connect(anotherAccount).updateAccounting(); 390 | }); 391 | it("should return the reward amount", async function () { 392 | checkAmplAprox(await totalRewardsFor(anotherAccount), 50); 393 | checkAmplAprox( 394 | await dist.connect(anotherAccount).unstake.staticCall($AMPL(30)), 395 | 60, 396 | ); 397 | }); 398 | }); 399 | }); 400 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true 10 | } 11 | } 12 | --------------------------------------------------------------------------------