├── .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 |
--------------------------------------------------------------------------------