├── .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 | [](https://travis-ci.com/ampleforth/token-geyser) [](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 |
--------------------------------------------------------------------------------