├── .editorconfig ├── .env.example ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierignore ├── .solhint.json ├── .vscode └── settings.json ├── LICENSE.md ├── README.md ├── airdrops ├── MerkleCreator.sol └── MerkleCreator.t.sol ├── bun.lock ├── flow ├── FlowBatchable.sol ├── FlowBatchable.t.sol ├── FlowStreamCreator.sol ├── FlowStreamCreator.t.sol ├── FlowStreamManager.sol └── FlowUtilities.sol ├── foundry.toml ├── lockup ├── BatchLDStreamCreator.sol ├── BatchLLStreamCreator.sol ├── BatchLTStreamCreator.sol ├── BatchStreamCreator.t.sol ├── LockupDynamicCurvesCreator.sol ├── LockupDynamicCurvesCreator.t.sol ├── LockupDynamicStreamCreator.sol ├── LockupLinearCurvesCreator.sol ├── LockupLinearCurvesCreator.t.sol ├── LockupLinearStreamCreator.sol ├── LockupStreamCreator.t.sol ├── LockupTranchedCurvesCreator.sol ├── LockupTranchedCurvesCreator.t.sol ├── LockupTranchedStreamCreator.sol ├── RecipientHooks.sol ├── StakeSablierNFT.sol ├── StreamManagement.sol ├── StreamManagementWithHook.sol ├── StreamManagementWithHook.t.sol └── tests │ └── stake-sablier-nft-test │ ├── StakeSablierNFT.t.sol │ ├── claim-rewards │ ├── claimRewards.t.sol │ └── claimRewards.tree │ ├── stake │ ├── stake.t.sol │ └── stake.tree │ └── unstake │ ├── unstake.t.sol │ └── unstake.tree └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # All files 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | indent_size = 2 11 | indent_style = space 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | 15 | [*.sol] 16 | indent_size = 4 17 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | export MAINNET_RPC_URL="YOUR_MAINNET_RPC_URL" 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | 3 | env: 4 | MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} 5 | FOUNDRY_PROFILE: "ci" 6 | 7 | on: 8 | workflow_dispatch: 9 | pull_request: 10 | push: 11 | branches: 12 | - "main" 13 | 14 | jobs: 15 | lint: 16 | runs-on: "ubuntu-latest" 17 | steps: 18 | - name: "Check out the repo" 19 | uses: "actions/checkout@v4" 20 | 21 | - name: "Install Foundry" 22 | uses: "foundry-rs/foundry-toolchain@v1" 23 | 24 | - name: "Install Bun" 25 | uses: "oven-sh/setup-bun@v1" 26 | 27 | - name: "Install the Node.js dependencies" 28 | run: "bun install" 29 | 30 | - name: "Lint the code" 31 | run: "bun run lint" 32 | 33 | - name: "Add lint summary" 34 | run: | 35 | echo "## Lint result" >> $GITHUB_STEP_SUMMARY 36 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 37 | 38 | build: 39 | runs-on: "ubuntu-latest" 40 | steps: 41 | - name: "Check out the repo" 42 | uses: "actions/checkout@v4" 43 | 44 | - name: "Install Foundry" 45 | uses: "foundry-rs/foundry-toolchain@v1" 46 | 47 | - name: "Install Bun" 48 | uses: "oven-sh/setup-bun@v1" 49 | 50 | - name: "Install the Node.js dependencies" 51 | run: "bun install" 52 | 53 | - name: "Build the contracts" 54 | run: "bun run build" 55 | 56 | - name: "Add build summary" 57 | run: | 58 | echo "## Build result" >> $GITHUB_STEP_SUMMARY 59 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 60 | 61 | test: 62 | needs: ["lint", "build"] 63 | runs-on: "ubuntu-latest" 64 | steps: 65 | - name: "Check out the repo" 66 | uses: "actions/checkout@v4" 67 | 68 | - name: "Install Foundry" 69 | uses: "foundry-rs/foundry-toolchain@v1" 70 | 71 | - name: "Install Bun" 72 | uses: "oven-sh/setup-bun@v1" 73 | 74 | - name: "Install the Node.js dependencies" 75 | run: "bun install" 76 | 77 | - name: "Show the Foundry config" 78 | run: "forge config" 79 | 80 | - name: "Run the tests" 81 | run: "bun run test" 82 | 83 | - name: "Add test summary" 84 | run: | 85 | echo "## Tests result" >> $GITHUB_STEP_SUMMARY 86 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 87 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # directories 2 | cache 3 | node_modules 4 | out 5 | 6 | # files 7 | *.env 8 | *.log 9 | .DS_Store 10 | .pnp.* 11 | bun.lockb 12 | package-lock.json 13 | pnpm-lock.yaml 14 | yarn.lock 15 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # directories 2 | broadcast 3 | cache 4 | node_modules 5 | out 6 | 7 | # files 8 | *.env 9 | *.log 10 | .DS_Store 11 | .pnp.* 12 | lcov.info 13 | bun.lockb 14 | package-lock.json 15 | pnpm-lock.yaml 16 | yarn.lock 17 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "code-complexity": ["error", 8], 5 | "compiler-version": ["error", ">=0.8.13"], 6 | "contract-name-capwords": "off", 7 | "func-name-mixedcase": "off", 8 | "func-visibility": ["error", { "ignoreConstructors": true }], 9 | "gas-custom-errors": "off", 10 | "immutable-vars-naming": "off", 11 | "max-line-length": ["error", 124], 12 | "named-parameters-mapping": "warn", 13 | "no-console": "off", 14 | "no-empty-blocks": "off", 15 | "not-rely-on-time": "off", 16 | "one-contract-per-file": "off" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[solidity]": { 3 | "editor.defaultFormatter": "NomicFoundation.hardhat-solidity" 4 | }, 5 | "solidity.formatter": "forge" 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # GNU General Public License 2 | 3 | _Version 3, 29 June 2007_ _Copyright © 2007 Free Software Foundation, Inc. <>_ 4 | 5 | Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. 6 | 7 | ## Preamble 8 | 9 | The GNU General Public License is a free, copyleft license for software and other kinds of works. 10 | 11 | The licenses for most software and other practical works are designed to take away your freedom to share and change the 12 | works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all 13 | versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use 14 | the GNU General Public License for most of our software; it applies also to any other work released this way by its 15 | authors. You can apply it to your programs, too. 16 | 17 | When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make 18 | sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive 19 | 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 20 | that you know you can do these things. 21 | 22 | To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. 23 | Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: 24 | responsibilities to respect the freedom of others. 25 | 26 | For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients 27 | the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must 28 | show them these terms so they know their rights. 29 | 30 | Developers that use the GNU GPL protect your rights with two steps: **(1)** assert copyright on the software, and 31 | **(2)** offer you this License giving you legal permission to copy, distribute and/or modify it. 32 | 33 | For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. 34 | For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems 35 | will not be attributed erroneously to authors of previous versions. 36 | 37 | Some devices are designed to deny users access to install or run modified versions of the software inside them, although 38 | the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the 39 | software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely 40 | where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those 41 | products. If such problems arise substantially in other domains, we stand ready to extend this provision to those 42 | domains in future versions of the GPL, as needed to protect the freedom of users. 43 | 44 | Finally, every program is threatened constantly by software patents. States should not allow patents to restrict 45 | development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger 46 | that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that 47 | patents cannot be used to render the program non-free. 48 | 49 | The precise terms and conditions for copying, distribution and modification follow. 50 | 51 | ## TERMS AND CONDITIONS 52 | 53 | ### 0. Definitions 54 | 55 | “This License” refers to version 3 of the GNU General Public License. 56 | 57 | “Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. 58 | 59 | “The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. 60 | “Licensees” and “recipients” may be individuals or organizations. 61 | 62 | To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, 63 | other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work 64 | “based on” the earlier work. 65 | 66 | A “covered work” means either the unmodified Program or a work based on the Program. 67 | 68 | To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily 69 | liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. 70 | Propagation includes copying, distribution (with or without modification), making available to the public, and in some 71 | countries other activities as well. 72 | 73 | To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction 74 | with a user through a computer network, with no transfer of a copy, is not conveying. 75 | 76 | An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and 77 | prominently visible feature that **(1)** displays an appropriate copyright notice, and **(2)** tells the user that there 78 | is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work 79 | under this License, and how to view a copy of this License. If the interface presents a list of user commands or 80 | options, such as a menu, a prominent item in the list meets this criterion. 81 | 82 | ### 1. Source Code 83 | 84 | The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means 85 | any non-source form of a work. 86 | 87 | A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, 88 | or, in the case of interfaces specified for a particular programming language, one that is widely used among developers 89 | working in that language. 90 | 91 | The “System Libraries” of an executable work include anything, other than the work as a whole, that **(a)** is included 92 | in the normal form of packaging a Major Component, but which is not part of that Major Component, and **(b)** serves 93 | only to enable use of the work with that Major Component, or to implement a Standard Interface for which an 94 | implementation is available to the public in source code form. A “Major Component”, in this context, means a major 95 | essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable 96 | work runs, or a compiler used to produce the work, or an object code interpreter used to run it. 97 | 98 | The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and 99 | (for an executable work) run the object code and to modify the work, including scripts to control those activities. 100 | However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs 101 | which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding 102 | Source includes interface definition files associated with source files for the work, and the source code for shared 103 | libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data 104 | communication or control flow between those subprograms and other parts of the work. 105 | 106 | The Corresponding Source need not include anything that users can regenerate automatically from other parts of the 107 | Corresponding Source. 108 | 109 | The Corresponding Source for a work in source code form is that same work. 110 | 111 | ### 2. Basic Permissions 112 | 113 | All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided 114 | the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. 115 | The output from running a covered work is covered by this License only if the output, given its content, constitutes a 116 | covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. 117 | 118 | You may make, run and propagate covered works that you do not convey, without conditions so long as your license 119 | otherwise remains in force. You may convey covered works to others for the sole purpose of having them make 120 | modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with 121 | the terms of this License in conveying all material for which you do not control copyright. Those thus making or running 122 | the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that 123 | prohibit them from making any copies of your copyrighted material outside their relationship with you. 124 | 125 | Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not 126 | allowed; section 10 makes it unnecessary. 127 | 128 | ### 3. Protecting Users' Legal Rights From Anti-Circumvention Law 129 | 130 | No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling 131 | obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or 132 | restricting circumvention of such measures. 133 | 134 | When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the 135 | extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you 136 | disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, 137 | your or third parties' legal rights to forbid circumvention of technological measures. 138 | 139 | ### 4. Conveying Verbatim Copies 140 | 141 | You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you 142 | conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating 143 | that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices 144 | of the absence of any warranty; and give all recipients a copy of this License along with the Program. 145 | 146 | You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for 147 | a fee. 148 | 149 | ### 5. Conveying Modified Source Versions 150 | 151 | You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source 152 | code under the terms of section 4, provided that you also meet all of these conditions: 153 | 154 | - **a)** The work must carry prominent notices stating that you modified it, and giving a relevant date. 155 | - **b)** The work must carry prominent notices stating that it is released under this License and any conditions added 156 | under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”. 157 | - **c)** You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. 158 | This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and 159 | all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other 160 | way, but it does not invalidate such permission if you have separately received it. 161 | - **d)** If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the 162 | Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. 163 | 164 | A compilation of a covered work with other separate and independent works, which are not by their nature extensions of 165 | 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 166 | distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the 167 | access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work 168 | in an aggregate does not cause this License to apply to the other parts of the aggregate. 169 | 170 | ### 6. Conveying Non-Source Forms 171 | 172 | You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the 173 | machine-readable Corresponding Source under the terms of this License, in one of these ways: 174 | 175 | - **a)** Convey the object code in, or embodied in, a physical product (including a physical distribution medium), 176 | accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. 177 | - **b)** Convey the object code in, or embodied in, a physical product (including a physical distribution medium), 178 | accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or 179 | customer support for that product model, to give anyone who possesses the object code either **(1)** a copy of the 180 | Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium 181 | customarily used for software interchange, for a price no more than your reasonable cost of physically performing this 182 | conveying of source, or **(2)** access to copy the Corresponding Source from a network server at no charge. 183 | - **c)** Convey individual copies of the object code with a copy of the written offer to provide the Corresponding 184 | Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code 185 | with such an offer, in accord with subsection 6b. 186 | - **d)** Convey the object code by offering access from a designated place (gratis or for a charge), and offer 187 | equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need 188 | not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object 189 | code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) 190 | that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying 191 | where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated 192 | to ensure that it is available for as long as needed to satisfy these requirements. 193 | - **e)** Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code 194 | and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. 195 | 196 | A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, 197 | need not be included in conveying the object code work. 198 | 199 | A “User Product” is either **(1)** a “consumer product”, which means any tangible personal property which is normally 200 | used for personal, family, or household purposes, or **(2)** anything designed or sold for incorporation into a 201 | dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. 202 | For a particular product received by a particular user, “normally used” refers to a typical or common use of that class 203 | of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or 204 | expects or is expected to use, the product. A product is a consumer product regardless of whether the product has 205 | substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of 206 | the product. 207 | 208 | “Installation Information” for a User Product means any methods, procedures, authorization keys, or other information 209 | required to install and execute modified versions of a covered work in that User Product from a modified version of its 210 | Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code 211 | is in no case prevented or interfered with solely because modification has been made. 212 | 213 | If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the 214 | conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to 215 | the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding 216 | Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not 217 | apply if neither you nor any third party retains the ability to install modified object code on the User Product (for 218 | example, the work has been installed in ROM). 219 | 220 | The requirement to provide Installation Information does not include a requirement to continue to provide support 221 | service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product 222 | in which it has been modified or installed. Access to a network may be denied when the modification itself materially 223 | and adversely affects the operation of the network or violates the rules and protocols for communication across the 224 | network. 225 | 226 | Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format 227 | that is publicly documented (and with an implementation available to the public in source code form), and must require 228 | no special password or key for unpacking, reading or copying. 229 | 230 | ### 7. Additional Terms 231 | 232 | “Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of 233 | its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were 234 | included in this License, to the extent that they are valid under applicable law. If additional permissions apply only 235 | to part of the Program, that part may be used separately under those permissions, but the entire Program remains 236 | governed by this License without regard to the additional permissions. 237 | 238 | When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or 239 | from any part of it. (Additional permissions may be written to require their own removal in certain cases when you 240 | modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have 241 | or can give appropriate copyright permission. 242 | 243 | Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by 244 | the copyright holders of that material) supplement the terms of this License with terms: 245 | 246 | - **a)** Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or 247 | - **b)** Requiring preservation of specified reasonable legal notices or author attributions in that material or in the 248 | Appropriate Legal Notices displayed by works containing it; or 249 | - **c)** Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such 250 | material be marked in reasonable ways as different from the original version; or 251 | - **d)** Limiting the use for publicity purposes of names of licensors or authors of the material; or 252 | - **e)** Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or 253 | - **f)** Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or 254 | modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these 255 | contractual assumptions directly impose on those licensors and authors. 256 | 257 | All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the 258 | Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with 259 | a term that is a further restriction, you may remove that term. If a license document contains a further restriction but 260 | permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of 261 | that license document, provided that the further restriction does not survive such relicensing or conveying. 262 | 263 | If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a 264 | statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. 265 | 266 | Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as 267 | exceptions; the above requirements apply either way. 268 | 269 | ### 8. Termination 270 | 271 | You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to 272 | propagate or modify it is void, and will automatically terminate your rights under this License (including any patent 273 | licenses granted under the third paragraph of section 11). 274 | 275 | However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated 276 | **(a)** provisionally, unless and until the copyright holder explicitly and finally terminates your license, and **(b)** 277 | permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days 278 | after the cessation. 279 | 280 | Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you 281 | of the violation by some reasonable means, this is the first time you have received notice of violation of this License 282 | (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. 283 | 284 | Termination of your rights under this section does not terminate the licenses of parties who have received copies or 285 | rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not 286 | qualify to receive new licenses for the same material under section 10. 287 | 288 | ### 9. Acceptance Not Required for Having Copies 289 | 290 | You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a 291 | covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not 292 | require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered 293 | work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a 294 | covered work, you indicate your acceptance of this License to do so. 295 | 296 | ### 10. Automatic Licensing of Downstream Recipients 297 | 298 | Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, 299 | modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third 300 | parties with this License. 301 | 302 | An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or 303 | subdividing an organization, or merging organizations. If propagation of a covered work results from an entity 304 | transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work 305 | the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the 306 | Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with 307 | reasonable efforts. 308 | 309 | You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For 310 | example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, 311 | and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent 312 | claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 313 | 314 | ### 11. Patents 315 | 316 | A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the 317 | Program is based. The work thus licensed is called the contributor's “contributor version”. 318 | 319 | A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already 320 | acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or 321 | selling its contributor version, but do not include claims that would be infringed only as a consequence of further 322 | modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent 323 | sublicenses in a manner consistent with the requirements of this License. 324 | 325 | Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential 326 | patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its 327 | contributor version. 328 | 329 | In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not 330 | to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). 331 | To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent 332 | against the party. 333 | 334 | If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not 335 | available for anyone to copy, free of charge and under the terms of this License, through a publicly available network 336 | server or other readily accessible means, then you must either **(1)** cause the Corresponding Source to be so 337 | available, or **(2)** arrange to deprive yourself of the benefit of the patent license for this particular work, or 338 | **(3)** arrange, in a manner consistent with the requirements of this License, to extend the patent license to 339 | downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your 340 | conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or 341 | more identifiable patents in that country that you have reason to believe are valid. 342 | 343 | If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring 344 | conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing 345 | them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is 346 | automatically extended to all recipients of the covered work and works based on it. 347 | 348 | A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, 349 | or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You 350 | may not convey a covered work if you are a party to an arrangement with a third party that is in the business of 351 | distributing software, under which you make payment to the third party based on the extent of your activity of conveying 352 | the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a 353 | discriminatory patent license **(a)** in connection with copies of the covered work conveyed by you (or copies made from 354 | those copies), or **(b)** primarily for and in connection with specific products or compilations that contain the 355 | covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. 356 | 357 | Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to 358 | infringement that may otherwise be available to you under applicable patent law. 359 | 360 | ### 12. No Surrender of Others' Freedom 361 | 362 | If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this 363 | License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to 364 | satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence 365 | you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further 366 | conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License 367 | would be to refrain entirely from conveying the Program. 368 | 369 | ### 13. Use with the GNU Affero General Public License 370 | 371 | Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work 372 | licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the 373 | resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special 374 | requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply 375 | to the combination as such. 376 | 377 | ### 14. Revised Versions of this License 378 | 379 | The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to 380 | time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new 381 | problems or concerns. 382 | 383 | Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the 384 | GNU General Public License “or any later version” applies to it, you have the option of following the terms and 385 | conditions either of that numbered version or of any later version published by the Free Software Foundation. If the 386 | Program does not specify a version number of the GNU General Public License, you may choose any version ever published 387 | by the Free Software Foundation. 388 | 389 | If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, 390 | that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the 391 | Program. 392 | 393 | Later license versions may give you additional or different permissions. However, no additional obligations are imposed 394 | on any author or copyright holder as a result of your choosing to follow a later version. 395 | 396 | ### 15. Disclaimer of Warranty 397 | 398 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING 399 | THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR 400 | IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. 401 | THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU 402 | ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 403 | 404 | ### 16. Limitation of Liability 405 | 406 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO 407 | MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, 408 | INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO 409 | LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM 410 | TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 411 | DAMAGES. 412 | 413 | ### 17. Interpretation of Sections 15 and 16 414 | 415 | If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to 416 | their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil 417 | liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program 418 | in return for a fee. 419 | 420 | _END OF TERMS AND CONDITIONS_ 421 | 422 | ## How to Apply These Terms to Your New Programs 423 | 424 | If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve 425 | this is to make it free software which everyone can redistribute and change under these terms. 426 | 427 | To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to 428 | most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer 429 | to where the full notice is found. 430 | 431 | 432 | Copyright (C) 433 | 434 | This program is free software: you can redistribute it and/or modify 435 | it under the terms of the GNU General Public License as published by 436 | the Free Software Foundation, either version 3 of the License, or 437 | (at your option) any later version. 438 | 439 | This program is distributed in the hope that it will be useful, 440 | but WITHOUT ANY WARRANTY; without even the implied warranty of 441 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 442 | GNU General Public License for more details. 443 | 444 | You should have received a copy of the GNU General Public License 445 | along with this program. If not, see . 446 | 447 | Also add information on how to contact you by electronic and paper mail. 448 | 449 | If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: 450 | 451 | Copyright (C) 452 | This program comes with ABSOLUTELY NO WARRANTY; for details type 'show w'. 453 | This is free software, and you are welcome to redistribute it 454 | under certain conditions; type 'show c' for details. 455 | 456 | The hypothetical commands `show w` and `show c` should show the appropriate parts of the General Public License. Of 457 | course, your program's commands might be different; for a GUI interface, you would use an “about box”. 458 | 459 | You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for 460 | the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see 461 | <>. 462 | 463 | The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is 464 | a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If 465 | this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read 466 | <>. 467 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sablier Examples 2 | 3 | This repository contains example integrations with the Sablier Protocol. More detailed guides walking through the logic of the examples can be found on the [Sablier docs](https://docs.sablier.com) website. 4 | 5 | ## Disclaimer 6 | 7 | The examples provided in this repo have NOT BEEN AUDITED and is provided "AS IS" with no warranties of any kind, either 8 | express or implied. It is intended solely for demonstration purposes. These examples should NOT be used in a production 9 | environment. It makes specific assumptions that may not apply to your particular needs. 10 | 11 | ## Contributing 12 | 13 | Make sure you have [Foundry](https://github.com/foundry-rs/foundry) installed, and that you have it configured correctly in [VSCode](https://book.getfoundry.sh/config/vscode). 14 | 15 | ## License 16 | 17 | This repo is licensed under GPL 3-0 or later. 18 | -------------------------------------------------------------------------------- /airdrops/MerkleCreator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import { ud2x18 } from "@prb/math/src/UD2x18.sol"; 6 | import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; 7 | import { ISablierMerkleInstant } from "@sablier/airdrops/src/interfaces/ISablierMerkleInstant.sol"; 8 | import { ISablierMerkleLL } from "@sablier/airdrops/src/interfaces/ISablierMerkleLL.sol"; 9 | import { ISablierMerkleLT } from "@sablier/airdrops/src/interfaces/ISablierMerkleLT.sol"; 10 | import { ISablierMerkleFactory } from "@sablier/airdrops/src/interfaces/ISablierMerkleFactory.sol"; 11 | import { MerkleBase, MerkleLL, MerkleLT } from "@sablier/airdrops/src/types/DataTypes.sol"; 12 | 13 | /// @notice Example of how to create Merkle airdrop campaigns. 14 | /// @dev This code is referenced in the docs: https://docs.sablier.com/guides/airdrops/examples/create-campaign 15 | contract MerkleCreator { 16 | // Mainnet addresses 17 | IERC20 public constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); 18 | 19 | // See https://docs.sablier.com/guides/lockup/deployments for all deployments 20 | ISablierMerkleFactory public constant FACTORY = ISablierMerkleFactory(0x71DD3Ca88E7564416E5C2E350090C12Bf8F6144a); 21 | ISablierLockup public constant LOCKUP = ISablierLockup(0x7C01AA3783577E15fD7e272443D44B92d5b21056); 22 | 23 | function createMerkleInstant() public virtual returns (ISablierMerkleInstant merkleInstant) { 24 | // Declare the constructor parameter of MerkleBase. 25 | MerkleBase.ConstructorParams memory baseParams; 26 | 27 | // Set the base parameters. 28 | baseParams.token = DAI; 29 | baseParams.expiration = uint40(block.timestamp + 12 weeks); // The expiration of the campaign 30 | baseParams.initialAdmin = address(0xBeeF); // Admin of the merkle lockup contract 31 | baseParams.ipfsCID = "QmT5NvUtoM5nWFfrQdVrFtvGfKFmG7AHE8P34isapyhCxX"; // IPFS hash of the campaign metadata 32 | baseParams.merkleRoot = 0x4e07408562bedb8b60ce05c1decfe3ad16b722309875f562c03d02d7aaacb123; 33 | baseParams.campaignName = "My First Campaign"; // Unique campaign name 34 | baseParams.shape = "A custom stream shape"; // Stream shape name for visualization in the UI 35 | 36 | // The total amount of tokens you want to airdrop to your users. 37 | uint256 aggregateAmount = 100_000_000e18; 38 | 39 | // The total number of addresses you want to airdrop your tokens to. 40 | uint256 recipientCount = 10_000; 41 | 42 | // Deploy the MerkleInstant campaign contract. The deployed contract will be completely owned by the campaign 43 | // admin. Recipients will interact with the deployed contract to claim their airdrop. 44 | merkleInstant = FACTORY.createMerkleInstant(baseParams, aggregateAmount, recipientCount); 45 | } 46 | 47 | function createMerkleLL() public returns (ISablierMerkleLL merkleLL) { 48 | // Declare the constructor parameter of MerkleBase. 49 | MerkleBase.ConstructorParams memory baseParams; 50 | 51 | // Set the base parameters. 52 | baseParams.token = DAI; 53 | baseParams.expiration = uint40(block.timestamp + 12 weeks); // The expiration of the campaign 54 | baseParams.initialAdmin = address(0xBeeF); // Admin of the merkle lockup contract 55 | baseParams.ipfsCID = "QmT5NvUtoM5nWFfrQdVrFtvGfKFmG7AHE8P34isapyhCxX"; // IPFS hash of the campaign metadata 56 | baseParams.merkleRoot = 0x4e07408562bedb8b60ce05c1decfe3ad16b722309875f562c03d02d7aaacb123; 57 | baseParams.campaignName = "My First Campaign"; // Unique campaign name 58 | baseParams.shape = "A custom stream shape"; // Stream shape name for visualization in the UI 59 | 60 | // The total amount of tokens you want to airdrop to your users. 61 | uint256 aggregateAmount = 100_000_000e18; 62 | 63 | // The total number of addresses you want to airdrop your tokens to. 64 | uint256 recipientCount = 10_000; 65 | 66 | // Set the schedule of the stream that will be created from this campaign. 67 | MerkleLL.Schedule memory schedule = MerkleLL.Schedule({ 68 | startTime: 0, // i.e. block.timestamp 69 | startPercentage: ud2x18(0.01e18), 70 | cliffDuration: 30 days, 71 | cliffPercentage: ud2x18(0.01e18), 72 | totalDuration: 90 days 73 | }); 74 | 75 | // Deploy the MerkleLL campaign contract. The deployed contract will be completely owned by the campaign admin. 76 | // Recipients will interact with the deployed contract to claim their airdrop. 77 | merkleLL = FACTORY.createMerkleLL({ 78 | baseParams: baseParams, 79 | lockup: LOCKUP, 80 | cancelable: false, 81 | transferable: true, 82 | schedule: schedule, 83 | aggregateAmount: aggregateAmount, 84 | recipientCount: recipientCount 85 | }); 86 | } 87 | 88 | function createMerkleLT() public returns (ISablierMerkleLT merkleLT) { 89 | // Prepare the constructor parameters. 90 | MerkleBase.ConstructorParams memory baseParams; 91 | 92 | // Set the base parameters. 93 | baseParams.token = DAI; 94 | baseParams.expiration = uint40(block.timestamp + 12 weeks); // The expiration of the campaign 95 | baseParams.initialAdmin = address(0xBeeF); // Admin of the merkle lockup contract 96 | baseParams.ipfsCID = "QmT5NvUtoM5nWFfrQdVrFtvGfKFmG7AHE8P34isapyhCxX"; // IPFS hash of the campaign metadata 97 | baseParams.merkleRoot = 0x4e07408562bedb8b60ce05c1decfe3ad16b722309875f562c03d02d7aaacb123; 98 | baseParams.campaignName = "My First Campaign"; // Unique campaign name 99 | baseParams.shape = "A custom stream shape"; // Stream shape name for visualization in the UI 100 | 101 | // The tranches with their unlock percentages and durations. 102 | MerkleLT.TrancheWithPercentage[] memory tranchesWithPercentages = new MerkleLT.TrancheWithPercentage[](2); 103 | tranchesWithPercentages[0] = 104 | MerkleLT.TrancheWithPercentage({ unlockPercentage: ud2x18(0.5e18), duration: 30 days }); 105 | tranchesWithPercentages[1] = 106 | MerkleLT.TrancheWithPercentage({ unlockPercentage: ud2x18(0.5e18), duration: 60 days }); 107 | 108 | // The total amount of tokens you want to airdrop to your users. 109 | uint256 aggregateAmount = 100_000_000e18; 110 | 111 | // The total number of addresses you want to airdrop your tokens to. 112 | uint256 recipientCount = 10_000; 113 | 114 | // Deploy the MerkleLT campaign contract. The deployed contract will be completely owned by the campaign admin. 115 | // Recipients will interact with the deployed contract to claim their airdrop. 116 | merkleLT = FACTORY.createMerkleLT({ 117 | baseParams: baseParams, 118 | lockup: LOCKUP, 119 | cancelable: true, 120 | transferable: true, 121 | streamStartTime: 0, // i.e. block.timestamp 122 | tranchesWithPercentages: tranchesWithPercentages, 123 | aggregateAmount: aggregateAmount, 124 | recipientCount: recipientCount 125 | }); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /airdrops/MerkleCreator.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3-0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { ISablierMerkleInstant } from "@sablier/airdrops/src/interfaces/ISablierMerkleInstant.sol"; 5 | import { ISablierMerkleLL } from "@sablier/airdrops/src/interfaces/ISablierMerkleLL.sol"; 6 | import { ISablierMerkleLT } from "@sablier/airdrops/src/interfaces/ISablierMerkleLT.sol"; 7 | import { Test } from "forge-std/src/Test.sol"; 8 | 9 | import { MerkleCreator } from "./MerkleCreator.sol"; 10 | 11 | contract MerkleCreatorTest is Test { 12 | // Test contract 13 | MerkleCreator internal merkleCreator; 14 | 15 | address internal user; 16 | 17 | function setUp() public { 18 | // Fork Ethereum Mainnet 19 | vm.createSelectFork("mainnet"); 20 | 21 | // Deploy the Merkle creator 22 | merkleCreator = new MerkleCreator(); 23 | 24 | // Create a test user 25 | user = payable(makeAddr("User")); 26 | vm.deal({ account: user, newBalance: 1 ether }); 27 | 28 | // Make the test user the `msg.sender` in all following calls 29 | vm.startPrank({ msgSender: user }); 30 | } 31 | 32 | // Test creating the MerkleInstant campaign. 33 | function test_CreateMerkleInstant() public { 34 | ISablierMerkleInstant merkleInstant = merkleCreator.createMerkleInstant(); 35 | 36 | // Assert the merkleLL contract was created with correct params 37 | assertEq(address(0xBeeF), merkleInstant.admin(), "admin"); 38 | assertEq( 39 | 0x4e07408562bedb8b60ce05c1decfe3ad16b722309875f562c03d02d7aaacb123, 40 | merkleInstant.MERKLE_ROOT(), 41 | "merkle-root" 42 | ); 43 | } 44 | 45 | // Test creating the MerkleLL campaign. 46 | function test_CreateMerkleLL() public { 47 | ISablierMerkleLL merkleLL = merkleCreator.createMerkleLL(); 48 | 49 | // Assert the merkleLL contract was created with correct params 50 | assertEq(address(0xBeeF), merkleLL.admin(), "admin"); 51 | assertEq( 52 | 0x4e07408562bedb8b60ce05c1decfe3ad16b722309875f562c03d02d7aaacb123, merkleLL.MERKLE_ROOT(), "merkle-root" 53 | ); 54 | } 55 | 56 | // Test creating the MerkleLT campaign. 57 | function test_CreateMerkleLT() public { 58 | ISablierMerkleLT merkleLT = merkleCreator.createMerkleLT(); 59 | 60 | // Assert the merkleLT contract was created with correct params 61 | assertEq(address(0xBeeF), merkleLT.admin(), "admin"); 62 | assertEq( 63 | 0x4e07408562bedb8b60ce05c1decfe3ad16b722309875f562c03d02d7aaacb123, merkleLT.MERKLE_ROOT(), "merkle-root" 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "workspaces": { 4 | "": { 5 | "name": "examples", 6 | "dependencies": { 7 | "@openzeppelin/contracts": "5.0.2", 8 | "@prb/math": "4.1.0", 9 | "@sablier/airdrops": "1.3.0", 10 | "@sablier/flow": "1.1.0", 11 | "@sablier/lockup": "2.0.0" 12 | }, 13 | "devDependencies": { 14 | "forge-std": "github:foundry-rs/forge-std#v1.8.1", 15 | "prettier": "^2.8.8", 16 | "solhint": "^5.0.3" 17 | } 18 | } 19 | }, 20 | "packages": { 21 | "@babel/code-frame": [ 22 | "@babel/code-frame@7.26.2", 23 | "", 24 | { 25 | "dependencies": { 26 | "@babel/helper-validator-identifier": "^7.25.9", 27 | "js-tokens": "^4.0.0", 28 | "picocolors": "^1.0.0" 29 | } 30 | }, 31 | "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==" 32 | ], 33 | 34 | "@babel/helper-validator-identifier": [ 35 | "@babel/helper-validator-identifier@7.25.9", 36 | "", 37 | {}, 38 | "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==" 39 | ], 40 | 41 | "@openzeppelin/contracts": [ 42 | "@openzeppelin/contracts@5.0.2", 43 | "", 44 | {}, 45 | "sha512-ytPc6eLGcHHnapAZ9S+5qsdomhjo6QBHTDRRBFfTxXIpsicMhVPouPgmUPebZZZGX7vt9USA+Z+0M0dSVtSUEA==" 46 | ], 47 | 48 | "@pnpm/config.env-replace": [ 49 | "@pnpm/config.env-replace@1.1.0", 50 | "", 51 | {}, 52 | "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==" 53 | ], 54 | 55 | "@pnpm/network.ca-file": [ 56 | "@pnpm/network.ca-file@1.0.2", 57 | "", 58 | { "dependencies": { "graceful-fs": "4.2.10" } }, 59 | "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==" 60 | ], 61 | 62 | "@pnpm/npm-conf": [ 63 | "@pnpm/npm-conf@2.3.1", 64 | "", 65 | { 66 | "dependencies": { 67 | "@pnpm/config.env-replace": "^1.1.0", 68 | "@pnpm/network.ca-file": "^1.0.1", 69 | "config-chain": "^1.1.11" 70 | } 71 | }, 72 | "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==" 73 | ], 74 | 75 | "@prb/math": [ 76 | "@prb/math@4.1.0", 77 | "", 78 | {}, 79 | "sha512-ef5Xrlh3BeX4xT5/Wi810dpEPq2bYPndRxgFIaKSU1F/Op/s8af03kyom+mfU7gEpvfIZ46xu8W0duiHplbBMg==" 80 | ], 81 | 82 | "@sablier/airdrops": [ 83 | "@sablier/airdrops@1.3.0", 84 | "", 85 | { 86 | "dependencies": { 87 | "@openzeppelin/contracts": "5.0.2", 88 | "@prb/math": "4.1.0", 89 | "@sablier/lockup": "2.0.0" 90 | } 91 | }, 92 | "sha512-rwyPJl7HX6iT0e/w3Wc2l5iq3xaFjiPMGFWinobBGeS3bxINHpSPaGH/Q1ffbGbwbkHS2ygZNfXte0uhR8aqbw==" 93 | ], 94 | 95 | "@sablier/flow": [ 96 | "@sablier/flow@1.1.0", 97 | "", 98 | { 99 | "dependencies": { 100 | "@openzeppelin/contracts": "5.0.2", 101 | "@prb/math": "4.1.0" 102 | } 103 | }, 104 | "sha512-kc75VmnrZkrFqZ3S96hAbs6LNiwENPnWlbKgMutvrV/ulc72HXlguwANbfWJK7lBC7jlBh6oXeE11maiLLyyGA==" 105 | ], 106 | 107 | "@sablier/lockup": [ 108 | "@sablier/lockup@2.0.0", 109 | "", 110 | { 111 | "dependencies": { 112 | "@openzeppelin/contracts": "5.0.2", 113 | "@prb/math": "4.1.0" 114 | } 115 | }, 116 | "sha512-ADuody1aF75MccFiPlwMkbVfuWbda9jYvZwEpQwZyxjS943OcH7ZY+ii477oxEYBB1xy8zW8INryl+jLP3HrXA==" 117 | ], 118 | 119 | "@sindresorhus/is": [ 120 | "@sindresorhus/is@5.6.0", 121 | "", 122 | {}, 123 | "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==" 124 | ], 125 | 126 | "@solidity-parser/parser": [ 127 | "@solidity-parser/parser@0.19.0", 128 | "", 129 | {}, 130 | "sha512-RV16k/qIxW/wWc+mLzV3ARyKUaMUTBy9tOLMzFhtNSKYeTAanQ3a5MudJKf/8arIFnA2L27SNjarQKmFg0w/jA==" 131 | ], 132 | 133 | "@szmarczak/http-timer": [ 134 | "@szmarczak/http-timer@5.0.1", 135 | "", 136 | { "dependencies": { "defer-to-connect": "^2.0.1" } }, 137 | "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==" 138 | ], 139 | 140 | "@types/http-cache-semantics": [ 141 | "@types/http-cache-semantics@4.0.4", 142 | "", 143 | {}, 144 | "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==" 145 | ], 146 | 147 | "ajv": [ 148 | "ajv@6.12.6", 149 | "", 150 | { 151 | "dependencies": { 152 | "fast-deep-equal": "^3.1.1", 153 | "fast-json-stable-stringify": "^2.0.0", 154 | "json-schema-traverse": "^0.4.1", 155 | "uri-js": "^4.2.2" 156 | } 157 | }, 158 | "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==" 159 | ], 160 | 161 | "ansi-regex": [ 162 | "ansi-regex@5.0.1", 163 | "", 164 | {}, 165 | "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" 166 | ], 167 | 168 | "ansi-styles": [ 169 | "ansi-styles@4.3.0", 170 | "", 171 | { "dependencies": { "color-convert": "^2.0.1" } }, 172 | "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==" 173 | ], 174 | 175 | "antlr4": [ 176 | "antlr4@4.13.2", 177 | "", 178 | {}, 179 | "sha512-QiVbZhyy4xAZ17UPEuG3YTOt8ZaoeOR1CvEAqrEsDBsOqINslaB147i9xqljZqoyf5S+EUlGStaj+t22LT9MOg==" 180 | ], 181 | 182 | "argparse": [ 183 | "argparse@2.0.1", 184 | "", 185 | {}, 186 | "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" 187 | ], 188 | 189 | "ast-parents": [ 190 | "ast-parents@0.0.1", 191 | "", 192 | {}, 193 | "sha512-XHusKxKz3zoYk1ic8Un640joHbFMhbqneyoZfoKnEGtf2ey9Uh/IdpcQplODdO/kENaMIWsD0nJm4+wX3UNLHA==" 194 | ], 195 | 196 | "astral-regex": [ 197 | "astral-regex@2.0.0", 198 | "", 199 | {}, 200 | "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==" 201 | ], 202 | 203 | "balanced-match": [ 204 | "balanced-match@1.0.2", 205 | "", 206 | {}, 207 | "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 208 | ], 209 | 210 | "brace-expansion": [ 211 | "brace-expansion@2.0.1", 212 | "", 213 | { "dependencies": { "balanced-match": "^1.0.0" } }, 214 | "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==" 215 | ], 216 | 217 | "cacheable-lookup": [ 218 | "cacheable-lookup@7.0.0", 219 | "", 220 | {}, 221 | "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==" 222 | ], 223 | 224 | "cacheable-request": [ 225 | "cacheable-request@10.2.14", 226 | "", 227 | { 228 | "dependencies": { 229 | "@types/http-cache-semantics": "^4.0.2", 230 | "get-stream": "^6.0.1", 231 | "http-cache-semantics": "^4.1.1", 232 | "keyv": "^4.5.3", 233 | "mimic-response": "^4.0.0", 234 | "normalize-url": "^8.0.0", 235 | "responselike": "^3.0.0" 236 | } 237 | }, 238 | "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==" 239 | ], 240 | 241 | "callsites": [ 242 | "callsites@3.1.0", 243 | "", 244 | {}, 245 | "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" 246 | ], 247 | 248 | "chalk": [ 249 | "chalk@4.1.2", 250 | "", 251 | { 252 | "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } 253 | }, 254 | "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==" 255 | ], 256 | 257 | "color-convert": [ 258 | "color-convert@2.0.1", 259 | "", 260 | { "dependencies": { "color-name": "~1.1.4" } }, 261 | "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==" 262 | ], 263 | 264 | "color-name": [ 265 | "color-name@1.1.4", 266 | "", 267 | {}, 268 | "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 269 | ], 270 | 271 | "commander": [ 272 | "commander@10.0.1", 273 | "", 274 | {}, 275 | "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==" 276 | ], 277 | 278 | "config-chain": [ 279 | "config-chain@1.1.13", 280 | "", 281 | { "dependencies": { "ini": "^1.3.4", "proto-list": "~1.2.1" } }, 282 | "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==" 283 | ], 284 | 285 | "cosmiconfig": [ 286 | "cosmiconfig@8.3.6", 287 | "", 288 | { 289 | "dependencies": { 290 | "import-fresh": "^3.3.0", 291 | "js-yaml": "^4.1.0", 292 | "parse-json": "^5.2.0", 293 | "path-type": "^4.0.0" 294 | }, 295 | "peerDependencies": { "typescript": ">=4.9.5" }, 296 | "optionalPeers": ["typescript"] 297 | }, 298 | "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==" 299 | ], 300 | 301 | "decompress-response": [ 302 | "decompress-response@6.0.0", 303 | "", 304 | { "dependencies": { "mimic-response": "^3.1.0" } }, 305 | "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==" 306 | ], 307 | 308 | "deep-extend": [ 309 | "deep-extend@0.6.0", 310 | "", 311 | {}, 312 | "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" 313 | ], 314 | 315 | "defer-to-connect": [ 316 | "defer-to-connect@2.0.1", 317 | "", 318 | {}, 319 | "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==" 320 | ], 321 | 322 | "emoji-regex": [ 323 | "emoji-regex@8.0.0", 324 | "", 325 | {}, 326 | "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 327 | ], 328 | 329 | "error-ex": [ 330 | "error-ex@1.3.2", 331 | "", 332 | { "dependencies": { "is-arrayish": "^0.2.1" } }, 333 | "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==" 334 | ], 335 | 336 | "fast-deep-equal": [ 337 | "fast-deep-equal@3.1.3", 338 | "", 339 | {}, 340 | "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" 341 | ], 342 | 343 | "fast-diff": [ 344 | "fast-diff@1.3.0", 345 | "", 346 | {}, 347 | "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==" 348 | ], 349 | 350 | "fast-json-stable-stringify": [ 351 | "fast-json-stable-stringify@2.1.0", 352 | "", 353 | {}, 354 | "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" 355 | ], 356 | 357 | "fast-uri": [ 358 | "fast-uri@3.0.6", 359 | "", 360 | {}, 361 | "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==" 362 | ], 363 | 364 | "forge-std": [ 365 | "forge-std@github:foundry-rs/forge-std#bb4ceea", 366 | {}, 367 | "foundry-rs-forge-std-bb4ceea" 368 | ], 369 | 370 | "form-data-encoder": [ 371 | "form-data-encoder@2.1.4", 372 | "", 373 | {}, 374 | "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==" 375 | ], 376 | 377 | "fs.realpath": [ 378 | "fs.realpath@1.0.0", 379 | "", 380 | {}, 381 | "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" 382 | ], 383 | 384 | "get-stream": [ 385 | "get-stream@6.0.1", 386 | "", 387 | {}, 388 | "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" 389 | ], 390 | 391 | "glob": [ 392 | "glob@8.1.0", 393 | "", 394 | { 395 | "dependencies": { 396 | "fs.realpath": "^1.0.0", 397 | "inflight": "^1.0.4", 398 | "inherits": "2", 399 | "minimatch": "^5.0.1", 400 | "once": "^1.3.0" 401 | } 402 | }, 403 | "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==" 404 | ], 405 | 406 | "got": [ 407 | "got@12.6.1", 408 | "", 409 | { 410 | "dependencies": { 411 | "@sindresorhus/is": "^5.2.0", 412 | "@szmarczak/http-timer": "^5.0.1", 413 | "cacheable-lookup": "^7.0.0", 414 | "cacheable-request": "^10.2.8", 415 | "decompress-response": "^6.0.0", 416 | "form-data-encoder": "^2.1.2", 417 | "get-stream": "^6.0.1", 418 | "http2-wrapper": "^2.1.10", 419 | "lowercase-keys": "^3.0.0", 420 | "p-cancelable": "^3.0.0", 421 | "responselike": "^3.0.0" 422 | } 423 | }, 424 | "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==" 425 | ], 426 | 427 | "graceful-fs": [ 428 | "graceful-fs@4.2.10", 429 | "", 430 | {}, 431 | "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" 432 | ], 433 | 434 | "has-flag": [ 435 | "has-flag@4.0.0", 436 | "", 437 | {}, 438 | "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" 439 | ], 440 | 441 | "http-cache-semantics": [ 442 | "http-cache-semantics@4.1.1", 443 | "", 444 | {}, 445 | "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" 446 | ], 447 | 448 | "http2-wrapper": [ 449 | "http2-wrapper@2.2.1", 450 | "", 451 | { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" } }, 452 | "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==" 453 | ], 454 | 455 | "ignore": [ 456 | "ignore@5.3.2", 457 | "", 458 | {}, 459 | "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==" 460 | ], 461 | 462 | "import-fresh": [ 463 | "import-fresh@3.3.1", 464 | "", 465 | { 466 | "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } 467 | }, 468 | "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==" 469 | ], 470 | 471 | "inflight": [ 472 | "inflight@1.0.6", 473 | "", 474 | { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, 475 | "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==" 476 | ], 477 | 478 | "inherits": [ 479 | "inherits@2.0.4", 480 | "", 481 | {}, 482 | "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 483 | ], 484 | 485 | "ini": [ 486 | "ini@1.3.8", 487 | "", 488 | {}, 489 | "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" 490 | ], 491 | 492 | "is-arrayish": [ 493 | "is-arrayish@0.2.1", 494 | "", 495 | {}, 496 | "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" 497 | ], 498 | 499 | "is-fullwidth-code-point": [ 500 | "is-fullwidth-code-point@3.0.0", 501 | "", 502 | {}, 503 | "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" 504 | ], 505 | 506 | "js-tokens": [ 507 | "js-tokens@4.0.0", 508 | "", 509 | {}, 510 | "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 511 | ], 512 | 513 | "js-yaml": [ 514 | "js-yaml@4.1.0", 515 | "", 516 | { 517 | "dependencies": { "argparse": "^2.0.1" }, 518 | "bin": { "js-yaml": "bin/js-yaml.js" } 519 | }, 520 | "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==" 521 | ], 522 | 523 | "json-buffer": [ 524 | "json-buffer@3.0.1", 525 | "", 526 | {}, 527 | "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" 528 | ], 529 | 530 | "json-parse-even-better-errors": [ 531 | "json-parse-even-better-errors@2.3.1", 532 | "", 533 | {}, 534 | "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" 535 | ], 536 | 537 | "json-schema-traverse": [ 538 | "json-schema-traverse@0.4.1", 539 | "", 540 | {}, 541 | "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 542 | ], 543 | 544 | "keyv": [ 545 | "keyv@4.5.4", 546 | "", 547 | { "dependencies": { "json-buffer": "3.0.1" } }, 548 | "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==" 549 | ], 550 | 551 | "latest-version": [ 552 | "latest-version@7.0.0", 553 | "", 554 | { "dependencies": { "package-json": "^8.1.0" } }, 555 | "sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==" 556 | ], 557 | 558 | "lines-and-columns": [ 559 | "lines-and-columns@1.2.4", 560 | "", 561 | {}, 562 | "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" 563 | ], 564 | 565 | "lodash": [ 566 | "lodash@4.17.21", 567 | "", 568 | {}, 569 | "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 570 | ], 571 | 572 | "lodash.truncate": [ 573 | "lodash.truncate@4.4.2", 574 | "", 575 | {}, 576 | "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==" 577 | ], 578 | 579 | "lowercase-keys": [ 580 | "lowercase-keys@3.0.0", 581 | "", 582 | {}, 583 | "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==" 584 | ], 585 | 586 | "mimic-response": [ 587 | "mimic-response@4.0.0", 588 | "", 589 | {}, 590 | "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==" 591 | ], 592 | 593 | "minimatch": [ 594 | "minimatch@5.1.6", 595 | "", 596 | { "dependencies": { "brace-expansion": "^2.0.1" } }, 597 | "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==" 598 | ], 599 | 600 | "minimist": [ 601 | "minimist@1.2.8", 602 | "", 603 | {}, 604 | "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" 605 | ], 606 | 607 | "normalize-url": [ 608 | "normalize-url@8.0.1", 609 | "", 610 | {}, 611 | "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==" 612 | ], 613 | 614 | "once": [ 615 | "once@1.4.0", 616 | "", 617 | { "dependencies": { "wrappy": "1" } }, 618 | "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==" 619 | ], 620 | 621 | "p-cancelable": [ 622 | "p-cancelable@3.0.0", 623 | "", 624 | {}, 625 | "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==" 626 | ], 627 | 628 | "package-json": [ 629 | "package-json@8.1.1", 630 | "", 631 | { 632 | "dependencies": { 633 | "got": "^12.1.0", 634 | "registry-auth-token": "^5.0.1", 635 | "registry-url": "^6.0.0", 636 | "semver": "^7.3.7" 637 | } 638 | }, 639 | "sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==" 640 | ], 641 | 642 | "parent-module": [ 643 | "parent-module@1.0.1", 644 | "", 645 | { "dependencies": { "callsites": "^3.0.0" } }, 646 | "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==" 647 | ], 648 | 649 | "parse-json": [ 650 | "parse-json@5.2.0", 651 | "", 652 | { 653 | "dependencies": { 654 | "@babel/code-frame": "^7.0.0", 655 | "error-ex": "^1.3.1", 656 | "json-parse-even-better-errors": "^2.3.0", 657 | "lines-and-columns": "^1.1.6" 658 | } 659 | }, 660 | "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==" 661 | ], 662 | 663 | "path-type": [ 664 | "path-type@4.0.0", 665 | "", 666 | {}, 667 | "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" 668 | ], 669 | 670 | "picocolors": [ 671 | "picocolors@1.1.1", 672 | "", 673 | {}, 674 | "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" 675 | ], 676 | 677 | "pluralize": [ 678 | "pluralize@8.0.0", 679 | "", 680 | {}, 681 | "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==" 682 | ], 683 | 684 | "prettier": [ 685 | "prettier@2.8.8", 686 | "", 687 | { "bin": { "prettier": "bin-prettier.js" } }, 688 | "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==" 689 | ], 690 | 691 | "proto-list": [ 692 | "proto-list@1.2.4", 693 | "", 694 | {}, 695 | "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==" 696 | ], 697 | 698 | "punycode": [ 699 | "punycode@2.3.1", 700 | "", 701 | {}, 702 | "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" 703 | ], 704 | 705 | "quick-lru": [ 706 | "quick-lru@5.1.1", 707 | "", 708 | {}, 709 | "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" 710 | ], 711 | 712 | "rc": [ 713 | "rc@1.2.8", 714 | "", 715 | { 716 | "dependencies": { 717 | "deep-extend": "^0.6.0", 718 | "ini": "~1.3.0", 719 | "minimist": "^1.2.0", 720 | "strip-json-comments": "~2.0.1" 721 | }, 722 | "bin": { "rc": "./cli.js" } 723 | }, 724 | "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==" 725 | ], 726 | 727 | "registry-auth-token": [ 728 | "registry-auth-token@5.1.0", 729 | "", 730 | { "dependencies": { "@pnpm/npm-conf": "^2.1.0" } }, 731 | "sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==" 732 | ], 733 | 734 | "registry-url": [ 735 | "registry-url@6.0.1", 736 | "", 737 | { "dependencies": { "rc": "1.2.8" } }, 738 | "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==" 739 | ], 740 | 741 | "require-from-string": [ 742 | "require-from-string@2.0.2", 743 | "", 744 | {}, 745 | "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" 746 | ], 747 | 748 | "resolve-alpn": [ 749 | "resolve-alpn@1.2.1", 750 | "", 751 | {}, 752 | "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" 753 | ], 754 | 755 | "resolve-from": [ 756 | "resolve-from@4.0.0", 757 | "", 758 | {}, 759 | "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" 760 | ], 761 | 762 | "responselike": [ 763 | "responselike@3.0.0", 764 | "", 765 | { "dependencies": { "lowercase-keys": "^3.0.0" } }, 766 | "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==" 767 | ], 768 | 769 | "semver": [ 770 | "semver@7.7.1", 771 | "", 772 | { "bin": { "semver": "bin/semver.js" } }, 773 | "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==" 774 | ], 775 | 776 | "slice-ansi": [ 777 | "slice-ansi@4.0.0", 778 | "", 779 | { 780 | "dependencies": { 781 | "ansi-styles": "^4.0.0", 782 | "astral-regex": "^2.0.0", 783 | "is-fullwidth-code-point": "^3.0.0" 784 | } 785 | }, 786 | "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==" 787 | ], 788 | 789 | "solhint": [ 790 | "solhint@5.0.5", 791 | "", 792 | { 793 | "dependencies": { 794 | "@solidity-parser/parser": "^0.19.0", 795 | "ajv": "^6.12.6", 796 | "antlr4": "^4.13.1-patch-1", 797 | "ast-parents": "^0.0.1", 798 | "chalk": "^4.1.2", 799 | "commander": "^10.0.0", 800 | "cosmiconfig": "^8.0.0", 801 | "fast-diff": "^1.2.0", 802 | "glob": "^8.0.3", 803 | "ignore": "^5.2.4", 804 | "js-yaml": "^4.1.0", 805 | "latest-version": "^7.0.0", 806 | "lodash": "^4.17.21", 807 | "pluralize": "^8.0.0", 808 | "semver": "^7.5.2", 809 | "strip-ansi": "^6.0.1", 810 | "table": "^6.8.1", 811 | "text-table": "^0.2.0" 812 | }, 813 | "optionalDependencies": { "prettier": "^2.8.3" }, 814 | "bin": { "solhint": "solhint.js" } 815 | }, 816 | "sha512-WrnG6T+/UduuzSWsSOAbfq1ywLUDwNea3Gd5hg6PS+pLUm8lz2ECNr0beX609clBxmDeZ3676AiA9nPDljmbJQ==" 817 | ], 818 | 819 | "string-width": [ 820 | "string-width@4.2.3", 821 | "", 822 | { 823 | "dependencies": { 824 | "emoji-regex": "^8.0.0", 825 | "is-fullwidth-code-point": "^3.0.0", 826 | "strip-ansi": "^6.0.1" 827 | } 828 | }, 829 | "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==" 830 | ], 831 | 832 | "strip-ansi": [ 833 | "strip-ansi@6.0.1", 834 | "", 835 | { "dependencies": { "ansi-regex": "^5.0.1" } }, 836 | "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==" 837 | ], 838 | 839 | "strip-json-comments": [ 840 | "strip-json-comments@2.0.1", 841 | "", 842 | {}, 843 | "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==" 844 | ], 845 | 846 | "supports-color": [ 847 | "supports-color@7.2.0", 848 | "", 849 | { "dependencies": { "has-flag": "^4.0.0" } }, 850 | "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==" 851 | ], 852 | 853 | "table": [ 854 | "table@6.9.0", 855 | "", 856 | { 857 | "dependencies": { 858 | "ajv": "^8.0.1", 859 | "lodash.truncate": "^4.4.2", 860 | "slice-ansi": "^4.0.0", 861 | "string-width": "^4.2.3", 862 | "strip-ansi": "^6.0.1" 863 | } 864 | }, 865 | "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==" 866 | ], 867 | 868 | "text-table": [ 869 | "text-table@0.2.0", 870 | "", 871 | {}, 872 | "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" 873 | ], 874 | 875 | "uri-js": [ 876 | "uri-js@4.4.1", 877 | "", 878 | { "dependencies": { "punycode": "^2.1.0" } }, 879 | "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==" 880 | ], 881 | 882 | "wrappy": [ 883 | "wrappy@1.0.2", 884 | "", 885 | {}, 886 | "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" 887 | ], 888 | 889 | "decompress-response/mimic-response": [ 890 | "mimic-response@3.1.0", 891 | "", 892 | {}, 893 | "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" 894 | ], 895 | 896 | "table/ajv": [ 897 | "ajv@8.17.1", 898 | "", 899 | { 900 | "dependencies": { 901 | "fast-deep-equal": "^3.1.3", 902 | "fast-uri": "^3.0.1", 903 | "json-schema-traverse": "^1.0.0", 904 | "require-from-string": "^2.0.2" 905 | } 906 | }, 907 | "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==" 908 | ], 909 | 910 | "table/ajv/json-schema-traverse": [ 911 | "json-schema-traverse@1.0.0", 912 | "", 913 | {}, 914 | "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" 915 | ] 916 | } 917 | } 918 | -------------------------------------------------------------------------------- /flow/FlowBatchable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import { ud21x18, UD21x18 } from "@prb/math/src/UD21x18.sol"; 6 | import { ud60x18 } from "@prb/math/src/UD60x18.sol"; 7 | import { Broker, ISablierFlow } from "@sablier/flow/src/interfaces/ISablierFlow.sol"; 8 | 9 | /// @notice The `Batch` contract, inherited in SablierFlow, allows multiple function calls to be batched together. This 10 | /// enables any possible combination of functions to be executed within a single transaction. 11 | /// @dev For some functions to work, `msg.sender` must have approved this contract to spend USDC. 12 | contract FlowBatchable { 13 | // Mainnet addresses 14 | IERC20 public constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); 15 | ISablierFlow public constant FLOW = ISablierFlow(0x3DF2AAEdE81D2F6b261F79047517713B8E844E04); 16 | 17 | /// @dev A function to adjust the rate per second and deposit into a stream in a single transaction. 18 | /// Note: The streamId's sender must be this contract, otherwise, the call will fail due to no authorization. 19 | function adjustRatePerSecondAndDeposit(uint256 streamId) external { 20 | UD21x18 newRatePerSecond = ud21x18(0.0002e18); 21 | uint128 depositAmount = 1000e6; 22 | 23 | // Transfer to this contract the amount to deposit in the stream. 24 | USDC.transferFrom(msg.sender, address(this), depositAmount); 25 | 26 | // Approve the Sablier contract to spend USDC. 27 | USDC.approve(address(FLOW), depositAmount); 28 | 29 | // Fetch the stream recipient. 30 | address recipient = FLOW.getRecipient(streamId); 31 | 32 | // The call data declared as bytes. 33 | bytes[] memory calls = new bytes[](2); 34 | calls[0] = abi.encodeCall(FLOW.adjustRatePerSecond, (streamId, newRatePerSecond)); 35 | calls[1] = abi.encodeCall(FLOW.deposit, (streamId, depositAmount, msg.sender, recipient)); 36 | 37 | FLOW.batch(calls); 38 | } 39 | 40 | /// @dev A function to create a stream and deposit via a broker in a single transaction. 41 | function createAndDepositViaBroker() external returns (uint256 streamId) { 42 | address sender = msg.sender; 43 | address recipient = address(0xCAFE); 44 | UD21x18 ratePerSecond = ud21x18(0.0001e18); 45 | uint128 depositAmount = 1000e6; 46 | bool transferable = true; 47 | 48 | // The broker struct. 49 | Broker memory broker = Broker({ 50 | account: address(0xDEAD), 51 | fee: ud60x18(0.0001e18) // the fee percentage 52 | }); 53 | 54 | // Transfer to this contract the amount to deposit in the stream. 55 | USDC.transferFrom(msg.sender, address(this), depositAmount); 56 | 57 | // Approve the Sablier contract to spend USDC. 58 | USDC.approve(address(FLOW), depositAmount); 59 | 60 | streamId = FLOW.nextStreamId(); 61 | 62 | // The call data declared as bytes 63 | bytes[] memory calls = new bytes[](2); 64 | calls[0] = abi.encodeCall(FLOW.create, (sender, recipient, ratePerSecond, USDC, transferable)); 65 | calls[1] = abi.encodeCall(FLOW.depositViaBroker, (streamId, depositAmount, sender, recipient, broker)); 66 | 67 | // Execute multiple calls in a single transaction using the prepared call data. 68 | FLOW.batch(calls); 69 | } 70 | 71 | /// @dev A function to create multiple streams in a single transaction. 72 | function createMultiple() external returns (uint256[] memory streamIds) { 73 | address sender = msg.sender; 74 | address firstRecipient = address(0xCAFE); 75 | address secondRecipient = address(0xBEEF); 76 | UD21x18 firstRatePerSecond = ud21x18(0.0001e18); 77 | UD21x18 secondRatePerSecond = ud21x18(0.0002e18); 78 | bool transferable = true; 79 | 80 | // The call data declared as bytes 81 | bytes[] memory calls = new bytes[](2); 82 | calls[0] = abi.encodeCall(FLOW.create, (sender, firstRecipient, firstRatePerSecond, USDC, transferable)); 83 | calls[1] = abi.encodeCall(FLOW.create, (sender, secondRecipient, secondRatePerSecond, USDC, transferable)); 84 | 85 | // Prepare the `streamIds` array to return them 86 | uint256 nextStreamId = FLOW.nextStreamId(); 87 | streamIds = new uint256[](2); 88 | streamIds[0] = nextStreamId; 89 | streamIds[1] = nextStreamId + 1; 90 | 91 | // Execute multiple calls in a single transaction using the prepared call data. 92 | FLOW.batch(calls); 93 | } 94 | 95 | /// @dev A function to create multiple streams and deposit via a broker into all the stream in a single transaction. 96 | function createMultipleAndDepositViaBroker() external returns (uint256[] memory streamIds) { 97 | address sender = msg.sender; 98 | address firstRecipient = address(0xCAFE); 99 | address secondRecipient = address(0xBEEF); 100 | UD21x18 ratePerSecond = ud21x18(0.0001e18); 101 | uint128 depositAmount = 1000e6; 102 | bool transferable = true; 103 | 104 | // Transfer the deposit amount of USDC tokens to this contract for both streams 105 | USDC.transferFrom(msg.sender, address(this), 2 * depositAmount); 106 | 107 | // Approve the Sablier contract to spend USDC. 108 | USDC.approve(address(FLOW), 2 * depositAmount); 109 | 110 | // The broker struct 111 | Broker memory broker = Broker({ 112 | account: address(0xDEAD), 113 | fee: ud60x18(0.0001e18) // the fee percentage 114 | }); 115 | 116 | uint256 nextStreamId = FLOW.nextStreamId(); 117 | streamIds = new uint256[](2); 118 | streamIds[0] = nextStreamId; 119 | streamIds[1] = nextStreamId + 1; 120 | 121 | // We need to have 4 different function calls, 2 for creating streams and 2 for depositing via broker 122 | bytes[] memory calls = new bytes[](4); 123 | calls[0] = abi.encodeCall(FLOW.create, (sender, firstRecipient, ratePerSecond, USDC, transferable)); 124 | calls[1] = abi.encodeCall(FLOW.create, (sender, secondRecipient, ratePerSecond, USDC, transferable)); 125 | calls[2] = abi.encodeCall(FLOW.depositViaBroker, (streamIds[0], depositAmount, sender, firstRecipient, broker)); 126 | calls[3] = abi.encodeCall(FLOW.depositViaBroker, (streamIds[1], depositAmount, sender, secondRecipient, broker)); 127 | 128 | // Execute multiple calls in a single transaction using the prepared call data. 129 | FLOW.batch(calls); 130 | } 131 | 132 | /// @dev A function to pause a stream and withdraw the maximum available funds. 133 | /// Note: The streamId's sender must be this contract, otherwise, the call will fail due to no authorization. 134 | function pauseAndWithdrawMax(uint256 streamId) external { 135 | // The call data declared as bytes. 136 | bytes[] memory calls = new bytes[](2); 137 | calls[0] = abi.encodeCall(FLOW.pause, (streamId)); 138 | calls[1] = abi.encodeCall(FLOW.withdrawMax, (streamId, address(0xCAFE))); 139 | 140 | // Execute multiple calls in a single transaction using the prepared call data. 141 | FLOW.batch(calls); 142 | } 143 | 144 | /// @dev A function to void a stream and withdraw what is left. 145 | /// Note: The streamId's sender must be this contract, otherwise, the call will fail due to no authorization. 146 | function voidAndWithdrawMax(uint256 streamId) external { 147 | // The call data declared as bytes 148 | bytes[] memory calls = new bytes[](2); 149 | calls[0] = abi.encodeCall(FLOW.void, (streamId)); 150 | calls[1] = abi.encodeCall(FLOW.withdrawMax, (streamId, address(0xCAFE))); 151 | 152 | // Execute multiple calls in a single transaction using the prepared call data. 153 | FLOW.batch(calls); 154 | } 155 | 156 | /// @dev A function to withdraw maximum available funds from multiple streams in a single transaction. 157 | function withdrawMaxMultiple(uint256[] calldata streamIds) external { 158 | uint256 count = streamIds.length; 159 | 160 | // Iterate over the streamIds and prepare the call data for each stream. 161 | bytes[] memory calls = new bytes[](count); 162 | for (uint256 i = 0; i < count; ++i) { 163 | address recipient = FLOW.getRecipient(streamIds[i]); 164 | calls[i] = abi.encodeCall(FLOW.withdrawMax, (streamIds[i], recipient)); 165 | } 166 | 167 | // Execute multiple calls in a single transaction using the prepared call data. 168 | FLOW.batch(calls); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /flow/FlowBatchable.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { Test } from "forge-std/src/Test.sol"; 5 | 6 | import { FlowBatchable } from "./FlowBatchable.sol"; 7 | 8 | contract FlowBatchable_Test is Test { 9 | FlowBatchable internal batchable; 10 | address internal user; 11 | 12 | function setUp() external { 13 | // Fork Ethereum Mainnet 14 | vm.createSelectFork("mainnet"); 15 | 16 | // Deploy the batchable contract 17 | batchable = new FlowBatchable(); 18 | 19 | user = makeAddr("User"); 20 | 21 | // Mint some DAI tokens to the test user, which will be pulled by the creator contract 22 | deal({ token: address(batchable.USDC()), to: user, give: 1_000_000e6 }); 23 | 24 | // Make the test user the `msg.sender` in all following calls 25 | vm.startPrank({ msgSender: user }); 26 | 27 | // Approve the batchable contract to pull USDC tokens from the test user 28 | batchable.USDC().approve({ spender: address(batchable), value: 1_000_000e6 }); 29 | } 30 | 31 | function test_CreateMultiple() external { 32 | uint256 nextStreamIdBefore = batchable.FLOW().nextStreamId(); 33 | 34 | uint256[] memory actualStreamIds = batchable.createMultiple(); 35 | uint256[] memory expectedStreamIds = new uint256[](2); 36 | expectedStreamIds[0] = nextStreamIdBefore; 37 | expectedStreamIds[1] = nextStreamIdBefore + 1; 38 | 39 | assertEq(actualStreamIds, expectedStreamIds); 40 | } 41 | 42 | function test_CreateAndDepositViaBroker() external { 43 | uint256 nextStreamIdBefore = batchable.FLOW().nextStreamId(); 44 | 45 | uint256[] memory actualStreamIds = batchable.createMultipleAndDepositViaBroker(); 46 | uint256[] memory expectedStreamIds = new uint256[](2); 47 | expectedStreamIds[0] = nextStreamIdBefore; 48 | expectedStreamIds[1] = nextStreamIdBefore + 1; 49 | 50 | assertEq(actualStreamIds, expectedStreamIds); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /flow/FlowStreamCreator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import { UD21x18 } from "@prb/math/src/UD21x18.sol"; 6 | import { ISablierFlow } from "@sablier/flow/src/interfaces/ISablierFlow.sol"; 7 | 8 | import { FlowUtilities } from "./FlowUtilities.sol"; 9 | 10 | contract FlowStreamCreator { 11 | // Mainnet addresses 12 | IERC20 public constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); 13 | ISablierFlow public constant FLOW = ISablierFlow(0x3DF2AAEdE81D2F6b261F79047517713B8E844E04); 14 | 15 | // Create a stream that sends 1000 USDC per month. 16 | function createStream_1K_PerMonth() external returns (uint256 streamId) { 17 | UD21x18 ratePerSecond = 18 | FlowUtilities.ratePerSecondWithDuration({ token: address(USDC), amount: 1000e6, duration: 30 days }); 19 | 20 | streamId = FLOW.create({ 21 | sender: msg.sender, // The sender will be able to manage the stream 22 | recipient: address(0xCAFE), // The recipient of the streamed tokens 23 | ratePerSecond: ratePerSecond, // The rate per second equivalent to 1000 USDC per month 24 | token: USDC, // The token to be streamed 25 | transferable: true // Whether the stream will be transferable or not 26 | }); 27 | } 28 | 29 | // Create a stream that sends 1,000,000 USDC per year. 30 | function createStream_1M_PerYear() external returns (uint256 streamId) { 31 | UD21x18 ratePerSecond = 32 | FlowUtilities.ratePerSecondWithDuration({ token: address(USDC), amount: 1_000_000e6, duration: 365 days }); 33 | 34 | streamId = FLOW.create({ 35 | sender: msg.sender, // The sender will be able to manage the stream 36 | recipient: address(0xCAFE), // The recipient of the streamed tokens 37 | ratePerSecond: ratePerSecond, // The rate per second equivalent to 1,000,00 USDC per year 38 | token: USDC, // The token to be streamed 39 | transferable: true // Whether the stream will be transferable or not 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /flow/FlowStreamCreator.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { Test } from "forge-std/src/Test.sol"; 5 | 6 | import { FlowStreamCreator } from "./FlowStreamCreator.sol"; 7 | 8 | contract FlowStreamCreator_Test is Test { 9 | FlowStreamCreator internal streamCreator; 10 | address internal user; 11 | 12 | function setUp() external { 13 | // Fork Ethereum Mainnet 14 | vm.createSelectFork("mainnet"); 15 | 16 | // Deploy the FlowStreamCreator contract 17 | streamCreator = new FlowStreamCreator(); 18 | 19 | user = makeAddr("User"); 20 | 21 | // Mint some DAI tokens to the test user, which will be pulled by the creator contract 22 | deal({ token: address(streamCreator.USDC()), to: user, give: 1_000_000e6 }); 23 | 24 | // Make the test user the `msg.sender` in all following calls 25 | vm.startPrank({ msgSender: user }); 26 | 27 | // Approve the streamCreator contract to pull USDC tokens from the test user 28 | streamCreator.USDC().approve({ spender: address(streamCreator), value: 1_000_000e6 }); 29 | } 30 | 31 | function test_CreateStream_1K_PerMonth() external { 32 | uint256 expectedStreamId = streamCreator.FLOW().nextStreamId(); 33 | 34 | uint256 actualStreamId = streamCreator.createStream_1K_PerMonth(); 35 | assertEq(actualStreamId, expectedStreamId); 36 | 37 | // Warp slightly over 30 days so that the debt accumulated is slightly over 1000 USDC. 38 | vm.warp({ newTimestamp: block.timestamp + 30 days + 1 seconds }); 39 | 40 | assertGe(streamCreator.FLOW().totalDebtOf(actualStreamId), 1000e6); 41 | } 42 | 43 | function test_CreateStream_1M_PerYear() external { 44 | uint256 expectedStreamId = streamCreator.FLOW().nextStreamId(); 45 | 46 | uint256 actualStreamId = streamCreator.createStream_1M_PerYear(); 47 | assertEq(actualStreamId, expectedStreamId); 48 | 49 | // Warp slightly over 365 days so that the debt accumulated is slightly over 1M USDC. 50 | vm.warp({ newTimestamp: block.timestamp + 365 days + 1 seconds }); 51 | 52 | assertGe(streamCreator.FLOW().totalDebtOf(actualStreamId), 1_000_000e6); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /flow/FlowStreamManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { ud21x18 } from "@prb/math/src/UD21x18.sol"; 5 | import { ud60x18 } from "@prb/math/src/UD60x18.sol"; 6 | import { Broker, ISablierFlow } from "@sablier/flow/src/interfaces/ISablierFlow.sol"; 7 | 8 | contract FlowStreamManager { 9 | // Mainnet address 10 | ISablierFlow public constant FLOW = ISablierFlow(0x3DF2AAEdE81D2F6b261F79047517713B8E844E04); 11 | 12 | function adjustRatePerSecond(uint256 streamId) external { 13 | FLOW.adjustRatePerSecond({ streamId: streamId, newRatePerSecond: ud21x18(0.0001e18) }); 14 | } 15 | 16 | function deposit(uint256 streamId) external { 17 | FLOW.deposit(streamId, 3.14159e18, msg.sender, address(0xCAFE)); 18 | } 19 | 20 | function depositAndPause(uint256 streamId) external { 21 | FLOW.depositAndPause(streamId, 3.14159e18); 22 | } 23 | 24 | function depositViaBroker(uint256 streamId) external { 25 | Broker memory broker = Broker({ account: address(0xDEAD), fee: ud60x18(0.0001e18) }); 26 | 27 | FLOW.depositViaBroker({ 28 | streamId: streamId, 29 | totalAmount: 3.14159e18, 30 | sender: msg.sender, 31 | recipient: address(0xCAFE), 32 | broker: broker 33 | }); 34 | } 35 | 36 | function pause(uint256 streamId) external { 37 | FLOW.pause(streamId); 38 | } 39 | 40 | function refund(uint256 streamId) external { 41 | FLOW.refund({ streamId: streamId, amount: 1.61803e18 }); 42 | } 43 | 44 | function refundAndPause(uint256 streamId) external { 45 | FLOW.refundAndPause({ streamId: streamId, amount: 1.61803e18 }); 46 | } 47 | 48 | function refundMax(uint256 streamId) external { 49 | FLOW.refundMax(streamId); 50 | } 51 | 52 | function restart(uint256 streamId) external { 53 | FLOW.restart({ streamId: streamId, ratePerSecond: ud21x18(0.0001e18) }); 54 | } 55 | 56 | function restartAndDeposit(uint256 streamId) external { 57 | FLOW.restartAndDeposit({ streamId: streamId, ratePerSecond: ud21x18(0.0001e18), amount: 2.71828e18 }); 58 | } 59 | 60 | function void(uint256 streamId) external { 61 | FLOW.void(streamId); 62 | } 63 | 64 | function withdraw(uint256 streamId) external { 65 | FLOW.withdraw({ streamId: streamId, to: address(0xCAFE), amount: 2.71828e18 }); 66 | } 67 | 68 | function withdrawMax(uint256 streamId) external { 69 | FLOW.withdrawMax({ streamId: streamId, to: address(0xCAFE) }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /flow/FlowUtilities.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; 5 | import { ud21x18, UD21x18 } from "@prb/math/src/UD21x18.sol"; 6 | 7 | /// @dev A utility library to calculate rate per second and streamed amount based on a given time frame. 8 | library FlowUtilities { 9 | /// @notice This function calculates the rate per second based on a given amount of tokens and a specified duration. 10 | /// @dev The rate per second is a 18-decimal fixed-point number and it is calculated as `amount / duration`. 11 | /// @param token The address of the token. 12 | /// @param amount The amount of tokens, denoted in token's decimals. 13 | /// @param duration The duration in seconds user wishes to stream. 14 | /// @return ratePerSecond The rate per second as a 18-decimal fixed-point number. 15 | function ratePerSecondWithDuration( 16 | address token, 17 | uint128 amount, 18 | uint40 duration 19 | ) 20 | internal 21 | view 22 | returns (UD21x18 ratePerSecond) 23 | { 24 | // Get the decimals of the token. 25 | uint8 decimals = IERC20Metadata(token).decimals(); 26 | 27 | // If the token has 18 decimals, we can simply divide the amount by the duration as it returns a 18 decimal 28 | // fixed-point number. 29 | if (decimals == 18) { 30 | return ud21x18(amount / duration); 31 | } 32 | 33 | // Calculate the scale factor from the token's decimals. 34 | uint128 scaleFactor = uint128(10 ** (18 - decimals)); 35 | 36 | // Multiply the amount by the scale factor and divide by the duration. 37 | ratePerSecond = ud21x18((scaleFactor * amount) / duration); 38 | } 39 | 40 | /// @notice This function calculates the rate per second based on a given amount of tokens and a specified range. 41 | /// @dev The rate per second is a 18-decimal fixed-point number and it is calculated as `amount / (end - start)`. 42 | /// @param token The address of the token. 43 | /// @param amount The amount of tokens, denoted in token's decimals. 44 | /// @param start The start timestamp. 45 | /// @param end The end timestamp. 46 | /// @return ratePerSecond The rate per second as a 18-decimal fixed-point number. 47 | function ratePerSecondForTimestamps( 48 | address token, 49 | uint128 amount, 50 | uint40 start, 51 | uint40 end 52 | ) 53 | internal 54 | view 55 | returns (UD21x18 ratePerSecond) 56 | { 57 | // Calculate the duration. 58 | uint40 duration = end - start; 59 | 60 | // Get the decimals of the token. 61 | uint8 decimals = IERC20Metadata(token).decimals(); 62 | 63 | if (decimals == 18) { 64 | return ud21x18(amount / duration); 65 | } 66 | 67 | // Calculate the scale factor from the token's decimals. 68 | uint128 scaleFactor = uint128(10 ** (18 - decimals)); 69 | 70 | // Multiply the amount by the scale factor and divide by the duration. 71 | ratePerSecond = ud21x18((scaleFactor * amount) / duration); 72 | } 73 | 74 | /// @notice This function calculates the amount streamed over a week for a given rate per second. 75 | /// @param ratePerSecond The rate per second as a 18-decimal fixed-point number. 76 | /// @return amountPerWeek The amount streamed over a week. 77 | function calculateAmountStreamedPerWeek(UD21x18 ratePerSecond) internal pure returns (uint128 amountPerWeek) { 78 | amountPerWeek = ratePerSecond.unwrap() * 1 weeks; 79 | } 80 | 81 | /// @notice This function calculates the amount streamed over a month for a given rate per second. 82 | /// @dev For simplicity, we have assumed that there are 30 days in a month. 83 | /// @param ratePerSecond The rate per second as a 18-decimal fixed-point number. 84 | /// @return amountPerMonth The amount streamed over a month. 85 | function calculateAmountStreamedPerMonth(UD21x18 ratePerSecond) internal pure returns (uint128 amountPerMonth) { 86 | amountPerMonth = ratePerSecond.unwrap() * 30 days; 87 | } 88 | 89 | /// @notice This function calculates the amount streamed over a year for a given rate per second. 90 | /// @dev For simplicity, we have assumed that there are 365 days in a year. 91 | /// @param ratePerSecond The rate per second as a fixed-point number. 92 | /// @return amountPerYear The amount streamed over a year. 93 | function calculateAmountStreamedPerYear(UD21x18 ratePerSecond) internal pure returns (uint128 amountPerYear) { 94 | amountPerYear = ratePerSecond.unwrap() * 365 days; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | # Full reference https://github.com/foundry-rs/foundry/tree/master/config 2 | 3 | [profile.default] 4 | bytecode_hash = "none" 5 | evm_version = "shanghai" 6 | fuzz = { runs = 1_000 } 7 | optimizer = true 8 | optimizer_runs = 5000 9 | out = "out" 10 | solc = "0.8.26" 11 | 12 | [profile.airdrops] 13 | src = "airdrops" 14 | test = "airdrops" 15 | 16 | [profile.flow] 17 | src = "flow" 18 | test = "flow" 19 | 20 | [profile.lockup] 21 | src = "lockup" 22 | test = "lockup" 23 | 24 | [fmt] 25 | bracket_spacing = true 26 | int_types = "long" 27 | line_length = 120 28 | multiline_func_header = "all" 29 | number_underscore = "thousands" 30 | quote_style = "double" 31 | tab_width = 4 32 | wrap_comments = true 33 | 34 | [rpc_endpoints] 35 | mainnet = "${MAINNET_RPC_URL}" 36 | -------------------------------------------------------------------------------- /lockup/BatchLDStreamCreator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import { ud2x18 } from "@prb/math/src/UD2x18.sol"; 6 | import { ud60x18 } from "@prb/math/src/UD60x18.sol"; 7 | import { ISablierBatchLockup } from "@sablier/lockup/src/interfaces/ISablierBatchLockup.sol"; 8 | import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; 9 | import { BatchLockup, Broker, LockupDynamic } from "@sablier/lockup/src/types/DataTypes.sol"; 10 | 11 | contract BatchLDStreamCreator { 12 | // Mainnet addresses 13 | IERC20 public constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); 14 | // See https://docs.sablier.com/guides/lockup/deployments for all deployments 15 | ISablierLockup public constant LOCKUP = ISablierLockup(0x7C01AA3783577E15fD7e272443D44B92d5b21056); 16 | ISablierBatchLockup public constant BATCH_LOCKUP = ISablierBatchLockup(0x3F6E8a8Cffe377c4649aCeB01e6F20c60fAA356c); 17 | 18 | /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. 19 | function batchCreateStreams(uint128 perStreamAmount) public returns (uint256[] memory streamIds) { 20 | // Create a batch of two streams 21 | uint256 batchSize = 2; 22 | 23 | // Calculate the combined amount of DAI tokens to transfer to this contract 24 | uint256 transferAmount = perStreamAmount * batchSize; 25 | 26 | // Transfer the provided amount of DAI tokens to this contract 27 | DAI.transferFrom(msg.sender, address(this), transferAmount); 28 | 29 | // Approve the Batch contract to spend DAI 30 | DAI.approve({ spender: address(BATCH_LOCKUP), value: transferAmount }); 31 | 32 | // Declare the first stream in the batch 33 | BatchLockup.CreateWithTimestampsLD memory stream0; 34 | stream0.sender = address(0xABCD); // The sender to stream the tokens, he will be able to cancel the stream 35 | stream0.recipient = address(0xCAFE); // The recipient of the streamed tokens 36 | stream0.totalAmount = perStreamAmount; // The total amount of each stream, inclusive of all fees 37 | stream0.cancelable = true; // Whether the stream will be cancelable or not 38 | stream0.transferable = false; // Whether the recipient can transfer the NFT or not 39 | stream0.startTime = uint40(block.timestamp); // The start time of the stream 40 | stream0.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined 41 | 42 | // Declare some dummy segments 43 | stream0.segments = new LockupDynamic.Segment[](2); 44 | stream0.segments[0] = LockupDynamic.Segment({ 45 | amount: uint128(perStreamAmount / 2), 46 | exponent: ud2x18(0.25e18), 47 | timestamp: uint40(block.timestamp + 1 weeks) 48 | }); 49 | stream0.segments[1] = ( 50 | LockupDynamic.Segment({ 51 | amount: uint128(perStreamAmount - stream0.segments[0].amount), 52 | exponent: ud2x18(2.71e18), 53 | timestamp: uint40(block.timestamp + 24 weeks) 54 | }) 55 | ); 56 | 57 | // Declare the second stream in the batch 58 | BatchLockup.CreateWithTimestampsLD memory stream1; 59 | stream1.sender = address(0xABCD); // The sender to stream the tokens, he will be able to cancel the stream 60 | stream1.recipient = address(0xBEEF); // The recipient of the streamed tokens 61 | stream1.totalAmount = perStreamAmount; // The total amount of each stream, inclusive of all fees 62 | stream1.cancelable = false; // Whether the stream will be cancelable or not 63 | stream1.transferable = false; // Whether the recipient can transfer the NFT or not 64 | stream1.startTime = uint40(block.timestamp); // The start time of the stream 65 | stream1.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined 66 | 67 | // Declare some dummy segments 68 | stream1.segments = new LockupDynamic.Segment[](2); 69 | stream1.segments[0] = LockupDynamic.Segment({ 70 | amount: uint128(perStreamAmount / 4), 71 | exponent: ud2x18(1e18), 72 | timestamp: uint40(block.timestamp + 4 weeks) 73 | }); 74 | stream1.segments[1] = ( 75 | LockupDynamic.Segment({ 76 | amount: uint128(perStreamAmount - stream1.segments[0].amount), 77 | exponent: ud2x18(3.14e18), 78 | timestamp: uint40(block.timestamp + 52 weeks) 79 | }) 80 | ); 81 | 82 | // Fill the batch array 83 | BatchLockup.CreateWithTimestampsLD[] memory batch = new BatchLockup.CreateWithTimestampsLD[](batchSize); 84 | batch[0] = stream0; 85 | batch[1] = stream1; 86 | 87 | streamIds = BATCH_LOCKUP.createWithTimestampsLD(LOCKUP, DAI, batch); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lockup/BatchLLStreamCreator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import { ud60x18 } from "@prb/math/src/UD60x18.sol"; 6 | import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; 7 | import { Broker, LockupLinear } from "@sablier/lockup/src/types/DataTypes.sol"; 8 | import { ISablierBatchLockup } from "@sablier/lockup/src/interfaces/ISablierBatchLockup.sol"; 9 | import { BatchLockup } from "@sablier/lockup/src/types/DataTypes.sol"; 10 | 11 | contract BatchLLStreamCreator { 12 | // Mainnet addresses 13 | IERC20 public constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); 14 | // See https://docs.sablier.com/guides/lockup/deployments for all deployments 15 | ISablierLockup public constant LOCKUP = ISablierLockup(0x7C01AA3783577E15fD7e272443D44B92d5b21056); 16 | ISablierBatchLockup public constant BATCH_LOCKUP = ISablierBatchLockup(0x3F6E8a8Cffe377c4649aCeB01e6F20c60fAA356c); 17 | 18 | /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. 19 | function batchCreateStreams(uint128 perStreamAmount) public returns (uint256[] memory streamIds) { 20 | // Create a batch of two streams 21 | uint256 batchSize = 2; 22 | 23 | // Calculate the combined amount of DAI tokens to transfer to this contract 24 | uint256 transferAmount = perStreamAmount * batchSize; 25 | 26 | // Transfer the provided amount of DAI tokens to this contract 27 | DAI.transferFrom(msg.sender, address(this), transferAmount); 28 | 29 | // Approve the Batch contract to spend DAI 30 | DAI.approve({ spender: address(BATCH_LOCKUP), value: transferAmount }); 31 | 32 | // Declare the first stream in the batch 33 | BatchLockup.CreateWithDurationsLL memory stream0; 34 | stream0.sender = address(0xABCD); // The sender to stream the tokens, he will be able to cancel the stream 35 | stream0.recipient = address(0xCAFE); // The recipient of the streamed tokens 36 | stream0.totalAmount = perStreamAmount; // The total amount of each stream, inclusive of all fees 37 | stream0.cancelable = true; // Whether the stream will be cancelable or not 38 | stream0.transferable = false; // Whether the recipient can transfer the NFT or not 39 | stream0.durations = LockupLinear.Durations({ 40 | cliff: 4 weeks, // Tokens will start streaming continuously after 4 weeks 41 | total: 52 weeks // Setting a total duration of ~1 year 42 | }); 43 | stream0.unlockAmounts = LockupLinear.UnlockAmounts({ 44 | start: 0, // Whether the stream will unlock a certain amount of tokens at the start time 45 | cliff: 0 // Whether the stream will unlock a certain amount of tokens at the cliff time 46 | }); 47 | stream0.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined 48 | 49 | // Declare the second stream in the batch 50 | BatchLockup.CreateWithDurationsLL memory stream1; 51 | stream1.sender = address(0xABCD); // The sender to stream the tokens, he will be able to cancel the stream 52 | stream1.recipient = address(0xBEEF); // The recipient of the streamed tokens 53 | stream1.totalAmount = perStreamAmount; // The total amount of each stream, inclusive of all fees 54 | stream1.cancelable = false; // Whether the stream will be cancelable or not 55 | stream1.transferable = false; // Whether the recipient can transfer the NFT or not 56 | stream1.durations = LockupLinear.Durations({ 57 | cliff: 1 weeks, // Tokens will start streaming continuously after 4 weeks 58 | total: 26 weeks // Setting a total duration of ~6 months 59 | }); 60 | stream1.unlockAmounts = LockupLinear.UnlockAmounts({ 61 | start: 0, // Whether the stream will unlock a certain amount of tokens at the start time 62 | cliff: 0 // Whether the stream will unlock a certain amount of tokens at the start time 63 | }); 64 | stream1.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined 65 | 66 | // Fill the batch param 67 | BatchLockup.CreateWithDurationsLL[] memory batch = new BatchLockup.CreateWithDurationsLL[](batchSize); 68 | batch[0] = stream0; 69 | batch[1] = stream1; 70 | 71 | streamIds = BATCH_LOCKUP.createWithDurationsLL(LOCKUP, DAI, batch); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lockup/BatchLTStreamCreator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import { ud60x18 } from "@prb/math/src/UD60x18.sol"; 6 | import { ISablierBatchLockup } from "@sablier/lockup/src/interfaces/ISablierBatchLockup.sol"; 7 | import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; 8 | import { BatchLockup, Broker, LockupTranched } from "@sablier/lockup/src/types/DataTypes.sol"; 9 | 10 | contract BatchLTStreamCreator { 11 | // Mainnet addresses 12 | IERC20 public constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); 13 | // See https://docs.sablier.com/guides/lockup/deployments for all deployments 14 | ISablierLockup public constant LOCKUP = ISablierLockup(0x7C01AA3783577E15fD7e272443D44B92d5b21056); 15 | ISablierBatchLockup public constant BATCH_LOCKUP = ISablierBatchLockup(0x3F6E8a8Cffe377c4649aCeB01e6F20c60fAA356c); 16 | 17 | /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. 18 | function batchCreateStreams(uint128 perStreamAmount) public returns (uint256[] memory streamIds) { 19 | // Create a batch of two streams 20 | uint256 batchSize = 2; 21 | 22 | // Calculate the combined amount of DAI tokens to transfer to this contract 23 | uint256 transferAmount = perStreamAmount * batchSize; 24 | 25 | // Transfer the provided amount of DAI tokens to this contract 26 | DAI.transferFrom(msg.sender, address(this), transferAmount); 27 | 28 | // Approve the Batch contract to spend DAI 29 | DAI.approve({ spender: address(BATCH_LOCKUP), value: transferAmount }); 30 | 31 | // Declare the first stream in the batch 32 | BatchLockup.CreateWithTimestampsLT memory stream0; 33 | stream0.sender = address(0xABCD); // The sender to stream the tokens, he will be able to cancel the stream 34 | stream0.recipient = address(0xCAFE); // The recipient of the streamed tokens 35 | stream0.totalAmount = perStreamAmount; // The total amount of each stream, inclusive of all fees 36 | stream0.cancelable = true; // Whether the stream will be cancelable or not 37 | stream0.transferable = false; // Whether the recipient can transfer the NFT or not 38 | stream0.startTime = uint40(block.timestamp); // Set the start time to block timestamp 39 | // Declare some dummy tranches 40 | stream0.tranches = new LockupTranched.Tranche[](2); 41 | stream0.tranches[0] = LockupTranched.Tranche({ 42 | amount: uint128(perStreamAmount / 2), 43 | timestamp: uint40(block.timestamp + 1 weeks) 44 | }); 45 | stream0.tranches[1] = LockupTranched.Tranche({ 46 | amount: uint128(perStreamAmount - stream0.tranches[0].amount), 47 | timestamp: uint40(block.timestamp + 24 weeks) 48 | }); 49 | stream0.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined 50 | 51 | // Declare the second stream in the batch 52 | BatchLockup.CreateWithTimestampsLT memory stream1; 53 | stream1.sender = address(0xABCD); // The sender to stream the tokens, he will be able to cancel the stream 54 | stream1.recipient = address(0xBEEF); // The recipient of the streamed tokens 55 | stream1.totalAmount = perStreamAmount; // The total amount of each stream, inclusive of all fees 56 | stream1.cancelable = false; // Whether the stream will be cancelable or not 57 | stream1.transferable = false; // Whether the recipient can transfer the NFT or not 58 | stream1.startTime = uint40(block.timestamp); // Set the start time to block timestamp 59 | // Declare some dummy tranches 60 | stream1.tranches = new LockupTranched.Tranche[](2); 61 | stream1.tranches[0] = LockupTranched.Tranche({ 62 | amount: uint128(perStreamAmount / 4), 63 | timestamp: uint40(block.timestamp + 4 weeks) 64 | }); 65 | stream1.tranches[1] = LockupTranched.Tranche({ 66 | amount: uint128(perStreamAmount - stream1.tranches[0].amount), 67 | timestamp: uint40(block.timestamp + 24 weeks) 68 | }); 69 | stream1.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined 70 | 71 | // Fill the batch array 72 | BatchLockup.CreateWithTimestampsLT[] memory batch = new BatchLockup.CreateWithTimestampsLT[](batchSize); 73 | batch[0] = stream0; 74 | batch[1] = stream1; 75 | 76 | streamIds = BATCH_LOCKUP.createWithTimestampsLT(LOCKUP, DAI, batch); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lockup/BatchStreamCreator.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3-0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { Test } from "forge-std/src/Test.sol"; 5 | 6 | import { BatchLDStreamCreator } from "./BatchLDStreamCreator.sol"; 7 | import { BatchLLStreamCreator } from "./BatchLLStreamCreator.sol"; 8 | import { BatchLTStreamCreator } from "./BatchLTStreamCreator.sol"; 9 | 10 | contract BatchStreamCreatorTest is Test { 11 | // Test contracts 12 | BatchLDStreamCreator internal dynamicCreator; 13 | BatchLLStreamCreator internal linearCreator; 14 | BatchLTStreamCreator internal tranchedCreator; 15 | 16 | address internal user; 17 | 18 | function setUp() public { 19 | // Fork Ethereum Mainnet 20 | vm.createSelectFork("mainnet"); 21 | 22 | // Deploy the stream creators 23 | dynamicCreator = new BatchLDStreamCreator(); 24 | linearCreator = new BatchLLStreamCreator(); 25 | tranchedCreator = new BatchLTStreamCreator(); 26 | 27 | // Create a test user 28 | user = payable(makeAddr("User")); 29 | vm.deal({ account: user, newBalance: 1 ether }); 30 | 31 | // Mint some DAI tokens to the test user, which will be pulled by the creator contracts 32 | deal({ token: address(linearCreator.DAI()), to: user, give: 6 * 1337e18 }); 33 | 34 | // Make the test user the `msg.sender` in all following calls 35 | vm.startPrank({ msgSender: user }); 36 | 37 | // Approve the creator contracts to pull DAI tokens from the test user 38 | dynamicCreator.DAI().approve({ spender: address(dynamicCreator), value: 2 * 1337e18 }); 39 | linearCreator.DAI().approve({ spender: address(linearCreator), value: 2 * 1337e18 }); 40 | tranchedCreator.DAI().approve({ spender: address(tranchedCreator), value: 2 * 1337e18 }); 41 | } 42 | 43 | /*////////////////////////////////////////////////////////////////////////// 44 | LOCKUP-DYNAMIC 45 | //////////////////////////////////////////////////////////////////////////*/ 46 | 47 | // Tests that creating streams works by checking the stream ids 48 | function test_BatchLockupDynamicStreamCreator() public { 49 | uint256 nextStreamId = dynamicCreator.LOCKUP().nextStreamId(); 50 | uint256[] memory actualStreamIds = dynamicCreator.batchCreateStreams({ perStreamAmount: 1337e18 }); 51 | uint256[] memory expectedStreamIds = new uint256[](2); 52 | expectedStreamIds[0] = nextStreamId; 53 | expectedStreamIds[1] = nextStreamId + 1; 54 | assertEq(actualStreamIds, expectedStreamIds); 55 | } 56 | 57 | /*////////////////////////////////////////////////////////////////////////// 58 | LOCKUP-LINEAR 59 | //////////////////////////////////////////////////////////////////////////*/ 60 | 61 | // Tests that creating streams works by checking the stream ids 62 | function test_BatchLockupLinearStreamCreator() public { 63 | uint256 nextStreamId = linearCreator.LOCKUP().nextStreamId(); 64 | uint256[] memory actualStreamIds = linearCreator.batchCreateStreams({ perStreamAmount: 1337e18 }); 65 | uint256[] memory expectedStreamIds = new uint256[](2); 66 | expectedStreamIds[0] = nextStreamId; 67 | expectedStreamIds[1] = nextStreamId + 1; 68 | assertEq(actualStreamIds, expectedStreamIds); 69 | } 70 | 71 | /*////////////////////////////////////////////////////////////////////////// 72 | LOCKUP-DYNAMIC 73 | //////////////////////////////////////////////////////////////////////////*/ 74 | 75 | // Tests that creating streams works by checking the stream ids 76 | function test_BatchLockupTranchedStreamCreator() public { 77 | uint256 nextStreamId = tranchedCreator.LOCKUP().nextStreamId(); 78 | uint256[] memory actualStreamIds = tranchedCreator.batchCreateStreams({ perStreamAmount: 1337e18 }); 79 | uint256[] memory expectedStreamIds = new uint256[](2); 80 | expectedStreamIds[0] = nextStreamId; 81 | expectedStreamIds[1] = nextStreamId + 1; 82 | assertEq(actualStreamIds, expectedStreamIds); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lockup/LockupDynamicCurvesCreator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import { ud2x18 } from "@prb/math/src/UD2x18.sol"; 6 | import { ud60x18 } from "@prb/math/src/UD60x18.sol"; 7 | import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; 8 | import { Broker, Lockup, LockupDynamic } from "@sablier/lockup/src/types/DataTypes.sol"; 9 | 10 | /// @notice Examples of how to create Lockup Dynamic streams with different curve shapes. 11 | /// @dev A visualization of the curve shapes can be found in the docs: 12 | /// https://docs.sablier.com/concepts/lockup/stream-shapes#lockup-dynamic 13 | /// Visualizing the curves while reviewing this code is recommended. The X axis will be assumed to represent "days". 14 | contract LockupDynamicCurvesCreator { 15 | // Mainnet addresses 16 | IERC20 public constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); 17 | ISablierLockup public constant LOCKUP = ISablierLockup(0x7C01AA3783577E15fD7e272443D44B92d5b21056); 18 | 19 | /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. 20 | function createStream_Exponential() external returns (uint256 streamId) { 21 | // Declare the total amount as 100 DAI 22 | uint128 totalAmount = 100e18; 23 | 24 | // Transfer the provided amount of DAI tokens to this contract 25 | DAI.transferFrom(msg.sender, address(this), totalAmount); 26 | 27 | // Approve the Sablier contract to spend DAI 28 | DAI.approve(address(LOCKUP), totalAmount); 29 | 30 | // Declare the params struct 31 | Lockup.CreateWithDurations memory params; 32 | 33 | // Declare the function parameters 34 | params.sender = msg.sender; // The sender will be able to cancel the stream 35 | params.recipient = address(0xCAFE); // The recipient of the streamed tokens 36 | params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees 37 | params.token = DAI; // The streaming token 38 | params.cancelable = true; // Whether the stream will be cancelable or not 39 | params.transferable = true; // Whether the stream will be transferable or not 40 | params.broker = Broker(address(0), ud60x18(0)); // Optional broker fee 41 | 42 | // Declare a single-size segment to match the curve shape 43 | LockupDynamic.SegmentWithDuration[] memory segments = new LockupDynamic.SegmentWithDuration[](1); 44 | segments[0] = LockupDynamic.SegmentWithDuration({ 45 | amount: uint128(totalAmount), 46 | duration: 100 days, 47 | exponent: ud2x18(6e18) 48 | }); 49 | 50 | // Create the Lockup stream using dynamic model with exponential shape 51 | streamId = LOCKUP.createWithDurationsLD(params, segments); 52 | } 53 | 54 | /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. 55 | function createStream_ExponentialCliff() external returns (uint256 streamId) { 56 | // Declare the total amount as 100 DAI 57 | uint128 totalAmount = 100e18; 58 | 59 | // Transfer the provided amount of DAI tokens to this contract 60 | DAI.transferFrom(msg.sender, address(this), totalAmount); 61 | 62 | // Approve the Sablier contract to spend DAI 63 | DAI.approve(address(LOCKUP), totalAmount); 64 | 65 | // Declare the params struct 66 | Lockup.CreateWithDurations memory params; 67 | 68 | // Declare the function parameters 69 | params.sender = msg.sender; // The sender will be able to cancel the stream 70 | params.recipient = address(0xCAFE); // The recipient of the streamed tokens 71 | params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees 72 | params.token = DAI; // The streaming token 73 | params.cancelable = true; // Whether the stream will be cancelable or not 74 | params.broker = Broker(address(0), ud60x18(0)); // Optional broker fee 75 | 76 | // Declare a three-size segment to match the curve shape 77 | LockupDynamic.SegmentWithDuration[] memory segments = new LockupDynamic.SegmentWithDuration[](3); 78 | segments[0] = 79 | LockupDynamic.SegmentWithDuration({ amount: 0, duration: 50 days - 1 seconds, exponent: ud2x18(1e18) }); 80 | segments[1] = LockupDynamic.SegmentWithDuration({ amount: 20e18, duration: 1 seconds, exponent: ud2x18(1e18) }); 81 | segments[2] = LockupDynamic.SegmentWithDuration({ amount: 80e18, duration: 50 days, exponent: ud2x18(6e18) }); 82 | 83 | // Create the Lockup stream using dynamic model with exponential cliff shape 84 | streamId = LOCKUP.createWithDurationsLD(params, segments); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lockup/LockupDynamicCurvesCreator.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3-0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { Test } from "forge-std/src/Test.sol"; 5 | 6 | import { LockupDynamicCurvesCreator } from "./LockupDynamicCurvesCreator.sol"; 7 | 8 | contract LockupDynamicCurvesCreatorTest is Test { 9 | // Test contracts 10 | LockupDynamicCurvesCreator internal creator; 11 | 12 | address internal user; 13 | 14 | function setUp() public { 15 | // Fork Ethereum Mainnet 16 | vm.createSelectFork("mainnet"); 17 | 18 | // Deploy the stream creator 19 | creator = new LockupDynamicCurvesCreator(); 20 | 21 | // Create a test user 22 | user = payable(makeAddr("User")); 23 | vm.deal({ account: user, newBalance: 1 ether }); 24 | 25 | // Mint some DAI tokens to the test user, which will be pulled by the creator contract 26 | deal({ token: address(creator.DAI()), to: user, give: 1337e18 }); 27 | 28 | // Make the test user the `msg.sender` in all following calls 29 | vm.startPrank({ msgSender: user }); 30 | 31 | // Approve the creator contract to pull DAI tokens from the test user 32 | creator.DAI().approve({ spender: address(creator), value: 1337e18 }); 33 | } 34 | 35 | function test_CreateStream_Exponential() public { 36 | uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); 37 | uint256 actualStreamId = creator.createStream_Exponential(); 38 | 39 | // Assert that the stream has been created. 40 | assertEq(actualStreamId, expectedStreamId); 41 | 42 | // Warp 50 days into the future, i.e. half way of the stream duration. 43 | vm.warp({ newTimestamp: block.timestamp + 50 days }); 44 | 45 | uint128 actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); 46 | uint128 expectedStreamedAmount = 1.5625e18; // 0.5^{6} * 100 + 0 47 | assertEq(actualStreamedAmount, expectedStreamedAmount); 48 | } 49 | 50 | function test_CreateStream_ExponentialCliff() public { 51 | uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); 52 | uint256 actualStreamId = creator.createStream_ExponentialCliff(); 53 | 54 | // Assert that the stream has been created. 55 | assertEq(actualStreamId, expectedStreamId); 56 | 57 | uint256 blockTimestamp = block.timestamp; 58 | 59 | // Warp 50 days into the future, i.e. half way of the stream duration (unlock moment). 60 | vm.warp({ newTimestamp: blockTimestamp + 50 days }); 61 | 62 | uint128 actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); 63 | uint128 expectedStreamedAmount = 20e18; 64 | assertEq(actualStreamedAmount, expectedStreamedAmount); 65 | 66 | // Warp 75 days into the future, i.e. half way of the stream's last segment. 67 | vm.warp({ newTimestamp: blockTimestamp + 75 days }); 68 | 69 | actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); 70 | expectedStreamedAmount = 21.25e18; // 0.5^{6} * 80 + 20 71 | assertEq(actualStreamedAmount, expectedStreamedAmount); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lockup/LockupDynamicStreamCreator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import { ud2x18 } from "@prb/math/src/UD2x18.sol"; 6 | import { ud60x18 } from "@prb/math/src/UD60x18.sol"; 7 | import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; 8 | import { Broker, Lockup, LockupDynamic } from "@sablier/lockup/src/types/DataTypes.sol"; 9 | 10 | /// @notice Example of how to create a Lockup Dynamic stream. 11 | /// @dev This code is referenced in the docs: 12 | /// https://docs.sablier.com/guides/lockup/examples/create-stream/lockup-dynamic 13 | contract LockupDynamicStreamCreator { 14 | // Mainnet addresses 15 | IERC20 public constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); 16 | ISablierLockup public constant LOCKUP = ISablierLockup(0x7C01AA3783577E15fD7e272443D44B92d5b21056); 17 | 18 | /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. 19 | function createStream(uint128 amount0, uint128 amount1) public returns (uint256 streamId) { 20 | // Sum the segment amounts 21 | uint256 totalAmount = amount0 + amount1; 22 | 23 | // Transfer the provided amount of DAI tokens to this contract 24 | DAI.transferFrom(msg.sender, address(this), totalAmount); 25 | 26 | // Approve the Sablier contract to spend DAI 27 | DAI.approve(address(LOCKUP), totalAmount); 28 | 29 | // Declare the params struct 30 | Lockup.CreateWithTimestamps memory params; 31 | 32 | // Declare the function parameters 33 | params.sender = msg.sender; // The sender will be able to cancel the stream 34 | params.recipient = address(0xCAFE); // The recipient of the streamed tokens 35 | params.totalAmount = uint128(totalAmount); // Total amount is the amount inclusive of all fees 36 | params.token = DAI; // The streaming token 37 | params.cancelable = true; // Whether the stream will be cancelable or not 38 | params.transferable = true; // Whether the stream will be transferable or not 39 | params.timestamps.start = uint40(block.timestamp + 100 seconds); 40 | params.timestamps.end = uint40(block.timestamp + 52 weeks); 41 | 42 | // Declare some dummy segments 43 | LockupDynamic.Segment[] memory segments = new LockupDynamic.Segment[](2); 44 | segments[0] = LockupDynamic.Segment({ 45 | amount: amount0, 46 | exponent: ud2x18(1e18), 47 | timestamp: uint40(block.timestamp + 4 weeks) 48 | }); 49 | segments[1] = ( 50 | LockupDynamic.Segment({ 51 | amount: amount1, 52 | exponent: ud2x18(3.14e18), 53 | timestamp: uint40(block.timestamp + 52 weeks) 54 | }) 55 | ); 56 | 57 | params.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined 58 | 59 | // Create the LockupDynamic stream 60 | streamId = LOCKUP.createWithTimestampsLD(params, segments); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lockup/LockupLinearCurvesCreator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import { ud60x18 } from "@prb/math/src/UD60x18.sol"; 6 | import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; 7 | import { Broker, Lockup, LockupLinear } from "@sablier/lockup/src/types/DataTypes.sol"; 8 | 9 | /// @notice Examples of how to create Lockup Linear streams with different curve shapes. 10 | /// @dev A visualization of the curve shapes can be found in the docs: 11 | /// https://docs.sablier.com/concepts/lockup/stream-shapes#lockup-linear 12 | /// Visualizing the curves while reviewing this code is recommended. The X axis will be assumed to represent "days". 13 | contract LockupLinearCurvesCreator { 14 | // Mainnet addresses 15 | IERC20 public constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); 16 | ISablierLockup public constant LOCKUP = ISablierLockup(0x7C01AA3783577E15fD7e272443D44B92d5b21056); 17 | 18 | /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. 19 | function createStream_Linear() public returns (uint256 streamId) { 20 | // Declare the total amount as 100 DAI 21 | uint128 totalAmount = 100e18; 22 | 23 | // Transfer the provided amount of DAI tokens to this contract 24 | DAI.transferFrom(msg.sender, address(this), totalAmount); 25 | 26 | // Approve the Sablier contract to spend DAI 27 | DAI.approve(address(LOCKUP), totalAmount); 28 | 29 | // Declare the params struct 30 | Lockup.CreateWithDurations memory params; 31 | 32 | // Declare the function parameters 33 | params.sender = msg.sender; // The sender will be able to cancel the stream 34 | params.recipient = address(0xCAFE); // The recipient of the streamed tokens 35 | params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees 36 | params.token = DAI; // The streaming token 37 | params.cancelable = true; // Whether the stream will be cancelable or not 38 | params.transferable = true; // Whether the stream will be transferable or not 39 | params.broker = Broker(address(0), ud60x18(0)); // Optional broker fee 40 | 41 | LockupLinear.UnlockAmounts memory unlockAmounts = LockupLinear.UnlockAmounts({ start: 0, cliff: 0 }); 42 | LockupLinear.Durations memory durations = LockupLinear.Durations({ 43 | cliff: 0, // Setting a cliff of 0 44 | total: 100 days // Setting a total duration of 100 days 45 | }); 46 | 47 | // Create the Lockup stream with Linear shape, no cliff and start time as `block.timestamp` 48 | streamId = LOCKUP.createWithDurationsLL(params, unlockAmounts, durations); 49 | } 50 | 51 | /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. 52 | function createStream_CliffUnlock() public returns (uint256 streamId) { 53 | // Declare the total amount as 100 DAI 54 | uint128 totalAmount = 100e18; 55 | 56 | // Transfer the provided amount of DAI tokens to this contract 57 | DAI.transferFrom(msg.sender, address(this), totalAmount); 58 | 59 | // Approve the Sablier contract to spend DAI 60 | DAI.approve(address(LOCKUP), totalAmount); 61 | 62 | // Declare the params struct 63 | Lockup.CreateWithDurations memory params; 64 | 65 | // Declare the function parameters 66 | params.sender = msg.sender; // The sender will be able to cancel the stream 67 | params.recipient = address(0xCAFE); // The recipient of the streamed tokens 68 | params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees 69 | params.token = DAI; // The streaming token 70 | params.cancelable = true; // Whether the stream will be cancelable or not 71 | params.transferable = true; // Whether the stream will be transferable or not 72 | params.broker = Broker(address(0), ud60x18(0)); // Optional broker fee 73 | 74 | // Setting a cliff unlock amount of 25 DAI 75 | LockupLinear.UnlockAmounts memory unlockAmounts = LockupLinear.UnlockAmounts({ start: 0, cliff: 25e18 }); 76 | LockupLinear.Durations memory durations = LockupLinear.Durations({ 77 | cliff: 25 days, // Setting a cliff of 25 days 78 | total: 100 days // Setting a total duration of 100 days 79 | }); 80 | 81 | // Create the Lockup stream with Linear shape, a cliff and start time as `block.timestamp` 82 | streamId = LOCKUP.createWithDurationsLL(params, unlockAmounts, durations); 83 | } 84 | 85 | /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. 86 | function createStream_InitialUnlock() public returns (uint256 streamId) { 87 | // Declare the total amount as 100 DAI 88 | uint128 totalAmount = 100e18; 89 | 90 | // Transfer the provided amount of DAI tokens to this contract 91 | DAI.transferFrom(msg.sender, address(this), totalAmount); 92 | 93 | // Approve the Sablier contract to spend DAI 94 | DAI.approve(address(LOCKUP), totalAmount); 95 | 96 | // Declare the params struct 97 | Lockup.CreateWithDurations memory params; 98 | 99 | // Declare the function parameters 100 | params.sender = msg.sender; // The sender will be able to cancel the stream 101 | params.recipient = address(0xCAFE); // The recipient of the streamed tokens 102 | params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees 103 | params.token = DAI; // The streaming token 104 | params.cancelable = true; // Whether the stream will be cancelable or not 105 | params.transferable = true; // Whether the stream will be transferable or not 106 | params.broker = Broker(address(0), ud60x18(0)); // Optional broker fee 107 | 108 | // Setting an initial unlock amount of 25 DAI 109 | LockupLinear.UnlockAmounts memory unlockAmounts = LockupLinear.UnlockAmounts({ start: 25e18, cliff: 0 }); 110 | LockupLinear.Durations memory durations = LockupLinear.Durations({ 111 | cliff: 0, // Setting a cliff of 0 112 | total: 100 days // Setting a total duration of 100 days 113 | }); 114 | 115 | // Create the Lockup stream with Linear shape, an initial unlock, no cliff and start time as `block.timestamp` 116 | streamId = LOCKUP.createWithDurationsLL(params, unlockAmounts, durations); 117 | } 118 | 119 | /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. 120 | function createStream_InitialCliffUnlock() public returns (uint256 streamId) { 121 | // Declare the total amount as 100 DAI 122 | uint128 totalAmount = 100e18; 123 | 124 | // Transfer the provided amount of DAI tokens to this contract 125 | DAI.transferFrom(msg.sender, address(this), totalAmount); 126 | 127 | // Approve the Sablier contract to spend DAI 128 | DAI.approve(address(LOCKUP), totalAmount); 129 | 130 | // Declare the params struct 131 | Lockup.CreateWithDurations memory params; 132 | 133 | // Declare the function parameters 134 | params.sender = msg.sender; // The sender will be able to cancel the stream 135 | params.recipient = address(0xCAFE); // The recipient of the streamed tokens 136 | params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees 137 | params.token = DAI; // The streaming token 138 | params.cancelable = true; // Whether the stream will be cancelable or not 139 | params.transferable = true; // Whether the stream will be transferable or not 140 | params.broker = Broker(address(0), ud60x18(0)); // Optional broker fee 141 | 142 | // Setting an initial and a cliff unlock amount of 25 DAI 143 | LockupLinear.UnlockAmounts memory unlockAmounts = LockupLinear.UnlockAmounts({ start: 25e18, cliff: 25e18 }); 144 | LockupLinear.Durations memory durations = LockupLinear.Durations({ 145 | cliff: 50 days, // Setting a cliff of 50 days 146 | total: 100 days // Setting a total duration of 100 days 147 | }); 148 | 149 | // Create the Lockup stream with Linear shape, an initial unlock, a cliff and start time as `block.timestamp` 150 | streamId = LOCKUP.createWithDurationsLL(params, unlockAmounts, durations); 151 | } 152 | 153 | /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. 154 | function createStream_ConstantCliff() public returns (uint256 streamId) { 155 | // Declare the total amount as 100 DAI 156 | uint128 totalAmount = 100e18; 157 | 158 | // Transfer the provided amount of DAI tokens to this contract 159 | DAI.transferFrom(msg.sender, address(this), totalAmount); 160 | 161 | // Approve the Sablier contract to spend DAI 162 | DAI.approve(address(LOCKUP), totalAmount); 163 | 164 | // Declare the params struct 165 | Lockup.CreateWithDurations memory params; 166 | 167 | // Declare the function parameters 168 | params.sender = msg.sender; // The sender will be able to cancel the stream 169 | params.recipient = address(0xCAFE); // The recipient of the streamed tokens 170 | params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees 171 | params.token = DAI; // The streaming token 172 | params.cancelable = true; // Whether the stream will be cancelable or not 173 | params.transferable = true; // Whether the stream will be transferable or not 174 | params.broker = Broker(address(0), ud60x18(0)); // Optional broker fee 175 | 176 | LockupLinear.UnlockAmounts memory unlockAmounts = LockupLinear.UnlockAmounts({ start: 0, cliff: 0 }); 177 | LockupLinear.Durations memory durations = LockupLinear.Durations({ 178 | cliff: 25 days, // Setting a cliff of 25 days 179 | total: 100 days // Setting a total duration of 100 days 180 | }); 181 | 182 | // Create the Lockup stream with Linear shape, zero unlock until cliff and start time as `block.timestamp` 183 | streamId = LOCKUP.createWithDurationsLL(params, unlockAmounts, durations); 184 | } 185 | 186 | /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. 187 | function createStream_InitialUnlockConstantCliff() public returns (uint256 streamId) { 188 | // Declare the total amount as 100 DAI 189 | uint128 totalAmount = 100e18; 190 | 191 | // Transfer the provided amount of DAI tokens to this contract 192 | DAI.transferFrom(msg.sender, address(this), totalAmount); 193 | 194 | // Approve the Sablier contract to spend DAI 195 | DAI.approve(address(LOCKUP), totalAmount); 196 | 197 | // Declare the params struct 198 | Lockup.CreateWithDurations memory params; 199 | 200 | // Declare the function parameters 201 | params.sender = msg.sender; // The sender will be able to cancel the stream 202 | params.recipient = address(0xCAFE); // The recipient of the streamed tokens 203 | params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees 204 | params.token = DAI; // The streaming token 205 | params.cancelable = true; // Whether the stream will be cancelable or not 206 | params.transferable = true; // Whether the stream will be transferable or not 207 | params.broker = Broker(address(0), ud60x18(0)); // Optional broker fee 208 | 209 | // Setting an initial unlock amount of 25 DAI 210 | LockupLinear.UnlockAmounts memory unlockAmounts = LockupLinear.UnlockAmounts({ start: 25e18, cliff: 0 }); 211 | LockupLinear.Durations memory durations = LockupLinear.Durations({ 212 | cliff: 25 days, // Setting a cliff of 25 days 213 | total: 100 days // Setting a total duration of 100 days 214 | }); 215 | 216 | // Create the Lockup stream with Linear shape, an initial unlock followed by zero unlock until cliff and start 217 | // time as `block.timestamp` 218 | streamId = LOCKUP.createWithDurationsLL(params, unlockAmounts, durations); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /lockup/LockupLinearCurvesCreator.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3-0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { Test } from "forge-std/src/Test.sol"; 5 | 6 | import { LockupLinearCurvesCreator } from "./LockupLinearCurvesCreator.sol"; 7 | 8 | contract LockupLinearCurvesCreatorTest is Test { 9 | // Test contracts 10 | LockupLinearCurvesCreator internal creator; 11 | 12 | address internal user; 13 | 14 | function setUp() public { 15 | // Fork Ethereum Mainnet 16 | vm.createSelectFork("mainnet"); 17 | 18 | // Deploy the stream creator 19 | creator = new LockupLinearCurvesCreator(); 20 | 21 | // Create a test user 22 | user = payable(makeAddr("User")); 23 | vm.deal({ account: user, newBalance: 1 ether }); 24 | 25 | // Mint some DAI tokens to the test user, which will be pulled by the creator contract 26 | deal({ token: address(creator.DAI()), to: user, give: 1337e18 }); 27 | 28 | // Make the test user the `msg.sender` in all following calls 29 | vm.startPrank({ msgSender: user }); 30 | 31 | // Approve the creator contract to pull DAI tokens from the test user 32 | creator.DAI().approve({ spender: address(creator), value: 1337e18 }); 33 | } 34 | 35 | function test_CreateStream_Linear() external { 36 | uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); 37 | uint256 actualStreamId = creator.createStream_Linear(); 38 | 39 | // Assert that the stream has been created. 40 | assertEq(actualStreamId, expectedStreamId, "streamId"); 41 | 42 | uint256 blockTimestamp = block.timestamp; 43 | 44 | // Assert that the amount is zero at start. 45 | uint128 actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); 46 | uint128 expectedStreamedAmount = 0; 47 | assertEq(actualStreamedAmount, expectedStreamedAmount); 48 | 49 | // Warp 20 days into the future. 50 | vm.warp({ newTimestamp: blockTimestamp + 20 days }); 51 | 52 | // Assert that the streamed amount has linearly increased. 53 | actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); 54 | expectedStreamedAmount = 20e18; 55 | assertEq(actualStreamedAmount, expectedStreamedAmount); 56 | 57 | // Warp 50 days into the future. 58 | vm.warp({ newTimestamp: blockTimestamp + 50 days }); 59 | 60 | // Assert that the streamed amount has linearly increased. 61 | actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); 62 | expectedStreamedAmount = 50e18; 63 | assertEq(actualStreamedAmount, expectedStreamedAmount); 64 | } 65 | 66 | function test_CreateStream_CliffUnlock() external { 67 | uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); 68 | uint256 actualStreamId = creator.createStream_CliffUnlock(); 69 | 70 | // Assert that the stream has been created. 71 | assertEq(actualStreamId, expectedStreamId); 72 | 73 | uint256 blockTimestamp = block.timestamp; 74 | 75 | // Warp a second before the cliff. 76 | vm.warp({ newTimestamp: blockTimestamp + 25 days - 1 seconds }); 77 | 78 | uint128 actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); 79 | uint128 expectedStreamedAmount = 0; 80 | assertEq(actualStreamedAmount, expectedStreamedAmount); 81 | 82 | // Warp to the cliff time. 83 | vm.warp({ newTimestamp: blockTimestamp + 25 days }); 84 | 85 | actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); 86 | expectedStreamedAmount = 25e18; 87 | assertEq(actualStreamedAmount, expectedStreamedAmount, "cliff unlock amount"); 88 | 89 | // Warp to the halfway point of the stream duration. 90 | vm.warp({ newTimestamp: blockTimestamp + 50 days }); 91 | 92 | // Assert that the streamed amount has linearly increased. 93 | actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); 94 | expectedStreamedAmount = 50e18; 95 | assertApproxEqAbs(actualStreamedAmount, expectedStreamedAmount, 100, "cliff unlock amount + linear streaming"); 96 | } 97 | 98 | function test_CreateStream_InitialUnlock() external { 99 | uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); 100 | uint256 actualStreamId = creator.createStream_InitialUnlock(); 101 | 102 | // Assert that the stream has been created. 103 | assertEq(actualStreamId, expectedStreamId); 104 | 105 | uint256 blockTimestamp = block.timestamp; 106 | 107 | // Warp 1 second into the future, i.e. the initial unlock. 108 | vm.warp({ newTimestamp: blockTimestamp }); 109 | 110 | uint128 actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); 111 | uint128 expectedStreamedAmount = 25e18; 112 | assertEq(actualStreamedAmount, expectedStreamedAmount); 113 | 114 | // Warp to the halfway point of the stream duration. 115 | vm.warp({ newTimestamp: blockTimestamp + 50 days }); 116 | 117 | actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); 118 | expectedStreamedAmount = 62.5e18; // 0.50 * 75 + 25 119 | assertEq(actualStreamedAmount, expectedStreamedAmount); 120 | } 121 | 122 | function test_CreateStream_InitialCliffUnlock() external { 123 | uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); 124 | uint256 actualStreamId = creator.createStream_InitialCliffUnlock(); 125 | 126 | // Assert that the stream has been created. 127 | assertEq(actualStreamId, expectedStreamId); 128 | 129 | uint256 blockTimestamp = block.timestamp; 130 | 131 | // Assert that the streamed amount is the initial unlock amount. 132 | uint128 actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); 133 | uint128 expectedStreamedAmount = 25e18; 134 | assertEq(actualStreamedAmount, expectedStreamedAmount, "initial unlock"); 135 | 136 | // Warp 1 second before the cliff time. 137 | vm.warp({ newTimestamp: blockTimestamp + 50 days - 1 }); 138 | 139 | // Assert that the streamed amount has remained the same. 140 | actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); 141 | assertEq(actualStreamedAmount, expectedStreamedAmount, "1 second before cliff"); 142 | 143 | // Warp to the cliff time. 144 | vm.warp({ newTimestamp: blockTimestamp + 50 days }); 145 | 146 | // Assert that the streamed amount has unlocked the cliff amount. 147 | actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); 148 | expectedStreamedAmount = 50e18; 149 | assertEq(actualStreamedAmount, expectedStreamedAmount, "cliff unlock amount"); 150 | 151 | // Warp 75 days into the future. 152 | vm.warp({ newTimestamp: blockTimestamp + 75 days }); 153 | 154 | // Assert that the streamed amount has increased linearly after cliff. 155 | actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); 156 | expectedStreamedAmount = 75e18; 157 | assertApproxEqAbs(actualStreamedAmount, expectedStreamedAmount, 100, "linear streaming after cliff"); 158 | } 159 | 160 | function test_CreateStream_ConstantCliff() external { 161 | uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); 162 | uint256 actualStreamId = creator.createStream_ConstantCliff(); 163 | 164 | // Assert that the stream has been created. 165 | assertEq(actualStreamId, expectedStreamId); 166 | 167 | uint256 blockTimestamp = block.timestamp; 168 | 169 | // Warp 1 second into the future. 170 | vm.warp({ newTimestamp: blockTimestamp + 1 seconds }); 171 | 172 | // Assert that the streamed amount is zero. 173 | uint128 actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); 174 | uint128 expectedStreamedAmount = 0; 175 | assertEq(actualStreamedAmount, expectedStreamedAmount, "not zero"); 176 | 177 | // Warp 1 second before the cliff time. 178 | vm.warp({ newTimestamp: blockTimestamp + 25 days - 1 }); 179 | 180 | // Assert that the streamed amount has remained the same. 181 | actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); 182 | assertEq(actualStreamedAmount, expectedStreamedAmount, "1 second before cliff"); 183 | 184 | // Warp to 62.5 days into the future. 185 | vm.warp({ newTimestamp: blockTimestamp + 62.5 days }); 186 | 187 | // Assert that the streamed amount has linearly increased. 188 | actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); 189 | expectedStreamedAmount = 50e18; 190 | assertEq(actualStreamedAmount, expectedStreamedAmount, "linear streaming"); 191 | } 192 | 193 | function test_CreateStream_InitialUnlockConstantCliff() external { 194 | uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); 195 | uint256 actualStreamId = creator.createStream_InitialUnlockConstantCliff(); 196 | 197 | // Assert that the stream has been created. 198 | assertEq(actualStreamId, expectedStreamId); 199 | 200 | uint256 blockTimestamp = block.timestamp; 201 | 202 | // Assert that the streamed amount is zero. 203 | uint128 actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); 204 | uint128 expectedStreamedAmount = 25e18; 205 | assertEq(actualStreamedAmount, expectedStreamedAmount, "initial unlock"); 206 | 207 | // Warp 1 second before the cliff time. 208 | vm.warp({ newTimestamp: blockTimestamp + 25 days - 1 }); 209 | 210 | // Assert that the streamed amount has remained the same. 211 | actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); 212 | assertEq(actualStreamedAmount, expectedStreamedAmount, "1 second before cliff"); 213 | 214 | // Warp to 75 days into the future. 215 | vm.warp({ newTimestamp: blockTimestamp + 62.5 days }); 216 | 217 | // Assert that the streamed amount has linearly increased. 218 | actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); 219 | expectedStreamedAmount = 62.5e18; 220 | assertEq(actualStreamedAmount, expectedStreamedAmount, "linear streaming"); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /lockup/LockupLinearStreamCreator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import { ud60x18 } from "@prb/math/src/UD60x18.sol"; 6 | import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; 7 | import { Broker, Lockup, LockupLinear } from "@sablier/lockup/src/types/DataTypes.sol"; 8 | 9 | /// @notice Example of how to create a Lockup Linear stream. 10 | /// @dev This code is referenced in the docs: 11 | /// https://docs.sablier.com/guides/lockup/examples/create-stream/lockup-linear 12 | contract LockupLinearStreamCreator { 13 | // Mainnet addresses 14 | IERC20 public constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); 15 | ISablierLockup public constant LOCKUP = ISablierLockup(0x7C01AA3783577E15fD7e272443D44B92d5b21056); 16 | 17 | /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. 18 | function createStream(uint128 totalAmount) public returns (uint256 streamId) { 19 | // Transfer the provided amount of DAI tokens to this contract 20 | DAI.transferFrom(msg.sender, address(this), totalAmount); 21 | 22 | // Approve the Sablier contract to spend DAI 23 | DAI.approve(address(LOCKUP), totalAmount); 24 | 25 | // Declare the params struct 26 | Lockup.CreateWithDurations memory params; 27 | 28 | // Declare the function parameters 29 | params.sender = msg.sender; // The sender will be able to cancel the stream 30 | params.recipient = address(0xCAFE); // The recipient of the streamed tokens 31 | params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees 32 | params.token = DAI; // The streaming token 33 | params.cancelable = true; // Whether the stream will be cancelable or not 34 | params.transferable = true; // Whether the stream will be transferable or not 35 | params.broker = Broker(address(0), ud60x18(0)); // Optional parameter for charging a fee 36 | 37 | LockupLinear.UnlockAmounts memory unlockAmounts = LockupLinear.UnlockAmounts({ start: 0, cliff: 0 }); 38 | LockupLinear.Durations memory durations = LockupLinear.Durations({ 39 | cliff: 0, // Setting a cliff of 0 40 | total: 52 weeks // Setting a total duration of ~1 year 41 | }); 42 | 43 | // Create the LockupLinear stream using a function that sets the start time to `block.timestamp` 44 | streamId = LOCKUP.createWithDurationsLL(params, unlockAmounts, durations); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lockup/LockupStreamCreator.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3-0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { Test } from "forge-std/src/Test.sol"; 5 | 6 | import { LockupDynamicStreamCreator } from "./LockupDynamicStreamCreator.sol"; 7 | import { LockupLinearStreamCreator } from "./LockupLinearStreamCreator.sol"; 8 | import { LockupTranchedStreamCreator } from "./LockupTranchedStreamCreator.sol"; 9 | 10 | contract LockupStreamCreatorTest is Test { 11 | // Test contracts 12 | LockupDynamicStreamCreator internal dynamicCreator; 13 | LockupLinearStreamCreator internal linearCreator; 14 | LockupTranchedStreamCreator internal tranchedCreator; 15 | 16 | address internal user; 17 | 18 | function setUp() public { 19 | // Fork Ethereum Mainnet 20 | vm.createSelectFork("mainnet"); 21 | 22 | // Deploy the stream creators 23 | dynamicCreator = new LockupDynamicStreamCreator(); 24 | linearCreator = new LockupLinearStreamCreator(); 25 | tranchedCreator = new LockupTranchedStreamCreator(); 26 | 27 | // Create a test user 28 | user = payable(makeAddr("User")); 29 | vm.deal({ account: user, newBalance: 1 ether }); 30 | 31 | // Mint some DAI tokens to the test user, which will be pulled by the creator contracts 32 | deal({ token: address(linearCreator.DAI()), to: user, give: 3 * 1337e18 }); 33 | 34 | // Make the test user the `msg.sender` in all following calls 35 | vm.startPrank({ msgSender: user }); 36 | 37 | // Approve the creator contracts to pull DAI tokens from the test user 38 | linearCreator.DAI().approve({ spender: address(dynamicCreator), value: 1337e18 }); 39 | linearCreator.DAI().approve({ spender: address(linearCreator), value: 1337e18 }); 40 | linearCreator.DAI().approve({ spender: address(tranchedCreator), value: 1337e18 }); 41 | } 42 | 43 | // Tests that creating streams works by checking the stream ids 44 | function test_LockupDynamicStreamCreator() public { 45 | uint128 amount0 = 1337e18 / 2; 46 | uint128 amount1 = 1337e18 - amount0; 47 | 48 | uint256 expectedStreamId = dynamicCreator.LOCKUP().nextStreamId(); 49 | uint256 actualStreamId = dynamicCreator.createStream(amount0, amount1); 50 | assertEq(actualStreamId, expectedStreamId); 51 | } 52 | 53 | // Tests that creating streams works by checking the stream ids 54 | function test_LockupLinearStreamCreator() public { 55 | uint256 expectedStreamId = linearCreator.LOCKUP().nextStreamId(); 56 | uint256 actualStreamId = linearCreator.createStream({ totalAmount: 1337e18 }); 57 | assertEq(actualStreamId, expectedStreamId); 58 | } 59 | 60 | // Tests that creating streams works by checking the stream ids 61 | function test_LockupTranchedStreamCreator() public { 62 | uint128 amount0 = 1337e18 / 2; 63 | uint128 amount1 = 1337e18 - amount0; 64 | 65 | uint256 expectedStreamId = tranchedCreator.LOCKUP().nextStreamId(); 66 | uint256 actualStreamId = tranchedCreator.createStream(amount0, amount1); 67 | assertEq(actualStreamId, expectedStreamId); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lockup/LockupTranchedCurvesCreator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import { ud60x18 } from "@prb/math/src/UD60x18.sol"; 6 | import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; 7 | import { Broker, Lockup, LockupTranched } from "@sablier/lockup/src/types/DataTypes.sol"; 8 | 9 | /// @notice Examples of how to create Lockup Linear streams with different curve shapes. 10 | /// @dev A visualization of the curve shapes can be found in the docs: 11 | /// https://docs.sablier.com/concepts/lockup/stream-shapes#lockup-tranched 12 | /// Visualizing the curves while reviewing this code is recommended. The X axis will be assumed to represent "days". 13 | contract LockupTranchedCurvesCreator { 14 | // Mainnet addresses 15 | IERC20 public constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); 16 | ISablierLockup public constant LOCKUP = ISablierLockup(0x7C01AA3783577E15fD7e272443D44B92d5b21056); 17 | 18 | function createStream_UnlockInSteps() external returns (uint256 streamId) { 19 | // Declare the total amount as 100 DAI 20 | uint128 totalAmount = 100e18; 21 | 22 | // Transfer the provided amount of DAI tokens to this contract 23 | DAI.transferFrom(msg.sender, address(this), totalAmount); 24 | 25 | // Approve the Sablier contract to spend DAI 26 | DAI.approve(address(LOCKUP), totalAmount); 27 | 28 | // Declare the params struct 29 | Lockup.CreateWithDurations memory params; 30 | 31 | // Declare the function parameters 32 | params.sender = msg.sender; // The sender will be able to cancel the stream 33 | params.recipient = address(0xCAFE); // The recipient of the streamed tokens 34 | params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees 35 | params.token = DAI; // The streaming token 36 | params.cancelable = true; // Whether the stream will be cancelable or not 37 | params.broker = Broker(address(0), ud60x18(0)); // Optional broker fee 38 | 39 | // Declare a four-size tranche to match the curve shape 40 | uint256 trancheSize = 4; 41 | LockupTranched.TrancheWithDuration[] memory tranches = new LockupTranched.TrancheWithDuration[](trancheSize); 42 | 43 | // The tranches are filled with the same amount and are spaced 25 days apart 44 | uint128 unlockAmount = uint128(totalAmount / trancheSize); 45 | for (uint256 i = 0; i < trancheSize; ++i) { 46 | tranches[i] = LockupTranched.TrancheWithDuration({ amount: unlockAmount, duration: 25 days }); 47 | } 48 | 49 | // Create the Lockup stream using tranche model with periodic unlocks in step 50 | streamId = LOCKUP.createWithDurationsLT(params, tranches); 51 | } 52 | 53 | function createStream_MonthlyUnlocks() external returns (uint256 streamId) { 54 | // Declare the total amount as 120 DAI 55 | uint128 totalAmount = 120e18; 56 | 57 | // Transfer the provided amount of DAI tokens to this contract 58 | DAI.transferFrom(msg.sender, address(this), totalAmount); 59 | 60 | // Approve the Sablier contract to spend DAI 61 | DAI.approve(address(LOCKUP), totalAmount); 62 | 63 | // Declare the params struct 64 | Lockup.CreateWithDurations memory params; 65 | 66 | // Declare the function parameters 67 | params.sender = msg.sender; // The sender will be able to cancel the stream 68 | params.recipient = address(0xCAFE); // The recipient of the streamed tokens 69 | params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees 70 | params.token = DAI; // The streaming token 71 | params.cancelable = true; // Whether the stream will be cancelable or not 72 | params.broker = Broker(address(0), ud60x18(0)); // Optional broker fee 73 | 74 | // Declare a twenty four size tranche to match the curve shape 75 | uint256 trancheSize = 12; 76 | LockupTranched.TrancheWithDuration[] memory tranches = new LockupTranched.TrancheWithDuration[](trancheSize); 77 | 78 | // The tranches are spaced 30 days apart (~one month) 79 | uint128 unlockAmount = uint128(totalAmount / trancheSize); 80 | for (uint256 i = 0; i < trancheSize; ++i) { 81 | tranches[i] = LockupTranched.TrancheWithDuration({ amount: unlockAmount, duration: 30 days }); 82 | } 83 | 84 | // Create the Lockup stream using tranche model with web2 style monthly unlocks 85 | streamId = LOCKUP.createWithDurationsLT(params, tranches); 86 | } 87 | 88 | function createStream_Timelock() external returns (uint256 streamId) { 89 | // Declare the total amount as 100 DAI 90 | uint128 totalAmount = 100e18; 91 | 92 | // Transfer the provided amount of DAI tokens to this contract 93 | DAI.transferFrom(msg.sender, address(this), totalAmount); 94 | 95 | // Approve the Sablier contract to spend DAI 96 | DAI.approve(address(LOCKUP), totalAmount); 97 | 98 | // Declare the params struct 99 | Lockup.CreateWithDurations memory params; 100 | 101 | // Declare the function parameters 102 | params.sender = msg.sender; // The sender will be able to cancel the stream 103 | params.recipient = address(0xCAFE); // The recipient of the streamed tokens 104 | params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees 105 | params.token = DAI; // The streaming token 106 | params.cancelable = true; // Whether the stream will be cancelable or not 107 | params.broker = Broker(address(0), ud60x18(0)); // Optional broker fee 108 | 109 | // Declare a two-size tranche to match the curve shape 110 | LockupTranched.TrancheWithDuration[] memory tranches = new LockupTranched.TrancheWithDuration[](1); 111 | tranches[0] = LockupTranched.TrancheWithDuration({ amount: 100e18, duration: 90 days }); 112 | 113 | // Create the Lockup stream using tranche model with full unlock only at the end 114 | streamId = LOCKUP.createWithDurationsLT(params, tranches); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lockup/LockupTranchedCurvesCreator.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3-0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { Test } from "forge-std/src/Test.sol"; 5 | 6 | import { LockupTranchedCurvesCreator } from "./LockupTranchedCurvesCreator.sol"; 7 | 8 | contract LockupTranchedCurvesCreatorTest is Test { 9 | // Test contracts 10 | LockupTranchedCurvesCreator internal creator; 11 | 12 | address internal user; 13 | 14 | function setUp() public { 15 | // Fork Ethereum Mainnet 16 | vm.createSelectFork("mainnet"); 17 | 18 | // Deploy the stream creator 19 | creator = new LockupTranchedCurvesCreator(); 20 | 21 | // Create a test user 22 | user = payable(makeAddr("User")); 23 | vm.deal({ account: user, newBalance: 1 ether }); 24 | 25 | // Mint some DAI tokens to the test user, which will be pulled by the creator contract 26 | deal({ token: address(creator.DAI()), to: user, give: 1337e18 }); 27 | 28 | // Make the test user the `msg.sender` in all following calls 29 | vm.startPrank({ msgSender: user }); 30 | 31 | // Approve the creator contract to pull DAI tokens from the test user 32 | creator.DAI().approve({ spender: address(creator), value: 1337e18 }); 33 | } 34 | 35 | function test_CreateStream_UnlockInSteps() public { 36 | uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); 37 | uint256 actualStreamId = creator.createStream_UnlockInSteps(); 38 | 39 | // Assert that the stream has been created. 40 | assertEq(actualStreamId, expectedStreamId); 41 | 42 | uint256 actualStreamedAmount; 43 | uint256 expectedStreamedAmount; 44 | 45 | for (uint256 i = 0; i < 4; ++i) { 46 | vm.warp({ newTimestamp: block.timestamp + 25 days - 1 seconds }); 47 | actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); 48 | assertEq(actualStreamedAmount, expectedStreamedAmount); 49 | expectedStreamedAmount += 25e18; 50 | vm.warp({ newTimestamp: block.timestamp + 1 seconds }); 51 | } 52 | } 53 | 54 | function test_CreateStream_MonthlyUnlocks() public { 55 | uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); 56 | uint256 actualStreamId = creator.createStream_MonthlyUnlocks(); 57 | 58 | // Assert that the stream has been created. 59 | assertEq(actualStreamId, expectedStreamId); 60 | 61 | uint256 actualStreamedAmount; 62 | uint256 expectedStreamedAmount; 63 | 64 | for (uint256 i = 0; i < 12; ++i) { 65 | vm.warp({ newTimestamp: block.timestamp + 30 days - 1 seconds }); 66 | actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); 67 | 68 | assertEq(actualStreamedAmount, expectedStreamedAmount); 69 | expectedStreamedAmount += 10e18; 70 | vm.warp({ newTimestamp: block.timestamp + 1 seconds }); 71 | } 72 | } 73 | 74 | function test_CreateStream_Timelock() external { 75 | uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); 76 | uint256 actualStreamId = creator.createStream_Timelock(); 77 | 78 | // Assert that the stream has been created. 79 | assertEq(actualStreamId, expectedStreamId); 80 | 81 | uint256 blockTimestamp = block.timestamp; 82 | 83 | // Warp 90 days - 1 second into the future, i.e. exactly 1 second before unlock. 84 | vm.warp({ newTimestamp: blockTimestamp + 90 days - 1 seconds }); 85 | 86 | uint128 actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); 87 | uint128 expectedStreamedAmount = 0; 88 | assertEq(actualStreamedAmount, expectedStreamedAmount); 89 | 90 | // Warp 90 days into the future, i.e. the unlock moment. 91 | vm.warp({ newTimestamp: blockTimestamp + 90 days }); 92 | 93 | actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); 94 | expectedStreamedAmount = 100e18; 95 | assertEq(actualStreamedAmount, expectedStreamedAmount); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lockup/LockupTranchedStreamCreator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import { ud60x18 } from "@prb/math/src/UD60x18.sol"; 6 | import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; 7 | import { Broker, Lockup, LockupTranched } from "@sablier/lockup/src/types/DataTypes.sol"; 8 | 9 | /// @notice Example of how to create a Lockup Tranched stream. 10 | /// @dev This code is referenced in the docs: 11 | /// https://docs.sablier.com/guides/lockup/examples/create-stream/lockup-tranched 12 | contract LockupTranchedStreamCreator { 13 | // Mainnet addresses 14 | IERC20 public constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); 15 | ISablierLockup public constant LOCKUP = ISablierLockup(0x7C01AA3783577E15fD7e272443D44B92d5b21056); 16 | 17 | /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. 18 | function createStream(uint128 amount0, uint128 amount1) public returns (uint256 streamId) { 19 | // Sum the tranche amounts 20 | uint256 totalAmount = amount0 + amount1; 21 | 22 | // Transfer the provided amount of DAI tokens to this contract 23 | DAI.transferFrom(msg.sender, address(this), totalAmount); 24 | 25 | // Approve the Sablier contract to spend DAI 26 | DAI.approve(address(LOCKUP), totalAmount); 27 | 28 | // Declare the params struct 29 | Lockup.CreateWithDurations memory params; 30 | 31 | // Declare the function parameters 32 | params.sender = msg.sender; // The sender will be able to cancel the stream 33 | params.recipient = address(0xCAFE); // The recipient of the streamed tokens 34 | params.totalAmount = uint128(totalAmount); // Total amount is the amount inclusive of all fees 35 | params.token = DAI; // The streaming token 36 | params.cancelable = true; // Whether the stream will be cancelable or not 37 | params.transferable = true; // Whether the stream will be transferable or not 38 | 39 | // Declare some dummy tranches 40 | LockupTranched.TrancheWithDuration[] memory tranches = new LockupTranched.TrancheWithDuration[](2); 41 | tranches[0] = LockupTranched.TrancheWithDuration({ amount: amount0, duration: uint40(4 weeks) }); 42 | tranches[1] = (LockupTranched.TrancheWithDuration({ amount: amount1, duration: uint40(6 weeks) })); 43 | 44 | params.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined 45 | 46 | // Create the LockupTranched stream 47 | streamId = LOCKUP.createWithDurationsLT(params, tranches); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lockup/RecipientHooks.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22; 3 | 4 | import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; 5 | import { ISablierLockupRecipient } from "@sablier/lockup/src/interfaces/ISablierLockupRecipient.sol"; 6 | 7 | contract RecipientHooks is ISablierLockupRecipient { 8 | error CallerNotSablierContract(address caller, address sablierLockup); 9 | 10 | /// @dev The address of the lockup contract. It could be either LockupLinear, LockupDynamic or LockupTranched 11 | /// depending on which type of streams are supported in this hook. 12 | address public immutable SABLIER_LOCKUP; 13 | 14 | mapping(address account => uint256 amount) internal _balances; 15 | 16 | /// @dev Constructor will set the address of the lockup contract. 17 | constructor(address sablierLockup_) { 18 | SABLIER_LOCKUP = sablierLockup_; 19 | } 20 | 21 | // {IERC165-supportsInterface} implementation as required by `ISablierLockupRecipient` interface. 22 | function supportsInterface(bytes4 interfaceId) public pure override(IERC165) returns (bool) { 23 | return interfaceId == 0xf8ee98d3; 24 | } 25 | 26 | // This will be called by Sablier contract when a stream is canceled by the sender. 27 | function onSablierLockupCancel( 28 | uint256 streamId, 29 | address sender, 30 | uint128 senderAmount, 31 | uint128 recipientAmount 32 | ) 33 | external 34 | view 35 | returns (bytes4 selector) 36 | { 37 | // Check: the caller is the lockup contract. 38 | if (msg.sender != SABLIER_LOCKUP) { 39 | revert CallerNotSablierContract(msg.sender, SABLIER_LOCKUP); 40 | } 41 | 42 | // Unstake the user's NFT. 43 | _unstake({ nftId: streamId }); 44 | 45 | // Update data. 46 | _updateData(streamId, sender, senderAmount, recipientAmount); 47 | 48 | return ISablierLockupRecipient.onSablierLockupCancel.selector; 49 | } 50 | 51 | // This will be called by Sablier contract when withdraw is called on a stream. 52 | function onSablierLockupWithdraw( 53 | uint256 streamId, 54 | address caller, 55 | address to, 56 | uint128 amount 57 | ) 58 | external 59 | view 60 | returns (bytes4 selector) 61 | { 62 | // Check: the caller is the lockup contract. 63 | if (msg.sender != SABLIER_LOCKUP) { 64 | revert CallerNotSablierContract(msg.sender, SABLIER_LOCKUP); 65 | } 66 | 67 | // Transfer the withdrawn amount to the original user. 68 | _transfer(to, amount); 69 | 70 | // Update data. 71 | _updateData(streamId, caller, amount, 0); 72 | 73 | return ISablierLockupRecipient.onSablierLockupWithdraw.selector; 74 | } 75 | 76 | function _unstake(uint256 nftId) internal pure { } 77 | function _updateData( 78 | uint256 streamId, 79 | address sender, 80 | uint128 senderAmount, 81 | uint128 recipientAmount 82 | ) 83 | internal 84 | pure 85 | { } 86 | function _transfer(address to, uint128 amount) internal pure { } 87 | } 88 | -------------------------------------------------------------------------------- /lockup/StakeSablierNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 6 | import { ERC721Holder } from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; 7 | import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; 8 | import { Adminable } from "@sablier/lockup/src/abstracts/Adminable.sol"; 9 | import { ISablierLockupRecipient } from "@sablier/lockup/src/interfaces/ISablierLockupRecipient.sol"; 10 | import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; 11 | 12 | /// @title StakeSablierNFT 13 | /// 14 | /// @notice DISCLAIMER: This template has not been audited and is provided "as is" with no warranties of any kind, 15 | /// either express or implied. It is intended solely for demonstration purposes on how to build a staking contract using 16 | /// Sablier NFT. This template should not be used in a production environment. It makes specific assumptions that may 17 | /// not apply to your particular needs. 18 | /// 19 | /// @dev This template allows users to stake Sablier NFTs and earn staking rewards based on the total amount available 20 | /// in the stream. The implementation is inspired by the Synthetix staking contract: 21 | /// https://github.com/Synthetixio/synthetix/blob/develop/contracts/StakingRewards.sol. 22 | /// 23 | /// Assumptions: 24 | /// - The Sablier NFT must be transferable because staking requires transferring the NFT to the staking contract. 25 | /// - This staking contract assumes that one user can only stake one NFT at a time. 26 | contract StakeSablierNFT is Adminable, ERC721Holder, ISablierLockupRecipient { 27 | using SafeERC20 for IERC20; 28 | 29 | /*////////////////////////////////////////////////////////////////////////// 30 | ERRORS 31 | //////////////////////////////////////////////////////////////////////////*/ 32 | 33 | error AlreadyStaking(address account, uint256 streamId); 34 | error DifferentStreamingToken(uint256 streamId, IERC20 rewardToken); 35 | error ProvidedRewardTooHigh(); 36 | error StakingAlreadyActive(); 37 | error UnauthorizedCaller(address account, uint256 streamId); 38 | error ZeroAddress(address account); 39 | error ZeroAmount(); 40 | error ZeroDuration(); 41 | 42 | /*////////////////////////////////////////////////////////////////////////// 43 | EVENTS 44 | //////////////////////////////////////////////////////////////////////////*/ 45 | 46 | event RewardAdded(uint256 reward); 47 | event RewardDurationUpdated(uint256 newDuration); 48 | event RewardPaid(address indexed user, uint256 reward); 49 | event Staked(address indexed user, uint256 streamId); 50 | event Unstaked(address indexed user, uint256 streamId); 51 | 52 | /*////////////////////////////////////////////////////////////////////////// 53 | USER-FACING STATE 54 | //////////////////////////////////////////////////////////////////////////*/ 55 | 56 | /// @dev The last time when rewards were updated. 57 | uint256 public lastUpdateTime; 58 | 59 | /// @dev This should be your own ERC-20 token in which the staking rewards will be distributed. 60 | IERC20 public rewardERC20Token; 61 | 62 | /// @dev Total rewards to be distributed per second. 63 | uint256 public rewardRate; 64 | 65 | /// @dev Earned rewards for each account. 66 | mapping(address account => uint256 earned) public rewards; 67 | 68 | /// @dev Duration for which staking is live. 69 | uint256 public rewardsDuration; 70 | 71 | /// @dev This should be the Sablier Lockup contract. 72 | ISablierLockup public sablierLockup; 73 | 74 | /// @dev The staked stream IDs mapped by user addresses. 75 | mapping(address account => uint256 streamId) public stakedStreams; 76 | 77 | /// @dev The owners of the streams mapped by stream IDs. 78 | mapping(uint256 streamId => address account) public stakedUsers; 79 | 80 | /// @dev The timestamp when the staking ends. 81 | uint256 public stakingEndTime; 82 | 83 | /// @dev The total amount of ERC-20 tokens staked through Sablier NFTs. 84 | uint256 public totalERC20StakedSupply; 85 | 86 | /// @dev Keeps track of the total rewards distributed divided by total staked supply. 87 | uint256 public totalRewardPaidPerERC20Token; 88 | 89 | /// @dev The rewards paid to each account per ERC-20 token mapped by the account. 90 | mapping(address account => uint256 reward) public userRewardPerERC20Token; 91 | 92 | /*////////////////////////////////////////////////////////////////////////// 93 | MODIFIERS 94 | //////////////////////////////////////////////////////////////////////////*/ 95 | 96 | /// @notice Modifier used to keep track of the earned rewards for user each time a `stake`, `unstake` or 97 | /// `claimRewards` is called. 98 | modifier updateReward(address account) { 99 | totalRewardPaidPerERC20Token = rewardPaidPerERC20Token(); 100 | lastUpdateTime = lastTimeRewardsApplicable(); 101 | rewards[account] = calculateUserRewards(account); 102 | userRewardPerERC20Token[account] = totalRewardPaidPerERC20Token; 103 | _; 104 | } 105 | 106 | /*////////////////////////////////////////////////////////////////////////// 107 | CONSTRUCTOR 108 | //////////////////////////////////////////////////////////////////////////*/ 109 | 110 | /// @param initialAdmin The address of the initial contract admin. 111 | /// @param rewardERC20Token_ The address of the ERC-20 token used for rewards. 112 | /// @param sablierLockup_ The address of the ERC-721 Contract. 113 | constructor( 114 | address initialAdmin, 115 | IERC20 rewardERC20Token_, 116 | ISablierLockup sablierLockup_ 117 | ) 118 | Adminable(initialAdmin) 119 | { 120 | rewardERC20Token = rewardERC20Token_; 121 | sablierLockup = sablierLockup_; 122 | } 123 | 124 | /*////////////////////////////////////////////////////////////////////////// 125 | USER-FACING CONSTANT FUNCTIONS 126 | //////////////////////////////////////////////////////////////////////////*/ 127 | 128 | /// @notice Calculate the earned rewards for an account. 129 | /// @param account The address of the account to calculate available rewards for. 130 | /// @return earned The amount available as rewards for the account. 131 | function calculateUserRewards(address account) public view returns (uint256 earned) { 132 | if (stakedStreams[account] == 0) { 133 | return rewards[account]; 134 | } 135 | 136 | uint256 amountInStream = _getAmountInStream(stakedStreams[account]); 137 | uint256 userRewardPerERC20Token_ = userRewardPerERC20Token[account]; 138 | uint256 rewardsSinceLastTime = (amountInStream * (rewardPaidPerERC20Token() - userRewardPerERC20Token_)) / 1e18; 139 | 140 | return rewardsSinceLastTime + rewards[account]; 141 | } 142 | 143 | /// @notice Get the last time when rewards were applicable 144 | function lastTimeRewardsApplicable() public view returns (uint256) { 145 | return block.timestamp < stakingEndTime ? block.timestamp : stakingEndTime; 146 | } 147 | 148 | /// @notice Calculates the total rewards distributed per ERC-20 token. 149 | /// @dev This is called by `updateReward`, which also updates the value of `totalRewardPaidPerERC20Token`. 150 | function rewardPaidPerERC20Token() public view returns (uint256) { 151 | // If the total staked supply is zero or staking has ended, return the stored value of reward per ERC-20. 152 | if (totalERC20StakedSupply == 0 || block.timestamp >= stakingEndTime) { 153 | return totalRewardPaidPerERC20Token; 154 | } 155 | 156 | uint256 totalRewardsPerERC20InCurrentPeriod = 157 | ((lastTimeRewardsApplicable() - lastUpdateTime) * rewardRate * 1e18) / totalERC20StakedSupply; 158 | 159 | return totalRewardPaidPerERC20Token + totalRewardsPerERC20InCurrentPeriod; 160 | } 161 | 162 | // {IERC165-supportsInterface} implementation as required by `ISablierLockupRecipient` interface. 163 | function supportsInterface(bytes4 interfaceId) public pure override(IERC165) returns (bool) { 164 | return interfaceId == 0xf8ee98d3; 165 | } 166 | 167 | /*////////////////////////////////////////////////////////////////////////// 168 | USER-FACING NON-CONSTANT FUNCTIONS 169 | //////////////////////////////////////////////////////////////////////////*/ 170 | 171 | /// @notice Function called by the user to claim his accumulated rewards. 172 | function claimRewards() public updateReward(msg.sender) { 173 | uint256 reward = rewards[msg.sender]; 174 | if (reward > 0) { 175 | delete rewards[msg.sender]; 176 | 177 | rewardERC20Token.safeTransfer(msg.sender, reward); 178 | 179 | emit RewardPaid(msg.sender, reward); 180 | } 181 | } 182 | 183 | /// @notice Implements the hook to handle cancelation events. This will be called by Sablier contract when a stream 184 | /// is canceled by the sender. 185 | /// @dev This function subtracts the amount refunded to the sender from `totalERC20StakedSupply`. 186 | /// - This function also updates the rewards for the staker. 187 | function onSablierLockupCancel( 188 | uint256 streamId, 189 | address, /* sender */ 190 | uint128 senderAmount, 191 | uint128 /* recipientAmount */ 192 | ) 193 | external 194 | updateReward(stakedUsers[streamId]) 195 | returns (bytes4 selector) 196 | { 197 | // Check: the caller is the Lockup contract. 198 | if (msg.sender != address(sablierLockup)) { 199 | revert UnauthorizedCaller(msg.sender, streamId); 200 | } 201 | 202 | // Effect: update the total staked amount. 203 | totalERC20StakedSupply -= senderAmount; 204 | 205 | return ISablierLockupRecipient.onSablierLockupCancel.selector; 206 | } 207 | 208 | /// @notice Implements the hook to handle withdraw events. This will be called by Sablier contract when withdraw is 209 | /// called on a stream. 210 | /// @dev This function transfers `amount` to the original staker. 211 | function onSablierLockupWithdraw( 212 | uint256 streamId, 213 | address, /* caller */ 214 | address, /* recipient */ 215 | uint128 amount 216 | ) 217 | external 218 | updateReward(stakedUsers[streamId]) 219 | returns (bytes4 selector) 220 | { 221 | // Check: the caller is the Lockup contract 222 | if (msg.sender != address(sablierLockup)) { 223 | revert UnauthorizedCaller(msg.sender, streamId); 224 | } 225 | 226 | address staker = stakedUsers[streamId]; 227 | 228 | // Check: the staker is not the zero address. 229 | if (staker == address(0)) { 230 | revert ZeroAddress(staker); 231 | } 232 | 233 | // Effect: update the total staked amount. 234 | totalERC20StakedSupply -= amount; 235 | 236 | // Interaction: transfer the withdrawn amount to the original staker. 237 | rewardERC20Token.safeTransfer(staker, amount); 238 | 239 | return ISablierLockupRecipient.onSablierLockupWithdraw.selector; 240 | } 241 | 242 | /// @notice Stake a Sablier NFT with specified base token. 243 | /// @dev The `msg.sender` must approve the staking contract to spend the Sablier NFT before calling this function. 244 | /// One user can only stake one NFT at a time. 245 | /// @param streamId The stream ID of the Sablier NFT to be staked. 246 | function stake(uint256 streamId) external updateReward(msg.sender) { 247 | // Check: the Sablier NFT is streaming the staking token. 248 | if (sablierLockup.getUnderlyingToken(streamId) != rewardERC20Token) { 249 | revert DifferentStreamingToken(streamId, rewardERC20Token); 250 | } 251 | 252 | // Check: the user is not already staking. 253 | if (stakedStreams[msg.sender] != 0) { 254 | revert AlreadyStaking(msg.sender, stakedStreams[msg.sender]); 255 | } 256 | 257 | // Effect: store the owner of the Sablier NFT. 258 | stakedUsers[streamId] = msg.sender; 259 | 260 | // Effect: store the stream ID. 261 | stakedStreams[msg.sender] = streamId; 262 | 263 | // Effect: update the total staked amount. 264 | totalERC20StakedSupply += _getAmountInStream(streamId); 265 | 266 | // Interaction: transfer NFT to the staking contract. 267 | sablierLockup.safeTransferFrom({ from: msg.sender, to: address(this), tokenId: streamId }); 268 | 269 | emit Staked(msg.sender, streamId); 270 | } 271 | 272 | /// @notice Unstaking a Sablier NFT will transfer the NFT back to the `msg.sender`. 273 | /// @param streamId The stream ID of the Sablier NFT to be unstaked. 274 | function unstake(uint256 streamId) public updateReward(msg.sender) { 275 | // Check: the caller is the stored owner of the NFT. 276 | if (stakedUsers[streamId] != msg.sender) { 277 | revert UnauthorizedCaller(msg.sender, streamId); 278 | } 279 | 280 | // Effect: update the total staked amount. 281 | totalERC20StakedSupply -= _getAmountInStream(streamId); 282 | 283 | _unstake({ streamId: streamId, account: msg.sender }); 284 | } 285 | 286 | /*////////////////////////////////////////////////////////////////////////// 287 | PRIVATE FUNCTIONS 288 | //////////////////////////////////////////////////////////////////////////*/ 289 | 290 | /// @notice Determine the amount available in the stream. 291 | /// @dev The following function determines the amounts of tokens in a stream irrespective of its cancelable status. 292 | function _getAmountInStream(uint256 streamId) private view returns (uint256 amount) { 293 | // The tokens in the stream = amount deposited - amount withdrawn - amount refunded. 294 | return sablierLockup.getDepositedAmount(streamId) - sablierLockup.getWithdrawnAmount(streamId) 295 | - sablierLockup.getRefundedAmount(streamId); 296 | } 297 | 298 | function _unstake(uint256 streamId, address account) private { 299 | // Check: account is not zero. 300 | if (account == address(0)) { 301 | revert ZeroAddress(account); 302 | } 303 | 304 | // Effect: delete the owner of the staked stream. 305 | delete stakedUsers[streamId]; 306 | 307 | // Effect: delete the Sablier NFT. 308 | delete stakedStreams[account]; 309 | 310 | // Interaction: transfer stream back to user. 311 | sablierLockup.safeTransferFrom({ from: address(this), to: account, tokenId: streamId }); 312 | 313 | emit Unstaked(account, streamId); 314 | } 315 | 316 | /*////////////////////////////////////////////////////////////////////////// 317 | ADMIN FUNCTIONS 318 | //////////////////////////////////////////////////////////////////////////*/ 319 | 320 | /// @notice Start a Staking period and set the amount of ERC-20 tokens to be distributed as rewards in said period. 321 | /// @dev The Staking Contract have to already own enough Rewards Tokens to distribute all the rewards, so make sure 322 | /// to send all the tokens to the contract before calling this function. 323 | /// @param rewardAmount The amount of Reward Tokens to be distributed. 324 | /// @param newDuration The duration in which the rewards will be distributed. 325 | function startStakingPeriod(uint256 rewardAmount, uint256 newDuration) external onlyAdmin { 326 | // Check: the amount is not zero 327 | if (rewardAmount == 0) { 328 | revert ZeroAmount(); 329 | } 330 | 331 | // Check: the duration is not zero. 332 | if (newDuration == 0) { 333 | revert ZeroDuration(); 334 | } 335 | 336 | // Check: the staking period is not already active. 337 | if (block.timestamp <= stakingEndTime) { 338 | revert StakingAlreadyActive(); 339 | } 340 | 341 | // Effect: update the rewards duration. 342 | rewardsDuration = newDuration; 343 | 344 | // Effect: update the reward rate. 345 | rewardRate = rewardAmount / rewardsDuration; 346 | 347 | // Check: the contract has enough tokens to distribute as rewards. 348 | uint256 balance = rewardERC20Token.balanceOf(address(this)); 349 | if (rewardRate > balance / rewardsDuration) { 350 | revert ProvidedRewardTooHigh(); 351 | } 352 | 353 | // Effect: update the `lastUpdateTime`. 354 | lastUpdateTime = block.timestamp; 355 | 356 | // Effect: update the `stakingEndTime`. 357 | stakingEndTime = block.timestamp + rewardsDuration; 358 | 359 | emit RewardAdded(rewardAmount); 360 | 361 | emit RewardDurationUpdated(rewardsDuration); 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /lockup/StreamManagement.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; 5 | 6 | /// @notice Examples of how to manage Sablier streams after they have been created. 7 | /// @dev This code is referenced in the docs: https://docs.sablier.com/guides/lockup/examples/stream-management/setup 8 | contract StreamManagement { 9 | ISablierLockup public immutable sablier; 10 | 11 | constructor(ISablierLockup sablier_) { 12 | sablier = sablier_; 13 | } 14 | 15 | /*////////////////////////////////////////////////////////////////////////// 16 | 02-WITHDRAW 17 | //////////////////////////////////////////////////////////////////////////*/ 18 | 19 | // This function can be called by the sender, recipient, or an approved NFT operator 20 | function withdraw(uint256 streamId) external { 21 | sablier.withdraw({ streamId: streamId, to: address(0xcafe), amount: 1337e18 }); 22 | } 23 | 24 | // This function can be called by the sender, recipient, or an approved NFT operator 25 | function withdrawMax(uint256 streamId) external { 26 | sablier.withdrawMax({ streamId: streamId, to: address(0xcafe) }); 27 | } 28 | 29 | // This function can be called by either the recipient or an approved NFT operator 30 | function withdrawMultiple(uint256[] calldata streamIds, uint128[] calldata amounts) external { 31 | sablier.withdrawMultiple({ streamIds: streamIds, amounts: amounts }); 32 | } 33 | 34 | /*////////////////////////////////////////////////////////////////////////// 35 | 03-CANCEL 36 | //////////////////////////////////////////////////////////////////////////*/ 37 | 38 | // This function can be called by either the sender or the recipient 39 | function cancel(uint256 streamId) external { 40 | sablier.cancel(streamId); 41 | } 42 | 43 | // This function can be called only by the sender 44 | function cancelMultiple(uint256[] calldata streamIds) external { 45 | sablier.cancelMultiple(streamIds); 46 | } 47 | 48 | /*////////////////////////////////////////////////////////////////////////// 49 | 04-RENOUNCE 50 | //////////////////////////////////////////////////////////////////////////*/ 51 | 52 | // This function can be called only by the sender 53 | function renounce(uint256 streamId) external { 54 | sablier.renounce(streamId); 55 | } 56 | 57 | /*////////////////////////////////////////////////////////////////////////// 58 | 05-TRANSFER 59 | //////////////////////////////////////////////////////////////////////////*/ 60 | 61 | // This function can be called by either the recipient or an approved NFT operator 62 | function safeTransferFrom(uint256 streamId) external { 63 | sablier.safeTransferFrom({ from: address(this), to: address(0xcafe), tokenId: streamId }); 64 | } 65 | 66 | // This function can be called by either the recipient or an approved NFT operator 67 | function transferFrom(uint256 streamId) external { 68 | sablier.transferFrom({ from: address(this), to: address(0xcafe), tokenId: streamId }); 69 | } 70 | 71 | // This function can be called only by the recipient 72 | function withdrawMaxAndTransfer(uint256 streamId) external { 73 | sablier.withdrawMaxAndTransfer({ streamId: streamId, newRecipient: address(0xcafe) }); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lockup/StreamManagementWithHook.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 6 | import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; 7 | import { ud60x18 } from "@prb/math/src/UD60x18.sol"; 8 | import { ISablierLockupRecipient } from "@sablier/lockup/src/interfaces/ISablierLockupRecipient.sol"; 9 | import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; 10 | import { Broker, Lockup, LockupLinear } from "@sablier/lockup/src/types/DataTypes.sol"; 11 | 12 | /// @notice Example of creating Sablier streams and managing them on behalf of users with some withdrawal restrictions 13 | /// powered by Sablier hooks. 14 | /// @dev To read more about the hooks, visit https://docs.sablier.com/concepts/protocol/hooks. 15 | contract StreamManagementWithHook is ISablierLockupRecipient { 16 | using SafeERC20 for IERC20; 17 | 18 | error CallerNotSablierContract(address caller, address sablierLockup); 19 | error CallerNotThisContract(); 20 | error Unauthorized(); 21 | 22 | ISablierLockup public immutable SABLIER; 23 | IERC20 public immutable TOKEN; 24 | 25 | /// @dev Stream IDs mapped to their beneficiaries. 26 | mapping(uint256 streamId => address beneficiary) public streamBeneficiaries; 27 | 28 | /// @dev This modifier will restrict the function to be called only by the stream beneficiary. 29 | modifier onlyStreamBeneficiary(uint256 streamId) { 30 | if (msg.sender != streamBeneficiaries[streamId]) { 31 | revert Unauthorized(); 32 | } 33 | 34 | _; 35 | } 36 | 37 | /// @dev Constructor will set the address of the lockup contract and ERC20 token. 38 | constructor(ISablierLockup sablier_, IERC20 token_) { 39 | SABLIER = sablier_; 40 | TOKEN = token_; 41 | } 42 | /*////////////////////////////////////////////////////////////////////////// 43 | CREATE 44 | //////////////////////////////////////////////////////////////////////////*/ 45 | 46 | /// @notice Creates a non-cancelable, non-transferable stream on behalf of `beneficiary`. 47 | /// @dev The stream recipient is set to `this` contract to have control over "withdraw" from streams. Actual 48 | /// recipient is managed via `streamBeneficiaries` mapping. 49 | /// @param beneficiary The ultimate recipient of the stream's token. 50 | /// @param totalAmount The total amount of tokens to be streamed. 51 | /// @return streamId The stream Id. 52 | function create(address beneficiary, uint128 totalAmount) external returns (uint256 streamId) { 53 | // Check: verify that this contract is allowed to hook into Sablier Lockup. 54 | if (!SABLIER.isAllowedToHook(address(this))) { 55 | revert Unauthorized(); 56 | } 57 | 58 | // Transfer tokens to this contract and approve Sablier to spend them. 59 | TOKEN.transferFrom(msg.sender, address(this), totalAmount); 60 | TOKEN.approve(address(SABLIER), totalAmount); 61 | 62 | Lockup.CreateWithDurations memory params; 63 | params.transferable = false; 64 | params.cancelable = true; 65 | // Set `this` as the recipient of the Stream. Only `this` will be able to call the "withdraw" function. 66 | params.recipient = address(this); 67 | // Set `this` as the sender of the Stream. Only `this` will be able to call the "cancel" function 68 | params.sender = address(this); 69 | params.totalAmount = totalAmount; 70 | params.token = TOKEN; 71 | params.broker = Broker(address(0), ud60x18(0)); // No broker fee 72 | 73 | LockupLinear.UnlockAmounts memory unlockAmounts = LockupLinear.UnlockAmounts({ start: 0, cliff: 0 }); 74 | LockupLinear.Durations memory durations = LockupLinear.Durations({ 75 | cliff: 0, // Setting a cliff of 0 76 | total: 52 weeks // Setting a total duration of ~1 year 77 | }); 78 | 79 | // Create the stream. 80 | streamId = SABLIER.createWithDurationsLL(params, unlockAmounts, durations); 81 | 82 | // Set the `beneficiary` . 83 | streamBeneficiaries[streamId] = beneficiary; 84 | } 85 | 86 | /*////////////////////////////////////////////////////////////////////////// 87 | WITHDRAW 88 | //////////////////////////////////////////////////////////////////////////*/ 89 | 90 | /// @dev This function can only be called by the stream beneficiary. 91 | function withdraw(uint256 streamId, uint128 amount) external onlyStreamBeneficiary(streamId) { 92 | // Withdraw the specified amount from the stream to the stream beneficiary. 93 | SABLIER.withdraw({ streamId: streamId, to: streamBeneficiaries[streamId], amount: amount }); 94 | } 95 | 96 | /// @dev This function can only be called by the stream beneficiary. 97 | function withdrawMax(uint256 streamId) external onlyStreamBeneficiary(streamId) { 98 | // Withdraw the maximum amount from the stream to the stream beneficiary. 99 | SABLIER.withdrawMax({ streamId: streamId, to: streamBeneficiaries[streamId] }); 100 | } 101 | 102 | /*////////////////////////////////////////////////////////////////////////// 103 | HOOKS 104 | //////////////////////////////////////////////////////////////////////////*/ 105 | 106 | // {IERC165-supportsInterface} implementation as required by `ISablierLockupRecipient` interface. 107 | function supportsInterface(bytes4 interfaceId) public pure override(IERC165) returns (bool) { 108 | return interfaceId == 0xf8ee98d3; 109 | } 110 | 111 | /// @notice This will be called by `SABLIER` contract everytime withdraw is called on a stream. 112 | /// @dev Reverts if the `msg.sender` is not `this` contract, preventing anyone else from calling the publicly 113 | /// callable "withdraw" function. 114 | function onSablierLockupWithdraw( 115 | uint256, /* streamId */ 116 | address caller, 117 | address, /* to */ 118 | uint128 /* amount */ 119 | ) 120 | external 121 | view 122 | returns (bytes4 selector) 123 | { 124 | // Check: the `msg.sender` is the lockup contract. 125 | if (msg.sender != address(SABLIER)) { 126 | revert CallerNotSablierContract(msg.sender, address(SABLIER)); 127 | } 128 | 129 | // Check: the `msg.sender` to the `SABLIER` contract is `this` contract. 130 | if (caller != address(this)) { 131 | revert CallerNotThisContract(); 132 | } 133 | 134 | return ISablierLockupRecipient.onSablierLockupWithdraw.selector; 135 | } 136 | 137 | /// @notice This will be called by `SABLIER` contract when cancel is called on a stream. 138 | /// @dev Since only the stream sender, which is `this` contract, can cancel the stream, this function does not 139 | /// require a check similar to `onSablierLockupWithdraw`. 140 | function onSablierLockupCancel( 141 | uint256, /* streamId */ 142 | address, /* sender */ 143 | uint128, /* senderAmount */ 144 | uint128 /* recipientAmount */ 145 | ) 146 | external 147 | view 148 | returns (bytes4 selector) 149 | { 150 | // Check: the `msg.sender` is the lockup contract. 151 | if (msg.sender != address(SABLIER)) { 152 | revert CallerNotSablierContract(msg.sender, address(SABLIER)); 153 | } 154 | 155 | return ISablierLockupRecipient.onSablierLockupCancel.selector; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /lockup/StreamManagementWithHook.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; 6 | import { ILockupNFTDescriptor } from "@sablier/lockup/src/interfaces/ILockupNFTDescriptor.sol"; 7 | import { SablierLockup } from "@sablier/lockup/src/SablierLockup.sol"; 8 | 9 | import { Test } from "forge-std/src/Test.sol"; 10 | import { StreamManagementWithHook } from "./StreamManagementWithHook.sol"; 11 | 12 | contract MockERC20 is ERC20 { 13 | constructor(address to) ERC20("MockERC20", "MockERC20") { 14 | _mint(to, 1_000_000e18); 15 | } 16 | } 17 | 18 | contract StreamManagementWithHookTest is Test { 19 | StreamManagementWithHook internal streamManager; 20 | ISablierLockup internal sablierLockup; 21 | 22 | ERC20 internal token; 23 | uint128 internal amount = 10e18; 24 | uint256 internal defaultStreamId; 25 | 26 | address internal alice; 27 | address internal bob; 28 | address internal sablierAdmin; 29 | 30 | function setUp() public { 31 | vm.createSelectFork("mainnet"); 32 | 33 | // Create a test users 34 | alice = makeAddr("Alice"); 35 | bob = makeAddr("Bob"); 36 | sablierAdmin = payable(makeAddr("SablierAdmin")); 37 | 38 | // Create a mock ERC20 token and send 1M tokens to Bob 39 | token = new MockERC20(bob); 40 | 41 | // Deploy Sablier Lockup Linear contract 42 | sablierLockup = new SablierLockup( 43 | sablierAdmin, 44 | ILockupNFTDescriptor(address(0)), // Irrelevant for test purposes 45 | 500 // the MAX_COUNT 46 | ); 47 | 48 | // Deploy StreamManagementWithHook contract 49 | streamManager = new StreamManagementWithHook(sablierLockup, token); 50 | 51 | // Whitelist the contract to be able to hook into Sablier Lockup contract 52 | vm.startPrank(sablierAdmin); 53 | sablierLockup.allowToHook(address(streamManager)); 54 | vm.stopPrank(); 55 | 56 | // Approve streamManager to spend MockERC20 on behalf of Bob 57 | vm.startPrank(bob); 58 | token.approve(address(streamManager), type(uint128).max); 59 | } 60 | 61 | // Test creating a stream from Bob (Stream Manager Owner) to Alice (Beneficiary) 62 | function test_Create() public { 63 | // Create a stream with Alice as the beneficiary 64 | uint256 streamId = streamManager.create({ beneficiary: alice, totalAmount: amount }); 65 | 66 | // Check streamId 67 | assertEq(streamId, 1); 68 | 69 | // Check balances 70 | assertEq(token.balanceOf(alice), 0); 71 | assertEq(token.balanceOf(bob), 1_000_000e18 - amount); 72 | assertEq(token.balanceOf(address(streamManager.SABLIER())), amount); 73 | 74 | // Check stream details are correct 75 | assertEq(address(sablierLockup.getUnderlyingToken(streamId)), address(token)); 76 | assertEq(sablierLockup.getRecipient(streamId), address(streamManager)); 77 | assertEq(sablierLockup.getDepositedAmount(streamId), amount); 78 | assertEq(sablierLockup.isCancelable(streamId), true); 79 | assertEq(sablierLockup.isTransferable(streamId), false); 80 | 81 | // Check streamManager details are correct 82 | assertEq(streamManager.streamBeneficiaries(streamId), alice); 83 | } 84 | 85 | modifier givenStreamsCreated() { 86 | // Create a stream with Alice as the beneficiary 87 | defaultStreamId = streamManager.create({ beneficiary: alice, totalAmount: amount }); 88 | require(defaultStreamId == 1, "Stream creation failed"); 89 | _; 90 | } 91 | 92 | // Test that withdraw from Sablier stream reverts if it is directly called on the Sablier Lockup contract 93 | function test_Withdraw_RevertWhen_CallerNotStreamManager() public givenStreamsCreated { 94 | // Warp time to exceed total duration 95 | vm.warp({ newTimestamp: block.timestamp + 60 weeks }); 96 | 97 | // Prank Alice to be the `msg.sender`. 98 | vm.startPrank(alice); 99 | 100 | // Since Alice is the `msg.sender`, `withdraw` to Sablier stream should revert due to hook restriction 101 | vm.expectRevert(abi.encodeWithSelector(StreamManagementWithHook.CallerNotThisContract.selector)); 102 | sablierLockup.withdraw(defaultStreamId, address(streamManager), 1e18); 103 | } 104 | 105 | // Test that withdraw from Sablier stream succeeds if it is called through the `streamManager` contract 106 | function test_Withdraw() public givenStreamsCreated { 107 | // Advance time enough to make cliff period over and the total duration to be over 108 | vm.warp({ newTimestamp: block.timestamp + 60 weeks }); 109 | 110 | // Prank Alice to be the `msg.sender` 111 | vm.startPrank(alice); 112 | 113 | // Alice can withdraw from the streamManager contract 114 | streamManager.withdraw(defaultStreamId, 1e18); 115 | 116 | assertEq(token.balanceOf(alice), 1e18); 117 | 118 | // Withdraw max tokens from the stream 119 | streamManager.withdrawMax(defaultStreamId); 120 | 121 | assertEq(token.balanceOf(alice), 10e18); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /lockup/tests/stake-sablier-nft-test/StakeSablierNFT.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.19; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import { ud60x18 } from "@prb/math/src/UD60x18.sol"; 6 | import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; 7 | import { Broker, Lockup, LockupLinear } from "@sablier/lockup/src/types/DataTypes.sol"; 8 | import { Test } from "forge-std/src/Test.sol"; 9 | 10 | import { StakeSablierNFT } from "../../StakeSablierNFT.sol"; 11 | 12 | struct StreamOwner { 13 | address addr; 14 | uint256 streamId; 15 | } 16 | 17 | struct Users { 18 | // Creator of the NFT staking contract. 19 | address admin; 20 | // Alice has already staked her NFT. 21 | StreamOwner alice; 22 | // Bob is unauthorized to stake. 23 | StreamOwner bob; 24 | // Joe wants to stake his NFT. 25 | StreamOwner joe; 26 | } 27 | 28 | abstract contract StakeSablierNFT_Fork_Test is Test { 29 | // Errors 30 | error AlreadyStaking(address account, uint256 streamId); 31 | error DifferentStreamingToken(uint256 streamId, IERC20 rewardToken); 32 | error ProvidedRewardTooHigh(); 33 | error StakingAlreadyActive(); 34 | error UnauthorizedCaller(address account, uint256 streamId); 35 | error ZeroAddress(address account); 36 | error ZeroAmount(); 37 | error ZeroDuration(); 38 | 39 | // Events 40 | event RewardAdded(uint256 reward); 41 | event RewardDurationUpdated(uint256 newDuration); 42 | event RewardPaid(address indexed user, uint256 reward); 43 | event Staked(address indexed user, uint256 streamId); 44 | event Unstaked(address indexed user, uint256 streamId); 45 | 46 | IERC20 public constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); 47 | IERC20 public constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); 48 | 49 | // Get the latest deployment address from the docs: https://docs.sablier.com/guides/lockup/deployments. 50 | ISablierLockup internal constant SABLIER = ISablierLockup(0x7C01AA3783577E15fD7e272443D44B92d5b21056); 51 | 52 | // Set a stream ID to stake. 53 | uint256 internal stakingStreamId = 2; 54 | 55 | // Reward rate based on the total amount staked. 56 | uint256 internal rewardRate; 57 | 58 | // Token used for creating streams as well as to distribute rewards. 59 | IERC20 internal rewardToken = DAI; 60 | 61 | StakeSablierNFT internal stakingContract; 62 | 63 | uint256 internal constant AMOUNT_IN_STREAM = 1000e18; 64 | 65 | Users internal users; 66 | 67 | function setUp() public { 68 | // Fork Ethereum Mainnet 69 | vm.createSelectFork("mainnet"); 70 | 71 | // Create users. 72 | users.admin = makeAddr("admin"); 73 | users.alice.addr = makeAddr("alice"); 74 | users.bob.addr = makeAddr("bob"); 75 | users.joe.addr = makeAddr("joe"); 76 | 77 | // Mint some reward tokens to the admin address which will be used to deposit to the staking contract. 78 | deal({ token: address(rewardToken), to: users.admin, give: 10_000e18 }); 79 | 80 | // Make the admin the `msg.sender` in all following calls. 81 | vm.startPrank({ msgSender: users.admin }); 82 | 83 | // Deploy the staking contract. 84 | stakingContract = 85 | new StakeSablierNFT({ initialAdmin: users.admin, rewardERC20Token_: rewardToken, sablierLockup_: SABLIER }); 86 | 87 | // Set expected reward rate. 88 | rewardRate = 10_000e18 / uint256(1 weeks); 89 | 90 | // Fund the staking contract with some reward tokens. 91 | rewardToken.transfer(address(stakingContract), 10_000e18); 92 | 93 | // Start the staking period. 94 | stakingContract.startStakingPeriod(10_000e18, 1 weeks); 95 | 96 | // Stake some streams. 97 | _createAndStakeStreamBy({ recipient: users.alice, token: DAI, stake: true }); 98 | _createAndStakeStreamBy({ recipient: users.bob, token: USDC, stake: false }); 99 | _createAndStakeStreamBy({ recipient: users.joe, token: DAI, stake: false }); 100 | 101 | // Make the stream owner the `msg.sender` in all the subsequent calls. 102 | resetPrank({ msgSender: users.joe.addr }); 103 | 104 | // Approve the staking contract to spend the NFT. 105 | SABLIER.setApprovalForAll(address(stakingContract), true); 106 | } 107 | 108 | /// @dev Stops the active prank and sets a new one. 109 | function resetPrank(address msgSender) internal { 110 | vm.stopPrank(); 111 | vm.startPrank(msgSender); 112 | } 113 | 114 | function _createLockupLinearStreams(address recipient, IERC20 token) private returns (uint256 streamId) { 115 | deal({ token: address(token), to: users.admin, give: AMOUNT_IN_STREAM }); 116 | 117 | resetPrank({ msgSender: users.admin }); 118 | 119 | token.approve(address(SABLIER), type(uint256).max); 120 | 121 | // Declare the params struct 122 | Lockup.CreateWithDurations memory params; 123 | 124 | // Declare the function parameters 125 | params.sender = users.admin; // The sender will be able to cancel the stream 126 | params.recipient = recipient; // The recipient of the streamed tokens 127 | params.totalAmount = uint128(AMOUNT_IN_STREAM); // Total amount is the amount inclusive of all fees 128 | params.token = token; // The streaming token 129 | params.cancelable = true; // Whether the stream will be cancelable or not 130 | params.transferable = true; // Whether the stream will be transferable or not 131 | params.broker = Broker(address(0), ud60x18(0)); // Optional parameter for charging a fee 132 | 133 | LockupLinear.UnlockAmounts memory unlockAmounts = LockupLinear.UnlockAmounts({ start: 0, cliff: 0 }); 134 | LockupLinear.Durations memory durations = LockupLinear.Durations({ 135 | cliff: 0, // Setting a cliff of 0 136 | total: 52 weeks // Setting a total duration of ~1 year 137 | }); 138 | 139 | // Create the Sablier stream using a function that sets the start time to `block.timestamp` 140 | streamId = SABLIER.createWithDurationsLL(params, unlockAmounts, durations); 141 | } 142 | 143 | function _createAndStakeStreamBy(StreamOwner storage recipient, IERC20 token, bool stake) private { 144 | resetPrank({ msgSender: users.admin }); 145 | 146 | uint256 streamId = _createLockupLinearStreams(recipient.addr, token); 147 | recipient.streamId = streamId; 148 | 149 | // Make the stream owner the `msg.sender` in all the subsequent calls. 150 | resetPrank({ msgSender: recipient.addr }); 151 | 152 | // Approve the staking contract to spend the NFT. 153 | SABLIER.setApprovalForAll(address(stakingContract), true); 154 | 155 | // Stake a few NFTs to simulate the actual staking behavior. 156 | if (stake) { 157 | stakingContract.stake(streamId); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /lockup/tests/stake-sablier-nft-test/claim-rewards/claimRewards.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.19; 3 | 4 | import { StakeSablierNFT_Fork_Test } from "../StakeSablierNFT.t.sol"; 5 | 6 | contract ClaimRewards_Test is StakeSablierNFT_Fork_Test { 7 | function test_WhenCallerIsNotStaker() external { 8 | // Change the caller to a staker. 9 | resetPrank({ msgSender: users.joe.addr }); 10 | 11 | // Expect no transfer. 12 | vm.expectCall({ 13 | callee: address(rewardToken), 14 | data: abi.encodeCall(rewardToken.transfer, (users.joe.addr, 0)), 15 | count: 0 16 | }); 17 | 18 | // Claim rewards. 19 | stakingContract.claimRewards(); 20 | } 21 | 22 | function test_WhenCallerIsStaker() external { 23 | // Prank the caller to a staker. 24 | resetPrank({ msgSender: users.alice.addr }); 25 | 26 | vm.warp(block.timestamp + 1 days); 27 | 28 | uint256 expectedReward = 1 days * rewardRate; 29 | uint256 initialBalance = rewardToken.balanceOf(users.alice.addr); 30 | 31 | // Claim the rewards. 32 | stakingContract.claimRewards(); 33 | 34 | // Assert balance increased by the expected reward. 35 | uint256 finalBalance = rewardToken.balanceOf(users.alice.addr); 36 | assertApproxEqAbs(finalBalance - initialBalance, expectedReward, 0.0001e18); 37 | 38 | // Assert rewards has been set to 0. 39 | assertEq(stakingContract.rewards(users.alice.addr), 0); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lockup/tests/stake-sablier-nft-test/claim-rewards/claimRewards.tree: -------------------------------------------------------------------------------- 1 | ClaimRewards_Test 2 | ├── when caller is not staker 3 | │ └── it should not transfer the rewards 4 | └── when caller is staker 5 | └── it should transfer the rewards 6 | -------------------------------------------------------------------------------- /lockup/tests/stake-sablier-nft-test/stake/stake.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.19; 3 | 4 | import { StakeSablierNFT_Fork_Test } from "../StakeSablierNFT.t.sol"; 5 | 6 | contract Stake_Test is StakeSablierNFT_Fork_Test { 7 | function test_RevertGiven_StreamingTokenIsNotRewardToken() external { 8 | resetPrank({ msgSender: users.bob.addr }); 9 | 10 | vm.expectRevert(abi.encodeWithSelector(DifferentStreamingToken.selector, users.bob.streamId, DAI)); 11 | stakingContract.stake(users.bob.streamId); 12 | } 13 | 14 | modifier givenStreamingTokenIsRewardToken() { 15 | _; 16 | } 17 | 18 | function test_RevertWhen_AlreadyStaking() external givenStreamingTokenIsRewardToken { 19 | resetPrank({ msgSender: users.alice.addr }); 20 | 21 | vm.expectRevert(abi.encodeWithSelector(AlreadyStaking.selector, users.alice.addr, users.alice.streamId)); 22 | stakingContract.stake(users.alice.streamId); 23 | } 24 | 25 | function test_WhenNotAlreadyStaking() external givenStreamingTokenIsRewardToken { 26 | // Prank to Joe who is not a staker. 27 | resetPrank({ msgSender: users.joe.addr }); 28 | 29 | // Expect {Staked} event to be emitted. 30 | vm.expectEmit({ emitter: address(stakingContract) }); 31 | emit Staked(users.joe.addr, users.joe.streamId); 32 | 33 | // Stake the NFT. 34 | stakingContract.stake(users.joe.streamId); 35 | 36 | // Assertions: NFT has been transferred to the staking contract. 37 | assertEq(SABLIER.ownerOf(users.joe.streamId), address(stakingContract)); 38 | 39 | // Assertions: storage variables. 40 | assertEq(stakingContract.stakedUsers(users.joe.streamId), users.joe.addr); 41 | assertEq(stakingContract.stakedStreams(users.joe.addr), users.joe.streamId); 42 | 43 | assertEq(stakingContract.totalERC20StakedSupply(), AMOUNT_IN_STREAM * 2); 44 | 45 | // Assert: `updateReward` has correctly updated the storage variables. 46 | assertApproxEqAbs(stakingContract.rewards(users.joe.addr), 0, 0); 47 | assertEq(stakingContract.lastUpdateTime(), block.timestamp); 48 | assertEq(stakingContract.totalRewardPaidPerERC20Token(), 0); 49 | assertEq(stakingContract.userRewardPerERC20Token(users.joe.addr), 0); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lockup/tests/stake-sablier-nft-test/stake/stake.tree: -------------------------------------------------------------------------------- 1 | Stake_Test 2 | ├── given streaming token is not reward token 3 | │ └── it should revert 4 | └── given streaming token is reward token 5 | ├── when already staking 6 | │ └── it should revert 7 | └── when not already staking 8 | ├── it should transfer the sablier NFT from the caller to the staking contract 9 | ├── it should update {streamOwner} and {stakedTokenId} 10 | ├── it should update {totalERC20StakedSupply} 11 | ├── it should update {updateReward} storage variables 12 | └── it should emit a {Staked} event 13 | -------------------------------------------------------------------------------- /lockup/tests/stake-sablier-nft-test/unstake/unstake.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.19; 3 | 4 | import { StakeSablierNFT_Fork_Test } from "../StakeSablierNFT.t.sol"; 5 | 6 | contract Unstake_Test is StakeSablierNFT_Fork_Test { 7 | function test_RevertWhen_CallerIsNotStaker() external { 8 | // Change the caller to a non staker. 9 | resetPrank({ msgSender: users.bob.addr }); 10 | 11 | vm.expectRevert(abi.encodeWithSelector(UnauthorizedCaller.selector, users.bob.addr, users.bob.streamId)); 12 | stakingContract.unstake(users.bob.streamId); 13 | } 14 | 15 | function test_WhenCallerIsStaker() external { 16 | // Change the caller to a non staker and stake a stream. 17 | resetPrank({ msgSender: users.joe.addr }); 18 | stakingContract.stake(users.joe.streamId); 19 | 20 | vm.warp(block.timestamp + 1 days); 21 | 22 | // Expect {Unstaked} event to be emitted. 23 | vm.expectEmit({ emitter: address(stakingContract) }); 24 | emit Unstaked(users.joe.addr, users.joe.streamId); 25 | 26 | // Unstake the NFT. 27 | stakingContract.unstake(users.joe.streamId); 28 | 29 | // Assert: NFT has been transferred. 30 | assertEq(SABLIER.ownerOf(users.joe.streamId), users.joe.addr); 31 | 32 | // Assert: `stakedTokens` and `stakedStreamId` have been deleted from storage. 33 | assertEq(stakingContract.stakedUsers(users.joe.streamId), address(0)); 34 | assertEq(stakingContract.stakedStreams(users.joe.addr), 0); 35 | 36 | // Assert: `totalERC20StakedSupply` has been updated. 37 | assertEq(stakingContract.totalERC20StakedSupply(), AMOUNT_IN_STREAM); 38 | 39 | // Assert: `updateReward` has correctly updated the storage variables. 40 | uint256 expectedReward = 1 days * rewardRate / 2; 41 | assertApproxEqAbs(stakingContract.rewards(users.joe.addr), expectedReward, 0.0001e18); 42 | assertEq(stakingContract.lastUpdateTime(), block.timestamp); 43 | assertEq(stakingContract.totalRewardPaidPerERC20Token(), (expectedReward * 1e18) / AMOUNT_IN_STREAM); 44 | assertEq(stakingContract.userRewardPerERC20Token(users.joe.addr), (expectedReward * 1e18) / AMOUNT_IN_STREAM); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lockup/tests/stake-sablier-nft-test/unstake/unstake.tree: -------------------------------------------------------------------------------- 1 | Unstake_Test 2 | ├── when caller is not staker 3 | │ └── it should revert 4 | └── when caller is staker 5 | ├── it should transfer the sablier NFT to the caller 6 | ├── it should delete {streamOwner} and {stakedTokenId} 7 | ├── it should update {totalERC20StakedSupply} 8 | ├── it should update {updateReward} storage variables 9 | └── it should emit a {Unstaked} event 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples", 3 | "description": "Solidity examples for on-chain interaction with Sablier Protocols", 4 | "version": "1.0.0", 5 | "author": { 6 | "name": "Sablier Labs Ltd", 7 | "url": "https://sablier.com" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/sablier-labs/examples/issues" 11 | }, 12 | "devDependencies": { 13 | "forge-std": "github:foundry-rs/forge-std#v1.8.1", 14 | "prettier": "^2.8.8", 15 | "solhint": "^5.0.3" 16 | }, 17 | "dependencies": { 18 | "@openzeppelin/contracts": "5.0.2", 19 | "@prb/math": "4.1.0", 20 | "@sablier/airdrops": "1.3.0", 21 | "@sablier/flow": "1.1.0", 22 | "@sablier/lockup": "2.0.0" 23 | }, 24 | "homepage": "https://github.com/sablier-labs/examples#readme", 25 | "keywords": [ 26 | "asset-streaming", 27 | "blockchain", 28 | "cryptoasset-streaming", 29 | "cryptoassets", 30 | "ethereum", 31 | "foundry", 32 | "money-streaming", 33 | "real-time-finance", 34 | "sablier", 35 | "sablier-v2", 36 | "smart-contracts", 37 | "solidity", 38 | "token-streaming" 39 | ], 40 | "license": "GPL-3.0-or-later", 41 | "private": true, 42 | "repository": "github.com:sablier-labs/examples", 43 | "scripts": { 44 | "build": "bun run build:airdrops && bun run build:flow && bun run build:lockup", 45 | "build:airdrops": "FOUNDRY_PROFILE=airdrops forge build", 46 | "build:flow": "FOUNDRY_PROFILE=flow forge build", 47 | "build:lockup": "FOUNDRY_PROFILE=lockup forge build", 48 | "clean": "rm -rf cache out", 49 | "lint": "bun run lint:sol && bun run prettier:check", 50 | "lint:sol": "forge fmt . && bun solhint \"{airdrops,flow,lockup}/**/*.sol\"", 51 | "prettier:check": "prettier --check \"**/*.{md,yml}\"", 52 | "prettier:write": "prettier --write \"**/*.{md,yml}\"", 53 | "test": "bun run test:airdrops && bun run test:flow && bun run test:lockup", 54 | "test:airdrops": "FOUNDRY_PROFILE=airdrops forge test", 55 | "test:flow": "FOUNDRY_PROFILE=flow forge test", 56 | "test:lockup": "FOUNDRY_PROFILE=lockup forge test" 57 | } 58 | } 59 | --------------------------------------------------------------------------------