diff --git a/.env.template b/.env.template
new file mode 100644
index 0000000..6529537
--- /dev/null
+++ b/.env.template
@@ -0,0 +1,4 @@
+MAINNET_RPC_URL=
+FOUNDRY_PROFILE=default
+DEPLOYER_PRIVATE_KEY=
+PROPOSER_ADDRESS=
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..823bfd7
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,140 @@
+name: CI
+
+on:
+ workflow_dispatch:
+ pull_request:
+ push:
+ branches:
+ - main
+
+env:
+ FOUNDRY_PROFILE: ci
+ MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Install Foundry
+ uses: foundry-rs/foundry-toolchain@v1
+
+ - name: Build contracts
+ run: |
+ forge --version
+ forge build --sizes
+
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Install Foundry
+ uses: foundry-rs/foundry-toolchain@v1
+
+ # https://twitter.com/PaulRBerg/status/1611116650664796166
+ - name: Generate fuzz seed with 1 day TTL
+ run: >
+ echo "FOUNDRY_FUZZ_SEED=$(
+ echo $(($EPOCHSECONDS - $EPOCHSECONDS % 86400))
+ )" >> $GITHUB_ENV
+
+ - name: Run tests
+ run: forge test
+
+ coverage:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Install Foundry
+ uses: foundry-rs/foundry-toolchain@v1
+
+ # https://twitter.com/PaulRBerg/status/1611116650664796166
+ - name: Generate fuzz seed with 1 day TTL
+ run: >
+ echo "FOUNDRY_FUZZ_SEED=$(
+ echo $(($EPOCHSECONDS - $EPOCHSECONDS % 86400))
+ )" >> $GITHUB_ENV
+
+ - name: Run coverage
+ run: forge coverage --report summary --report lcov
+
+ # To ignore coverage for certain directories modify the paths in this step as needed. The
+ # below default ignores coverage results for the test and script directories. Alternatively,
+ # to include coverage in all directories, comment out this step. Note that because this
+ # filtering applies to the lcov file, the summary table generated in the previous step will
+ # still include all files and directories.
+ # The `--rc lcov_branch_coverage=1` part keeps branch info in the filtered report, since lcov
+ # defaults to removing branch info.
+ - name: Filter directories
+ run: |
+ sudo apt update && sudo apt install -y lcov
+ lcov --remove lcov.info 'test/*' 'script/*' --output-file lcov.info --rc lcov_branch_coverage=1
+
+ # This step posts a detailed coverage report as a comment and deletes previous comments on
+ # each push. The below step is used to fail coverage if the specified coverage threshold is
+ # not met. The below step can post a comment (when it's `github-token` is specified) but it's
+ # not as useful, and this action cannot fail CI based on a minimum coverage threshold, which
+ # is why we use both in this way.
+ - name: Post coverage report
+ if: github.event_name == 'pull_request' # This action fails when ran outside of a pull request.
+ uses: romeovs/lcov-reporter-action@v0.3.1
+ with:
+ delete-old-comments: true
+ lcov-file: ./lcov.info
+ github-token: ${{ secrets.GITHUB_TOKEN }} # Adds a coverage summary comment to the PR.
+
+ - name: Verify minimum coverage
+ uses: zgosalvez/github-actions-report-lcov@v2
+ with:
+ coverage-files: ./lcov.info
+ minimum-coverage: 100
+
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Install Foundry
+ uses: foundry-rs/foundry-toolchain@v1
+ with:
+ cache: false
+
+ - name: Install scopelint
+ uses: engineerd/configurator@v0.0.8
+ with:
+ name: scopelint
+ repo: ScopeLift/scopelint
+ fromGitHubReleases: true
+ version: latest
+ pathInArchive: scopelint-x86_64-linux/scopelint
+ urlTemplate: https://github.com/ScopeLift/scopelint/releases/download/{{version}}/scopelint-x86_64-linux.tar.xz
+ token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Check formatting
+ run: |
+ scopelint --version
+ scopelint check
+
+ # slither-analyze:
+ # runs-on: ubuntu-latest
+ # permissions:
+ # contents: read
+ # security-events: write
+ # steps:
+ # - uses: actions/checkout@v3
+
+ # - name: Run Slither
+ # uses: crytic/slither-action@v0.3.0
+ # id: slither # Required to reference this step in the next step.
+ # with:
+ # fail-on: none # Required to avoid failing the CI run regardless of findings.
+ # sarif: results.sarif
+ # slither-args: --filter-paths "./lib|./test" --exclude naming-convention,solc-version
+
+ # - name: Upload SARIF file
+ # uses: github/codeql-action/upload-sarif@v2
+ # with:
+ # sarif_file: ${{ steps.slither.outputs.sarif }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2036134
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,18 @@
+# Compiler files
+cache/
+out/
+
+# Ignores development broadcast logs
+!/broadcast
+/broadcast/*/31337/
+/broadcast/**/dry-run/
+
+# Dotenv file
+.env
+
+# Coverage
+lcov.info
+
+# IDEs
+.vscode
+.idea
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..690924b
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,6 @@
+[submodule "lib/forge-std"]
+ path = lib/forge-std
+ url = https://github.com/foundry-rs/forge-std
+[submodule "lib/openzeppelin-contracts"]
+ path = lib/openzeppelin-contracts
+ url = https://github.com/OpenZeppelin/openzeppelin-contracts
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..0780b86
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,622 @@
+Copyright (c) 2024 Tally
+info@tally.xyz
+
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..885aab2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,54 @@
+# Governance Staker
+
+🚧 A complete README is Coming Soon™.
+
+## Development
+
+These contracts were built and tested with care by the team at [ScopeLift](https://scopelift.co).
+
+### Build and test
+
+This project uses [Foundry](https://github.com/foundry-rs/foundry). Follow [these instructions](https://github.com/foundry-rs/foundry#installation) to install it.
+
+Clone the repo.
+
+Set up your .env file
+
+```bash
+cp .env.template .env
+# edit the .env to fill in values
+```
+
+Install dependencies & run tests.
+
+```bash
+forge install
+forge build
+forge test
+```
+
+### Spec and lint
+
+This project uses [scopelint](https://github.com/ScopeLift/scopelint) for linting and spec generation. Follow [these instructions](https://github.com/ScopeLift/scopelint?tab=readme-ov-file#installation) to install it.
+
+To use scopelint's linting functionality, run:
+
+```bash
+scopelint check # check formatting
+scopelint fmt # apply formatting changes
+```
+
+To use scopelint's spec generation functionality, run:
+
+```bash
+scopelint spec
+```
+
+This command will use the names of the contract's unit tests to generate a human readable spec. It will list each contract, its constituent functions, and the human readable description of functionality each unit test aims to assert.
+
+
+## License
+
+The code in this repository is licensed under the [GNU Affero General Public License](LICENSE) unless otherwise indicated.
+
+Copyright (C) 2024 Tally
diff --git a/audits/2024_02_UniStaker_ToB_Report.pdf b/audits/2024_02_UniStaker_ToB_Report.pdf
new file mode 100644
index 0000000..5859a98
Binary files /dev/null and b/audits/2024_02_UniStaker_ToB_Report.pdf differ
diff --git a/audits/2024_03_UniStaker_Code4rena Contest.md b/audits/2024_03_UniStaker_Code4rena Contest.md
new file mode 100644
index 0000000..34455be
--- /dev/null
+++ b/audits/2024_03_UniStaker_Code4rena Contest.md
@@ -0,0 +1,1241 @@
+---
+sponsor: "Uniswap Foundation"
+slug: "2024-02-uniswap-foundation"
+date: "2024-04-11"
+title: "UniStaker Infrastructure"
+findings: "https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues"
+contest: 336
+---
+
+# Overview
+
+## About C4
+
+Code4rena (C4) is an open organization consisting of security researchers, auditors, developers, and individuals with domain expertise in smart contracts.
+
+A C4 audit is an event in which community participants, referred to as Wardens, review, audit, or analyze smart contract logic in exchange for a bounty provided by sponsoring projects.
+
+During the audit outlined in this document, C4 conducted an analysis of the UniStaker Infrastructure smart contract system written in Solidity. The audit took place between February 23 — March 5, 2024.
+
+## Wardens
+
+54 Wardens contributed reports to UniStaker Infrastructure:
+
+ 1. [CodeWasp](https://code4rena.com/@CodeWasp) ([slylandro\_star](https://code4rena.com/@slylandro_star), [kuprum](https://code4rena.com/@kuprum), [audithare](https://code4rena.com/@audithare) and [spaghetticode\_sentinel](https://code4rena.com/@spaghetticode_sentinel))
+ 2. [Al-Qa-qa](https://code4rena.com/@Al-Qa-qa)
+ 3. [DadeKuma](https://code4rena.com/@DadeKuma)
+ 4. [Trust](https://code4rena.com/@Trust)
+ 5. [0xlemon](https://code4rena.com/@0xlemon)
+ 6. [Shield](https://code4rena.com/@Shield) ([Viraz](https://code4rena.com/@Viraz), [0xA5DF](https://code4rena.com/@0xA5DF), [Dravee](https://code4rena.com/@Dravee) and [Udsen](https://code4rena.com/@Udsen))
+ 7. [lsaudit](https://code4rena.com/@lsaudit)
+ 8. [Breeje](https://code4rena.com/@Breeje)
+ 9. [osmanozdemir1](https://code4rena.com/@osmanozdemir1)
+ 10. [SpicyMeatball](https://code4rena.com/@SpicyMeatball)
+ 11. [peanuts](https://code4rena.com/@peanuts)
+ 12. [Aamir](https://code4rena.com/@Aamir)
+ 13. [ZanyBonzy](https://code4rena.com/@ZanyBonzy)
+ 14. [AlexCzm](https://code4rena.com/@AlexCzm)
+ 15. [0xdice91](https://code4rena.com/@0xdice91)
+ 16. [gesha17](https://code4rena.com/@gesha17)
+ 17. [marchev](https://code4rena.com/@marchev)
+ 18. [kutugu](https://code4rena.com/@kutugu)
+ 19. [haxatron](https://code4rena.com/@haxatron)
+ 20. [cheatc0d3](https://code4rena.com/@cheatc0d3)
+ 21. [visualbits](https://code4rena.com/@visualbits)
+ 22. [radev\_sw](https://code4rena.com/@radev_sw)
+ 23. [imare](https://code4rena.com/@imare)
+ 24. [nnez](https://code4rena.com/@nnez)
+ 25. [PetarTolev](https://code4rena.com/@PetarTolev)
+ 26. [BAHOZ](https://code4rena.com/@BAHOZ)
+ 27. [Bauchibred](https://code4rena.com/@Bauchibred)
+ 28. [jesjupyter](https://code4rena.com/@jesjupyter)
+ 29. [twicek](https://code4rena.com/@twicek)
+ 30. [Fassi\_Security](https://code4rena.com/@Fassi_Security) ([bronze\_pickaxe](https://code4rena.com/@bronze_pickaxe) and [mxuse](https://code4rena.com/@mxuse))
+ 31. [merlinboii](https://code4rena.com/@merlinboii)
+ 32. [roguereggiant](https://code4rena.com/@roguereggiant)
+ 33. [hunter\_w3b](https://code4rena.com/@hunter_w3b)
+ 34. [kaveyjoe](https://code4rena.com/@kaveyjoe)
+ 35. [McToady](https://code4rena.com/@McToady)
+ 36. [Sathish9098](https://code4rena.com/@Sathish9098)
+ 37. [0xepley](https://code4rena.com/@0xepley)
+ 38. [fouzantanveer](https://code4rena.com/@fouzantanveer)
+ 39. [hassanshakeel13](https://code4rena.com/@hassanshakeel13)
+ 40. [MSK](https://code4rena.com/@MSK)
+ 41. [LinKenji](https://code4rena.com/@LinKenji)
+ 42. [SAQ](https://code4rena.com/@SAQ)
+ 43. [Myd](https://code4rena.com/@Myd)
+ 44. [ihtishamsudo](https://code4rena.com/@ihtishamsudo)
+ 45. [emerald7017](https://code4rena.com/@emerald7017)
+ 46. [aariiif](https://code4rena.com/@aariiif)
+ 47. [cudo](https://code4rena.com/@cudo)
+
+This audit was judged by [0xTheC0der](https://code4rena.com/@0xTheC0der).
+
+Final report assembled by [thebrittfactor](https://twitter.com/brittfactorC4).
+
+# Summary
+
+The C4 analysis yielded an aggregated total of 0 unique vulnerabilities.
+
+Additionally, C4 analysis included 31 reports detailing issues with a risk rating of LOW severity or non-critical.
+
+All of the issues presented here are linked back to their original finding.
+
+# Scope
+
+The code under review can be found within the [C4 UniStaker Infrastructure repository](https://github.com/code-423n4/2024-02-uniswap-foundation), and is composed of 7 smart contracts written in the Solidity programming language and includes 557 lines of Solidity code.
+
+In addition to the known issues identified by the project team, a Code4rena bot race was conducted at the start of the audit. The winning bot, **LightChaser** from warden ChaseTheLight, generated the [Automated Findings report](https://github.com/code-423n4/2024-02-uniswap-foundation/blob/main/bot-report.md) and all findings therein were classified as out of scope.
+
+# Severity Criteria
+
+C4 assesses the severity of disclosed vulnerabilities based on three primary risk categories: high, medium, and low/non-critical.
+
+High-level considerations for vulnerabilities span the following key areas when conducting assessments:
+
+- Malicious Input Handling
+- Escalation of privileges
+- Arithmetic
+- Gas use
+
+For more information regarding the severity criteria referenced throughout the submission review process, please refer to the documentation provided on [the C4 website](https://code4rena.com), specifically our section on [Severity Categorization](https://docs.code4rena.com/awarding/judging-criteria/severity-categorization).
+
+# Low Risk and Non-Critical Issues
+
+For this audit, 31 reports were submitted by wardens detailing low risk and non-critical issues. The [report highlighted below](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/299) by **CodeWasp** received the top score from the judge.
+
+*The following wardens also submitted reports: [DadeKuma](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/380), [Trust](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/331), [0xlemon](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/255), [Shield](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/169), [lsaudit](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/168), [Breeje](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/107), [Al-Qa-qa](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/90), [osmanozdemir1](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/45), [SpicyMeatball](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/8), [AlexCzm](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/415), [peanuts](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/414), [0xdice91](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/413), [gesha17](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/410), [Aamir](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/409), [marchev](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/379), [kutugu](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/372), [haxatron](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/368), [cheatc0d3](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/355), [visualbits](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/345), [radev\_sw](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/237), [imare](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/230), [nnez](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/201), [ZanyBonzy](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/151), [PetarTolev](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/141), [BAHOZ](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/130), [Bauchibred](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/115), [jesjupyter](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/99), [twicek](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/73), [Fassi\_Security](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/67), and [merlinboii](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/59).*
+
+## [01] Adapting UniStaker test infrastructure to UNI token
+
+Current testing infrastructure for UniStaker includes fuzz and integration tests which employ mocks for the governance token, in particular [test/mocks/MockERC20Votes.sol](https://github.com/code-423n4/2024-02-uniswap-foundation/blob/5a2761c8277541a24bc551fbd624413b384bea94/test/mocks/MockERC20Votes.sol). The sponsors have confirmed in the Discord audit channel though that exclusively the [currently deployed UNI token](https://etherscan.io/token/0x1f9840a85d5af5bf1d1762f925bdaddc4201f984#code) will be used as the governance token. In light of that information, it should be noted that `MockERC20Votes.sol` is a very crude approximation of the functionality contained in `Uni.sol`. In particular, the latter:
+
+- Allows token holders to delegate their voting power directly, via the `delegate()` method.
+- Employs a non-trivial accounting scheme for delegated votes, indexed according to block numbers.
+- Is written using Solidity 0.5.16 compiler, and moreover, restricts many of its underlying datatypes to `uint96` / `uint32`.
+
+Moreover, the current UniStaker testing infrastructure doesn't try to test for the correct votes accounting at all, although it's a crucial aspect of integrating `UniStaker` with the currently deployed `UNI` token. Taking this into account, we've undertaken the steps to integrate `UNI` token into the `UniStaker` testing, of which activity we report below. In particular, we:
+
+- Ported `Uni.sol` from Solidity 0.5.16 to Solidity 0.8.23.
+- Adjusted the tests in `UniStaker.t.sol` such that they pass when used with `Uni.sol` instead of `MockERC20Votes.sol`.
+- Added some assertions to `UniStaker.t.sol` to track for voting power in tests.
+- Wrote a handler around `Uni.sol`, `Uni.handler.sol`, which allows to call for its most important user-facing methods from Foundry fuzz/invariant tester.
+- Performed necessary adaptations to `UniStaker.handler.sol`, to integrate `UNI` and avoid failing tests due to a low-level foundry function.
+- Extended `UniStaker.invariants.t.sol` with an additional invariant, `invariant_Total_stake_plus_direct_delegations_equals_current_votes`, which captures the relation between the voting power delegated directly through users and via UniStaker surrogates.
+- Extended the helper library `AddressSet.sol`, to be able to track external user delegations.
+- Made necessary changes to `foundry.toml` to make the project compile, and run a reasonable amount of fuzz/invariant tests.
+
+While these activities have not allowed us to catch any critical vulnerabilities, they did allow us to identify and fix many implicit assumptions in the testing infrastructure that made it incompatible with the real `UNI` token, and not the mock. We also have been able to identify and fix a few false positives, i.e. the tests that were failing due to the deficiencies in the tests themselves. We hope that our efforts will help the UniSwap developers in seamlessly integrating their new staking contracts with the currently deployed ones.
+
+All of the added/modified files are available in [this gist](https://gist.github.com/kuprumion/b7b0e03ea52ff925d0f9a9a4dcd7116f).
+
+## [02] A port of `Uni.sol` from Solidity 0.5.16 to Solidity 0.8.23
+
+This is the simplest of undertaken activities, which amounted in fixing a couple of incompatibilities between the compiler versions, disabling some checks which were not compatible with the current test suite (like minting restrictions), and adding the `DOMAIN_SEPARATOR()` function required by tests. The changes between the [deployed UNI token](https://etherscan.io/token/0x1f9840a85d5af5bf1d1762f925bdaddc4201f984#code) and the adaptation are summarized in the diff below:
+
+```diff
+--- test/mocks/Uni.sol.orig 2024-03-04 13:51:22.540178698 +0100
++++ test/mocks/Uni.sol 2024-03-04 14:22:43.058757812 +0100
+@@ -1,4 +1,8 @@
+-pragma solidity ^0.5.16;
++// Adaptation of the UNI code from https://etherscan.io/token/0x1f9840a85d5af5bf1d1762f925bdaddc4201f984#code
++// To make the tests pass. For the original version see "Uni.sol.orig"
++pragma solidity 0.8.23;
+ pragma experimental ABIEncoderV2;
+
++import {IERC20Delegates} from "src/interfaces/IERC20Delegates.sol";
++
+ // From https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/math/Math.sol
+@@ -188,3 +192,3 @@
+
+-contract Uni {
++contract Uni is IERC20Delegates {
+ /// @notice EIP-20 token name for this token
+@@ -293,4 +297,4 @@
+ function mint(address dst, uint rawAmount) external {
+- require(msg.sender == minter, "Uni::mint: only the minter can mint");
+- require(block.timestamp >= mintingAllowedAfter, "Uni::mint: minting not allowed yet");
++ // require(msg.sender == minter, "Uni::mint: only the minter can mint");
++ // require(block.timestamp >= mintingAllowedAfter, "Uni::mint: minting not allowed yet");
+ require(dst != address(0), "Uni::mint: cannot transfer to the zero address");
+@@ -302,3 +306,3 @@
+ uint96 amount = safe96(rawAmount, "Uni::mint: amount exceeds 96 bits");
+- require(amount <= SafeMath.div(SafeMath.mul(totalSupply, mintCap), 100), "Uni::mint: exceeded mint cap");
++ // require(amount <= SafeMath.div(SafeMath.mul(totalSupply, mintCap), 100), "Uni::mint: exceeded mint cap");
+ totalSupply = safe96(SafeMath.add(totalSupply, amount), "Uni::mint: totalSupply exceeds 96 bits");
+@@ -333,4 +337,4 @@
+ uint96 amount;
+- if (rawAmount == uint(-1)) {
+- amount = uint96(-1);
++ if (rawAmount == type(uint).max) {
++ amount = type(uint96).max;
+ } else {
+@@ -345,2 +349,6 @@
+
++ function DOMAIN_SEPARATOR() external view virtual returns (bytes32) {
++ return keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name)), getChainId(), address(this)));
++ }
++
+ /**
+@@ -357,4 +365,4 @@
+ uint96 amount;
+- if (rawAmount == uint(-1)) {
+- amount = uint96(-1);
++ if (rawAmount == type(uint).max) {
++ amount = type(uint96).max;
+ } else {
+@@ -369,3 +377,3 @@
+ require(signatory == owner, "Uni::permit: unauthorized");
+- require(now <= deadline, "Uni::permit: signature expired");
++ require(block.timestamp <= deadline, "Uni::permit: signature expired");
+
+@@ -409,3 +417,3 @@
+
+- if (spender != src && spenderAllowance != uint96(-1)) {
++ if (spender != src && spenderAllowance != type(uint96).max) {
+ uint96 newAllowance = sub96(spenderAllowance, amount, "Uni::transferFrom: transfer amount exceeds spender allowance");
+@@ -444,3 +452,3 @@
+ require(nonce == nonces[signatory]++, "Uni::delegateBySig: invalid nonce");
+- require(now <= expiry, "Uni::delegateBySig: signature expired");
++ require(block.timestamp <= expiry, "Uni::delegateBySig: signature expired");
+ return _delegate(signatory, delegatee);
+@@ -572,3 +580,3 @@
+
+- function getChainId() internal pure returns (uint) {
++ function getChainId() internal view returns (uint) {
+ uint256 chainId;
+```
+
+## [03] Adjustment of the tests in `UniStaker.t.sol` to use `Uni.sol` instead of `MockERC20Votes.sol`
+
+We don't list here the whole diff, only the most important parts of it; also omitting duplicate changes in multiple places.
+
+### Preamble and set up: replace `ERC20VotesMock` with `Uni`**
+
+```diff
+diff --git a/test/UniStaker.t.sol b/test/UniStaker.t.sol
+index 89124f8..22e0534 100644
+--- a/test/UniStaker.t.sol
++++ b/test/UniStaker.t.sol
+@@ -9,2 +9,3 @@ import {ERC20Fake} from "test/fakes/ERC20Fake.sol";
+ import {PercentAssertions} from "test/helpers/PercentAssertions.sol";
++import {Uni} from "test/mocks/Uni.sol";
+
+@@ -12,3 +13,3 @@ contract UniStakerTest is Test, PercentAssertions {
+ ERC20Fake rewardToken;
+- ERC20VotesMock govToken;
++ Uni govToken;
+ address admin;
+@@ -38,4 +39,6 @@ contract UniStakerTest is Test, PercentAssertions {
+
+- govToken = new ERC20VotesMock();
++ admin = makeAddr("admin");
++ govToken = new Uni(admin, admin, 2000);
+ vm.label(address(govToken), "Governance Token");
++ _jumpAhead(1234);
+
+@@ -44,4 +47,2 @@ contract UniStakerTest is Test, PercentAssertions {
+
+- admin = makeAddr("admin");
+-
+ uniStaker = new UniStakerHarness(rewardToken, govToken, admin);
+@@ -61,3 +62,3 @@ contract UniStakerTest is Test, PercentAssertions {
+ function _boundMintAmount(uint256 _amount) internal pure returns (uint256) {
+- return bound(_amount, 0, 100_000_000_000e18);
++ return bound(_amount, 0, 100_000_000_000e12); // reduced for tests to pass with UNI
+ }
+@@ -66,2 +67,4 @@ contract UniStakerTest is Test, PercentAssertions {
+ vm.assume(_to != address(0));
++ vm.assume(_to != admin); // needed to avoid using admin's address in tests
++ vm.prank(admin);
+ govToken.mint(_to, _amount);
+```
+
+### Reduce the maximum constants used to be compatible with `uint96` used in `Uni`
+
+```diff
+@@ -74,3 +77,3 @@ contract UniStakerTest is Test, PercentAssertions {
+ {
+- _boundedStakeAmount = bound(_stakeAmount, 0.1e18, 25_000_000e18);
++ _boundedStakeAmount = bound(_stakeAmount, 0.1e18, 25_000_000e12); // reduced for tests to pass with UNI
+ }
+@@ -194,3 +197,3 @@ contract Stake is UniStakerTest {
+ ) public {
+- _amount = bound(_amount, 1, type(uint224).max);
++ _amount = bound(_amount, 1, type(uint88).max);
+ _mintGovToken(_depositor, _amount);
+@@ -721,3 +733,3 @@ contract PermitAndStake is UniStakerTest {
+ uint256 _deadline,
+- uint256 _currentNonce
++ uint248 _currentNonce
+ ) public {
+@@ -2371,3 +2384,3 @@ contract Withdraw is UniStakerTest {
+ (_amount, _depositId) = _boundMintAndStake(_depositor, _amount, _delegatee);
+- _amountOver = bound(_amountOver, 1, type(uint128).max);
++ _amountOver = bound(_amountOver, 1, type(uint88).max);
+```
+
+### Miscellaneous changes
+
+```diff
+@@ -793,3 +805,3 @@ contract PermitAndStake is UniStakerTest {
+ vm.expectRevert(
+- abi.encodeWithSelector(ERC20Permit.ERC2612InvalidSigner.selector, _depositor, _notDepositor)
++ "Uni::permit: unauthorized"
+ );
+@@ -4670,5 +4682,5 @@ contract _FetchOrDeploySurrogate is UniStakerRewardsTest {
+
+- assertEq(logs[1].topics[0], keccak256("SurrogateDeployed(address,address)"));
+- assertEq(logs[1].topics[1], bytes32(uint256(uint160(_delegatee))));
+- assertEq(logs[1].topics[2], bytes32(uint256(uint160(address(_surrogate)))));
++ assertEq(logs[2].topics[0], keccak256("SurrogateDeployed(address,address)"));
++ assertEq(logs[2].topics[1], bytes32(uint256(uint160(_delegatee))));
++ assertEq(logs[2].topics[2], bytes32(uint256(uint160(address(_surrogate)))));
+ }
+```
+
+## [04] Additional assertions to track voting power changes in `Uni`
+
+As already explained above, voting power is a very important aspect of `UNI` token, which, on the one hand, is influenced by the introduction of `UniStaker` (via surrogate delegations), and on the other hand voting power changes are not tracked at all in the current test suite. We have added corresponding assertions to a few of the current tests; the rest of the test suite needs to be examined, and assertions added as well; we leave this to UniSwap developers.
+
+An example of one of the modified tests is below:
+
+```diff
+@@ -189,15 +191,16 @@ contract Constructor is UniStakerTest {
+ contract Stake is UniStakerTest {
+ function testFuzz_DeploysAndTransfersTokensToANewSurrogateWhenAnAccountStakes(
+ address _depositor,
+ uint256 _amount,
+ address _delegatee
+ ) public {
+- _amount = bound(_amount, 1, type(uint224).max);
++ _amount = bound(_amount, 1, type(uint88).max);
+ _mintGovToken(_depositor, _amount);
+ _stake(_depositor, _amount, _delegatee);
+
+ DelegationSurrogate _surrogate = uniStaker.surrogates(_delegatee);
+
+ assertEq(govToken.balanceOf(address(_surrogate)), _amount);
+ assertEq(govToken.delegates(address(_surrogate)), _delegatee);
+ assertEq(govToken.balanceOf(_depositor), 0);
++ assertEq(govToken.getCurrentVotes(_delegatee), _amount);
+ }
+ ```
+
+## [05] Add `Uni.handler.sol`, the wrapper around `Uni`, allowing to call its functions from fuzz/invariant tests
+
+Similar to the already present [test/helpers/UniStaker.handler.sol](https://github.com/code-423n4/2024-02-uniswap-foundation/blob/5a2761c8277541a24bc551fbd624413b384bea94/test/helpers/UniStaker.handler.sol), we have implemented the lightweight `test/helpers/Uni.handler.sol`, which allows to call most crucial for testing user-facing functions of `UNI`.
+
+```solidity
+// SPDX-License-Identifier: GPL-3.0-or-later
+pragma solidity ^0.8.13;
+
+import {CommonBase} from "forge-std/Base.sol";
+import {StdCheats} from "forge-std/StdCheats.sol";
+import {StdUtils} from "forge-std/StdUtils.sol";
+import {AddressSet, LibAddressSet} from "../helpers/AddressSet.sol";
+import {Uni} from "test/mocks/Uni.sol";
+
+contract UniHandler is CommonBase, StdCheats, StdUtils {
+ using LibAddressSet for AddressSet;
+
+ Uni public uni;
+
+ // delegator -> delegatee
+ mapping(address => address) private _delegatee;
+
+ // delegatee -> delegators
+ mapping(address => AddressSet) private _delegators;
+
+ constructor(Uni _uni) {
+ uni= _uni;
+ }
+
+ function approve(address spender, uint _amount) external returns (bool)
+ {
+ _amount = bound(_amount, 0, type(uint96).max);
+ vm.startPrank(msg.sender);
+ uni.approve(spender, _amount);
+ vm.stopPrank();
+ return true;
+ }
+
+ // Track delegations performed by the users directly via the UNI token
+ function transfer(address dst, uint _amount) external returns (bool)
+ {
+ // bound to the max available amount
+ vm.startPrank(msg.sender);
+ uint256 balance = uni.balanceOf(msg.sender);
+ _amount = bound(_amount, 0, balance);
+ uni.transfer(dst, _amount);
+ vm.stopPrank();
+ return true;
+ }
+
+ // Track delegations performed by users directly via the UNI token
+ function delegate(address delegatee) public
+ {
+ address prev_delegatee = _delegatee[msg.sender];
+ _delegators[prev_delegatee].remove(msg.sender);
+ _delegators[delegatee].add(msg.sender);
+ _delegatee[msg.sender] = delegatee;
+
+ vm.startPrank(msg.sender);
+ uni.delegate(delegatee);
+ vm.stopPrank();
+ }
+
+ // Advance the specified number of blocks.
+ // Needed to trigger UNI's block-numbers-based votes accounting
+ function roll(uint16 advance) public
+ {
+ vm.roll(block.number + advance);
+ }
+
+ function addDelegator(uint256 acc, address delegator) external view returns (uint256) {
+ return acc + uni.balanceOf(delegator);
+ }
+
+ function sumDelegatorVotes(address delegatee)
+ public view
+ returns (uint256)
+ {
+ return _delegators[delegatee].reduce(0, this.addDelegator);
+ }
+}
+```
+
+## [06] Necessary adaptations to `UniStaker.handler.sol`
+
+We had to perform necessary adaptations to `UniStaker.handler.sol`, to integrate `UNI` and avoid failing tests due to the usage of a low-level foundry function; the changes are outlined below:
+
+```diff
+diff --git a/test/helpers/UniStaker.handler.sol b/test/helpers/UniStaker.handler.sol
+index f8fe335..9622571 100644
+--- a/test/helpers/UniStaker.handler.sol
++++ b/test/helpers/UniStaker.handler.sol
+@@ -10,2 +10,3 @@ import {UniStaker} from "src/UniStaker.sol";
+ import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol";
++import {Uni} from "test/mocks/Uni.sol";
+- IERC20 public stakeToken;
++ Uni public stakeToken;
+ IERC20 public rewardToken;
+@@ -50,3 +51,3 @@ contract UniStakerHandler is CommonBase, StdCheats, StdUtils {
+ uniStaker = _uniStaker;
+- stakeToken = IERC20(address(_uniStaker.STAKE_TOKEN()));
++ stakeToken = Uni(address(_uniStaker.STAKE_TOKEN()));
+ rewardToken = IERC20(address(_uniStaker.REWARD_TOKEN()));
+@@ -57,3 +58,5 @@ contract UniStakerHandler is CommonBase, StdCheats, StdUtils {
+ vm.assume(_to != address(0));
+- deal(address(stakeToken), _to, _amount, true);
++ vm.prank(admin);
++ stakeToken.mint(_to, _amount);
++ vm.stopPrank();
+ }
+@@ -98,2 +101,4 @@ contract UniStakerHandler is CommonBase, StdCheats, StdUtils {
+ {
++ vm.assume(_delegatee != address(0));
++ vm.assume(_beneficiary != address(0));
+ _createDepositor();
+@@ -185,4 +190,4 @@ contract UniStakerHandler is CommonBase, StdCheats, StdUtils {
+
+- function reduceDepositors(uint256 acc, function(uint256,address) external returns (uint256) func)
+- public
++ function reduceDepositors(uint256 acc, function(uint256,address) external view returns (uint256) func)
++ public view
+ returns (uint256)
+@@ -194,4 +199,4 @@ contract UniStakerHandler is CommonBase, StdCheats, StdUtils {
+ uint256 acc,
+- function(uint256,address) external returns (uint256) func
+- ) public returns (uint256) {
++ function(uint256,address) external view returns (uint256) func
++ ) public view returns (uint256) {
+ return _beneficiaries.reduce(acc, func);
+@@ -199,4 +204,4 @@ contract UniStakerHandler is CommonBase, StdCheats, StdUtils {
+
+- function reduceDelegates(uint256 acc, function(uint256,address) external returns (uint256) func)
+- public
++ function reduceDelegates(uint256 acc, function(uint256,address) external view returns (uint256) func)
++ public view
+ returns (uint256)
+```
+
+In particular, the usage of the low-level Foundry's `deal` function, which modifies in place the storage of an ERC20 contract, is incompatible with `UNI`'s vote accounting mechanism and leads to underflows in vote computations with the error thrown `Uni::_moveVotes: vote amount underflows`.
+
+## [07] Extensions to `UniStaker.invariants.t.sol` to track an additional invariant, `invariant_Total_stake_plus_direct_delegations_equals_current_votes`
+
+We have extended `UniStaker.invariants.t.sol` with an additional invariant that asserts that on all changes, either via `UniStaker` or via direct user delegations via `UNI`, the total stake via `UniStaker` summed up with direct delegations, gives the total voting power for all delegates. The changes are outlined below:
+
+
+
+```diff
+diff --git a/test/UniStaker.invariants.t.sol b/test/UniStaker.invariants.t.sol
+index 4c80ce1..5148548 100644
+--- a/test/UniStaker.invariants.t.sol
++++ b/test/UniStaker.invariants.t.sol
+@@ -8,3 +8,4 @@ import {UniStaker} from "src/UniStaker.sol";
+ import {UniStakerHandler} from "test/helpers/UniStaker.handler.sol";
+-import {ERC20VotesMock} from "test/mocks/MockERC20Votes.sol";
++import {Uni} from "test/mocks/Uni.sol";
++import {UniHandler} from "test/helpers/Uni.handler.sol";
+ import {ERC20Fake} from "test/fakes/ERC20Fake.sol";
+@@ -15,4 +16,23 @@ contract UniStakerInvariants is Test {
+ ERC20Fake rewardToken;
+- ERC20VotesMock govToken;
++ Uni govToken;
++ UniHandler uniHandler;
+ address rewardsNotifier;
++ address admin;
++ address alice;
++ address bob;
++ address carol;
++ address dave;
++ address eve;
++ address frank;
++
++ function _jumpAhead(uint256 _seconds) public {
++ vm.warp(block.timestamp + _seconds);
++ }
++
++ function _mintGovToken(address _to, uint256 _amount) internal {
++ vm.assume(_to != address(0));
++ vm.prank(admin);
++ govToken.mint(_to, _amount);
++ vm.stopPrank();
++ }
+
+@@ -22,4 +42,22 @@ contract UniStakerInvariants is Test {
+
+- govToken = new ERC20VotesMock();
+- vm.label(address(govToken), "Governance Token");
++ _jumpAhead(1234);
++ admin = makeAddr("admin");
++ alice = makeAddr("alice");
++ bob = makeAddr("bob");
++ carol = makeAddr("carol");
++ dave = makeAddr("dave");
++ eve = makeAddr("eve");
++ frank = makeAddr("frank");
++
++ govToken = new Uni(admin, admin, 2000);
++ vm.label(address(govToken), "Uni Token");
++ _jumpAhead(1234);
++
++ _mintGovToken(admin, 1e27);
++ _mintGovToken(alice, 1e27);
++ _mintGovToken(bob, 1e27);
++ _mintGovToken(carol, 1e27);
++ _mintGovToken(dave, 1e27);
++ _mintGovToken(eve, 1e27);
++ _mintGovToken(frank, 1e27);
+
+@@ -42,2 +80,19 @@ contract UniStakerInvariants is Test {
+ targetContract(address(handler));
++
++ uniHandler = new UniHandler(govToken);
++ bytes4[] memory uniSelectors = new bytes4[](4);
++ uniSelectors[0] = UniHandler.transfer.selector;
++ uniSelectors[1] = UniHandler.approve.selector;
++ uniSelectors[2] = UniHandler.delegate.selector;
++ uniSelectors[3] = UniHandler.roll.selector;
++
++ targetSelector(FuzzSelector({addr: address(uniHandler), selectors: uniSelectors}));
++
++ targetContract(address(uniHandler));
++ targetSender(alice);
++ targetSender(bob);
++ targetSender(carol);
++ targetSender(dave);
++ targetSender(eve);
++ targetSender(frank);
+ }
+@@ -84,2 +139,23 @@ contract UniStakerInvariants is Test {
+
++ function invariant_Total_stake_plus_direct_delegations_equals_current_votes() public {
++ assertEq(uniStaker.totalStaked() + handler.reduceDelegates(0, this.accumulateDirectDelegateVotes),
++ handler.reduceDelegates(0, this.accumulateCurrentDelegateVotes));
++ }
++
++ function accumulateDirectDelegateVotes(uint256 votes, address delegate)
++ external
++ view
++ returns (uint256)
++ {
++ return votes + uniHandler.sumDelegatorVotes(delegate);
++ }
++
++ function accumulateCurrentDelegateVotes(uint256 votes, address delegate)
++ external
++ view
++ returns (uint256)
++ {
++ return votes + govToken.getCurrentVotes(delegate);
++ }
++
+ // Used to see distribution of non-reverting calls
+```
+
+
+
+## [08] Necessary changes to `AddressSet.sol`
+
+In order to be able to track external user delegations, we had to adapt slightly the helper library `AddressSet.sol`:
+
+```diff
+diff --git a/test/helpers/AddressSet.sol b/test/helpers/AddressSet.sol
+index 83327a7..323ed2c 100644
+--- a/test/helpers/AddressSet.sol
++++ b/test/helpers/AddressSet.sol
+@@ -17,6 +17,20 @@ library LibAddressSet {
+ }
+ }
+
++ function remove(AddressSet storage s, address addr) internal {
++ if (s.saved[addr]) {
++ uint256 len = s.addrs.length;
++ for(uint256 i = 0; i < len; ++i) {
++ if(s.addrs[i] == addr) {
++ s.addrs[i] = s.addrs[len-1];
++ break;
++ }
++ }
++ s.addrs.pop();
++ s.saved[addr] = false;
++ }
++ }
++
+ function contains(AddressSet storage s, address addr) internal view returns (bool) {
+ return s.saved[addr];
+ }
+@@ -39,8 +53,8 @@ library LibAddressSet {
+ function reduce(
+ AddressSet storage s,
+ uint256 acc,
+- function(uint256,address) external returns (uint256) func
+- ) internal returns (uint256) {
++ function(uint256,address) external view returns (uint256) func
++ ) internal view returns (uint256) {
+ for (uint256 i; i < s.addrs.length; ++i) {
+ acc = func(acc, s.addrs[i]);
+ }
+```
+
+## [09] Necessary changes to `foundry.toml`
+
+We had to introduce a few changes to `foundry.toml`. On the one hand, a couple of dependencies were missing, so we've introduced them for the project to compile. On the other hand, the fuzzing/invariant test settings have been in our opinion very low, so we increased the number or the depth of the runs in order to increase the coverage.
+
+```diff
+diff --git a/foundry.toml b/foundry.toml
+index a3031f2..64d0f63 100644
+--- a/foundry.toml
++++ b/foundry.toml
+@@ -2,17 +2,23 @@
+ evm_version = "paris"
+ optimizer = true
+ optimizer_runs = 10_000_000
+- remappings = ["openzeppelin/=lib/openzeppelin-contracts/contracts"]
++ remappings = [
++ "openzeppelin/=lib/openzeppelin-contracts/contracts",
++ "uniswap-periphery/=lib/v3-periphery/contracts",
++ "@uniswap/v3-core=lib/v3-core",
++ ]
+ solc_version = "0.8.23"
+ verbosity = 3
++ fuzz = { runs = 500 }
++ invariant = { runs = 100, depth = 100 }
+
+ [profile.ci]
+ fuzz = { runs = 5000 }
+- invariant = { runs = 1000 }
++ invariant = { runs = 1000, depth = 100 }
+
+ [profile.lite]
+ fuzz = { runs = 50 }
+- invariant = { runs = 10 }
++ invariant = { runs = 10, depth = 100 }
+ # Speed up compilation and tests during development.
+ optimizer = false
+```
+
+Increasing the fuzz/invariant bounds allowed us in particular to observe the following failing test
+
+```sh
+[FAIL. Reason: assertion failed; counterexample: calldata=0xc1e611e700000000000000000000000000000000000000000000000000000000000029fa00000000000000000000000000000000000000000000000000000000000004d3000000000000000000000000aa10a84ce7d9ae517a52c6d5ca153b369af99ecf0000000000000000000000000000000000000000000000000000000000002d6900000000000000000000000000000000000000000000000000000000000000970000000000000000000000000000000000000000000000000000000000000631 args=[0x00000000000000000000000000000000000029fa, 1235, 0xaA10a84CE7d9AE517a52c6d5cA153b369Af99ecF, 11625 [1.162e4], 0x0000000000000000000000000000000000000097, 0x0000000000000000000000000000000000000631]] testFuzz_DeploysAndTransfersTokenToTwoSurrogatesWhenAccountsStakesToDifferentDelegatees(address,uint256,address,uint256,address,address) (runs: 370, μ: 803661, ~: 816488)
+Logs:
+ Bound Result 1235
+ Bound Result 11625
+ Error: a == b not satisfied [uint]
+ Left: 1000000000000000000000000000
+ Right: 0
+```
+
+The reason for the test failure was that due to an increased number of alternatives tried, Foundry's fuzz testing engine picked admin's address to mint to, and thus [this assertion](https://github.com/code-423n4/2024-02-uniswap-foundation/blob/5a2761c8277541a24bc551fbd624413b384bea94/test/UniStaker.t.sol#L414) failed as a result. We have repaired the failing test by disallowing to mint governance tokens to admin's address.
+
+## [10] Applying the changes to the UniStaker testing infrastructure, and running the tests
+
+To correctly set up the environment and apply the modifications, do the following:
+
+- `git clone https://github.com/code-423n4/2024-02-uniswap-foundation.git`
+- `cd 2024-02-uniswap-foundation`
+- `forge install uniswap/v3-core`
+- `forge install uniswap/v3-periphery`
+- Download [this gist](https://gist.github.com/kuprumion/b7b0e03ea52ff925d0f9a9a4dcd7116f), and unpack it e.g. into `../uni`;
+- Place the files as follows inside the repo:
+ - `cp ../uni/foundry.toml ./`
+ - `cp ../uni/Uni.sol ./test/mocks/`
+ - `cp ../uni/Uni.handler.sol ./test/helpers/`
+ - `cp ../uni/UniStaker.handler.sol ./test/helpers/`
+ - `cp ../uni/AddressSet.sol ./test/helpers/`
+ - `cp ../uni/UniStaker.t.sol ./test/`
+ - `cp ../uni/UniStaker.invariants.t.sol ./test/`
+
+Then, execute the tests (excluding the integration tests) via this command:
+
+```sh
+forge test --nmp '*integration*'
+```
+
+To execute and examine the working of the newly introduced invariant, we recommend to focus on it and execute it in verbose mode:
+
+```sh
+forge test -vvvv --nmp '*integration*' --match-test invariant_Total_stake_plus_direct_delegations_equals_current_votes
+```
+
+## [[11] Small stakes reward griefing due to rounding, and actions by anyone with nothing at stake](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/388)
+
+*Note: At the judge’s request [here](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/299#issuecomment-1997457762), this downgraded issue from the same warden has been included in this report for completeness.*
+
+https://github.com/code-423n4/2024-02-uniswap-foundation/blob/491c7f63e5799d95a181be4a978b2f074dc219a5/src/UniStaker.sol#L256-L261
+https://github.com/code-423n4/2024-02-uniswap-foundation/blob/491c7f63e5799d95a181be4a978b2f074dc219a5/src/UniStaker.sol#L292-L303
+https://github.com/code-423n4/2024-02-uniswap-foundation/blob/491c7f63e5799d95a181be4a978b2f074dc219a5/src/UniStaker.sol#L315-L334
+https://github.com/code-423n4/2024-02-uniswap-foundation/blob/491c7f63e5799d95a181be4a978b2f074dc219a5/src/UniStaker.sol#L342-L346
+https://github.com/code-423n4/2024-02-uniswap-foundation/blob/491c7f63e5799d95a181be4a978b2f074dc219a5/src/UniStaker.sol#L360-L373
+https://github.com/code-423n4/2024-02-uniswap-foundation/blob/491c7f63e5799d95a181be4a978b2f074dc219a5/src/UniStaker.sol#L382-L402
+https://github.com/code-423n4/2024-02-uniswap-foundation/blob/491c7f63e5799d95a181be4a978b2f074dc219a5/src/UniStaker.sol#L453-L457
+https://github.com/code-423n4/2024-02-uniswap-foundation/blob/491c7f63e5799d95a181be4a978b2f074dc219a5/src/UniStaker.sol#L466-L492
+https://github.com/code-423n4/2024-02-uniswap-foundation/blob/491c7f63e5799d95a181be4a978b2f074dc219a5/src/UniStaker.sol#L499-L503
+https://github.com/code-423n4/2024-02-uniswap-foundation/blob/491c7f63e5799d95a181be4a978b2f074dc219a5/src/UniStaker.sol#L512-L532
+
+### Impact
+
+Whenever any operation with the given user as a beneficiary is performed, this user's rewards are checkpointed via function [`_checkpointReward()`](https://github.com/code-423n4/2024-02-uniswap-foundation/blob/491c7f63e5799d95a181be4a978b2f074dc219a5/src/UniStaker.sol#L764-L767), which calculates the reward checkpoint by a call to function [`unclaimedReward()`](https://github.com/code-423n4/2024-02-uniswap-foundation/blob/491c7f63e5799d95a181be4a978b2f074dc219a5/src/UniStaker.sol#L241-L247):
+
+```solidity
+ function unclaimedReward(address _beneficiary) public view returns (uint256) {
+ return unclaimedRewardCheckpoint[_beneficiary]
+ + (
+ earningPower[_beneficiary]
+ * (rewardPerTokenAccumulated() - beneficiaryRewardPerTokenCheckpoint[_beneficiary])
+ ) / SCALE_FACTOR;
+ }
+```
+
+The problem with the above function is that it allows for rounding errors, in that it divides by the large `SCALE_FACTOR = 1e36`, which is intended exactly to prevent rounding errors (but in another place). More specifically, the rounding errors happen when:
+
+- The user stake is relatively small (thus, `earningPower[_beneficiary]` is small).
+- The reward amount is relatively small.
+- A small period of time has passed since the previous checkpoint (thus, the second factor becomes small as well).
+
+The last aspect is controllable by any external user (an attacker), which may have zero stake in the system, and still designate the grieved user as a beneficiary, and the attacker can also do it as frequently as needed (e.g. every block). The vulnerable functions are almost all externally callable functions:
+
+- `stake()`, `permitAndStake`, `stakeOnBehalf()`: allow to deposit a zero stake, and to designate arbitrary user as a beneficiary.
+- `stakeMore()`, `permitAndStakeMore()`, `stakeMoreOnBehalf()`: allow to extend an existing stake with an additional zero amount, while checkpointing the same beneficiary.
+- `alterBeneficiary()`, `alterBeneficiaryOnBehalf()`: allow to change deposit beneficiary to an arbitrary user, while checkpointing two users simultaneously (the old and the new beneficiary).
+- `withdraw()`, `withdrawOnBehalf()`: allow to withdraw a zero amount, also from a zero stake.
+
+Any of those functions can be called by an attacker who doesn't need to stake anything (nothing at stake). As a result, the attacked user will be eligible to disproportionately smaller rewards than other users that staked the same amounts, over the same period of time.
+
+### Proof of Concept
+
+The test below demonstrates the exploit; to be placed in [test/UniStaker.t.sol](https://github.com/code-423n4/2024-02-uniswap-foundation/blob/491c7f63e5799d95a181be4a978b2f074dc219a5/test/UniStaker.t.sol#L2709). All amounts are within the bounds as provided by the functions `_boundToRealisticStake()` and `_boundToRealisticReward()`. Instead of `stakeMore()`, an attacker could employ any of the vulnerable functions listed above.
+
+```diff
+diff --git a/test/UniStaker.t.sol b/test/UniStaker.t.sol
+index 89124f8..9a01043 100644
+--- a/test/UniStaker.t.sol
++++ b/test/UniStaker.t.sol
+@@ -2708,2 +2708,50 @@ contract UniStakerRewardsTest is UniStakerTest {
+ contract NotifyRewardAmount is UniStakerRewardsTest {
++ function test_SmallStakesRewardGriefing() public {
++ address _user1 = address(1);
++ address _user2 = address(2);
++ address _user3 = address(3);
++ address _delegatee = address(4);
++ address _attacker = address(5);
++
++ // Mint necessary amounts
++ uint256 _smallDepositAmount = 0.1e18; // from _boundToRealisticStake
++ uint256 _largeDepositAmount = 25_000_000e18; // from _boundToRealisticStake
++ _mintGovToken(_user1, _smallDepositAmount);
++ _mintGovToken(_user2, _smallDepositAmount);
++ _mintGovToken(_user3, _largeDepositAmount);
++
++ // Notify of the rewards
++ uint256 _rewardAmount = 1e14; // from _boundToRealisticReward
++ rewardToken.mint(rewardNotifier, _rewardAmount);
++ vm.startPrank(rewardNotifier);
++ rewardToken.transfer(address(uniStaker), _rewardAmount);
++ uniStaker.notifyRewardAmount(_rewardAmount);
++ vm.stopPrank();
++
++ // Users stake for themselves
++ _stake(_user1, _smallDepositAmount, _delegatee);
++ _stake(_user2, _smallDepositAmount, _delegatee);
++ _stake(_user3, _largeDepositAmount, _delegatee);
++
++ // _attacker has zero funds
++ assertEq(govToken.balanceOf(_attacker), 0);
++
++ // The attack: every block _attacker deposits 0 stake
++ // and assigns _user1 as beneficiary,
++ // thus leading to frequent updates of the reward checkpoint for _user1
++ // with the rounding errors accumulating
++ UniStaker.DepositIdentifier _depositId = _stake(_attacker, 0, _delegatee, _user1);
++ for(uint i = 0; i < 1000; ++i) {
++ _jumpAhead(10); // a conservative 10 seconds between blocks
++ vm.startPrank(_attacker);
++ uniStaker.stakeMore(_depositId, 0);
++ vm.stopPrank();
++ }
++
++ console2.log("Unclaimed reward for _user1: ", uniStaker.unclaimedReward(_user1));
++ console2.log("Unclaimed reward for _user2: ", uniStaker.unclaimedReward(_user2));
++ // This assertion fails: _user1 can now claim substantially less rewards than _user2
++ assertLteWithinOnePercent(uniStaker.unclaimedReward(_user1), uniStaker.unclaimedReward(_user2));
++ }
++
+ function testFuzz_UpdatesTheRewardRate(uint256 _amount) public {
+```
+
+Run the test using `forge test -vvvv --nmp '*integration*' --match-test test_SmallStakesRewardGriefing`.
+Notice that exploit succeeds if the test fails; the failing test prints then the following output, showing that `_user1` may claim only `1000` in rewards, contrary to `_user2`, who staked the same amount but may claim `1543` in rewards.
+
+```sh
+ ├─ [0] VM::startPrank(0x0000000000000000000000000000000000000005)
+ │ └─ ← ()
+ ├─ [14341] UniStaker::stakeMore(3, 0)
+ │ ├─ [4113] Governance Token::transferFrom(0x0000000000000000000000000000000000000005, DelegationSurrogate: [0x4f81992FCe2E1846dD528eC0102e6eE1f61ed3e2], 0)
+ │ │ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000005, to: DelegationSurrogate: [0x4f81992FCe2E1846dD528eC0102e6eE1f61ed3e2], value: 0)
+ │ │ └─ ← true
+ │ ├─ emit StakeDeposited(owner: 0x0000000000000000000000000000000000000005, depositId: 3, amount: 0, depositBalance: 0)
+ │ └─ ← ()
+ ├─ [0] VM::stopPrank()
+ │ └─ ← ()
+ ├─ [2293] UniStaker::unclaimedReward(0x0000000000000000000000000000000000000001) [staticcall]
+ │ └─ ← 1000
+ ├─ [0] console::log("Unclaimed reward for _user1: ", 1000) [staticcall]
+ │ └─ ← ()
+ ├─ [2293] UniStaker::unclaimedReward(0x0000000000000000000000000000000000000002) [staticcall]
+ │ └─ ← 1543
+ ├─ [0] console::log("Unclaimed reward for _user2: ", 1543) [staticcall]
+ │ └─ ← ()
+ ├─ [2293] UniStaker::unclaimedReward(0x0000000000000000000000000000000000000001) [staticcall]
+ │ └─ ← 1000
+ ├─ [2293] UniStaker::unclaimedReward(0x0000000000000000000000000000000000000002) [staticcall]
+ │ └─ ← 1543
+ ├─ emit log(val: "Error: a >= 0.99 * b not satisfied")
+ ├─ emit log_named_uint(key: " Expected", val: 1543)
+ ├─ emit log_named_uint(key: " Actual", val: 1000)
+ ├─ emit log_named_uint(key: " minBound", val: 1527)
+ ├─ [0] VM::store(VM: [0x7109709ECfa91a80626fF3989D68f67F5b1DD12D], 0x6661696c65640000000000000000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000000000000000000000000001)
+ │ └─ ← ()
+ └─ ← ()
+
+Test result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 466.54s
+```
+
+### Tools Used
+
+Foundry
+
+### Recommended Mitigation Steps
+
+We recommend the following simple change to be applied to `src/Unistaker.sol`, which avoids division by `SCALE_FACTOR` when storing checkpoints internally, and instead divides by it only when the rewards are claimed:
+
+```diff
+diff --git a/src/UniStaker.sol b/src/UniStaker.sol
+index babdc1a..237b833 100644
+--- a/src/UniStaker.sol
++++ b/src/UniStaker.sol
+@@ -239,9 +239,9 @@ contract UniStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonces {
+ /// until it is reset to zero once the beneficiary account claims their unearned rewards.
+ /// @return Live value of the unclaimed rewards earned by a given beneficiary account.
+ function unclaimedReward(address _beneficiary) public view returns (uint256) {
+- return unclaimedRewardCheckpoint[_beneficiary]
+- + (
+- earningPower[_beneficiary]
++ return (
++ unclaimedRewardCheckpoint[_beneficiary]
++ + earningPower[_beneficiary]
+ * (rewardPerTokenAccumulated() - beneficiaryRewardPerTokenCheckpoint[_beneficiary])
+ ) / SCALE_FACTOR;
+ }
+@@ -746,7 +746,7 @@ contract UniStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonces {
+ unclaimedRewardCheckpoint[_beneficiary] = 0;
+ emit RewardClaimed(_beneficiary, _reward);
+
+- SafeERC20.safeTransfer(REWARD_TOKEN, _beneficiary, _reward);
++ SafeERC20.safeTransfer(REWARD_TOKEN, _beneficiary, _reward / SCALE_FACTOR);
+ }
+
+ /// @notice Checkpoints the global reward per token accumulator.
+@@ -762,7 +762,11 @@ contract UniStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonces {
+ /// accumulator has been checkpointed. It assumes the global `rewardPerTokenCheckpoint` is up to
+ /// date.
+ function _checkpointReward(address _beneficiary) internal {
+- unclaimedRewardCheckpoint[_beneficiary] = unclaimedReward(_beneficiary);
++ unclaimedRewardCheckpoint[_beneficiary] += (
++ earningPower[_beneficiary]
++ * (rewardPerTokenAccumulated() - beneficiaryRewardPerTokenCheckpoint[_beneficiary])
++ );
++
+ beneficiaryRewardPerTokenCheckpoint[_beneficiary] = rewardPerTokenAccumulatedCheckpoint;
+ }
+```
+
+This change alleviates the problem completely. Now, the output from the previously failing test reads:
+
+```sh
+ ├─ [0] VM::startPrank(0x0000000000000000000000000000000000000005)
+ │ └─ ← ()
+ ├─ [14185] UniStaker::stakeMore(3, 0)
+ │ ├─ [4113] Governance Token::transferFrom(0x0000000000000000000000000000000000000005, DelegationSurrogate: [0x4f81992FCe2E1846dD528eC0102e6eE1f61ed3e2], 0)
+ │ │ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000005, to: DelegationSurrogate: [0x4f81992FCe2E1846dD528eC0102e6eE1f61ed3e2], value: 0)
+ │ │ └─ ← true
+ │ ├─ emit StakeDeposited(owner: 0x0000000000000000000000000000000000000005, depositId: 3, amount: 0, depositBalance: 0)
+ │ └─ ← ()
+ ├─ [0] VM::stopPrank()
+ │ └─ ← ()
+ ├─ [2293] UniStaker::unclaimedReward(0x0000000000000000000000000000000000000001) [staticcall]
+ │ └─ ← 1543
+ ├─ [0] console::log("Unclaimed reward for _user1: ", 1543) [staticcall]
+ │ └─ ← ()
+ ├─ [2293] UniStaker::unclaimedReward(0x0000000000000000000000000000000000000002) [staticcall]
+ │ └─ ← 1543
+ ├─ [0] console::log("Unclaimed reward for _user2: ", 1543) [staticcall]
+ │ └─ ← ()
+ ├─ [2293] UniStaker::unclaimedReward(0x0000000000000000000000000000000000000001) [staticcall]
+ │ └─ ← 1543
+ ├─ [2293] UniStaker::unclaimedReward(0x0000000000000000000000000000000000000002) [staticcall]
+ │ └─ ← 1543
+ └─ ← ()
+
+Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 247.92ms
+```
+
+Besides that, we recommend to apply minimal input validation to all vulnerable functions listed above: allow to stake only above some minimal amount (no zero stakes), disallow to alter beneficiary to the same address, disallow withdrawing zero amounts, etc. While in itself such actions may seem harmless, leaving functions that accept insensible inputs in the system, in combination with other potential problems, may open the way to exploits.
+
+### Assessed type
+
+Math
+
+**[wildmolasses (Uniswap) acknowledged and commented](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/299#issuecomment-1992314009):**
+ > Some decent callouts here; although nothing was found, we appreciate the rigor. I think we would like to mark high quality, thanks warden!
+
+**[0xTheC0der (judge) commented](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/299#issuecomment-1997621087):**
+ > The majority of initial H/M findings which were downgraded to QA exceed the present QA reports in value provided, and none of the present QA reports stand out enough in terms of valid and valuable Low findings to be selected for report. As a consequence, the current report was selected due to its high quality, diligence and value provided to the sponsor.
+
+***
+
+# Audit Analysis
+
+For this audit, 20 analysis reports were submitted by wardens. An analysis report examines the codebase as a whole, providing observations and advice on such topics as architecture, mechanism, or approach. The [report highlighted below](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/312) by **roguereggiant** received the top score from the judge.
+
+*The following wardens also submitted reports: [hunter\_w3b](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/341), [kaveyjoe](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/110), [McToady](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/58), [peanuts](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/399), [Sathish9098](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/389), [0xepley](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/347), [Aamir](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/322), [fouzantanveer](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/320), [hassanshakeel13](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/310), [MSK](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/307), [LinKenji](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/298), [SAQ](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/285), [Myd](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/279), [ihtishamsudo](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/278), [emerald7017](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/266), [aariiif](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/252), [ZanyBonzy](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/153), [cudo](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/139), and [Al-Qa-qa](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/100).*
+
+
+## Project Overview
+
+UniStaker is a mechanism designed to facilitate the collection and distribution of protocol fees generated by the Uniswap V3 pools through UNI token staking. This setup allows Uniswap Governance to enable and manage these protocol fees effectively. By integrating contracts from this repository, Uniswap Governance could maintain the authority to set protocol fees for Uniswap V3 Pools without directly handling the fee assets. Instead, the fees generated are distributed in a trustless manner to UNI holders who opt to stake their tokens. The unique aspect of this system is that rewards for stakers are not in the form of fee tokens directly but in a predefined token established at the deployment of these contracts. The accumulated fees from each pool are periodically auctioned to entities willing to exchange them for the specified token, thereby facilitating the distribution of rewards to stakers.
+
+The operational framework of UniStaker is built around two core contracts: V3FactoryOwner.sol and UniStaker.sol. The V3FactoryOwner contract functions as the new owner of the Uniswap V3 Factory, allowing governance to transfer factory ownership to this contract while retaining control over fee settings through a governance mechanism. On the other hand, the UniStaker contract is responsible for the distribution of staking rewards, employing a mechanism that allows rewards to drip over a fixed period, similar to the Synthetix StakingRewards.sol model. This contract enables UNI stakers to maintain their governance rights, designate beneficiaries for their rewards, and manage their stakes on a per-deposit basis, introducing efficiencies in terms of precision, gas usage, and code clarity. Additionally, UniStaker is designed to accommodate rewards from various sources, with the potential for future expansion beyond Uniswap V3 protocol fees, under the administration of Uniswap Governance.
+
+| File Name | Description |
+| -- | -- |
+| UniStaker.sol | The code defines a smart contract, UniStaker, responsible for managing the distribution of staking rewards in the form of ERC20 tokens to participants who deposit a specific governance token. It allows for flexible management of staking positions, enabling users to delegate voting power, specify reward beneficiaries, and alter these designations while participating in a reward distribution mechanism inspired by Synthetix's model. |
+| V3FactoryOwner.sol | The code defines V3FactoryOwner, a contract serving as the owner of the Uniswap v3 factory, allowing an admin (expected to be Uniswap Governance) to manage fee settings on pools and the factory itself. It enables a public function for collecting protocol fees from pools in exchange for a specified token, aiming to create a competitive market for fee collection. |
+| DelegationSurrogate.sol | DelegationSurrogate is a streamlined contract designed to hold governance tokens on behalf of users while delegating their voting power to a specified delegatee. This approach enables individual token holders to maintain their governance rights by using a separate surrogate for each delegatee, even when their tokens are pooled together under a single contract. |
+
+## Architecture Diagram
+
+The architecture diagram below illustrates the interaction between various components of the system, focusing on governance token delegation and staking rewards distribution. This system allows governance token holders to stake their tokens, delegate their voting power, and earn rewards, all while maintaining their governance rights.
+
+*Note: to view the provided image, please see the original submission [here](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/312).*
+
+### Architecture Overview
+
+1. **Token Holders** represent individuals or entities that own governance tokens. They have the option to stake these tokens in a staking contract to earn rewards and participate in governance by delegating their voting power.
+
+2. **Staking Contract** is the central hub where token holders stake their governance tokens to earn rewards. It interacts with other system components to manage staked tokens and distribute rewards.
+
+3. **DelegationSurrogate** is deployed by the staking contract for each delegatee. Its purpose is to hold staked governance tokens and delegate voting power to a specified delegatee, allowing token holders to maintain their governance rights even when their tokens are pooled together.
+
+4. **Rewards Distribution Mechanism** is responsible for distributing rewards to token holders based on the amount of tokens they have staked and other criteria defined by the system.
+
+5. **Delegatee** is an individual or entity to which the DelegationSurrogate delegates voting power. This allows them to vote in governance proposals on behalf of the token holders who have staked their tokens.
+
+6. **Uniswap V3 Factory** is part of the broader ecosystem, where the staking contract might interact with Uniswap V3 to manage liquidity pools, set fees, or perform other actions related to the governance of Uniswap V3 pools.
+
+7. **Uniswap V3 Pools** are liquidity pools managed by the Uniswap V3 Factory, which can be influenced by governance decisions made by the staking contract, delegatees, or directly by token holders.
+
+## Sequence Diagram
+
+This architecture enables a decentralized and democratic governance system where token holders can earn rewards while participating in the governance of the protocol or ecosystem they are invested in. It balances the need for efficient governance token management with the desire to empower individual token holders.
+Below is a sequence diagram illustrating the interactions within the system, focusing on the process of staking tokens, delegating voting power, and distributing rewards.
+
+*Note: to view the provided image, please see the original submission [here](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/312).*
+
+### Sequence Diagram Overview
+
+1. **Token Holder** initiates the process by staking their governance tokens in the **Staking Contract**.
+
+2. For each stake, the **Staking Contract** either deploys a new **Delegation Surrogate** or selects an existing one, based on the designated **Delegatee**.
+
+3. The **Delegation Surrogate** then delegates the voting power of the staked tokens to the specified **Delegatee**, ensuring that token holders retain their governance rights.
+
+4. Parallelly or subsequently, the **Staking Contract** communicates with the **Rewards Distribution Mechanism** to calculate the rewards for each token holder based on the staked tokens and other criteria.
+
+5. The **Rewards Distribution Mechanism** distributes the calculated rewards back to the **Token Holder**.
+
+6. Optionally, the **Token Holder** might directly delegate their voting power to a **Delegatee**, bypassing the staking mechanism for governance participation.
+
+7. Optionally, the **Staking Contract** might interact with the **Uniswap V3 Factory** for liquidity pool management or other governance actions. The **Uniswap V3 Factory** updates the **Staking Contract** with any changes to pool status or information.
+
+This sequence outlines the flow of actions from staking tokens to receiving rewards while ensuring governance participation through delegation.
+
+## Overview of Functions in the UniStaker Smart Contract
+
+*Note: to view the provided image, please see the original submission [here](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/312).*
+
+### Admin and Reward Notifier Management
+
+- **`setAdmin`**: Updates the admin of the contract. Only the current admin can perform this action.
+- **`setRewardNotifier`**: Enables or disables a reward notifier address, allowing or disallowing it from notifying the contract about new rewards. This action is also restricted to the admin.
+
+### Staking Operations
+
+- **`stake`**: Allows a user to stake tokens into a new deposit, automatically delegating voting power and setting themselves as the reward beneficiary.
+- **`stakeMore`**: Enables adding more tokens to an existing stake, maintaining the current delegatee and beneficiary settings.
+- **`permitAndStake`**: Similar to `stake`, but includes an ERC-20 permit for token approval, reducing transaction steps.
+- **`stakeOnBehalf`**: Allows staking on behalf of another user, with their permission, enabling the staker to specify delegatee and beneficiary.
+- **`stakeMoreOnBehalf`**: Adds more tokens to an existing deposit on behalf of another user, with their permission.
+
+### Delegation and Beneficiary Management
+
+- **`alterDelegatee`**: Changes the delegatee for a specific deposit, allowing the stake's voting power to be redirected.
+- **`alterDelegateeOnBehalf`**: Similar to `alterDelegatee`, but performed on behalf of the deposit owner with their permission.
+- **`alterBeneficiary`**: Changes the beneficiary who earns rewards from a specific deposit.
+- **`alterBeneficiaryOnBehalf`**: Allows changing the beneficiary on behalf of the deposit owner, with their permission.
+
+### Withdrawal and Reward Claiming
+
+- **`withdraw`**: Withdraws staked tokens from a deposit, reducing the stake and potentially affecting reward earnings.
+- **`withdrawOnBehalf`**: Performs a withdrawal on behalf of the deposit owner, with their permission.
+- **`claimReward`**: Allows a beneficiary to claim their earned rewards.
+- **`claimRewardOnBehalf`**: Claims rewards on behalf of a beneficiary, with their permission.
+
+### Reward Notification
+
+- **`notifyRewardAmount`**: Called by authorized reward notifiers to inform the contract about new rewards being added. It adjusts the reward rate and duration accordingly.
+
+### Internal Helper Functions
+
+- **`_fetchOrDeploySurrogate`**: Deploys or retrieves a Delegation Surrogate contract for a specified delegatee.
+- **`_stakeTokenSafeTransferFrom`**: Safely transfers staked tokens from one address to another.
+- **`_useDepositId`**: Generates a unique identifier for a new deposit.
+- **`_stake`**: Core logic for staking operations, handling token transfers, and setting deposit parameters.
+- **`_stakeMore`**: Adds tokens to an existing stake, updating the total staked amount and rewards.
+- **`_alterDelegatee`**: Updates the delegatee for a deposit, managing the delegation of voting power.
+- **`_alterBeneficiary`**: Changes the beneficiary for a deposit, affecting who earns the rewards.
+- **`_withdraw`**: Handles the withdrawal of staked tokens from a deposit.
+- **`_claimReward`**: Processes reward claims, transferring earned rewards to beneficiaries.
+- **`_checkpointGlobalReward`**: Updates the global reward rate and distribution end time based on new rewards.
+- **`_checkpointReward`**: Updates the reward calculation for a specific beneficiary.
+- **`_setAdmin`**: Sets the admin address internally.
+- **`_revertIfNotAdmin`**: Checks if the caller is the admin and reverts if not.
+- **`_revertIfNotDepositOwner`**: Ensures the caller owns the deposit they are trying to modify.
+- **`_revertIfAddressZero`**: Checks for zero addresses in critical parameters.
+- **`_revertIfSignatureIsNotValidNow`**: Validates EIP-712 signatures for actions performed on behalf of others.
+
+This contract facilitates complex staking, delegation, and rewards management operations, integrating with ERC-20 tokens and leveraging DeFi conventions for governance and reward distribution.
+
+## UniStaker Smart Contract Functionalities Overview
+
+### Main Functionalities
+
+- **Stake Tokens**: Allows users to deposit governance tokens into the contract to participate in staking. Users can choose to delegate their voting power to a specific delegatee and designate a beneficiary for their rewards.
+- **Withdraw Tokens**: Permits stakers to withdraw their deposited tokens from the contract. This action ceases their participation in reward distribution.
+- **Claim Rewards**: Enables beneficiaries to claim their accrued rewards. The rewards are calculated based on the proportion of the user's stake relative to the total staked amount and the duration for which the tokens were staked.
+
+### Delegation and Beneficiary Management
+
+- **Delegate Voting Power**: Through the creation or selection of a Delegation Surrogate, stakers can delegate the voting power of their staked tokens to a chosen delegatee, allowing them to participate in governance decisions.
+- **Alter Delegatee**: Stakers have the flexibility to change the delegatee to whom their voting power is assigned.
+- **Designate or Change Beneficiary**: Stakers can specify or change the beneficiary address that will receive the staking rewards for their deposit.
+
+### Reward Notification and Distribution
+
+- **Notify Reward Amount**: Authorized entities can notify the contract about new rewards that have been added to the pool. This resets the reward distribution duration and updates the rate at which rewards are distributed.
+
+### Administration and Permissions
+
+- **Set Admin**: Designates a new admin for the contract. Only the current admin can perform this action.
+- **Enable/Disable Reward Notifier**: Allows the admin to authorize or revoke the permission of addresses to notify the contract of new rewards.
+
+### Utility and Maintenance
+
+- **Fetch or Deploy Surrogate**: Internally handles the deployment of a new Delegation Surrogate contract or selects an existing one for a specific delegatee.
+- **Safe Transfer Operations**: Ensures the safe transfer of tokens to and from the contract, adhering to the ERC20 standard's security practices.
+- **Checkpoints and Accumulators**: Manages checkpoints for global reward distribution and individual beneficiary reward accumulation to ensure accurate and fair reward calculations.
+
+### Security and Validation
+
+- **Unauthorized Access Handling**: The contract includes several checks to prevent unauthorized actions, such as altering delegatees or beneficiaries, withdrawing tokens, and managing admin functions.
+- **Signature Validation**: Supports operations on behalf of users through EIP-712 compliant signatures, ensuring that actions such as staking, withdrawing, and claiming rewards are securely authorized.
+
+### Events
+
+- **Emitted Events**: The contract emits events for significant actions, including deposits, withdrawals, changes in delegatees or beneficiaries, reward claims, and administrative changes. These events facilitate transparency and allow tracking of contract activities.
+
+This smart contract introduces a comprehensive system for staking governance tokens, managing voting power delegation, and distributing rewards. It emphasizes user autonomy by allowing stakers to retain their governance rights through delegation and to designate beneficiaries for their rewards. The contract's security measures, including checks for unauthorized access and signature validation, ensure the integrity of its operations.
+
+*Note: to view the provided image, please see the original submission [here](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/312).*
+
+This sequence diagram outlines the interactions between a user, the UniStaker contract, ERC20 tokens, the Delegation Surrogate, and a reward notifier within the UniStaker system. It demonstrates the flow of stake deposits, stake modifications, withdrawals, reward claims, and reward notifications, emphasizing the contract's role in managing staked tokens, delegating voting power, and distributing rewards.
+
+## V3FactoryOwner Smart Contract Functionalities Overview
+
+### Contract Purpose and Overview
+
+The V3FactoryOwner contract acts as the owner of the Uniswap V3 factory, enabling privileged control over factory and pool settings, including fee management. It also allows the collection of protocol fees from pools through a public function, facilitating an arbitrage opportunity by trading a designated token amount for pool fees.
+
+*Note: to view the provided image, please see the original submission [here](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/312).*
+
+### Key Functionalities
+
+**Administrative Control**
+
+- **Set Admin**: Assigns a new admin to the contract, transferring the ability to perform privileged actions. Only the current admin can execute this change.
+- **Set Payout Amount**: Updates the amount of the payout token required for claiming fees from a pool. This function is reserved for the admin.
+
+**Fee Management**
+
+- **Enable Fee Amount**: Allows the admin to enable new fee tiers within the Uniswap V3 factory, specifying the fee amount and associated tick spacing.
+- **Set Fee Protocol**: Grants the admin the ability to set protocol fee percentages for individual Uniswap V3 pools, adjusting the split between liquidity providers and the protocol.
+
+**Fee Claiming**
+
+- **Claim Fees**: Open to any caller, this function enables the collection of accumulated protocol fees from a specified Uniswap V3 pool. The caller must pay a predetermined amount of a designated payout token, which is then forwarded to a specified reward receiver.
+
+**Constructor and Initialization**
+
+Upon deployment, the constructor initializes the contract by setting:
+- The admin address, who will have exclusive rights to perform certain actions within the contract.
+- The Uniswap V3 Factory contract instance, which this contract will own and manage.
+- The payout token, used as payment for claiming pool fees.
+- The initial payout amount, specifying how much of the payout token must be paid to claim pool fees.
+- The reward receiver contract, which will be notified and receive the payout token when pool fees are claimed.
+
+**Events**
+
+- **`FeesClaimed`**: Emitted when protocol fees are claimed from a pool, indicating the pool address, caller, recipient of the fees, and the amounts of token0 and token1 claimed.
+- **`AdminSet`**: Signals the assignment of a new admin for the contract.
+- **`PayoutAmountSet`**: Announces changes to the payout amount required for claiming pool fees.
+
+**Error Handling**
+
+- **Unauthorized**: Indicates an attempt to perform an action reserved for the admin by an unauthorized address.
+- **Invalid Address**: Used when an operation involves an address parameter that must not be the zero address, such as setting a new admin.
+- **Invalid Payout Amount**: Triggered when attempting to set a zero payout amount, which is not allowed.
+- **Insufficient Fees Collected**: Occurs if the actual fees collected from a pool are less than the amount requested by the caller.
+
+**Security and Permission Checks**
+
+- **`_revertIfNotAdmin`**: A modifier-like internal function that ensures only the admin can perform certain actions, reinforcing the contract's security by restricting sensitive operations.
+
+### Summary
+
+The V3FactoryOwner contract is a critical component for managing Uniswap V3 factory settings, including fee structures and protocol fee collection. Its design focuses on providing administrative control over key parameters while enabling an innovative mechanism for protocol fee collection. Through its public claim fees function, it incentivizes external parties to participate in protocol fee collection, creating a competitive market dynamic. This sequence diagram shows the over all flow of the functionality:
+
+*Note: to view the provided image, please see the original submission [here](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/312).*
+
+## DelegationSurrogate Smart Contract Functionalities Detailed Overview
+
+### Contract Purpose
+
+The `DelegationSurrogate` contract is designed to facilitate governance participation for token holders whose tokens are pooled. It addresses the challenge of maintaining individual governance rights in a pooled environment by allowing the delegation of voting power from pooled tokens to a specified delegatee.
+
+*Note: to view the provided image, please see the original submission [here](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/312).*
+
+### Key Functionalities
+
+**Constructor and Initial Setup**
+
+Upon deployment, the constructor performs crucial initializations to set up the contract's core functionality:
+
+- **Token Delegation**: The constructor takes two arguments: a governance token (`_token`) and a delegatee (`_delegatee`). It immediately delegates the voting power of any governance tokens that will be held by this contract to the specified delegatee. This delegation is crucial for ensuring that the voting power associated with pooled tokens is not lost and can be exercised according to the preferences of the token holders.
+- **Token Approval for Reclaiming**: In addition to delegating voting power, the constructor sets up an approval, allowing the deployer of the contract (most likely a staking pool or another contract pooling governance tokens) to reclaim the tokens without requiring further permissions. This is done by approving the maximum possible amount of tokens (`type(uint256).max`), ensuring that the deployer can manage the tokens as needed without additional transaction overhead.
+
+**Operational Context**
+
+- **Maintaining Governance Rights**: The contract serves to ensure that token holders who contribute their tokens to a pool still have their preferences represented in governance decisions. By delegating the voting power of pooled tokens to chosen delegatees, it ensures that the governance influence of individual token holders is preserved.
+- **Simplifying Token Management**: By approving the contract deployer to manage the tokens, the `DelegationSurrogate` simplifies the administrative aspect of token pooling. This setup allows for the efficient handling of tokens, enabling their movement without requiring individual approval transactions for each action.
+
+**Security and Permissions**
+
+- **Immutable Delegation and Approval**: The actions taken by the constructor - delegating voting power and setting token approval - are performed at the time of contract deployment and cannot be altered afterward. This design choice simplifies the contract's operation and enhances its security by reducing the surface area for potential malicious actions or mistakes after deployment.
+
+**Use Cases**
+
+- **Staking Pools and Governance**: The `DelegationSurrogate` is particularly useful in the context of staking pools or other mechanisms where governance tokens are pooled. It allows these structures to maintain the governance participation rights of their contributors, ensuring that the aggregation of tokens does not dilute individual governance influence.
+- **Token Management Efficiency**: For contracts that manage pooled governance tokens on behalf of users, the `DelegationSurrogate` offers an efficient way to handle these tokens, particularly for operations like reallocating tokens back to users or moving them based on the pool's needs.
+
+### Summary
+
+The `DelegationSurrogate` contract is a streamlined solution designed to preserve the governance rights of token holders within pooled environments. Through its straightforward mechanism of delegating voting power and setting up token approvals at deployment, it ensures that governance participation remains effective and that token management remains efficient.
+
+*Note: to view the provided image, please see the original submission [here](https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/312).*
+
+## Centralization Risks
+
+**Admin Control and Privileged Actions**: A significant centralization risk arises from the extensive control and privileged actions that an admin can perform, such as updating admin addresses, setting payout amounts, enabling or disabling reward notifiers, and other administrative functions. This centralized control could lead to potential misuse or abuse if the admin keys are compromised or if the admin acts maliciously.
+
+**DelegationSurrogate and Voting Power**: The use of `DelegationSurrogate` to delegate voting power centralizes the governance influence in the hands of a few, potentially skewing governance decisions. Although it aims to empower token holders, the actual implementation could lead to centralization of voting power, especially if surrogate contracts are managed or influenced by a small group.
+
+## Systematic Risks
+
+**Dependency on External Contracts and Interfaces**: The system's reliance on external contracts and interfaces like `IUniswapV3PoolOwnerActions`, `IUniswapV3FactoryOwnerActions`, and `IERC20` introduces systematic risks. Changes or vulnerabilities in these external contracts could adversely affect the functionality and security of the system.
+
+**Reward Distribution Mechanism**: The reward distribution mechanism, based on the notification of new rewards and the calculation of distributed rewards, introduces a risk of manipulation or errors in reward calculations. This could lead to loss of funds or unfair distribution of rewards, impacting the integrity of the staking and reward system.
+
+## Architectural Risks
+
+**Upgradability and Flexibility**: The contracts' architecture does not explicitly address upgradability or the ability to adapt to future requirements or fixes. This rigidity could lead to challenges in responding to discovered vulnerabilities, evolving governance models, or integrating with new protocols and standards.
+
+**Inter-contract Communication**: The architecture involves multiple contracts interacting with each other, such as the delegation of voting power through `DelegationSurrogate` and the management of rewards in `UniStaker`. This interdependency increases the complexity and the risk of unintended consequences due to errors in communication or execution logic between contracts.
+
+## Complexity Risks
+
+**Contract Complexity and Interactions**: The contracts exhibit a high degree of complexity, particularly in the management of staking, delegation, and rewards distribution. This complexity increases the risk of bugs or vulnerabilities remaining undetected despite testing and audits.
+
+**Understanding and Participation Barrier**: The complexity of contract interactions and the governance model may pose a barrier to understanding for potential users and participants. This could lead to lower participation in governance or staking, affecting the decentralization and security of the system.
+
+In summary, while the system introduces innovative mechanisms for staking, delegation, and rewards distribution, it also presents centralization, systematic, architectural, and complexity risks that should be carefully managed and mitigated through rigorous security practices, audits, and potentially introducing more decentralized governance mechanisms over time.
+
+### Conclusion
+
+The UniStaker system presents an innovative approach to staking, voting delegation, and rewards distribution within the DeFi ecosystem. While it offers significant benefits in terms of governance participation and incentive mechanisms, it also carries risks related to centralization, system dependencies, architectural rigidity, and operational complexity. Addressing these concerns through continuous audits, enhancing decentralization, and simplifying user interactions will be crucial for.
+
+### Time spent
+
+28 hours
+
+***
+
+# Disclosures
+
+C4 is an open organization governed by participants in the community.
+
+C4 audits incentivize the discovery of exploits, vulnerabilities, and bugs in smart contracts. Security researchers are rewarded at an increasing rate for finding higher-risk issues. Audit submissions are judged by a knowledgeable security researcher and solidity developer and disclosed to sponsoring developers. C4 does not conduct formal verification regarding the provided code but instead provides final verification.
+
+C4 does not provide any guarantee or warranty regarding the security of this project. All smart contract software should be used at the sole risk and responsibility of users.
\ No newline at end of file
diff --git a/audits/2024_04_UniStaker_Cantina_Report.pdf b/audits/2024_04_UniStaker_Cantina_Report.pdf
new file mode 100644
index 0000000..2cbed06
Binary files /dev/null and b/audits/2024_04_UniStaker_Cantina_Report.pdf differ
diff --git a/foundry.toml b/foundry.toml
new file mode 100644
index 0000000..83749d2
--- /dev/null
+++ b/foundry.toml
@@ -0,0 +1,52 @@
+[profile.default]
+ evm_version = "paris"
+ optimizer = true
+ optimizer_runs = 10_000_000
+ remappings = [
+ "openzeppelin/=lib/openzeppelin-contracts/contracts",
+ "uniswap-periphery/=lib/v3-periphery/contracts",
+ "@uniswap/v3-core=lib/v3-core",
+ ]
+ solc_version = "0.8.26"
+ verbosity = 3
+
+[profile.ci]
+ fuzz = { runs = 5000 }
+ invariant = { runs = 1000 }
+
+[profile.lite]
+ fuzz = { runs = 50 }
+ invariant = { runs = 10 }
+ # Speed up compilation and tests during development.
+ optimizer = false
+
+[rpc_endpoints]
+ mainnet = "${MAINNET_RPC_URL}"
+
+[fmt]
+ bracket_spacing = false
+ int_types = "long"
+ line_length = 100
+ multiline_func_header = "attributes_first"
+ number_underscore = "thousands"
+ quote_style = "double"
+ single_line_statement_blocks = "single"
+ tab_width = 2
+ wrap_comments = true
+
+[fuzz]
+ # We turn on this setting to prevent the fuzzer from picking DelegationSurrogate contracts,
+ # including before they're actually even deployed, as some other entity in the test, for example
+ # depositor. This makes no sense and breaks test assertions, but is extremely difficult to handle
+ # with assume statements because we don't have the surrogate address until it's deployed later in
+ # the test.
+ include_storage = false
+
+[invariant]
+ call_override = false
+ depth = 50
+ dictionary_weight = 80
+ fail_on_revert = false
+ include_push_bytes = true
+ include_storage = true
+ runs = 256
diff --git a/lib/forge-std b/lib/forge-std
new file mode 160000
index 0000000..1714bee
--- /dev/null
+++ b/lib/forge-std
@@ -0,0 +1 @@
+Subproject commit 1714bee72e286e73f76e320d110e0eaf5c4e649d
diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts
new file mode 160000
index 0000000..dbb6104
--- /dev/null
+++ b/lib/openzeppelin-contracts
@@ -0,0 +1 @@
+Subproject commit dbb6104ce834628e473d2173bbc9d47f81a9eec3
diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol
new file mode 100644
index 0000000..c8cca3b
--- /dev/null
+++ b/script/Deploy.s.sol
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+// slither-disable-start reentrancy-benign
+
+pragma solidity ^0.8.23;
+
+import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol";
+import {Script} from "forge-std/Script.sol";
+
+import {DeployInput} from "script/DeployInput.sol";
+import {UniStaker} from "src/UniStaker.sol";
+import {IERC20Delegates} from "src/interfaces/IERC20Delegates.sol";
+import {INotifiableRewardReceiver} from "src/interfaces/INotifiableRewardReceiver.sol";
+
+contract Deploy is Script, DeployInput {
+ uint256 deployerPrivateKey;
+
+ function setUp() public {
+ deployerPrivateKey = vm.envOr(
+ "DEPLOYER_PRIVATE_KEY",
+ uint256(0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80)
+ );
+ }
+
+ function run() public returns (UniStaker) {
+ vm.startBroadcast(deployerPrivateKey);
+ // Deploy the staking contract
+ UniStaker uniStaker = new UniStaker(
+ IERC20(PAYOUT_TOKEN_ADDRESS),
+ IERC20Delegates(STAKE_TOKEN_ADDRESS),
+ vm.addr(deployerPrivateKey)
+ );
+
+ // Change UniStaker admin from `msg.sender` to the Governor timelock
+ uniStaker.setAdmin(UNISWAP_GOVERNOR_TIMELOCK);
+ vm.stopBroadcast();
+
+ return uniStaker;
+ }
+}
diff --git a/script/DeployInput.sol b/script/DeployInput.sol
new file mode 100644
index 0000000..6cfe873
--- /dev/null
+++ b/script/DeployInput.sol
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+// slither-disable-start reentrancy-benign
+
+pragma solidity ^0.8.23;
+
+contract DeployInput {
+ address constant UNISWAP_GOVERNOR = 0x408ED6354d4973f66138C91495F2f2FCbd8724C3;
+ address constant UNISWAP_GOVERNOR_TIMELOCK = 0x1a9C8182C09F50C8318d769245beA52c32BE35BC;
+ address constant UNISWAP_V3_FACTORY_ADDRESS = 0x1F98431c8aD98523631AE4a59f267346ea31F984;
+ address constant PAYOUT_TOKEN_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; // WETH
+ uint256 constant PAYOUT_AMOUNT = 10e18; // 10 (WETH)
+ address constant STAKE_TOKEN_ADDRESS = 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984; // UNI
+}
diff --git a/script/interfaces/GovernorBravoInterfaces.sol b/script/interfaces/GovernorBravoInterfaces.sol
new file mode 100644
index 0000000..fe1ed1d
--- /dev/null
+++ b/script/interfaces/GovernorBravoInterfaces.sol
@@ -0,0 +1,114 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+pragma solidity ^0.8.23;
+
+// This interface was created using cast interface. The contract can be found at
+// https://etherscan.io/address/0x53a328f4086d7c0f1fa19e594c9b842125263026#code#F2#L182
+
+interface GovernorBravoDelegate {
+ type ProposalState is uint8;
+
+ struct Receipt {
+ bool hasVoted;
+ uint8 support;
+ uint96 votes;
+ }
+
+ event NewAdmin(address oldAdmin, address newAdmin);
+ event NewImplementation(address oldImplementation, address newImplementation);
+ event NewPendingAdmin(address oldPendingAdmin, address newPendingAdmin);
+ event ProposalCanceled(uint256 id);
+ event ProposalCreated(
+ uint256 id,
+ address proposer,
+ address[] targets,
+ uint256[] values,
+ string[] signatures,
+ bytes[] calldatas,
+ uint256 startBlock,
+ uint256 endBlock,
+ string description
+ );
+ event ProposalExecuted(uint256 id);
+ event ProposalQueued(uint256 id, uint256 eta);
+ event ProposalThresholdSet(uint256 oldProposalThreshold, uint256 newProposalThreshold);
+ event VoteCast(
+ address indexed voter, uint256 proposalId, uint8 support, uint256 votes, string reason
+ );
+ event VotingDelaySet(uint256 oldVotingDelay, uint256 newVotingDelay);
+ event VotingPeriodSet(uint256 oldVotingPeriod, uint256 newVotingPeriod);
+
+ function BALLOT_TYPEHASH() external view returns (bytes32);
+ function DOMAIN_TYPEHASH() external view returns (bytes32);
+ function MAX_PROPOSAL_THRESHOLD() external view returns (uint256);
+ function MAX_VOTING_DELAY() external view returns (uint256);
+ function MAX_VOTING_PERIOD() external view returns (uint256);
+ function MIN_PROPOSAL_THRESHOLD() external view returns (uint256);
+ function MIN_VOTING_DELAY() external view returns (uint256);
+ function MIN_VOTING_PERIOD() external view returns (uint256);
+ function _acceptAdmin() external;
+ function _initiate(uint256 proposalCount) external;
+ function _setPendingAdmin(address newPendingAdmin) external;
+ function _setProposalThreshold(uint256 newProposalThreshold) external;
+ function _setVotingDelay(uint256 newVotingDelay) external;
+ function _setVotingPeriod(uint256 newVotingPeriod) external;
+ function admin() external view returns (address);
+ function cancel(uint256 proposalId) external;
+ function castVote(uint256 proposalId, uint8 support) external;
+ function castVoteBySig(uint256 proposalId, uint8 support, uint8 v, bytes32 r, bytes32 s) external;
+ function castVoteWithReason(uint256 proposalId, uint8 support, string memory reason) external;
+ function execute(uint256 proposalId) external payable;
+ function getActions(uint256 proposalId)
+ external
+ view
+ returns (
+ address[] memory targets,
+ uint256[] memory values,
+ string[] memory signatures,
+ bytes[] memory calldatas
+ );
+ function getReceipt(uint256 proposalId, address voter) external view returns (Receipt memory);
+ function implementation() external view returns (address);
+ function initialProposalId() external view returns (uint256);
+ function initialize(
+ address timelock_,
+ address uni_,
+ uint256 votingPeriod_,
+ uint256 votingDelay_,
+ uint256 proposalThreshold_
+ ) external;
+ function latestProposalIds(address) external view returns (uint256);
+ function name() external view returns (string memory);
+ function pendingAdmin() external view returns (address);
+ function proposalCount() external view returns (uint256);
+ function proposalMaxOperations() external view returns (uint256);
+ function proposalThreshold() external view returns (uint256);
+ function proposals(uint256)
+ external
+ view
+ returns (
+ uint256 id,
+ address proposer,
+ uint256 eta,
+ uint256 startBlock,
+ uint256 endBlock,
+ uint256 forVotes,
+ uint256 againstVotes,
+ uint256 abstainVotes,
+ bool canceled,
+ bool executed
+ );
+ function propose(
+ address[] memory targets,
+ uint256[] memory values,
+ string[] memory signatures,
+ bytes[] memory calldatas,
+ string memory description
+ ) external returns (uint256);
+ function queue(uint256 proposalId) external;
+ function quorumVotes() external view returns (uint256);
+ function state(uint256 proposalId) external view returns (ProposalState);
+ function timelock() external view returns (address);
+ function uni() external view returns (address);
+ function votingDelay() external view returns (uint256);
+ function votingPeriod() external view returns (uint256);
+}
diff --git a/src/DelegationSurrogate.sol b/src/DelegationSurrogate.sol
new file mode 100644
index 0000000..c08bdd3
--- /dev/null
+++ b/src/DelegationSurrogate.sol
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+pragma solidity ^0.8.23;
+
+import {IERC20Delegates} from "src/interfaces/IERC20Delegates.sol";
+
+/// @title DelegationSurrogate
+/// @author ScopeLift
+/// @notice A dead-simple contract whose only purpose is to hold governance tokens on behalf of
+/// users while delegating voting power to one specific delegatee. This is needed because a single
+/// address can only delegate its (full) token weight to a single address at a time. Thus, when a
+/// contract holds governance tokens in a pool on behalf of disparate token holders, those holders
+/// are typically disenfranchised from their governance rights.
+///
+/// If a pool contract deploys a DelegationSurrogate for each delegatee, and transfers each
+/// depositor's tokens to the appropriate surrogate—or deploys it on their behalf—users can retain
+/// their governance rights.
+///
+/// The pool contract deploying the surrogates must handle all accounting. The surrogate simply
+/// delegates its voting weight and max-approves its deployer to allow tokens to be reclaimed.
+contract DelegationSurrogate {
+ /// @param _token The governance token that will be held by this surrogate
+ /// @param _delegatee The address of the would-be voter to which this surrogate will delegate its
+ /// voting weight. 100% of all voting tokens held by this surrogate will be delegated to this
+ /// address.
+ constructor(IERC20Delegates _token, address _delegatee) {
+ _token.delegate(_delegatee);
+ _token.approve(msg.sender, type(uint256).max);
+ }
+}
diff --git a/src/UniStaker.sol b/src/UniStaker.sol
new file mode 100644
index 0000000..2c21eec
--- /dev/null
+++ b/src/UniStaker.sol
@@ -0,0 +1,879 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+pragma solidity ^0.8.23;
+
+import {DelegationSurrogate} from "src/DelegationSurrogate.sol";
+import {INotifiableRewardReceiver} from "src/interfaces/INotifiableRewardReceiver.sol";
+import {IERC20Delegates} from "src/interfaces/IERC20Delegates.sol";
+import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol";
+import {SafeERC20} from "openzeppelin/token/ERC20/utils/SafeERC20.sol";
+import {Multicall} from "openzeppelin/utils/Multicall.sol";
+import {Nonces} from "openzeppelin/utils/Nonces.sol";
+import {SignatureChecker} from "openzeppelin/utils/cryptography/SignatureChecker.sol";
+import {EIP712} from "openzeppelin/utils/cryptography/EIP712.sol";
+
+/// @title UniStaker
+/// @author ScopeLift
+/// @notice This contract manages the distribution of rewards to stakers. Rewards are denominated
+/// in an ERC20 token and sent to the contract by authorized reward notifiers. To stake means to
+/// deposit a designated, delegable ERC20 governance token and leave it over a period of time.
+/// The contract allows stakers to delegate the voting power of the tokens they stake to any
+/// governance delegatee on a per deposit basis. The contract also allows stakers to designate the
+/// beneficiary address that earns rewards for the associated deposit.
+///
+/// The staking mechanism of this contract is directly inspired by the Synthetix StakingRewards.sol
+/// implementation. The core mechanic involves the streaming of rewards over a designated period
+/// of time. Each staker earns rewards proportional to their share of the total stake, and each
+/// staker earns only while their tokens are staked. Stakers may add or withdraw their stake at any
+/// point. Beneficiaries can claim the rewards they've earned at any point. When a new reward is
+/// received, the reward duration restarts, and the rate at which rewards are streamed is updated
+/// to include the newly received rewards along with any remaining rewards that have finished
+/// streaming since the last time a reward was received.
+contract UniStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonces {
+ type DepositIdentifier is uint256;
+
+ /// @notice Emitted when stake is deposited by a depositor, either to a new deposit or one that
+ /// already exists.
+ event StakeDeposited(
+ address owner, DepositIdentifier indexed depositId, uint256 amount, uint256 depositBalance
+ );
+
+ /// @notice Emitted when a depositor withdraws some portion of stake from a given deposit.
+ event StakeWithdrawn(DepositIdentifier indexed depositId, uint256 amount, uint256 depositBalance);
+
+ /// @notice Emitted when a deposit's delegatee is changed.
+ event DelegateeAltered(
+ DepositIdentifier indexed depositId, address oldDelegatee, address newDelegatee
+ );
+
+ /// @notice Emitted when a deposit's beneficiary is changed.
+ event BeneficiaryAltered(
+ DepositIdentifier indexed depositId,
+ address indexed oldBeneficiary,
+ address indexed newBeneficiary
+ );
+
+ /// @notice Emitted when a beneficiary claims their earned reward.
+ event RewardClaimed(address indexed beneficiary, uint256 amount);
+
+ /// @notice Emitted when this contract is notified of a new reward.
+ event RewardNotified(uint256 amount, address notifier);
+
+ /// @notice Emitted when the admin address is set.
+ event AdminSet(address indexed oldAdmin, address indexed newAdmin);
+
+ /// @notice Emitted when a reward notifier address is enabled or disabled.
+ event RewardNotifierSet(address indexed account, bool isEnabled);
+
+ /// @notice Emitted when a surrogate contract is deployed.
+ event SurrogateDeployed(address indexed delegatee, address indexed surrogate);
+
+ /// @notice Thrown when an account attempts a call for which it lacks appropriate permission.
+ /// @param reason Human readable code explaining why the call is unauthorized.
+ /// @param caller The address that attempted the unauthorized call.
+ error UniStaker__Unauthorized(bytes32 reason, address caller);
+
+ /// @notice Thrown if the new rate after a reward notification would be zero.
+ error UniStaker__InvalidRewardRate();
+
+ /// @notice Thrown if the following invariant is broken after a new reward: the contract should
+ /// always have a reward balance sufficient to distribute at the reward rate across the reward
+ /// duration.
+ error UniStaker__InsufficientRewardBalance();
+
+ /// @notice Thrown if a caller attempts to specify address zero for certain designated addresses.
+ error UniStaker__InvalidAddress();
+
+ /// @notice Thrown when an onBehalf method is called with a deadline that has expired.
+ error UniStaker__ExpiredDeadline();
+
+ /// @notice Thrown if a caller supplies an invalid signature to a method that requires one.
+ error UniStaker__InvalidSignature();
+
+ /// @notice Metadata associated with a discrete staking deposit.
+ /// @param balance The deposit's staked balance.
+ /// @param owner The owner of this deposit.
+ /// @param delegatee The governance delegate who receives the voting weight for this deposit.
+ /// @param beneficiary The address that accrues staking rewards earned by this deposit.
+ struct Deposit {
+ uint96 balance;
+ address owner;
+ address delegatee;
+ address beneficiary;
+ }
+
+ /// @notice Type hash used when encoding data for `stakeOnBehalf` calls.
+ bytes32 public constant STAKE_TYPEHASH = keccak256(
+ "Stake(uint96 amount,address delegatee,address beneficiary,address depositor,uint256 nonce,uint256 deadline)"
+ );
+ /// @notice Type hash used when encoding data for `stakeMoreOnBehalf` calls.
+ bytes32 public constant STAKE_MORE_TYPEHASH = keccak256(
+ "StakeMore(uint256 depositId,uint96 amount,address depositor,uint256 nonce,uint256 deadline)"
+ );
+ /// @notice Type hash used when encoding data for `alterDelegateeOnBehalf` calls.
+ bytes32 public constant ALTER_DELEGATEE_TYPEHASH = keccak256(
+ "AlterDelegatee(uint256 depositId,address newDelegatee,address depositor,uint256 nonce,uint256 deadline)"
+ );
+ /// @notice Type hash used when encoding data for `alterBeneficiaryOnBehalf` calls.
+ bytes32 public constant ALTER_BENEFICIARY_TYPEHASH = keccak256(
+ "AlterBeneficiary(uint256 depositId,address newBeneficiary,address depositor,uint256 nonce,uint256 deadline)"
+ );
+ /// @notice Type hash used when encoding data for `withdrawOnBehalf` calls.
+ bytes32 public constant WITHDRAW_TYPEHASH = keccak256(
+ "Withdraw(uint256 depositId,uint96 amount,address depositor,uint256 nonce,uint256 deadline)"
+ );
+ /// @notice Type hash used when encoding data for `claimRewardOnBehalf` calls.
+ bytes32 public constant CLAIM_REWARD_TYPEHASH =
+ keccak256("ClaimReward(address beneficiary,uint256 nonce,uint256 deadline)");
+
+ /// @notice ERC20 token in which rewards are denominated and distributed.
+ IERC20 public immutable REWARD_TOKEN;
+
+ /// @notice Delegable governance token which users stake to earn rewards.
+ IERC20Delegates public immutable STAKE_TOKEN;
+
+ /// @notice Length of time over which rewards sent to this contract are distributed to stakers.
+ uint256 public constant REWARD_DURATION = 30 days;
+
+ /// @notice Scale factor used in reward calculation math to reduce rounding errors caused by
+ /// truncation during division.
+ uint256 public constant SCALE_FACTOR = 1e36;
+
+ /// @dev Unique identifier that will be used for the next deposit.
+ DepositIdentifier private nextDepositId;
+
+ /// @notice Permissioned actor that can enable/disable `rewardNotifier` addresses.
+ address public admin;
+
+ /// @notice Global amount currently staked across all deposits.
+ uint256 public totalStaked;
+
+ /// @notice Tracks the total staked by a depositor across all unique deposits.
+ mapping(address depositor => uint256 amount) public depositorTotalStaked;
+
+ /// @notice Tracks the total stake actively earning rewards for a given beneficiary account.
+ mapping(address beneficiary => uint256 amount) public earningPower;
+
+ /// @notice Stores the metadata associated with a given deposit.
+ mapping(DepositIdentifier depositId => Deposit deposit) public deposits;
+
+ /// @notice Maps the account of each governance delegate with the surrogate contract which holds
+ /// the staked tokens from deposits which assign voting weight to said delegate.
+ mapping(address delegatee => DelegationSurrogate surrogate) public surrogates;
+
+ /// @notice Time at which rewards distribution will complete if there are no new rewards.
+ uint256 public rewardEndTime;
+
+ /// @notice Last time at which the global rewards accumulator was updated.
+ uint256 public lastCheckpointTime;
+
+ /// @notice Global rate at which rewards are currently being distributed to stakers,
+ /// denominated in scaled reward tokens per second, using the SCALE_FACTOR.
+ uint256 public scaledRewardRate;
+
+ /// @notice Checkpoint value of the global reward per token accumulator.
+ uint256 public rewardPerTokenAccumulatedCheckpoint;
+
+ /// @notice Checkpoint of the reward per token accumulator on a per account basis. It represents
+ /// the value of the global accumulator at the last time a given beneficiary's rewards were
+ /// calculated and stored. The difference between the global value and this value can be
+ /// used to calculate the interim rewards earned by given account.
+ mapping(address account => uint256) public beneficiaryRewardPerTokenCheckpoint;
+
+ /// @notice Checkpoint of the unclaimed rewards earned by a given beneficiary with the scale
+ /// factor included. This value is stored any time an action is taken that specifically impacts
+ /// the rate at which rewards are earned by a given beneficiary account. Total unclaimed rewards
+ /// for an account are thus this value plus all rewards earned after this checkpoint was taken.
+ /// This value is reset to zero when a beneficiary account claims their earned rewards.
+ mapping(address account => uint256 amount) public scaledUnclaimedRewardCheckpoint;
+
+ /// @notice Maps addresses to whether they are authorized to call `notifyRewardAmount`.
+ mapping(address rewardNotifier => bool) public isRewardNotifier;
+
+ /// @param _rewardToken ERC20 token in which rewards will be denominated.
+ /// @param _stakeToken Delegable governance token which users will stake to earn rewards.
+ /// @param _admin Address which will have permission to manage rewardNotifiers.
+ constructor(IERC20 _rewardToken, IERC20Delegates _stakeToken, address _admin)
+ EIP712("UniStaker", "1")
+ {
+ REWARD_TOKEN = _rewardToken;
+ STAKE_TOKEN = _stakeToken;
+ _setAdmin(_admin);
+ }
+
+ /// @notice Set the admin address.
+ /// @param _newAdmin Address of the new admin.
+ /// @dev Caller must be the current admin.
+ function setAdmin(address _newAdmin) external {
+ _revertIfNotAdmin();
+ _setAdmin(_newAdmin);
+ }
+
+ /// @notice Enables or disables a reward notifier address.
+ /// @param _rewardNotifier Address of the reward notifier.
+ /// @param _isEnabled `true` to enable the `_rewardNotifier`, or `false` to disable.
+ /// @dev Caller must be the current admin.
+ function setRewardNotifier(address _rewardNotifier, bool _isEnabled) external {
+ _revertIfNotAdmin();
+ isRewardNotifier[_rewardNotifier] = _isEnabled;
+ emit RewardNotifierSet(_rewardNotifier, _isEnabled);
+ }
+
+ /// @notice Timestamp representing the last time at which rewards have been distributed, which is
+ /// either the current timestamp (because rewards are still actively being streamed) or the time
+ /// at which the reward duration ended (because all rewards to date have already been streamed).
+ /// @return Timestamp representing the last time at which rewards have been distributed.
+ function lastTimeRewardDistributed() public view returns (uint256) {
+ if (rewardEndTime <= block.timestamp) return rewardEndTime;
+ else return block.timestamp;
+ }
+
+ /// @notice Live value of the global reward per token accumulator. It is the sum of the last
+ /// checkpoint value with the live calculation of the value that has accumulated in the interim.
+ /// This number should monotonically increase over time as more rewards are distributed.
+ /// @return Live value of the global reward per token accumulator.
+ function rewardPerTokenAccumulated() public view returns (uint256) {
+ if (totalStaked == 0) return rewardPerTokenAccumulatedCheckpoint;
+
+ return rewardPerTokenAccumulatedCheckpoint
+ + (scaledRewardRate * (lastTimeRewardDistributed() - lastCheckpointTime)) / totalStaked;
+ }
+
+ /// @notice Live value of the unclaimed rewards earned by a given beneficiary account. It is the
+ /// sum of the last checkpoint value of their unclaimed rewards with the live calculation of the
+ /// rewards that have accumulated for this account in the interim. This value can only increase,
+ /// until it is reset to zero once the beneficiary account claims their unearned rewards.
+ ///
+ /// Note that the contract tracks the unclaimed rewards internally with the scale factor
+ /// included, in order to avoid the accrual of precision losses as users takes actions that
+ /// cause rewards to be checkpointed. This external helper method is useful for integrations, and
+ /// returns the value after it has been scaled down to the reward token's raw decimal amount.
+ /// @return Live value of the unclaimed rewards earned by a given beneficiary account.
+ function unclaimedReward(address _beneficiary) external view returns (uint256) {
+ return _scaledUnclaimedReward(_beneficiary) / SCALE_FACTOR;
+ }
+
+ /// @notice Stake tokens to a new deposit. The caller must pre-approve the staking contract to
+ /// spend at least the would-be staked amount of the token.
+ /// @param _amount The amount of the staking token to stake.
+ /// @param _delegatee The address to assign the governance voting weight of the staked tokens.
+ /// @return _depositId The unique identifier for this deposit.
+ /// @dev The delegatee may not be the zero address. The deposit will be owned by the message
+ /// sender, and the beneficiary will also be the message sender.
+ function stake(uint96 _amount, address _delegatee)
+ external
+ returns (DepositIdentifier _depositId)
+ {
+ _depositId = _stake(msg.sender, _amount, _delegatee, msg.sender);
+ }
+
+ /// @notice Method to stake tokens to a new deposit. The caller must pre-approve the staking
+ /// contract to spend at least the would-be staked amount of the token.
+ /// @param _amount Quantity of the staking token to stake.
+ /// @param _delegatee Address to assign the governance voting weight of the staked tokens.
+ /// @param _beneficiary Address that will accrue rewards for this stake.
+ /// @return _depositId Unique identifier for this deposit.
+ /// @dev Neither the delegatee nor the beneficiary may be the zero address. The deposit will be
+ /// owned by the message sender.
+ function stake(uint96 _amount, address _delegatee, address _beneficiary)
+ external
+ returns (DepositIdentifier _depositId)
+ {
+ _depositId = _stake(msg.sender, _amount, _delegatee, _beneficiary);
+ }
+
+ /// @notice Method to stake tokens to a new deposit. Before the staking operation occurs, a
+ /// signature is passed to the token contract's permit method to spend the would-be staked amount
+ /// of the token.
+ /// @param _amount Quantity of the staking token to stake.
+ /// @param _delegatee Address to assign the governance voting weight of the staked tokens.
+ /// @param _beneficiary Address that will accrue rewards for this stake.
+ /// @param _deadline The timestamp after which the permit signature should expire.
+ /// @param _v ECDSA signature component: Parity of the `y` coordinate of point `R`
+ /// @param _r ECDSA signature component: x-coordinate of `R`
+ /// @param _s ECDSA signature component: `s` value of the signature
+ /// @return _depositId Unique identifier for this deposit.
+ /// @dev Neither the delegatee nor the beneficiary may be the zero address. The deposit will be
+ /// owned by the message sender.
+ function permitAndStake(
+ uint96 _amount,
+ address _delegatee,
+ address _beneficiary,
+ uint256 _deadline,
+ uint8 _v,
+ bytes32 _r,
+ bytes32 _s
+ ) external returns (DepositIdentifier _depositId) {
+ try STAKE_TOKEN.permit(msg.sender, address(this), _amount, _deadline, _v, _r, _s) {} catch {}
+ _depositId = _stake(msg.sender, _amount, _delegatee, _beneficiary);
+ }
+
+ /// @notice Stake tokens to a new deposit on behalf of a user, using a signature to validate the
+ /// user's intent. The caller must pre-approve the staking contract to spend at least the
+ /// would-be staked amount of the token.
+ /// @param _amount Quantity of the staking token to stake.
+ /// @param _delegatee Address to assign the governance voting weight of the staked tokens.
+ /// @param _beneficiary Address that will accrue rewards for this stake.
+ /// @param _depositor Address of the user on whose behalf this stake is being made.
+ /// @param _deadline The timestamp after which the signature should expire.
+ /// @param _signature Signature of the user authorizing this stake.
+ /// @return _depositId Unique identifier for this deposit.
+ /// @dev Neither the delegatee nor the beneficiary may be the zero address.
+ function stakeOnBehalf(
+ uint96 _amount,
+ address _delegatee,
+ address _beneficiary,
+ address _depositor,
+ uint256 _deadline,
+ bytes memory _signature
+ ) external returns (DepositIdentifier _depositId) {
+ _revertIfPastDeadline(_deadline);
+ _revertIfSignatureIsNotValidNow(
+ _depositor,
+ _hashTypedDataV4(
+ keccak256(
+ abi.encode(
+ STAKE_TYPEHASH,
+ _amount,
+ _delegatee,
+ _beneficiary,
+ _depositor,
+ _useNonce(_depositor),
+ _deadline
+ )
+ )
+ ),
+ _signature
+ );
+ _depositId = _stake(_depositor, _amount, _delegatee, _beneficiary);
+ }
+
+ /// @notice Add more staking tokens to an existing deposit. A staker should call this method when
+ /// they have an existing deposit, and wish to stake more while retaining the same delegatee and
+ /// beneficiary.
+ /// @param _depositId Unique identifier of the deposit to which stake will be added.
+ /// @param _amount Quantity of stake to be added.
+ /// @dev The message sender must be the owner of the deposit.
+ function stakeMore(DepositIdentifier _depositId, uint96 _amount) external {
+ Deposit storage deposit = deposits[_depositId];
+ _revertIfNotDepositOwner(deposit, msg.sender);
+ _stakeMore(deposit, _depositId, _amount);
+ }
+
+ /// @notice Add more staking tokens to an existing deposit. A staker should call this method when
+ /// they have an existing deposit, and wish to stake more while retaining the same delegatee and
+ /// beneficiary. Before the staking operation occurs, a signature is passed to the token
+ /// contract's permit method to spend the would-be staked amount of the token.
+ /// @param _depositId Unique identifier of the deposit to which stake will be added.
+ /// @param _amount Quantity of stake to be added.
+ /// @param _deadline The timestamp after which the permit signature should expire.
+ /// @param _v ECDSA signature component: Parity of the `y` coordinate of point `R`
+ /// @param _r ECDSA signature component: x-coordinate of `R`
+ /// @param _s ECDSA signature component: `s` value of the signature
+ /// @dev The message sender must be the owner of the deposit.
+ function permitAndStakeMore(
+ DepositIdentifier _depositId,
+ uint96 _amount,
+ uint256 _deadline,
+ uint8 _v,
+ bytes32 _r,
+ bytes32 _s
+ ) external {
+ Deposit storage deposit = deposits[_depositId];
+ _revertIfNotDepositOwner(deposit, msg.sender);
+
+ try STAKE_TOKEN.permit(msg.sender, address(this), _amount, _deadline, _v, _r, _s) {} catch {}
+ _stakeMore(deposit, _depositId, _amount);
+ }
+
+ /// @notice Add more staking tokens to an existing deposit on behalf of a user, using a signature
+ /// to validate the user's intent. A staker should call this method when they have an existing
+ /// deposit, and wish to stake more while retaining the same delegatee and beneficiary.
+ /// @param _depositId Unique identifier of the deposit to which stake will be added.
+ /// @param _amount Quantity of stake to be added.
+ /// @param _depositor Address of the user on whose behalf this stake is being made.
+ /// @param _deadline The timestamp after which the signature should expire.
+ /// @param _signature Signature of the user authorizing this stake.
+ function stakeMoreOnBehalf(
+ DepositIdentifier _depositId,
+ uint96 _amount,
+ address _depositor,
+ uint256 _deadline,
+ bytes memory _signature
+ ) external {
+ Deposit storage deposit = deposits[_depositId];
+ _revertIfNotDepositOwner(deposit, _depositor);
+ _revertIfPastDeadline(_deadline);
+ _revertIfSignatureIsNotValidNow(
+ _depositor,
+ _hashTypedDataV4(
+ keccak256(
+ abi.encode(
+ STAKE_MORE_TYPEHASH, _depositId, _amount, _depositor, _useNonce(_depositor), _deadline
+ )
+ )
+ ),
+ _signature
+ );
+
+ _stakeMore(deposit, _depositId, _amount);
+ }
+
+ /// @notice For an existing deposit, change the address to which governance voting power is
+ /// assigned.
+ /// @param _depositId Unique identifier of the deposit which will have its delegatee altered.
+ /// @param _newDelegatee Address of the new governance delegate.
+ /// @dev The new delegatee may not be the zero address. The message sender must be the owner of
+ /// the deposit.
+ function alterDelegatee(DepositIdentifier _depositId, address _newDelegatee) external {
+ Deposit storage deposit = deposits[_depositId];
+ _revertIfNotDepositOwner(deposit, msg.sender);
+ _alterDelegatee(deposit, _depositId, _newDelegatee);
+ }
+
+ /// @notice For an existing deposit, change the address to which governance voting power is
+ /// assigned on behalf of a user, using a signature to validate the user's intent.
+ /// @param _depositId Unique identifier of the deposit which will have its delegatee altered.
+ /// @param _newDelegatee Address of the new governance delegate.
+ /// @param _depositor Address of the user on whose behalf this stake is being made.
+ /// @param _deadline The timestamp after which the signature should expire.
+ /// @param _signature Signature of the user authorizing this stake.
+ /// @dev The new delegatee may not be the zero address.
+ function alterDelegateeOnBehalf(
+ DepositIdentifier _depositId,
+ address _newDelegatee,
+ address _depositor,
+ uint256 _deadline,
+ bytes memory _signature
+ ) external {
+ Deposit storage deposit = deposits[_depositId];
+ _revertIfNotDepositOwner(deposit, _depositor);
+ _revertIfPastDeadline(_deadline);
+ _revertIfSignatureIsNotValidNow(
+ _depositor,
+ _hashTypedDataV4(
+ keccak256(
+ abi.encode(
+ ALTER_DELEGATEE_TYPEHASH,
+ _depositId,
+ _newDelegatee,
+ _depositor,
+ _useNonce(_depositor),
+ _deadline
+ )
+ )
+ ),
+ _signature
+ );
+
+ _alterDelegatee(deposit, _depositId, _newDelegatee);
+ }
+
+ /// @notice For an existing deposit, change the beneficiary to which staking rewards are
+ /// accruing.
+ /// @param _depositId Unique identifier of the deposit which will have its beneficiary altered.
+ /// @param _newBeneficiary Address of the new rewards beneficiary.
+ /// @dev The new beneficiary may not be the zero address. The message sender must be the owner of
+ /// the deposit.
+ function alterBeneficiary(DepositIdentifier _depositId, address _newBeneficiary) external {
+ Deposit storage deposit = deposits[_depositId];
+ _revertIfNotDepositOwner(deposit, msg.sender);
+ _alterBeneficiary(deposit, _depositId, _newBeneficiary);
+ }
+
+ /// @notice For an existing deposit, change the beneficiary to which staking rewards are
+ /// accruing on behalf of a user, using a signature to validate the user's intent.
+ /// @param _depositId Unique identifier of the deposit which will have its beneficiary altered.
+ /// @param _newBeneficiary Address of the new rewards beneficiary.
+ /// @param _depositor Address of the user on whose behalf this stake is being made.
+ /// @param _deadline The timestamp after which the signature should expire.
+ /// @param _signature Signature of the user authorizing this stake.
+ /// @dev The new beneficiary may not be the zero address.
+ function alterBeneficiaryOnBehalf(
+ DepositIdentifier _depositId,
+ address _newBeneficiary,
+ address _depositor,
+ uint256 _deadline,
+ bytes memory _signature
+ ) external {
+ Deposit storage deposit = deposits[_depositId];
+ _revertIfNotDepositOwner(deposit, _depositor);
+ _revertIfPastDeadline(_deadline);
+ _revertIfSignatureIsNotValidNow(
+ _depositor,
+ _hashTypedDataV4(
+ keccak256(
+ abi.encode(
+ ALTER_BENEFICIARY_TYPEHASH,
+ _depositId,
+ _newBeneficiary,
+ _depositor,
+ _useNonce(_depositor),
+ _deadline
+ )
+ )
+ ),
+ _signature
+ );
+
+ _alterBeneficiary(deposit, _depositId, _newBeneficiary);
+ }
+
+ /// @notice Withdraw staked tokens from an existing deposit.
+ /// @param _depositId Unique identifier of the deposit from which stake will be withdrawn.
+ /// @param _amount Quantity of staked token to withdraw.
+ /// @dev The message sender must be the owner of the deposit. Stake is withdrawn to the message
+ /// sender's account.
+ function withdraw(DepositIdentifier _depositId, uint96 _amount) external {
+ Deposit storage deposit = deposits[_depositId];
+ _revertIfNotDepositOwner(deposit, msg.sender);
+ _withdraw(deposit, _depositId, _amount);
+ }
+
+ /// @notice Withdraw staked tokens from an existing deposit on behalf of a user, using a
+ /// signature to validate the user's intent.
+ /// @param _depositId Unique identifier of the deposit from which stake will be withdrawn.
+ /// @param _amount Quantity of staked token to withdraw.
+ /// @param _depositor Address of the user on whose behalf this stake is being made.
+ /// @param _deadline The timestamp after which the signature should expire.
+ /// @param _signature Signature of the user authorizing this stake.
+ /// @dev Stake is withdrawn to the deposit owner's account.
+ function withdrawOnBehalf(
+ DepositIdentifier _depositId,
+ uint96 _amount,
+ address _depositor,
+ uint256 _deadline,
+ bytes memory _signature
+ ) external {
+ Deposit storage deposit = deposits[_depositId];
+ _revertIfNotDepositOwner(deposit, _depositor);
+ _revertIfPastDeadline(_deadline);
+ _revertIfSignatureIsNotValidNow(
+ _depositor,
+ _hashTypedDataV4(
+ keccak256(
+ abi.encode(
+ WITHDRAW_TYPEHASH, _depositId, _amount, _depositor, _useNonce(_depositor), _deadline
+ )
+ )
+ ),
+ _signature
+ );
+
+ _withdraw(deposit, _depositId, _amount);
+ }
+
+ /// @notice Claim reward tokens the message sender has earned as a stake beneficiary. Tokens are
+ /// sent to the message sender.
+ /// @return Amount of reward tokens claimed.
+ function claimReward() external returns (uint256) {
+ return _claimReward(msg.sender);
+ }
+
+ /// @notice Claim earned reward tokens for a beneficiary, using a signature to validate the
+ /// beneficiary's intent. Tokens are sent to the beneficiary.
+ /// @param _beneficiary Address of the beneficiary who will receive the reward.
+ /// @param _deadline The timestamp after which the signature should expire.
+ /// @param _signature Signature of the beneficiary authorizing this reward claim.
+ /// @return Amount of reward tokens claimed.
+ function claimRewardOnBehalf(address _beneficiary, uint256 _deadline, bytes memory _signature)
+ external
+ returns (uint256)
+ {
+ _revertIfPastDeadline(_deadline);
+ _revertIfSignatureIsNotValidNow(
+ _beneficiary,
+ _hashTypedDataV4(
+ keccak256(
+ abi.encode(CLAIM_REWARD_TYPEHASH, _beneficiary, _useNonce(_beneficiary), _deadline)
+ )
+ ),
+ _signature
+ );
+ return _claimReward(_beneficiary);
+ }
+
+ /// @notice Called by an authorized rewards notifier to alert the staking contract that a new
+ /// reward has been transferred to it. It is assumed that the reward has already been
+ /// transferred to this staking contract before the rewards notifier calls this method.
+ /// @param _amount Quantity of reward tokens the staking contract is being notified of.
+ /// @dev It is critical that only well behaved contracts are approved by the admin to call this
+ /// method, for two reasons.
+ ///
+ /// 1. A misbehaving contract could grief stakers by frequently notifying this contract of tiny
+ /// rewards, thereby continuously stretching out the time duration over which real rewards are
+ /// distributed. It is required that reward notifiers supply reasonable rewards at reasonable
+ /// intervals.
+ // 2. A misbehaving contract could falsely notify this contract of rewards that were not actually
+ /// distributed, creating a shortfall for those claiming their rewards after others. It is
+ /// required that a notifier contract always transfers the `_amount` to this contract before
+ /// calling this method.
+ function notifyRewardAmount(uint256 _amount) external {
+ if (!isRewardNotifier[msg.sender]) revert UniStaker__Unauthorized("not notifier", msg.sender);
+
+ // We checkpoint the accumulator without updating the timestamp at which it was updated,
+ // because that second operation will be done after updating the reward rate.
+ rewardPerTokenAccumulatedCheckpoint = rewardPerTokenAccumulated();
+
+ if (block.timestamp >= rewardEndTime) {
+ scaledRewardRate = (_amount * SCALE_FACTOR) / REWARD_DURATION;
+ } else {
+ uint256 _remainingReward = scaledRewardRate * (rewardEndTime - block.timestamp);
+ scaledRewardRate = (_remainingReward + _amount * SCALE_FACTOR) / REWARD_DURATION;
+ }
+
+ rewardEndTime = block.timestamp + REWARD_DURATION;
+ lastCheckpointTime = block.timestamp;
+
+ if ((scaledRewardRate / SCALE_FACTOR) == 0) revert UniStaker__InvalidRewardRate();
+
+ // This check cannot _guarantee_ sufficient rewards have been transferred to the contract,
+ // because it cannot isolate the unclaimed rewards owed to stakers left in the balance. While
+ // this check is useful for preventing degenerate cases, it is not sufficient. Therefore, it is
+ // critical that only safe reward notifier contracts are approved to call this method by the
+ // admin.
+ if (
+ (scaledRewardRate * REWARD_DURATION) > (REWARD_TOKEN.balanceOf(address(this)) * SCALE_FACTOR)
+ ) revert UniStaker__InsufficientRewardBalance();
+
+ emit RewardNotified(_amount, msg.sender);
+ }
+
+ /// @notice Live value of the unclaimed rewards earned by a given beneficiary account with the
+ /// scale factor included. Used internally for calculating reward checkpoints while minimizing
+ /// precision loss.
+ /// @return Live value of the unclaimed rewards earned by a given beneficiary account with the
+ /// scale factor included.
+ /// @dev See documentation for the public, non-scaled `unclaimedReward` method for more details.
+ function _scaledUnclaimedReward(address _beneficiary) internal view returns (uint256) {
+ return scaledUnclaimedRewardCheckpoint[_beneficiary]
+ + (
+ earningPower[_beneficiary]
+ * (rewardPerTokenAccumulated() - beneficiaryRewardPerTokenCheckpoint[_beneficiary])
+ );
+ }
+
+ /// @notice Allows an address to increment their nonce and therefore invalidate any pending signed
+ /// actions.
+ function invalidateNonce() external {
+ _useNonce(msg.sender);
+ }
+
+ /// @notice Internal method which finds the existing surrogate contract—or deploys a new one if
+ /// none exists—for a given delegatee.
+ /// @param _delegatee Account for which a surrogate is sought.
+ /// @return _surrogate The address of the surrogate contract for the delegatee.
+ function _fetchOrDeploySurrogate(address _delegatee)
+ internal
+ returns (DelegationSurrogate _surrogate)
+ {
+ _surrogate = surrogates[_delegatee];
+
+ if (address(_surrogate) == address(0)) {
+ _surrogate = new DelegationSurrogate(STAKE_TOKEN, _delegatee);
+ surrogates[_delegatee] = _surrogate;
+ emit SurrogateDeployed(_delegatee, address(_surrogate));
+ }
+ }
+
+ /// @notice Internal convenience method which calls the `transferFrom` method on the stake token
+ /// contract and reverts on failure.
+ /// @param _from Source account from which stake token is to be transferred.
+ /// @param _to Destination account of the stake token which is to be transferred.
+ /// @param _value Quantity of stake token which is to be transferred.
+ function _stakeTokenSafeTransferFrom(address _from, address _to, uint256 _value) internal {
+ SafeERC20.safeTransferFrom(IERC20(address(STAKE_TOKEN)), _from, _to, _value);
+ }
+
+ /// @notice Internal method which generates and returns a unique, previously unused deposit
+ /// identifier.
+ /// @return _depositId Previously unused deposit identifier.
+ function _useDepositId() internal returns (DepositIdentifier _depositId) {
+ _depositId = nextDepositId;
+ nextDepositId = DepositIdentifier.wrap(DepositIdentifier.unwrap(_depositId) + 1);
+ }
+
+ /// @notice Internal convenience methods which performs the staking operations.
+ /// @dev This method must only be called after proper authorization has been completed.
+ /// @dev See public stake methods for additional documentation.
+ function _stake(address _depositor, uint96 _amount, address _delegatee, address _beneficiary)
+ internal
+ returns (DepositIdentifier _depositId)
+ {
+ _revertIfAddressZero(_delegatee);
+ _revertIfAddressZero(_beneficiary);
+
+ _checkpointGlobalReward();
+ _checkpointReward(_beneficiary);
+
+ DelegationSurrogate _surrogate = _fetchOrDeploySurrogate(_delegatee);
+ _depositId = _useDepositId();
+
+ totalStaked += _amount;
+ depositorTotalStaked[_depositor] += _amount;
+ earningPower[_beneficiary] += _amount;
+ deposits[_depositId] = Deposit({
+ balance: _amount,
+ owner: _depositor,
+ delegatee: _delegatee,
+ beneficiary: _beneficiary
+ });
+ _stakeTokenSafeTransferFrom(_depositor, address(_surrogate), _amount);
+ emit StakeDeposited(_depositor, _depositId, _amount, _amount);
+ emit BeneficiaryAltered(_depositId, address(0), _beneficiary);
+ emit DelegateeAltered(_depositId, address(0), _delegatee);
+ }
+
+ /// @notice Internal convenience method which adds more stake to an existing deposit.
+ /// @dev This method must only be called after proper authorization has been completed.
+ /// @dev See public stakeMore methods for additional documentation.
+ function _stakeMore(Deposit storage deposit, DepositIdentifier _depositId, uint96 _amount)
+ internal
+ {
+ _checkpointGlobalReward();
+ _checkpointReward(deposit.beneficiary);
+
+ DelegationSurrogate _surrogate = surrogates[deposit.delegatee];
+
+ totalStaked += _amount;
+ depositorTotalStaked[deposit.owner] += _amount;
+ earningPower[deposit.beneficiary] += _amount;
+ deposit.balance += _amount;
+ _stakeTokenSafeTransferFrom(deposit.owner, address(_surrogate), _amount);
+ emit StakeDeposited(deposit.owner, _depositId, _amount, deposit.balance);
+ }
+
+ /// @notice Internal convenience method which alters the delegatee of an existing deposit.
+ /// @dev This method must only be called after proper authorization has been completed.
+ /// @dev See public alterDelegatee methods for additional documentation.
+ function _alterDelegatee(
+ Deposit storage deposit,
+ DepositIdentifier _depositId,
+ address _newDelegatee
+ ) internal {
+ _revertIfAddressZero(_newDelegatee);
+ DelegationSurrogate _oldSurrogate = surrogates[deposit.delegatee];
+ emit DelegateeAltered(_depositId, deposit.delegatee, _newDelegatee);
+ deposit.delegatee = _newDelegatee;
+ DelegationSurrogate _newSurrogate = _fetchOrDeploySurrogate(_newDelegatee);
+ _stakeTokenSafeTransferFrom(address(_oldSurrogate), address(_newSurrogate), deposit.balance);
+ }
+
+ /// @notice Internal convenience method which alters the beneficiary of an existing deposit.
+ /// @dev This method must only be called after proper authorization has been completed.
+ /// @dev See public alterBeneficiary methods for additional documentation.
+ function _alterBeneficiary(
+ Deposit storage deposit,
+ DepositIdentifier _depositId,
+ address _newBeneficiary
+ ) internal {
+ _revertIfAddressZero(_newBeneficiary);
+ _checkpointGlobalReward();
+ _checkpointReward(deposit.beneficiary);
+ earningPower[deposit.beneficiary] -= deposit.balance;
+
+ _checkpointReward(_newBeneficiary);
+ emit BeneficiaryAltered(_depositId, deposit.beneficiary, _newBeneficiary);
+ deposit.beneficiary = _newBeneficiary;
+ earningPower[_newBeneficiary] += deposit.balance;
+ }
+
+ /// @notice Internal convenience method which withdraws the stake from an existing deposit.
+ /// @dev This method must only be called after proper authorization has been completed.
+ /// @dev See public withdraw methods for additional documentation.
+ function _withdraw(Deposit storage deposit, DepositIdentifier _depositId, uint96 _amount)
+ internal
+ {
+ _checkpointGlobalReward();
+ _checkpointReward(deposit.beneficiary);
+
+ deposit.balance -= _amount; // overflow prevents withdrawing more than balance
+ totalStaked -= _amount;
+ depositorTotalStaked[deposit.owner] -= _amount;
+ earningPower[deposit.beneficiary] -= _amount;
+ _stakeTokenSafeTransferFrom(address(surrogates[deposit.delegatee]), deposit.owner, _amount);
+ emit StakeWithdrawn(_depositId, _amount, deposit.balance);
+ }
+
+ /// @notice Internal convenience method which claims earned rewards.
+ /// @return Amount of reward tokens claimed.
+ /// @dev This method must only be called after proper authorization has been completed.
+ /// @dev See public claimReward methods for additional documentation.
+ function _claimReward(address _beneficiary) internal returns (uint256) {
+ _checkpointGlobalReward();
+ _checkpointReward(_beneficiary);
+
+ uint256 _reward = scaledUnclaimedRewardCheckpoint[_beneficiary] / SCALE_FACTOR;
+ if (_reward == 0) return 0;
+
+ // retain sub-wei dust that would be left due to the precision loss
+ scaledUnclaimedRewardCheckpoint[_beneficiary] =
+ scaledUnclaimedRewardCheckpoint[_beneficiary] - (_reward * SCALE_FACTOR);
+ emit RewardClaimed(_beneficiary, _reward);
+
+ SafeERC20.safeTransfer(REWARD_TOKEN, _beneficiary, _reward);
+ return _reward;
+ }
+
+ /// @notice Checkpoints the global reward per token accumulator.
+ function _checkpointGlobalReward() internal {
+ rewardPerTokenAccumulatedCheckpoint = rewardPerTokenAccumulated();
+ lastCheckpointTime = lastTimeRewardDistributed();
+ }
+
+ /// @notice Checkpoints the unclaimed rewards and reward per token accumulator of a given
+ /// beneficiary account.
+ /// @param _beneficiary The account for which reward parameters will be checkpointed.
+ /// @dev This is a sensitive internal helper method that must only be called after global rewards
+ /// accumulator has been checkpointed. It assumes the global `rewardPerTokenCheckpoint` is up to
+ /// date.
+ function _checkpointReward(address _beneficiary) internal {
+ scaledUnclaimedRewardCheckpoint[_beneficiary] = _scaledUnclaimedReward(_beneficiary);
+ beneficiaryRewardPerTokenCheckpoint[_beneficiary] = rewardPerTokenAccumulatedCheckpoint;
+ }
+
+ /// @notice Internal helper method which sets the admin address.
+ /// @param _newAdmin Address of the new admin.
+ function _setAdmin(address _newAdmin) internal {
+ _revertIfAddressZero(_newAdmin);
+ emit AdminSet(admin, _newAdmin);
+ admin = _newAdmin;
+ }
+
+ /// @notice Internal helper method which reverts UniStaker__Unauthorized if the message sender is
+ /// not the admin.
+ function _revertIfNotAdmin() internal view {
+ if (msg.sender != admin) revert UniStaker__Unauthorized("not admin", msg.sender);
+ }
+
+ /// @notice Internal helper method which reverts UniStaker__Unauthorized if the alleged owner is
+ /// not the true owner of the deposit.
+ /// @param deposit Deposit to validate.
+ /// @param owner Alleged owner of deposit.
+ function _revertIfNotDepositOwner(Deposit storage deposit, address owner) internal view {
+ if (owner != deposit.owner) revert UniStaker__Unauthorized("not owner", owner);
+ }
+
+ /// @notice Internal helper method which reverts with UniStaker__InvalidAddress if the account in
+ /// question is address zero.
+ /// @param _account Account to verify.
+ function _revertIfAddressZero(address _account) internal pure {
+ if (_account == address(0)) revert UniStaker__InvalidAddress();
+ }
+
+ function _revertIfPastDeadline(uint256 _deadline) internal view {
+ if (block.timestamp > _deadline) revert UniStaker__ExpiredDeadline();
+ }
+
+ /// @notice Internal helper method which reverts with UniStaker__InvalidSignature if the signature
+ /// is invalid.
+ /// @param _signer Address of the signer.
+ /// @param _hash Hash of the message.
+ /// @param _signature Signature to validate.
+ function _revertIfSignatureIsNotValidNow(address _signer, bytes32 _hash, bytes memory _signature)
+ internal
+ view
+ {
+ bool _isValid = SignatureChecker.isValidSignatureNow(_signer, _hash, _signature);
+ if (!_isValid) revert UniStaker__InvalidSignature();
+ }
+}
diff --git a/src/interfaces/IERC20Delegates.sol b/src/interfaces/IERC20Delegates.sol
new file mode 100644
index 0000000..46353e3
--- /dev/null
+++ b/src/interfaces/IERC20Delegates.sol
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+pragma solidity ^0.8.23;
+
+/// @notice A subset of the ERC20Votes-style governance token to which UNI conforms.
+/// Methods related to standard ERC20 functionality and to delegation are included.
+/// These methods are needed in the context of this system. Methods related to check pointing,
+/// past voting weights, and other functionality are omitted.
+interface IERC20Delegates {
+ // ERC20 related methods
+ function allowance(address account, address spender) external view returns (uint256);
+ function approve(address spender, uint256 rawAmount) external returns (bool);
+ function balanceOf(address account) external view returns (uint256);
+ function decimals() external view returns (uint8);
+ function symbol() external view returns (string memory);
+ function totalSupply() external view returns (uint256);
+ function transfer(address dst, uint256 rawAmount) external returns (bool);
+ function transferFrom(address src, address dst, uint256 rawAmount) external returns (bool);
+ function permit(
+ address owner,
+ address spender,
+ uint256 rawAmount,
+ uint256 deadline,
+ uint8 v,
+ bytes32 r,
+ bytes32 s
+ ) external;
+
+ // ERC20Votes delegation methods
+ function delegate(address delegatee) external;
+ function delegates(address) external view returns (address);
+}
diff --git a/src/interfaces/INotifiableRewardReceiver.sol b/src/interfaces/INotifiableRewardReceiver.sol
new file mode 100644
index 0000000..b132311
--- /dev/null
+++ b/src/interfaces/INotifiableRewardReceiver.sol
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+pragma solidity ^0.8.23;
+
+/// @title INotifiableRewardReceiver
+/// @author ScopeLift
+/// @notice The communication interface between the V3FactoryOwner contract and the UniStaker
+/// contract. In particular, the V3FactoryOwner only needs to know the latter implements the
+/// specified method in order to forward payouts to the UniStaker contract. The UniStaker contract
+/// receives the rewards and abstracts the distribution mechanics
+interface INotifiableRewardReceiver {
+ /// @notice Method called to notify a reward receiver it has received a reward.
+ /// @param _amount The amount of reward.
+ function notifyRewardAmount(uint256 _amount) external;
+}
diff --git a/test/DelegationSurrogate.t.sol b/test/DelegationSurrogate.t.sol
new file mode 100644
index 0000000..f74276e
--- /dev/null
+++ b/test/DelegationSurrogate.t.sol
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+pragma solidity ^0.8.23;
+
+import {Test, console2} from "forge-std/Test.sol";
+import {DelegationSurrogate} from "src/DelegationSurrogate.sol";
+import {ERC20VotesMock} from "test/mocks/MockERC20Votes.sol";
+
+contract DelegationSurrogateTest is Test {
+ ERC20VotesMock govToken;
+
+ function setUp() public {
+ govToken = new ERC20VotesMock();
+ vm.label(address(govToken), "Governance Token");
+ }
+
+ function __deploy(address _deployer, address _delegatee) public returns (DelegationSurrogate) {
+ vm.assume(_deployer != address(0));
+
+ vm.prank(_deployer);
+ DelegationSurrogate _surrogate = new DelegationSurrogate(govToken, _delegatee);
+ return _surrogate;
+ }
+}
+
+contract Constructor is DelegationSurrogateTest {
+ function testFuzz_DelegatesToDeployer(address _deployer, address _delegatee) public {
+ DelegationSurrogate _surrogate = __deploy(_deployer, _delegatee);
+ assertEq(_delegatee, govToken.delegates(address(_surrogate)));
+ }
+
+ function testFuzz_MaxApprovesDeployerToEnableWithdrawals(
+ address _deployer,
+ address _delegatee,
+ uint256 _amount,
+ address _receiver
+ ) public {
+ vm.assume(_receiver != address(0));
+
+ DelegationSurrogate _surrogate = __deploy(_deployer, _delegatee);
+ govToken.mint(address(_surrogate), _amount);
+
+ uint256 _allowance = govToken.allowance(address(_surrogate), _deployer);
+ assertEq(_allowance, type(uint256).max);
+
+ vm.prank(_deployer);
+ govToken.transferFrom(address(_surrogate), _receiver, _amount);
+
+ assertEq(govToken.balanceOf(_receiver), _amount);
+ }
+}
diff --git a/test/README.md b/test/README.md
new file mode 100644
index 0000000..24daad7
--- /dev/null
+++ b/test/README.md
@@ -0,0 +1,68 @@
+# Invariant Suite
+
+The invariant suite is a collection of tests designed to build confidence around certain properties of the system expected to be true.
+
+## Invariants under test
+
+- The total staked balance should equal the sum of all individual depositors' balances
+- The sum of beneficiary earning power should equal the total staked balance
+- The sum of all surrogate balance should equal the total staked balance
+- Cumulative deposits minus withdrawals should equal the total staked balance
+- The sum of all notified rewards should be greater or equal to all claimed rewards plus the rewards balance in the staking contract (TODO: not strictly equal because of stray transfers in, which are not yet implemented in handler)
+- Sum of unclaimed reward across all beneficiaries should be less than or equal to total rewards
+- `rewardPerTokenAccumulatedCheckpoint` should be greater or equal to the last `rewardPerTokenAccumulatedCheckpoint` value
+
+## Invariant Handler
+
+The handler contract specifies the actions that should be taken in the black box of an invariant run. Here is a list of implemented actions the handler contract can take, as well as ideas for further actions.
+
+### Valid user actions
+
+These actions are typical user actions that can be taken on the system. They are used to test the system's behavior under normal conditions.
+
+- [x] stake: a user deposits some amount of STAKE_TOKEN, specifying a delegatee and optionally a beneficiary.
+ - Action taken by: any user
+- [x] stakeMore: a user increments the balance on an existing deposit that she owns.
+ - Action taken by: existing depositors
+- [x] withdraw: a user withdraws some balance from a deposit that she owns.
+ - Action taken by: existing depositors
+- [x] claimReward: A beneficiary claims the reward that is due to her.
+ - Action taken by: existing beneficiaries
+- [ ] alterDelegatee
+- [ ] alterBeneficiary
+- [ ] permitAndStake
+- [x] enable rewards notifier
+- [x] notifyRewardAmount
+- [ ] all of the `onBehalf` methods
+- [ ] multicall
+
+### Invalid user actions
+
+- [ ] Staking without sufficient ERC20 approval
+- [ ] Stake more on a deposit that does not belong to you
+- [ ] State more on a deposit that does not exist
+- [ ] Alter beneficiary and alter delegatee on a deposit that is not yours or does not exist
+- [ ] withdraw on deposit that's not yours
+- [ ] call notifyRewardsAmount if you are not rewards notifier, or insufficient/incorrect reward balance
+- [ ] setAdmin and setRewardNotifier without being the admin
+- [ ] Invalid signature on the `onBehalf` methods
+- [ ] multicall
+
+### Weird user actions
+
+These are actions that are outside the normal use of the system. They are used to test the system's behavior under abnormal conditions.
+
+- [ ] directly transfer in some amount of STAKE_TOKEN to UniStaker
+- [ ] directly transfer some amount of REWARD_TOKEN to UniStaker
+- [ ] transfer stake directly to surrogate
+- [ ] reentrancy attempts
+- [ ] SELFDESTRUCT to this contract
+- [ ] flash loan?
+- [ ] User uses the staking contract as the from address in a `transferFrom`
+- [ ] A non-beneficiary calls claim reward
+- [x] withdraw with zero amount
+- [ ] multicall
+
+### Utility actions
+
+- [x] `warpAhead`: warp the block timestamp ahead by a specified number of seconds.
diff --git a/test/UniStaker.invariants.t.sol b/test/UniStaker.invariants.t.sol
new file mode 100644
index 0000000..2625a13
--- /dev/null
+++ b/test/UniStaker.invariants.t.sol
@@ -0,0 +1,123 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+pragma solidity ^0.8.23;
+
+import {Test} from "forge-std/Test.sol";
+import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol";
+
+import {UniStaker} from "src/UniStaker.sol";
+import {UniStakerHandler} from "test/helpers/UniStaker.handler.sol";
+import {ERC20VotesMock} from "test/mocks/MockERC20Votes.sol";
+import {ERC20Fake} from "test/fakes/ERC20Fake.sol";
+
+contract UniStakerInvariants is Test {
+ UniStakerHandler public handler;
+ UniStaker public uniStaker;
+ ERC20Fake rewardToken;
+ ERC20VotesMock govToken;
+ address rewardsNotifier;
+
+ function setUp() public {
+ rewardToken = new ERC20Fake();
+ vm.label(address(rewardToken), "Rewards Token");
+
+ govToken = new ERC20VotesMock();
+ vm.label(address(govToken), "Governance Token");
+
+ rewardsNotifier = address(0xaffab1ebeef);
+ vm.label(rewardsNotifier, "Rewards Notifier");
+ uniStaker = new UniStaker(rewardToken, govToken, rewardsNotifier);
+ handler = new UniStakerHandler(uniStaker);
+
+ bytes4[] memory selectors = new bytes4[](7);
+ selectors[0] = UniStakerHandler.stake.selector;
+ selectors[1] = UniStakerHandler.validStakeMore.selector;
+ selectors[2] = UniStakerHandler.validWithdraw.selector;
+ selectors[3] = UniStakerHandler.warpAhead.selector;
+ selectors[4] = UniStakerHandler.claimReward.selector;
+ selectors[5] = UniStakerHandler.enableRewardNotifier.selector;
+ selectors[6] = UniStakerHandler.notifyRewardAmount.selector;
+
+ targetSelector(FuzzSelector({addr: address(handler), selectors: selectors}));
+
+ targetContract(address(handler));
+ }
+
+ // Invariants
+
+ function invariant_Sum_of_all_depositor_balances_equals_total_stake() public {
+ assertEq(uniStaker.totalStaked(), handler.reduceDepositors(0, this.accumulateDeposits));
+ }
+
+ function invariant_Sum_of_beneficiary_earning_power_equals_total_stake() public {
+ assertEq(uniStaker.totalStaked(), handler.reduceBeneficiaries(0, this.accumulateEarningPower));
+ }
+
+ function invariant_Sum_of_surrogate_balance_equals_total_stake() public {
+ assertEq(uniStaker.totalStaked(), handler.reduceDelegates(0, this.accumulateSurrogateBalance));
+ }
+
+ function invariant_Cumulative_staked_minus_withdrawals_equals_total_stake() public view {
+ assertEq(uniStaker.totalStaked(), handler.ghost_stakeSum() - handler.ghost_stakeWithdrawn());
+ }
+
+ function invariant_Sum_of_notified_rewards_equals_all_claimed_rewards_plus_rewards_left()
+ public
+ view
+ {
+ assertEq(
+ handler.ghost_rewardsNotified(),
+ rewardToken.balanceOf(address(uniStaker)) + handler.ghost_rewardsClaimed()
+ );
+ }
+
+ function invariant_Sum_of_unclaimed_reward_should_be_less_than_or_equal_to_total_rewards() public {
+ assertLe(
+ handler.reduceBeneficiaries(0, this.accumulateUnclaimedReward),
+ rewardToken.balanceOf(address(uniStaker))
+ );
+ }
+
+ function invariant_RewardPerTokenAccumulatedCheckpoint_should_be_greater_or_equal_to_the_last_rewardPerTokenAccumulatedCheckpoint(
+ ) public view {
+ assertGe(
+ uniStaker.rewardPerTokenAccumulatedCheckpoint(),
+ handler.ghost_prevRewardPerTokenAccumulatedCheckpoint()
+ );
+ }
+
+ // Used to see distribution of non-reverting calls
+ function invariant_callSummary() public view {
+ handler.callSummary();
+ }
+
+ // Helpers
+
+ function accumulateDeposits(uint256 balance, address depositor) external view returns (uint256) {
+ return balance + uniStaker.depositorTotalStaked(depositor);
+ }
+
+ function accumulateEarningPower(uint256 earningPower, address caller)
+ external
+ view
+ returns (uint256)
+ {
+ return earningPower + uniStaker.earningPower(caller);
+ }
+
+ function accumulateUnclaimedReward(uint256 unclaimedReward, address beneficiary)
+ external
+ view
+ returns (uint256)
+ {
+ return unclaimedReward + uniStaker.unclaimedReward(beneficiary);
+ }
+
+ function accumulateSurrogateBalance(uint256 balance, address delegate)
+ external
+ view
+ returns (uint256)
+ {
+ address surrogateAddr = address(uniStaker.surrogates(delegate));
+ return balance + IERC20(address(uniStaker.STAKE_TOKEN())).balanceOf(surrogateAddr);
+ }
+}
diff --git a/test/UniStaker.t.sol b/test/UniStaker.t.sol
new file mode 100644
index 0000000..feff2f1
--- /dev/null
+++ b/test/UniStaker.t.sol
@@ -0,0 +1,5460 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+pragma solidity ^0.8.23;
+
+import {Vm, Test, stdStorage, StdStorage, console2} from "forge-std/Test.sol";
+import {UniStaker, DelegationSurrogate, IERC20, IERC20Delegates} from "src/UniStaker.sol";
+import {UniStakerHarness} from "test/harnesses/UniStakerHarness.sol";
+import {ERC20VotesMock, ERC20Permit} from "test/mocks/MockERC20Votes.sol";
+import {IERC20Errors} from "openzeppelin/interfaces/draft-IERC6093.sol";
+import {ERC20Fake} from "test/fakes/ERC20Fake.sol";
+import {PercentAssertions} from "test/helpers/PercentAssertions.sol";
+
+contract UniStakerTest is Test, PercentAssertions {
+ ERC20Fake rewardToken;
+ ERC20VotesMock govToken;
+ address admin;
+ address rewardNotifier;
+ UniStakerHarness uniStaker;
+ uint256 SCALE_FACTOR;
+ // console2.log(uint(_domainSeparatorV4()))
+ bytes32 EIP712_DOMAIN_SEPARATOR = bytes32(
+ uint256(
+ 108_300_748_413_663_721_746_865_897_746_851_483_791_898_864_552_448_882_835_473_754_343_054_398_579_494
+ )
+ );
+
+ bytes32 constant PERMIT_TYPEHASH =
+ keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
+
+ event RewardNotifierSet(address indexed account, bool isEnabled);
+ event AdminSet(address indexed oldAdmin, address indexed newAdmin);
+
+ mapping(DelegationSurrogate surrogate => bool isKnown) isKnownSurrogate;
+ mapping(address depositor => bool isKnown) isKnownDepositor;
+
+ function setUp() public {
+ // Set the block timestamp to an arbitrary value to avoid introducing assumptions into tests
+ // based on a starting timestamp of 0, which is the default.
+ _jumpAhead(1234);
+
+ rewardToken = new ERC20Fake();
+ vm.label(address(rewardToken), "Reward Token");
+
+ govToken = new ERC20VotesMock();
+ vm.label(address(govToken), "Governance Token");
+
+ rewardNotifier = address(0xaffab1ebeef);
+ vm.label(rewardNotifier, "Reward Notifier");
+
+ admin = makeAddr("admin");
+
+ uniStaker = new UniStakerHarness(rewardToken, govToken, admin);
+ vm.label(address(uniStaker), "UniStaker");
+
+ vm.prank(admin);
+ uniStaker.setRewardNotifier(rewardNotifier, true);
+
+ // Convenience for use in tests
+ SCALE_FACTOR = uniStaker.SCALE_FACTOR();
+ }
+
+ function _jumpAhead(uint256 _seconds) public {
+ vm.warp(block.timestamp + _seconds);
+ }
+
+ function _boundMintAmount(uint96 _amount) internal pure returns (uint96) {
+ return uint96(bound(_amount, 0, 100_000_000e18));
+ }
+
+ function _mintGovToken(address _to, uint96 _amount) internal {
+ vm.assume(_to != address(0));
+ govToken.mint(_to, _amount);
+ }
+
+ function _boundToRealisticStake(uint96 _stakeAmount)
+ public
+ pure
+ returns (uint96 _boundedStakeAmount)
+ {
+ _boundedStakeAmount = uint96(bound(_stakeAmount, 0.1e18, 25_000_000e18));
+ }
+
+ // Remember each depositor and surrogate (as they're deployed) and ensure that there is
+ // no overlap between them. This is to prevent the fuzzer from selecting a surrogate as a
+ // depositor or vice versa.
+ function _assumeSafeDepositorAndSurrogate(address _depositor, address _delegatee) internal {
+ DelegationSurrogate _surrogate = uniStaker.surrogates(_delegatee);
+ isKnownDepositor[_depositor] = true;
+ isKnownSurrogate[_surrogate] = true;
+
+ vm.assume(
+ (!isKnownSurrogate[DelegationSurrogate(_depositor)])
+ && (!isKnownDepositor[address(_surrogate)])
+ );
+ }
+
+ function _stake(address _depositor, uint96 _amount, address _delegatee)
+ internal
+ returns (UniStaker.DepositIdentifier _depositId)
+ {
+ vm.assume(_delegatee != address(0));
+
+ vm.startPrank(_depositor);
+ govToken.approve(address(uniStaker), _amount);
+ _depositId = uniStaker.stake(_amount, _delegatee);
+ vm.stopPrank();
+
+ // Called after the stake so the surrogate will exist
+ _assumeSafeDepositorAndSurrogate(_depositor, _delegatee);
+ }
+
+ function _stake(address _depositor, uint96 _amount, address _delegatee, address _beneficiary)
+ internal
+ returns (UniStaker.DepositIdentifier _depositId)
+ {
+ vm.assume(_delegatee != address(0) && _beneficiary != address(0));
+
+ vm.startPrank(_depositor);
+ govToken.approve(address(uniStaker), _amount);
+ _depositId = uniStaker.stake(_amount, _delegatee, _beneficiary);
+ vm.stopPrank();
+
+ // Called after the stake so the surrogate will exist
+ _assumeSafeDepositorAndSurrogate(_depositor, _delegatee);
+ }
+
+ function _fetchDeposit(UniStaker.DepositIdentifier _depositId)
+ internal
+ view
+ returns (UniStaker.Deposit memory)
+ {
+ (uint96 _balance, address _owner, address _delegatee, address _beneficiary) =
+ uniStaker.deposits(_depositId);
+ return UniStaker.Deposit({
+ balance: _balance,
+ owner: _owner,
+ delegatee: _delegatee,
+ beneficiary: _beneficiary
+ });
+ }
+
+ function _boundMintAndStake(address _depositor, uint96 _amount, address _delegatee)
+ internal
+ returns (uint96 _boundedAmount, UniStaker.DepositIdentifier _depositId)
+ {
+ _boundedAmount = _boundMintAmount(_amount);
+ _mintGovToken(_depositor, _boundedAmount);
+ _depositId = _stake(_depositor, _boundedAmount, _delegatee);
+ }
+
+ function _boundMintAndStake(
+ address _depositor,
+ uint96 _amount,
+ address _delegatee,
+ address _beneficiary
+ ) internal returns (uint96 _boundedAmount, UniStaker.DepositIdentifier _depositId) {
+ _boundedAmount = _boundMintAmount(_amount);
+ _mintGovToken(_depositor, _boundedAmount);
+ _depositId = _stake(_depositor, _boundedAmount, _delegatee, _beneficiary);
+ }
+
+ // Scales first param and divides it by second
+ function _scaledDiv(uint256 _x, uint256 _y) public view returns (uint256) {
+ return (_x * SCALE_FACTOR) / _y;
+ }
+
+ function _sign(uint256 _privateKey, bytes32 _messageHash) internal pure returns (bytes memory) {
+ (uint8 _v, bytes32 _r, bytes32 _s) = vm.sign(_privateKey, _messageHash);
+ return abi.encodePacked(_r, _s, _v);
+ }
+
+ function _modifyMessage(bytes32 _message, uint256 _index) internal pure returns (bytes32) {
+ _index = bound(_index, 0, 31);
+ bytes memory _messageBytes = abi.encodePacked(_message);
+ // zero out the byte at the given index, or set it to 1 if it's already zero
+ if (_messageBytes[_index] == 0) _messageBytes[_index] = bytes1(uint8(1));
+ else _messageBytes[_index] = bytes1(uint8(0));
+ return bytes32(_messageBytes);
+ }
+
+ function _modifySignature(bytes memory _signature, uint256 _index)
+ internal
+ pure
+ returns (bytes memory)
+ {
+ _index = bound(_index, 0, _signature.length - 1);
+ // zero out the byte at the given index, or set it to 1 if it's already zero
+ if (_signature[_index] == 0) _signature[_index] = bytes1(uint8(1));
+ else _signature[_index] = bytes1(uint8(0));
+ return _signature;
+ }
+}
+
+contract Constructor is UniStakerTest {
+ function test_SetsTheRewardTokenStakeTokenAndRewardNotifier() public view {
+ assertEq(address(uniStaker.REWARD_TOKEN()), address(rewardToken));
+ assertEq(address(uniStaker.STAKE_TOKEN()), address(govToken));
+ assertEq(uniStaker.admin(), admin);
+ }
+
+ function testFuzz_SetsTheRewardTokenStakeTokenAndOwnerToArbitraryAddresses(
+ address _rewardToken,
+ address _stakeToken,
+ address _admin
+ ) public {
+ vm.assume(_admin != address(0));
+ UniStaker _uniStaker = new UniStaker(IERC20(_rewardToken), IERC20Delegates(_stakeToken), _admin);
+ assertEq(address(_uniStaker.REWARD_TOKEN()), address(_rewardToken));
+ assertEq(address(_uniStaker.STAKE_TOKEN()), address(_stakeToken));
+ assertEq(_uniStaker.admin(), _admin);
+ }
+}
+
+contract Stake is UniStakerTest {
+ function testFuzz_DeploysAndTransfersTokensToANewSurrogateWhenAnAccountStakes(
+ address _depositor,
+ uint96 _amount,
+ address _delegatee
+ ) public {
+ _amount = uint96(bound(_amount, 1, type(uint96).max));
+ _mintGovToken(_depositor, _amount);
+ _stake(_depositor, _amount, _delegatee);
+
+ DelegationSurrogate _surrogate = uniStaker.surrogates(_delegatee);
+
+ assertEq(govToken.balanceOf(address(_surrogate)), _amount);
+ assertEq(govToken.delegates(address(_surrogate)), _delegatee);
+ assertEq(govToken.balanceOf(_depositor), 0);
+ }
+
+ function testFuzz_EmitsAStakingDepositEventWhenStakingWithoutASpecifiedBeneficiary(
+ address _depositor,
+ uint96 _amount,
+ address _delegatee
+ ) public {
+ _amount = uint96(bound(_amount, 1, type(uint96).max));
+ _mintGovToken(_depositor, _amount);
+ UniStaker.DepositIdentifier depositId = uniStaker.exposed_useDepositId();
+
+ vm.assume(_delegatee != address(0));
+
+ vm.startPrank(_depositor);
+ govToken.approve(address(uniStaker), _amount);
+ vm.expectEmit();
+ emit UniStaker.StakeDeposited(
+ _depositor,
+ UniStaker.DepositIdentifier.wrap(UniStaker.DepositIdentifier.unwrap(depositId) + 1),
+ _amount,
+ _amount
+ );
+
+ uniStaker.stake(_amount, _delegatee);
+ vm.stopPrank();
+ }
+
+ function testFuzz_EmitsABeneficiaryAlteredEventWhenStakingWithoutASpecifiedBeneficiary(
+ address _depositor,
+ uint96 _amount,
+ address _delegatee
+ ) public {
+ _amount = uint96(bound(_amount, 1, type(uint96).max));
+ _mintGovToken(_depositor, _amount);
+ UniStaker.DepositIdentifier depositId = uniStaker.exposed_useDepositId();
+
+ vm.assume(_delegatee != address(0));
+
+ vm.startPrank(_depositor);
+ govToken.approve(address(uniStaker), _amount);
+ vm.expectEmit();
+ emit UniStaker.BeneficiaryAltered(
+ UniStaker.DepositIdentifier.wrap(UniStaker.DepositIdentifier.unwrap(depositId) + 1),
+ address(0),
+ _depositor
+ );
+
+ uniStaker.stake(_amount, _delegatee);
+ vm.stopPrank();
+ }
+
+ function testFuzz_EmitsADelegateeAlteredEventWhenStakingWithoutASpecifiedBeneficiary(
+ address _depositor,
+ uint96 _amount,
+ address _delegatee
+ ) public {
+ _amount = uint96(bound(_amount, 1, type(uint96).max));
+ _mintGovToken(_depositor, _amount);
+ UniStaker.DepositIdentifier depositId = uniStaker.exposed_useDepositId();
+
+ vm.assume(_delegatee != address(0));
+
+ vm.startPrank(_depositor);
+ govToken.approve(address(uniStaker), _amount);
+ vm.expectEmit();
+ emit UniStaker.DelegateeAltered(
+ UniStaker.DepositIdentifier.wrap(UniStaker.DepositIdentifier.unwrap(depositId) + 1),
+ address(0),
+ _delegatee
+ );
+
+ uniStaker.stake(_amount, _delegatee);
+ vm.stopPrank();
+ }
+
+ function testFuzz_EmitsAStakingDepositEventWhenStakingWithASpecifiedBeneficiary(
+ address _depositor,
+ uint96 _amount,
+ address _delegatee,
+ address _beneficiary
+ ) public {
+ _amount = uint96(bound(_amount, 1, type(uint96).max));
+ _mintGovToken(_depositor, _amount);
+ UniStaker.DepositIdentifier depositId = uniStaker.exposed_useDepositId();
+
+ vm.assume(_delegatee != address(0) && _beneficiary != address(0));
+
+ vm.startPrank(_depositor);
+ govToken.approve(address(uniStaker), _amount);
+ vm.expectEmit();
+ emit UniStaker.StakeDeposited(
+ _depositor,
+ UniStaker.DepositIdentifier.wrap(UniStaker.DepositIdentifier.unwrap(depositId) + 1),
+ _amount,
+ _amount
+ );
+
+ uniStaker.stake(_amount, _delegatee, _beneficiary);
+ vm.stopPrank();
+ }
+
+ function testFuzz_EmitsABeneficiaryAlteredEventWhenStakingWithASpecifiedBeneficiary(
+ address _depositor,
+ uint96 _amount,
+ address _delegatee,
+ address _beneficiary
+ ) public {
+ _amount = uint96(bound(_amount, 1, type(uint96).max));
+ _mintGovToken(_depositor, _amount);
+ UniStaker.DepositIdentifier depositId = uniStaker.exposed_useDepositId();
+
+ vm.assume(_delegatee != address(0) && _beneficiary != address(0));
+
+ vm.startPrank(_depositor);
+ govToken.approve(address(uniStaker), _amount);
+ vm.expectEmit();
+ emit UniStaker.BeneficiaryAltered(
+ UniStaker.DepositIdentifier.wrap(UniStaker.DepositIdentifier.unwrap(depositId) + 1),
+ address(0),
+ _beneficiary
+ );
+
+ uniStaker.stake(_amount, _delegatee, _beneficiary);
+ vm.stopPrank();
+ }
+
+ function testFuzz_EmitsADelegateeAlteredEventWhenStakingWithASpecifiedBeneficiary(
+ address _depositor,
+ uint96 _amount,
+ address _delegatee,
+ address _beneficiary
+ ) public {
+ _amount = uint96(bound(_amount, 1, type(uint96).max));
+ _mintGovToken(_depositor, _amount);
+ UniStaker.DepositIdentifier depositId = uniStaker.exposed_useDepositId();
+
+ vm.assume(_delegatee != address(0) && _beneficiary != address(0));
+
+ vm.startPrank(_depositor);
+ govToken.approve(address(uniStaker), _amount);
+ vm.expectEmit();
+ emit UniStaker.DelegateeAltered(
+ UniStaker.DepositIdentifier.wrap(UniStaker.DepositIdentifier.unwrap(depositId) + 1),
+ address(0),
+ _delegatee
+ );
+
+ uniStaker.stake(_amount, _delegatee, _beneficiary);
+ vm.stopPrank();
+ }
+
+ function testFuzz_TransfersToAnExistingSurrogateWhenStakedToTheSameDelegatee(
+ address _depositor1,
+ uint96 _amount1,
+ address _depositor2,
+ uint96 _amount2,
+ address _delegatee
+ ) public {
+ _amount1 = _boundMintAmount(_amount1);
+ _amount2 = _boundMintAmount(_amount2);
+ _mintGovToken(_depositor1, _amount1);
+ _mintGovToken(_depositor2, _amount2);
+
+ // Perform first stake with this delegatee
+ _stake(_depositor1, _amount1, _delegatee);
+ // Remember the surrogate which was deployed for this delegatee
+ DelegationSurrogate _surrogate = uniStaker.surrogates(_delegatee);
+
+ // Perform the second stake with this delegatee
+ _stake(_depositor2, _amount2, _delegatee);
+
+ // Ensure surrogate for this delegatee hasn't changed and has summed stake balance
+ assertEq(address(uniStaker.surrogates(_delegatee)), address(_surrogate));
+ assertEq(govToken.delegates(address(_surrogate)), _delegatee);
+ assertEq(govToken.balanceOf(address(_surrogate)), _amount1 + _amount2);
+ assertEq(govToken.balanceOf(_depositor1), 0);
+ assertEq(govToken.balanceOf(_depositor2), 0);
+ }
+
+ function testFuzz_DeploysAndTransfersTokenToTwoSurrogatesWhenAccountsStakesToDifferentDelegatees(
+ address _depositor1,
+ uint96 _amount1,
+ address _depositor2,
+ uint96 _amount2,
+ address _delegatee1,
+ address _delegatee2
+ ) public {
+ vm.assume(_delegatee1 != _delegatee2);
+ _amount1 = _boundMintAmount(_amount1);
+ _amount2 = _boundMintAmount(_amount2);
+ _mintGovToken(_depositor1, _amount1);
+ _mintGovToken(_depositor2, _amount2);
+
+ // Perform first stake with first delegatee
+ _stake(_depositor1, _amount1, _delegatee1);
+ // Remember the surrogate which was deployed for first delegatee
+ DelegationSurrogate _surrogate1 = uniStaker.surrogates(_delegatee1);
+
+ // Perform second stake with second delegatee
+ _stake(_depositor2, _amount2, _delegatee2);
+ // Remember the surrogate which was deployed for first delegatee
+ DelegationSurrogate _surrogate2 = uniStaker.surrogates(_delegatee2);
+
+ // Ensure surrogates are different with discreet delegation & balances
+ assertTrue(_surrogate1 != _surrogate2);
+ assertEq(govToken.delegates(address(_surrogate1)), _delegatee1);
+ assertEq(govToken.balanceOf(address(_surrogate1)), _amount1);
+ assertEq(govToken.delegates(address(_surrogate2)), _delegatee2);
+ assertEq(govToken.balanceOf(address(_surrogate2)), _amount2);
+ assertEq(govToken.balanceOf(_depositor1), 0);
+ assertEq(govToken.balanceOf(_depositor2), 0);
+ }
+
+ function testFuzz_UpdatesTheTotalStakedWhenAnAccountStakes(
+ address _depositor,
+ uint96 _amount,
+ address _delegatee
+ ) public {
+ _amount = _boundMintAmount(_amount);
+ _mintGovToken(_depositor, _amount);
+
+ _stake(_depositor, _amount, _delegatee);
+
+ assertEq(uniStaker.totalStaked(), _amount);
+ }
+
+ function testFuzz_UpdatesTheTotalStakedWhenTwoAccountsStake(
+ address _depositor1,
+ uint96 _amount1,
+ address _depositor2,
+ uint96 _amount2,
+ address _delegatee1,
+ address _delegatee2
+ ) public {
+ _amount1 = _boundMintAmount(_amount1);
+ _amount2 = _boundMintAmount(_amount2);
+ _mintGovToken(_depositor1, _amount1);
+ _mintGovToken(_depositor2, _amount2);
+
+ _stake(_depositor1, _amount1, _delegatee1);
+ assertEq(uniStaker.totalStaked(), _amount1);
+
+ _stake(_depositor2, _amount2, _delegatee2);
+ assertEq(uniStaker.totalStaked(), _amount1 + _amount2);
+ }
+
+ function testFuzz_UpdatesAnAccountsTotalStakedAccounting(
+ address _depositor,
+ uint96 _amount1,
+ uint96 _amount2,
+ address _delegatee1,
+ address _delegatee2
+ ) public {
+ _amount1 = _boundMintAmount(_amount1);
+ _amount2 = _boundMintAmount(_amount2);
+ _mintGovToken(_depositor, _amount1 + _amount2);
+
+ // First stake + check total
+ _stake(_depositor, _amount1, _delegatee1);
+ assertEq(uniStaker.depositorTotalStaked(_depositor), _amount1);
+
+ // Second stake + check total
+ _stake(_depositor, _amount2, _delegatee2);
+ assertEq(uniStaker.depositorTotalStaked(_depositor), _amount1 + _amount2);
+ }
+
+ function testFuzz_UpdatesDifferentAccountsTotalStakedAccountingIndependently(
+ address _depositor1,
+ uint96 _amount1,
+ address _depositor2,
+ uint96 _amount2,
+ address _delegatee1,
+ address _delegatee2
+ ) public {
+ vm.assume(_depositor1 != _depositor2);
+ _amount1 = _boundMintAmount(_amount1);
+ _amount2 = _boundMintAmount(_amount2);
+ _mintGovToken(_depositor1, _amount1);
+ _mintGovToken(_depositor2, _amount2);
+
+ _stake(_depositor1, _amount1, _delegatee1);
+ assertEq(uniStaker.depositorTotalStaked(_depositor1), _amount1);
+
+ _stake(_depositor2, _amount2, _delegatee2);
+ assertEq(uniStaker.depositorTotalStaked(_depositor2), _amount2);
+ }
+
+ function testFuzz_TracksTheBalanceForASpecificDeposit(
+ address _depositor,
+ uint96 _amount,
+ address _delegatee
+ ) public {
+ _amount = _boundMintAmount(_amount);
+ _mintGovToken(_depositor, _amount);
+
+ UniStaker.DepositIdentifier _depositId = _stake(_depositor, _amount, _delegatee);
+ UniStaker.Deposit memory _deposit = _fetchDeposit(_depositId);
+ assertEq(_deposit.balance, _amount);
+ assertEq(_deposit.owner, _depositor);
+ assertEq(_deposit.delegatee, _delegatee);
+ }
+
+ function testFuzz_TracksTheBalanceForDifferentDepositsFromTheSameAccountIndependently(
+ address _depositor,
+ uint96 _amount1,
+ uint96 _amount2,
+ address _delegatee1,
+ address _delegatee2
+ ) public {
+ _amount1 = _boundMintAmount(_amount1);
+ _amount2 = _boundMintAmount(_amount2);
+ _mintGovToken(_depositor, _amount1 + _amount2);
+
+ // Perform both deposits and track their identifiers separately
+ UniStaker.DepositIdentifier _depositId1 = _stake(_depositor, _amount1, _delegatee1);
+ UniStaker.DepositIdentifier _depositId2 = _stake(_depositor, _amount2, _delegatee2);
+ UniStaker.Deposit memory _deposit1 = _fetchDeposit(_depositId1);
+ UniStaker.Deposit memory _deposit2 = _fetchDeposit(_depositId2);
+
+ // Check that the deposits have been recorded independently
+ assertEq(_deposit1.balance, _amount1);
+ assertEq(_deposit1.owner, _depositor);
+ assertEq(_deposit1.delegatee, _delegatee1);
+ assertEq(_deposit2.balance, _amount2);
+ assertEq(_deposit2.owner, _depositor);
+ assertEq(_deposit2.delegatee, _delegatee2);
+ }
+
+ function testFuzz_TracksTheBalanceForDepositsFromDifferentAccountsIndependently(
+ address _depositor1,
+ address _depositor2,
+ uint96 _amount1,
+ uint96 _amount2,
+ address _delegatee1,
+ address _delegatee2
+ ) public {
+ _amount1 = _boundMintAmount(_amount1);
+ _amount2 = _boundMintAmount(_amount2);
+ _mintGovToken(_depositor1, _amount1);
+ _mintGovToken(_depositor2, _amount2);
+
+ // Perform both deposits and track their identifiers separately
+ UniStaker.DepositIdentifier _depositId1 = _stake(_depositor1, _amount1, _delegatee1);
+ UniStaker.DepositIdentifier _depositId2 = _stake(_depositor2, _amount2, _delegatee2);
+ UniStaker.Deposit memory _deposit1 = _fetchDeposit(_depositId1);
+ UniStaker.Deposit memory _deposit2 = _fetchDeposit(_depositId2);
+
+ // Check that the deposits have been recorded independently
+ assertEq(_deposit1.balance, _amount1);
+ assertEq(_deposit1.owner, _depositor1);
+ assertEq(_deposit1.delegatee, _delegatee1);
+ assertEq(_deposit2.balance, _amount2);
+ assertEq(_deposit2.owner, _depositor2);
+ assertEq(_deposit2.delegatee, _delegatee2);
+ }
+
+ function testFuzz_AssignsEarningPowerToDepositorIfNoBeneficiaryIsSpecified(
+ address _depositor,
+ uint96 _amount,
+ address _delegatee
+ ) public {
+ _amount = _boundMintAmount(_amount);
+ _mintGovToken(_depositor, _amount);
+
+ UniStaker.DepositIdentifier _depositId = _stake(_depositor, _amount, _delegatee);
+ UniStaker.Deposit memory _deposit = _fetchDeposit(_depositId);
+
+ assertEq(uniStaker.earningPower(_depositor), _amount);
+ assertEq(_deposit.beneficiary, _depositor);
+ }
+
+ function testFuzz_AssignsEarningPowerToTheBeneficiaryProvided(
+ address _depositor,
+ uint96 _amount,
+ address _delegatee,
+ address _beneficiary
+ ) public {
+ _amount = _boundMintAmount(_amount);
+ _mintGovToken(_depositor, _amount);
+
+ UniStaker.DepositIdentifier _depositId = _stake(_depositor, _amount, _delegatee, _beneficiary);
+ UniStaker.Deposit memory _deposit = _fetchDeposit(_depositId);
+
+ assertEq(uniStaker.earningPower(_beneficiary), _amount);
+ assertEq(_deposit.beneficiary, _beneficiary);
+ }
+
+ function testFuzz_AssignsEarningPowerToDifferentBeneficiariesForDifferentDepositsFromTheSameDepositor(
+ address _depositor,
+ uint96 _amount1,
+ uint96 _amount2,
+ address _delegatee,
+ address _beneficiary1,
+ address _beneficiary2
+ ) public {
+ vm.assume(_beneficiary1 != _beneficiary2);
+ _amount1 = _boundMintAmount(_amount1);
+ _amount2 = _boundMintAmount(_amount2);
+ _mintGovToken(_depositor, _amount1 + _amount2);
+
+ // Perform both deposits and track their identifiers separately
+ UniStaker.DepositIdentifier _depositId1 =
+ _stake(_depositor, _amount1, _delegatee, _beneficiary1);
+ UniStaker.DepositIdentifier _depositId2 =
+ _stake(_depositor, _amount2, _delegatee, _beneficiary2);
+ UniStaker.Deposit memory _deposit1 = _fetchDeposit(_depositId1);
+ UniStaker.Deposit memory _deposit2 = _fetchDeposit(_depositId2);
+
+ // Check that the earning power has been recorded independently
+ assertEq(_deposit1.beneficiary, _beneficiary1);
+ assertEq(uniStaker.earningPower(_beneficiary1), _amount1);
+ assertEq(_deposit2.beneficiary, _beneficiary2);
+ assertEq(uniStaker.earningPower(_beneficiary2), _amount2);
+ }
+
+ function testFuzz_AssignsEarningPowerToTheSameBeneficiarySpecifiedByTwoDifferentDepositors(
+ address _depositor1,
+ address _depositor2,
+ uint96 _amount1,
+ uint96 _amount2,
+ address _delegatee,
+ address _beneficiary
+ ) public {
+ _amount1 = _boundMintAmount(_amount1);
+ _amount2 = _boundMintAmount(_amount2);
+ _mintGovToken(_depositor1, _amount1);
+ _mintGovToken(_depositor2, _amount2);
+
+ // Perform both deposits and track their identifiers separately
+ UniStaker.DepositIdentifier _depositId1 =
+ _stake(_depositor1, _amount1, _delegatee, _beneficiary);
+ UniStaker.DepositIdentifier _depositId2 =
+ _stake(_depositor2, _amount2, _delegatee, _beneficiary);
+ UniStaker.Deposit memory _deposit1 = _fetchDeposit(_depositId1);
+ UniStaker.Deposit memory _deposit2 = _fetchDeposit(_depositId2);
+
+ assertEq(_deposit1.beneficiary, _beneficiary);
+ assertEq(_deposit2.beneficiary, _beneficiary);
+ assertEq(uniStaker.earningPower(_beneficiary), _amount1 + _amount2);
+ }
+
+ mapping(UniStaker.DepositIdentifier depositId => bool isUsed) isIdUsed;
+
+ function test_NeverReusesADepositIdentifier() public {
+ address _depositor = address(0xdeadbeef);
+ uint96 _amount = 116;
+ address _delegatee = address(0xaceface);
+
+ UniStaker.DepositIdentifier _depositId;
+
+ vm.pauseGasMetering();
+
+ // Repeat the deposit over and over ensuring a new DepositIdentifier is assigned each time.
+ for (uint256 _i; _i < 100; _i++) {
+ // Perform the stake and save the deposit identifier
+ _mintGovToken(_depositor, _amount);
+ _depositId = _stake(_depositor, _amount, _delegatee);
+
+ // Ensure the identifier hasn't yet been used
+ assertFalse(isIdUsed[_depositId]);
+ // Record the fact this deposit Id has been used
+ isIdUsed[_depositId] = true;
+ }
+
+ // Now make a bunch more deposits with different depositors and parameters, continuing to check
+ // that the DepositIdentifier is never reused.
+ for (uint256 _i; _i < 100; _i++) {
+ // Perform the stake and save the deposit identifier
+ _amount = uint96(_bound(_amount, 0, 100_000_000_000e18));
+ _mintGovToken(_depositor, _amount);
+ _depositId = _stake(_depositor, _amount, _delegatee);
+
+ // Ensure the identifier hasn't yet been used
+ assertFalse(isIdUsed[_depositId]);
+ // Record the fact this deposit Id has been used
+ isIdUsed[_depositId] = true;
+
+ // Assign new inputs for the next deposit by hashing the last inputs
+ _depositor = address(uint160(uint256(keccak256(abi.encode(_depositor)))));
+ _amount = uint96(uint256(keccak256(abi.encode(_amount))));
+ _delegatee = address(uint160(uint256(keccak256(abi.encode(_delegatee)))));
+ }
+ }
+
+ function testFuzz_RevertIf_DelegateeIsTheZeroAddress(address _depositor, uint96 _amount) public {
+ _amount = _boundMintAmount(_amount);
+ _mintGovToken(_depositor, _amount);
+ govToken.approve(address(uniStaker), _amount);
+
+ vm.prank(_depositor);
+ vm.expectRevert(UniStaker.UniStaker__InvalidAddress.selector);
+ uniStaker.stake(_amount, address(0));
+ }
+
+ function testFuzz_RevertIf_BeneficiaryIsTheZeroAddress(
+ address _depositor,
+ uint96 _amount,
+ address _delegatee
+ ) public {
+ vm.assume(_delegatee != address(0));
+
+ _amount = _boundMintAmount(_amount);
+ _mintGovToken(_depositor, _amount);
+ govToken.approve(address(uniStaker), _amount);
+
+ vm.prank(_depositor);
+ vm.expectRevert(UniStaker.UniStaker__InvalidAddress.selector);
+ uniStaker.stake(_amount, _delegatee, address(0));
+ }
+}
+
+contract PermitAndStake is UniStakerTest {
+ using stdStorage for StdStorage;
+
+ function testFuzz_PerformsTheApprovalByCallingPermitThenPerformsStake(
+ uint256 _depositorPrivateKey,
+ uint96 _depositAmount,
+ address _delegatee,
+ address _beneficiary,
+ uint256 _deadline,
+ uint256 _currentNonce
+ ) public {
+ vm.assume(_delegatee != address(0) && _beneficiary != address(0));
+ _deadline = bound(_deadline, block.timestamp, type(uint256).max);
+ _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18);
+ address _depositor = vm.addr(_depositorPrivateKey);
+ _depositAmount = _boundMintAmount(_depositAmount);
+ _mintGovToken(_depositor, _depositAmount);
+
+ stdstore.target(address(govToken)).sig("nonces(address)").with_key(_depositor).checked_write(
+ _currentNonce
+ );
+
+ bytes32 _message = keccak256(
+ abi.encode(
+ PERMIT_TYPEHASH,
+ _depositor,
+ address(uniStaker),
+ _depositAmount,
+ govToken.nonces(_depositor),
+ _deadline
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", govToken.DOMAIN_SEPARATOR(), _message));
+ (uint8 _v, bytes32 _r, bytes32 _s) = vm.sign(_depositorPrivateKey, _messageHash);
+
+ vm.prank(_depositor);
+ UniStaker.DepositIdentifier _depositId =
+ uniStaker.permitAndStake(_depositAmount, _delegatee, _beneficiary, _deadline, _v, _r, _s);
+ UniStaker.Deposit memory _deposit = _fetchDeposit(_depositId);
+
+ assertEq(_deposit.balance, _depositAmount);
+ assertEq(_deposit.owner, _depositor);
+ assertEq(_deposit.delegatee, _delegatee);
+ assertEq(_deposit.beneficiary, _beneficiary);
+ }
+
+ function testFuzz_SuccessfullyStakeWhenApprovalExistsAndPermitSignatureIsInvalid(
+ uint256 _depositorPrivateKey,
+ uint96 _depositAmount,
+ uint256 _approvalAmount,
+ address _delegatee,
+ address _beneficiary
+ ) public {
+ vm.assume(_delegatee != address(0) && _beneficiary != address(0));
+ _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18);
+ address _depositor = vm.addr(_depositorPrivateKey);
+ _depositAmount = _boundMintAmount(_depositAmount);
+ _approvalAmount = bound(_approvalAmount, _depositAmount, type(uint256).max);
+ _mintGovToken(_depositor, _depositAmount);
+ vm.startPrank(_depositor);
+ govToken.approve(address(uniStaker), _approvalAmount);
+ vm.stopPrank();
+
+ bytes32 _message = keccak256(
+ abi.encode(
+ PERMIT_TYPEHASH,
+ _depositor,
+ address(uniStaker),
+ _depositAmount,
+ 1, // intentionally wrong nonce
+ block.timestamp
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", govToken.DOMAIN_SEPARATOR(), _message));
+ (uint8 _v, bytes32 _r, bytes32 _s) = vm.sign(_depositorPrivateKey, _messageHash);
+
+ vm.prank(_depositor);
+ uniStaker.permitAndStake(_depositAmount, _delegatee, _beneficiary, block.timestamp, _v, _r, _s);
+ assertEq(uniStaker.depositorTotalStaked(_depositor), _depositAmount);
+ }
+
+ function testFuzz_RevertIf_ThePermitSignatureIsInvalidAndTheApprovalIsInsufficient(
+ address _notDepositor,
+ uint256 _depositorPrivateKey,
+ uint96 _depositAmount,
+ uint256 _approvalAmount,
+ address _delegatee,
+ address _beneficiary,
+ uint256 _deadline
+ ) public {
+ vm.assume(_delegatee != address(0) && _beneficiary != address(0));
+ _deadline = bound(_deadline, block.timestamp, type(uint256).max);
+ _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18);
+ address _depositor = vm.addr(_depositorPrivateKey);
+ vm.assume(_notDepositor != _depositor);
+ _depositAmount = _boundMintAmount(_depositAmount) + 1;
+ _approvalAmount = bound(_approvalAmount, 0, _depositAmount - 1);
+ _mintGovToken(_depositor, _depositAmount);
+ vm.startPrank(_depositor);
+ govToken.approve(address(uniStaker), _approvalAmount);
+ vm.stopPrank();
+
+ bytes32 _message = keccak256(
+ abi.encode(
+ PERMIT_TYPEHASH,
+ _notDepositor,
+ address(uniStaker),
+ _depositAmount,
+ govToken.nonces(_depositor),
+ _deadline
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", govToken.DOMAIN_SEPARATOR(), _message));
+ (uint8 _v, bytes32 _r, bytes32 _s) = vm.sign(_depositorPrivateKey, _messageHash);
+
+ vm.prank(_depositor);
+ vm.expectRevert(
+ abi.encodeWithSelector(
+ IERC20Errors.ERC20InsufficientAllowance.selector,
+ address(uniStaker),
+ _approvalAmount,
+ _depositAmount
+ )
+ );
+ uniStaker.permitAndStake(_depositAmount, _delegatee, _beneficiary, _deadline, _v, _r, _s);
+ }
+}
+
+contract StakeOnBehalf is UniStakerTest {
+ using stdStorage for StdStorage;
+
+ function testFuzz_StakesOnBehalfOfAnotherAccount(
+ uint256 _depositorPrivateKey,
+ address _sender,
+ uint96 _depositAmount,
+ address _delegatee,
+ address _beneficiary,
+ uint256 _currentNonce,
+ uint256 _deadline
+ ) public {
+ vm.assume(_delegatee != address(0) && _beneficiary != address(0) && _sender != address(0));
+ _deadline = bound(_deadline, block.timestamp, type(uint256).max);
+ _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18);
+ address _depositor = vm.addr(_depositorPrivateKey);
+ _depositAmount = _boundMintAmount(_depositAmount);
+ _mintGovToken(_depositor, _depositAmount);
+
+ stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_depositor).checked_write(
+ _currentNonce
+ );
+
+ vm.prank(_depositor);
+ govToken.approve(address(uniStaker), _depositAmount);
+
+ bytes32 _message = keccak256(
+ abi.encode(
+ uniStaker.STAKE_TYPEHASH(),
+ _depositAmount,
+ _delegatee,
+ _beneficiary,
+ _depositor,
+ uniStaker.nonces(_depositor),
+ _deadline
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message));
+ bytes memory _signature = _sign(_depositorPrivateKey, _messageHash);
+
+ vm.prank(_sender);
+ UniStaker.DepositIdentifier _depositId = uniStaker.stakeOnBehalf(
+ _depositAmount, _delegatee, _beneficiary, _depositor, _deadline, _signature
+ );
+
+ UniStaker.Deposit memory _deposit = _fetchDeposit(_depositId);
+
+ assertEq(_deposit.balance, _depositAmount);
+ assertEq(_deposit.owner, _depositor);
+ assertEq(_deposit.delegatee, _delegatee);
+ assertEq(_deposit.beneficiary, _beneficiary);
+ }
+
+ function testFuzz_RevertIf_WrongNonceIsUsed(
+ uint256 _depositorPrivateKey,
+ address _sender,
+ uint96 _depositAmount,
+ address _delegatee,
+ address _beneficiary,
+ uint256 _currentNonce,
+ uint256 _suppliedNonce,
+ uint256 _deadline
+ ) public {
+ vm.assume(_currentNonce != _suppliedNonce);
+ vm.assume(_delegatee != address(0) && _beneficiary != address(0) && _sender != address(0));
+ _deadline = bound(_deadline, block.timestamp, type(uint256).max);
+ _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18);
+ address _depositor = vm.addr(_depositorPrivateKey);
+ _depositAmount = _boundMintAmount(_depositAmount);
+ _mintGovToken(_depositor, _depositAmount);
+
+ stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_depositor).checked_write(
+ _currentNonce
+ );
+
+ vm.prank(_depositor);
+ govToken.approve(address(uniStaker), _depositAmount);
+
+ bytes32 _message = keccak256(
+ abi.encode(
+ uniStaker.STAKE_TYPEHASH(),
+ _depositAmount,
+ _delegatee,
+ _beneficiary,
+ _depositor,
+ _suppliedNonce,
+ _deadline
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message));
+ bytes memory _signature = _sign(_depositorPrivateKey, _messageHash);
+
+ vm.expectRevert(UniStaker.UniStaker__InvalidSignature.selector);
+ vm.prank(_sender);
+ uniStaker.stakeOnBehalf(
+ _depositAmount, _delegatee, _beneficiary, _depositor, _deadline, _signature
+ );
+ }
+
+ function testFuzz_RevertIf_DeadlineExpired(
+ uint256 _depositorPrivateKey,
+ address _sender,
+ uint96 _depositAmount,
+ address _delegatee,
+ address _beneficiary,
+ uint256 _currentNonce,
+ uint256 _deadline
+ ) public {
+ vm.assume(_delegatee != address(0) && _beneficiary != address(0) && _sender != address(0));
+ _deadline = bound(_deadline, 0, block.timestamp - 1);
+ _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18);
+ address _depositor = vm.addr(_depositorPrivateKey);
+ _depositAmount = _boundMintAmount(_depositAmount);
+ _mintGovToken(_depositor, _depositAmount);
+
+ stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_depositor).checked_write(
+ _currentNonce
+ );
+
+ vm.prank(_depositor);
+ govToken.approve(address(uniStaker), _depositAmount);
+
+ bytes32 _message = keccak256(
+ abi.encode(
+ uniStaker.STAKE_TYPEHASH(),
+ _depositAmount,
+ _delegatee,
+ _beneficiary,
+ _depositor,
+ _currentNonce,
+ _deadline
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message));
+ bytes memory _signature = _sign(_depositorPrivateKey, _messageHash);
+
+ vm.expectRevert(UniStaker.UniStaker__ExpiredDeadline.selector);
+ vm.prank(_sender);
+ uniStaker.stakeOnBehalf(
+ _depositAmount, _delegatee, _beneficiary, _depositor, _deadline, _signature
+ );
+ }
+
+ function testFuzz_RevertIf_InvalidSignatureIsPassed(
+ uint256 _depositorPrivateKey,
+ address _sender,
+ uint96 _depositAmount,
+ address _delegatee,
+ address _beneficiary,
+ uint256 _currentNonce,
+ uint256 _randomSeed,
+ uint256 _deadline
+ ) public {
+ vm.assume(_delegatee != address(0) && _beneficiary != address(0));
+ _deadline = bound(_deadline, block.timestamp, type(uint256).max);
+ _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18);
+ address _depositor = vm.addr(_depositorPrivateKey);
+ stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_depositor).checked_write(
+ _currentNonce
+ );
+ _depositAmount = _boundMintAmount(_depositAmount);
+ _mintGovToken(_depositor, _depositAmount);
+
+ vm.prank(_depositor);
+ govToken.approve(address(uniStaker), _depositAmount);
+
+ bytes32 _message = keccak256(
+ abi.encode(
+ uniStaker.STAKE_TYPEHASH(),
+ _depositAmount,
+ _delegatee,
+ _beneficiary,
+ _depositor,
+ uniStaker.nonces(_depositor),
+ _deadline
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message));
+
+ // Here we use `_randomSeed` as an arbitrary source of randomness to replace a legit parameter
+ // with an attack-like one.
+ if (_randomSeed % 6 == 0) {
+ _depositAmount = uint96(uint256(keccak256(abi.encode(_depositAmount))));
+ } else if (_randomSeed % 6 == 1) {
+ _delegatee = address(uint160(uint256(keccak256(abi.encode(_delegatee)))));
+ } else if (_randomSeed % 6 == 2) {
+ _depositor = address(uint160(uint256(keccak256(abi.encode(_depositor)))));
+ } else if (_randomSeed % 6 == 3) {
+ _messageHash = _modifyMessage(_messageHash, uint256(keccak256(abi.encode(_randomSeed))));
+ } else if (_randomSeed % 6 == 4) {
+ _deadline = uint256(keccak256(abi.encode(_deadline)));
+ }
+ bytes memory _signature = _sign(_depositorPrivateKey, _messageHash);
+ if (_randomSeed % 6 == 5) _signature = _modifySignature(_signature, _randomSeed);
+
+ vm.prank(_sender);
+ vm.expectRevert(UniStaker.UniStaker__InvalidSignature.selector);
+ uniStaker.stakeOnBehalf(
+ _depositAmount, _delegatee, _beneficiary, _depositor, _deadline, _signature
+ );
+ }
+}
+
+contract StakeMore is UniStakerTest {
+ function testFuzz_TransfersStakeToTheExistingSurrogate(
+ address _depositor,
+ uint96 _depositAmount,
+ uint96 _addAmount,
+ address _delegatee,
+ address _beneficiary
+ ) public {
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary);
+ UniStaker.Deposit memory _deposit = _fetchDeposit(_depositId);
+ DelegationSurrogate _surrogate = uniStaker.surrogates(_deposit.delegatee);
+
+ _addAmount = _boundToRealisticStake(_addAmount);
+ _mintGovToken(_depositor, _addAmount);
+
+ vm.startPrank(_depositor);
+ govToken.approve(address(uniStaker), _addAmount);
+ uniStaker.stakeMore(_depositId, _addAmount);
+ vm.stopPrank();
+
+ assertEq(govToken.balanceOf(address(_surrogate)), _depositAmount + _addAmount);
+ }
+
+ function testFuzz_AddsToExistingBeneficiaryEarningPower(
+ address _depositor,
+ uint96 _depositAmount,
+ uint96 _addAmount,
+ address _delegatee,
+ address _beneficiary
+ ) public {
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary);
+
+ _addAmount = _boundToRealisticStake(_addAmount);
+ _mintGovToken(_depositor, _addAmount);
+
+ vm.startPrank(_depositor);
+ govToken.approve(address(uniStaker), _addAmount);
+ uniStaker.stakeMore(_depositId, _addAmount);
+ vm.stopPrank();
+
+ assertEq(uniStaker.earningPower(_beneficiary), _depositAmount + _addAmount);
+ }
+
+ function testFuzz_AddsToTheTotalStaked(
+ address _depositor,
+ uint96 _depositAmount,
+ uint96 _addAmount,
+ address _delegatee,
+ address _beneficiary
+ ) public {
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary);
+
+ _addAmount = _boundToRealisticStake(_addAmount);
+ _mintGovToken(_depositor, _addAmount);
+
+ vm.startPrank(_depositor);
+ govToken.approve(address(uniStaker), _addAmount);
+ uniStaker.stakeMore(_depositId, _addAmount);
+ vm.stopPrank();
+
+ assertEq(uniStaker.totalStaked(), _depositAmount + _addAmount);
+ }
+
+ function testFuzz_AddsToDepositorsTotalStaked(
+ address _depositor,
+ uint96 _depositAmount,
+ uint96 _addAmount,
+ address _delegatee,
+ address _beneficiary
+ ) public {
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary);
+
+ _addAmount = _boundToRealisticStake(_addAmount);
+ _mintGovToken(_depositor, _addAmount);
+
+ vm.startPrank(_depositor);
+ govToken.approve(address(uniStaker), _addAmount);
+ uniStaker.stakeMore(_depositId, _addAmount);
+ vm.stopPrank();
+
+ assertEq(uniStaker.depositorTotalStaked(_depositor), _depositAmount + _addAmount);
+ }
+
+ function testFuzz_AddsToTheDepositBalance(
+ address _depositor,
+ uint96 _depositAmount,
+ uint96 _addAmount,
+ address _delegatee,
+ address _beneficiary
+ ) public {
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary);
+
+ _addAmount = _boundToRealisticStake(_addAmount);
+ _mintGovToken(_depositor, _addAmount);
+
+ vm.startPrank(_depositor);
+ govToken.approve(address(uniStaker), _addAmount);
+ uniStaker.stakeMore(_depositId, _addAmount);
+ vm.stopPrank();
+
+ UniStaker.Deposit memory _deposit = _fetchDeposit(_depositId);
+
+ assertEq(_deposit.balance, _depositAmount + _addAmount);
+ }
+
+ function testFuzz_EmitsAnEventWhenStakingMore(
+ address _depositor,
+ uint96 _depositAmount,
+ uint96 _addAmount,
+ address _delegatee,
+ address _beneficiary
+ ) public {
+ uint96 _totalAdditionalStake;
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary);
+ // Second stake
+ _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary);
+
+ _addAmount = _boundToRealisticStake(_addAmount);
+ _totalAdditionalStake = _addAmount * 2;
+ _mintGovToken(_depositor, _totalAdditionalStake);
+
+ vm.startPrank(_depositor);
+ govToken.approve(address(uniStaker), _addAmount * 2);
+
+ uniStaker.stakeMore(_depositId, _addAmount);
+
+ vm.expectEmit();
+ emit UniStaker.StakeDeposited(
+ _depositor, _depositId, _addAmount, _depositAmount + _totalAdditionalStake
+ );
+
+ uniStaker.stakeMore(_depositId, _addAmount);
+ vm.stopPrank();
+ }
+
+ function testFuzz_RevertIf_TheCallerIsNotTheDepositor(
+ address _depositor,
+ address _notDepositor,
+ uint96 _depositAmount,
+ uint96 _addAmount,
+ address _delegatee,
+ address _beneficiary
+ ) public {
+ vm.assume(_notDepositor != _depositor);
+
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary);
+
+ _addAmount = _boundToRealisticStake(_addAmount);
+ _mintGovToken(_depositor, _addAmount);
+
+ vm.prank(_depositor);
+ govToken.approve(address(uniStaker), _addAmount);
+
+ vm.prank(_notDepositor);
+ vm.expectRevert(
+ abi.encodeWithSelector(
+ UniStaker.UniStaker__Unauthorized.selector, bytes32("not owner"), _notDepositor
+ )
+ );
+ uniStaker.stakeMore(_depositId, _addAmount);
+ }
+
+ function testFuzz_RevertIf_TheDepositIdentifierIsInvalid(
+ address _depositor,
+ UniStaker.DepositIdentifier _depositId,
+ uint96 _addAmount
+ ) public {
+ vm.assume(_depositor != address(0));
+ _addAmount = _boundToRealisticStake(_addAmount);
+
+ // Since no deposits have been made yet, all DepositIdentifiers are invalid, and any call to
+ // add stake to one should revert. We rely on the default owner of any uninitialized deposit
+ // being address zero, which means the address attempting to alter it won't be able to.
+ vm.prank(_depositor);
+ vm.expectRevert(
+ abi.encodeWithSelector(
+ UniStaker.UniStaker__Unauthorized.selector, bytes32("not owner"), _depositor
+ )
+ );
+ uniStaker.stakeMore(_depositId, _addAmount);
+ }
+}
+
+contract PermitAndStakeMore is UniStakerTest {
+ using stdStorage for StdStorage;
+
+ function testFuzz_PerformsTheApprovalByCallingPermitThenPerformsStakeMore(
+ uint256 _depositorPrivateKey,
+ uint96 _initialDepositAmount,
+ uint96 _stakeMoreAmount,
+ address _delegatee,
+ address _beneficiary,
+ uint256 _currentNonce,
+ uint256 _deadline
+ ) public {
+ vm.assume(_delegatee != address(0) && _beneficiary != address(0));
+ _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18);
+ address _depositor = vm.addr(_depositorPrivateKey);
+ _deadline = bound(_deadline, block.timestamp, type(uint256).max);
+
+ UniStaker.DepositIdentifier _depositId;
+ (_initialDepositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _initialDepositAmount, _delegatee, _beneficiary);
+
+ _stakeMoreAmount = _boundToRealisticStake(_stakeMoreAmount);
+ _mintGovToken(_depositor, _stakeMoreAmount);
+
+ stdstore.target(address(govToken)).sig("nonces(address)").with_key(_depositor).checked_write(
+ _currentNonce
+ );
+
+ // Separate scope to avoid stack to deep errors
+ {
+ bytes32 _message = keccak256(
+ abi.encode(
+ PERMIT_TYPEHASH,
+ _depositor,
+ address(uniStaker),
+ _stakeMoreAmount,
+ govToken.nonces(_depositor),
+ _deadline
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", govToken.DOMAIN_SEPARATOR(), _message));
+ (uint8 _v, bytes32 _r, bytes32 _s) = vm.sign(_depositorPrivateKey, _messageHash);
+
+ vm.prank(_depositor);
+ uniStaker.permitAndStakeMore(_depositId, _stakeMoreAmount, _deadline, _v, _r, _s);
+ }
+
+ UniStaker.Deposit memory _deposit = _fetchDeposit(_depositId);
+
+ assertEq(_deposit.balance, _initialDepositAmount + _stakeMoreAmount);
+ assertEq(_deposit.owner, _depositor);
+ assertEq(_deposit.delegatee, _delegatee);
+ assertEq(_deposit.beneficiary, _beneficiary);
+ }
+
+ function testFuzz_SuccessfullyStakeMoreWhenApprovalExistsAndPermitSignatureIsInvalid(
+ uint96 _initialDepositAmount,
+ uint96 _stakeMoreAmount,
+ uint256 _approvalAmount,
+ address _delegatee,
+ address _beneficiary
+ ) public {
+ vm.assume(_delegatee != address(0) && _beneficiary != address(0));
+ (address _depositor, uint256 _depositorPrivateKey) = makeAddrAndKey("depositor");
+ UniStaker.DepositIdentifier _depositId;
+ (_initialDepositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _initialDepositAmount, _delegatee, _beneficiary);
+ _stakeMoreAmount = uint96(bound(_stakeMoreAmount, 0, type(uint96).max - _initialDepositAmount));
+ _approvalAmount = bound(_approvalAmount, _stakeMoreAmount, type(uint256).max);
+ _mintGovToken(_depositor, _stakeMoreAmount);
+ vm.prank(_depositor);
+ govToken.approve(address(uniStaker), _approvalAmount);
+
+ bytes32 _message = keccak256(
+ abi.encode(
+ PERMIT_TYPEHASH,
+ _depositor,
+ address(uniStaker),
+ _stakeMoreAmount,
+ 1, // intentionally incorrect nonce, which should be 0
+ block.timestamp + 10_000
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", govToken.DOMAIN_SEPARATOR(), _message));
+
+ (uint8 _v, bytes32 _r, bytes32 _s) = vm.sign(_depositorPrivateKey, _messageHash);
+
+ vm.prank(_depositor);
+ uniStaker.permitAndStakeMore(_depositId, _stakeMoreAmount, block.timestamp, _v, _r, _s);
+ assertEq(uniStaker.depositorTotalStaked(_depositor), _initialDepositAmount + _stakeMoreAmount);
+ }
+
+ function testFuzz_RevertIf_CallerIsNotTheDepositOwner(
+ address _depositor,
+ uint256 _notDepositorPrivateKey,
+ uint96 _initialDepositAmount,
+ uint96 _stakeMoreAmount,
+ address _delegatee,
+ address _beneficiary,
+ uint256 _deadline
+ ) public {
+ vm.assume(_delegatee != address(0) && _beneficiary != address(0));
+ _notDepositorPrivateKey = bound(_notDepositorPrivateKey, 1, 100e18);
+ address _notDepositor = vm.addr(_notDepositorPrivateKey);
+ vm.assume(_depositor != _notDepositor);
+ _deadline = bound(_deadline, block.timestamp, type(uint256).max);
+
+ UniStaker.DepositIdentifier _depositId;
+ (_initialDepositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _initialDepositAmount, _delegatee, _beneficiary);
+
+ _stakeMoreAmount = _boundToRealisticStake(_stakeMoreAmount);
+ _mintGovToken(_depositor, _stakeMoreAmount);
+
+ // Separate scope to avoid stack to deep errors
+ {
+ bytes32 _message = keccak256(
+ abi.encode(
+ PERMIT_TYPEHASH,
+ _notDepositor,
+ address(uniStaker),
+ _stakeMoreAmount,
+ govToken.nonces(_depositor),
+ _deadline
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", govToken.DOMAIN_SEPARATOR(), _message));
+ (uint8 _v, bytes32 _r, bytes32 _s) = vm.sign(_notDepositorPrivateKey, _messageHash);
+
+ vm.prank(_notDepositor);
+ vm.expectRevert(
+ abi.encodeWithSelector(
+ UniStaker.UniStaker__Unauthorized.selector, bytes32("not owner"), _notDepositor
+ )
+ );
+ uniStaker.permitAndStakeMore(_depositId, _stakeMoreAmount, _deadline, _v, _r, _s);
+ }
+ }
+
+ function testFuzz_RevertIf_ThePermitSignatureIsInvalidAndTheApprovalIsInsufficient(
+ uint96 _initialDepositAmount,
+ address _delegatee,
+ address _beneficiary
+ ) public {
+ vm.assume(_delegatee != address(0) && _beneficiary != address(0));
+
+ // We can't fuzz the these values because we need to pre-compute the invalid
+ // recovered signer so we can expect it in the revert error message thrown
+ (address _depositor, uint256 _depositorPrivateKey) = makeAddrAndKey("depositor");
+ uint96 _stakeMoreAmount = 1578e18;
+ uint256 _deadline = 1e18 days;
+ uint256 _wrongNonce = 1;
+ uint256 _approvalAmount = _stakeMoreAmount - 1;
+
+ UniStaker.DepositIdentifier _depositId;
+ (_initialDepositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _initialDepositAmount, _delegatee, _beneficiary);
+ _mintGovToken(_depositor, _stakeMoreAmount);
+ vm.prank(_depositor);
+ govToken.approve(address(uniStaker), _approvalAmount);
+
+ bytes32 _message = keccak256(
+ abi.encode(
+ PERMIT_TYPEHASH,
+ _depositor,
+ address(uniStaker),
+ _stakeMoreAmount,
+ _wrongNonce, // intentionally incorrect nonce, which should be 0
+ _deadline
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", govToken.DOMAIN_SEPARATOR(), _message));
+
+ (uint8 _v, bytes32 _r, bytes32 _s) = vm.sign(_depositorPrivateKey, _messageHash);
+
+ vm.prank(_depositor);
+ vm.expectRevert(
+ abi.encodeWithSelector(
+ IERC20Errors.ERC20InsufficientAllowance.selector,
+ address(uniStaker),
+ _approvalAmount,
+ _stakeMoreAmount
+ )
+ );
+ uniStaker.permitAndStakeMore(_depositId, _stakeMoreAmount, _deadline, _v, _r, _s);
+ }
+}
+
+contract StakeMoreOnBehalf is UniStakerTest {
+ using stdStorage for StdStorage;
+
+ function testFuzz_StakeMoreOnBehalfOfDepositor(
+ uint256 _depositorPrivateKey,
+ address _sender,
+ uint96 _initialDepositAmount,
+ uint96 _stakeMoreAmount,
+ address _delegatee,
+ address _beneficiary,
+ uint256 _currentNonce,
+ uint256 _deadline
+ ) public {
+ vm.assume(_delegatee != address(0) && _beneficiary != address(0) && _sender != address(0));
+ _deadline = bound(_deadline, block.timestamp, type(uint256).max);
+ _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18);
+ address _depositor = vm.addr(_depositorPrivateKey);
+
+ UniStaker.DepositIdentifier _depositId;
+ (_initialDepositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _initialDepositAmount, _delegatee, _beneficiary);
+ UniStaker.Deposit memory _deposit = _fetchDeposit(_depositId);
+
+ _stakeMoreAmount = _boundToRealisticStake(_stakeMoreAmount);
+ _mintGovToken(_depositor, _stakeMoreAmount);
+
+ vm.startPrank(_depositor);
+ govToken.approve(address(uniStaker), _stakeMoreAmount);
+ vm.stopPrank();
+
+ stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_depositor).checked_write(
+ _currentNonce
+ );
+
+ bytes32 _message = keccak256(
+ abi.encode(
+ uniStaker.STAKE_MORE_TYPEHASH(),
+ _depositId,
+ _stakeMoreAmount,
+ _depositor,
+ uniStaker.nonces(_depositor),
+ _deadline
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message));
+ bytes memory _signature = _sign(_depositorPrivateKey, _messageHash);
+
+ vm.prank(_sender);
+ uniStaker.stakeMoreOnBehalf(_depositId, _stakeMoreAmount, _depositor, _deadline, _signature);
+
+ _deposit = _fetchDeposit(_depositId);
+
+ assertEq(_deposit.balance, _initialDepositAmount + _stakeMoreAmount);
+ assertEq(_deposit.owner, _depositor);
+ assertEq(_deposit.delegatee, _delegatee);
+ assertEq(_deposit.beneficiary, _beneficiary);
+ }
+
+ function testFuzz_RevertIf_WrongNonceIsUsed(
+ uint256 _depositorPrivateKey,
+ address _sender,
+ uint96 _initialDepositAmount,
+ uint96 _stakeMoreAmount,
+ address _delegatee,
+ address _beneficiary,
+ uint256 _currentNonce,
+ uint256 _suppliedNonce,
+ uint256 _deadline
+ ) public {
+ vm.assume(_currentNonce != _suppliedNonce);
+ vm.assume(_delegatee != address(0) && _beneficiary != address(0) && _sender != address(0));
+ _deadline = bound(_deadline, block.timestamp, type(uint256).max);
+ _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18);
+ address _depositor = vm.addr(_depositorPrivateKey);
+ _initialDepositAmount = _boundMintAmount(_initialDepositAmount);
+ _mintGovToken(_depositor, _initialDepositAmount);
+ stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_depositor).checked_write(
+ _currentNonce
+ );
+
+ UniStaker.DepositIdentifier _depositId;
+ (_initialDepositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _initialDepositAmount, _delegatee, _beneficiary);
+
+ _stakeMoreAmount = _boundToRealisticStake(_stakeMoreAmount);
+ _mintGovToken(_depositor, _stakeMoreAmount);
+
+ vm.startPrank(_depositor);
+ govToken.approve(address(uniStaker), _stakeMoreAmount);
+ vm.stopPrank();
+
+ bytes32 _message = keccak256(
+ abi.encode(
+ uniStaker.STAKE_MORE_TYPEHASH(),
+ _depositId,
+ _stakeMoreAmount,
+ _depositor,
+ _suppliedNonce,
+ _deadline
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message));
+ bytes memory _signature = _sign(_depositorPrivateKey, _messageHash);
+
+ vm.expectRevert(UniStaker.UniStaker__InvalidSignature.selector);
+ vm.prank(_sender);
+ uniStaker.stakeMoreOnBehalf(_depositId, _stakeMoreAmount, _depositor, _deadline, _signature);
+ }
+
+ function testFuzz_RevertIf_DeadlineExpired(
+ uint256 _depositorPrivateKey,
+ address _sender,
+ uint96 _initialDepositAmount,
+ uint96 _stakeMoreAmount,
+ address _delegatee,
+ address _beneficiary,
+ uint256 _currentNonce,
+ uint256 _deadline
+ ) public {
+ vm.assume(_delegatee != address(0) && _beneficiary != address(0) && _sender != address(0));
+ _deadline = bound(_deadline, 0, block.timestamp - 1);
+ _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18);
+ address _depositor = vm.addr(_depositorPrivateKey);
+ _initialDepositAmount = _boundMintAmount(_initialDepositAmount);
+ _mintGovToken(_depositor, _initialDepositAmount);
+ stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_depositor).checked_write(
+ _currentNonce
+ );
+
+ UniStaker.DepositIdentifier _depositId;
+ (_initialDepositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _initialDepositAmount, _delegatee, _beneficiary);
+
+ _stakeMoreAmount = _boundToRealisticStake(_stakeMoreAmount);
+ _mintGovToken(_depositor, _stakeMoreAmount);
+
+ vm.startPrank(_depositor);
+ govToken.approve(address(uniStaker), _stakeMoreAmount);
+ vm.stopPrank();
+
+ bytes32 _message = keccak256(
+ abi.encode(
+ uniStaker.STAKE_MORE_TYPEHASH(),
+ _depositId,
+ _stakeMoreAmount,
+ _depositor,
+ _currentNonce,
+ _deadline
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message));
+ bytes memory _signature = _sign(_depositorPrivateKey, _messageHash);
+
+ vm.expectRevert(UniStaker.UniStaker__ExpiredDeadline.selector);
+ vm.prank(_sender);
+ uniStaker.stakeMoreOnBehalf(_depositId, _stakeMoreAmount, _depositor, _deadline, _signature);
+ }
+
+ function testFuzz_RevertIf_DepositorIsNotDepositOwner(
+ address _sender,
+ address _depositor,
+ address _notDepositor,
+ uint96 _initialDepositAmount,
+ uint96 _stakeMoreAmount,
+ address _delegatee,
+ address _beneficiary,
+ uint256 _currentNonce,
+ uint256 _deadline
+ ) public {
+ vm.assume(
+ _delegatee != address(0) && _beneficiary != address(0) && _sender != address(0)
+ && _depositor != _notDepositor
+ );
+ _deadline = bound(_deadline, block.timestamp, type(uint256).max);
+ _initialDepositAmount = _boundMintAmount(_initialDepositAmount);
+ _mintGovToken(_depositor, _initialDepositAmount);
+ stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_depositor).checked_write(
+ _currentNonce
+ );
+
+ UniStaker.DepositIdentifier _depositId;
+ (_initialDepositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _initialDepositAmount, _delegatee, _beneficiary);
+
+ vm.expectRevert(
+ abi.encodeWithSelector(
+ UniStaker.UniStaker__Unauthorized.selector, bytes32("not owner"), _notDepositor
+ )
+ );
+ vm.prank(_sender);
+ uniStaker.stakeMoreOnBehalf(_depositId, _stakeMoreAmount, _notDepositor, _deadline, "");
+ }
+
+ function testFuzz_RevertIf_InvalidSignatureIsPassed(
+ uint256 _depositorPrivateKey,
+ address _sender,
+ uint96 _initialDepositAmount,
+ uint96 _stakeMoreAmount,
+ address _delegatee,
+ address _beneficiary,
+ uint256 _currentNonce,
+ uint256 _randomSeed,
+ uint256 _deadline
+ ) public {
+ vm.assume(_delegatee != address(0) && _beneficiary != address(0) && _sender != address(0));
+ _deadline = bound(_deadline, block.timestamp, type(uint256).max);
+ _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18);
+ address _depositor = vm.addr(_depositorPrivateKey);
+ _initialDepositAmount = _boundMintAmount(_initialDepositAmount);
+ _mintGovToken(_depositor, _initialDepositAmount);
+ stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_depositor).checked_write(
+ _currentNonce
+ );
+
+ UniStaker.DepositIdentifier _depositId;
+ (_initialDepositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _initialDepositAmount, _delegatee, _beneficiary);
+
+ _stakeMoreAmount = _boundToRealisticStake(_stakeMoreAmount);
+ _mintGovToken(_depositor, _stakeMoreAmount);
+
+ vm.startPrank(_depositor);
+ govToken.approve(address(uniStaker), _stakeMoreAmount);
+ vm.stopPrank();
+
+ bytes32 _message = keccak256(
+ abi.encode(
+ uniStaker.STAKE_MORE_TYPEHASH(),
+ _depositId,
+ _stakeMoreAmount,
+ _depositor,
+ uniStaker.nonces(_depositor),
+ _deadline
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message));
+
+ // Here we use `_randomSeed` as an arbitrary source of randomness to replace a legit parameter
+ // with an attack-like one.
+ if (_randomSeed % 4 == 0) {
+ _stakeMoreAmount = uint96(uint256(keccak256(abi.encode(_stakeMoreAmount))));
+ } else if (_randomSeed % 4 == 1) {
+ _messageHash = _modifyMessage(_messageHash, uint256(keccak256(abi.encode(_randomSeed))));
+ } else if (_randomSeed % 4 == 2) {
+ _deadline = uint256(keccak256(abi.encode(_deadline)));
+ }
+ bytes memory _signature = _sign(_depositorPrivateKey, _messageHash);
+ if (_randomSeed % 4 == 3) _signature = _modifySignature(_signature, _randomSeed);
+
+ vm.expectRevert(UniStaker.UniStaker__InvalidSignature.selector);
+ vm.prank(_sender);
+ uniStaker.stakeMoreOnBehalf(_depositId, _stakeMoreAmount, _depositor, _deadline, _signature);
+ }
+}
+
+contract AlterDelegatee is UniStakerTest {
+ function testFuzz_AllowsStakerToUpdateTheirDelegatee(
+ address _depositor,
+ uint96 _depositAmount,
+ address _firstDelegatee,
+ address _beneficiary,
+ address _newDelegatee
+ ) public {
+ vm.assume(_newDelegatee != address(0) && _newDelegatee != _firstDelegatee);
+
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _depositAmount, _firstDelegatee, _beneficiary);
+ address _firstSurrogate = address(uniStaker.surrogates(_firstDelegatee));
+
+ vm.prank(_depositor);
+ uniStaker.alterDelegatee(_depositId, _newDelegatee);
+
+ UniStaker.Deposit memory _deposit = _fetchDeposit(_depositId);
+ address _newSurrogate = address(uniStaker.surrogates(_deposit.delegatee));
+
+ assertEq(_deposit.delegatee, _newDelegatee);
+ assertEq(govToken.balanceOf(_newSurrogate), _depositAmount);
+ assertEq(govToken.balanceOf(_firstSurrogate), 0);
+ }
+
+ function testFuzz_AllowsStakerToReiterateTheirDelegatee(
+ address _depositor,
+ uint96 _depositAmount,
+ address _delegatee,
+ address _beneficiary
+ ) public {
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary);
+ address _beforeSurrogate = address(uniStaker.surrogates(_delegatee));
+
+ // We are calling alterDelegatee with the address that is already the delegatee to ensure that
+ // doing so does not break anything other than wasting the user's gas
+ vm.prank(_depositor);
+ uniStaker.alterDelegatee(_depositId, _delegatee);
+
+ UniStaker.Deposit memory _deposit = _fetchDeposit(_depositId);
+ address _afterSurrogate = address(uniStaker.surrogates(_deposit.delegatee));
+
+ assertEq(_deposit.delegatee, _delegatee);
+ assertEq(_beforeSurrogate, _afterSurrogate);
+ assertEq(govToken.balanceOf(_afterSurrogate), _depositAmount);
+ }
+
+ function testFuzz_EmitsAnEventWhenADelegateeIsChanged(
+ address _depositor,
+ uint96 _depositAmount,
+ address _firstDelegatee,
+ address _beneficiary,
+ address _newDelegatee
+ ) public {
+ vm.assume(_newDelegatee != address(0) && _newDelegatee != _firstDelegatee);
+
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _depositAmount, _firstDelegatee, _beneficiary);
+
+ vm.expectEmit();
+ emit UniStaker.DelegateeAltered(_depositId, _firstDelegatee, _newDelegatee);
+
+ vm.prank(_depositor);
+ uniStaker.alterDelegatee(_depositId, _newDelegatee);
+ }
+
+ function testFuzz_RevertIf_TheCallerIsNotTheDepositor(
+ address _depositor,
+ address _notDepositor,
+ uint96 _depositAmount,
+ address _firstDelegatee,
+ address _beneficiary,
+ address _newDelegatee
+ ) public {
+ vm.assume(
+ _depositor != _notDepositor && _newDelegatee != address(0) && _newDelegatee != _firstDelegatee
+ );
+
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _depositAmount, _firstDelegatee, _beneficiary);
+
+ vm.prank(_notDepositor);
+ vm.expectRevert(
+ abi.encodeWithSelector(
+ UniStaker.UniStaker__Unauthorized.selector, bytes32("not owner"), _notDepositor
+ )
+ );
+ uniStaker.alterDelegatee(_depositId, _newDelegatee);
+ }
+
+ function testFuzz_RevertIf_TheDepositIdentifierIsInvalid(
+ address _depositor,
+ UniStaker.DepositIdentifier _depositId,
+ address _newDelegatee
+ ) public {
+ vm.assume(_depositor != address(0) && _newDelegatee != address(0));
+
+ // Since no deposits have been made yet, all DepositIdentifiers are invalid, and any call to
+ // alter one should revert. We rely on the default owner of any uninitialized deposit being
+ // address zero, which means the address attempting to alter it won't be able to.
+ vm.prank(_depositor);
+ vm.expectRevert(
+ abi.encodeWithSelector(
+ UniStaker.UniStaker__Unauthorized.selector, bytes32("not owner"), _depositor
+ )
+ );
+ uniStaker.alterDelegatee(_depositId, _newDelegatee);
+ }
+
+ function testFuzz_RevertIf_DelegateeIsTheZeroAddress(
+ address _depositor,
+ uint96 _depositAmount,
+ address _delegatee
+ ) public {
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _depositId) = _boundMintAndStake(_depositor, _depositAmount, _delegatee);
+
+ vm.prank(_depositor);
+ vm.expectRevert(UniStaker.UniStaker__InvalidAddress.selector);
+ uniStaker.alterDelegatee(_depositId, address(0));
+ }
+}
+
+contract AlterDelegateeOnBehalf is UniStakerTest {
+ using stdStorage for StdStorage;
+
+ function testFuzz_AlterDelegateeOnBehalfOfDepositor(
+ uint256 _depositorPrivateKey,
+ address _sender,
+ uint96 _amount,
+ address _delegatee,
+ address _beneficiary,
+ address _newDelegatee,
+ uint256 _currentNonce,
+ uint256 _deadline
+ ) public {
+ vm.assume(
+ _delegatee != address(0) && _beneficiary != address(0) && _sender != address(0)
+ && _newDelegatee != address(0)
+ );
+ _deadline = bound(_deadline, block.timestamp, type(uint256).max);
+ _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18);
+ address _depositor = vm.addr(_depositorPrivateKey);
+
+ UniStaker.DepositIdentifier _depositId;
+ (_amount, _depositId) = _boundMintAndStake(_depositor, _amount, _delegatee, _beneficiary);
+ UniStaker.Deposit memory _deposit = _fetchDeposit(_depositId);
+
+ stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_depositor).checked_write(
+ _currentNonce
+ );
+
+ bytes32 _message = keccak256(
+ abi.encode(
+ uniStaker.ALTER_DELEGATEE_TYPEHASH(),
+ _depositId,
+ _newDelegatee,
+ _depositor,
+ uniStaker.nonces(_depositor),
+ _deadline
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message));
+ bytes memory _signature = _sign(_depositorPrivateKey, _messageHash);
+
+ vm.prank(_sender);
+ uniStaker.alterDelegateeOnBehalf(_depositId, _newDelegatee, _depositor, _deadline, _signature);
+
+ _deposit = _fetchDeposit(_depositId);
+
+ assertEq(_deposit.delegatee, _newDelegatee);
+ }
+
+ function testFuzz_RevertIf_WrongNonceIsUsed(
+ uint256 _depositorPrivateKey,
+ address _sender,
+ uint96 _amount,
+ address _delegatee,
+ address _beneficiary,
+ address _newDelegatee,
+ uint256 _currentNonce,
+ uint256 _suppliedNonce,
+ uint256 _deadline
+ ) public {
+ vm.assume(_currentNonce != _suppliedNonce);
+ vm.assume(
+ _delegatee != address(0) && _beneficiary != address(0) && _sender != address(0)
+ && _newDelegatee != address(0)
+ );
+ _deadline = bound(_deadline, block.timestamp, type(uint256).max);
+ _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18);
+ address _depositor = vm.addr(_depositorPrivateKey);
+ _amount = _boundMintAmount(_amount);
+ _mintGovToken(_depositor, _amount);
+ stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_depositor).checked_write(
+ _currentNonce
+ );
+
+ UniStaker.DepositIdentifier _depositId;
+ (_amount, _depositId) = _boundMintAndStake(_depositor, _amount, _delegatee, _beneficiary);
+
+ bytes32 _message = keccak256(
+ abi.encode(
+ uniStaker.ALTER_DELEGATEE_TYPEHASH(), _depositId, _newDelegatee, _depositor, _suppliedNonce
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message));
+ bytes memory _signature = _sign(_depositorPrivateKey, _messageHash);
+
+ vm.expectRevert(UniStaker.UniStaker__InvalidSignature.selector);
+ vm.prank(_sender);
+ uniStaker.alterDelegateeOnBehalf(_depositId, _newDelegatee, _depositor, _deadline, _signature);
+ }
+
+ function testFuzz_RevertIf_DeadlineExpired(
+ uint256 _depositorPrivateKey,
+ address _sender,
+ uint96 _amount,
+ address _delegatee,
+ address _beneficiary,
+ address _newDelegatee,
+ uint256 _currentNonce,
+ uint256 _deadline
+ ) public {
+ vm.assume(
+ _delegatee != address(0) && _beneficiary != address(0) && _sender != address(0)
+ && _newDelegatee != address(0)
+ );
+ _deadline = bound(_deadline, 0, block.timestamp - 1);
+ _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18);
+ address _depositor = vm.addr(_depositorPrivateKey);
+ _amount = _boundMintAmount(_amount);
+ _mintGovToken(_depositor, _amount);
+ stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_depositor).checked_write(
+ _currentNonce
+ );
+
+ UniStaker.DepositIdentifier _depositId;
+ (_amount, _depositId) = _boundMintAndStake(_depositor, _amount, _delegatee, _beneficiary);
+
+ bytes32 _message = keccak256(
+ abi.encode(
+ uniStaker.ALTER_DELEGATEE_TYPEHASH(),
+ _depositId,
+ _newDelegatee,
+ _depositor,
+ _currentNonce,
+ _deadline
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message));
+ bytes memory _signature = _sign(_depositorPrivateKey, _messageHash);
+
+ vm.expectRevert(UniStaker.UniStaker__ExpiredDeadline.selector);
+ vm.prank(_sender);
+ uniStaker.alterDelegateeOnBehalf(_depositId, _newDelegatee, _depositor, _deadline, _signature);
+ }
+
+ function testFuzz_RevertIf_DepositorIsNotDepositOwner(
+ address _sender,
+ address _depositor,
+ address _notDepositor,
+ uint96 _amount,
+ address _delegatee,
+ address _newDelegatee,
+ address _beneficiary,
+ uint256 _deadline,
+ bytes memory _signature
+ ) public {
+ vm.assume(
+ _delegatee != address(0) && _beneficiary != address(0) && _sender != address(0)
+ && _depositor != _notDepositor
+ );
+ _deadline = bound(_deadline, block.timestamp, type(uint256).max);
+ _amount = _boundMintAmount(_amount);
+ _mintGovToken(_depositor, _amount);
+
+ UniStaker.DepositIdentifier _depositId;
+ (_amount, _depositId) = _boundMintAndStake(_depositor, _amount, _delegatee, _beneficiary);
+
+ vm.expectRevert(
+ abi.encodeWithSelector(
+ UniStaker.UniStaker__Unauthorized.selector, bytes32("not owner"), _notDepositor
+ )
+ );
+ vm.prank(_sender);
+ uniStaker.alterDelegateeOnBehalf(
+ _depositId, _newDelegatee, _notDepositor, _deadline, _signature
+ );
+ }
+
+ function testFuzz_RevertIf_InvalidSignatureIsPassed(
+ uint256 _depositorPrivateKey,
+ address _sender,
+ uint96 _amount,
+ address _delegatee,
+ address _beneficiary,
+ address _newDelegatee,
+ uint256 _currentNonce,
+ uint256 _randomSeed,
+ uint256 _deadline
+ ) public {
+ vm.assume(_delegatee != address(0) && _beneficiary != address(0) && _sender != address(0));
+ _deadline = bound(_deadline, block.timestamp, type(uint256).max);
+ _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18);
+ address _depositor = vm.addr(_depositorPrivateKey);
+ _amount = _boundMintAmount(_amount);
+ _mintGovToken(_depositor, _amount);
+ stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_depositor).checked_write(
+ _currentNonce
+ );
+
+ UniStaker.DepositIdentifier _depositId;
+ (_amount, _depositId) = _boundMintAndStake(_depositor, _amount, _delegatee, _beneficiary);
+
+ bytes32 _message = keccak256(
+ abi.encode(
+ uniStaker.ALTER_DELEGATEE_TYPEHASH(),
+ _depositId,
+ _newDelegatee,
+ _depositor,
+ uniStaker.nonces(_depositor),
+ _deadline
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message));
+
+ // Here we use `_randomSeed` as an arbitrary source of randomness to replace a legit parameter
+ // with an attack-like one.
+ if (_randomSeed % 4 == 0) {
+ _newDelegatee = address(uint160(uint256(keccak256(abi.encode(_newDelegatee)))));
+ } else if (_randomSeed % 4 == 1) {
+ _messageHash = _modifyMessage(_messageHash, uint256(keccak256(abi.encode(_randomSeed))));
+ } else if (_randomSeed % 4 == 2) {
+ _deadline = uint256(keccak256(abi.encode(_deadline)));
+ }
+ bytes memory _signature = _sign(_depositorPrivateKey, _messageHash);
+ if (_randomSeed % 4 == 3) _signature = _modifySignature(_signature, _randomSeed);
+
+ vm.expectRevert(UniStaker.UniStaker__InvalidSignature.selector);
+ vm.prank(_sender);
+ uniStaker.alterDelegateeOnBehalf(_depositId, _newDelegatee, _depositor, _deadline, _signature);
+ }
+}
+
+contract AlterBeneficiary is UniStakerTest {
+ function testFuzz_AllowsStakerToUpdateTheirBeneficiary(
+ address _depositor,
+ uint96 _depositAmount,
+ address _delegatee,
+ address _firstBeneficiary,
+ address _newBeneficiary
+ ) public {
+ vm.assume(_newBeneficiary != address(0) && _newBeneficiary != _firstBeneficiary);
+
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _depositAmount, _delegatee, _firstBeneficiary);
+
+ vm.prank(_depositor);
+ uniStaker.alterBeneficiary(_depositId, _newBeneficiary);
+
+ UniStaker.Deposit memory _deposit = _fetchDeposit(_depositId);
+
+ assertEq(_deposit.beneficiary, _newBeneficiary);
+ assertEq(uniStaker.earningPower(_newBeneficiary), _depositAmount);
+ assertEq(uniStaker.earningPower(_firstBeneficiary), 0);
+ }
+
+ function testFuzz_AllowsStakerToReiterateTheirBeneficiary(
+ address _depositor,
+ uint96 _depositAmount,
+ address _delegatee,
+ address _beneficiary
+ ) public {
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary);
+
+ // We are calling alterBeneficiary with the address that is already the beneficiary to ensure
+ // that doing so does not break anything other than wasting the user's gas
+ vm.prank(_depositor);
+ uniStaker.alterBeneficiary(_depositId, _beneficiary);
+
+ UniStaker.Deposit memory _deposit = _fetchDeposit(_depositId);
+
+ assertEq(_deposit.beneficiary, _beneficiary);
+ assertEq(uniStaker.earningPower(_beneficiary), _depositAmount);
+ }
+
+ function testFuzz_EmitsAnEventWhenBeneficiaryAltered(
+ address _depositor,
+ uint96 _depositAmount,
+ address _delegatee,
+ address _firstBeneficiary,
+ address _newBeneficiary
+ ) public {
+ vm.assume(_newBeneficiary != address(0) && _newBeneficiary != _firstBeneficiary);
+
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _depositAmount, _delegatee, _firstBeneficiary);
+
+ vm.expectEmit();
+ emit UniStaker.BeneficiaryAltered(_depositId, _firstBeneficiary, _newBeneficiary);
+
+ vm.prank(_depositor);
+ uniStaker.alterBeneficiary(_depositId, _newBeneficiary);
+ }
+
+ function testFuzz_RevertIf_TheCallerIsNotTheDepositor(
+ address _depositor,
+ address _notDepositor,
+ uint96 _depositAmount,
+ address _delegatee,
+ address _firstBeneficiary,
+ address _newBeneficiary
+ ) public {
+ vm.assume(
+ _notDepositor != _depositor && _newBeneficiary != address(0)
+ && _newBeneficiary != _firstBeneficiary
+ );
+
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _depositAmount, _delegatee, _firstBeneficiary);
+
+ vm.prank(_notDepositor);
+ vm.expectRevert(
+ abi.encodeWithSelector(
+ UniStaker.UniStaker__Unauthorized.selector, bytes32("not owner"), _notDepositor
+ )
+ );
+ uniStaker.alterBeneficiary(_depositId, _newBeneficiary);
+ }
+
+ function testFuzz_RevertIf_TheDepositIdentifierIsInvalid(
+ address _depositor,
+ UniStaker.DepositIdentifier _depositId,
+ address _newBeneficiary
+ ) public {
+ vm.assume(_depositor != address(0) && _newBeneficiary != address(0));
+
+ // Since no deposits have been made yet, all DepositIdentifiers are invalid, and any call to
+ // alter one should revert. We rely on the default owner of any uninitialized deposit being
+ // address zero, which means the address attempting to alter it won't be able to.
+ vm.prank(_depositor);
+ vm.expectRevert(
+ abi.encodeWithSelector(
+ UniStaker.UniStaker__Unauthorized.selector, bytes32("not owner"), _depositor
+ )
+ );
+ uniStaker.alterBeneficiary(_depositId, _newBeneficiary);
+ }
+
+ function testFuzz_RevertIf_BeneficiaryIsTheZeroAddress(
+ address _depositor,
+ uint96 _depositAmount,
+ address _delegatee
+ ) public {
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _depositId) = _boundMintAndStake(_depositor, _depositAmount, _delegatee);
+
+ vm.prank(_depositor);
+ vm.expectRevert(UniStaker.UniStaker__InvalidAddress.selector);
+ uniStaker.alterBeneficiary(_depositId, address(0));
+ }
+}
+
+contract AlterBeneficiaryOnBehalf is UniStakerTest {
+ using stdStorage for StdStorage;
+
+ function testFuzz_AlterBeneficiaryOnBehalfOfDepositor(
+ uint256 _depositorPrivateKey,
+ address _sender,
+ uint96 _amount,
+ address _delegatee,
+ address _beneficiary,
+ address _newBeneficiary,
+ uint256 _currentNonce,
+ uint256 _deadline
+ ) public {
+ vm.assume(
+ _delegatee != address(0) && _beneficiary != address(0) && _sender != address(0)
+ && _newBeneficiary != address(0)
+ );
+ _deadline = bound(_deadline, block.timestamp, type(uint256).max);
+ _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18);
+ address _depositor = vm.addr(_depositorPrivateKey);
+
+ UniStaker.DepositIdentifier _depositId;
+ (_amount, _depositId) = _boundMintAndStake(_depositor, _amount, _delegatee, _beneficiary);
+ UniStaker.Deposit memory _deposit = _fetchDeposit(_depositId);
+
+ stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_depositor).checked_write(
+ _currentNonce
+ );
+
+ bytes32 _message = keccak256(
+ abi.encode(
+ uniStaker.ALTER_BENEFICIARY_TYPEHASH(),
+ _depositId,
+ _newBeneficiary,
+ _depositor,
+ uniStaker.nonces(_depositor),
+ _deadline
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message));
+ bytes memory _signature = _sign(_depositorPrivateKey, _messageHash);
+
+ vm.prank(_sender);
+ uniStaker.alterBeneficiaryOnBehalf(
+ _depositId, _newBeneficiary, _depositor, _deadline, _signature
+ );
+
+ _deposit = _fetchDeposit(_depositId);
+
+ assertEq(_deposit.beneficiary, _newBeneficiary);
+ }
+
+ function testFuzz_RevertIf_WrongNonceIsUsed(
+ uint256 _depositorPrivateKey,
+ address _sender,
+ uint96 _amount,
+ address _delegatee,
+ address _beneficiary,
+ address _newBeneficiary,
+ uint256 _currentNonce,
+ uint256 _suppliedNonce,
+ uint256 _deadline
+ ) public {
+ vm.assume(_currentNonce != _suppliedNonce);
+ vm.assume(
+ _delegatee != address(0) && _beneficiary != address(0) && _sender != address(0)
+ && _newBeneficiary != address(0)
+ );
+ _deadline = bound(_deadline, block.timestamp, type(uint256).max);
+ _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18);
+ address _depositor = vm.addr(_depositorPrivateKey);
+ _amount = _boundMintAmount(_amount);
+ _mintGovToken(_depositor, _amount);
+ stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_depositor).checked_write(
+ _currentNonce
+ );
+
+ UniStaker.DepositIdentifier _depositId;
+ (_amount, _depositId) = _boundMintAndStake(_depositor, _amount, _delegatee, _beneficiary);
+
+ bytes32 _message = keccak256(
+ abi.encode(
+ uniStaker.ALTER_BENEFICIARY_TYPEHASH(),
+ _depositId,
+ _newBeneficiary,
+ _depositor,
+ _suppliedNonce,
+ _deadline
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message));
+ bytes memory _signature = _sign(_depositorPrivateKey, _messageHash);
+
+ vm.expectRevert(UniStaker.UniStaker__InvalidSignature.selector);
+ vm.prank(_sender);
+ uniStaker.alterBeneficiaryOnBehalf(
+ _depositId, _newBeneficiary, _depositor, _deadline, _signature
+ );
+ }
+
+ function testFuzz_RevertIf_DeadlineExpired(
+ uint256 _depositorPrivateKey,
+ address _sender,
+ uint96 _amount,
+ address _delegatee,
+ address _beneficiary,
+ address _newBeneficiary,
+ uint256 _currentNonce,
+ uint256 _deadline
+ ) public {
+ vm.assume(
+ _delegatee != address(0) && _beneficiary != address(0) && _sender != address(0)
+ && _newBeneficiary != address(0)
+ );
+ _deadline = bound(_deadline, 0, block.timestamp - 1);
+ _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18);
+ address _depositor = vm.addr(_depositorPrivateKey);
+ _amount = _boundMintAmount(_amount);
+ _mintGovToken(_depositor, _amount);
+ stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_depositor).checked_write(
+ _currentNonce
+ );
+
+ UniStaker.DepositIdentifier _depositId;
+ (_amount, _depositId) = _boundMintAndStake(_depositor, _amount, _delegatee, _beneficiary);
+
+ bytes32 _message = keccak256(
+ abi.encode(
+ uniStaker.ALTER_BENEFICIARY_TYPEHASH(),
+ _depositId,
+ _newBeneficiary,
+ _depositor,
+ _currentNonce,
+ _deadline
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message));
+ bytes memory _signature = _sign(_depositorPrivateKey, _messageHash);
+
+ vm.expectRevert(UniStaker.UniStaker__ExpiredDeadline.selector);
+ vm.prank(_sender);
+ uniStaker.alterBeneficiaryOnBehalf(
+ _depositId, _newBeneficiary, _depositor, _deadline, _signature
+ );
+ }
+
+ function testFuzz_RevertIf_DepositorIsNotDepositOwner(
+ address _sender,
+ address _depositor,
+ address _notDepositor,
+ uint96 _amount,
+ address _delegatee,
+ address _beneficiary,
+ address _newBeneficiary,
+ uint256 _deadline,
+ bytes memory _signature
+ ) public {
+ vm.assume(
+ _delegatee != address(0) && _beneficiary != address(0) && _sender != address(0)
+ && _depositor != _notDepositor
+ );
+ _deadline = bound(_deadline, block.timestamp, type(uint256).max);
+ _amount = _boundMintAmount(_amount);
+ _mintGovToken(_depositor, _amount);
+
+ UniStaker.DepositIdentifier _depositId;
+ (_amount, _depositId) = _boundMintAndStake(_depositor, _amount, _delegatee, _beneficiary);
+
+ vm.expectRevert(
+ abi.encodeWithSelector(
+ UniStaker.UniStaker__Unauthorized.selector, bytes32("not owner"), _notDepositor
+ )
+ );
+ vm.prank(_sender);
+ uniStaker.alterBeneficiaryOnBehalf(
+ _depositId, _newBeneficiary, _notDepositor, _deadline, _signature
+ );
+ }
+
+ function testFuzz_RevertIf_InvalidSignatureIsPassed(
+ uint256 _depositorPrivateKey,
+ address _sender,
+ uint96 _amount,
+ address _delegatee,
+ address _beneficiary,
+ address _newBeneficiary,
+ uint256 _currentNonce,
+ uint256 _randomSeed,
+ uint256 _deadline
+ ) public {
+ vm.assume(_delegatee != address(0) && _beneficiary != address(0) && _sender != address(0));
+ _deadline = bound(_deadline, block.timestamp, type(uint256).max);
+ _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18);
+ address _depositor = vm.addr(_depositorPrivateKey);
+ _amount = _boundMintAmount(_amount);
+ _mintGovToken(_depositor, _amount);
+ stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_depositor).checked_write(
+ _currentNonce
+ );
+
+ UniStaker.DepositIdentifier _depositId;
+ (_amount, _depositId) = _boundMintAndStake(_depositor, _amount, _delegatee, _beneficiary);
+
+ bytes32 _message = keccak256(
+ abi.encode(
+ uniStaker.ALTER_BENEFICIARY_TYPEHASH(),
+ _depositId,
+ _newBeneficiary,
+ _depositor,
+ uniStaker.nonces(_depositor),
+ _deadline
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message));
+
+ // Here we use `_randomSeed` as an arbitrary source of randomness to replace a legit parameter
+ // with an attack-like one.
+ if (_randomSeed % 4 == 0) {
+ _newBeneficiary = address(uint160(uint256(keccak256(abi.encode(_newBeneficiary)))));
+ } else if (_randomSeed % 4 == 1) {
+ _messageHash = _modifyMessage(_messageHash, uint256(keccak256(abi.encode(_randomSeed))));
+ } else if (_randomSeed % 4 == 2) {
+ _deadline = uint256(keccak256(abi.encode(_deadline)));
+ }
+ bytes memory _signature = _sign(_depositorPrivateKey, _messageHash);
+ if (_randomSeed % 4 == 3) _signature = _modifySignature(_signature, _randomSeed);
+
+ vm.expectRevert(UniStaker.UniStaker__InvalidSignature.selector);
+ vm.prank(_sender);
+ uniStaker.alterBeneficiaryOnBehalf(
+ _depositId, _newBeneficiary, _depositor, _deadline, _signature
+ );
+ }
+}
+
+contract Withdraw is UniStakerTest {
+ function testFuzz_AllowsDepositorToWithdrawStake(
+ address _depositor,
+ uint96 _depositAmount,
+ address _delegatee,
+ uint96 _withdrawalAmount
+ ) public {
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _depositId) = _boundMintAndStake(_depositor, _depositAmount, _delegatee);
+ _withdrawalAmount = uint96(bound(_withdrawalAmount, 0, _depositAmount));
+
+ vm.prank(_depositor);
+ uniStaker.withdraw(_depositId, _withdrawalAmount);
+
+ UniStaker.Deposit memory _deposit = _fetchDeposit(_depositId);
+ address _surrogate = address(uniStaker.surrogates(_deposit.delegatee));
+
+ assertEq(govToken.balanceOf(_depositor), _withdrawalAmount);
+ assertEq(_deposit.balance, _depositAmount - _withdrawalAmount);
+ assertEq(govToken.balanceOf(_surrogate), _depositAmount - _withdrawalAmount);
+ }
+
+ function testFuzz_UpdatesTheTotalStakedWhenAnAccountWithdraws(
+ address _depositor,
+ uint96 _depositAmount,
+ address _delegatee,
+ uint96 _withdrawalAmount
+ ) public {
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _depositId) = _boundMintAndStake(_depositor, _depositAmount, _delegatee);
+ _withdrawalAmount = uint96(bound(_withdrawalAmount, 0, _depositAmount));
+
+ vm.prank(_depositor);
+ uniStaker.withdraw(_depositId, _withdrawalAmount);
+
+ assertEq(uniStaker.totalStaked(), _depositAmount - _withdrawalAmount);
+ }
+
+ function testFuzz_UpdatesTheTotalStakedWhenTwoAccountsWithdraw(
+ address _depositor1,
+ uint96 _depositAmount1,
+ address _delegatee1,
+ address _depositor2,
+ uint96 _depositAmount2,
+ address _delegatee2,
+ uint96 _withdrawalAmount1,
+ uint96 _withdrawalAmount2
+ ) public {
+ // Make two separate deposits
+ UniStaker.DepositIdentifier _depositId1;
+ (_depositAmount1, _depositId1) = _boundMintAndStake(_depositor1, _depositAmount1, _delegatee1);
+ UniStaker.DepositIdentifier _depositId2;
+ (_depositAmount2, _depositId2) = _boundMintAndStake(_depositor2, _depositAmount2, _delegatee2);
+
+ // Calculate withdrawal amounts
+ _withdrawalAmount1 = uint96(bound(_withdrawalAmount1, 0, _depositAmount1));
+ _withdrawalAmount2 = uint96(bound(_withdrawalAmount2, 0, _depositAmount2));
+
+ // Execute both withdrawals
+ vm.prank(_depositor1);
+ uniStaker.withdraw(_depositId1, _withdrawalAmount1);
+ vm.prank(_depositor2);
+ uniStaker.withdraw(_depositId2, _withdrawalAmount2);
+
+ uint96 _remainingDeposits =
+ _depositAmount1 + _depositAmount2 - _withdrawalAmount1 - _withdrawalAmount2;
+ assertEq(uniStaker.totalStaked(), _remainingDeposits);
+ }
+
+ function testFuzz_UpdatesAnAccountsTotalStakedWhenItWithdrawals(
+ address _depositor,
+ uint96 _depositAmount1,
+ uint96 _depositAmount2,
+ address _delegatee1,
+ address _delegatee2,
+ uint96 _withdrawalAmount
+ ) public {
+ // Make two separate deposits
+ UniStaker.DepositIdentifier _depositId1;
+ (_depositAmount1, _depositId1) = _boundMintAndStake(_depositor, _depositAmount1, _delegatee1);
+ UniStaker.DepositIdentifier _depositId2;
+ (_depositAmount2, _depositId2) = _boundMintAndStake(_depositor, _depositAmount2, _delegatee2);
+
+ // Withdraw part of the first deposit
+ _withdrawalAmount = uint96(bound(_withdrawalAmount, 0, _depositAmount1));
+ vm.prank(_depositor);
+ uniStaker.withdraw(_depositId1, _withdrawalAmount);
+
+ // Ensure the account's total balance + global balance accounting have been updated
+ assertEq(
+ uniStaker.depositorTotalStaked(_depositor),
+ _depositAmount1 + _depositAmount2 - _withdrawalAmount
+ );
+ assertEq(uniStaker.totalStaked(), _depositAmount1 + _depositAmount2 - _withdrawalAmount);
+ }
+
+ function testFuzz_RemovesEarningPowerFromADepositorWhoHadSelfAssignedIt(
+ address _depositor,
+ uint96 _depositAmount,
+ address _delegatee,
+ uint96 _withdrawalAmount
+ ) public {
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _depositId) = _boundMintAndStake(_depositor, _depositAmount, _delegatee);
+ _withdrawalAmount = uint96(bound(_withdrawalAmount, 0, _depositAmount));
+
+ vm.prank(_depositor);
+ uniStaker.withdraw(_depositId, _withdrawalAmount);
+
+ assertEq(uniStaker.earningPower(_depositor), _depositAmount - _withdrawalAmount);
+ }
+
+ function testFuzz_RemovesEarningPowerFromABeneficiary(
+ address _depositor,
+ uint96 _depositAmount,
+ address _delegatee,
+ address _beneficiary,
+ uint96 _withdrawalAmount
+ ) public {
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary);
+ _withdrawalAmount = uint96(bound(_withdrawalAmount, 0, _depositAmount));
+
+ vm.prank(_depositor);
+ uniStaker.withdraw(_depositId, _withdrawalAmount);
+
+ assertEq(uniStaker.earningPower(_beneficiary), _depositAmount - _withdrawalAmount);
+ }
+
+ function testFuzz_RemovesEarningPowerFromABeneficiaryAssignedByTwoDepositors(
+ address _depositor1,
+ address _depositor2,
+ uint96 _depositAmount1,
+ uint96 _depositAmount2,
+ address _delegatee,
+ address _beneficiary,
+ uint96 _withdrawalAmount1,
+ uint96 _withdrawalAmount2
+ ) public {
+ UniStaker.DepositIdentifier _depositId1;
+ (_depositAmount1, _depositId1) =
+ _boundMintAndStake(_depositor1, _depositAmount1, _delegatee, _beneficiary);
+ _withdrawalAmount1 = uint96(bound(_withdrawalAmount1, 0, _depositAmount1));
+
+ UniStaker.DepositIdentifier _depositId2;
+ (_depositAmount2, _depositId2) =
+ _boundMintAndStake(_depositor2, _depositAmount2, _delegatee, _beneficiary);
+ _withdrawalAmount2 = uint96(bound(_withdrawalAmount2, 0, _depositAmount2));
+
+ vm.prank(_depositor1);
+ uniStaker.withdraw(_depositId1, _withdrawalAmount1);
+
+ assertEq(
+ uniStaker.earningPower(_beneficiary), _depositAmount1 - _withdrawalAmount1 + _depositAmount2
+ );
+
+ vm.prank(_depositor2);
+ uniStaker.withdraw(_depositId2, _withdrawalAmount2);
+
+ assertEq(
+ uniStaker.earningPower(_beneficiary),
+ _depositAmount1 - _withdrawalAmount1 + _depositAmount2 - _withdrawalAmount2
+ );
+ }
+
+ function testFuzz_RemovesEarningPowerFromDifferentBeneficiariesOfTheSameDepositor(
+ address _depositor,
+ uint96 _depositAmount1,
+ uint96 _depositAmount2,
+ address _delegatee,
+ address _beneficiary1,
+ address _beneficiary2,
+ uint96 _withdrawalAmount1,
+ uint96 _withdrawalAmount2
+ ) public {
+ vm.assume(_beneficiary1 != _beneficiary2);
+
+ UniStaker.DepositIdentifier _depositId1;
+ (_depositAmount1, _depositId1) =
+ _boundMintAndStake(_depositor, _depositAmount1, _delegatee, _beneficiary1);
+ _withdrawalAmount1 = uint96(bound(_withdrawalAmount1, 0, _depositAmount1));
+
+ UniStaker.DepositIdentifier _depositId2;
+ (_depositAmount2, _depositId2) =
+ _boundMintAndStake(_depositor, _depositAmount2, _delegatee, _beneficiary2);
+ _withdrawalAmount2 = uint96(bound(_withdrawalAmount2, 0, _depositAmount2));
+
+ vm.prank(_depositor);
+ uniStaker.withdraw(_depositId1, _withdrawalAmount1);
+
+ assertEq(uniStaker.earningPower(_beneficiary1), _depositAmount1 - _withdrawalAmount1);
+ assertEq(uniStaker.earningPower(_beneficiary2), _depositAmount2);
+
+ vm.prank(_depositor);
+ uniStaker.withdraw(_depositId2, _withdrawalAmount2);
+
+ assertEq(uniStaker.earningPower(_beneficiary1), _depositAmount1 - _withdrawalAmount1);
+ assertEq(uniStaker.earningPower(_beneficiary2), _depositAmount2 - _withdrawalAmount2);
+ }
+
+ function testFuzz_RemovesEarningPowerFromDifferentBeneficiariesAndDifferentDepositors(
+ address _depositor1,
+ address _depositor2,
+ uint96 _depositAmount1,
+ uint96 _depositAmount2,
+ address _delegatee,
+ address _beneficiary1,
+ address _beneficiary2,
+ uint96 _withdrawalAmount1,
+ uint96 _withdrawalAmount2
+ ) public {
+ vm.assume(_beneficiary1 != _beneficiary2);
+
+ UniStaker.DepositIdentifier _depositId1;
+ (_depositAmount1, _depositId1) =
+ _boundMintAndStake(_depositor1, _depositAmount1, _delegatee, _beneficiary1);
+ _withdrawalAmount1 = uint96(bound(_withdrawalAmount1, 0, _depositAmount1));
+
+ UniStaker.DepositIdentifier _depositId2;
+ (_depositAmount2, _depositId2) =
+ _boundMintAndStake(_depositor2, _depositAmount2, _delegatee, _beneficiary2);
+ _withdrawalAmount2 = uint96(bound(_withdrawalAmount2, 0, _depositAmount2));
+
+ vm.prank(_depositor1);
+ uniStaker.withdraw(_depositId1, _withdrawalAmount1);
+
+ assertEq(uniStaker.earningPower(_beneficiary1), _depositAmount1 - _withdrawalAmount1);
+ assertEq(uniStaker.earningPower(_beneficiary2), _depositAmount2);
+
+ vm.prank(_depositor2);
+ uniStaker.withdraw(_depositId2, _withdrawalAmount2);
+
+ assertEq(uniStaker.earningPower(_beneficiary1), _depositAmount1 - _withdrawalAmount1);
+ assertEq(uniStaker.earningPower(_beneficiary2), _depositAmount2 - _withdrawalAmount2);
+ }
+
+ function testFuzz_EmitsAnEventWhenThereIsAWithdrawal(
+ address _depositor,
+ uint96 _depositAmount,
+ address _delegatee,
+ uint96 _withdrawalAmount
+ ) public {
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _depositId) = _boundMintAndStake(_depositor, _depositAmount, _delegatee);
+ _withdrawalAmount = uint96(bound(_withdrawalAmount, 0, _depositAmount));
+
+ vm.expectEmit();
+ emit UniStaker.StakeWithdrawn(_depositId, _withdrawalAmount, _depositAmount - _withdrawalAmount);
+
+ vm.prank(_depositor);
+ uniStaker.withdraw(_depositId, _withdrawalAmount);
+ }
+
+ function testFuzz_RevertIf_TheWithdrawerIsNotTheDepositor(
+ address _depositor,
+ uint96 _amount,
+ address _delegatee,
+ address _notDepositor
+ ) public {
+ UniStaker.DepositIdentifier _depositId;
+ (_amount, _depositId) = _boundMintAndStake(_depositor, _amount, _delegatee);
+ vm.assume(_depositor != _notDepositor);
+
+ vm.prank(_notDepositor);
+ vm.expectRevert(
+ abi.encodeWithSelector(
+ UniStaker.UniStaker__Unauthorized.selector, bytes32("not owner"), _notDepositor
+ )
+ );
+ uniStaker.withdraw(_depositId, _amount);
+ }
+
+ function testFuzz_RevertIf_TheWithdrawalAmountIsGreaterThanTheBalance(
+ address _depositor,
+ uint96 _amount,
+ uint96 _amountOver,
+ address _delegatee
+ ) public {
+ UniStaker.DepositIdentifier _depositId;
+ (_amount, _depositId) = _boundMintAndStake(_depositor, _amount, _delegatee);
+ _amountOver = uint96(bound(_amountOver, 1, type(uint128).max));
+
+ vm.prank(_depositor);
+ vm.expectRevert();
+ uniStaker.withdraw(_depositId, _amount + _amountOver);
+ }
+}
+
+contract WithdrawOnBehalf is UniStakerTest {
+ using stdStorage for StdStorage;
+
+ function testFuzz_WithdrawOnBehalfOfDepositor(
+ uint256 _depositorPrivateKey,
+ address _sender,
+ uint96 _depositAmount,
+ address _delegatee,
+ address _beneficiary,
+ uint96 _withdrawAmount,
+ uint256 _currentNonce,
+ uint256 _deadline
+ ) public {
+ vm.assume(_delegatee != address(0) && _beneficiary != address(0) && _sender != address(0));
+ _deadline = bound(_deadline, block.timestamp, type(uint256).max);
+ _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18);
+ address _depositor = vm.addr(_depositorPrivateKey);
+
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary);
+ UniStaker.Deposit memory _deposit = _fetchDeposit(_depositId);
+ _withdrawAmount = uint96(bound(_withdrawAmount, 0, _depositAmount));
+
+ stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_depositor).checked_write(
+ _currentNonce
+ );
+
+ bytes32 _message = keccak256(
+ abi.encode(
+ uniStaker.WITHDRAW_TYPEHASH(),
+ _depositId,
+ _withdrawAmount,
+ _depositor,
+ uniStaker.nonces(_depositor),
+ _deadline
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message));
+ bytes memory _signature = _sign(_depositorPrivateKey, _messageHash);
+
+ vm.prank(_sender);
+ uniStaker.withdrawOnBehalf(_depositId, _withdrawAmount, _depositor, _deadline, _signature);
+
+ _deposit = _fetchDeposit(_depositId);
+
+ assertEq(_deposit.balance, _depositAmount - _withdrawAmount);
+ }
+
+ function testFuzz_RevertIf_WrongNonceIsUsed(
+ uint256 _depositorPrivateKey,
+ address _sender,
+ uint96 _depositAmount,
+ address _delegatee,
+ address _beneficiary,
+ uint96 _withdrawAmount,
+ uint256 _currentNonce,
+ uint256 _suppliedNonce,
+ uint256 _deadline
+ ) public {
+ vm.assume(_currentNonce != _suppliedNonce);
+ vm.assume(_delegatee != address(0) && _beneficiary != address(0) && _sender != address(0));
+ _deadline = bound(_deadline, block.timestamp, type(uint256).max);
+ _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18);
+ address _depositor = vm.addr(_depositorPrivateKey);
+ _depositAmount = _boundMintAmount(_depositAmount);
+ _mintGovToken(_depositor, _depositAmount);
+ stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_depositor).checked_write(
+ _currentNonce
+ );
+
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary);
+
+ bytes32 _message = keccak256(
+ abi.encode(
+ uniStaker.WITHDRAW_TYPEHASH(),
+ _depositId,
+ _withdrawAmount,
+ _depositor,
+ _suppliedNonce,
+ _deadline
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message));
+ bytes memory _signature = _sign(_depositorPrivateKey, _messageHash);
+
+ vm.expectRevert(UniStaker.UniStaker__InvalidSignature.selector);
+ vm.prank(_sender);
+ uniStaker.withdrawOnBehalf(_depositId, _withdrawAmount, _depositor, _deadline, _signature);
+ }
+
+ function testFuzz_RevertIf_DeadlineExpired(
+ uint256 _depositorPrivateKey,
+ address _sender,
+ uint96 _depositAmount,
+ address _delegatee,
+ address _beneficiary,
+ uint96 _withdrawAmount,
+ uint256 _currentNonce,
+ uint256 _deadline
+ ) public {
+ vm.assume(_delegatee != address(0) && _beneficiary != address(0) && _sender != address(0));
+ _deadline = bound(_deadline, 0, block.timestamp - 1);
+ _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18);
+ address _depositor = vm.addr(_depositorPrivateKey);
+ _depositAmount = _boundMintAmount(_depositAmount);
+ _mintGovToken(_depositor, _depositAmount);
+ stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_depositor).checked_write(
+ _currentNonce
+ );
+
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary);
+
+ bytes32 _message = keccak256(
+ abi.encode(
+ uniStaker.WITHDRAW_TYPEHASH(),
+ _depositId,
+ _withdrawAmount,
+ _depositor,
+ _currentNonce,
+ _deadline
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message));
+ bytes memory _signature = _sign(_depositorPrivateKey, _messageHash);
+
+ vm.expectRevert(UniStaker.UniStaker__ExpiredDeadline.selector);
+ vm.prank(_sender);
+ uniStaker.withdrawOnBehalf(_depositId, _withdrawAmount, _depositor, _deadline, _signature);
+ }
+
+ function testFuzz_RevertIf_DepositorIsNotDepositOwner(
+ address _sender,
+ address _depositor,
+ address _notDepositor,
+ uint96 _amount,
+ address _delegatee,
+ address _beneficiary,
+ uint96 _withdrawAmount,
+ uint256 _deadline,
+ bytes memory _signature
+ ) public {
+ vm.assume(
+ _delegatee != address(0) && _beneficiary != address(0) && _sender != address(0)
+ && _depositor != _notDepositor
+ );
+ _deadline = bound(_deadline, block.timestamp, type(uint256).max);
+ _amount = _boundMintAmount(_amount);
+ _mintGovToken(_depositor, _amount);
+
+ UniStaker.DepositIdentifier _depositId;
+ (_amount, _depositId) = _boundMintAndStake(_depositor, _amount, _delegatee, _beneficiary);
+
+ vm.expectRevert(
+ abi.encodeWithSelector(
+ UniStaker.UniStaker__Unauthorized.selector, bytes32("not owner"), _notDepositor
+ )
+ );
+ vm.prank(_sender);
+ uniStaker.withdrawOnBehalf(_depositId, _withdrawAmount, _notDepositor, _deadline, _signature);
+ }
+
+ function testFuzz_RevertIf_InvalidSignatureIsPassed(
+ uint256 _depositorPrivateKey,
+ address _sender,
+ uint96 _depositAmount,
+ address _delegatee,
+ address _beneficiary,
+ uint96 _withdrawAmount,
+ uint256 _currentNonce,
+ uint256 _randomSeed,
+ uint256 _deadline
+ ) public {
+ vm.assume(_delegatee != address(0) && _beneficiary != address(0) && _sender != address(0));
+ _deadline = bound(_deadline, block.timestamp, type(uint256).max);
+ _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18);
+ address _depositor = vm.addr(_depositorPrivateKey);
+ _depositAmount = _boundMintAmount(_depositAmount);
+ _mintGovToken(_depositor, _depositAmount);
+ stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_depositor).checked_write(
+ _currentNonce
+ );
+
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary);
+
+ bytes32 _message = keccak256(
+ abi.encode(
+ uniStaker.WITHDRAW_TYPEHASH(),
+ _depositId,
+ _withdrawAmount,
+ _depositor,
+ uniStaker.nonces(_depositor),
+ _deadline
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message));
+
+ // Here we use `_randomSeed` as an arbitrary source of randomness to replace a legit parameter
+ // with an attack-like one.
+ if (_randomSeed % 4 == 0) {
+ _withdrawAmount = uint96(uint256(keccak256(abi.encode(_withdrawAmount))));
+ } else if (_randomSeed % 4 == 1) {
+ _messageHash = _modifyMessage(_messageHash, uint256(keccak256(abi.encode(_randomSeed))));
+ } else if (_randomSeed % 4 == 2) {
+ _deadline = uint256(keccak256(abi.encode(_deadline)));
+ }
+ bytes memory _signature = _sign(_depositorPrivateKey, _messageHash);
+ if (_randomSeed % 4 == 3) _signature = _modifySignature(_signature, _randomSeed);
+
+ vm.expectRevert(UniStaker.UniStaker__InvalidSignature.selector);
+ vm.prank(_sender);
+ uniStaker.withdrawOnBehalf(_depositId, _withdrawAmount, _depositor, _deadline, _signature);
+ }
+}
+
+contract SetRewardNotifier is UniStakerTest {
+ function testFuzz_AllowsAdminToSetRewardNotifier(address _rewardNotifier, bool _isEnabled) public {
+ vm.prank(admin);
+ uniStaker.setRewardNotifier(_rewardNotifier, _isEnabled);
+
+ assertEq(uniStaker.isRewardNotifier(_rewardNotifier), _isEnabled);
+ }
+
+ function test_AllowsTheAdminToDisableAnActiveRewardNotifier() public {
+ vm.prank(admin);
+ uniStaker.setRewardNotifier(rewardNotifier, false);
+
+ assertFalse(uniStaker.isRewardNotifier(rewardNotifier));
+ }
+
+ function testFuzz_EmitsEventWhenRewardNotifierIsSet(address _rewardNotifier, bool _isEnabled)
+ public
+ {
+ vm.expectEmit();
+ emit RewardNotifierSet(_rewardNotifier, _isEnabled);
+ vm.prank(admin);
+ uniStaker.setRewardNotifier(_rewardNotifier, _isEnabled);
+ }
+
+ function testFuzz_RevertIf_TheCallerIsNotTheAdmin(
+ address _notAdmin,
+ address _newRewardNotifier,
+ bool _isEnabled
+ ) public {
+ vm.assume(_notAdmin != uniStaker.admin());
+
+ vm.prank(_notAdmin);
+ vm.expectRevert(
+ abi.encodeWithSelector(
+ UniStaker.UniStaker__Unauthorized.selector, bytes32("not admin"), _notAdmin
+ )
+ );
+ uniStaker.setRewardNotifier(_newRewardNotifier, _isEnabled);
+ }
+}
+
+contract SetAdmin is UniStakerTest {
+ function testFuzz_AllowsAdminToSetAdmin(address _newAdmin) public {
+ vm.assume(_newAdmin != address(0));
+
+ vm.prank(admin);
+ uniStaker.setAdmin(_newAdmin);
+
+ assertEq(uniStaker.admin(), _newAdmin);
+ }
+
+ function testFuzz_EmitsEventWhenAdminIsSet(address _newAdmin) public {
+ vm.assume(_newAdmin != address(0));
+
+ vm.expectEmit();
+ emit AdminSet(admin, _newAdmin);
+
+ vm.prank(admin);
+ uniStaker.setAdmin(_newAdmin);
+ }
+
+ function testFuzz_RevertIf_TheCallerIsNotTheAdmin(address _notAdmin, address _newAdmin) public {
+ vm.assume(_notAdmin != uniStaker.admin());
+
+ vm.prank(_notAdmin);
+ vm.expectRevert(
+ abi.encodeWithSelector(
+ UniStaker.UniStaker__Unauthorized.selector, bytes32("not admin"), _notAdmin
+ )
+ );
+ uniStaker.setAdmin(_newAdmin);
+ }
+
+ function test_RevertIf_NewAdminAddressIsZeroAddress() public {
+ vm.prank(admin);
+ vm.expectRevert(UniStaker.UniStaker__InvalidAddress.selector);
+ uniStaker.setAdmin(address(0));
+ }
+}
+
+contract InvalidateNonce is UniStakerTest {
+ using stdStorage for StdStorage;
+
+ function testFuzz_SuccessfullyIncrementsTheNonceOfTheSender(
+ address _caller,
+ uint256 _initialNonce
+ ) public {
+ vm.assume(_caller != address(0));
+ vm.assume(_initialNonce != type(uint256).max);
+
+ stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_caller).checked_write(
+ _initialNonce
+ );
+
+ vm.prank(_caller);
+ uniStaker.invalidateNonce();
+
+ uint256 currentNonce = uniStaker.nonces(_caller);
+
+ assertEq(currentNonce, _initialNonce + 1, "Current nonce is incorrect");
+ }
+
+ function testFuzz_IncreasesTheNonceByTwoWhenCalledTwice(address _caller, uint256 _initialNonce)
+ public
+ {
+ vm.assume(_caller != address(0));
+ _initialNonce = bound(_initialNonce, 0, type(uint256).max - 2);
+
+ stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_caller).checked_write(
+ _initialNonce
+ );
+
+ vm.prank(_caller);
+ uniStaker.invalidateNonce();
+
+ vm.prank(_caller);
+ uniStaker.invalidateNonce();
+
+ uint256 currentNonce = uniStaker.nonces(_caller);
+
+ assertEq(currentNonce, _initialNonce + 2, "Current nonce is incorrect");
+ }
+}
+
+contract UniStakerRewardsTest is UniStakerTest {
+ // Helper methods for dumping contract state related to rewards calculation for debugging
+ function __dumpDebugGlobalRewards() public view {
+ console2.log("reward balance");
+ console2.log(rewardToken.balanceOf(address(uniStaker)));
+ console2.log("rewardDuration");
+ console2.log(uniStaker.REWARD_DURATION());
+ console2.log("rewardEndTime");
+ console2.log(uniStaker.rewardEndTime());
+ console2.log("lastCheckpointTime");
+ console2.log(uniStaker.lastCheckpointTime());
+ console2.log("totalStake");
+ console2.log(uniStaker.totalStaked());
+ console2.log("scaledRewardRate");
+ console2.log(uniStaker.scaledRewardRate());
+ console2.log("block.timestamp");
+ console2.log(block.timestamp);
+ console2.log("rewardPerTokenAccumulatedCheckpoint");
+ console2.log(uniStaker.rewardPerTokenAccumulatedCheckpoint());
+ console2.log("lastTimeRewardDistributed()");
+ console2.log(uniStaker.lastTimeRewardDistributed());
+ console2.log("rewardPerTokenAccumulated()");
+ console2.log(uniStaker.rewardPerTokenAccumulated());
+ console2.log("-----------------------------------------------");
+ }
+
+ function __dumpDebugDepositorRewards(address _depositor) public view {
+ console2.log("earningPower[_depositor]");
+ console2.log(uniStaker.earningPower(_depositor));
+ console2.log("beneficiaryRewardPerTokenCheckpoint[_depositor]");
+ console2.log(uniStaker.beneficiaryRewardPerTokenCheckpoint(_depositor));
+ console2.log("scaledUnclaimedRewardCheckpoint[_depositor]");
+ console2.log(uniStaker.scaledUnclaimedRewardCheckpoint(_depositor));
+ console2.log("unclaimedReward(_depositor)");
+ console2.log(uniStaker.unclaimedReward(_depositor));
+ console2.log("-----------------------------------------------");
+ }
+
+ function _jumpAheadByPercentOfRewardDuration(uint256 _percent) public {
+ uint256 _seconds = (_percent * uniStaker.REWARD_DURATION()) / 100;
+ _jumpAhead(_seconds);
+ }
+
+ function _boundToRealisticReward(uint256 _rewardAmount)
+ public
+ pure
+ returns (uint256 _boundedRewardAmount)
+ {
+ _boundedRewardAmount = bound(_rewardAmount, 200e6, 10_000_000e18);
+ }
+
+ function _boundToRealisticStakeAndReward(uint96 _stakeAmount, uint256 _rewardAmount)
+ public
+ pure
+ returns (uint96 _boundedStakeAmount, uint256 _boundedRewardAmount)
+ {
+ _boundedStakeAmount = _boundToRealisticStake(_stakeAmount);
+ _boundedRewardAmount = _boundToRealisticReward(_rewardAmount);
+ }
+
+ function _mintTransferAndNotifyReward(uint256 _amount) public {
+ rewardToken.mint(rewardNotifier, _amount);
+
+ vm.startPrank(rewardNotifier);
+ rewardToken.transfer(address(uniStaker), _amount);
+ uniStaker.notifyRewardAmount(_amount);
+ vm.stopPrank();
+ }
+
+ function _mintTransferAndNotifyReward(address _rewardNotifier, uint256 _amount) public {
+ vm.assume(_rewardNotifier != address(0));
+ rewardToken.mint(_rewardNotifier, _amount);
+
+ vm.startPrank(_rewardNotifier);
+ rewardToken.transfer(address(uniStaker), _amount);
+ uniStaker.notifyRewardAmount(_amount);
+ vm.stopPrank();
+ }
+}
+
+contract NotifyRewardAmount is UniStakerRewardsTest {
+ function testFuzz_UpdatesTheRewardRate(uint256 _amount) public {
+ _amount = _boundToRealisticReward(_amount);
+ _mintTransferAndNotifyReward(_amount);
+
+ uint256 _expectedRewardRate = (SCALE_FACTOR * _amount) / uniStaker.REWARD_DURATION();
+ assertEq(uniStaker.scaledRewardRate(), _expectedRewardRate);
+ }
+
+ function testFuzz_UpdatesTheRewardRateOnASecondCall(uint256 _amount1, uint256 _amount2) public {
+ _amount1 = _boundToRealisticReward(_amount1);
+ _amount2 = _boundToRealisticReward(_amount2);
+
+ _mintTransferAndNotifyReward(_amount1);
+ uint256 _expectedRewardRate = (SCALE_FACTOR * _amount1) / uniStaker.REWARD_DURATION();
+ assertEq(uniStaker.scaledRewardRate(), _expectedRewardRate);
+
+ _mintTransferAndNotifyReward(_amount2);
+ _expectedRewardRate = (SCALE_FACTOR * (_amount1 + _amount2)) / uniStaker.REWARD_DURATION();
+ assertLteWithinOneUnit(uniStaker.scaledRewardRate(), _expectedRewardRate);
+ }
+
+ function testFuzz_UpdatesTheAccumulatorTimestamps(uint256 _amount, uint256 _jumpTime) public {
+ _amount = _boundToRealisticReward(_amount);
+ _jumpTime = bound(_jumpTime, 0, 50_000 days); // prevent overflow in timestamps
+ uint256 _futureTimestamp = block.timestamp + _jumpTime;
+ _jumpAhead(_jumpTime);
+
+ _mintTransferAndNotifyReward(_amount);
+ uint256 _expectedFinishTimestamp = _futureTimestamp + uniStaker.REWARD_DURATION();
+
+ assertEq(uniStaker.lastCheckpointTime(), _futureTimestamp);
+ assertEq(uniStaker.rewardEndTime(), _expectedFinishTimestamp);
+ }
+
+ function testFuzz_UpdatesTheCheckpointedRewardPerTokenAccumulator(
+ address _depositor,
+ address _delegatee,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount,
+ uint256 _durationPercent
+ ) public {
+ // In order to force calculation of a non-zero, there must be some staked supply, so we do
+ // that deposit first
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+ _boundMintAndStake(_depositor, _stakeAmount, _delegatee);
+ // We will jump ahead by some percentage of the duration
+ _durationPercent = bound(_durationPercent, 1, 100);
+
+ // Now the contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // Some time elapses
+ _jumpAheadByPercentOfRewardDuration(_durationPercent);
+ // We make another reward which should write the non-zero reward amount
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // Sanity check on our test assumptions
+ require(
+ uniStaker.rewardPerTokenAccumulated() != 0,
+ "Broken test assumption: expecting a non-zero reward accumulator"
+ );
+
+ // We are not testing the calculation of the reward amount, but only that the value in storage
+ // has been updated on reward notification and thus matches the "live" calculation.
+ assertEq(uniStaker.rewardPerTokenAccumulatedCheckpoint(), uniStaker.rewardPerTokenAccumulated());
+ }
+
+ function testFuzz_AllowsMultipleApprovedRewardNotifiersToNotifyOfRewards(
+ uint256 _amount1,
+ uint256 _amount2,
+ uint256 _amount3,
+ address _rewardNotifier1,
+ address _rewardNotifier2,
+ address _rewardNotifier3
+ ) public {
+ _amount1 = _boundToRealisticReward(_amount1);
+ _amount2 = _boundToRealisticReward(_amount2);
+ _amount3 = _boundToRealisticReward(_amount3);
+
+ vm.startPrank(admin);
+ uniStaker.setRewardNotifier(_rewardNotifier1, true);
+ uniStaker.setRewardNotifier(_rewardNotifier2, true);
+ uniStaker.setRewardNotifier(_rewardNotifier3, true);
+ vm.stopPrank();
+
+ // The first notifier notifies
+ _mintTransferAndNotifyReward(_rewardNotifier1, _amount1);
+
+ // The second notifier notifies
+ _mintTransferAndNotifyReward(_rewardNotifier2, _amount2);
+
+ // The third notifier notifies
+ _mintTransferAndNotifyReward(_rewardNotifier3, _amount3);
+ uint256 _expectedRewardRate =
+ (SCALE_FACTOR * (_amount1 + _amount2 + _amount3)) / uniStaker.REWARD_DURATION();
+ // because we summed 3 amounts, the rounding error can be as much as 2 units
+ assertApproxEqAbs(uniStaker.scaledRewardRate(), _expectedRewardRate, 2);
+ assertLe(uniStaker.scaledRewardRate(), _expectedRewardRate);
+ }
+
+ function testFuzz_EmitsAnEventWhenRewardsAreNotified(uint256 _amount) public {
+ _amount = _boundToRealisticReward(_amount);
+ rewardToken.mint(rewardNotifier, _amount);
+
+ vm.startPrank(rewardNotifier);
+ rewardToken.transfer(address(uniStaker), _amount);
+
+ vm.expectEmit();
+ emit UniStaker.RewardNotified(_amount, rewardNotifier);
+
+ uniStaker.notifyRewardAmount(_amount);
+ vm.stopPrank();
+ }
+
+ function testFuzz_RevertIf_CallerIsNotTheRewardNotifier(uint256 _amount, address _notNotifier)
+ public
+ {
+ vm.assume(!uniStaker.isRewardNotifier(_notNotifier) && _notNotifier != address(0));
+ _amount = _boundToRealisticReward(_amount);
+
+ rewardToken.mint(_notNotifier, _amount);
+
+ vm.startPrank(_notNotifier);
+ rewardToken.transfer(address(uniStaker), _amount);
+ vm.expectRevert(
+ abi.encodeWithSelector(
+ UniStaker.UniStaker__Unauthorized.selector, bytes32("not notifier"), _notNotifier
+ )
+ );
+ uniStaker.notifyRewardAmount(_amount);
+ vm.stopPrank();
+ }
+
+ function testFuzz_RevertIf_RewardAmountIsTooSmall(uint256 _amount) public {
+ // If the amount is less than the rewards duration the reward rate will be truncated to 0
+ _amount = bound(_amount, 0, uniStaker.REWARD_DURATION() - 1);
+ rewardToken.mint(rewardNotifier, _amount);
+
+ vm.startPrank(rewardNotifier);
+ rewardToken.transfer(address(uniStaker), _amount);
+ vm.expectRevert(UniStaker.UniStaker__InvalidRewardRate.selector);
+ uniStaker.notifyRewardAmount(_amount);
+ vm.stopPrank();
+ }
+
+ function testFuzz_RevertIf_InsufficientRewardsAreTransferredToContract(
+ uint256 _amount,
+ uint256 _transferPercent
+ ) public {
+ _amount = _boundToRealisticReward(_amount);
+ // Transfer (at most) 99% of the reward amount. We calculate as a percentage rather than simply
+ // an amount - 1 because rounding errors when calculating the reward rate, which favor the
+ // staking contract can actually allow for something just below the amount to meet the criteria
+ _transferPercent = _bound(_transferPercent, 1, 99);
+
+ uint256 _transferAmount = _percentOf(_amount, _transferPercent);
+ rewardToken.mint(rewardNotifier, _amount);
+
+ vm.startPrank(rewardNotifier);
+ // Something less than the supposed reward is sent
+ rewardToken.transfer(address(uniStaker), _transferAmount);
+ // The reward notification should revert because the contract doesn't have enough tokens
+ vm.expectRevert(UniStaker.UniStaker__InsufficientRewardBalance.selector);
+ uniStaker.notifyRewardAmount(_amount);
+ vm.stopPrank();
+ }
+}
+
+contract LastTimeRewardDistributed is UniStakerRewardsTest {
+ function test_ReturnsZeroBeforeARewardNotificationHasOccurred() public view {
+ assertEq(uniStaker.lastTimeRewardDistributed(), 0);
+ }
+
+ function testFuzz_ReturnsTheBlockTimestampAfterARewardNotificationButBeforeTheRewardDurationElapses(
+ uint256 _amount,
+ uint256 _durationPercent
+ ) public {
+ _amount = _boundToRealisticReward(_amount);
+ _mintTransferAndNotifyReward(_amount);
+
+ _durationPercent = bound(_durationPercent, 0, 100);
+ _jumpAheadByPercentOfRewardDuration(_durationPercent);
+
+ assertEq(uniStaker.lastTimeRewardDistributed(), block.timestamp);
+ }
+
+ function testFuzz_ReturnsTheEndOfTheRewardDurationIfItHasFullyElapsed(
+ uint256 _amount,
+ uint256 _durationPercent
+ ) public {
+ _amount = _boundToRealisticReward(_amount);
+ _mintTransferAndNotifyReward(_amount);
+
+ uint256 _durationEnd = block.timestamp + uniStaker.REWARD_DURATION();
+
+ _durationPercent = bound(_durationPercent, 101, 1000);
+ _jumpAheadByPercentOfRewardDuration(_durationPercent);
+
+ assertEq(uniStaker.lastTimeRewardDistributed(), _durationEnd);
+ }
+
+ function testFuzz_ReturnsTheBlockTimestampWhileWithinTheDurationOfASecondReward(
+ uint256 _amount,
+ uint256 _durationPercent1,
+ uint256 _durationPercent2
+ ) public {
+ _amount = _boundToRealisticReward(_amount);
+ // Notification of first reward
+ _mintTransferAndNotifyReward(_amount);
+
+ // Some time elapses, which could be more or less than the duration
+ _durationPercent1 = bound(_durationPercent1, 0, 200);
+ _jumpAheadByPercentOfRewardDuration(_durationPercent1);
+
+ // Notification of the second reward
+ _mintTransferAndNotifyReward(_amount);
+
+ // Some more time elapses, this time no more than the duration
+ _durationPercent2 = bound(_durationPercent2, 0, 100);
+ _jumpAheadByPercentOfRewardDuration(_durationPercent2);
+
+ assertEq(uniStaker.lastTimeRewardDistributed(), block.timestamp);
+ }
+
+ function testFuzz_ReturnsTheEndOfTheSecondRewardDurationAfterTwoRewards(
+ uint256 _amount,
+ uint256 _durationPercent1,
+ uint256 _durationPercent2
+ ) public {
+ _amount = _boundToRealisticReward(_amount);
+ // Notification of first reward
+ _mintTransferAndNotifyReward(_amount);
+
+ // Some time elapses, which could be more or less than the duration
+ _durationPercent1 = bound(_durationPercent1, 0, 200);
+ _jumpAheadByPercentOfRewardDuration(_durationPercent1);
+
+ // Notification of the second reward
+ _mintTransferAndNotifyReward(_amount);
+ uint256 _durationEnd = block.timestamp + uniStaker.REWARD_DURATION();
+
+ // Some more time elapses, placing us beyond the duration of the second reward
+ _durationPercent2 = bound(_durationPercent2, 101, 1000);
+ _jumpAheadByPercentOfRewardDuration(_durationPercent2);
+
+ assertEq(uniStaker.lastTimeRewardDistributed(), _durationEnd);
+ }
+}
+
+contract RewardPerTokenAccumulated is UniStakerRewardsTest {
+ function testFuzz_ReturnsZeroIfThereHasNeverBeenAReward(
+ address _depositor1,
+ address _depositor2,
+ uint96 _stakeAmount1,
+ uint96 _stakeAmount2,
+ uint96 _withdrawAmount,
+ uint96 _stakeMoreAmount,
+ uint256 _durationPercent1
+ ) public {
+ // We'll perform a few arbitrary actions, such as staking, withdrawing, and staking more.
+ // No matter these actions, the reward per token should always be zero since there has never
+ // been the notification of a reward.
+
+ // Derive and bound the values we'll use for jumping ahead in time
+ uint256 _durationPercent2 = uint256(keccak256(abi.encode(_durationPercent1)));
+ uint256 _durationPercent3 = uint256(keccak256(abi.encode(_durationPercent2)));
+ _durationPercent1 = bound(_durationPercent1, 0, 200);
+ _durationPercent2 = bound(_durationPercent2, 0, 200);
+ _durationPercent3 = bound(_durationPercent3, 0, 200);
+
+ // First deposit
+ UniStaker.DepositIdentifier _depositId1;
+ (_stakeAmount1, _depositId1) = _boundMintAndStake(_depositor1, _stakeAmount1, _depositor1);
+ _jumpAheadByPercentOfRewardDuration(_durationPercent1);
+
+ // Second deposit
+ (, UniStaker.DepositIdentifier _depositId2) =
+ _boundMintAndStake(_depositor2, _stakeAmount2, _depositor2);
+ _jumpAheadByPercentOfRewardDuration(_durationPercent2);
+
+ // First depositor withdraws some stake
+ _withdrawAmount = uint96(bound(_withdrawAmount, 0, _stakeAmount1));
+ vm.prank(_depositor1);
+ uniStaker.withdraw(_depositId1, _withdrawAmount);
+ _jumpAheadByPercentOfRewardDuration(_durationPercent3);
+
+ // Second depositor adds some stake
+ _stakeMoreAmount = _boundToRealisticStake(_stakeMoreAmount);
+ govToken.mint(_depositor2, _stakeMoreAmount);
+ vm.startPrank(_depositor2);
+ govToken.approve(address(uniStaker), _stakeMoreAmount);
+ uniStaker.stakeMore(_depositId2, _stakeMoreAmount);
+ vm.stopPrank();
+
+ // Reward per token is still just 0
+ assertEq(uniStaker.rewardPerTokenAccumulated(), 0);
+ }
+
+ function testFuzz_DoesNotChangeWhileNoTokensAreStaked(
+ address _depositor,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount,
+ uint256 _durationPercent1,
+ uint256 _durationPercent2,
+ uint256 _durationPercent3
+ ) public {
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+ _durationPercent1 = _bound(_durationPercent1, 0, 100);
+ _durationPercent2 = _bound(_durationPercent2, 0, 200);
+ _durationPercent3 = _bound(_durationPercent3, 0, 200);
+
+ // A user deposits staking tokens
+ (, UniStaker.DepositIdentifier _depositId) =
+ _boundMintAndStake(_depositor, _stakeAmount, _depositor);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // Some time less than the full duration passes
+ _jumpAheadByPercentOfRewardDuration(_durationPercent1);
+ // We archive the value here before withdrawing the stake
+ uint256 _valueBeforeStakeIsWithdrawn = uniStaker.rewardPerTokenAccumulated();
+ // All of the stake is withdrawn
+ vm.prank(_depositor);
+ uniStaker.withdraw(_depositId, _stakeAmount);
+ require(uniStaker.totalStaked() == 0, "Test Invariant violated: expected 0 stake");
+ // Some additional time passes
+ _jumpAheadByPercentOfRewardDuration(_durationPercent2);
+ // The contract is notified of another reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // Even more time passes
+ _jumpAheadByPercentOfRewardDuration(_durationPercent2);
+
+ // The value should not have changed since the amount staked became zero
+ assertEq(uniStaker.rewardPerTokenAccumulated(), _valueBeforeStakeIsWithdrawn);
+ }
+
+ function testFuzz_DoesNotChangeWhileNoRewardsAreBeingDistributed(
+ address _depositor,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount,
+ uint96 _withdrawAmount,
+ uint256 _durationPercent1,
+ uint256 _durationPercent2,
+ uint256 _durationPercent3
+ ) public {
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+ _withdrawAmount = uint96(_bound(_withdrawAmount, 0, _stakeAmount));
+ _durationPercent1 = _bound(_durationPercent1, 0, 200);
+ _durationPercent2 = _bound(_durationPercent2, 0, 200);
+ _durationPercent3 = _bound(_durationPercent3, 0, 200);
+
+ // A user deposits staking tokens
+ (, UniStaker.DepositIdentifier _depositId) =
+ _boundMintAndStake(_depositor, _stakeAmount, _depositor);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // The full duration of the reward notification passes, meaning all rewards have dripped out
+ _jumpAheadByPercentOfRewardDuration(101);
+ // We archive the value here before anything else happens
+ uint256 _valueAfterRewardDurationCompletes = uniStaker.rewardPerTokenAccumulated();
+ // Some additional time elapses
+ _jumpAheadByPercentOfRewardDuration(_durationPercent1);
+ // Some amount of the stake is withdrawn
+ vm.prank(_depositor);
+ uniStaker.withdraw(_depositId, _withdrawAmount);
+ // Some additional time elapses
+ _jumpAheadByPercentOfRewardDuration(_durationPercent1);
+ // The user makes another deposit
+ _boundMintAndStake(_depositor, _stakeAmount, _depositor);
+ // Even more time passes
+ _jumpAheadByPercentOfRewardDuration(_durationPercent3);
+
+ // The value should not have changed since the rewards stopped dripping out
+ assertEq(uniStaker.rewardPerTokenAccumulated(), _valueAfterRewardDurationCompletes);
+ }
+
+ function testFuzz_DoesNotChangeIfTimeDoesNotElapse(
+ address _depositor,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount,
+ uint96 _withdrawAmount,
+ uint256 _durationPercent1
+ ) public {
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+ _withdrawAmount = uint96(_bound(_withdrawAmount, 0, _stakeAmount));
+ _durationPercent1 = _bound(_durationPercent1, 0, 200);
+
+ // A user deposits staking tokens
+ (, UniStaker.DepositIdentifier _depositId) =
+ _boundMintAndStake(_depositor, _stakeAmount, _depositor);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // Some amount of time elapses
+ _jumpAheadByPercentOfRewardDuration(_durationPercent1);
+ // We archive the value here before anything else happens
+ uint256 _valueAfterTimeElapses = uniStaker.rewardPerTokenAccumulated();
+ // Some amount of the stake is withdrawn
+ vm.prank(_depositor);
+ uniStaker.withdraw(_depositId, _withdrawAmount);
+ // The contract is notified of another reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // The user makes another deposit
+ _boundMintAndStake(_depositor, _stakeAmount, _depositor);
+
+ // The value should not have changed since no additional time has elapsed
+ assertEq(uniStaker.rewardPerTokenAccumulated(), _valueAfterTimeElapses);
+ }
+
+ function testFuzz_AccruesTheCorrectValueWhenADepositorStakesForSomePortionOfAReward(
+ address _depositor,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount,
+ uint256 _durationPercent
+ ) public {
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+ _durationPercent = _bound(_durationPercent, 0, 100);
+
+ // A user deposits staking tokens
+ _boundMintAndStake(_depositor, _stakeAmount, _depositor);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // Some time less than the full duration passes
+ _jumpAheadByPercentOfRewardDuration(_durationPercent);
+
+ // The reward per token is the reward by the stake amount proportional to the elapsed time
+ uint256 _expected = _percentOf(_scaledDiv(_rewardAmount, _stakeAmount), _durationPercent);
+ assertLteWithinOnePercent(uniStaker.rewardPerTokenAccumulated(), _expected);
+ }
+
+ function testFuzz_AccruesTheCorrectValueWhenADepositorStakesAfterAReward(
+ address _depositor,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount,
+ uint256 _durationPercent
+ ) public {
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+ _durationPercent = _bound(_durationPercent, 0, 100);
+
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // Some time less than the full duration passes
+ _jumpAheadByPercentOfRewardDuration(_durationPercent);
+ // A user deposits staking tokens
+ _boundMintAndStake(_depositor, _stakeAmount, _depositor);
+ // The rest of the duration passes
+ _jumpAheadByPercentOfRewardDuration(100 - _durationPercent);
+
+ // The reward per token is the reward by the stake amount proportional to the elapsed time
+ uint256 _expected = _percentOf(_scaledDiv(_rewardAmount, _stakeAmount), 100 - _durationPercent);
+ assertLteWithinOnePercent(uniStaker.rewardPerTokenAccumulated(), _expected);
+ }
+
+ function testFuzz_AccruesTheCorrectValueWhenADepositorStakesForARewardDurationAndAnotherDepositorStakesForASecondRewardDuration(
+ address _depositor,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount,
+ uint256 _durationPercent
+ ) public {
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+ _durationPercent = _bound(_durationPercent, 100, 1000);
+
+ // A user deposits staking tokens
+ _boundMintAndStake(_depositor, _stakeAmount, _depositor);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // The full duration passes
+ _jumpAheadByPercentOfRewardDuration(_durationPercent);
+ // The amount of stake exactly doubles
+ _boundMintAndStake(_depositor, _stakeAmount, _depositor);
+ // The contract is notified of another reward of the same size
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // The full duration passes for the second reward
+ _jumpAheadByPercentOfRewardDuration(_durationPercent);
+
+ // We expect the sum of the reward over the stake, plus the reward over twice the stake
+ uint256 _expected =
+ _scaledDiv(_rewardAmount, _stakeAmount) + _scaledDiv(_rewardAmount, 2 * _stakeAmount);
+ assertLteWithinOnePercent(uniStaker.rewardPerTokenAccumulated(), _expected);
+ }
+
+ function testFuzz_AccruesTheCorrectValueWhenTwoDepositorsStakeAtDifferentTimesAndThereAreTwoRewards(
+ address _depositor,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount,
+ uint256 _durationPercent1,
+ uint256 _durationPercent2
+ ) public {
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+ _durationPercent1 = _bound(_durationPercent1, 0, 100);
+ _durationPercent2 = _bound(_durationPercent2, 0, 100);
+
+ // A user deposits staking tokens
+ _boundMintAndStake(_depositor, _stakeAmount, _depositor);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // Part of the reward duration passes
+ _jumpAheadByPercentOfRewardDuration(_durationPercent1);
+ // The amount of stake exactly doubles
+ _boundMintAndStake(_depositor, _stakeAmount, _depositor);
+ // The contract is notified of another reward of the same size
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // Some additional time elapses which is less than the total duration
+ _jumpAheadByPercentOfRewardDuration(_durationPercent2);
+
+ // During the first period, the value accrued is equal to to reward amount over the stake amount
+ // proportional to the time elapsed
+ uint256 _expectedDuration1 =
+ _percentOf(_scaledDiv(_rewardAmount, _stakeAmount), _durationPercent1);
+ // After the first period, some amount of the first reward remains to be distributed
+ uint256 _firstRewardRemainingAmount = _percentOf(_rewardAmount, 100 - _durationPercent1);
+ // During the second period, the remaining reward plus the next reward must be divided by the
+ // new staked total (2x)
+ uint256 _expectedDuration2 = _percentOf(
+ _scaledDiv(_firstRewardRemainingAmount + _rewardAmount, 2 * _stakeAmount), _durationPercent2
+ );
+ // The total expected value is the sum of the value accrued during these two periods
+ uint256 _expected = _expectedDuration1 + _expectedDuration2;
+ assertLteWithinOnePercent(uniStaker.rewardPerTokenAccumulated(), _expected);
+ }
+
+ function testFuzz_AccruesTheCorrectValueWhenADepositorStakesAndWithdrawsDuringARewardDuration(
+ address _depositor,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount,
+ uint96 _withdrawalAmount,
+ uint256 _durationPercent1,
+ uint256 _durationPercent2
+ ) public {
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+ _withdrawalAmount = uint96(_bound(_withdrawalAmount, 0, _stakeAmount - 1));
+ _durationPercent1 = _bound(_durationPercent1, 0, 100);
+ _durationPercent2 = _bound(_durationPercent2, 0, 100 - _durationPercent1);
+
+ UniStaker.DepositIdentifier _depositId;
+
+ // A user deposits staking tokens
+ (, _depositId) = _boundMintAndStake(_depositor, _stakeAmount, _depositor);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // Part of the reward duration passes
+ _jumpAheadByPercentOfRewardDuration(_durationPercent1);
+ // Some of the stake is withdrawn
+ vm.prank(_depositor);
+ uniStaker.withdraw(_depositId, _withdrawalAmount);
+ // More of the duration elapses
+ _jumpAheadByPercentOfRewardDuration(_durationPercent2);
+
+ uint256 _expectedDuration1 =
+ _percentOf(_scaledDiv(_rewardAmount, _stakeAmount), _durationPercent1);
+ uint256 _expectedDuration2 =
+ _percentOf(_scaledDiv(_rewardAmount, _stakeAmount - _withdrawalAmount), _durationPercent2);
+ uint256 _expected = _expectedDuration1 + _expectedDuration2;
+ assertLteWithinOnePercent(uniStaker.rewardPerTokenAccumulated(), _expected);
+ }
+
+ function testFuzz_AccruesTheCorrectValueWhenTwoDepositorsStakeDifferentAmountsAtDifferentTimesOverTwoRewards(
+ address _depositor1,
+ address _depositor2,
+ uint96 _stakeAmount1,
+ uint96 _stakeAmount2,
+ uint256 _rewardAmount,
+ uint256 _durationPercent1,
+ uint256 _durationPercent2,
+ uint256 _durationPercent3
+ ) public {
+ _stakeAmount1 = _boundToRealisticStake(_stakeAmount1);
+ _stakeAmount2 = _boundToRealisticStake(_stakeAmount2);
+ _rewardAmount = _boundToRealisticReward(_rewardAmount);
+ _durationPercent1 = _bound(_durationPercent1, 0, 100);
+ _durationPercent2 = _bound(_durationPercent2, 0, 100);
+ _durationPercent3 = _bound(_durationPercent2, 0, 100 - _durationPercent2);
+
+ // A user deposits staking tokens
+ _boundMintAndStake(_depositor1, _stakeAmount1, _depositor1);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // Part of the reward duration passes
+ _jumpAheadByPercentOfRewardDuration(_durationPercent1);
+ // The contract is notified of another reward, resetting the duration
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // Part of the new reward duration passes
+ _jumpAheadByPercentOfRewardDuration(_durationPercent2);
+ // Another user deposits a different number of staking tokens
+ _boundMintAndStake(_depositor2, _stakeAmount2, _depositor2);
+ // More of the reward duration passes
+ _jumpAheadByPercentOfRewardDuration(_durationPercent3);
+
+ // During the first time period, the expected value is the reward amount over the staked amount,
+ // proportional to the time elapsed
+ uint256 _expectedDuration1 =
+ _percentOf(_scaledDiv(_rewardAmount, _stakeAmount1), _durationPercent1);
+ // The rewards after the second notification are the remaining rewards plus the new rewards.
+ // We scale them up here to avoid losing precision in our expectation estimates.
+ uint256 _scaledRewardsAfterDuration1 = (
+ SCALE_FACTOR * _rewardAmount - _percentOf(SCALE_FACTOR * _rewardAmount, _durationPercent1)
+ ) + SCALE_FACTOR * _rewardAmount;
+ // During the second time period, the expected value is the new reward amount over the staked
+ // amount, proportional to the time elapsed
+ uint256 _expectedDuration2 =
+ _percentOf(_scaledRewardsAfterDuration1 / _stakeAmount1, _durationPercent2);
+ // During the third time period, the expected value is the reward amount over the new total
+ // staked amount, proportional to the time elapsed
+ uint256 _expectedDuration3 =
+ _percentOf(_scaledRewardsAfterDuration1 / (_stakeAmount1 + _stakeAmount2), _durationPercent3);
+
+ uint256 _expected = _expectedDuration1 + _expectedDuration2 + _expectedDuration3;
+ assertLteWithinOnePercent(uniStaker.rewardPerTokenAccumulated(), _expected);
+ }
+
+ function testFuzz_AccruesTheCorrectValueWhenAnArbitraryNumberOfDepositorsStakeDifferentAmountsOverTheCourseOfARewardDuration(
+ address _depositor,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount,
+ uint256 _durationPercent
+ ) public {
+ _rewardAmount = _boundToRealisticReward(_rewardAmount);
+
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+
+ uint256 _expected;
+ uint256 _totalStake;
+ uint256 _totalDurationPercent;
+
+ // Now we'll perform an arbitrary number of differently sized deposits
+ while (_totalDurationPercent < 100) {
+ // On each iteration we derive new values
+ _depositor = address(uint160(uint256(keccak256(abi.encode(_depositor)))));
+ _stakeAmount = uint96(uint256(keccak256(abi.encode(_stakeAmount))));
+ _stakeAmount = _boundToRealisticStake(_stakeAmount);
+ _durationPercent = uint256(keccak256(abi.encode(_durationPercent)));
+ // We make sure the duration jump on each iteration isn't so small that we slow the test
+ // down excessively, but also isn't so big we don't get at least a few iterations.
+ _durationPercent = bound(_durationPercent, 10, 33);
+
+ // A user deposits some staking tokens
+ _boundMintAndStake(_depositor, _stakeAmount, _depositor);
+ _totalStake += _stakeAmount;
+
+ // Part of the reward duration passes
+ _jumpAheadByPercentOfRewardDuration(_durationPercent);
+ _totalDurationPercent += _durationPercent;
+
+ if (_totalDurationPercent > 100) {
+ // If we've jumped ahead past the end of the duration, this will be the last iteration, so
+ // the only portion of the time elapsed that contributed to the accrued expected value is
+ // the portion before we reached 100% of the duration.
+ _durationPercent = 100 - (_totalDurationPercent - _durationPercent);
+ }
+
+ // At each iteration, we recalculate and check the accruing value
+ _expected += _percentOf(_scaledDiv(_rewardAmount, _totalStake), _durationPercent);
+ assertLteWithinOnePercent(uniStaker.rewardPerTokenAccumulated(), _expected);
+ }
+ }
+}
+
+contract UnclaimedReward is UniStakerRewardsTest {
+ function testFuzz_CalculatesCorrectEarningsForASingleDepositorThatStakesForFullDuration(
+ address _depositor,
+ address _delegatee,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount
+ ) public {
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+
+ // A user deposits staking tokens
+ _boundMintAndStake(_depositor, _stakeAmount, _delegatee);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // The full duration passes
+ _jumpAheadByPercentOfRewardDuration(101);
+
+ // The user should have earned all the rewards
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor), _rewardAmount);
+ }
+
+ function testFuzz_CalculatesCorrectEarningsWhenASingleDepositorAssignsABeneficiaryAndStakesForFullDuration(
+ address _depositor,
+ address _delegatee,
+ address _beneficiary,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount
+ ) public {
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+
+ // A user deposits staking tokens w/ a beneficiary
+ _boundMintAndStake(_depositor, _stakeAmount, _delegatee, _beneficiary);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // The full duration passes
+ _jumpAheadByPercentOfRewardDuration(101);
+
+ // The beneficiary should have earned all the rewards
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_beneficiary), _rewardAmount);
+ }
+
+ function testFuzz_CalculatesCorrectEarningsWhenASingleDepositorUpdatesTheirBeneficiary(
+ address _depositor,
+ address _delegatee,
+ address _beneficiary1,
+ address _beneficiary2,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount,
+ uint256 _percentDuration
+ ) public {
+ vm.assume(
+ _beneficiary1 != _beneficiary2 && _beneficiary1 != address(0) && _beneficiary2 != address(0)
+ );
+
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+ _percentDuration = bound(_percentDuration, 0, 100);
+
+ // A user deposits staking tokens w/ a beneficiary
+ (, UniStaker.DepositIdentifier _depositId) =
+ _boundMintAndStake(_depositor, _stakeAmount, _delegatee, _beneficiary1);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // Part of the rewards duration passes
+ _jumpAheadByPercentOfRewardDuration(_percentDuration);
+ // The depositor alters their beneficiary
+ vm.prank(_depositor);
+ uniStaker.alterBeneficiary(_depositId, _beneficiary2);
+ // The rest of the duration elapses
+ _jumpAheadByPercentOfRewardDuration(100 - _percentDuration);
+
+ // The beneficiary should have earned all the rewards
+ assertLteWithinOnePercent(
+ uniStaker.unclaimedReward(_beneficiary1), _percentOf(_rewardAmount, _percentDuration)
+ );
+ assertLteWithinOnePercent(
+ uniStaker.unclaimedReward(_beneficiary2), _percentOf(_rewardAmount, 100 - _percentDuration)
+ );
+ }
+
+ function testFuzz_CalculatesCorrectEarningsForASingleUserThatDepositsStakeForPartialDuration(
+ address _depositor,
+ address _delegatee,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount,
+ uint256 _durationPercent
+ ) public {
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+ _durationPercent = bound(_durationPercent, 0, 100);
+
+ // A user deposits staking tokens
+ _boundMintAndStake(_depositor, _stakeAmount, _delegatee);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // One third of the duration passes
+ _jumpAheadByPercentOfRewardDuration(_durationPercent);
+
+ // The user should have earned one third of the rewards
+ assertLteWithinOnePercent(
+ uniStaker.unclaimedReward(_depositor), _percentOf(_rewardAmount, _durationPercent)
+ );
+ }
+
+ function testFuzz_CalculatesCorrectEarningsForASingleUserThatDepositsPartiallyThroughTheDuration(
+ address _depositor,
+ address _delegatee,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount
+ ) public {
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // Two thirds of the duration time passes
+ _jumpAheadByPercentOfRewardDuration(66);
+ // A user deposits staking tokens
+ _boundMintAndStake(_depositor, _stakeAmount, _delegatee);
+ // The rest of the duration elapses
+ _jumpAheadByPercentOfRewardDuration(34);
+
+ // The user should have earned 1/3rd of the rewards
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor), _percentOf(_rewardAmount, 34));
+ }
+
+ function testFuzz_CalculatesCorrectEarningsForASingleUserStakeForPartialDurationWithABeneficiary(
+ address _depositor,
+ address _delegatee,
+ address _beneficiary,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount,
+ uint256 _durationPercent
+ ) public {
+ vm.assume(_beneficiary != address(0));
+
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+ _durationPercent = bound(_durationPercent, 0, 100);
+
+ // A user deposits staking tokens and assigns a beneficiary
+ _boundMintAndStake(_depositor, _stakeAmount, _delegatee, _beneficiary);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // Some portion of the duration passes
+ _jumpAheadByPercentOfRewardDuration(_durationPercent);
+
+ // The beneficiary should have earned a portion of the rewards equal to the amount of the
+ // duration that has passed
+ assertLteWithinOnePercent(
+ uniStaker.unclaimedReward(_beneficiary), _percentOf(_rewardAmount, _durationPercent)
+ );
+ }
+
+ function testFuzz_CalculatesCorrectEarningsForASingleUserThatDepositsPartiallyThroughTheDurationWithABeneficiary(
+ address _depositor,
+ address _delegatee,
+ address _beneficiary,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount
+ ) public {
+ vm.assume(_beneficiary != address(0));
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // Two thirds of the duration time passes
+ _jumpAheadByPercentOfRewardDuration(66);
+ // A user deposits staking tokens and assigns a beneficiary
+ _boundMintAndStake(_depositor, _stakeAmount, _delegatee, _beneficiary);
+ // The rest of the duration elapses
+ _jumpAheadByPercentOfRewardDuration(34);
+
+ // The beneficiary should have earned 1/3rd of the reward
+ assertLteWithinOnePercent(
+ uniStaker.unclaimedReward(_beneficiary), _percentOf(_rewardAmount, 34)
+ );
+ }
+
+ function testFuzz_CalculatesCorrectEarningsForASingleUserThatDepositsStakeForTheFullDurationWithNoNewRewards(
+ address _depositor,
+ address _delegatee,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount,
+ uint16 _noRewardsSkip
+ ) public {
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+
+ // A user deposits staking tokens
+ _boundMintAndStake(_depositor, _stakeAmount, _delegatee);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+
+ // The full duration passes
+ _jumpAheadByPercentOfRewardDuration(100);
+ // Time moves forward with no rewards
+ _jumpAheadByPercentOfRewardDuration(_noRewardsSkip);
+
+ // Send new rewards, which should have no impact on the amount earned until time elapses
+ _mintTransferAndNotifyReward(_rewardAmount);
+
+ // The user should have earned all the rewards
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor), _rewardAmount);
+ }
+
+ function testFuzz_CalculatesCorrectEarningsForASingleUserThatDepositsStakeForTheFullDurationWithDelayedReward(
+ address _depositor,
+ address _delegatee,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount,
+ uint16 _noRewardsSkip
+ ) public {
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+
+ // A user deposits staking tokens
+ _boundMintAndStake(_depositor, _stakeAmount, _delegatee);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+
+ // The full duration passes
+ _jumpAheadByPercentOfRewardDuration(100);
+ // Time moves forward with no rewards
+ _jumpAheadByPercentOfRewardDuration(_noRewardsSkip);
+
+ // Send new rewards
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // We end another full duration
+ _jumpAheadByPercentOfRewardDuration(100);
+
+ // The user should have earned all the rewards
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor), _rewardAmount * 2);
+ }
+
+ function testFuzz_CalculatesCorrectEarningsWhenASingleDepositorUpdatesTheirBeneficiaryWithNoNewRewards(
+ address _depositor,
+ address _delegatee,
+ address _beneficiary1,
+ address _beneficiary2,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount,
+ uint256 _percentDuration,
+ uint16 _noRewardsSkip
+ ) public {
+ vm.assume(
+ _beneficiary1 != _beneficiary2 && _beneficiary1 != address(0) && _beneficiary2 != address(0)
+ );
+
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+ _percentDuration = bound(_percentDuration, 0, 100);
+
+ // A user deposits staking tokens w/ a beneficiary
+ (, UniStaker.DepositIdentifier _depositId) =
+ _boundMintAndStake(_depositor, _stakeAmount, _delegatee, _beneficiary1);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // Part of the rewards duration passes
+ _jumpAheadByPercentOfRewardDuration(_percentDuration);
+
+ // The depositor alters their beneficiary
+ vm.prank(_depositor);
+ uniStaker.alterBeneficiary(_depositId, _beneficiary2);
+
+ // The rest of the duration elapses
+ _jumpAheadByPercentOfRewardDuration(100 - _percentDuration);
+
+ // Skip ahead with no rewards
+ _jumpAheadByPercentOfRewardDuration(_noRewardsSkip);
+
+ // Send new rewards, which should have no impact on the amount earned until time elapses
+ _mintTransferAndNotifyReward(_rewardAmount);
+
+ // The beneficiaries should have earned all the rewards for the first duration
+ assertLteWithinOnePercent(
+ uniStaker.unclaimedReward(_beneficiary1), _percentOf(_rewardAmount, _percentDuration)
+ );
+ assertLteWithinOnePercent(
+ uniStaker.unclaimedReward(_beneficiary2), _percentOf(_rewardAmount, 100 - _percentDuration)
+ );
+ }
+
+ function testFuzz_CalculatesCorrectEarningsForASingleUserThatDepositsStakeForTheFullDurationAndClaims(
+ address _depositor,
+ address _delegatee,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount,
+ uint256 _durationPercent
+ ) public {
+ vm.assume(_depositor != address(uniStaker));
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+ _durationPercent = bound(_durationPercent, 0, 100);
+
+ // A user deposits staking tokens
+ _boundMintAndStake(_depositor, _stakeAmount, _delegatee);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+
+ // The full duration passes
+ _jumpAheadByPercentOfRewardDuration(101);
+
+ // The depositor claims the rewards
+ vm.prank(_depositor);
+ uniStaker.claimReward();
+
+ // Send new rewards
+ _mintTransferAndNotifyReward(_rewardAmount);
+ _jumpAheadByPercentOfRewardDuration(_durationPercent);
+
+ uint256 balance = uniStaker.REWARD_TOKEN().balanceOf(address(_depositor));
+
+ // The depositors balance should reflect the first full duration
+ assertLteWithinOnePercent(balance, _rewardAmount);
+ // The depositor should have earned a portion of the rewards equal to the amount of the next
+ // duration that has passed.
+ assertLteWithinOnePercent(
+ uniStaker.unclaimedReward(_depositor), _percentOf(_rewardAmount, _durationPercent)
+ );
+ }
+
+ function testFuzz_CalculatesCorrectEarningsForASingleUserThatDepositsStakeForThePartialDurationAndClaims(
+ address _depositor,
+ address _delegatee,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount,
+ uint256 _durationPercent
+ ) public {
+ vm.assume(_depositor != address(uniStaker));
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+ _durationPercent = bound(_durationPercent, 0, 100);
+
+ // A user deposits staking tokens
+ _boundMintAndStake(_depositor, _stakeAmount, _delegatee);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+
+ // The full duration passes
+ _jumpAheadByPercentOfRewardDuration(_durationPercent);
+
+ // The depositor claims the reward
+ vm.prank(_depositor);
+ uniStaker.claimReward();
+
+ // We skip ahead to the end of the duration
+ _jumpAheadByPercentOfRewardDuration(100 - _durationPercent);
+
+ uint256 balance = uniStaker.REWARD_TOKEN().balanceOf(address(_depositor));
+
+ // The depositors balance should match the portion of the duration that passed before the
+ // rewards were claimed
+ assertLteWithinOnePercent(balance, _percentOf(_rewardAmount, _durationPercent));
+ // The depositor earned the portion of the reward after the rewards were claimed
+ assertLteWithinOnePercent(
+ uniStaker.unclaimedReward(_depositor), _percentOf(_rewardAmount, 100 - _durationPercent)
+ );
+ }
+
+ function testFuzz_CalculatesCorrectEarningsForTwoUsersThatDepositEqualStakeForFullDuration(
+ address _depositor1,
+ address _depositor2,
+ address _delegatee,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount
+ ) public {
+ vm.assume(_depositor1 != _depositor2);
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+
+ // A user deposits staking tokens
+ _boundMintAndStake(_depositor1, _stakeAmount, _delegatee);
+ // Some time passes
+ _jumpAhead(3000);
+ // Another depositor deposits the same number of staking tokens
+ _boundMintAndStake(_depositor2, _stakeAmount, _delegatee);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // The full duration passes
+ _jumpAheadByPercentOfRewardDuration(101);
+
+ // Each user should have earned half of the rewards
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor1), _percentOf(_rewardAmount, 50));
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor2), _percentOf(_rewardAmount, 50));
+ }
+
+ function testFuzz_CalculatesCorrectEarningsForTwoUsersWhenOneStakesMorePartiallyThroughTheDuration(
+ address _depositor1,
+ address _depositor2,
+ address _delegatee,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount
+ ) public {
+ vm.assume(_depositor1 != _depositor2);
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+
+ // A user deposits staking tokens
+ (, UniStaker.DepositIdentifier _depositId1) =
+ _boundMintAndStake(_depositor1, _stakeAmount, _delegatee);
+ // Some time passes
+ _jumpAhead(3000);
+ // Another depositor deposits the same number of staking tokens
+ _boundMintAndStake(_depositor2, _stakeAmount, _delegatee);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // One third of the duration passes
+ _jumpAheadByPercentOfRewardDuration(34);
+ // The first user triples their deposit by staking 2x more
+ _mintGovToken(_depositor1, 2 * _stakeAmount);
+ vm.startPrank(_depositor1);
+ govToken.approve(address(uniStaker), 2 * _stakeAmount);
+ uniStaker.stakeMore(_depositId1, 2 * _stakeAmount);
+ vm.stopPrank();
+ // The rest of the duration passes
+ _jumpAheadByPercentOfRewardDuration(66);
+
+ // Depositor 1 earns half the reward for one third the time and three quarters for two thirds of
+ // the time
+ uint256 _depositor1ExpectedEarnings =
+ _percentOf(_percentOf(_rewardAmount, 50), 34) + _percentOf(_percentOf(_rewardAmount, 75), 66);
+ // Depositor 2 earns half the reward for one third the time and one quarter for two thirds of
+ // the time
+ uint256 _depositor2ExpectedEarnings =
+ _percentOf(_percentOf(_rewardAmount, 50), 34) + _percentOf(_percentOf(_rewardAmount, 25), 66);
+
+ // Each user should have earned half of the rewards
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor1), _depositor1ExpectedEarnings);
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor2), _depositor2ExpectedEarnings);
+ }
+
+ function testFuzz_CalculatesCorrectEarningsForTwoUsersThatDepositEqualStakeForFullDurationAndBothClaim(
+ address _depositor1,
+ address _depositor2,
+ address _delegatee,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount
+ ) public {
+ vm.assume(_depositor1 != _depositor2);
+ vm.assume(_depositor1 != address(uniStaker) && _depositor2 != address(uniStaker));
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+
+ // A user deposits staking tokens
+ _boundMintAndStake(_depositor1, _stakeAmount, _delegatee);
+ // Some time passes
+ _jumpAhead(3000);
+ // Another depositor deposits the same number of staking tokens
+ _boundMintAndStake(_depositor2, _stakeAmount, _delegatee);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // The full duration passes
+ _jumpAheadByPercentOfRewardDuration(101);
+
+ // Depositor 1 claims
+ vm.prank(_depositor1);
+ uniStaker.claimReward();
+
+ // Depositor 2 claims
+ vm.prank(_depositor2);
+ uniStaker.claimReward();
+
+ uint256 depositor1Balance = uniStaker.REWARD_TOKEN().balanceOf(address(_depositor1));
+ uint256 depositor2Balance = uniStaker.REWARD_TOKEN().balanceOf(address(_depositor2));
+
+ // Each depositors balance should be half of the reward
+ assertLteWithinOnePercent(depositor1Balance, _percentOf(_rewardAmount, 50));
+ assertLteWithinOnePercent(depositor2Balance, _percentOf(_rewardAmount, 50));
+
+ // Each user should have earned nothing since they both claimed their rewards
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor1), 0);
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor2), 0);
+ }
+
+ function testFuzz_CalculatesCorrectEarningsForTwoUsersWhenOneStakesMorePartiallyThroughTheDurationAndOneClaims(
+ address _depositor1,
+ address _depositor2,
+ address _delegatee,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount
+ ) public {
+ vm.assume(_depositor1 != _depositor2);
+ vm.assume(_depositor1 != address(uniStaker) && _depositor2 != address(uniStaker));
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+
+ // A user deposits staking tokens
+ (, UniStaker.DepositIdentifier _depositId1) =
+ _boundMintAndStake(_depositor1, _stakeAmount, _delegatee);
+ // Some time passes
+ _jumpAhead(3000);
+ // Another depositor deposits the same number of staking tokens
+ _boundMintAndStake(_depositor2, _stakeAmount, _delegatee);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // One third of the duration passes
+ _jumpAheadByPercentOfRewardDuration(34);
+ // The first depositor claims their reward
+ vm.prank(_depositor1);
+ uniStaker.claimReward();
+ // The first depositor triples their deposit by staking 2x more
+ _mintGovToken(_depositor1, 2 * _stakeAmount);
+ vm.startPrank(_depositor1);
+ govToken.approve(address(uniStaker), 2 * _stakeAmount);
+ uniStaker.stakeMore(_depositId1, 2 * _stakeAmount);
+ vm.stopPrank();
+ // The rest of the duration passes
+ _jumpAheadByPercentOfRewardDuration(66);
+
+ // Depositor 1 earns three quarters of the reward for two thirds of the time
+ uint256 _depositor1ExpectedEarnings = _percentOf(_percentOf(_rewardAmount, 75), 66);
+ // Depositor 2 earns half the reward for one third the time and one quarter for two thirds of
+ // the time
+ uint256 _depositor2ExpectedEarnings =
+ _percentOf(_percentOf(_rewardAmount, 50), 34) + _percentOf(_percentOf(_rewardAmount, 25), 66);
+
+ uint256 depositor1Balance = uniStaker.REWARD_TOKEN().balanceOf(address(_depositor1));
+ uint256 depositor2Balance = uniStaker.REWARD_TOKEN().balanceOf(address(_depositor2));
+
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor1), _depositor1ExpectedEarnings);
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor2), _depositor2ExpectedEarnings);
+
+ // Depositor 1 should have received the reward they earned from before they claimed
+ assertLteWithinOnePercent(depositor1Balance, _percentOf(_percentOf(_rewardAmount, 50), 34));
+ // Depositor 2 should not have received anything because they did not claim
+ assertLteWithinOnePercent(depositor2Balance, 0);
+ }
+
+ function testFuzz_CalculatesCorrectEarningsForFourUsersThatDepositEqualStakeForFullDurationWhereOneIsABeneficiaryOfTwoOthers(
+ address _depositor1,
+ address _depositor2,
+ address _depositor3,
+ address _depositor4,
+ address _delegatee,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount
+ ) public {
+ vm.assume(
+ _depositor1 != _depositor2 && _depositor1 != _depositor2 && _depositor2 != _depositor3
+ && _depositor1 != _depositor3 && _depositor1 != _depositor4 && _depositor2 != _depositor4
+ && _depositor3 != _depositor4
+ );
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+
+ // A user deposits staking tokens
+ _boundMintAndStake(_depositor1, _stakeAmount, _delegatee);
+ // Some time passes
+ _jumpAhead(3000);
+ // Another depositor deposits the same number of staking tokens
+ _boundMintAndStake(_depositor2, _stakeAmount, _delegatee);
+ // Some time passes
+ _jumpAhead(3000);
+ // Another depositor deposits and adds the first depositor as the beneficiary
+ _boundMintAndStake(_depositor3, _stakeAmount, _delegatee, _depositor1);
+ // Some time passes
+ _jumpAhead(3000);
+ // Another depositor deposits and adds the first depositor as the beneficiary
+ _boundMintAndStake(_depositor4, _stakeAmount, _delegatee, _depositor1);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // The full duration passes
+ _jumpAheadByPercentOfRewardDuration(101);
+
+ // The first depositor has earn 3/4 of the and depositor 2 should earn a quarter of the reward
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor1), _percentOf(_rewardAmount, 75));
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor2), _percentOf(_rewardAmount, 25));
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor3), 0);
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor4), 0);
+ }
+
+ function testFuzz_CalculatesCorrectEarningsForFourUsersWhenOneStakesMorePartiallyThroughTheDurationAndTwoBeneficiaries(
+ address _depositor1,
+ address _depositor2,
+ address _depositor3,
+ address _depositor4,
+ address _delegatee,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount
+ ) public {
+ vm.assume(
+ _depositor1 != _depositor2 && _depositor1 != _depositor2 && _depositor2 != _depositor3
+ && _depositor1 != _depositor3 && _depositor1 != _depositor4 && _depositor2 != _depositor4
+ && _depositor3 != _depositor4
+ );
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+
+ // A user deposits staking tokens
+ (, UniStaker.DepositIdentifier _depositId1) =
+ _boundMintAndStake(_depositor1, _stakeAmount, _delegatee);
+ // Some time passes
+ _jumpAhead(3000);
+ // Another depositor deposits the same number of staking tokens
+ _boundMintAndStake(_depositor2, _stakeAmount, _delegatee);
+ // Some time passes
+ _jumpAhead(3000);
+ // Another depositor deposits the same number of staking tokens and adds the second depositor as
+ // a beneficiary
+ _boundMintAndStake(_depositor3, _stakeAmount, _delegatee, _depositor2);
+ // Some time passes
+ _jumpAhead(3000);
+ // Another depositor deposits the same number of staking tokens and adds the first depositor as
+ // a beneficiary
+ _boundMintAndStake(_depositor4, _stakeAmount, _delegatee, _depositor1);
+
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // One third of the duration passes
+ _jumpAheadByPercentOfRewardDuration(34);
+ // The first user doubles their stake
+ _mintGovToken(_depositor1, _stakeAmount);
+ vm.startPrank(_depositor1);
+ govToken.approve(address(uniStaker), _stakeAmount);
+ uniStaker.stakeMore(_depositId1, _stakeAmount);
+ vm.stopPrank();
+ // The rest of the duration passes
+ _jumpAheadByPercentOfRewardDuration(66);
+
+ // Depositor 1 earns half the reward for one third the time and three fifths for two thirds of
+ // the time
+ uint256 _depositor1ExpectedEarnings =
+ _percentOf(_percentOf(_rewardAmount, 50), 34) + _percentOf(_percentOf(_rewardAmount, 60), 66);
+ // Depositor 2 earns half the reward for one third the time and two fifths for two thirds of
+ // the time
+ uint256 _depositor2ExpectedEarnings =
+ _percentOf(_percentOf(_rewardAmount, 50), 34) + _percentOf(_percentOf(_rewardAmount, 40), 66);
+
+ // The third and fourth depositor earn nothing because they are sending their rewards to a
+ // beneficiary
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor1), _depositor1ExpectedEarnings);
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor2), _depositor2ExpectedEarnings);
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor3), 0);
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor4), 0);
+ }
+
+ function testFuzz_CalculatesCorrectEarningsForFourUsersWhenTwoStakeMorePartiallyThroughTheDurationAndOneBeneficiary(
+ address _depositor1,
+ address _depositor2,
+ address _depositor3,
+ address _depositor4,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount
+ ) public {
+ vm.assume(
+ _depositor1 != _depositor2 && _depositor1 != _depositor2 && _depositor2 != _depositor3
+ && _depositor1 != _depositor3 && _depositor1 != _depositor4 && _depositor2 != _depositor4
+ && _depositor3 != _depositor4
+ );
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+
+ // A user deposits staking tokens
+ (, UniStaker.DepositIdentifier _depositId1) =
+ _boundMintAndStake(_depositor1, _stakeAmount, _depositor1);
+ // Some time passes
+ _jumpAhead(3000);
+ // Another depositor deposits the same number of staking tokens
+ (, UniStaker.DepositIdentifier _depositId2) =
+ _boundMintAndStake(_depositor2, _stakeAmount, _depositor1);
+ // Some time passes
+ _jumpAhead(3000);
+ // Another depositor deposits the same number of staking tokens
+ (, UniStaker.DepositIdentifier _depositId3) =
+ _boundMintAndStake(_depositor3, _stakeAmount, _depositor1);
+ // Some time passes
+ _jumpAhead(3000);
+ // Another depositor deposits the same number of staking tokens
+ _boundMintAndStake(_depositor4, _stakeAmount, _depositor1);
+
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // One quarter of the duration passes
+ _jumpAheadByPercentOfRewardDuration(25);
+ // The first user doubles their deposit
+ _mintGovToken(_depositor1, _stakeAmount);
+ vm.startPrank(_depositor1);
+ govToken.approve(address(uniStaker), _stakeAmount);
+ uniStaker.stakeMore(_depositId1, _stakeAmount);
+ vm.stopPrank();
+
+ // Another quarter of the duration passes
+ _jumpAheadByPercentOfRewardDuration(25);
+ // The second users doubles their deposit
+ vm.startPrank(_depositor2);
+ _mintGovToken(_depositor2, _stakeAmount);
+ vm.startPrank(_depositor2);
+ govToken.approve(address(uniStaker), _stakeAmount);
+ uniStaker.stakeMore(_depositId2, _stakeAmount);
+ vm.stopPrank();
+
+ // The third user changes their beneficiary
+ vm.startPrank(_depositor3);
+ uniStaker.alterBeneficiary(_depositId3, _depositor1);
+ vm.stopPrank();
+
+ // The first depositor withdraws half of their deposit
+ vm.startPrank(_depositor1);
+ uniStaker.withdraw(_depositId1, _stakeAmount);
+ vm.stopPrank();
+
+ // The rest of the duration passes
+ _jumpAheadByPercentOfRewardDuration(50);
+
+ // Depositor 1 earns 25% of the reward for one quarter of the time and 40% of the reward three
+ // quarter of the time
+ uint256 _depositor1ExpectedEarnings =
+ _percentOf(_percentOf(_rewardAmount, 25), 25) + _percentOf(_percentOf(_rewardAmount, 40), 75);
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor1), _depositor1ExpectedEarnings);
+
+ // Depositor 2 earns a quarter of the reward for one quarter of the time, a fifth of the
+ // reward one quarter of the time, and 40 percent of the reward half the time
+ uint256 _depositor2ExpectedEarnings = _percentOf(_percentOf(_rewardAmount, 25), 25)
+ + _percentOf(_percentOf(_rewardAmount, 20), 25) + _percentOf(_percentOf(_rewardAmount, 40), 50);
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor2), _depositor2ExpectedEarnings);
+
+ // Depositor 3 earns 25% of the reward for a quarter of the time, 20% of the reward a quarter of
+ // the time and no reward half the time.
+ uint256 _depositor3ExpectedEarnings =
+ _percentOf(_percentOf(_rewardAmount, 25), 25) + _percentOf(_percentOf(_rewardAmount, 20), 25);
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor3), _depositor3ExpectedEarnings);
+
+ // Depositor 4 earns 25% of the reward for a quarter of the time, 20% of the reward 3 quarters
+ // of the time.
+ uint256 _depositor4ExpectedEarnings =
+ _percentOf(_percentOf(_rewardAmount, 25), 25) + _percentOf(_percentOf(_rewardAmount, 20), 75);
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor4), _depositor4ExpectedEarnings);
+ }
+
+ function testFuzz_CalculatesCorrectEarningsWhenAUserStakesThroughTheDurationAndAnotherStakesPartially(
+ address _depositor1,
+ address _depositor2,
+ address _delegatee,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount
+ ) public {
+ vm.assume(_depositor1 != _depositor2);
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+
+ // The first user stakes some tokens
+ _boundMintAndStake(_depositor1, _stakeAmount, _delegatee);
+ // A small amount of time passes
+ _jumpAhead(3000);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // Two thirds of the duration time elapses
+ _jumpAheadByPercentOfRewardDuration(66);
+ // A second user stakes the same amount of tokens
+ _boundMintAndStake(_depositor2, _stakeAmount, _delegatee);
+ // The rest of the duration elapses
+ _jumpAheadByPercentOfRewardDuration(34);
+
+ // Depositor 1 earns the full rewards for 2/3rds of the time & 1/2 the reward for 1/3rd of the
+ // time
+ uint256 _depositor1ExpectedEarnings =
+ _percentOf(_rewardAmount, 66) + _percentOf(_percentOf(_rewardAmount, 50), 34);
+ // Depositor 2 earns 1/2 the rewards for 1/3rd of the duration time
+ uint256 _depositor2ExpectedEarnings = _percentOf(_percentOf(_rewardAmount, 50), 34);
+
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor1), _depositor1ExpectedEarnings);
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor2), _depositor2ExpectedEarnings);
+ }
+
+ function testFuzz_CalculatesCorrectEarningsWhenAUserDepositsAndThereAreTwoRewards(
+ address _depositor,
+ address _delegatee,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount1,
+ uint256 _rewardAmount2
+ ) public {
+ (_stakeAmount, _rewardAmount1) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount1);
+ (_stakeAmount, _rewardAmount2) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount2);
+
+ // A user stakes tokens
+ _boundMintAndStake(_depositor, _stakeAmount, _delegatee);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount1);
+ // Two thirds of duration elapses
+ _jumpAheadByPercentOfRewardDuration(66);
+ // The contract is notified of a new reward, which restarts the reward the duration
+ _mintTransferAndNotifyReward(_rewardAmount2);
+ // Another third of the duration time elapses
+ _jumpAheadByPercentOfRewardDuration(34);
+
+ // For the first two thirds of the duration, the depositor earned all of the rewards being
+ // dripped out. Then more rewards were distributed. This resets the period. For the next
+ // period, which we chose to be another third of the duration, the depositor continued to earn
+ // all of the rewards being dripped, which now comprised of the remaining third of the first
+ // reward plus the second reward.
+ uint256 _depositorExpectedEarnings = _percentOf(_rewardAmount1, 66)
+ + _percentOf(_percentOf(_rewardAmount1, 34) + _rewardAmount2, 34);
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor), _depositorExpectedEarnings);
+ }
+
+ function testFuzz_CalculatesCorrectEarningsWhenTwoUsersDepositForPartialDurationsAndThereAreTwoRewards(
+ address _depositor1,
+ address _depositor2,
+ address _delegatee,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount1,
+ uint256 _rewardAmount2
+ ) public {
+ vm.assume(_depositor1 != _depositor2);
+ (_stakeAmount, _rewardAmount1) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount1);
+ (_stakeAmount, _rewardAmount2) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount2);
+
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount1);
+ // One quarter of the duration elapses
+ _jumpAheadByPercentOfRewardDuration(25);
+ // A user stakes some tokens
+ _boundMintAndStake(_depositor1, _stakeAmount, _delegatee);
+ // Another 40 percent of the duration time elapses
+ _jumpAheadByPercentOfRewardDuration(40);
+ // Another user stakes some tokens
+ _boundMintAndStake(_depositor2, _stakeAmount, _delegatee);
+ // Another quarter of the duration elapses
+ _jumpAheadByPercentOfRewardDuration(25);
+ // The contract receives another reward, resetting the duration
+ _mintTransferAndNotifyReward(_rewardAmount2);
+ // Another 20 percent of the duration elapses
+ _jumpAheadByPercentOfRewardDuration(20);
+
+ // The second depositor earns:
+ // * Half the rewards distributed (split with depositor 1) over 1/4 of the duration, where the
+ // rewards being earned are all from the first reward notification
+ // * Half the rewards (split with depositor 1) over 1/5 of the duration, where the rewards
+ // being earned are the remaining 10% of the first reward notification, plus the second
+ // reward notification
+ uint256 _depositor2ExpectedEarnings = _percentOf(_percentOf(_rewardAmount1, 25), 50)
+ + _percentOf(_percentOf(_percentOf(_rewardAmount1, 10) + _rewardAmount2, 20), 50);
+
+ // The first depositor earns the same amount as the second depositor, since they had the same
+ // stake and thus split the rewards during the period where both were staking. But the first
+ // depositor also earned all of the rewards for 40% of the duration, where the rewards being
+ // earned were from the first reward notification.
+ uint256 _depositor1ExpectedEarnings =
+ _percentOf(_rewardAmount1, 40) + _depositor2ExpectedEarnings;
+
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor1), _depositor1ExpectedEarnings);
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor2), _depositor2ExpectedEarnings);
+ }
+
+ function testFuzz_CalculatesCorrectEarningsWhenTwoUsersDepositDifferentAmountsForPartialDurationsAndThereAreTwoRewards(
+ address _depositor1,
+ address _depositor2,
+ address _delegatee,
+ uint96 _stakeAmount1,
+ uint96 _stakeAmount2,
+ uint256 _rewardAmount1,
+ uint256 _rewardAmount2
+ ) public {
+ vm.assume(_depositor1 != _depositor2);
+ (_stakeAmount1, _rewardAmount1) = _boundToRealisticStakeAndReward(_stakeAmount1, _rewardAmount1);
+ (_stakeAmount2, _rewardAmount2) = _boundToRealisticStakeAndReward(_stakeAmount2, _rewardAmount2);
+
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount1);
+ // One quarter of the duration elapses
+ _jumpAheadByPercentOfRewardDuration(25);
+ // A user stakes some tokens
+ _boundMintAndStake(_depositor1, _stakeAmount1, _delegatee);
+ // Another 40 percent of the duration time elapses
+ _jumpAheadByPercentOfRewardDuration(40);
+ // Another user stakes some tokens
+ _boundMintAndStake(_depositor2, _stakeAmount2, _delegatee);
+ // Another quarter of the duration elapses
+ _jumpAheadByPercentOfRewardDuration(25);
+ // The contract receives another reward, resetting the duration
+ _mintTransferAndNotifyReward(_rewardAmount2);
+ // Another 20 percent of the duration elapses
+ _jumpAheadByPercentOfRewardDuration(20);
+
+ // The total staked by both depositors together
+ uint256 _combinedStake = _stakeAmount1 + _stakeAmount2;
+ // These are the total rewards distributed by the contract after the second depositor adds
+ // their stake. It is the first reward for a quarter of the duration, plus the remaining 10% of
+ // the first reward, plus the second reward, for a fifth of the duration.
+ uint256 _combinedPhaseExpectedTotalRewards = _percentOf(_rewardAmount1, 25)
+ + _percentOf(_percentOf(_rewardAmount1, 10) + _rewardAmount2, 20);
+
+ // The second depositor should earn a share of the combined phase reward scaled by their
+ // portion of the total stake.
+ uint256 _depositor2ExpectedEarnings =
+ (_stakeAmount2 * _combinedPhaseExpectedTotalRewards) / _combinedStake;
+
+ // The first depositor earned all of the rewards for 40% of the duration, where the rewards
+ // were from the first reward notification. The first depositor also earns a share of the
+ // combined phase rewards proportional to his share of the stake.
+ uint256 _depositor1ExpectedEarnings = _percentOf(_rewardAmount1, 40)
+ + (_stakeAmount1 * _combinedPhaseExpectedTotalRewards) / _combinedStake;
+
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor1), _depositor1ExpectedEarnings);
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor2), _depositor2ExpectedEarnings);
+ }
+
+ // Could potentially add duration
+ function testFuzz_CalculatesCorrectEarningsWhenAUserDepositsAndThereAreThreeRewards(
+ address _depositor,
+ address _delegatee,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount1,
+ uint256 _rewardAmount2,
+ uint256 _rewardAmount3
+ ) public {
+ (_stakeAmount, _rewardAmount1) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount1);
+ (_stakeAmount, _rewardAmount2) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount2);
+ (_stakeAmount, _rewardAmount3) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount3);
+
+ // A user stakes tokens
+ _boundMintAndStake(_depositor, _stakeAmount, _delegatee);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount1);
+ // Two thirds of duration elapses
+ _jumpAheadByPercentOfRewardDuration(40);
+ // The contract is notified of a new reward, which restarts the reward the duration
+ _mintTransferAndNotifyReward(_rewardAmount2);
+ // Another third of the duration time elapses
+ _jumpAheadByPercentOfRewardDuration(30);
+ _mintTransferAndNotifyReward(_rewardAmount3);
+
+ _jumpAheadByPercentOfRewardDuration(30);
+
+ // For the first 40% of the duration, the depositor earned all of the rewards being
+ // dripped out. Then more rewards were distributed. This resets the period. For the next
+ // period, which we chose to be 30% of the duration, the depositor continued to earn
+ // all of the rewards being dripped, which now comprised of the remaining 60% of the first
+ // reward plus the second reward. For the next period, which we chose to be another 30% of the
+ // duration, the depositor continued to earn the rewards of the previous period, which now
+ // comprised of the remaining 70% of second period reward plus 30% of the third reward.
+ uint256 _depositorExpectedEarnings = _percentOf(_rewardAmount1, 40)
+ + _percentOf(_percentOf(_rewardAmount1, 60) + _rewardAmount2, 30)
+ + _percentOf(
+ _percentOf(_percentOf(_rewardAmount1, 60) + _rewardAmount2, 70) + _rewardAmount3, 30
+ );
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor), _depositorExpectedEarnings);
+ }
+
+ function testFuzz_CalculatesCorrectEarningsWhenTwoUsersDepositForPartialDurationsAndThereAreThreeRewards(
+ address _depositor1,
+ address _depositor2,
+ address _delegatee,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount1,
+ uint256 _rewardAmount2,
+ uint256 _rewardAmount3
+ ) public {
+ vm.assume(_depositor1 != _depositor2);
+ (_stakeAmount, _rewardAmount1) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount1);
+ (_stakeAmount, _rewardAmount2) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount2);
+ (_stakeAmount, _rewardAmount3) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount3);
+
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount1);
+ // One quarter of the duration elapses
+ _jumpAheadByPercentOfRewardDuration(25);
+ // A user stakes some tokens
+ _boundMintAndStake(_depositor1, _stakeAmount, _delegatee);
+ // Another 20 percent of the duration time elapses
+ _jumpAheadByPercentOfRewardDuration(20);
+ // Another user stakes some tokens
+ _boundMintAndStake(_depositor2, _stakeAmount, _delegatee);
+ // Another 20 percent of the duration time elapses
+ _jumpAheadByPercentOfRewardDuration(20);
+ // The contract receives another reward, resetting the duration
+ _mintTransferAndNotifyReward(_rewardAmount2);
+ // Another 20 percent of the duration elapses
+ _jumpAheadByPercentOfRewardDuration(20);
+ // The contract receives another reward, resetting the duration
+ _mintTransferAndNotifyReward(_rewardAmount3);
+ // Another 20 percent of the duration elapses
+ _jumpAheadByPercentOfRewardDuration(20);
+
+ // The second depositor earns:
+ // * Half the rewards distributed (split with depositor 1) over 1/5 of the duration, where the
+ // rewards being earned are all from the first reward notification
+ // * Half the rewards (split with depositor 1) over 1/5 of the duration, where the rewards
+ // being earned are the remaining 35% of the first reward notification, plus 20% the second
+ // reward notification
+ // * Half the rewards (split with depositor 1) over 1/5 the duration where the rewards being
+ // earned
+ // are 20% of the previous reward and the third reward
+ uint256 _depositor2ExpectedEarnings = _percentOf(_percentOf(_rewardAmount1, 20), 50)
+ + _percentOf(_percentOf(_percentOf(_rewardAmount1, 35) + _rewardAmount2, 20), 50)
+ + _percentOf(
+ _percentOf(
+ _percentOf(_percentOf(_rewardAmount1, 35) + _rewardAmount2, 80) + _rewardAmount3, 20
+ ),
+ 50
+ );
+
+ // // The first depositor earns the same amount as the second depositor, since they had the same
+ // // stake and thus split the rewards during the period where both were staking. But the first
+ // // depositor also earned all of the rewards for 20% of the duration, where the rewards being
+ // // earned were from the first reward notification.
+ uint256 _depositor1ExpectedEarnings =
+ _percentOf(_rewardAmount1, 20) + _depositor2ExpectedEarnings;
+
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor1), _depositor1ExpectedEarnings);
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor2), _depositor2ExpectedEarnings);
+ }
+
+ function testFuzz_CalculatesCorrectEarningsWhenTwoUsersDepositDifferentAmountsForPartialDurationsAndThereAreThreeRewards(
+ address _depositor1,
+ address _depositor2,
+ address _delegatee,
+ uint96 _stakeAmount1,
+ uint96 _stakeAmount2,
+ uint256 _rewardAmount1,
+ uint256 _rewardAmount2,
+ uint256 _rewardAmount3
+ ) public {
+ vm.assume(_depositor1 != _depositor2);
+ (_stakeAmount1, _rewardAmount1) = _boundToRealisticStakeAndReward(_stakeAmount1, _rewardAmount1);
+ (_stakeAmount2, _rewardAmount2) = _boundToRealisticStakeAndReward(_stakeAmount2, _rewardAmount2);
+ (_stakeAmount2, _rewardAmount3) = _boundToRealisticStakeAndReward(_stakeAmount2, _rewardAmount3);
+
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount1);
+ // One quarter of the duration elapses
+ _jumpAheadByPercentOfRewardDuration(25);
+ // A user stakes some tokens
+ _boundMintAndStake(_depositor1, _stakeAmount1, _delegatee);
+ // Another 40 percent of the duration time elapses
+ _jumpAheadByPercentOfRewardDuration(20);
+ // Another user stakes some tokens
+ _boundMintAndStake(_depositor2, _stakeAmount2, _delegatee);
+ // Another quarter of the duration elapses
+ _jumpAheadByPercentOfRewardDuration(20);
+ // The contract receives another reward, resetting the duration
+ _mintTransferAndNotifyReward(_rewardAmount2);
+ // Another quarter of the duration elapses
+ _jumpAheadByPercentOfRewardDuration(20);
+ // The contract receives another reward, resetting the duration
+ _mintTransferAndNotifyReward(_rewardAmount3);
+ // Another 20 percent of the duration elapses
+ _jumpAheadByPercentOfRewardDuration(20);
+
+ // The total staked by both depositors together
+ uint256 _combinedStake = _stakeAmount1 + _stakeAmount2;
+ // These are the total rewards distributed by the contract after the second depositor adds
+ // their stake. It is the first reward for a fifth of the duration, plus the remaining 35% of
+ // the first reward, plus 20% the second reward, for a fifth of the duration, plus the 80% of
+ // the previous amount plus the third reward for 20% of the duration.
+ uint256 _combinedPhaseExpectedTotalRewards = _percentOf(_rewardAmount1, 20)
+ + _percentOf(_percentOf(_rewardAmount1, 35) + _rewardAmount2, 20)
+ + _percentOf(
+ _percentOf(_percentOf(_rewardAmount1, 35) + _rewardAmount2, 80) + _rewardAmount3, 20
+ );
+
+ // The second depositor should earn a share of the combined phase reward scaled by their
+ // portion of the total stake.
+ uint256 _depositor2ExpectedEarnings =
+ (_stakeAmount2 * _combinedPhaseExpectedTotalRewards) / _combinedStake;
+
+ // The first depositor earned all of the rewards for 20% of the duration, where the rewards
+ // were from the first reward notification. The first depositor also earns a share of the
+ // combined phase rewards proportional to his share of the stake.
+ uint256 _depositor1ExpectedEarnings = _percentOf(_rewardAmount1, 20)
+ + (_stakeAmount1 * _combinedPhaseExpectedTotalRewards) / _combinedStake;
+
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor1), _depositor1ExpectedEarnings);
+ assertLteWithinOnePercent(uniStaker.unclaimedReward(_depositor2), _depositor2ExpectedEarnings);
+ }
+
+ function testFuzz_CalculatesEarningsThatAreLessThanOrEqualToRewardsReceived(
+ address _depositor1,
+ address _depositor2,
+ address _delegatee,
+ uint96 _stakeAmount1,
+ uint96 _stakeAmount2,
+ uint256 _rewardAmount,
+ uint256 _durationPercent
+ ) public {
+ vm.assume(_depositor1 != _depositor2);
+
+ (_stakeAmount1, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount1, _rewardAmount);
+ _stakeAmount2 = _boundToRealisticStake(_stakeAmount2);
+ _durationPercent = bound(_durationPercent, 0, 100);
+
+ // A user deposits staking tokens
+ _boundMintAndStake(_depositor1, _stakeAmount1, _delegatee);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // A portion of the duration passes
+ _jumpAheadByPercentOfRewardDuration(_durationPercent);
+ // Another user deposits stake
+ _boundMintAndStake(_depositor2, _stakeAmount2, _delegatee);
+ // The rest of the duration elapses
+ _jumpAheadByPercentOfRewardDuration(100 - _durationPercent);
+
+ uint256 _earned1 = uniStaker.unclaimedReward(_depositor1);
+ uint256 _earned2 = uniStaker.unclaimedReward(_depositor2);
+
+ // Rewards earned by depositors should always at most equal to the actual reward amount
+ assertLteWithinOnePercent(_earned1 + _earned2, _rewardAmount);
+ }
+
+ function test_CalculatesEarningsInAWayThatMitigatesRewardGriefing() public {
+ address _depositor1 = makeAddr("Depositor 1");
+ address _depositor2 = makeAddr("Depositor 2");
+ address _depositor3 = makeAddr("Depositor 3");
+ address _delegatee = makeAddr("Delegatee");
+ address _attacker = makeAddr("Attacker");
+
+ uint96 _smallDepositAmount = 0.1e18;
+ uint96 _largeDepositAmount = 25_000_000e18;
+ _mintGovToken(_depositor1, _smallDepositAmount);
+ _mintGovToken(_depositor2, _smallDepositAmount);
+ _mintGovToken(_depositor3, _largeDepositAmount);
+ uint256 _rewardAmount = 1e14;
+ rewardToken.mint(rewardNotifier, _rewardAmount);
+
+ // The contract is notified of a reward
+ vm.startPrank(rewardNotifier);
+ rewardToken.transfer(address(uniStaker), _rewardAmount);
+ uniStaker.notifyRewardAmount(_rewardAmount);
+ vm.stopPrank();
+
+ // User deposit staking tokens
+ _stake(_depositor1, _smallDepositAmount, _delegatee);
+ _stake(_depositor2, _smallDepositAmount, _delegatee);
+ _stake(_depositor3, _largeDepositAmount, _delegatee);
+
+ // Every block _attacker deposits 0 stake and assigns _depositor1 as beneficiary, thus leading
+ // to frequent updates of the reward checkpoint for _depositor1, during which rounding errors
+ // could accrue.
+ UniStaker.DepositIdentifier _depositId = _stake(_attacker, 0, _delegatee, _depositor1);
+ for (uint256 i = 0; i < 1000; ++i) {
+ _jumpAhead(12);
+ vm.prank(_attacker);
+ uniStaker.stakeMore(_depositId, 0);
+ }
+
+ // Despite the attempted griefing attack, the unclaimed rewards for the two depositors should
+ // be ~the same.
+ assertLteWithinOnePercent(
+ uniStaker.unclaimedReward(_depositor1), uniStaker.unclaimedReward(_depositor2)
+ );
+ }
+}
+
+contract ClaimReward is UniStakerRewardsTest {
+ function testFuzz_SendsRewardsEarnedToTheUser(
+ address _depositor,
+ address _delegatee,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount,
+ uint256 _durationPercent
+ ) public {
+ vm.assume(_depositor != address(uniStaker));
+
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+ _durationPercent = bound(_durationPercent, 0, 100);
+
+ // A user deposits staking tokens
+ _boundMintAndStake(_depositor, _stakeAmount, _delegatee);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // A portion of the duration passes
+ _jumpAheadByPercentOfRewardDuration(_durationPercent);
+
+ uint256 _earned = uniStaker.unclaimedReward(_depositor);
+
+ vm.prank(_depositor);
+ uniStaker.claimReward();
+
+ assertEq(rewardToken.balanceOf(_depositor), _earned);
+ }
+
+ function testFuzz_ReturnsClaimedRewardAmount(
+ address _depositor,
+ address _delegatee,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount,
+ uint256 _durationPercent
+ ) public {
+ vm.assume(_depositor != address(uniStaker));
+
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+ _durationPercent = bound(_durationPercent, 0, 100);
+
+ // A user deposits staking tokens
+ _boundMintAndStake(_depositor, _stakeAmount, _delegatee);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // A portion of the duration passes
+ _jumpAheadByPercentOfRewardDuration(_durationPercent);
+
+ uint256 _earned = uniStaker.unclaimedReward(_depositor);
+
+ vm.prank(_depositor);
+ uint256 _claimedAmount = uniStaker.claimReward();
+
+ assertEq(_earned, _claimedAmount);
+ }
+
+ function testFuzz_ResetsTheRewardsEarnedByTheUser(
+ address _depositor,
+ address _delegatee,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount,
+ uint256 _durationPercent
+ ) public {
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+ _durationPercent = bound(_durationPercent, 0, 100);
+
+ // A user deposits staking tokens
+ _boundMintAndStake(_depositor, _stakeAmount, _delegatee);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // A portion of the duration passes
+ _jumpAheadByPercentOfRewardDuration(_durationPercent);
+
+ vm.prank(_depositor);
+ uniStaker.claimReward();
+
+ assertEq(uniStaker.unclaimedReward(_depositor), 0);
+ }
+
+ function testFuzz_EmitsAnEventWhenRewardsAreClaimed(
+ address _depositor,
+ address _delegatee,
+ uint96 _stakeAmount,
+ uint256 _rewardAmount,
+ uint256 _durationPercent
+ ) public {
+ (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount);
+ _durationPercent = bound(_durationPercent, 1, 100);
+
+ // A user deposits staking tokens
+ _boundMintAndStake(_depositor, _stakeAmount, _delegatee);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // A portion of the duration passes
+ _jumpAheadByPercentOfRewardDuration(_durationPercent);
+
+ uint256 _earned = uniStaker.unclaimedReward(_depositor);
+
+ vm.expectEmit();
+ emit UniStaker.RewardClaimed(_depositor, _earned);
+
+ vm.prank(_depositor);
+ uniStaker.claimReward();
+ }
+}
+
+contract ClaimRewardOnBehalf is UniStakerRewardsTest {
+ using stdStorage for StdStorage;
+
+ function testFuzz_ClaimRewardOnBehalfOfBeneficiary(
+ uint256 _beneficiaryPrivateKey,
+ address _sender,
+ uint96 _depositAmount,
+ uint256 _durationPercent,
+ uint256 _rewardAmount,
+ address _delegatee,
+ address _depositor,
+ uint256 _currentNonce,
+ uint256 _deadline
+ ) public {
+ vm.assume(_delegatee != address(0) && _depositor != address(0) && _sender != address(0));
+ _beneficiaryPrivateKey = bound(_beneficiaryPrivateKey, 1, 100e18);
+ address _beneficiary = vm.addr(_beneficiaryPrivateKey);
+
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_depositAmount, _rewardAmount);
+ _durationPercent = bound(_durationPercent, 0, 100);
+
+ // A user deposits staking tokens
+ (_depositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // A portion of the duration passes
+ _jumpAheadByPercentOfRewardDuration(_durationPercent);
+ _deadline = bound(_deadline, block.timestamp, type(uint256).max);
+
+ uint256 _earned = uniStaker.unclaimedReward(_beneficiary);
+
+ stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_beneficiary).checked_write(
+ _currentNonce
+ );
+
+ bytes32 _message = keccak256(
+ abi.encode(
+ uniStaker.CLAIM_REWARD_TYPEHASH(), _beneficiary, uniStaker.nonces(_beneficiary), _deadline
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message));
+ bytes memory _signature = _sign(_beneficiaryPrivateKey, _messageHash);
+
+ vm.prank(_sender);
+ uniStaker.claimRewardOnBehalf(_beneficiary, _deadline, _signature);
+
+ assertEq(rewardToken.balanceOf(_beneficiary), _earned);
+ }
+
+ function testFuzz_ReturnsClaimedRewardAmount(
+ uint256 _beneficiaryPrivateKey,
+ address _sender,
+ uint96 _depositAmount,
+ uint256 _durationPercent,
+ uint256 _rewardAmount,
+ address _delegatee,
+ address _depositor,
+ uint256 _currentNonce,
+ uint256 _deadline
+ ) public {
+ vm.assume(_delegatee != address(0) && _depositor != address(0) && _sender != address(0));
+ _beneficiaryPrivateKey = bound(_beneficiaryPrivateKey, 1, 100e18);
+ address _beneficiary = vm.addr(_beneficiaryPrivateKey);
+
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_depositAmount, _rewardAmount);
+ _durationPercent = bound(_durationPercent, 0, 100);
+
+ // A user deposits staking tokens
+ (_depositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // A portion of the duration passes
+ _jumpAheadByPercentOfRewardDuration(_durationPercent);
+ _deadline = bound(_deadline, block.timestamp, type(uint256).max);
+
+ uint256 _earned = uniStaker.unclaimedReward(_beneficiary);
+
+ stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_beneficiary).checked_write(
+ _currentNonce
+ );
+
+ bytes32 _message = keccak256(
+ abi.encode(
+ uniStaker.CLAIM_REWARD_TYPEHASH(), _beneficiary, uniStaker.nonces(_beneficiary), _deadline
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message));
+ bytes memory _signature = _sign(_beneficiaryPrivateKey, _messageHash);
+
+ vm.prank(_sender);
+ uint256 _claimedAmount = uniStaker.claimRewardOnBehalf(_beneficiary, _deadline, _signature);
+
+ assertEq(_earned, _claimedAmount);
+ }
+
+ function testFuzz_RevertIf_WrongNonceIsUsed(
+ uint256 _beneficiaryPrivateKey,
+ address _sender,
+ uint96 _depositAmount,
+ uint256 _durationPercent,
+ uint256 _rewardAmount,
+ address _delegatee,
+ address _depositor,
+ uint256 _currentNonce,
+ uint256 _suppliedNonce,
+ uint256 _deadline
+ ) public {
+ vm.assume(_currentNonce != _suppliedNonce);
+ vm.assume(_delegatee != address(0) && _depositor != address(0) && _sender != address(0));
+ _beneficiaryPrivateKey = bound(_beneficiaryPrivateKey, 1, 100e18);
+ address _beneficiary = vm.addr(_beneficiaryPrivateKey);
+
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_depositAmount, _rewardAmount);
+ _durationPercent = bound(_durationPercent, 0, 100);
+
+ // A user deposits staking tokens
+ (_depositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // A portion of the duration passes
+ _jumpAheadByPercentOfRewardDuration(_durationPercent);
+ _deadline = bound(_deadline, block.timestamp, type(uint256).max);
+
+ stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_beneficiary).checked_write(
+ _currentNonce
+ );
+
+ bytes32 _message = keccak256(
+ abi.encode(uniStaker.CLAIM_REWARD_TYPEHASH(), _beneficiary, _suppliedNonce, _deadline)
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message));
+ bytes memory _signature = _sign(_beneficiaryPrivateKey, _messageHash);
+
+ vm.expectRevert(UniStaker.UniStaker__InvalidSignature.selector);
+ vm.prank(_sender);
+ uniStaker.claimRewardOnBehalf(_beneficiary, _deadline, _signature);
+ }
+
+ function testFuzz_RevertIf_DeadlineExpired(
+ uint256 _beneficiaryPrivateKey,
+ address _sender,
+ uint96 _depositAmount,
+ uint256 _durationPercent,
+ uint256 _rewardAmount,
+ address _delegatee,
+ address _depositor,
+ uint256 _currentNonce,
+ uint256 _deadline
+ ) public {
+ vm.assume(_delegatee != address(0) && _depositor != address(0) && _sender != address(0));
+ _beneficiaryPrivateKey = bound(_beneficiaryPrivateKey, 1, 100e18);
+ address _beneficiary = vm.addr(_beneficiaryPrivateKey);
+
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_depositAmount, _rewardAmount);
+ _durationPercent = bound(_durationPercent, 0, 100);
+
+ // A user deposits staking tokens
+ (_depositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary);
+ // The contract is notified of a reward
+ _mintTransferAndNotifyReward(_rewardAmount);
+ // A portion of the duration passes
+ _jumpAheadByPercentOfRewardDuration(_durationPercent);
+ _deadline = bound(_deadline, 0, block.timestamp - 1);
+
+ stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_beneficiary).checked_write(
+ _currentNonce
+ );
+
+ bytes32 _message = keccak256(
+ abi.encode(uniStaker.CLAIM_REWARD_TYPEHASH(), _beneficiary, _currentNonce, _deadline)
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message));
+ bytes memory _signature = _sign(_beneficiaryPrivateKey, _messageHash);
+
+ vm.expectRevert(UniStaker.UniStaker__ExpiredDeadline.selector);
+ vm.prank(_sender);
+ uniStaker.claimRewardOnBehalf(_beneficiary, _deadline, _signature);
+ }
+
+ function testFuzz_RevertIf_InvalidSignatureIsPassed(
+ uint256 _beneficiaryPrivateKey,
+ address _sender,
+ uint96 _depositAmount,
+ address _depositor,
+ address _delegatee,
+ uint256 _currentNonce,
+ uint256 _randomSeed,
+ uint256 _deadline
+ ) public {
+ vm.assume(_delegatee != address(0) && _depositor != address(0) && _sender != address(0));
+ _deadline = bound(_deadline, block.timestamp, type(uint256).max);
+ _beneficiaryPrivateKey = bound(_beneficiaryPrivateKey, 1, 100e18);
+ address _beneficiary = vm.addr(_beneficiaryPrivateKey);
+ _depositAmount = _boundMintAmount(_depositAmount);
+ _mintGovToken(_depositor, _depositAmount);
+ stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_beneficiary).checked_write(
+ _currentNonce
+ );
+
+ UniStaker.DepositIdentifier _depositId;
+ (_depositAmount, _depositId) =
+ _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary);
+
+ bytes32 _message = keccak256(
+ abi.encode(
+ uniStaker.CLAIM_REWARD_TYPEHASH(), _beneficiary, uniStaker.nonces(_beneficiary), _deadline
+ )
+ );
+
+ bytes32 _messageHash =
+ keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message));
+
+ // Here we use `_randomSeed` as an arbitrary source of randomness to replace a legit
+ // parameter with an attack-like one.
+ if (_randomSeed % 4 == 0) {
+ _beneficiary = address(uint160(uint256(keccak256(abi.encode(_beneficiary)))));
+ } else if (_randomSeed % 4 == 1) {
+ _messageHash = _modifyMessage(_messageHash, uint256(keccak256(abi.encode(_randomSeed))));
+ } else if (_randomSeed % 4 == 2) {
+ _deadline = uint256(keccak256(abi.encode(_deadline)));
+ }
+ bytes memory _signature = _sign(_beneficiaryPrivateKey, _messageHash);
+ if (_randomSeed % 4 == 3) _signature = _modifySignature(_signature, _randomSeed);
+
+ vm.expectRevert(UniStaker.UniStaker__InvalidSignature.selector);
+ vm.prank(_sender);
+ uniStaker.claimRewardOnBehalf(_beneficiary, _deadline, _signature);
+ }
+}
+
+contract _FetchOrDeploySurrogate is UniStakerRewardsTest {
+ function testFuzz_EmitsAnEventWhenASurrogateIsDeployed(address _delegatee) public {
+ vm.assume(_delegatee != address(0));
+ vm.recordLogs();
+ uniStaker.exposed_fetchOrDeploySurrogate(_delegatee);
+
+ Vm.Log[] memory logs = vm.getRecordedLogs();
+ DelegationSurrogate _surrogate = uniStaker.surrogates(_delegatee);
+
+ assertEq(logs[1].topics[0], keccak256("SurrogateDeployed(address,address)"));
+ assertEq(logs[1].topics[1], bytes32(uint256(uint160(_delegatee))));
+ assertEq(logs[1].topics[2], bytes32(uint256(uint160(address(_surrogate)))));
+ }
+}
+
+contract Multicall is UniStakerRewardsTest {
+ function _encodeStake(address _delegatee, uint96 _stakeAmount)
+ internal
+ pure
+ returns (bytes memory)
+ {
+ return
+ abi.encodeWithSelector(bytes4(keccak256("stake(uint96,address)")), _stakeAmount, _delegatee);
+ }
+
+ function _encodeStake(address _delegatee, uint96 _stakeAmount, address _beneficiary)
+ internal
+ pure
+ returns (bytes memory)
+ {
+ return abi.encodeWithSelector(
+ bytes4(keccak256("stake(uint96,address,address)")), _stakeAmount, _delegatee, _beneficiary
+ );
+ }
+
+ function _encodeStakeMore(UniStaker.DepositIdentifier _depositId, uint96 _stakeAmount)
+ internal
+ pure
+ returns (bytes memory)
+ {
+ return abi.encodeWithSelector(
+ bytes4(keccak256("stakeMore(uint256,uint96)")), _depositId, _stakeAmount
+ );
+ }
+
+ function _encodeWithdraw(UniStaker.DepositIdentifier _depositId, uint96 _amount)
+ internal
+ pure
+ returns (bytes memory)
+ {
+ return
+ abi.encodeWithSelector(bytes4(keccak256("withdraw(uint256,uint96)")), _depositId, _amount);
+ }
+
+ function _encodeAlterBeneficiary(UniStaker.DepositIdentifier _depositId, address _beneficiary)
+ internal
+ pure
+ returns (bytes memory)
+ {
+ return abi.encodeWithSelector(
+ bytes4(keccak256("alterBeneficiary(uint256,address)")), _depositId, _beneficiary
+ );
+ }
+
+ function _encodeAlterDelegatee(UniStaker.DepositIdentifier _depositId, address _delegatee)
+ internal
+ pure
+ returns (bytes memory)
+ {
+ return abi.encodeWithSelector(
+ bytes4(keccak256("alterDelegatee(uint256,address)")), _depositId, _delegatee
+ );
+ }
+
+ function testFuzz_CanUseMulticallToStakeMultipleTimes(
+ address _depositor,
+ address _delegatee1,
+ address _delegatee2,
+ uint96 _stakeAmount1,
+ uint96 _stakeAmount2
+ ) public {
+ _stakeAmount1 = _boundToRealisticStake(_stakeAmount1);
+ _stakeAmount2 = _boundToRealisticStake(_stakeAmount2);
+ vm.assume(_delegatee1 != address(0) && _delegatee2 != address(0));
+ _mintGovToken(_depositor, _stakeAmount1 + _stakeAmount2);
+
+ vm.prank(_depositor);
+ govToken.approve(address(uniStaker), _stakeAmount1 + _stakeAmount2);
+
+ bytes[] memory _calls = new bytes[](2);
+ _calls[0] = _encodeStake(_delegatee1, _stakeAmount1);
+ _calls[1] = _encodeStake(_delegatee2, _stakeAmount2);
+ vm.prank(_depositor);
+ uniStaker.multicall(_calls);
+ assertEq(uniStaker.depositorTotalStaked(_depositor), _stakeAmount1 + _stakeAmount2);
+ }
+
+ function testFuzz_CanUseMulticallToStakeAndAlterBeneficiaryAndDelegatee(
+ address _depositor,
+ address _delegatee0,
+ address _delegatee1,
+ address _beneficiary0,
+ address _beneficiary1,
+ uint96 _stakeAmount0,
+ uint96 _stakeAmount1,
+ uint256 _timeElapsed
+ ) public {
+ _stakeAmount0 = _boundToRealisticStake(_stakeAmount0);
+ _stakeAmount1 = _boundToRealisticStake(_stakeAmount1);
+
+ vm.assume(
+ _depositor != address(0) && _delegatee0 != address(0) && _delegatee1 != address(0)
+ && _beneficiary0 != address(0) && _beneficiary1 != address(0)
+ );
+ _mintGovToken(_depositor, _stakeAmount0 + _stakeAmount1);
+
+ vm.startPrank(_depositor);
+ govToken.approve(address(uniStaker), _stakeAmount0 + _stakeAmount1);
+
+ // first, do initial stake without multicall
+ UniStaker.DepositIdentifier _depositId =
+ uniStaker.stake(_stakeAmount0, _delegatee0, _beneficiary0);
+
+ // some time goes by...
+ vm.warp(_timeElapsed);
+
+ // now I want to stake more, and also change my delegatee and beneficiary
+ bytes[] memory _calls = new bytes[](3);
+ _calls[0] = _encodeStakeMore(_depositId, _stakeAmount1);
+ _calls[1] = _encodeAlterBeneficiary(_depositId, _beneficiary1);
+ _calls[2] = _encodeAlterDelegatee(_depositId, _delegatee1);
+ uniStaker.multicall(_calls);
+ vm.stopPrank();
+
+ (uint96 _amountResult,, address _delegateeResult, address _beneficiaryResult) =
+ uniStaker.deposits(_depositId);
+ assertEq(uniStaker.depositorTotalStaked(_depositor), _stakeAmount0 + _stakeAmount1);
+ assertEq(_amountResult, _stakeAmount0 + _stakeAmount1);
+ assertEq(_delegateeResult, _delegatee1);
+ assertEq(_beneficiaryResult, _beneficiary1);
+ }
+}
diff --git a/test/fakes/ERC20Fake.sol b/test/fakes/ERC20Fake.sol
new file mode 100644
index 0000000..017cc15
--- /dev/null
+++ b/test/fakes/ERC20Fake.sol
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+pragma solidity ^0.8.23;
+
+import {ERC20} from "openzeppelin/token/ERC20/ERC20.sol";
+
+/// @dev An ERC20 token that allows for public minting for use in tests.
+contract ERC20Fake is ERC20 {
+ constructor() ERC20("Fake Token", "FAKE") {}
+
+ /// @dev Public mint function useful for testing
+ function mint(address _account, uint256 _value) public {
+ _mint(_account, _value);
+ }
+}
diff --git a/test/harnesses/UniStakerHarness.sol b/test/harnesses/UniStakerHarness.sol
new file mode 100644
index 0000000..60a0518
--- /dev/null
+++ b/test/harnesses/UniStakerHarness.sol
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+pragma solidity ^0.8.23;
+
+import {DelegationSurrogate} from "src/DelegationSurrogate.sol";
+import {UniStaker} from "src/UniStaker.sol";
+
+import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol";
+import {SafeERC20} from "openzeppelin/token/ERC20/utils/SafeERC20.sol";
+import {IERC20Delegates} from "src/interfaces/IERC20Delegates.sol";
+
+contract UniStakerHarness is UniStaker {
+ constructor(IERC20 _rewardsToken, IERC20Delegates _stakeToken, address _admin)
+ UniStaker(_rewardsToken, _stakeToken, _admin)
+ {}
+
+ function exposed_useDepositId() external returns (DepositIdentifier _depositId) {
+ _depositId = _useDepositId();
+ }
+
+ function exposed_fetchOrDeploySurrogate(address delegatee)
+ external
+ returns (DelegationSurrogate _surrogate)
+ {
+ _surrogate = _fetchOrDeploySurrogate(delegatee);
+ }
+}
diff --git a/test/helpers/AddressSet.sol b/test/helpers/AddressSet.sol
new file mode 100644
index 0000000..83327a7
--- /dev/null
+++ b/test/helpers/AddressSet.sol
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+pragma solidity ^0.8.23;
+
+// AddressSet.sol comes from
+// https://github.com/horsefacts/weth-invariant-testing/blob/973156bc9b6684f0cf62de19e9bb4c5c27a41bb2/test/helpers/AddressSet.sol
+
+struct AddressSet {
+ address[] addrs;
+ mapping(address => bool) saved;
+}
+
+library LibAddressSet {
+ function add(AddressSet storage s, address addr) internal {
+ if (!s.saved[addr]) {
+ s.addrs.push(addr);
+ s.saved[addr] = true;
+ }
+ }
+
+ function contains(AddressSet storage s, address addr) internal view returns (bool) {
+ return s.saved[addr];
+ }
+
+ function count(AddressSet storage s) internal view returns (uint256) {
+ return s.addrs.length;
+ }
+
+ function rand(AddressSet storage s, uint256 seed) internal view returns (address) {
+ if (s.addrs.length > 0) return s.addrs[seed % s.addrs.length];
+ else return address(0);
+ }
+
+ function forEach(AddressSet storage s, function(address) external func) internal {
+ for (uint256 i; i < s.addrs.length; ++i) {
+ func(s.addrs[i]);
+ }
+ }
+
+ function reduce(
+ AddressSet storage s,
+ uint256 acc,
+ function(uint256,address) external returns (uint256) func
+ ) internal returns (uint256) {
+ for (uint256 i; i < s.addrs.length; ++i) {
+ acc = func(acc, s.addrs[i]);
+ }
+ return acc;
+ }
+}
diff --git a/test/helpers/Constants.sol b/test/helpers/Constants.sol
new file mode 100644
index 0000000..b57c1de
--- /dev/null
+++ b/test/helpers/Constants.sol
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+pragma solidity ^0.8.23;
+
+contract Constants {
+ address constant UNISWAP_GOVERNOR_ADDRESS = 0x408ED6354d4973f66138C91495F2f2FCbd8724C3;
+ address constant WBTC_WETH_3000_POOL = 0xCBCdF9626bC03E24f779434178A73a0B4bad62eD;
+ address constant DAI_WETH_3000_POOL = 0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8;
+ address constant DAI_USDC_100_POOL = 0x5777d92f208679DB4b9778590Fa3CAB3aC9e2168;
+
+ address constant WETH_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; // use deposit
+ address constant DAI_ADDRESS = 0x6B175474E89094C44Da98b954EedeAC495271d0F; // mint with auth
+ address constant USDC_ADDRESS = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; // mint only minters
+ address constant STAKING_TOKEN_MINTER = 0x1a9C8182C09F50C8318d769245beA52c32BE35BC;
+}
diff --git a/test/helpers/PercentAssertions.sol b/test/helpers/PercentAssertions.sol
new file mode 100644
index 0000000..79eb63f
--- /dev/null
+++ b/test/helpers/PercentAssertions.sol
@@ -0,0 +1,59 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+pragma solidity ^0.8.23;
+
+import {Test} from "forge-std/Test.sol";
+
+contract PercentAssertions is Test {
+ // Because there will be (expected) rounding errors in the amount of rewards earned, this helper
+ // checks that the truncated number is lesser and within 1% of the expected number.
+ function assertLteWithinOnePercent(uint256 a, uint256 b) public {
+ if (a > b) {
+ emit log("Error: a <= b not satisfied");
+ emit log_named_uint(" Expected", b);
+ emit log_named_uint(" Actual", a);
+
+ fail();
+ }
+
+ uint256 minBound = (b * 9900) / 10_000;
+
+ if (a < minBound) {
+ emit log("Error: a >= 0.99 * b not satisfied");
+ emit log_named_uint(" Expected", b);
+ emit log_named_uint(" Actual", a);
+ emit log_named_uint(" minBound", minBound);
+
+ fail();
+ }
+ }
+
+ function _percentOf(uint256 _amount, uint256 _percent) public pure returns (uint256) {
+ // For cases where the percentage is less than 100, we calculate the percentage by
+ // taking the inverse percentage and subtracting it. This effectively rounds _up_ the
+ // value by putting the truncation on the opposite side. For example, 92% of 555 is 510.6.
+ // Calculating it in this way would yield (555 - 44) = 511, instead of 510.
+ if (_percent < 100) return _amount - ((100 - _percent) * _amount) / 100;
+ else return (_percent * _amount) / 100;
+ }
+
+ // This helper is for normal rounding errors, i.e. if the number might be truncated down by 1
+ function assertLteWithinOneUnit(uint256 a, uint256 b) public {
+ if (a > b) {
+ emit log("Error: a <= b not satisfied");
+ emit log_named_uint(" Expected", b);
+ emit log_named_uint(" Actual", a);
+
+ fail();
+ }
+
+ uint256 minBound = b - 1;
+
+ if (!((a == b) || (a == minBound))) {
+ emit log("Error: a == b || a == b-1");
+ emit log_named_uint(" Expected", b);
+ emit log_named_uint(" Actual", a);
+
+ fail();
+ }
+ }
+}
diff --git a/test/helpers/UniStaker.handler.sol b/test/helpers/UniStaker.handler.sol
new file mode 100644
index 0000000..a2d2c4e
--- /dev/null
+++ b/test/helpers/UniStaker.handler.sol
@@ -0,0 +1,224 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+pragma solidity ^0.8.13;
+
+import {CommonBase} from "forge-std/Base.sol";
+import {StdCheats} from "forge-std/StdCheats.sol";
+import {StdUtils} from "forge-std/StdUtils.sol";
+import {console} from "forge-std/console.sol";
+import {AddressSet, LibAddressSet} from "../helpers/AddressSet.sol";
+import {UniStaker} from "src/UniStaker.sol";
+import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol";
+
+contract UniStakerHandler is CommonBase, StdCheats, StdUtils {
+ using LibAddressSet for AddressSet;
+
+ // system setup
+ UniStaker public uniStaker;
+ IERC20 public stakeToken;
+ IERC20 public rewardToken;
+ address public admin;
+
+ // actors, deposit state
+ address internal _currentActor;
+ AddressSet internal _depositors;
+ AddressSet internal _delegates;
+ AddressSet internal _beneficiaries;
+ AddressSet internal _surrogates;
+ AddressSet internal _rewardNotifiers;
+ mapping(address => uint256[]) internal _depositIds;
+ mapping(bytes32 => uint256) public calls;
+
+ // ghost vars
+ uint256 public ghost_stakeSum;
+ uint256 public ghost_stakeWithdrawn;
+ uint256 public ghost_depositCount;
+ uint256 public ghost_rewardsClaimed;
+ uint256 public ghost_rewardsNotified;
+ uint256 public ghost_prevRewardPerTokenAccumulatedCheckpoint;
+
+ modifier countCall(bytes32 key) {
+ calls[key]++;
+ _;
+ }
+
+ modifier doCheckpoints() {
+ _checkpoint_ghost_prevRewardPerTokenAccumulatedCheckpoint();
+ _;
+ }
+
+ constructor(UniStaker _uniStaker) {
+ uniStaker = _uniStaker;
+ stakeToken = IERC20(address(_uniStaker.STAKE_TOKEN()));
+ rewardToken = IERC20(address(_uniStaker.REWARD_TOKEN()));
+ admin = uniStaker.admin();
+ }
+
+ function _mintStakeToken(address _to, uint256 _amount) internal {
+ vm.assume(_to != address(0));
+ deal(address(stakeToken), _to, _amount, true);
+ }
+
+ function _mintRewardToken(address _to, uint256 _amount) internal {
+ vm.assume(_to != address(0));
+ deal(address(rewardToken), _to, _amount, true);
+ }
+
+ function enableRewardNotifier(address _notifier)
+ public
+ countCall("enableRewardNotifier")
+ doCheckpoints
+ {
+ vm.assume(_notifier != address(0) && _notifier != address(uniStaker));
+ _rewardNotifiers.add(_notifier);
+ vm.prank(admin);
+ uniStaker.setRewardNotifier(_notifier, true);
+ }
+
+ function notifyRewardAmount(uint256 _amount, uint256 _actorSeed)
+ public
+ countCall("notifyRewardAmount")
+ doCheckpoints
+ {
+ _useActor(_rewardNotifiers, _actorSeed);
+ vm.assume(_currentActor != address(0));
+ _amount = _bound(_amount, 0, 100_000_000e18);
+ ghost_prevRewardPerTokenAccumulatedCheckpoint = uniStaker.rewardPerTokenAccumulatedCheckpoint();
+ _mintRewardToken(_currentActor, _amount);
+ vm.startPrank(_currentActor);
+ rewardToken.transfer(address(uniStaker), _amount);
+ uniStaker.notifyRewardAmount(_amount);
+ vm.stopPrank();
+ ghost_rewardsNotified += _amount;
+ }
+
+ function stake(uint96 _amount, address _delegatee, address _beneficiary)
+ public
+ countCall("stake")
+ doCheckpoints
+ {
+ _createDepositor();
+
+ _beneficiaries.add(_beneficiary);
+ _delegates.add(_delegatee);
+ _amount = uint96(_bound(_amount, 0, 100_000_000e18));
+
+ // assume user has stake amount
+ _mintStakeToken(_currentActor, _amount);
+
+ vm.startPrank(_currentActor);
+ stakeToken.approve(address(uniStaker), _amount);
+ uniStaker.stake(_amount, _delegatee, _beneficiary);
+ vm.stopPrank();
+
+ // update handler state
+ _depositIds[_currentActor].push(ghost_depositCount);
+ ghost_depositCount++;
+ _surrogates.add(address(uniStaker.surrogates(_delegatee)));
+ ghost_stakeSum += _amount;
+ }
+
+ function validStakeMore(uint96 _amount, uint256 _actorSeed, uint256 _actorDepositSeed)
+ public
+ countCall("validStakeMore")
+ doCheckpoints
+ {
+ _useActor(_depositors, _actorSeed);
+ vm.assume(_currentActor != address(0));
+ vm.assume(_depositIds[_currentActor].length > 0);
+ UniStaker.DepositIdentifier _depositId =
+ UniStaker.DepositIdentifier.wrap(_getActorRandDepositId(_actorDepositSeed));
+ (uint96 _balance,,,) = uniStaker.deposits(_depositId);
+ _amount = uint96(_bound(_amount, 0, _balance));
+ vm.startPrank(_currentActor);
+ stakeToken.approve(address(uniStaker), _amount);
+ uniStaker.stakeMore(_depositId, _amount);
+ vm.stopPrank();
+ ghost_stakeSum += _amount;
+ }
+
+ function validWithdraw(uint96 _amount, uint256 _actorSeed, uint256 _actorDepositSeed)
+ public
+ countCall("validWithdraw")
+ doCheckpoints
+ {
+ _useActor(_depositors, _actorSeed);
+ vm.assume(_currentActor != address(0));
+ vm.assume(_depositIds[_currentActor].length > 0);
+ UniStaker.DepositIdentifier _depositId =
+ UniStaker.DepositIdentifier.wrap(_getActorRandDepositId(_actorDepositSeed));
+ (uint96 _balance,,,) = uniStaker.deposits(_depositId);
+ _amount = uint96(_bound(_amount, 0, _balance));
+ vm.startPrank(_currentActor);
+ uniStaker.withdraw(_depositId, _amount);
+ vm.stopPrank();
+ ghost_stakeWithdrawn += _amount;
+ }
+
+ function claimReward(uint256 _actorSeed) public countCall("claimReward") doCheckpoints {
+ _useActor(_beneficiaries, _actorSeed);
+ vm.startPrank(_currentActor);
+ uint256 rewardsClaimed = uniStaker.claimReward();
+ vm.stopPrank();
+ ghost_rewardsClaimed += rewardsClaimed;
+ }
+
+ function warpAhead(uint256 _seconds) public countCall("warpAhead") doCheckpoints {
+ _seconds = _bound(_seconds, 0, uniStaker.REWARD_DURATION() * 2);
+ skip(_seconds);
+ }
+
+ function _getActorRandDepositId(uint256 _randomDepositSeed) internal view returns (uint256) {
+ return _depositIds[_currentActor][_randomDepositSeed % _depositIds[_currentActor].length];
+ }
+
+ function _createDepositor() internal {
+ _currentActor = msg.sender;
+ // Surrogates can't stake. We won't include them as potential depositors.
+ vm.assume(!_surrogates.contains(_currentActor));
+ _depositors.add(msg.sender);
+ }
+
+ function _useActor(AddressSet storage _set, uint256 _randomActorSeed) internal {
+ _currentActor = _set.rand(_randomActorSeed);
+ }
+
+ function reduceDepositors(uint256 acc, function(uint256,address) external returns (uint256) func)
+ public
+ returns (uint256)
+ {
+ return _depositors.reduce(acc, func);
+ }
+
+ function reduceBeneficiaries(
+ uint256 acc,
+ function(uint256,address) external returns (uint256) func
+ ) public returns (uint256) {
+ return _beneficiaries.reduce(acc, func);
+ }
+
+ function reduceDelegates(uint256 acc, function(uint256,address) external returns (uint256) func)
+ public
+ returns (uint256)
+ {
+ return _delegates.reduce(acc, func);
+ }
+
+ function _checkpoint_ghost_prevRewardPerTokenAccumulatedCheckpoint() internal {
+ ghost_prevRewardPerTokenAccumulatedCheckpoint = uniStaker.rewardPerTokenAccumulatedCheckpoint();
+ }
+
+ function callSummary() external view {
+ console.log("\nCall summary:");
+ console.log("-------------------");
+ console.log("stake", calls["stake"]);
+ console.log("validStakeMore", calls["validStakeMore"]);
+ console.log("validWithdraw", calls["validWithdraw"]);
+ console.log("claimReward", calls["claimReward"]);
+ console.log("enableRewardNotifier", calls["enableRewardNotifier"]);
+ console.log("notifyRewardAmount", calls["notifyRewardAmount"]);
+ console.log("warpAhead", calls["warpAhead"]);
+ console.log("-------------------\n");
+ }
+
+ receive() external payable {}
+}
diff --git a/test/helpers/interfaces/IERC20Mint.sol b/test/helpers/interfaces/IERC20Mint.sol
new file mode 100644
index 0000000..efada59
--- /dev/null
+++ b/test/helpers/interfaces/IERC20Mint.sol
@@ -0,0 +1,6 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+pragma solidity ^0.8.23;
+
+interface IERC20Mint {
+ function mint(address dst, uint256 rawAmount) external;
+}
diff --git a/test/mocks/MockERC20Votes.sol b/test/mocks/MockERC20Votes.sol
new file mode 100644
index 0000000..1451edf
--- /dev/null
+++ b/test/mocks/MockERC20Votes.sol
@@ -0,0 +1,104 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+pragma solidity ^0.8.23;
+
+import {IERC20Delegates} from "src/interfaces/IERC20Delegates.sol";
+import {ERC20, ERC20Permit} from "openzeppelin/token/ERC20/extensions/ERC20Permit.sol";
+
+/// @dev An ERC20Permit token that allows for public minting and mocks the delegation methods used
+/// in ERC20Votes governance tokens. It does not included check pointing functionality. This
+/// contract is intended only for use as a stand in for contracts that interface with ERC20Votes
+// tokens.
+contract ERC20VotesMock is IERC20Delegates, ERC20Permit {
+ /// @dev Track delegations for mocked delegation methods
+ mapping(address account => address delegate) private delegations;
+
+ constructor() ERC20("Governance Token", "GOV") ERC20Permit("Governance Token") {}
+
+ /// @dev Public mint function useful for testing
+ function mint(address _account, uint256 _value) public {
+ _mint(_account, _value);
+ }
+
+ /// @dev Mock delegation method
+ function delegate(address _delegatee) external {
+ delegations[msg.sender] = _delegatee;
+ }
+
+ /// @dev Mock method for fetching to which address the provided account last delegated
+ /// via `delegate`
+ function delegates(address _account) external view returns (address) {
+ return delegations[_account];
+ }
+
+ //---------------------------------------------------------------------------------------------//
+ // All methods below this line are overridden solely for the sake of disambiguating identical //
+ // method signatures for Solidity. No functionality is implemented and all parameters are //
+ // curried to the standard implementations from OpenZeppelin's ERC20 contract. //
+ //---------------------------------------------------------------------------------------------//
+
+ function allowance(address account, address spender)
+ public
+ view
+ override(IERC20Delegates, ERC20)
+ returns (uint256)
+ {
+ return ERC20.allowance(account, spender);
+ }
+
+ function balanceOf(address account)
+ public
+ view
+ override(IERC20Delegates, ERC20)
+ returns (uint256)
+ {
+ return ERC20.balanceOf(account);
+ }
+
+ function approve(address spender, uint256 rawAmount)
+ public
+ override(IERC20Delegates, ERC20)
+ returns (bool)
+ {
+ return ERC20.approve(spender, rawAmount);
+ }
+
+ function decimals() public view override(IERC20Delegates, ERC20) returns (uint8) {
+ return ERC20.decimals();
+ }
+
+ function symbol() public view override(IERC20Delegates, ERC20) returns (string memory) {
+ return ERC20.symbol();
+ }
+
+ function totalSupply() public view override(IERC20Delegates, ERC20) returns (uint256) {
+ return ERC20.totalSupply();
+ }
+
+ function transfer(address dst, uint256 rawAmount)
+ public
+ override(IERC20Delegates, ERC20)
+ returns (bool)
+ {
+ return ERC20.transfer(dst, rawAmount);
+ }
+
+ function transferFrom(address src, address dst, uint256 rawAmount)
+ public
+ override(IERC20Delegates, ERC20)
+ returns (bool)
+ {
+ return ERC20.transferFrom(src, dst, rawAmount);
+ }
+
+ function permit(
+ address owner,
+ address spender,
+ uint256 rawAmount,
+ uint256 deadline,
+ uint8 v,
+ bytes32 r,
+ bytes32 s
+ ) public override(IERC20Delegates, ERC20Permit) {
+ return ERC20Permit.permit(owner, spender, rawAmount, deadline, v, r, s);
+ }
+}