├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .gitmodules
├── LICENSE
├── Makefile
├── README.md
├── audits
├── cantinasec-dss-conduits.pdf
└── chainsecurity-dss-conduits.pdf
├── certora
├── ArrangerConduit.conf
├── ArrangerConduit.spec
└── Auxiliar.sol
├── foundry.toml
├── remappings.txt
├── src
├── ArrangerConduit.sol
└── interfaces
│ └── IArrangerConduit.sol
└── test
└── arranger-conduit
├── ArrangerConduitHarness.sol
├── ArrangerConduitHarness.t.sol
├── AuthFunctions.t.sol
├── CancelFundRequest.t.sol
├── ConduitTestBase.sol
├── Constructor.t.sol
├── Deposit.t.sol
├── DrawFunds.t.sol
├── EventsData.t.sol
├── RequestFunds.t.sol
├── ReturnFunds.t.sol
├── ViewFunctions.t.sol
├── Withdraw.t.sol
└── invariants
├── BoundedInvariants.t.sol
├── InvariantTestBase.t.sol
├── handlers
├── Arranger.sol
├── HandlerBase.sol
├── Operator.sol
└── Transferer.sol
└── interfaces
└── Interfaces.sol
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | workflow_dispatch:
5 | pull_request:
6 | push:
7 | branches:
8 | - master
9 |
10 | env:
11 | FOUNDRY_PROFILE: ci
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v3
18 |
19 | - name: Install Foundry
20 | uses: foundry-rs/foundry-toolchain@v1
21 |
22 | - name: Build contracts
23 | run: |
24 | forge --version
25 | forge build --sizes
26 |
27 | test:
28 | runs-on: ubuntu-latest
29 | steps:
30 | - uses: actions/checkout@v3
31 |
32 | - name: Install Foundry
33 | uses: foundry-rs/foundry-toolchain@v1
34 |
35 | - name: Run tests
36 | run: FOUNDRY_PROFILE=ci forge test
37 |
38 | test-unbounded-invariants:
39 | runs-on: ubuntu-latest
40 | steps:
41 | - uses: actions/checkout@v3
42 |
43 | - name: Install Foundry
44 | uses: foundry-rs/foundry-toolchain@v1
45 |
46 | - name: Run tests
47 | run: FOUNDRY_PROFILE=unbounded-ci forge test --mt invariant
48 |
49 | coverage:
50 | runs-on: ubuntu-latest
51 | steps:
52 | - uses: actions/checkout@v3
53 |
54 | - name: Install Foundry
55 | uses: foundry-rs/foundry-toolchain@v1
56 |
57 | - name: Run coverage
58 | run: forge coverage --report summary --report lcov
59 |
60 | # To ignore coverage for certain directories modify the paths in this step as needed. The
61 | # below default ignores coverage results for the test and script directories. Alternatively,
62 | # to include coverage in all directories, comment out this step. Note that because this
63 | # filtering applies to the lcov file, the summary table generated in the previous step will
64 | # still include all files and directories.
65 | # The `--rc lcov_branch_coverage=1` part keeps branch info in the filtered report, since lcov
66 | # defaults to removing branch info.
67 | - name: Filter directories
68 | run: |
69 | sudo apt update && sudo apt install -y lcov
70 | lcov --remove lcov.info 'test/*' 'script/*' --output-file lcov.info --rc lcov_branch_coverage=1
71 |
72 | # This step posts a detailed coverage report as a comment and deletes previous comments on
73 | # each push. The below step is used to fail coverage if the specified coverage threshold is
74 | # not met. The below step can post a comment (when it's `github-token` is specified) but it's
75 | # not as useful, and this action cannot fail CI based on a minimum coverage threshold, which
76 | # is why we use both in this way.
77 | - name: Post coverage report
78 | if: github.event_name == 'pull_request' # This action fails when ran outside of a pull request.
79 | uses: romeovs/lcov-reporter-action@v0.3.1
80 | with:
81 | delete-old-comments: true
82 | lcov-file: ./lcov.info
83 | github-token: ${{ secrets.GITHUB_TOKEN }} # Adds a coverage summary comment to the PR.
84 |
85 | - name: Verify minimum coverage
86 | uses: zgosalvez/github-actions-report-lcov@v2
87 | with:
88 | coverage-files: ./lcov.info
89 | minimum-coverage: 90 # Set coverage threshold.
90 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiler files
2 | cache/
3 | out/
4 |
5 | # Ignores development broadcast logs
6 | !/broadcast
7 | /broadcast/*/31337/
8 | /broadcast/**/dry-run/
9 |
10 | # Docs
11 | docs/
12 |
13 | # Dotenv file
14 | .env
15 |
16 | # Test coverage
17 | lcov.info
18 |
19 | # Certora
20 | .*certora*
21 | .last_confs/
22 | *.zip
23 | resource_errors.json
24 | .zip-output-url.txt
25 | certora_debug_log.txt
26 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "lib/forge-std"]
2 | path = lib/forge-std
3 | url = https://github.com/foundry-rs/forge-std
4 | [submodule "lib/upgradeable-proxy"]
5 | path = lib/upgradeable-proxy
6 | url = https://github.com/marsfoundation/upgradeable-proxy
7 | [submodule "lib/dss-test"]
8 | path = lib/dss-test
9 | url = https://github.com/makerdao/dss-test
10 | [submodule "lib/erc20-helpers"]
11 | path = lib/erc20-helpers
12 | url = https://github.com/marsfoundation/erc20-helpers
13 | [submodule "lib/dss-allocator"]
14 | path = lib/dss-allocator
15 | url = https://github.com/makerdao/dss-allocator
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU AFFERO GENERAL PUBLIC LICENSE
2 | Version 3, 19 November 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU Affero General Public License is a free, copyleft license for
11 | software and other kinds of works, specifically designed to ensure
12 | cooperation with the community in the case of network server software.
13 |
14 | The licenses for most software and other practical works are designed
15 | to take away your freedom to share and change the works. By contrast,
16 | our General Public Licenses are intended to guarantee your freedom to
17 | share and change all versions of a program--to make sure it remains free
18 | software for all its users.
19 |
20 | When we speak of free software, we are referring to freedom, not
21 | price. Our General Public Licenses are designed to make sure that you
22 | have the freedom to distribute copies of free software (and charge for
23 | them if you wish), that you receive source code or can get it if you
24 | want it, that you can change the software or use pieces of it in new
25 | free programs, and that you know you can do these things.
26 |
27 | Developers that use our General Public Licenses protect your rights
28 | with two steps: (1) assert copyright on the software, and (2) offer
29 | you this License which gives you legal permission to copy, distribute
30 | and/or modify the software.
31 |
32 | A secondary benefit of defending all users' freedom is that
33 | improvements made in alternate versions of the program, if they
34 | receive widespread use, become available for other developers to
35 | incorporate. Many developers of free software are heartened and
36 | encouraged by the resulting cooperation. However, in the case of
37 | software used on network servers, this result may fail to come about.
38 | The GNU General Public License permits making a modified version and
39 | letting the public access it on a server without ever releasing its
40 | source code to the public.
41 |
42 | The GNU Affero General Public License is designed specifically to
43 | ensure that, in such cases, the modified source code becomes available
44 | to the community. It requires the operator of a network server to
45 | provide the source code of the modified version running there to the
46 | users of that server. Therefore, public use of a modified version, on
47 | a publicly accessible server, gives the public access to the source
48 | code of the modified version.
49 |
50 | An older license, called the Affero General Public License and
51 | published by Affero, was designed to accomplish similar goals. This is
52 | a different license, not a version of the Affero GPL, but Affero has
53 | released a new version of the Affero GPL which permits relicensing under
54 | this license.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | TERMS AND CONDITIONS
60 |
61 | 0. Definitions.
62 |
63 | "This License" refers to version 3 of the GNU Affero General Public License.
64 |
65 | "Copyright" also means copyright-like laws that apply to other kinds of
66 | works, such as semiconductor masks.
67 |
68 | "The Program" refers to any copyrightable work licensed under this
69 | License. Each licensee is addressed as "you". "Licensees" and
70 | "recipients" may be individuals or organizations.
71 |
72 | To "modify" a work means to copy from or adapt all or part of the work
73 | in a fashion requiring copyright permission, other than the making of an
74 | exact copy. The resulting work is called a "modified version" of the
75 | earlier work or a work "based on" the earlier work.
76 |
77 | A "covered work" means either the unmodified Program or a work based
78 | on the Program.
79 |
80 | To "propagate" a work means to do anything with it that, without
81 | permission, would make you directly or secondarily liable for
82 | infringement under applicable copyright law, except executing it on a
83 | computer or modifying a private copy. Propagation includes copying,
84 | distribution (with or without modification), making available to the
85 | public, and in some countries other activities as well.
86 |
87 | To "convey" a work means any kind of propagation that enables other
88 | parties to make or receive copies. Mere interaction with a user through
89 | a computer network, with no transfer of a copy, is not conveying.
90 |
91 | An interactive user interface displays "Appropriate Legal Notices"
92 | to the extent that it includes a convenient and prominently visible
93 | feature that (1) displays an appropriate copyright notice, and (2)
94 | tells the user that there is no warranty for the work (except to the
95 | extent that warranties are provided), that licensees may convey the
96 | work under this License, and how to view a copy of this License. If
97 | the interface presents a list of user commands or options, such as a
98 | menu, a prominent item in the list meets this criterion.
99 |
100 | 1. Source Code.
101 |
102 | The "source code" for a work means the preferred form of the work
103 | for making modifications to it. "Object code" means any non-source
104 | form of a work.
105 |
106 | A "Standard Interface" means an interface that either is an official
107 | standard defined by a recognized standards body, or, in the case of
108 | interfaces specified for a particular programming language, one that
109 | is widely used among developers working in that language.
110 |
111 | The "System Libraries" of an executable work include anything, other
112 | than the work as a whole, that (a) is included in the normal form of
113 | packaging a Major Component, but which is not part of that Major
114 | Component, and (b) serves only to enable use of the work with that
115 | Major Component, or to implement a Standard Interface for which an
116 | implementation is available to the public in source code form. A
117 | "Major Component", in this context, means a major essential component
118 | (kernel, window system, and so on) of the specific operating system
119 | (if any) on which the executable work runs, or a compiler used to
120 | produce the work, or an object code interpreter used to run it.
121 |
122 | The "Corresponding Source" for a work in object code form means all
123 | the source code needed to generate, install, and (for an executable
124 | work) run the object code and to modify the work, including scripts to
125 | control those activities. However, it does not include the work's
126 | System Libraries, or general-purpose tools or generally available free
127 | programs which are used unmodified in performing those activities but
128 | which are not part of the work. For example, Corresponding Source
129 | includes interface definition files associated with source files for
130 | the work, and the source code for shared libraries and dynamically
131 | linked subprograms that the work is specifically designed to require,
132 | such as by intimate data communication or control flow between those
133 | subprograms and other parts of the work.
134 |
135 | The Corresponding Source need not include anything that users
136 | can regenerate automatically from other parts of the Corresponding
137 | Source.
138 |
139 | The Corresponding Source for a work in source code form is that
140 | same work.
141 |
142 | 2. Basic Permissions.
143 |
144 | All rights granted under this License are granted for the term of
145 | copyright on the Program, and are irrevocable provided the stated
146 | conditions are met. This License explicitly affirms your unlimited
147 | permission to run the unmodified Program. The output from running a
148 | covered work is covered by this License only if the output, given its
149 | content, constitutes a covered work. This License acknowledges your
150 | rights of fair use or other equivalent, as provided by copyright law.
151 |
152 | You may make, run and propagate covered works that you do not
153 | convey, without conditions so long as your license otherwise remains
154 | in force. You may convey covered works to others for the sole purpose
155 | of having them make modifications exclusively for you, or provide you
156 | with facilities for running those works, provided that you comply with
157 | the terms of this License in conveying all material for which you do
158 | not control copyright. Those thus making or running the covered works
159 | for you must do so exclusively on your behalf, under your direction
160 | and control, on terms that prohibit them from making any copies of
161 | your copyrighted material outside their relationship with you.
162 |
163 | Conveying under any other circumstances is permitted solely under
164 | the conditions stated below. Sublicensing is not allowed; section 10
165 | makes it unnecessary.
166 |
167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168 |
169 | No covered work shall be deemed part of an effective technological
170 | measure under any applicable law fulfilling obligations under article
171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
172 | similar laws prohibiting or restricting circumvention of such
173 | measures.
174 |
175 | When you convey a covered work, you waive any legal power to forbid
176 | circumvention of technological measures to the extent such circumvention
177 | is effected by exercising rights under this License with respect to
178 | the covered work, and you disclaim any intention to limit operation or
179 | modification of the work as a means of enforcing, against the work's
180 | users, your or third parties' legal rights to forbid circumvention of
181 | technological measures.
182 |
183 | 4. Conveying Verbatim Copies.
184 |
185 | You may convey verbatim copies of the Program's source code as you
186 | receive it, in any medium, provided that you conspicuously and
187 | appropriately publish on each copy an appropriate copyright notice;
188 | keep intact all notices stating that this License and any
189 | non-permissive terms added in accord with section 7 apply to the code;
190 | keep intact all notices of the absence of any warranty; and give all
191 | recipients a copy of this License along with the Program.
192 |
193 | You may charge any price or no price for each copy that you convey,
194 | and you may offer support or warranty protection for a fee.
195 |
196 | 5. Conveying Modified Source Versions.
197 |
198 | You may convey a work based on the Program, or the modifications to
199 | produce it from the Program, in the form of source code under the
200 | terms of section 4, provided that you also meet all of these conditions:
201 |
202 | a) The work must carry prominent notices stating that you modified
203 | it, and giving a relevant date.
204 |
205 | b) The work must carry prominent notices stating that it is
206 | released under this License and any conditions added under section
207 | 7. This requirement modifies the requirement in section 4 to
208 | "keep intact all notices".
209 |
210 | c) You must license the entire work, as a whole, under this
211 | License to anyone who comes into possession of a copy. This
212 | License will therefore apply, along with any applicable section 7
213 | additional terms, to the whole of the work, and all its parts,
214 | regardless of how they are packaged. This License gives no
215 | permission to license the work in any other way, but it does not
216 | invalidate such permission if you have separately received it.
217 |
218 | d) If the work has interactive user interfaces, each must display
219 | Appropriate Legal Notices; however, if the Program has interactive
220 | interfaces that do not display Appropriate Legal Notices, your
221 | work need not make them do so.
222 |
223 | A compilation of a covered work with other separate and independent
224 | works, which are not by their nature extensions of the covered work,
225 | and which are not combined with it such as to form a larger program,
226 | in or on a volume of a storage or distribution medium, is called an
227 | "aggregate" if the compilation and its resulting copyright are not
228 | used to limit the access or legal rights of the compilation's users
229 | beyond what the individual works permit. Inclusion of a covered work
230 | in an aggregate does not cause this License to apply to the other
231 | parts of the aggregate.
232 |
233 | 6. Conveying Non-Source Forms.
234 |
235 | You may convey a covered work in object code form under the terms
236 | of sections 4 and 5, provided that you also convey the
237 | machine-readable Corresponding Source under the terms of this License,
238 | in one of these ways:
239 |
240 | a) Convey the object code in, or embodied in, a physical product
241 | (including a physical distribution medium), accompanied by the
242 | Corresponding Source fixed on a durable physical medium
243 | customarily used for software interchange.
244 |
245 | b) Convey the object code in, or embodied in, a physical product
246 | (including a physical distribution medium), accompanied by a
247 | written offer, valid for at least three years and valid for as
248 | long as you offer spare parts or customer support for that product
249 | model, to give anyone who possesses the object code either (1) a
250 | copy of the Corresponding Source for all the software in the
251 | product that is covered by this License, on a durable physical
252 | medium customarily used for software interchange, for a price no
253 | more than your reasonable cost of physically performing this
254 | conveying of source, or (2) access to copy the
255 | Corresponding Source from a network server at no charge.
256 |
257 | c) Convey individual copies of the object code with a copy of the
258 | written offer to provide the Corresponding Source. This
259 | alternative is allowed only occasionally and noncommercially, and
260 | only if you received the object code with such an offer, in accord
261 | with subsection 6b.
262 |
263 | d) Convey the object code by offering access from a designated
264 | place (gratis or for a charge), and offer equivalent access to the
265 | Corresponding Source in the same way through the same place at no
266 | further charge. You need not require recipients to copy the
267 | Corresponding Source along with the object code. If the place to
268 | copy the object code is a network server, the Corresponding Source
269 | may be on a different server (operated by you or a third party)
270 | that supports equivalent copying facilities, provided you maintain
271 | clear directions next to the object code saying where to find the
272 | Corresponding Source. Regardless of what server hosts the
273 | Corresponding Source, you remain obligated to ensure that it is
274 | available for as long as needed to satisfy these requirements.
275 |
276 | e) Convey the object code using peer-to-peer transmission, provided
277 | you inform other peers where the object code and Corresponding
278 | Source of the work are being offered to the general public at no
279 | charge under subsection 6d.
280 |
281 | A separable portion of the object code, whose source code is excluded
282 | from the Corresponding Source as a System Library, need not be
283 | included in conveying the object code work.
284 |
285 | A "User Product" is either (1) a "consumer product", which means any
286 | tangible personal property which is normally used for personal, family,
287 | or household purposes, or (2) anything designed or sold for incorporation
288 | into a dwelling. In determining whether a product is a consumer product,
289 | doubtful cases shall be resolved in favor of coverage. For a particular
290 | product received by a particular user, "normally used" refers to a
291 | typical or common use of that class of product, regardless of the status
292 | of the particular user or of the way in which the particular user
293 | actually uses, or expects or is expected to use, the product. A product
294 | is a consumer product regardless of whether the product has substantial
295 | commercial, industrial or non-consumer uses, unless such uses represent
296 | the only significant mode of use of the product.
297 |
298 | "Installation Information" for a User Product means any methods,
299 | procedures, authorization keys, or other information required to install
300 | and execute modified versions of a covered work in that User Product from
301 | a modified version of its Corresponding Source. The information must
302 | suffice to ensure that the continued functioning of the modified object
303 | code is in no case prevented or interfered with solely because
304 | modification has been made.
305 |
306 | If you convey an object code work under this section in, or with, or
307 | specifically for use in, a User Product, and the conveying occurs as
308 | part of a transaction in which the right of possession and use of the
309 | User Product is transferred to the recipient in perpetuity or for a
310 | fixed term (regardless of how the transaction is characterized), the
311 | Corresponding Source conveyed under this section must be accompanied
312 | by the Installation Information. But this requirement does not apply
313 | if neither you nor any third party retains the ability to install
314 | modified object code on the User Product (for example, the work has
315 | been installed in ROM).
316 |
317 | The requirement to provide Installation Information does not include a
318 | requirement to continue to provide support service, warranty, or updates
319 | for a work that has been modified or installed by the recipient, or for
320 | the User Product in which it has been modified or installed. Access to a
321 | network may be denied when the modification itself materially and
322 | adversely affects the operation of the network or violates the rules and
323 | protocols for communication across the network.
324 |
325 | Corresponding Source conveyed, and Installation Information provided,
326 | in accord with this section must be in a format that is publicly
327 | documented (and with an implementation available to the public in
328 | source code form), and must require no special password or key for
329 | unpacking, reading or copying.
330 |
331 | 7. Additional Terms.
332 |
333 | "Additional permissions" are terms that supplement the terms of this
334 | License by making exceptions from one or more of its conditions.
335 | Additional permissions that are applicable to the entire Program shall
336 | be treated as though they were included in this License, to the extent
337 | that they are valid under applicable law. If additional permissions
338 | apply only to part of the Program, that part may be used separately
339 | under those permissions, but the entire Program remains governed by
340 | this License without regard to the additional permissions.
341 |
342 | When you convey a copy of a covered work, you may at your option
343 | remove any additional permissions from that copy, or from any part of
344 | it. (Additional permissions may be written to require their own
345 | removal in certain cases when you modify the work.) You may place
346 | additional permissions on material, added by you to a covered work,
347 | for which you have or can give appropriate copyright permission.
348 |
349 | Notwithstanding any other provision of this License, for material you
350 | add to a covered work, you may (if authorized by the copyright holders of
351 | that material) supplement the terms of this License with terms:
352 |
353 | a) Disclaiming warranty or limiting liability differently from the
354 | terms of sections 15 and 16 of this License; or
355 |
356 | b) Requiring preservation of specified reasonable legal notices or
357 | author attributions in that material or in the Appropriate Legal
358 | Notices displayed by works containing it; or
359 |
360 | c) Prohibiting misrepresentation of the origin of that material, or
361 | requiring that modified versions of such material be marked in
362 | reasonable ways as different from the original version; or
363 |
364 | d) Limiting the use for publicity purposes of names of licensors or
365 | authors of the material; or
366 |
367 | e) Declining to grant rights under trademark law for use of some
368 | trade names, trademarks, or service marks; or
369 |
370 | f) Requiring indemnification of licensors and authors of that
371 | material by anyone who conveys the material (or modified versions of
372 | it) with contractual assumptions of liability to the recipient, for
373 | any liability that these contractual assumptions directly impose on
374 | those licensors and authors.
375 |
376 | All other non-permissive additional terms are considered "further
377 | restrictions" within the meaning of section 10. If the Program as you
378 | received it, or any part of it, contains a notice stating that it is
379 | governed by this License along with a term that is a further
380 | restriction, you may remove that term. If a license document contains
381 | a further restriction but permits relicensing or conveying under this
382 | License, you may add to a covered work material governed by the terms
383 | of that license document, provided that the further restriction does
384 | not survive such relicensing or conveying.
385 |
386 | If you add terms to a covered work in accord with this section, you
387 | must place, in the relevant source files, a statement of the
388 | additional terms that apply to those files, or a notice indicating
389 | where to find the applicable terms.
390 |
391 | Additional terms, permissive or non-permissive, may be stated in the
392 | form of a separately written license, or stated as exceptions;
393 | the above requirements apply either way.
394 |
395 | 8. Termination.
396 |
397 | You may not propagate or modify a covered work except as expressly
398 | provided under this License. Any attempt otherwise to propagate or
399 | modify it is void, and will automatically terminate your rights under
400 | this License (including any patent licenses granted under the third
401 | paragraph of section 11).
402 |
403 | However, if you cease all violation of this License, then your
404 | license from a particular copyright holder is reinstated (a)
405 | provisionally, unless and until the copyright holder explicitly and
406 | finally terminates your license, and (b) permanently, if the copyright
407 | holder fails to notify you of the violation by some reasonable means
408 | prior to 60 days after the cessation.
409 |
410 | Moreover, your license from a particular copyright holder is
411 | reinstated permanently if the copyright holder notifies you of the
412 | violation by some reasonable means, this is the first time you have
413 | received notice of violation of this License (for any work) from that
414 | copyright holder, and you cure the violation prior to 30 days after
415 | your receipt of the notice.
416 |
417 | Termination of your rights under this section does not terminate the
418 | licenses of parties who have received copies or rights from you under
419 | this License. If your rights have been terminated and not permanently
420 | reinstated, you do not qualify to receive new licenses for the same
421 | material under section 10.
422 |
423 | 9. Acceptance Not Required for Having Copies.
424 |
425 | You are not required to accept this License in order to receive or
426 | run a copy of the Program. Ancillary propagation of a covered work
427 | occurring solely as a consequence of using peer-to-peer transmission
428 | to receive a copy likewise does not require acceptance. However,
429 | nothing other than this License grants you permission to propagate or
430 | modify any covered work. These actions infringe copyright if you do
431 | not accept this License. Therefore, by modifying or propagating a
432 | covered work, you indicate your acceptance of this License to do so.
433 |
434 | 10. Automatic Licensing of Downstream Recipients.
435 |
436 | Each time you convey a covered work, the recipient automatically
437 | receives a license from the original licensors, to run, modify and
438 | propagate that work, subject to this License. You are not responsible
439 | for enforcing compliance by third parties with this License.
440 |
441 | An "entity transaction" is a transaction transferring control of an
442 | organization, or substantially all assets of one, or subdividing an
443 | organization, or merging organizations. If propagation of a covered
444 | work results from an entity transaction, each party to that
445 | transaction who receives a copy of the work also receives whatever
446 | licenses to the work the party's predecessor in interest had or could
447 | give under the previous paragraph, plus a right to possession of the
448 | Corresponding Source of the work from the predecessor in interest, if
449 | the predecessor has it or can get it with reasonable efforts.
450 |
451 | You may not impose any further restrictions on the exercise of the
452 | rights granted or affirmed under this License. For example, you may
453 | not impose a license fee, royalty, or other charge for exercise of
454 | rights granted under this License, and you may not initiate litigation
455 | (including a cross-claim or counterclaim in a lawsuit) alleging that
456 | any patent claim is infringed by making, using, selling, offering for
457 | sale, or importing the Program or any portion of it.
458 |
459 | 11. Patents.
460 |
461 | A "contributor" is a copyright holder who authorizes use under this
462 | License of the Program or a work on which the Program is based. The
463 | work thus licensed is called the contributor's "contributor version".
464 |
465 | A contributor's "essential patent claims" are all patent claims
466 | owned or controlled by the contributor, whether already acquired or
467 | hereafter acquired, that would be infringed by some manner, permitted
468 | by this License, of making, using, or selling its contributor version,
469 | but do not include claims that would be infringed only as a
470 | consequence of further modification of the contributor version. For
471 | purposes of this definition, "control" includes the right to grant
472 | patent sublicenses in a manner consistent with the requirements of
473 | this License.
474 |
475 | Each contributor grants you a non-exclusive, worldwide, royalty-free
476 | patent license under the contributor's essential patent claims, to
477 | make, use, sell, offer for sale, import and otherwise run, modify and
478 | propagate the contents of its contributor version.
479 |
480 | In the following three paragraphs, a "patent license" is any express
481 | agreement or commitment, however denominated, not to enforce a patent
482 | (such as an express permission to practice a patent or covenant not to
483 | sue for patent infringement). To "grant" such a patent license to a
484 | party means to make such an agreement or commitment not to enforce a
485 | patent against the party.
486 |
487 | If you convey a covered work, knowingly relying on a patent license,
488 | and the Corresponding Source of the work is not available for anyone
489 | to copy, free of charge and under the terms of this License, through a
490 | publicly available network server or other readily accessible means,
491 | then you must either (1) cause the Corresponding Source to be so
492 | available, or (2) arrange to deprive yourself of the benefit of the
493 | patent license for this particular work, or (3) arrange, in a manner
494 | consistent with the requirements of this License, to extend the patent
495 | license to downstream recipients. "Knowingly relying" means you have
496 | actual knowledge that, but for the patent license, your conveying the
497 | covered work in a country, or your recipient's use of the covered work
498 | in a country, would infringe one or more identifiable patents in that
499 | country that you have reason to believe are valid.
500 |
501 | If, pursuant to or in connection with a single transaction or
502 | arrangement, you convey, or propagate by procuring conveyance of, a
503 | covered work, and grant a patent license to some of the parties
504 | receiving the covered work authorizing them to use, propagate, modify
505 | or convey a specific copy of the covered work, then the patent license
506 | you grant is automatically extended to all recipients of the covered
507 | work and works based on it.
508 |
509 | A patent license is "discriminatory" if it does not include within
510 | the scope of its coverage, prohibits the exercise of, or is
511 | conditioned on the non-exercise of one or more of the rights that are
512 | specifically granted under this License. You may not convey a covered
513 | work if you are a party to an arrangement with a third party that is
514 | in the business of distributing software, under which you make payment
515 | to the third party based on the extent of your activity of conveying
516 | the work, and under which the third party grants, to any of the
517 | parties who would receive the covered work from you, a discriminatory
518 | patent license (a) in connection with copies of the covered work
519 | conveyed by you (or copies made from those copies), or (b) primarily
520 | for and in connection with specific products or compilations that
521 | contain the covered work, unless you entered into that arrangement,
522 | or that patent license was granted, prior to 28 March 2007.
523 |
524 | Nothing in this License shall be construed as excluding or limiting
525 | any implied license or other defenses to infringement that may
526 | otherwise be available to you under applicable patent law.
527 |
528 | 12. No Surrender of Others' Freedom.
529 |
530 | If conditions are imposed on you (whether by court order, agreement or
531 | otherwise) that contradict the conditions of this License, they do not
532 | excuse you from the conditions of this License. If you cannot convey a
533 | covered work so as to satisfy simultaneously your obligations under this
534 | License and any other pertinent obligations, then as a consequence you may
535 | not convey it at all. For example, if you agree to terms that obligate you
536 | to collect a royalty for further conveying from those to whom you convey
537 | the Program, the only way you could satisfy both those terms and this
538 | License would be to refrain entirely from conveying the Program.
539 |
540 | 13. Remote Network Interaction; Use with the GNU General Public License.
541 |
542 | Notwithstanding any other provision of this License, if you modify the
543 | Program, your modified version must prominently offer all users
544 | interacting with it remotely through a computer network (if your version
545 | supports such interaction) an opportunity to receive the Corresponding
546 | Source of your version by providing access to the Corresponding Source
547 | from a network server at no charge, through some standard or customary
548 | means of facilitating copying of software. This Corresponding Source
549 | shall include the Corresponding Source for any work covered by version 3
550 | of the GNU General Public License that is incorporated pursuant to the
551 | following paragraph.
552 |
553 | Notwithstanding any other provision of this License, you have
554 | permission to link or combine any covered work with a work licensed
555 | under version 3 of the GNU General Public License into a single
556 | combined work, and to convey the resulting work. The terms of this
557 | License will continue to apply to the part which is the covered work,
558 | but the work with which it is combined will remain governed by version
559 | 3 of the GNU General Public License.
560 |
561 | 14. Revised Versions of this License.
562 |
563 | The Free Software Foundation may publish revised and/or new versions of
564 | the GNU Affero General Public License from time to time. Such new versions
565 | will be similar in spirit to the present version, but may differ in detail to
566 | address new problems or concerns.
567 |
568 | Each version is given a distinguishing version number. If the
569 | Program specifies that a certain numbered version of the GNU Affero General
570 | Public License "or any later version" applies to it, you have the
571 | option of following the terms and conditions either of that numbered
572 | version or of any later version published by the Free Software
573 | Foundation. If the Program does not specify a version number of the
574 | GNU Affero General Public License, you may choose any version ever published
575 | by the Free Software Foundation.
576 |
577 | If the Program specifies that a proxy can decide which future
578 | versions of the GNU Affero General Public License can be used, that proxy's
579 | public statement of acceptance of a version permanently authorizes you
580 | to choose that version for the Program.
581 |
582 | Later license versions may give you additional or different
583 | permissions. However, no additional obligations are imposed on any
584 | author or copyright holder as a result of your choosing to follow a
585 | later version.
586 |
587 | 15. Disclaimer of Warranty.
588 |
589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597 |
598 | 16. Limitation of Liability.
599 |
600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608 | SUCH DAMAGES.
609 |
610 | 17. Interpretation of Sections 15 and 16.
611 |
612 | If the disclaimer of warranty and limitation of liability provided
613 | above cannot be given local legal effect according to their terms,
614 | reviewing courts shall apply local law that most closely approximates
615 | an absolute waiver of all civil liability in connection with the
616 | Program, unless a warranty or assumption of liability accompanies a
617 | copy of the Program in return for a fee.
618 |
619 | END OF TERMS AND CONDITIONS
620 |
621 | How to Apply These Terms to Your New Programs
622 |
623 | If you develop a new program, and you want it to be of the greatest
624 | possible use to the public, the best way to achieve this is to make it
625 | free software which everyone can redistribute and change under these terms.
626 |
627 | To do so, attach the following notices to the program. It is safest
628 | to attach them to the start of each source file to most effectively
629 | state the exclusion of warranty; and each file should have at least
630 | the "copyright" line and a pointer to where the full notice is found.
631 |
632 |
633 | Copyright (C)
634 |
635 | This program is free software: you can redistribute it and/or modify
636 | it under the terms of the GNU Affero General Public License as published
637 | by the Free Software Foundation, either version 3 of the License, or
638 | (at your option) any later version.
639 |
640 | This program is distributed in the hope that it will be useful,
641 | but WITHOUT ANY WARRANTY; without even the implied warranty of
642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643 | GNU Affero General Public License for more details.
644 |
645 | You should have received a copy of the GNU Affero General Public License
646 | along with this program. If not, see .
647 |
648 | Also add information on how to contact you by electronic and paper mail.
649 |
650 | If your software can interact with users remotely through a computer
651 | network, you should also make sure that it provides a way for users to
652 | get its source. For example, if your program is a web application, its
653 | interface could display a "Source" link that leads users to an archive
654 | of the code. There are many ways you could offer source, and different
655 | solutions will be better for different programs; see section 13 for the
656 | specific requirements.
657 |
658 | You should also get your employer (if you work as a programmer) or school,
659 | if any, to sign a "copyright disclaimer" for the program, if necessary.
660 | For more information on this, and how to apply and follow the GNU AGPL, see
661 | .
662 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | PATH := ~/.solc-select/artifacts/solc-0.8.16:~/.solc-select/artifacts:$(PATH)
2 | certora-conduit :; PATH=${PATH} certoraRun certora/ArrangerConduit.conf$(if $(rule), --rule $(rule),)
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # `dss-conduits`
2 |
3 | 
4 | [![Foundry][foundry-badge]][foundry]
5 | [](https://github.com/makerdao/dss-conduits/blob/master/LICENSE)
6 |
7 | [foundry]: https://getfoundry.sh/
8 | [foundry-badge]: https://img.shields.io/badge/Built%20with-Foundry-FFDB1C.svg
9 |
10 | ## Overview
11 |
12 | The Conduit is a smart contract that facilitates the deployment of funds to yield bearing strategies as part of the DSS Allocator System. The Conduit has a standard interface, defined in the `dss-allocator` repo [here](https://github.com/makerdao/dss-allocator/blob/dev/src/interfaces/IAllocatorConduit.sol). There are intended to be many Conduit implementations, each with a different use case. All Conduits can be found in the `src` directory.
13 |
14 | For technical documentation and further information on specific Conduits, please see the [wiki](https://github.com/makerdao/dss-conduits/wiki).
15 |
16 | ## Upgradeability
17 |
18 | Since Conduits will likely require maintenance as their desired usage evolves, they will be upgradeable contracts, using [`upgradeable-proxy`](https://github.com/marsfoundation/upgradeable-proxy) for upgradeable logic. This is a non-transparent proxy contract that gives upgrade rights to the PauseProxy.
19 |
20 | ## Testing
21 |
22 | To run the tests, do the following:
23 |
24 | ```
25 | forge test
26 | ```
27 |
28 | ## Functionality
29 |
30 | ### `deposit`
31 |
32 | The `deposit` function is used to move funds from a given `ilk`s `buffer` into the Conduit. From the Conduit, the funds can be deployed to a yield bearing strategy. This can happen atomically in the case of DeFi protocols, or can happen in a separate function call made by a permissioned actor in the case of Real World Asset strategies.
33 |
34 |
35 |
36 |
37 |
38 | ### `withdraw`
39 |
40 | The `withdraw` function is used to move funds from the Conduit into a given `ilk`s `buffer`. This can pull funds atomically from a yield bearing strategy in the case of DeFi protocols, or can pull the funds directly from the Conduit in the case of a Real World Asset strategy where the permissioned actor has returned the funds manually. Both situations require that there is available liquidity, which is why `maxWithdraw` exists. This view function should report the maximum amount of `asset` that can be withdrawn for a given `ilk`.
41 |
42 |
43 |
44 |
45 |
46 | ## Disclaimer
47 |
48 | This code belongs to the MakerDAO community and the Copyright for the code belongs to the Dai Foundation.
49 |
50 | ---
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/audits/cantinasec-dss-conduits.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/makerdao/dss-conduits/a09e16356374b3ec4f25292d3ae5b4682790263f/audits/cantinasec-dss-conduits.pdf
--------------------------------------------------------------------------------
/audits/chainsecurity-dss-conduits.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/makerdao/dss-conduits/a09e16356374b3ec4f25292d3ae5b4682790263f/audits/chainsecurity-dss-conduits.pdf
--------------------------------------------------------------------------------
/certora/ArrangerConduit.conf:
--------------------------------------------------------------------------------
1 | {
2 | "files": [
3 | "src/ArrangerConduit.sol",
4 | "lib/dss-allocator/src/AllocatorRoles.sol",
5 | "lib/dss-allocator/src/AllocatorRegistry.sol",
6 | "lib/erc20-helpers/src/MockERC20.sol",
7 | "certora/Auxiliar.sol"
8 | ],
9 | "link": [
10 | "ArrangerConduit:roles=AllocatorRoles",
11 | "ArrangerConduit:registry=AllocatorRegistry",
12 | ],
13 | "optimistic_loop": true,
14 | "rule_sanity": "basic",
15 | "solc": "solc-0.8.16",
16 | "solc_optimize_map": {
17 | "ArrangerConduit": "200",
18 | "AllocatorRoles": "200",
19 | "AllocatorRegistry": "200",
20 | "MockERC20": "0",
21 | "Auxiliar": "0"
22 | },
23 | "verify": "ArrangerConduit:certora/ArrangerConduit.spec",
24 | "parametric_contracts": [
25 | "ArrangerConduit"
26 | ],
27 | "wait_for_results": "all"
28 | }
29 |
--------------------------------------------------------------------------------
/certora/ArrangerConduit.spec:
--------------------------------------------------------------------------------
1 | // ArrangerConduit.spec
2 |
3 | using AllocatorRoles as roles;
4 | using AllocatorRegistry as registry;
5 | using MockERC20 as gem;
6 | using Auxiliar as aux;
7 |
8 | methods {
9 | function wards(address) external returns (uint256) envfree;
10 | function arranger() external returns (address) envfree;
11 | function registry() external returns (address) envfree;
12 | function roles() external returns (address) envfree;
13 | function totalDeposits(address) external returns (uint256) envfree;
14 | function totalRequestedFunds(address) external returns (uint256) envfree;
15 | function totalWithdrawableFunds(address) external returns (uint256) envfree;
16 | function totalWithdrawals(address) external returns (uint256) envfree;
17 | function isBroker(address, address) external returns (bool) envfree;
18 | function deposits(address, bytes32) external returns (uint256) envfree;
19 | function requestedFunds(address, bytes32) external returns (uint256) envfree;
20 | function withdrawableFunds(address, bytes32) external returns (uint256) envfree;
21 | function withdrawals(address, bytes32) external returns (uint256) envfree;
22 | function getFundRequestsLength() external returns (uint256) envfree;
23 | function getFundRequest(uint256) external returns (IArrangerConduit.FundRequest) envfree;
24 |
25 | // needed for resolving calls in the spec
26 | function gem.balanceOf(address) external returns (uint256) envfree;
27 | function gem.allowance(address, address) external returns (uint256) envfree;
28 | function registry.buffers(bytes32) external returns (address) envfree;
29 | function roles.canCall(bytes32, address, address, bytes4) external returns (bool) envfree;
30 | function aux.hashString(string) external returns (bytes32) envfree;
31 |
32 | // needed for resolving calls in the contract
33 | function _.balanceOf(address) external => DISPATCHER(true) UNRESOLVED;
34 | function _.transfer(address, uint256) external => DISPATCHER(true) UNRESOLVED;
35 | function _.transferFrom(address, address, uint256) external => DISPATCHER(true) UNRESOLVED;
36 | }
37 |
38 | definition min(mathint x, mathint y) returns mathint = x < y ? x : y;
39 |
40 | // -----------------------------------------------------------------------------
41 | /**
42 | * Verifying the string encoding
43 | * =============================
44 | * Whenever Solidity reads a string from storage, it verifies that it is properly
45 | * encoded, otherwise it reverts. Note that this occurs also when writing a string
46 | * to the storage.
47 | * The encoding of a string is as follows:
48 | * 1. If the first 32-bytes are even (the very last bit is 0) then:
49 | * - The length of the string is less than 32 bytes
50 | * - The first 31 bytes contain the string
51 | * - The last byte is twice the length of the string
52 | * 2. If the first 32-bytes are odd (the very last bit is 1) then:
53 | * - The length of the string is 32 bytes or more
54 | * - The first 32-bytes are twice the length of the string plus 1!
55 | * - The string itself is in the following bytes
56 | *
57 | * So, an encoding is illegal if either:
58 | * - the first 32-bytes are even and the value of the last byte >= 64, or
59 | * - the first 32-bytes are odd but as an integer they are less than 65.
60 | *
61 | * One solution is to require that there are no illegal strings in storage, using
62 | * a hook.
63 | */
64 |
65 |
66 | /** This hook is called whenever the `info` field of a `FundRequest` in `fundRequests`
67 | * is read. To be precise, whenever the first 32 bytes of `info` are read.
68 | * To create the hook we needed to find the offset of `info` in the `FundRequest`
69 | * struct, here is the calculation:
70 | struct FundRequest {
71 | StatusEnum status; // 1 byte
72 | address asset; // 20 bytes
73 | bytes32 ilk; // 32 bytes
74 | uint256 amountRequested; // 32 bytes
75 | uint256 amountFilled; // 32 bytes
76 | string info;
77 | }
78 | * The storage is 32-bytes aligned.
79 | * Since the `status` and `asset` fields are packed together the offset is 32 * 4 = 128.
80 | */
81 |
82 | /** On top of the above, the Certora tool currently can not handle properly hashing of
83 | * strings with a length that is not a multiple of 32 (which would require masking out the extra bytes in
84 | * the last word). Therefore we also add a `require info.length % 32 == 0`;
85 | *
86 | * Another thing to note is that the code has an implicit loop in it - when copying the
87 | * string to storage for example. The copying is done in batches of 32 bytes.
88 | * For example, if loop_iter is 3 then the loop's length is either 0, 32, 64 or 96.
89 | */
90 |
91 |
92 | hook Sload bytes32 str fundRequests[INDEX uint256 index].(offset 128) STORAGE {
93 | uint256 read;
94 | require to_bytes32(read) == str;
95 | mathint strLen = (read % 256) / 2; // The string length for short strings only
96 | bool isOdd = read % 2 == 1;
97 | require (read > 64 && isOdd) || (strLen <= 31 && !isOdd);
98 | }
99 |
100 | // -----------------------------------------------------------------------------
101 |
102 | // Verify that each storage layout is only modified in the corresponding functions
103 | rule storageAffected(method f) {
104 | env e;
105 |
106 | address anyAddr;
107 | address anyAsset;
108 | bytes32 anyIlk;
109 | uint256 anyIndex;
110 |
111 | mathint wardsBefore = wards(anyAddr);
112 |
113 | IArrangerConduit.FundRequest requestBefore = getFundRequest(anyIndex);
114 | bytes32 infoHashBefore = aux.hashString(requestBefore.info);
115 |
116 | address arrangerBefore = arranger();
117 | address registryBefore = registry();
118 | address rolesBefore = roles();
119 | mathint totalDepositsBefore = totalDeposits(anyAsset);
120 | mathint totalRequestedFundsBefore = totalRequestedFunds(anyAsset);
121 | mathint totalWithdrawableFundsBefore = totalWithdrawableFunds(anyAsset);
122 | mathint totalWithdrawalsBefore = totalWithdrawals(anyAsset);
123 | bool isBrokerBefore = isBroker(anyAddr, anyAsset);
124 | mathint depositsBefore = deposits(anyAddr, anyIlk);
125 | mathint requestedFundsBefore = requestedFunds(anyAddr, anyIlk);
126 | mathint withdrawableFundsBefore = withdrawableFunds(anyAddr, anyIlk);
127 | mathint withdrawalsBefore = withdrawals(anyAddr, anyIlk);
128 |
129 | calldataarg args;
130 | f(e, args);
131 |
132 | mathint wardsAfter = wards(anyAddr);
133 |
134 | IArrangerConduit.FundRequest requestAfter = getFundRequest(anyIndex);
135 | bytes32 infoHashAfter = aux.hashString(requestAfter.info);
136 |
137 | address arrangerAfter= arranger();
138 | address registryAfter= registry();
139 | address rolesAfter = roles();
140 | mathint totalDepositsAfter = totalDeposits(anyAsset);
141 | mathint totalRequestedFundsAfter = totalRequestedFunds(anyAsset);
142 | mathint totalWithdrawableFundsAfter = totalWithdrawableFunds(anyAsset);
143 | mathint totalWithdrawalsAfter = totalWithdrawals(anyAsset);
144 | bool isBrokerAfter = isBroker(anyAddr, anyAsset);
145 | mathint depositsAfter = deposits(anyAddr, anyIlk);
146 | mathint requestedFundsAfter = requestedFunds(anyAddr, anyIlk);
147 | mathint withdrawableFundsAfter = withdrawableFunds(anyAddr, anyIlk);
148 | mathint withdrawalsAfter = withdrawals(anyAddr, anyIlk);
149 |
150 | assert wardsAfter == wardsBefore, "wards changed unexpectedly through the proxied contract";
151 |
152 | assert requestAfter.status != requestBefore.status
153 | || requestAfter.asset != requestBefore.asset
154 | || requestAfter.ilk != requestBefore.ilk
155 | || requestAfter.amountRequested != requestBefore.amountRequested
156 | || requestAfter.amountFilled != requestBefore.amountFilled
157 | || infoHashAfter != infoHashBefore
158 | => f.selector == sig:requestFunds(bytes32, address, uint256, string).selector
159 | || f.selector == sig:cancelFundRequest(uint256).selector
160 | || f.selector == sig:returnFunds(uint256, uint256).selector
161 | , "fundRequests list content changed in an unexpected function";
162 |
163 | assert arrangerAfter != arrangerBefore => f.selector == sig:file(bytes32, address).selector, "arranger changed in an unexpected function";
164 | assert registryAfter != registryBefore => f.selector == sig:file(bytes32, address).selector, "registry changed in an unexpected function";
165 | assert rolesAfter != rolesBefore => f.selector == sig:file(bytes32, address).selector, "roles changed in an unexpected function";
166 | assert totalDepositsAfter != totalDepositsBefore => f.selector == sig:deposit(bytes32, address, uint256).selector, "totalDeposits changed in an unexpected function";
167 |
168 | assert totalRequestedFundsAfter != totalRequestedFundsBefore =>
169 | f.selector == sig:requestFunds(bytes32, address, uint256, string).selector
170 | || f.selector == sig:cancelFundRequest(uint256).selector
171 | || f.selector == sig:returnFunds(uint256, uint256).selector
172 | , "totalRequestedFunds changed in an unexpected function";
173 |
174 | assert totalWithdrawableFundsAfter != totalWithdrawableFundsBefore =>
175 | f.selector == sig:withdraw(bytes32, address, uint256).selector
176 | || f.selector == sig:returnFunds(uint256, uint256).selector
177 | , "totalWithdrawableFunds changed in an unexpected function";
178 |
179 | assert totalWithdrawalsAfter != totalWithdrawalsBefore => f.selector == sig:withdraw(bytes32, address, uint256).selector, "totalWithdrawals changed in an unexpected function";
180 | assert isBrokerAfter != isBrokerBefore => f.selector == sig:setBroker(address, address, bool).selector, "isBroker changed in an unexpected function";
181 | assert depositsAfter != depositsBefore => f.selector == sig:deposit(bytes32, address, uint256).selector, "deposits changed in an unexpected function";
182 |
183 | assert requestedFundsAfter != requestedFundsBefore =>
184 | f.selector == sig:requestFunds(bytes32, address, uint256, string).selector
185 | || f.selector == sig:cancelFundRequest(uint256).selector
186 | || f.selector == sig:returnFunds(uint256, uint256).selector
187 | , "requestedFunds changed in an unexpected function";
188 |
189 | assert withdrawableFundsAfter != withdrawableFundsBefore =>
190 | f.selector == sig:withdraw(bytes32, address, uint256).selector
191 | || f.selector == sig:returnFunds(uint256, uint256).selector
192 | , "withdrawableFunds changed in an unexpected function";
193 |
194 | assert withdrawalsAfter != withdrawalsBefore => f.selector == sig:withdraw(bytes32, address, uint256).selector, "withdrawals changed in an unexpected function";
195 | }
196 |
197 | // Verify correct storage changes for non reverting file
198 | rule file_address(bytes32 what, address data) {
199 | env e;
200 |
201 | address arrangerBefore = arranger();
202 | address registryBefore = registry();
203 | address rolesBefore = roles();
204 |
205 | file(e, what, data);
206 |
207 | address arrangerAfter = arranger();
208 | address registryAfter = registry();
209 | address rolesAfter = roles();
210 |
211 | assert what == to_bytes32(0x617272616e676572000000000000000000000000000000000000000000000000)
212 | => arrangerAfter == data, "file did not set arranger";
213 | assert what != to_bytes32(0x617272616e676572000000000000000000000000000000000000000000000000)
214 | => arrangerAfter == arrangerBefore, "file did not keep unchanged arranger";
215 |
216 | assert what == to_bytes32(0x7265676973747279000000000000000000000000000000000000000000000000)
217 | => registryAfter == data, "file did not set registry";
218 | assert what != to_bytes32(0x7265676973747279000000000000000000000000000000000000000000000000)
219 | => registryAfter == registryBefore, "file did not keep unchanged registry";
220 |
221 | assert what == to_bytes32(0x726f6c6573000000000000000000000000000000000000000000000000000000)
222 | => rolesAfter == data, "file did not set roles";
223 | assert what != to_bytes32(0x726f6c6573000000000000000000000000000000000000000000000000000000)
224 | => rolesAfter == rolesBefore, "file did not keep unchanged roles";
225 | }
226 |
227 | // Verify revert rules on file
228 | rule file_address_revert(bytes32 what, address data) {
229 | env e;
230 |
231 | mathint wardsSender = wards(e.msg.sender);
232 |
233 | file@withrevert(e, what, data);
234 |
235 | bool revert1 = e.msg.value > 0;
236 | bool revert2 = wardsSender != 1;
237 | bool revert3 = what != to_bytes32(0x617272616e676572000000000000000000000000000000000000000000000000)
238 | && what != to_bytes32(0x7265676973747279000000000000000000000000000000000000000000000000)
239 | && what != to_bytes32(0x726f6c6573000000000000000000000000000000000000000000000000000000);
240 |
241 | assert lastReverted <=> revert1 || revert2 || revert3, "Revert rules failed";
242 | }
243 |
244 | // Verify correct storage changes for non reverting setBroker
245 | // Verify correct storage changes for non reverting setBroker
246 | rule setBroker(address usr, address asset, bool valid) {
247 | env e;
248 |
249 | address otherUsr;
250 | address otherAsset;
251 | require otherUsr != usr || otherAsset != asset;
252 |
253 | bool isBrokerOtherBefore = isBroker(otherUsr, otherAsset);
254 |
255 | setBroker(e, usr, asset, valid);
256 |
257 | bool isBrokerUsrAfter = isBroker(usr, asset);
258 | bool isBrokerOtherAfter = isBroker(otherUsr, otherAsset);
259 |
260 | assert isBrokerUsrAfter == valid, "setBroker did not set brokers[usr][asset] to valid";
261 | assert isBrokerOtherAfter == isBrokerOtherBefore, "setBroker did not keep unchanged the rest of brokers[x][y]";
262 | }
263 |
264 | // Verify revert rules on setBroker
265 | rule setBroker_revert(address usr, address asset, bool valid) {
266 | env e;
267 |
268 | mathint wardsSender = wards(e.msg.sender);
269 |
270 | setBroker@withrevert(e, usr, asset, valid);
271 |
272 | bool revert1 = e.msg.value > 0;
273 | bool revert2 = wardsSender != 1;
274 |
275 | assert revert1 => lastReverted, "revert1 failed";
276 | assert revert2 => lastReverted, "revert2 failed";
277 | assert lastReverted => revert1 || revert2, "Revert rules are not covering all the cases";
278 | }
279 |
280 | // Verify correct storage changes for non reverting deposit
281 | rule deposit(bytes32 ilk, address asset, uint256 amount) {
282 | env e;
283 |
284 | require asset == gem;
285 |
286 | address buffer = registry.buffers(ilk);
287 | require buffer != currentContract;
288 |
289 | mathint balanceOfBufferBefore = gem.balanceOf(buffer);
290 | mathint balanceOfConduitBefore = gem.balanceOf(currentContract);
291 | mathint depositsBefore = deposits(asset, ilk);
292 | mathint totalDepositsBefore = totalDeposits(asset);
293 |
294 | bytes32 otherIlk;
295 | address otherAsset;
296 | require otherIlk != ilk || otherAsset != asset;
297 | mathint depositsOtherBefore = deposits(otherAsset, otherIlk);
298 |
299 | address otherAsset2;
300 | require otherAsset2 != asset;
301 | mathint totalDepositsOtherBefore = totalDeposits(otherAsset2);
302 |
303 | require balanceOfBufferBefore + balanceOfConduitBefore <= max_uint256;
304 |
305 | deposit(e, ilk, asset, amount);
306 |
307 | mathint balanceOfBufferAfter = gem.balanceOf(buffer);
308 | mathint balanceOfConduitAfter = gem.balanceOf(currentContract);
309 | mathint depositsAfter = deposits(asset, ilk);
310 | mathint totalDepositsAfter = totalDeposits(asset);
311 |
312 | mathint depositsOtherAfter = deposits(otherAsset, otherIlk);
313 | mathint totalDepositsOtherAfter = totalDeposits(otherAsset2);
314 |
315 | assert balanceOfBufferAfter == balanceOfBufferBefore - amount, "deposit did not decrease balance of buffer by amount";
316 | assert balanceOfConduitAfter == balanceOfConduitBefore + amount, "deposit did not increase balance of conduit by amount";
317 | assert depositsAfter == depositsBefore + amount, "deposit did not increase deposits by amount";
318 | assert totalDepositsAfter == totalDepositsBefore + amount, "deposit did not increase totalDeposits by amount";
319 |
320 | assert depositsOtherAfter == depositsOtherBefore, "other deposits changed unexpectedly";
321 | assert totalDepositsOtherAfter == totalDepositsOtherBefore, "other total deposits changed unexpectedly";
322 | }
323 |
324 | // Verify revert rules on deposit
325 | rule deposit_revert(bytes32 ilk, address asset, uint256 amount) {
326 | env e;
327 |
328 | require asset == gem;
329 |
330 | address buffer = registry.buffers(ilk);
331 | require buffer != currentContract;
332 |
333 | bool canCall = roles.canCall(ilk, e.msg.sender, currentContract, to_bytes4(0xd954863c)); // deposit(bytes32,address,uint256)
334 | mathint balanceOfBuffer = gem.balanceOf(buffer);
335 | mathint allowanceBuffer = gem.allowance(buffer, currentContract);
336 | mathint deposits = deposits(asset, ilk);
337 | mathint totalDeposits = totalDeposits(asset);
338 |
339 | deposit@withrevert(e, ilk, asset, amount);
340 |
341 | bool revert1 = e.msg.value > 0;
342 | bool revert2 = !canCall;
343 | bool revert3 = deposits + amount > max_uint256;
344 | bool revert4 = totalDeposits + amount > max_uint256;
345 | bool revert5 = buffer == 0;
346 | bool revert6 = balanceOfBuffer < to_mathint(amount);
347 | bool revert7 = allowanceBuffer < to_mathint(amount);
348 |
349 | assert lastReverted <=> revert1 || revert2 || revert3 ||
350 | revert4 || revert5 || revert6 ||
351 | revert7 , "Revert rules failed";
352 |
353 | }
354 |
355 | // Verify correct storage changes for non reverting withdraw
356 | rule withdraw(bytes32 ilk, address asset, uint256 maxAmount) {
357 | env e;
358 |
359 | require asset == gem;
360 |
361 | address buffer = registry.buffers(ilk);
362 | require buffer != currentContract;
363 |
364 | mathint balanceOfBufferBefore = gem.balanceOf(buffer);
365 | mathint balanceOfConduitBefore = gem.balanceOf(currentContract);
366 | mathint withdrawableFundsBefore = withdrawableFunds(asset, ilk);
367 | mathint totalWithdrawableFundsBefore = totalWithdrawableFunds(asset);
368 | mathint withdrawalsBefore = withdrawals(asset, ilk);
369 | mathint totalWithdrawalsBefore = totalWithdrawals(asset);
370 |
371 | bytes32 otherIlk;
372 | address otherAsset;
373 | require otherIlk != ilk || otherAsset != asset;
374 | mathint withdrawableFundsOtherBefore = withdrawableFunds(otherAsset, otherIlk);
375 |
376 | address otherAsset2;
377 | require otherAsset2 != asset;
378 | mathint totalWithdrawableFundsOtherBefore = totalWithdrawableFunds(otherAsset2);
379 |
380 | mathint withdrawalsOtherBefore = withdrawals(otherAsset, otherIlk);
381 | mathint totalWithdrawalsOtherBefore = totalWithdrawals(otherAsset2);
382 |
383 | require balanceOfBufferBefore + balanceOfConduitBefore <= max_uint256;
384 |
385 | mathint amount = min(maxAmount, withdrawableFundsBefore);
386 | withdraw(e, ilk, asset, maxAmount);
387 |
388 | mathint balanceOfBufferAfter = gem.balanceOf(buffer);
389 | mathint balanceOfConduitAfter = gem.balanceOf(currentContract);
390 | mathint withdrawableFundsAfter = withdrawableFunds(asset, ilk);
391 | mathint totalWithdrawableFundsAfter = totalWithdrawableFunds(asset);
392 | mathint withdrawalsAfter = withdrawals(asset, ilk);
393 | mathint totalWithdrawalsAfter = totalWithdrawals(asset);
394 |
395 | mathint withdrawableFundsOtherAfter = withdrawableFunds(otherAsset, otherIlk);
396 | mathint totalWithdrawableFundsOtherAfter = totalWithdrawableFunds(otherAsset2);
397 | mathint withdrawalsOtherAfter = withdrawals(otherAsset, otherIlk);
398 | mathint totalWithdrawalsOtherAfter = totalWithdrawals(otherAsset2);
399 |
400 | assert balanceOfBufferAfter == balanceOfBufferBefore + amount, "balance of buffer did not increase by amount";
401 | assert balanceOfConduitAfter == balanceOfConduitBefore - amount, "balance of conduit did not decrease by amount";
402 | assert withdrawableFundsAfter == withdrawableFundsBefore - amount, "withdrawableFunds did not decrease by amount";
403 | assert totalWithdrawableFundsAfter == totalWithdrawableFundsBefore - amount, "totalWithdrawableFunds did not decrease by amount";
404 | assert withdrawalsAfter == withdrawalsBefore + amount, "withdrawals did not increase by amount";
405 | assert totalWithdrawalsAfter == totalWithdrawalsBefore + amount, "totalWithdrawals did not increase by amount";
406 |
407 | assert withdrawableFundsOtherAfter == withdrawableFundsOtherBefore, "other withdrawable funds changed unexpectedly";
408 | assert totalWithdrawableFundsOtherAfter == totalWithdrawableFundsOtherBefore, "other total withdrawable funds changed unexpectedly";
409 | assert withdrawalsOtherAfter == withdrawalsOtherBefore, "other withdrawals changed unexpectedly";
410 | assert totalWithdrawalsOtherAfter == totalWithdrawalsOtherBefore, "other total withdrawals changed unexpectedly";
411 | }
412 |
413 | // Verify revert rules on withdraw
414 | rule withdraw_revert(bytes32 ilk, address asset, uint256 maxAmount) {
415 | env e;
416 |
417 | require asset == gem;
418 |
419 | address buffer = registry.buffers(ilk);
420 | require buffer != currentContract;
421 |
422 | bool canCall = roles.canCall(ilk, e.msg.sender, currentContract, to_bytes4(0xa6fb97d1)); // withdraw(bytes32,address,uint256)
423 | mathint balanceOfConduit = gem.balanceOf(currentContract);
424 | mathint withdrawals = withdrawals(asset, ilk);
425 | mathint totalWithdrawals = totalWithdrawals(asset);
426 | mathint withdrawableFunds = withdrawableFunds(asset, ilk);
427 | mathint totalWithdrawableFunds = totalWithdrawableFunds(asset);
428 |
429 | mathint amount = min(maxAmount, withdrawableFunds);
430 | withdraw@withrevert(e, ilk, asset, maxAmount);
431 |
432 | bool revert1 = e.msg.value > 0;
433 | bool revert2 = !canCall;
434 | bool revert3 = totalWithdrawableFunds < amount;
435 | bool revert4 = withdrawals + amount > max_uint256;
436 | bool revert5 = totalWithdrawals + amount > max_uint256;
437 | bool revert6 = buffer == 0;
438 | bool revert7 = balanceOfConduit < amount;
439 |
440 | assert lastReverted <=> revert1 || revert2 || revert3 ||
441 | revert4 || revert5 || revert6 ||
442 | revert7, "Revert rules failed";
443 | }
444 |
445 | // Verify correct storage changes for non reverting requestFunds
446 | rule requestFunds(bytes32 ilk, address asset, uint256 amount, string info) {
447 | env e;
448 |
449 | require info.length % 32 == 0; // See explanation for that in the header on the top
450 |
451 | // general check before
452 | uint256 numRequestsBefore = getFundRequestsLength();
453 |
454 | // actual (asset,ilk) before
455 | mathint requestedFundsBefore = requestedFunds(asset, ilk);
456 | mathint totalRequestedFundsBefore = totalRequestedFunds(asset);
457 |
458 | // other (asset,ilk) before
459 | bytes32 otherIlk;
460 | address otherAsset;
461 | require otherIlk != ilk || otherAsset != asset;
462 |
463 | address otherAsset2;
464 | require otherAsset2 != asset;
465 |
466 | mathint requestedFundsOtherBefore = requestedFunds(otherAsset, otherIlk);
467 | mathint totalRequestedFundsOtherBefore = totalRequestedFunds(otherAsset2);
468 |
469 | // other request before
470 | uint256 otherIndex;
471 | require otherIndex != numRequestsBefore;
472 |
473 | IArrangerConduit.FundRequest requestOtherBefore = getFundRequest(otherIndex);
474 | bytes32 infoHashOtherBefore = aux.hashString(requestOtherBefore.info);
475 |
476 | requestFunds(e, ilk, asset, amount, info);
477 |
478 | // general check after
479 | uint256 numRequestsAfter = getFundRequestsLength();
480 |
481 | // new request
482 | IArrangerConduit.FundRequest requestAfter = getFundRequest(numRequestsBefore);
483 | bytes32 infoHashAfter = aux.hashString(requestAfter.info);
484 |
485 | // actual (asset,ilk) after
486 | mathint requestedFundsAfter = requestedFunds(asset, ilk);
487 | mathint totalRequestedFundsAfter = totalRequestedFunds(asset);
488 |
489 | // other (asset,ilk) after
490 | mathint requestedFundsOtherAfter = requestedFunds(otherAsset, otherIlk);
491 | mathint totalRequestedFundsOtherAfter = totalRequestedFunds(otherAsset2);
492 |
493 | // other request
494 | IArrangerConduit.FundRequest requestOtherAfter = getFundRequest(otherIndex);
495 | bytes32 infoHashOtherAfter = aux.hashString(requestOtherAfter.info);
496 |
497 | // general asserts
498 | assert to_mathint(numRequestsAfter) == numRequestsBefore + 1, "num request did not increase by 1";
499 |
500 | // asserts on actual (asset,ilk) and actual request
501 | assert requestedFundsAfter == requestedFundsBefore + amount, "requestedFunds did not increase by amount";
502 | assert totalRequestedFundsAfter == totalRequestedFundsBefore + amount, "totalRequestedFunds did not increase by amount";
503 |
504 | assert requestAfter.status == IArrangerConduit.StatusEnum.PENDING
505 | && requestAfter.asset == asset
506 | && requestAfter.ilk == ilk
507 | && requestAfter.amountRequested == amount
508 | && requestAfter.amountFilled == 0
509 | && infoHashAfter == aux.hashString(info),
510 | "the new request params are not as expected";
511 |
512 | // asserts on other request
513 | assert requestOtherAfter.status == requestOtherBefore.status
514 | && requestOtherAfter.asset == requestOtherBefore.asset
515 | && requestOtherAfter.ilk == requestOtherBefore.ilk
516 | && requestOtherAfter.amountRequested == requestOtherBefore.amountRequested
517 | && requestOtherAfter.amountFilled == requestOtherBefore.amountFilled
518 | && infoHashOtherAfter == infoHashOtherBefore,
519 | "unrelated request params are not as before";
520 |
521 | // asserts on other (asset,ilk)
522 | assert requestedFundsOtherAfter == requestedFundsOtherBefore, "other requested funds changed unexpectedly";
523 | assert totalRequestedFundsOtherAfter == totalRequestedFundsOtherBefore, "other total requested funds changed unexpectedly";
524 | }
525 |
526 | // Verify revert rules on requestFunds
527 | rule requestFunds_revert(bytes32 ilk, address asset, uint256 amount, string info) {
528 | env e;
529 |
530 | bool canCall = roles.canCall(ilk, e.msg.sender, currentContract, to_bytes4(0xd2543ccb)); // requestFunds(bytes32,address,uint256,string)
531 | mathint requestedFunds = requestedFunds(asset, ilk);
532 | mathint totalRequestedFunds = totalRequestedFunds(asset);
533 | mathint numRequests = getFundRequestsLength();
534 |
535 | requestFunds@withrevert(e, ilk, asset, amount, info);
536 |
537 | bool revert1 = e.msg.value > 0;
538 | bool revert2 = !canCall;
539 | bool revert3 = requestedFunds + amount > max_uint256;
540 | bool revert4 = totalRequestedFunds + amount > max_uint256;
541 |
542 | assert lastReverted <=> revert1 || revert2 || revert3 ||
543 | revert4, "Revert rules failed";
544 | }
545 |
546 | // Verify correct storage changes for non reverting requestFunds
547 | rule cancelFundRequest(uint256 fundRequestId) {
548 | env e;
549 |
550 | // general check before
551 | mathint numRequestsBefore = getFundRequestsLength();
552 |
553 | // actual (asset,ilk) for actual request Id before
554 | IArrangerConduit.FundRequest requestBefore = getFundRequest(fundRequestId);
555 | bytes32 infoHashBefore = aux.hashString(requestBefore.info);
556 |
557 | mathint requestedFundsBefore = requestedFunds(requestBefore.asset, requestBefore.ilk);
558 | mathint totalRequestedFundsBefore = totalRequestedFunds(requestBefore.asset);
559 |
560 | // other (asset,ilk) for actual request Id before
561 | bytes32 otherIlk;
562 | address otherAsset;
563 | require otherIlk != requestBefore.ilk || otherAsset != requestBefore.asset;
564 | mathint requestedFundsOtherBefore = requestedFunds(otherAsset, otherIlk);
565 |
566 | address otherAsset2;
567 | require otherAsset2 != requestBefore.asset;
568 | mathint totalRequestedFundsOtherBefore = totalRequestedFunds(otherAsset2);
569 |
570 | // other request Id before
571 | uint256 otherIndex;
572 | require otherIndex != fundRequestId;
573 |
574 | IArrangerConduit.FundRequest requestOtherBefore = getFundRequest(otherIndex);
575 | bytes32 infoHashOtherBefore = aux.hashString(requestOtherBefore.info);
576 |
577 | cancelFundRequest(e, fundRequestId);
578 |
579 | // general check after
580 | mathint numRequestsAfter = getFundRequestsLength();
581 |
582 | // actual (asset,ilk) for actual request Id after
583 | IArrangerConduit.FundRequest requestAfter = getFundRequest(fundRequestId);
584 | bytes32 infoHashAfter = aux.hashString(requestAfter.info);
585 |
586 | mathint requestedFundsAfter = requestedFunds(requestBefore.asset, requestBefore.ilk);
587 | mathint totalRequestedFundsAfter= totalRequestedFunds(requestBefore.asset);
588 |
589 | // other (asset,ilk) for actual request Id before
590 | mathint requestedFundsOtherAfter = requestedFunds(otherAsset, otherIlk);
591 | mathint totalRequestedFundsOtherAfter = totalRequestedFunds(otherAsset2);
592 |
593 | // other request Id after
594 | IArrangerConduit.FundRequest requestOtherAfter = getFundRequest(otherIndex);
595 | bytes32 infoHashOtherAfter = aux.hashString(requestOtherBefore.info);
596 |
597 | // general asserts
598 | assert numRequestsAfter == numRequestsBefore, "num requests changed";
599 |
600 | // asserts on actual (asset,ilk) for actual request Id
601 | assert requestAfter.status == IArrangerConduit.StatusEnum.CANCELLED, "cancelFundRequest did not change status to CANCELLED";
602 | assert requestAfter.asset == requestBefore.asset
603 | && requestAfter.ilk == requestBefore.ilk
604 | && requestAfter.amountRequested == requestBefore.amountRequested
605 | && requestAfter.amountFilled == requestBefore.amountFilled
606 | && infoHashAfter == infoHashBefore,
607 | "unrelated request params not as before";
608 |
609 | assert requestedFundsAfter == requestedFundsBefore - requestBefore.amountRequested, "requestedFunds did not decrease by amountRequested";
610 | assert totalRequestedFundsAfter == totalRequestedFundsBefore - requestBefore.amountRequested, "totalRequestedFunds did not decrease by amountRequested";
611 |
612 | // asserts on actual (asset,ilk) for another request Id
613 | assert requestOtherAfter.status == requestOtherBefore.status
614 | && requestOtherAfter.asset == requestOtherBefore.asset
615 | && requestOtherAfter.ilk == requestOtherBefore.ilk
616 | && requestOtherAfter.amountRequested == requestOtherBefore.amountRequested
617 | && requestOtherAfter.amountFilled == requestOtherBefore.amountFilled
618 | && infoHashOtherAfter == infoHashOtherBefore,
619 | "unrelated request params not as before for another request Id";
620 |
621 | // asserts on other (asset,ilk) for actual request Id
622 | assert requestedFundsOtherAfter == requestedFundsOtherBefore, "other requested funds changed unexpectedly";
623 | assert totalRequestedFundsOtherAfter == totalRequestedFundsOtherBefore, "other total requested funds changed unexpectedly";
624 | }
625 |
626 | // Verify revert rules on cancelFundRequest
627 | rule cancelFundRequest_revert(uint256 fundRequestId) {
628 | env e;
629 |
630 | IArrangerConduit.FundRequest request = getFundRequest(fundRequestId);
631 |
632 | bool canCall = roles.canCall(request.ilk, e.msg.sender, currentContract, to_bytes4(0x933d9476)); // cancelFundRequest(uint256)
633 | mathint requestedFunds = requestedFunds(request.asset, request.ilk);
634 | mathint totalRequestedFunds = totalRequestedFunds(request.asset);
635 |
636 | cancelFundRequest@withrevert(e, fundRequestId);
637 |
638 | bool revert1 = e.msg.value > 0;
639 | bool revert2 = !canCall;
640 | bool revert3 = request.status != IArrangerConduit.StatusEnum.PENDING;
641 | bool revert4 = requestedFunds < to_mathint(request.amountRequested);
642 | bool revert5 = totalRequestedFunds < to_mathint(request.amountRequested);
643 |
644 | assert lastReverted <=> revert1 || revert2 || revert3 ||
645 | revert4 || revert5, "Revert rules failed";
646 |
647 | }
648 |
649 | // Verify correct storage changes for non reverting requestFunds
650 | rule drawFunds(address asset, address destination, uint256 amount) {
651 | env e;
652 |
653 | require asset == gem;
654 |
655 | mathint balanceOfConduitBefore = gem.balanceOf(currentContract);
656 | mathint balanceOfDestinationBefore = gem.balanceOf(destination);
657 |
658 | require currentContract == destination || balanceOfConduitBefore + balanceOfDestinationBefore <= max_uint256;
659 |
660 | drawFunds(e, asset, destination, amount);
661 |
662 | mathint balanceOfConduitAfter = gem.balanceOf(currentContract);
663 | mathint balanceOfDestinationAfter = gem.balanceOf(destination);
664 |
665 | assert currentContract != destination => balanceOfConduitAfter == balanceOfConduitBefore - amount, "balance of conduit did not decrease by amount";
666 | assert currentContract != destination => balanceOfDestinationAfter == balanceOfDestinationBefore + amount, "balance of destination did not increase by amount";
667 | assert currentContract == destination => balanceOfConduitAfter == balanceOfConduitBefore, "balance of conduit changed";
668 | }
669 |
670 | // Verify revert rules on drawFunds
671 | rule drawFunds_revert(address asset, address destination, uint256 amount) {
672 | env e;
673 |
674 | require asset == gem;
675 |
676 | mathint balanceOfConduit = gem.balanceOf(currentContract);
677 | mathint totalWithdrawableFunds = totalWithdrawableFunds(asset);
678 |
679 | bool isBroker = isBroker(destination, asset);
680 | address arranger = arranger();
681 |
682 | drawFunds@withrevert(e, asset, destination, amount);
683 |
684 | bool revert1 = e.msg.value > 0;
685 | bool revert2 = arranger != e.msg.sender;
686 | bool revert3 = totalWithdrawableFunds > balanceOfConduit;
687 | bool revert4 = to_mathint(amount) > balanceOfConduit - totalWithdrawableFunds;
688 | bool revert5 = !isBroker;
689 |
690 | assert lastReverted <=> revert1 || revert2 || revert3 ||
691 | revert4 || revert5, "Revert rules failed";
692 |
693 | }
694 |
695 | // Verify correct storage changes for non reverting returnFunds
696 | rule returnFunds(uint256 fundRequestId, uint256 returnAmount) {
697 | env e;
698 |
699 | // general check before
700 | mathint numRequestsBefore = getFundRequestsLength();
701 |
702 | // actual (asset,ilk) for actual request Id before
703 | IArrangerConduit.FundRequest requestBefore = getFundRequest(fundRequestId);
704 | bytes32 infoHashBefore = aux.hashString(requestBefore.info);
705 |
706 | mathint requestedFundsBefore = requestedFunds(requestBefore.asset, requestBefore.ilk);
707 | mathint totalRequestedFundsBefore = totalRequestedFunds(requestBefore.asset);
708 | mathint withdrawableFundsBefore = withdrawableFunds(requestBefore.asset, requestBefore.ilk);
709 | mathint totalWithdrawableFundsBefore = totalWithdrawableFunds(requestBefore.asset);
710 |
711 | // other (asset,ilk) for actual request Id before
712 | bytes32 otherIlk;
713 | address otherAsset;
714 | require otherIlk != requestBefore.ilk || otherAsset != requestBefore.asset;
715 |
716 | address otherAsset2;
717 | require otherAsset2 != requestBefore.asset;
718 |
719 | mathint requestedFundsOtherBefore = requestedFunds(otherAsset, otherIlk);
720 | mathint totalRequestedFundsOtherBefore = totalRequestedFunds(otherAsset2);
721 | mathint withdrawableFundsOtherBefore = withdrawableFunds(otherAsset, otherIlk);
722 | mathint totalWithdrawableFundsOtherBefore = totalWithdrawableFunds(otherAsset2);
723 |
724 | // other request Id before
725 | uint256 otherIndex;
726 | require otherIndex != fundRequestId;
727 |
728 | IArrangerConduit.FundRequest requestOtherBefore = getFundRequest(otherIndex);
729 | bytes32 infoHashOtherBefore = aux.hashString(requestOtherBefore.info);
730 |
731 | returnFunds(e, fundRequestId, returnAmount);
732 |
733 | // general check after
734 | mathint numRequestsAfter = getFundRequestsLength();
735 |
736 | // actual (asset,ilk) for actual request Id after
737 | IArrangerConduit.FundRequest requestAfter = getFundRequest(fundRequestId);
738 | bytes32 infoHashAfter = aux.hashString(requestAfter.info);
739 |
740 | mathint requestedFundsAfter = requestedFunds(requestBefore.asset, requestBefore.ilk);
741 | mathint totalRequestedFundsAfter= totalRequestedFunds(requestBefore.asset);
742 | mathint withdrawableFundsAfter = withdrawableFunds(requestBefore.asset, requestBefore.ilk);
743 | mathint totalWithdrawableFundsAfter = totalWithdrawableFunds(requestBefore.asset);
744 |
745 | // other (asset,ilk) for actual request Id after
746 | mathint requestedFundsOtherAfter = requestedFunds(otherAsset, otherIlk);
747 | mathint totalRequestedFundsOtherAfter= totalRequestedFunds(otherAsset2);
748 | mathint withdrawableFundsOtherAfter = withdrawableFunds(otherAsset, otherIlk);
749 | mathint totalWithdrawableFundsOtherAfter = totalWithdrawableFunds(otherAsset2);
750 |
751 | // other request Id after
752 | IArrangerConduit.FundRequest requestOtherAfter = getFundRequest(otherIndex);
753 | bytes32 infoHashOtherAfter = aux.hashString(requestOtherAfter.info);
754 |
755 | // general asserts
756 | assert numRequestsAfter == numRequestsBefore, "num requests changed";
757 |
758 | // asserts on actual (asset,ilk) for actual request Id
759 | assert requestAfter.status == IArrangerConduit.StatusEnum.COMPLETED, "returnFunds did not change status to COMPLETED";
760 | assert requestAfter.amountFilled == returnAmount, "returnFunds did not change amountFilled to returnAmount";
761 | assert requestAfter.asset == requestBefore.asset
762 | && requestAfter.ilk == requestBefore.ilk
763 | && requestAfter.amountRequested == requestBefore.amountRequested
764 | && infoHashAfter == infoHashBefore,
765 | "request params not as before";
766 |
767 | assert requestedFundsAfter == requestedFundsBefore - requestBefore.amountRequested, "requestedFunds did not decrease by amountRequested";
768 | assert totalRequestedFundsAfter == totalRequestedFundsBefore - requestBefore.amountRequested, "totalRequestedFunds did not decrease by amountRequested";
769 | assert withdrawableFundsAfter == withdrawableFundsBefore + returnAmount, "withdrawableFunds did not increase by returnAmount";
770 | assert totalWithdrawableFundsAfter == totalWithdrawableFundsBefore + returnAmount, "totalWithdrawableFunds did not increase by returnAmount";
771 |
772 | // asserts on other (asset,ilk) for actual request Id
773 | assert requestedFundsOtherAfter == requestedFundsOtherBefore, "other requested funds changed unexpectedly";
774 | assert totalRequestedFundsOtherAfter == totalRequestedFundsOtherBefore, "other total requested funds changed unexpectedly";
775 | assert withdrawableFundsOtherAfter == withdrawableFundsOtherBefore, "other withdrawable funds changed unexpectedly";
776 | assert totalWithdrawableFundsOtherAfter == totalWithdrawableFundsOtherBefore, "other total withdrawable funds changed unexpectedly";
777 |
778 | // asserts on other request Id
779 | assert requestOtherAfter.status == requestOtherBefore.status
780 | && requestOtherAfter.asset == requestOtherBefore.asset
781 | && requestOtherAfter.ilk == requestOtherBefore.ilk
782 | && requestOtherAfter.amountRequested == requestOtherBefore.amountRequested
783 | && requestOtherAfter.amountFilled == requestOtherBefore.amountFilled
784 | && infoHashOtherAfter == infoHashOtherBefore,
785 | "other request Id request params not as before";
786 | }
787 |
788 | // Verify revert rules on returnFunds
789 | rule returnFunds_revert(uint256 fundRequestId, uint256 returnAmount) {
790 | env e;
791 |
792 | address arranger = arranger();
793 |
794 | IArrangerConduit.FundRequest request = getFundRequest(fundRequestId);
795 |
796 | mathint balanceOfConduit = gem.balanceOf(currentContract);
797 | mathint withdrawableFunds = withdrawableFunds(request.asset, request.ilk);
798 | mathint totalWithdrawableFunds = totalWithdrawableFunds(request.asset);
799 | mathint requestedFunds = requestedFunds(request.asset, request.ilk);
800 | mathint totalRequestedFunds = totalRequestedFunds(request.asset);
801 |
802 | returnFunds@withrevert(e, fundRequestId, returnAmount);
803 |
804 | bool revert1 = e.msg.value > 0;
805 | bool revert2 = arranger != e.msg.sender;
806 | bool revert3 = request.status != IArrangerConduit.StatusEnum.PENDING;
807 | bool revert4 = totalWithdrawableFunds > balanceOfConduit;
808 | bool revert5 = balanceOfConduit - totalWithdrawableFunds < to_mathint(returnAmount);
809 | bool revert6 = withdrawableFunds + to_mathint(returnAmount) > max_uint256;
810 | bool revert7 = totalWithdrawableFunds + to_mathint(returnAmount) > max_uint256;
811 | bool revert8 = requestedFunds < to_mathint(request.amountRequested);
812 | bool revert9 = totalRequestedFunds < to_mathint(request.amountRequested);
813 |
814 | assert lastReverted <=> revert1 || revert2 || revert3 ||
815 | revert4 || revert5 || revert6 ||
816 | revert7 || revert8 || revert9,
817 | "Revert rules failed";
818 | }
819 |
820 | // Verify variables change together
821 | rule changeTogether(method f) {
822 | env e;
823 |
824 | bytes32 anyIlk;
825 | address anyAsset;
826 |
827 | mathint depositsBefore = deposits(anyAsset, anyIlk);
828 | mathint requestedFundsBefore = requestedFunds(anyAsset, anyIlk);
829 | mathint withdrawableFundsBefore = withdrawableFunds(anyAsset, anyIlk);
830 | mathint withdrawalsBefore = withdrawals(anyAsset, anyIlk);
831 | mathint totalDepositsBefore = totalDeposits(anyAsset);
832 | mathint totalRequestedFundsBefore = totalRequestedFunds(anyAsset);
833 | mathint totalWithdrawableFundsBefore = totalWithdrawableFunds(anyAsset);
834 | mathint totalWithdrawalsBefore = totalWithdrawals(anyAsset);
835 |
836 | calldataarg args;
837 | f(e, args);
838 |
839 | mathint depositsDiff = deposits(anyAsset, anyIlk) - depositsBefore;
840 | mathint requestedFundsDiff = requestedFunds(anyAsset, anyIlk) - requestedFundsBefore;
841 | mathint withdrawableFundsDiff = withdrawableFunds(anyAsset, anyIlk) - withdrawableFundsBefore;
842 | mathint withdrawalsDiff = withdrawals(anyAsset, anyIlk) - withdrawalsBefore;
843 | mathint totalDepositsDiff = totalDeposits(anyAsset) - totalDepositsBefore;
844 | mathint totalRequestedFundsDiff= totalRequestedFunds(anyAsset) - totalRequestedFundsBefore;
845 | mathint totalWithdrawableFundsDiff = totalWithdrawableFunds(anyAsset) - totalWithdrawableFundsBefore;
846 | mathint totalWithdrawalsDiff = totalWithdrawals(anyAsset) - totalWithdrawalsBefore;
847 |
848 | assert depositsDiff != 0 => depositsDiff == totalDepositsDiff, "deposits and totalDeposit diff differed";
849 | assert requestedFundsDiff != 0 => requestedFundsDiff == totalRequestedFundsDiff, "requestedFunds and totaRequestedFundsdiff differed";
850 | assert withdrawableFundsDiff != 0 => withdrawableFundsDiff == totalWithdrawableFundsDiff, "withdrawableFunds and totalWithdrawableFunds diff differed";
851 | assert withdrawalsDiff != 0 => withdrawalsDiff == totalWithdrawalsDiff, "withdrawals and totalWithdrawals diff differed";
852 | }
853 |
854 | // Verify request status change as allowed
855 | rule statusChanges(method f) {
856 | env e;
857 | uint256 anyIndex;
858 |
859 | IArrangerConduit.FundRequest requestBefore = getFundRequest(anyIndex);
860 |
861 | calldataarg args;
862 | f(e, args);
863 |
864 | IArrangerConduit.FundRequest requestAfter = getFundRequest(anyIndex);
865 | bool statusSame = requestBefore.status == requestAfter.status;
866 |
867 | assert requestBefore.status == IArrangerConduit.StatusEnum.UNINITIALIZED => statusSame || requestAfter.status == IArrangerConduit.StatusEnum.PENDING, "status changed from UNINITIALIZED to something other than PENDING";
868 | assert requestBefore.status == IArrangerConduit.StatusEnum.PENDING => statusSame || requestAfter.status == IArrangerConduit.StatusEnum.CANCELLED || requestAfter.status == IArrangerConduit.StatusEnum.COMPLETED, "status changed from PENDING to something other than CANCELLED or COMPLETED";
869 | assert requestBefore.status == IArrangerConduit.StatusEnum.CANCELLED => statusSame, "status changed from CANCELLED";
870 | assert requestBefore.status == IArrangerConduit.StatusEnum.COMPLETED => statusSame, "status changed from COMPLETED";
871 | }
872 |
--------------------------------------------------------------------------------
/certora/Auxiliar.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 | pragma solidity ^0.8.13;
3 |
4 | contract Auxiliar {
5 | function hashString(string memory data) external pure returns (bytes32) {
6 | return keccak256(abi.encodePacked(data));
7 | }
8 | }
9 |
10 |
--------------------------------------------------------------------------------
/foundry.toml:
--------------------------------------------------------------------------------
1 | [profile.default]
2 | src = "src"
3 | out = "out"
4 | libs = ["lib"]
5 | verbosity = 3
6 | solc = "0.8.16"
7 | optimizer = true
8 | optimizer_runs = 200
9 |
10 | [fuzz]
11 | runs = 5_000
12 |
13 | [profile.ci.fuzz]
14 | runs = 250_000
15 |
16 | [invariant]
17 | runs = 50 # The number of calls to make in the invariant tests
18 | depth = 100 # The number of times to run the invariant tests
19 | call_override = false # Override calls
20 | fail_on_revert = true # Fail the test if the contract reverts
21 |
22 | [profile.unbounded.invariant]
23 | fail_on_revert = false # Fail the test if the contract reverts
24 |
25 | [profile.ci.invariant]
26 | optimizer = true
27 | runs = 250 # The number of times to run the invariant tests
28 | depth = 250 # The number of calls to make in the invariant tests
29 |
30 | [profile.unbounded-ci.invariant]
31 | optimizer = true
32 | runs = 250 # The number of times to run the invariant tests
33 | depth = 250 # The number of calls to make in the invariant tests
34 | fail_on_revert = false # Fail the test if the contract reverts
35 |
36 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config
37 |
--------------------------------------------------------------------------------
/remappings.txt:
--------------------------------------------------------------------------------
1 | dss-allocator/=lib/dss-allocator/src/
2 | upgradeable-proxy/=lib/upgradeable-proxy/src/
3 |
--------------------------------------------------------------------------------
/src/ArrangerConduit.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 | pragma solidity ^0.8.13;
3 |
4 | import { UpgradeableProxied } from "upgradeable-proxy/UpgradeableProxied.sol";
5 |
6 | import { IArrangerConduit } from "./interfaces/IArrangerConduit.sol";
7 |
8 | interface IERC20Like {
9 | function balanceOf(address) external view returns (uint256);
10 | function transfer(address, uint256) external;
11 | function transferFrom(address, address, uint256) external;
12 | }
13 |
14 | interface RolesLike {
15 | function canCall(bytes32, address, address, bytes4) external view returns (bool);
16 | }
17 |
18 | interface RegistryLike {
19 | function buffers(bytes32 ilk) external view returns (address buffer);
20 | }
21 |
22 | contract ArrangerConduit is UpgradeableProxied, IArrangerConduit {
23 |
24 | /**********************************************************************************************/
25 | /*** Declarations and Constructor ***/
26 | /**********************************************************************************************/
27 |
28 | FundRequest[] internal fundRequests;
29 |
30 | address public override arranger;
31 | address public override registry;
32 | address public override roles;
33 |
34 | mapping(address => uint256) public override totalDeposits;
35 | mapping(address => uint256) public override totalRequestedFunds;
36 | mapping(address => uint256) public override totalWithdrawableFunds;
37 | mapping(address => uint256) public override totalWithdrawals;
38 |
39 | mapping(address => mapping(address => bool)) public override isBroker;
40 |
41 | mapping(address => mapping(bytes32 => uint256)) public override deposits;
42 | mapping(address => mapping(bytes32 => uint256)) public override requestedFunds;
43 | mapping(address => mapping(bytes32 => uint256)) public override withdrawableFunds;
44 | mapping(address => mapping(bytes32 => uint256)) public override withdrawals;
45 |
46 | /**********************************************************************************************/
47 | /*** Modifiers ***/
48 | /**********************************************************************************************/
49 |
50 | modifier auth {
51 | require(wards[msg.sender] == 1, "ArrangerConduit/not-authorized");
52 | _;
53 | }
54 |
55 | modifier ilkAuth(bytes32 ilk) {
56 | _checkAuth(ilk);
57 | _;
58 | }
59 |
60 | modifier isArranger {
61 | require(msg.sender == arranger, "ArrangerConduit/not-arranger");
62 | _;
63 | }
64 |
65 | /**********************************************************************************************/
66 | /*** Administrative Functions ***/
67 | /**********************************************************************************************/
68 |
69 | function file(bytes32 what, address data) external auth {
70 | if (what == "arranger") arranger = data;
71 | else if (what == "registry") registry = data;
72 | else if (what == "roles") roles = data;
73 | else revert("ArrangerConduit/file-unrecognized-param");
74 | emit File(what, data);
75 | }
76 |
77 | function setBroker(address broker, address asset, bool valid) external auth {
78 | isBroker[broker][asset] = valid;
79 | emit SetBroker(broker, asset, valid);
80 | }
81 |
82 | /**********************************************************************************************/
83 | /*** Operator Functions ***/
84 | /**********************************************************************************************/
85 |
86 | function deposit(bytes32 ilk, address asset, uint256 amount) external override ilkAuth(ilk) {
87 | deposits[asset][ilk] += amount;
88 | totalDeposits[asset] += amount;
89 |
90 | address source = RegistryLike(registry).buffers(ilk);
91 |
92 | require(source != address(0), "ArrangerConduit/no-buffer-registered");
93 |
94 | IERC20Like(asset).transferFrom(source, address(this), amount);
95 |
96 | emit Deposit(ilk, asset, source, amount);
97 | }
98 |
99 | function withdraw(bytes32 ilk, address asset, uint256 maxAmount)
100 | external override ilkAuth(ilk) returns (uint256 amount)
101 | {
102 | uint256 withdrawableFunds_ = withdrawableFunds[asset][ilk];
103 |
104 | amount = maxAmount > withdrawableFunds_ ? withdrawableFunds_ : maxAmount;
105 |
106 | withdrawableFunds[asset][ilk] -= amount;
107 | totalWithdrawableFunds[asset] -= amount;
108 |
109 | withdrawals[asset][ilk] += amount;
110 | totalWithdrawals[asset] += amount;
111 |
112 | address destination = RegistryLike(registry).buffers(ilk);
113 |
114 | require(destination != address(0), "ArrangerConduit/no-buffer-registered");
115 |
116 | IERC20Like(asset).transfer(destination, amount);
117 |
118 | emit Withdraw(ilk, asset, destination, amount);
119 | }
120 |
121 | function requestFunds(bytes32 ilk, address asset, uint256 amount, string memory info)
122 | external override ilkAuth(ilk) returns (uint256 fundRequestId)
123 | {
124 | fundRequestId = fundRequests.length; // Current length will be the next index
125 |
126 | fundRequests.push(FundRequest({
127 | status: StatusEnum.PENDING,
128 | asset: asset,
129 | ilk: ilk,
130 | amountRequested: amount,
131 | amountFilled: 0,
132 | info: info
133 | }));
134 |
135 | requestedFunds[asset][ilk] += amount;
136 | totalRequestedFunds[asset] += amount;
137 |
138 | emit RequestFunds(ilk, asset, fundRequestId, amount, info);
139 | }
140 |
141 | function cancelFundRequest(uint256 fundRequestId) external override {
142 | FundRequest memory fundRequest = fundRequests[fundRequestId];
143 |
144 | require(fundRequest.status == StatusEnum.PENDING, "ArrangerConduit/invalid-status");
145 |
146 | address asset = fundRequest.asset;
147 | bytes32 ilk = fundRequest.ilk;
148 |
149 | _checkAuth(ilk);
150 |
151 | uint256 amountRequested = fundRequest.amountRequested;
152 |
153 | fundRequests[fundRequestId].status = StatusEnum.CANCELLED;
154 |
155 | requestedFunds[asset][ilk] -= amountRequested;
156 | totalRequestedFunds[asset] -= amountRequested;
157 |
158 | emit CancelFundRequest(fundRequestId);
159 | }
160 |
161 | /**********************************************************************************************/
162 | /*** Fund Manager Functions ***/
163 | /**********************************************************************************************/
164 |
165 | function drawFunds(
166 | address asset,
167 | address destination,
168 | uint256 amount
169 | )
170 | external override isArranger
171 | {
172 | require(amount <= availableFunds(asset), "ArrangerConduit/insufficient-funds");
173 | require(isBroker[destination][asset], "ArrangerConduit/invalid-broker");
174 |
175 | IERC20Like(asset).transfer(destination, amount);
176 |
177 | emit DrawFunds(asset, destination, amount);
178 | }
179 |
180 | function returnFunds(uint256 fundRequestId, uint256 returnAmount)
181 | external override isArranger
182 | {
183 | FundRequest storage fundRequest = fundRequests[fundRequestId];
184 |
185 | address asset = fundRequest.asset;
186 |
187 | require(fundRequest.status == StatusEnum.PENDING, "ArrangerConduit/invalid-status");
188 | require(returnAmount <= availableFunds(asset), "ArrangerConduit/insufficient-funds");
189 |
190 | bytes32 ilk = fundRequest.ilk;
191 |
192 | withdrawableFunds[asset][ilk] += returnAmount;
193 | totalWithdrawableFunds[asset] += returnAmount;
194 |
195 | uint256 amountRequested = fundRequest.amountRequested;
196 |
197 | requestedFunds[asset][ilk] -= amountRequested;
198 | totalRequestedFunds[asset] -= amountRequested;
199 |
200 | fundRequest.amountFilled = returnAmount;
201 |
202 | fundRequest.status = StatusEnum.COMPLETED;
203 |
204 | emit ReturnFunds(ilk, asset, fundRequestId, amountRequested, returnAmount);
205 | }
206 |
207 | /**********************************************************************************************/
208 | /*** View Functions ***/
209 | /**********************************************************************************************/
210 |
211 | function availableFunds(address asset) public view override returns (uint256 availableFunds_) {
212 | availableFunds_
213 | = IERC20Like(asset).balanceOf(address(this)) - totalWithdrawableFunds[asset];
214 | }
215 |
216 | function getFundRequest(uint256 fundRequestId)
217 | external override view returns (FundRequest memory fundRequest)
218 | {
219 | fundRequest = fundRequests[fundRequestId];
220 | }
221 |
222 | function getFundRequestsLength() external override view returns (uint256 fundRequestsLength) {
223 | fundRequestsLength = fundRequests.length;
224 | }
225 |
226 | function isCancelable(uint256 fundRequestId)
227 | external override view returns (bool isCancelable_)
228 | {
229 | isCancelable_ = fundRequests[fundRequestId].status == StatusEnum.PENDING;
230 | }
231 |
232 | function maxDeposit(bytes32, address)
233 | external override pure returns (uint256 maxDeposit_)
234 | {
235 | maxDeposit_ = type(uint256).max;
236 | }
237 |
238 | function maxWithdraw(bytes32 ilk, address asset)
239 | external override view returns (uint256 maxWithdraw_)
240 | {
241 | maxWithdraw_ = withdrawableFunds[asset][ilk];
242 | }
243 |
244 | /**********************************************************************************************/
245 | /*** Internal Functions ***/
246 | /**********************************************************************************************/
247 |
248 | function _checkAuth(bytes32 ilk) internal view {
249 | require(
250 | RolesLike(roles).canCall(ilk, msg.sender, address(this), msg.sig),
251 | "ArrangerConduit/not-authorized"
252 | );
253 | }
254 |
255 | }
256 |
--------------------------------------------------------------------------------
/src/interfaces/IArrangerConduit.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 | pragma solidity ^0.8.13;
3 |
4 | import { IAllocatorConduit } from "dss-allocator/IAllocatorConduit.sol";
5 |
6 | /**
7 | * @title IArrangerConduit
8 | * @dev Conduits are to be used to manage positions for multiple Allocators.
9 | * After funds are deposited into a Conduit, they can be deployed by Arrangers to earn
10 | * yield. When Allocators want funds back, they can request funds from the Arrangers and
11 | * then withdraw once liquidity is available.
12 | */
13 | interface IArrangerConduit is IAllocatorConduit {
14 |
15 | /**********************************************************************************************/
16 | /*** Administrative Events ***/
17 | /**********************************************************************************************/
18 |
19 | /**
20 | * @dev Event emitted when a value is changed by an admin.
21 | * @param what The identifier of the value changed.
22 | * @param data The new value of the identifier.
23 | */
24 | event File(bytes32 indexed what, address data);
25 |
26 | /**
27 | * @dev Event emitted when a broker is added or removed from the whitelist.
28 | * @param broker The address of the broker.
29 | * @param asset The address of the asset.
30 | * @param valid Boolean value indicating if the broker is whitelisted or not.
31 | */
32 | event SetBroker(address indexed broker, address indexed asset, bool valid);
33 |
34 | /**********************************************************************************************/
35 | /*** Fund Events ***/
36 | /**********************************************************************************************/
37 |
38 | /**
39 | * @dev Event emitted when a fund request is cancelled.
40 | * @param fundRequestId The ID of the cancelled fund request.
41 | */
42 | event CancelFundRequest(uint256 fundRequestId);
43 |
44 | /**
45 | * @dev Event emitted when funds are drawn from the Conduit by the Arranger.
46 | * @param asset The address of the asset to be withdrawn.
47 | * @param destination The address to transfer the funds to.
48 | * @param amount The amount of asset to be withdrawn.
49 | */
50 | event DrawFunds(address indexed asset, address indexed destination, uint256 amount);
51 |
52 | /**
53 | * @dev Event emitted when a fund request is made.
54 | * @param ilk The unique identifier of the ilk.
55 | * @param asset The address of the asset to be withdrawn.
56 | * @param fundRequestId The ID of the fund request.
57 | * @param amount The amount of asset to be withdrawn.
58 | * @param info Arbitrary string to provide additional info to the Arranger.
59 | */
60 | event RequestFunds(
61 | bytes32 indexed ilk,
62 | address indexed asset,
63 | uint256 fundRequestId,
64 | uint256 amount,
65 | string info
66 | );
67 |
68 | /**
69 | * @dev Event emitted when an Arranger returns funds to the Conduit to fill a fund request.
70 | * @param ilk The unique identifier of the ilk.
71 | * @param asset The address of the asset to be withdrawn.
72 | * @param fundRequestId The ID of the fund request.
73 | * @param amountRequested The amount of asset that was requested by the ilk to be withdrawn.
74 | * @param returnAmount The resulting amount that was returned by the Arranger.
75 | */
76 | event ReturnFunds(
77 | bytes32 indexed ilk,
78 | address indexed asset,
79 | uint256 fundRequestId,
80 | uint256 amountRequested,
81 | uint256 returnAmount
82 | );
83 |
84 | /**********************************************************************************************/
85 | /*** Data Types ***/
86 | /**********************************************************************************************/
87 |
88 | /**
89 | * @dev Struct representing a fund request.
90 | * @param status The current status of the fund request.
91 | * @param asset The address of the asset requested in the fund request.
92 | * @param ilk The unique identifier of the ilk.
93 | * @param amountRequested The amount of asset requested in the fund request.
94 | * @param amountFilled The amount of asset filled in the fund request.
95 | * @param info Arbitrary string to provide additional info to the Arranger.
96 | */
97 | struct FundRequest {
98 | StatusEnum status;
99 | address asset;
100 | bytes32 ilk;
101 | uint256 amountRequested;
102 | uint256 amountFilled;
103 | string info;
104 | }
105 |
106 | /**
107 | * @dev Enum representing the status of a fund request.
108 | * @notice PENDING - Null state before the fund request has been made.
109 | * @notice PENDING - The fund request has been made, but not yet processed.
110 | * @notice CANCELLED - The fund request has been cancelled by the ilk.
111 | * @notice COMPLETED - The fund request has been fully processed and completed.
112 | */
113 | enum StatusEnum {
114 | UNINITIALIZED,
115 | PENDING,
116 | CANCELLED,
117 | COMPLETED
118 | }
119 |
120 | /**********************************************************************************************/
121 | /*** Storage Variables ***/
122 | /**********************************************************************************************/
123 |
124 | /**
125 | * @dev Returns the arranger address.
126 | * @return arranger_ The address of the arranger.
127 | */
128 | function arranger() external view returns (address arranger_);
129 |
130 | /**
131 | * @dev Returns the AllocationRegistry address.
132 | * @return registry_ The address of the registry contract.
133 | */
134 | function registry() external view returns (address registry_);
135 |
136 | /**
137 | * @dev Returns the roles address.
138 | * @return roles_ The address of the roles.
139 | */
140 | function roles() external view returns (address roles_);
141 |
142 | /**
143 | * @dev Returns the total deposits for a given asset.
144 | * @param asset The address of the asset.
145 | * @return totalDeposits_ The total deposits held in the asset.
146 | */
147 | function totalDeposits(address asset) external view returns (uint256 totalDeposits_);
148 |
149 | /**
150 | * @dev Returns the total requested funds for a given asset.
151 | * @param asset The address of the asset.
152 | * @return totalRequestedFunds_ The total requested funds held in the asset.
153 | */
154 | function totalRequestedFunds(address asset)
155 | external view returns (uint256 totalRequestedFunds_);
156 |
157 | /**
158 | * @dev Returns the total amount that can be withdrawn for a given asset.
159 | * @param asset The address of the asset.
160 | * @return totalWithdrawableFunds_ The total amount that can be withdrawn from the asset.
161 | */
162 | function totalWithdrawableFunds(address asset)
163 | external view returns (uint256 totalWithdrawableFunds_);
164 |
165 | /**
166 | * @dev Returns the total amount of cumulative withdrawals for a given asset.
167 | * @param asset The address of the asset.
168 | * @return totalWithdrawals_ The total amount that can be withdrawn from the asset.
169 | */
170 | function totalWithdrawals(address asset)
171 | external view returns (uint256 totalWithdrawals_);
172 |
173 | /**
174 | * @dev Returns if an address is a valid broker for a given asset.
175 | * @param broker The address of the broker to check.
176 | * @param asset The address of the asset that the broker is valid for.
177 | * @return isBroker_ Boolean value indicating if the broker is valid or not.
178 | */
179 | function isBroker(address broker, address asset) external view returns (bool isBroker_);
180 |
181 | /**
182 | * @dev Returns the aggregate deposits for a given ilk and asset.
183 | * @param asset The address of the asset.
184 | * @param ilk The unique identifier for a particular ilk.
185 | * @return deposits_ The deposits for the given ilk and asset.
186 | */
187 | function deposits(address asset, bytes32 ilk) external view returns (uint256 deposits_);
188 |
189 | /**
190 | * @dev Returns the aggregate requested funds for a given ilk and asset.
191 | * @param asset The address of the asset.
192 | * @param ilk The unique identifier for a particular ilk.
193 | * @return requestedFunds_ The requested funds for the given ilk and asset.
194 | */
195 | function requestedFunds(address asset, bytes32 ilk)
196 | external view returns (uint256 requestedFunds_);
197 |
198 | /**
199 | * @dev Returns the aggregate withdrawable funds for a given ilk and asset.
200 | * @param asset The address of the asset.
201 | * @param ilk The unique identifier for a particular ilk.
202 | * @return withdrawableFunds_ The withdrawableFunds funds for the given ilk and asset.
203 | */
204 | function withdrawableFunds(address asset, bytes32 ilk)
205 | external view returns (uint256 withdrawableFunds_);
206 |
207 | /**
208 | * @dev Returns the aggregate cumulative withdraws for a given ilk and asset.
209 | * @param asset The address of the asset.
210 | * @param ilk The unique identifier for a particular ilk.
211 | * @return withdrawals_ The withdrawals funds for the given ilk and asset.
212 | */
213 | function withdrawals(address asset, bytes32 ilk) external view returns (uint256 withdrawals_);
214 |
215 | /**********************************************************************************************/
216 | /*** Administrative Functions ***/
217 | /**********************************************************************************************/
218 |
219 | /**
220 | * @dev Function to set a value in the contract, called by the admin.
221 | * @param what The identifier for the value to be set.
222 | * @param data The value to be set.
223 | */
224 | function file(bytes32 what, address data) external;
225 |
226 | /**********************************************************************************************/
227 | /*** Operator Functions ***/
228 | /**********************************************************************************************/
229 |
230 | /**
231 | * @dev Function to cancel a withdrawal request from a Arranger.
232 | * @param fundRequestId The ID of the withdrawal request.
233 | */
234 | function cancelFundRequest(uint256 fundRequestId) external;
235 |
236 | /**
237 | * @dev Function to initiate a withdrawal request from a Arranger.
238 | * @param ilk The unique identifier for a particular ilk.
239 | * @param asset The asset to withdraw.
240 | * @param amount The amount of tokens to withdraw.
241 | * @param info Arbitrary string to provide additional info to the Arranger.
242 | * @return fundRequestId The ID of the withdrawal request.
243 | */
244 | function requestFunds(bytes32 ilk, address asset, uint256 amount, string memory info)
245 | external returns (uint256 fundRequestId);
246 |
247 | /**********************************************************************************************/
248 | /*** Arranger Functions ***/
249 | /**********************************************************************************************/
250 |
251 | /**
252 | * @notice Draw funds from the contract to a `destination` that the Arranger specifies. This
253 | * destination MUST be a whitelisted `broker` address for the given `asset`.
254 | * @dev Only the Arranger is authorized to call this function.
255 | * @param asset The ERC20 token contract address from which funds are being drawn.
256 | * @param destination The destination to transfer the funds to.
257 | * @param amount The amount of tokens to be drawn.
258 | */
259 | function drawFunds(address asset, address destination, uint256 amount) external;
260 |
261 | /**
262 | * @notice Return funds (principal only) from the Arranger back to the contract.
263 | * @dev Only the Arranger is authorized to call this function.
264 | * @param fundRequestId The ID of the withdrawal request.
265 | * @param amount The amount of tokens to be returned.
266 | */
267 | function returnFunds(uint256 fundRequestId, uint256 amount)
268 | external;
269 |
270 | /**********************************************************************************************/
271 | /*** View Functions ***/
272 | /**********************************************************************************************/
273 |
274 | /**
275 | * @dev Function to get the amount of funds that can be drawn by the Arranger.
276 | * @param asset The asset to check.
277 | * @return availableFunds_ The amount of funds that can be drawn by the Arranger.
278 | */
279 | function availableFunds(address asset) external view returns (uint256 availableFunds_);
280 |
281 | /**
282 | * @dev Returns a FundRequest struct at a given fundRequestId.
283 | * @param fundRequestId The id of the fund request.
284 | * @return fundRequest The FundRequest struct at the fundRequestId.
285 | */
286 | function getFundRequest(uint256 fundRequestId)
287 | external view returns (FundRequest memory fundRequest);
288 |
289 | /**
290 | * @dev Returns the length of the fundRequests array.
291 | * @return fundRequestsLength The length of the fundRequests array.
292 | */
293 | function getFundRequestsLength() external view returns (uint256 fundRequestsLength);
294 |
295 | /**
296 | * @dev Function to check if a withdrawal request can be cancelled.
297 | * @param fundRequestId The ID of the withdrawal request.
298 | * @return isCancelable_ True if the withdrawal request can be cancelled, false otherwise.
299 | */
300 | function isCancelable(uint256 fundRequestId) external view returns (bool isCancelable_);
301 |
302 | }
303 |
--------------------------------------------------------------------------------
/test/arranger-conduit/ArrangerConduitHarness.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 | pragma solidity ^0.8.13;
3 |
4 | import { ArrangerConduit } from "../../src/ArrangerConduit.sol";
5 |
6 | contract ArrangerConduitHarness is ArrangerConduit {
7 |
8 | constructor()
9 | ArrangerConduit() {}
10 |
11 | function __setFundRequestStatus(uint256 fundRequestId, ArrangerConduit.StatusEnum status)
12 | external
13 | {
14 | fundRequests[fundRequestId].status = status;
15 | }
16 |
17 | function __setWithdrawableFunds(bytes32 ilk, address asset, uint256 amount)
18 | external
19 | {
20 | withdrawableFunds[asset][ilk] = amount;
21 | }
22 |
23 | function __setTotalWithdrawableFunds(address asset, uint256 amount)
24 | external
25 | {
26 | totalWithdrawableFunds[asset] = amount;
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/test/arranger-conduit/ArrangerConduitHarness.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 | pragma solidity ^0.8.13;
3 |
4 | import { ArrangerConduit } from "../../src/ArrangerConduit.sol";
5 |
6 | contract ArrangerConduitHarness is ArrangerConduit {
7 |
8 | constructor() ArrangerConduit() {}
9 |
10 | function __setFundRequestStatus(uint256 fundRequestId, ArrangerConduit.StatusEnum status)
11 | external
12 | {
13 | fundRequests[fundRequestId].status = status;
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/test/arranger-conduit/AuthFunctions.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 | pragma solidity ^0.8.13;
3 |
4 | import "./ConduitTestBase.sol";
5 |
6 | contract ArrangerConduit_AuthTests is ConduitTestBase {
7 |
8 | function test_auth() external {
9 | checkAuth(address(conduitProxy), "UpgradeableProxy");
10 | }
11 |
12 | function test_modifiers() external {
13 | bytes4[] memory authedMethods = new bytes4[](1);
14 | authedMethods[0] = conduitProxy.setImplementation.selector;
15 |
16 | vm.startPrank(makeAddr("non-admin"));
17 | checkModifier(address(conduitProxy), "UpgradeableProxy/not-authorized", authedMethods);
18 | vm.stopPrank();
19 | }
20 |
21 | function test_file() external {
22 | checkFileAddress(
23 | address(conduitProxy),
24 | "ArrangerConduit",
25 | ["arranger", "registry", "roles"]
26 | );
27 | }
28 |
29 | function test_setImplementation() external {
30 | address newImplementation = makeAddr("new-implementation");
31 |
32 | assertEq(conduitProxy.implementation(), address(conduitImplementation));
33 |
34 | conduitProxy.setImplementation(newImplementation);
35 |
36 | assertEq(conduitProxy.implementation(), newImplementation);
37 | }
38 |
39 | function test_setBroker_noAuth() external {
40 | vm.prank(makeAddr("non-admin"));
41 | vm.expectRevert("ArrangerConduit/not-authorized");
42 | conduit.setBroker(makeAddr("broker"), makeAddr("asset"), true);
43 | }
44 |
45 | function test_setBroker() external {
46 | address asset1 = makeAddr("asset1");
47 | address asset2 = makeAddr("asset2");
48 | address broker1 = makeAddr("broker1");
49 | address broker2 = makeAddr("broker2");
50 |
51 | _assertBrokerSetter(broker1, asset1);
52 | _assertBrokerSetter(broker1, asset2);
53 | _assertBrokerSetter(broker2, asset1);
54 | _assertBrokerSetter(broker2, asset2);
55 | }
56 |
57 | function _assertBrokerSetter(address broker, address asset) internal {
58 | assertTrue(!conduit.isBroker(broker, asset));
59 |
60 | conduit.setBroker(broker, asset, true);
61 |
62 | assertTrue(conduit.isBroker(broker, asset));
63 |
64 | conduit.setBroker(broker, asset, false);
65 |
66 | assertTrue(!conduit.isBroker(broker, asset));
67 | }
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/test/arranger-conduit/CancelFundRequest.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 | pragma solidity ^0.8.13;
3 |
4 | import { IArrangerConduit } from "../../src/interfaces/IArrangerConduit.sol";
5 |
6 | import "./ConduitTestBase.sol";
7 |
8 | contract ArrangerConduit_CancelFundRequestFailureTests is ConduitAssetTestBase {
9 |
10 | function test_cancelFundRequest_noIlkAuth() public {
11 | asset1.mint(buffer1, 100);
12 |
13 | vm.startPrank(operator1);
14 |
15 | conduit.deposit(ilk1, address(asset1), 100);
16 | conduit.requestFunds(ilk1, address(asset1), 100, "info");
17 |
18 | vm.stopPrank();
19 |
20 | vm.prank(arranger);
21 | vm.expectRevert("ArrangerConduit/not-authorized");
22 | conduit.cancelFundRequest(0);
23 | }
24 |
25 | function test_cancelFundRequest_notInitialized() public {
26 | asset1.mint(buffer1, 100);
27 |
28 | vm.prank(operator1);
29 | conduit.deposit(ilk1, address(asset1), 100);
30 |
31 | vm.prank(arranger);
32 | vm.expectRevert(stdError.indexOOBError);
33 | conduit.cancelFundRequest(0);
34 | }
35 |
36 | function test_cancelFundRequest_completed() public {
37 | asset1.mint(buffer1, 100);
38 |
39 | vm.prank(operator1);
40 | conduit.deposit(ilk1, address(asset1), 100);
41 |
42 | vm.prank(arranger);
43 | conduit.drawFunds(address(asset1), broker1, 100);
44 |
45 | vm.prank(operator1);
46 | conduit.requestFunds(ilk1, address(asset1), 100, "info");
47 |
48 | vm.prank(broker1);
49 | asset1.transfer(address(conduit), 100);
50 |
51 | vm.prank(arranger);
52 | conduit.returnFunds(0, 100);
53 |
54 | vm.prank(operator1);
55 | vm.expectRevert("ArrangerConduit/invalid-status");
56 | conduit.cancelFundRequest(0);
57 | }
58 |
59 | function test_cancelFundRequest_cancelled() public {
60 | asset1.mint(buffer1, 100);
61 |
62 | vm.startPrank(operator1);
63 |
64 | conduit.deposit(ilk1, address(asset1), 100);
65 | conduit.requestFunds(ilk1, address(asset1), 100, "info");
66 | conduit.cancelFundRequest(0);
67 |
68 | vm.expectRevert("ArrangerConduit/invalid-status");
69 | conduit.cancelFundRequest(0);
70 | }
71 |
72 | }
73 |
74 | contract ArrangerConduit_CancelFundRequestTests is ConduitAssetTestBase {
75 |
76 | function test_cancelFundRequest() public {
77 | asset1.mint(buffer1, 100);
78 |
79 | vm.startPrank(operator1);
80 |
81 | conduit.deposit(ilk1, address(asset1), 100);
82 | conduit.requestFunds(ilk1, address(asset1), 100, "info");
83 |
84 | IArrangerConduit.FundRequest memory fundRequest = conduit.getFundRequest(0);
85 |
86 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.PENDING);
87 |
88 | assertEq(fundRequest.asset, address(asset1));
89 | assertEq(fundRequest.ilk, ilk1);
90 | assertEq(fundRequest.amountRequested, 100);
91 | assertEq(fundRequest.amountFilled, 0);
92 | assertEq(fundRequest.info, "info");
93 |
94 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 100);
95 | assertEq(conduit.totalRequestedFunds(address(asset1)), 100);
96 |
97 | _assertInvariants();
98 |
99 | conduit.cancelFundRequest(0);
100 |
101 | fundRequest = conduit.getFundRequest(0);
102 |
103 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.CANCELLED);
104 |
105 | assertEq(fundRequest.asset, address(asset1));
106 | assertEq(fundRequest.ilk, ilk1);
107 | assertEq(fundRequest.amountRequested, 100);
108 | assertEq(fundRequest.amountFilled, 0);
109 | assertEq(fundRequest.info, "info");
110 |
111 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 0);
112 | assertEq(conduit.totalRequestedFunds(address(asset1)), 0);
113 |
114 | _assertInvariants();
115 | }
116 |
117 | }
118 |
--------------------------------------------------------------------------------
/test/arranger-conduit/ConduitTestBase.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 | pragma solidity ^0.8.13;
3 |
4 | import "dss-test/DssTest.sol";
5 |
6 | import { AllocatorRegistry } from "dss-allocator/AllocatorRegistry.sol";
7 | import { AllocatorRoles } from "dss-allocator/AllocatorRoles.sol";
8 |
9 | import { MockERC20 } from "erc20-helpers/MockERC20.sol";
10 |
11 | import { UpgradeableProxy } from "upgradeable-proxy/UpgradeableProxy.sol";
12 |
13 | import { ArrangerConduit } from "../../src/ArrangerConduit.sol";
14 |
15 | contract ConduitTestBase is DssTest {
16 |
17 | address admin = makeAddr("admin");
18 | address arranger = makeAddr("arranger");
19 | address buffer1 = makeAddr("buffer1");
20 | address buffer2 = makeAddr("buffer2");
21 | address operator1 = makeAddr("operator1");
22 | address operator2 = makeAddr("operator2");
23 |
24 | AllocatorRegistry registry = new AllocatorRegistry();
25 | AllocatorRoles roles = new AllocatorRoles();
26 |
27 | ArrangerConduit conduit;
28 | ArrangerConduit conduitImplementation;
29 | UpgradeableProxy conduitProxy;
30 |
31 | function setUp() public virtual {
32 | conduitProxy = new UpgradeableProxy();
33 |
34 | conduitImplementation = new ArrangerConduit();
35 |
36 | conduitProxy.setImplementation(address(conduitImplementation));
37 |
38 | conduit = ArrangerConduit(address(conduitProxy));
39 |
40 | conduit.file("arranger", arranger);
41 | conduit.file("registry", address(registry));
42 | conduit.file("roles", address(roles));
43 | }
44 |
45 | }
46 |
47 | contract ConduitAssetTestBase is ConduitTestBase {
48 |
49 | uint8 ROLE = 0;
50 |
51 | address broker1 = makeAddr("broker1");
52 | address broker2 = makeAddr("broker2");
53 |
54 | bytes32 ilk1 = "ilk1";
55 | bytes32 ilk2 = "ilk2";
56 |
57 | MockERC20 asset1 = new MockERC20("asset1", "ASSET1", 18);
58 | MockERC20 asset2 = new MockERC20("asset2", "ASSET2", 18);
59 |
60 | function setUp() public virtual override {
61 | super.setUp();
62 |
63 | _setupOperatorRole(ilk1, operator1);
64 | _setupOperatorRole(ilk2, operator2);
65 |
66 | registry.file(ilk1, "buffer", buffer1);
67 | registry.file(ilk2, "buffer", buffer2);
68 |
69 | conduit.setBroker(broker1, address(asset1), true);
70 | conduit.setBroker(broker2, address(asset2), true);
71 |
72 | vm.startPrank(buffer1);
73 | asset1.approve(address(conduit), type(uint256).max);
74 | asset2.approve(address(conduit), type(uint256).max);
75 |
76 | vm.startPrank(buffer2);
77 | asset1.approve(address(conduit), type(uint256).max);
78 | asset2.approve(address(conduit), type(uint256).max);
79 |
80 | vm.stopPrank();
81 | }
82 |
83 | function _assertInvariants() internal {
84 | _assertInvariants(address(asset1));
85 | _assertInvariants(address(asset2));
86 | }
87 |
88 | function _assertInvariants(address asset_) internal {
89 | assertEq(
90 | conduit.totalDeposits(asset_),
91 | conduit.deposits(asset_, ilk1) + conduit.deposits(asset_, ilk2)
92 | );
93 |
94 | assertEq(
95 | conduit.totalRequestedFunds(asset_),
96 | conduit.requestedFunds(asset_, ilk1) + conduit.requestedFunds(asset_, ilk2)
97 | );
98 |
99 | assertEq(
100 | conduit.totalWithdrawableFunds(asset_),
101 | conduit.withdrawableFunds(asset_, ilk1) + conduit.withdrawableFunds(asset_, ilk2)
102 | );
103 |
104 | assertEq(
105 | conduit.totalWithdrawals(asset_),
106 | conduit.withdrawals(asset_, ilk1) + conduit.withdrawals(asset_, ilk2)
107 | );
108 | }
109 |
110 | function _depositAndDrawFunds(
111 | MockERC20 asset_,
112 | address operator_,
113 | address buffer_,
114 | address broker_,
115 | bytes32 ilk_,
116 | uint256 amount
117 | )
118 | internal
119 | {
120 | asset_.mint(buffer_, amount);
121 |
122 | vm.prank(operator_);
123 | conduit.deposit(ilk_, address(asset_), amount);
124 |
125 | vm.prank(arranger);
126 | conduit.drawFunds(address(asset_), broker_, amount);
127 | }
128 |
129 | // NOTE: Majority of tests use these params
130 | function _depositAndDrawFunds(uint256 amount) internal {
131 | _depositAndDrawFunds(asset1, operator1, buffer1, broker1, ilk1, amount);
132 | }
133 |
134 | function _setupOperatorRole(bytes32 ilk_, address operator_) internal {
135 | // Ensure address(this) can always set for a new ilk
136 | roles.setIlkAdmin(ilk_, address(this));
137 |
138 | roles.setUserRole(ilk_, operator_, ROLE, true);
139 |
140 | address conduit_ = address(conduit);
141 |
142 | roles.setRoleAction(ilk_, ROLE, conduit_, conduit.deposit.selector, true);
143 | roles.setRoleAction(ilk_, ROLE, conduit_, conduit.withdraw.selector, true);
144 | roles.setRoleAction(ilk_, ROLE, conduit_, conduit.requestFunds.selector, true);
145 | roles.setRoleAction(ilk_, ROLE, conduit_, conduit.cancelFundRequest.selector, true);
146 | }
147 |
148 | }
149 |
150 |
--------------------------------------------------------------------------------
/test/arranger-conduit/Constructor.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 | pragma solidity ^0.8.13;
3 |
4 | import "./ConduitTestBase.sol";
5 |
6 | contract ArrangerConduit_ConstructorTests is ConduitTestBase {
7 |
8 | function test_constructor() public {
9 | assertEq(conduit.wards(address(this)), 1);
10 | }
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/test/arranger-conduit/Deposit.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 | pragma solidity ^0.8.13;
3 |
4 | import "./ConduitTestBase.sol";
5 |
6 | contract ArrangerConduit_DepositFailureTests is ConduitAssetTestBase {
7 |
8 | function test_deposit_noIlkAuth() public {
9 | asset1.mint(buffer1, 100);
10 |
11 | vm.expectRevert("ArrangerConduit/not-authorized");
12 | conduit.deposit(ilk1, address(asset1), 100);
13 | }
14 |
15 | // NOTE: This test doesn't apply really in practice because of buffer setup, but being thorough
16 | function test_deposit_insufficientApproveBoundary() public {
17 | asset1.mint(buffer1, 100);
18 |
19 | vm.prank(buffer1);
20 | asset1.approve(address(conduit), 99);
21 |
22 | vm.prank(operator1);
23 | vm.expectRevert(stdError.arithmeticError);
24 | conduit.deposit(ilk1, address(asset1), 100);
25 |
26 | vm.prank(buffer1);
27 | asset1.approve(address(conduit), 100);
28 |
29 | vm.prank(operator1);
30 | conduit.deposit(ilk1, address(asset1), 100);
31 | }
32 |
33 | // NOTE: This test doesn't apply really in practice because of buffer setup, but being thorough
34 | function testFuzz_deposit_insufficientApproveBoundary(uint256 amount) public {
35 | vm.assume(amount != 0);
36 |
37 | asset1.mint(buffer1, amount);
38 |
39 | vm.prank(buffer1);
40 | asset1.approve(address(conduit), amount - 1);
41 |
42 | vm.prank(operator1);
43 | vm.expectRevert(stdError.arithmeticError);
44 | conduit.deposit(ilk1, address(asset1), amount);
45 |
46 | vm.prank(buffer1);
47 | asset1.approve(address(conduit), amount);
48 |
49 | vm.prank(operator1);
50 | conduit.deposit(ilk1, address(asset1), amount);
51 | }
52 |
53 | function test_deposit_insufficientFundsBoundary() public {
54 | asset1.mint(buffer1, 99);
55 |
56 | vm.startPrank(operator1);
57 |
58 | vm.expectRevert(stdError.arithmeticError);
59 | conduit.deposit(ilk1, address(asset1), 100);
60 |
61 | asset1.mint(buffer1, 1);
62 |
63 | conduit.deposit(ilk1, address(asset1), 100);
64 | }
65 |
66 | function testFuzz_deposit_insufficientFundsBoundary(uint256 amount) public {
67 | vm.assume(amount != 0);
68 |
69 | asset1.mint(buffer1, amount - 1);
70 |
71 | vm.startPrank(operator1);
72 |
73 | vm.expectRevert(stdError.arithmeticError);
74 | conduit.deposit(ilk1, address(asset1), amount);
75 |
76 | asset1.mint(buffer1, 1);
77 |
78 | conduit.deposit(ilk1, address(asset1), amount);
79 | }
80 |
81 | function test_deposit_noBufferRegistered() external {
82 | asset1.mint(operator1, 100);
83 |
84 | registry.file(ilk1, "buffer", address(0));
85 |
86 | vm.startPrank(operator1);
87 | asset1.approve(address(conduit), 100);
88 |
89 | vm.expectRevert("ArrangerConduit/no-buffer-registered");
90 | conduit.deposit(ilk1, address(asset1), 100);
91 |
92 | vm.stopPrank();
93 |
94 | registry.file(ilk1, "buffer", operator1);
95 |
96 | vm.prank(operator1);
97 | conduit.deposit(ilk1, address(asset1), 100);
98 | }
99 |
100 | }
101 |
102 | contract ArrangerConduit_DepositTests is ConduitAssetTestBase {
103 |
104 | function test_deposit_singleIlk() external {
105 | asset1.mint(buffer1, 100);
106 |
107 | vm.startPrank(operator1);
108 |
109 | assertEq(asset1.balanceOf(buffer1), 100);
110 | assertEq(asset1.balanceOf(address(conduit)), 0);
111 |
112 | assertEq(conduit.deposits(address(asset1), ilk1), 0);
113 | assertEq(conduit.totalDeposits(address(asset1)), 0);
114 |
115 | conduit.deposit(ilk1, address(asset1), 100);
116 |
117 | assertEq(asset1.balanceOf(buffer1), 0);
118 | assertEq(asset1.balanceOf(address(conduit)), 100);
119 |
120 | assertEq(conduit.deposits(address(asset1), ilk1), 100);
121 | assertEq(conduit.totalDeposits(address(asset1)), 100);
122 |
123 | _assertInvariants();
124 | }
125 |
126 | function testFuzz_deposit_singleIlk(uint256 amount) external {
127 | asset1.mint(buffer1, amount);
128 |
129 | vm.startPrank(operator1);
130 |
131 | assertEq(asset1.balanceOf(buffer1), amount);
132 | assertEq(asset1.balanceOf(address(conduit)), 0);
133 |
134 | assertEq(conduit.deposits(address(asset1), ilk1), 0);
135 | assertEq(conduit.totalDeposits(address(asset1)), 0);
136 |
137 | conduit.deposit(ilk1, address(asset1), amount);
138 |
139 | assertEq(asset1.balanceOf(buffer1), 0);
140 | assertEq(asset1.balanceOf(address(conduit)), amount);
141 |
142 | assertEq(conduit.deposits(address(asset1), ilk1), amount);
143 | assertEq(conduit.totalDeposits(address(asset1)), amount);
144 |
145 | _assertInvariants();
146 | }
147 |
148 | function test_deposit_multiIlk() external {
149 | asset1.mint(buffer1, 100);
150 | asset1.mint(buffer2, 300);
151 |
152 | assertEq(asset1.balanceOf(buffer1), 100);
153 | assertEq(asset1.balanceOf(buffer2), 300);
154 | assertEq(asset1.balanceOf(address(conduit)), 0);
155 |
156 | assertEq(conduit.deposits(address(asset1), ilk1), 0);
157 | assertEq(conduit.deposits(address(asset1), ilk2), 0);
158 | assertEq(conduit.totalDeposits(address(asset1)), 0);
159 |
160 | vm.prank(operator1);
161 | conduit.deposit(ilk1, address(asset1), 100);
162 |
163 | assertEq(asset1.balanceOf(buffer1), 0);
164 | assertEq(asset1.balanceOf(buffer2), 300);
165 | assertEq(asset1.balanceOf(address(conduit)), 100);
166 |
167 | assertEq(conduit.deposits(address(asset1), ilk1), 100);
168 | assertEq(conduit.deposits(address(asset1), ilk2), 0);
169 | assertEq(conduit.totalDeposits(address(asset1)), 100);
170 |
171 | _assertInvariants();
172 |
173 | vm.prank(operator2);
174 | conduit.deposit(ilk2, address(asset1), 300);
175 |
176 | assertEq(asset1.balanceOf(buffer1), 0);
177 | assertEq(asset1.balanceOf(buffer2), 0);
178 | assertEq(asset1.balanceOf(address(conduit)), 400);
179 |
180 | assertEq(conduit.deposits(address(asset1), ilk1), 100);
181 | assertEq(conduit.deposits(address(asset1), ilk2), 300);
182 | assertEq(conduit.totalDeposits(address(asset1)), 400);
183 |
184 | _assertInvariants();
185 | }
186 |
187 | }
188 |
--------------------------------------------------------------------------------
/test/arranger-conduit/DrawFunds.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 | pragma solidity ^0.8.13;
3 |
4 | import "./ConduitTestBase.sol";
5 |
6 | contract ArrangerConduit_DrawFundsTests is ConduitAssetTestBase {
7 |
8 | function setUp() public virtual override {
9 | super.setUp();
10 |
11 | asset1.mint(buffer1, 100);
12 |
13 | vm.startPrank(operator1);
14 |
15 | asset1.approve(address(conduit), 100);
16 | conduit.deposit(ilk1, address(asset1), 100);
17 |
18 | vm.stopPrank();
19 | }
20 |
21 | function test_drawFunds_notArranger() external {
22 | vm.expectRevert("ArrangerConduit/not-arranger");
23 | conduit.drawFunds(address(asset1), broker1, 100);
24 | }
25 |
26 | function test_drawFunds_invalidBroker() external {
27 | vm.startPrank(arranger);
28 |
29 | // NOTE: `broker2` was not configured in setup for `asset1`
30 | vm.expectRevert("ArrangerConduit/invalid-broker");
31 | conduit.drawFunds(address(asset1), broker2, 100);
32 | }
33 |
34 | function test_drawFunds_insufficientDrawableBoundary() external {
35 | assertEq(conduit.availableFunds(address(asset1)), 100);
36 |
37 | vm.startPrank(arranger);
38 |
39 | vm.expectRevert("ArrangerConduit/insufficient-funds");
40 | conduit.drawFunds(address(asset1), broker1, 101);
41 |
42 | conduit.drawFunds(address(asset1), broker1, 100);
43 | }
44 |
45 | function test_drawFunds() public {
46 | assertEq(conduit.availableFunds(address(asset1)), 100);
47 |
48 | assertEq(asset1.balanceOf(address(conduit)), 100);
49 | assertEq(asset1.balanceOf(broker1), 0);
50 |
51 | vm.prank(arranger);
52 | conduit.drawFunds(address(asset1), broker1, 40);
53 |
54 | assertEq(conduit.availableFunds(address(asset1)), 60);
55 |
56 | assertEq(asset1.balanceOf(address(conduit)), 60);
57 | assertEq(asset1.balanceOf(broker1), 40);
58 |
59 | _assertInvariants();
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/test/arranger-conduit/EventsData.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 | pragma solidity ^0.8.13;
3 |
4 | import "./ConduitTestBase.sol";
5 |
6 | contract ArrangerConduit_EventTests is ConduitAssetTestBase {
7 |
8 | event CancelFundRequest(uint256 fundRequestId);
9 | event Deposit(bytes32 indexed ilk, address indexed asset, address origin, uint256 amount);
10 | event DrawFunds(address indexed asset, address indexed destination, uint256 amount);
11 | event RequestFunds(
12 | bytes32 indexed ilk,
13 | address indexed asset,
14 | uint256 fundRequestId,
15 | uint256 amount,
16 | string info
17 | );
18 | event ReturnFunds(
19 | bytes32 indexed ilk,
20 | address indexed asset,
21 | uint256 fundRequestId,
22 | uint256 amountRequested,
23 | uint256 returnAmount
24 | );
25 | event Withdraw(bytes32 indexed ilk, address indexed asset, address destination, uint256 amount);
26 |
27 | function test_cancelFundRequest() public {
28 | asset1.mint(buffer1, 200);
29 |
30 | vm.startPrank(operator1);
31 |
32 | conduit.deposit(ilk1, address(asset1), 100);
33 | conduit.requestFunds(ilk1, address(asset1), 100, "info");
34 |
35 | vm.expectEmit(address(conduit));
36 | emit CancelFundRequest(0);
37 | conduit.cancelFundRequest(0);
38 |
39 | conduit.requestFunds(ilk1, address(asset1), 100, "info");
40 |
41 | // Ensure incrementing
42 | vm.expectEmit(address(conduit));
43 | emit CancelFundRequest(1);
44 | conduit.cancelFundRequest(1);
45 | }
46 |
47 | function test_deposit() external {
48 | asset1.mint(buffer1, 100);
49 |
50 | vm.prank(operator1);
51 | vm.expectEmit(address(conduit));
52 | emit Deposit(ilk1, address(asset1), buffer1, 100);
53 | conduit.deposit(ilk1, address(asset1), 100);
54 | }
55 |
56 | function test_drawFunds() public {
57 | asset1.mint(buffer1, 100);
58 |
59 | vm.prank(operator1);
60 | conduit.deposit(ilk1, address(asset1), 100);
61 |
62 | vm.prank(arranger);
63 | vm.expectEmit(address(conduit));
64 | emit DrawFunds(address(asset1), broker1, 40);
65 | conduit.drawFunds(address(asset1), broker1, 40);
66 | }
67 |
68 | function test_file() public {
69 | vm.expectEmit(address(conduit));
70 | emit File("arranger", makeAddr("arranger"));
71 | conduit.file("arranger", makeAddr("arranger"));
72 | }
73 |
74 | function test_requestFunds() public {
75 | asset1.mint(buffer1, 100);
76 |
77 | vm.startPrank(operator1);
78 |
79 | conduit.deposit(ilk1, address(asset1), 100);
80 |
81 | vm.expectEmit(address(conduit));
82 | emit RequestFunds(ilk1, address(asset1), 0, 100, "info");
83 | conduit.requestFunds(ilk1, address(asset1), 100, "info");
84 |
85 | // Assert id increments
86 | vm.expectEmit(address(conduit));
87 | emit RequestFunds(ilk1, address(asset1), 1, 50, "info");
88 | conduit.requestFunds(ilk1, address(asset1), 50, "info");
89 | }
90 |
91 | function test_returnFunds() external {
92 | asset1.mint(buffer1, 100);
93 |
94 | vm.prank(operator1);
95 | conduit.deposit(ilk1, address(asset1), 100);
96 |
97 | vm.prank(arranger);
98 | conduit.drawFunds(address(asset1), broker1, 100);
99 |
100 | asset1.mint(address(conduit), 100);
101 |
102 | vm.prank(operator1);
103 | conduit.requestFunds(ilk1, address(asset1), 20, "info");
104 |
105 | vm.startPrank(arranger);
106 |
107 | asset1.approve(address(conduit), 30);
108 |
109 | // Request 20, return 30
110 | vm.expectEmit(address(conduit));
111 | emit ReturnFunds(ilk1, address(asset1), 0, 20, 30);
112 | conduit.returnFunds(0, 30);
113 |
114 | vm.stopPrank();
115 |
116 | vm.prank(operator1);
117 | conduit.requestFunds(ilk1, address(asset1), 50, "info");
118 |
119 | vm.startPrank(arranger);
120 | asset1.approve(address(conduit), 40);
121 |
122 | // Request 50, return 40, assert id increments
123 | vm.expectEmit(address(conduit));
124 | emit ReturnFunds(ilk1, address(asset1), 1, 50, 40);
125 | conduit.returnFunds(1, 40);
126 | }
127 |
128 | function test_withdraw() external {
129 | _depositAndDrawFunds(100);
130 |
131 | vm.prank(operator1);
132 | conduit.requestFunds(ilk1, address(asset1), 100, "info");
133 |
134 | asset1.mint(address(conduit), 100);
135 |
136 | vm.prank(arranger);
137 | conduit.returnFunds(0, 100);
138 |
139 | vm.prank(operator1);
140 | vm.expectEmit(address(conduit));
141 | emit Withdraw(ilk1, address(asset1), buffer1, 100);
142 | conduit.withdraw(ilk1, address(asset1), 100);
143 | }
144 |
145 | }
146 |
--------------------------------------------------------------------------------
/test/arranger-conduit/RequestFunds.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 | pragma solidity ^0.8.13;
3 |
4 | import { IArrangerConduit } from "../../src/interfaces/IArrangerConduit.sol";
5 |
6 | import "./ConduitTestBase.sol";
7 |
8 | contract ArrangerConduit_RequestFundsTests is ConduitAssetTestBase {
9 |
10 | function test_requestFunds_noIlkAuth() public {
11 | asset1.mint(buffer1, 100);
12 |
13 | vm.prank(operator1);
14 | conduit.deposit(ilk1, address(asset1), 100);
15 |
16 | vm.expectRevert("ArrangerConduit/not-authorized");
17 | conduit.requestFunds(ilk1, address(asset1), 100, "info");
18 | }
19 |
20 | function test_requestFunds_singleIlk_singleRequest() public {
21 | asset1.mint(buffer1, 100);
22 |
23 | vm.startPrank(operator1);
24 |
25 | conduit.deposit(ilk1, address(asset1), 100);
26 |
27 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 0);
28 |
29 | conduit.requestFunds(ilk1, address(asset1), 100, "info");
30 |
31 | IArrangerConduit.FundRequest memory fundRequest = conduit.getFundRequest(0);
32 |
33 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.PENDING);
34 |
35 | assertEq(fundRequest.asset, address(asset1));
36 | assertEq(fundRequest.ilk, ilk1);
37 | assertEq(fundRequest.amountRequested, 100);
38 | assertEq(fundRequest.amountFilled, 0);
39 | assertEq(fundRequest.info, "info");
40 |
41 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 100);
42 | assertEq(conduit.totalRequestedFunds(address(asset1)), 100);
43 |
44 | _assertInvariants();
45 | }
46 |
47 | function test_requestFunds_multiIlk_singleRequest_singleAsset() public {
48 | asset1.mint(buffer1, 40);
49 | asset1.mint(buffer2, 60);
50 |
51 | vm.prank(operator1);
52 | conduit.deposit(ilk1, address(asset1), 40);
53 |
54 | vm.prank(operator2);
55 | conduit.deposit(ilk2, address(asset1), 60);
56 |
57 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 0);
58 | assertEq(conduit.requestedFunds(address(asset1), ilk2), 0);
59 |
60 | vm.prank(operator1);
61 | uint256 returnFundRequestId = conduit.requestFunds(ilk1, address(asset1), 40, "info1");
62 |
63 | assertEq(returnFundRequestId, 0);
64 |
65 | IArrangerConduit.FundRequest memory fundRequest = conduit.getFundRequest(0);
66 |
67 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.PENDING);
68 |
69 | assertEq(fundRequest.asset, address(asset1));
70 | assertEq(fundRequest.ilk, ilk1);
71 | assertEq(fundRequest.amountRequested, 40);
72 | assertEq(fundRequest.amountFilled, 0);
73 | assertEq(fundRequest.info, "info1");
74 |
75 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 40);
76 | assertEq(conduit.totalRequestedFunds(address(asset1)), 40);
77 |
78 | vm.prank(operator2);
79 | returnFundRequestId = conduit.requestFunds(ilk2, address(asset1), 60, "info2");
80 |
81 | assertEq(returnFundRequestId, 1);
82 |
83 | fundRequest = conduit.getFundRequest(1);
84 |
85 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.PENDING);
86 |
87 | assertEq(fundRequest.asset, address(asset1));
88 | assertEq(fundRequest.ilk, ilk2);
89 | assertEq(fundRequest.amountRequested, 60);
90 | assertEq(fundRequest.amountFilled, 0);
91 | assertEq(fundRequest.info, "info2");
92 |
93 | assertEq(conduit.requestedFunds(address(asset1), ilk2), 60);
94 | assertEq(conduit.totalRequestedFunds(address(asset1)), 100);
95 |
96 | _assertInvariants();
97 | }
98 |
99 | // NOTE: Removing all struct-level assertions for below tests since they have been adequately
100 | // asserted in above tests and FundRequest structs are mutually exclusive to each other
101 | // as proven in the above test. Below tests are only testing the mapping-level handling of
102 | // multi-ilk multi-asset scenarios.
103 |
104 | function test_requestFunds_singleIlk_multiRequest_singleAsset() public {
105 | asset1.mint(buffer1, 100);
106 |
107 | vm.startPrank(operator1);
108 |
109 | conduit.deposit(ilk1, address(asset1), 100);
110 |
111 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 0);
112 |
113 | uint256 returnFundRequestId = conduit.requestFunds(ilk1, address(asset1), 40, "info");
114 |
115 | assertEq(returnFundRequestId, 0);
116 |
117 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 40);
118 | assertEq(conduit.totalRequestedFunds(address(asset1)), 40);
119 |
120 | returnFundRequestId = conduit.requestFunds(ilk1, address(asset1), 60, "info");
121 |
122 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 100);
123 | assertEq(conduit.totalRequestedFunds(address(asset1)), 100);
124 |
125 | _assertInvariants();
126 | }
127 |
128 | function test_requestFunds_singleIlk_singleRequest_multiAsset() public {
129 | asset1.mint(buffer1, 100);
130 | asset2.mint(buffer1, 300);
131 |
132 | vm.startPrank(operator1);
133 |
134 | conduit.deposit(ilk1, address(asset1), 100);
135 | conduit.deposit(ilk1, address(asset2), 300);
136 |
137 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 0);
138 | assertEq(conduit.requestedFunds(address(asset2), ilk1), 0);
139 |
140 | uint256 returnFundRequestId = conduit.requestFunds(ilk1, address(asset1), 100, "info1");
141 |
142 | assertEq(returnFundRequestId, 0);
143 |
144 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 100);
145 | assertEq(conduit.requestedFunds(address(asset2), ilk1), 0);
146 | assertEq(conduit.totalRequestedFunds(address(asset1)), 100);
147 | assertEq(conduit.totalRequestedFunds(address(asset2)), 0);
148 |
149 | returnFundRequestId = conduit.requestFunds(ilk1, address(asset2), 300, "info2");
150 |
151 | assertEq(returnFundRequestId, 1);
152 |
153 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 100);
154 | assertEq(conduit.requestedFunds(address(asset2), ilk1), 300);
155 | assertEq(conduit.totalRequestedFunds(address(asset1)), 100);
156 | assertEq(conduit.totalRequestedFunds(address(asset2)), 300);
157 |
158 | _assertInvariants();
159 | }
160 |
161 | function test_requestFunds_multiIlk_multiRequest_multiAsset() public {
162 |
163 | /********************************************/
164 | /*** First round of deposits and requests ***/
165 | /********************************************/
166 |
167 | asset1.mint(buffer1, 40);
168 | asset1.mint(buffer2, 60);
169 | asset2.mint(buffer1, 100);
170 | asset2.mint(buffer2, 300);
171 |
172 | vm.startPrank(operator1);
173 |
174 | conduit.deposit(ilk1, address(asset1), 40);
175 | conduit.deposit(ilk1, address(asset2), 100);
176 |
177 | vm.stopPrank();
178 |
179 | vm.startPrank(operator2);
180 |
181 | conduit.deposit(ilk2, address(asset1), 60);
182 | conduit.deposit(ilk2, address(asset2), 300);
183 |
184 | vm.stopPrank();
185 |
186 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 0);
187 | assertEq(conduit.requestedFunds(address(asset2), ilk1), 0);
188 | assertEq(conduit.requestedFunds(address(asset1), ilk2), 0);
189 | assertEq(conduit.requestedFunds(address(asset2), ilk2), 0);
190 |
191 | assertEq(conduit.totalRequestedFunds(address(asset1)), 0);
192 | assertEq(conduit.totalRequestedFunds(address(asset2)), 0);
193 |
194 | _assertInvariants();
195 |
196 | // Request Funds for asset1 ilk1
197 |
198 | vm.prank(operator1);
199 | uint256 returnFundRequestId = conduit.requestFunds(ilk1, address(asset1), 40, "info");
200 |
201 | assertEq(returnFundRequestId, 0);
202 |
203 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 40);
204 | assertEq(conduit.requestedFunds(address(asset1), ilk2), 0);
205 | assertEq(conduit.requestedFunds(address(asset2), ilk1), 0);
206 | assertEq(conduit.requestedFunds(address(asset2), ilk2), 0);
207 |
208 | assertEq(conduit.totalRequestedFunds(address(asset1)), 40);
209 | assertEq(conduit.totalRequestedFunds(address(asset2)), 0);
210 |
211 | _assertInvariants();
212 |
213 | // Request Funds for asset1 ilk2
214 |
215 | vm.prank(operator2);
216 | returnFundRequestId = conduit.requestFunds(ilk2, address(asset1), 60, "info");
217 |
218 | assertEq(returnFundRequestId, 1);
219 |
220 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 40);
221 | assertEq(conduit.requestedFunds(address(asset1), ilk2), 60);
222 | assertEq(conduit.requestedFunds(address(asset2), ilk1), 0);
223 | assertEq(conduit.requestedFunds(address(asset2), ilk2), 0);
224 |
225 | assertEq(conduit.totalRequestedFunds(address(asset1)), 100);
226 | assertEq(conduit.totalRequestedFunds(address(asset2)), 0);
227 |
228 | _assertInvariants();
229 |
230 | // Request Funds for asset2 ilk1
231 |
232 | vm.prank(operator1);
233 | returnFundRequestId = conduit.requestFunds(ilk1, address(asset2), 100, "info");
234 |
235 | assertEq(returnFundRequestId, 2);
236 |
237 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 40);
238 | assertEq(conduit.requestedFunds(address(asset1), ilk2), 60);
239 | assertEq(conduit.requestedFunds(address(asset2), ilk1), 100);
240 | assertEq(conduit.requestedFunds(address(asset2), ilk2), 0);
241 |
242 | assertEq(conduit.totalRequestedFunds(address(asset1)), 100);
243 | assertEq(conduit.totalRequestedFunds(address(asset2)), 100);
244 |
245 | _assertInvariants();
246 |
247 | // Request Funds for asset2 ilk2
248 |
249 | vm.prank(operator2);
250 | returnFundRequestId = conduit.requestFunds(ilk2, address(asset2), 300, "info");
251 |
252 | assertEq(returnFundRequestId, 3);
253 |
254 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 40);
255 | assertEq(conduit.requestedFunds(address(asset1), ilk2), 60);
256 | assertEq(conduit.requestedFunds(address(asset2), ilk1), 100);
257 | assertEq(conduit.requestedFunds(address(asset2), ilk2), 300);
258 |
259 | assertEq(conduit.totalRequestedFunds(address(asset1)), 100);
260 | assertEq(conduit.totalRequestedFunds(address(asset2)), 400);
261 |
262 | _assertInvariants();
263 |
264 | /*********************************************/
265 | /*** Second round of deposits and requests ***/
266 | /*********************************************/
267 |
268 | asset1.mint(buffer1, 40);
269 | asset1.mint(buffer2, 60);
270 | asset2.mint(buffer1, 100);
271 | asset2.mint(buffer2, 300);
272 |
273 | vm.startPrank(operator1);
274 |
275 | conduit.deposit(ilk1, address(asset1), 40);
276 | conduit.deposit(ilk1, address(asset2), 100);
277 |
278 | vm.startPrank(operator2);
279 |
280 | conduit.deposit(ilk2, address(asset1), 60);
281 | conduit.deposit(ilk2, address(asset2), 300);
282 |
283 | vm.stopPrank();
284 |
285 | // Request Funds for asset1 ilk1
286 |
287 | vm.prank(operator1);
288 | returnFundRequestId = conduit.requestFunds(ilk1, address(asset1), 40, "info");
289 |
290 | assertEq(returnFundRequestId, 4);
291 |
292 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 80);
293 | assertEq(conduit.requestedFunds(address(asset1), ilk2), 60);
294 | assertEq(conduit.requestedFunds(address(asset2), ilk1), 100);
295 | assertEq(conduit.requestedFunds(address(asset2), ilk2), 300);
296 |
297 | assertEq(conduit.totalRequestedFunds(address(asset1)), 140);
298 | assertEq(conduit.totalRequestedFunds(address(asset2)), 400);
299 |
300 | _assertInvariants();
301 |
302 | // Request Funds for asset1 ilk2
303 |
304 | vm.prank(operator2);
305 | returnFundRequestId = conduit.requestFunds(ilk2, address(asset1), 60, "info");
306 |
307 | assertEq(returnFundRequestId, 5);
308 |
309 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 80);
310 | assertEq(conduit.requestedFunds(address(asset1), ilk2), 120);
311 | assertEq(conduit.requestedFunds(address(asset2), ilk1), 100);
312 | assertEq(conduit.requestedFunds(address(asset2), ilk2), 300);
313 |
314 | assertEq(conduit.totalRequestedFunds(address(asset1)), 200);
315 | assertEq(conduit.totalRequestedFunds(address(asset2)), 400);
316 |
317 | _assertInvariants();
318 |
319 | // Request Funds for asset2 ilk1
320 |
321 | vm.prank(operator1);
322 | returnFundRequestId = conduit.requestFunds(ilk1, address(asset2), 100, "info");
323 |
324 | assertEq(returnFundRequestId, 6);
325 |
326 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 80);
327 | assertEq(conduit.requestedFunds(address(asset1), ilk2), 120);
328 | assertEq(conduit.requestedFunds(address(asset2), ilk1), 200);
329 | assertEq(conduit.requestedFunds(address(asset2), ilk2), 300);
330 |
331 | assertEq(conduit.totalRequestedFunds(address(asset1)), 200);
332 | assertEq(conduit.totalRequestedFunds(address(asset2)), 500);
333 |
334 | _assertInvariants();
335 |
336 | // Request Funds for asset2 ilk2
337 |
338 | vm.prank(operator2);
339 | returnFundRequestId = conduit.requestFunds(ilk2, address(asset2), 300, "info");
340 |
341 | assertEq(returnFundRequestId, 7);
342 |
343 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 80);
344 | assertEq(conduit.requestedFunds(address(asset1), ilk2), 120);
345 | assertEq(conduit.requestedFunds(address(asset2), ilk1), 200);
346 | assertEq(conduit.requestedFunds(address(asset2), ilk2), 600);
347 |
348 | assertEq(conduit.totalRequestedFunds(address(asset1)), 200);
349 | assertEq(conduit.totalRequestedFunds(address(asset2)), 800);
350 |
351 | _assertInvariants();
352 | }
353 |
354 | }
355 |
--------------------------------------------------------------------------------
/test/arranger-conduit/ReturnFunds.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 | pragma solidity ^0.8.13;
3 |
4 | import { IArrangerConduit } from "../../src/interfaces/IArrangerConduit.sol";
5 |
6 | import "./ConduitTestBase.sol";
7 |
8 | contract ArrangerConduit_ReturnFundsTests is ConduitAssetTestBase {
9 |
10 | function test_returnFunds_notArranger() public {
11 | _depositAndDrawFunds(100);
12 |
13 | vm.prank(operator1);
14 | conduit.requestFunds(ilk1, address(asset1), 100, "info");
15 |
16 | vm.expectRevert("ArrangerConduit/not-arranger");
17 | conduit.returnFunds(0, 100);
18 | }
19 |
20 | function test_returnFunds_notPending_completed() external {
21 | _depositAndDrawFunds(100);
22 |
23 | vm.prank(operator1);
24 | conduit.requestFunds(ilk1, address(asset1), 100, "info");
25 |
26 | asset1.mint(address(conduit), 100);
27 |
28 | vm.startPrank(arranger);
29 |
30 | conduit.returnFunds(0, 100);
31 |
32 | vm.expectRevert("ArrangerConduit/invalid-status");
33 | conduit.returnFunds(0, 100);
34 | }
35 |
36 | function test_returnFunds_notPending_canceled() external {
37 | _depositAndDrawFunds(100);
38 |
39 | vm.startPrank(operator1);
40 |
41 | conduit.requestFunds(ilk1, address(asset1), 100, "info");
42 |
43 | conduit.cancelFundRequest(0);
44 |
45 | vm.stopPrank();
46 |
47 | vm.prank(arranger);
48 | vm.expectRevert("ArrangerConduit/invalid-status");
49 | conduit.returnFunds(0, 100);
50 | }
51 |
52 | function test_returnFunds_insufficientFundsBoundary() external {
53 | _depositAndDrawFunds(100);
54 |
55 | vm.prank(operator1);
56 | conduit.requestFunds(ilk1, address(asset1), 100, "info");
57 |
58 | asset1.mint(address(conduit), 99);
59 |
60 | vm.startPrank(arranger);
61 | vm.expectRevert("ArrangerConduit/insufficient-funds");
62 | conduit.returnFunds(0, 100);
63 |
64 | asset1.mint(address(conduit), 1);
65 |
66 | conduit.returnFunds(0, 100);
67 | }
68 |
69 | function test_returnFunds_insufficientFundsBoundaryWithWithdrawable() external {
70 | _depositAndDrawFunds(100);
71 |
72 | vm.prank(operator1);
73 | conduit.requestFunds(ilk1, address(asset1), 60, "info");
74 |
75 | asset1.mint(address(conduit), 99);
76 |
77 | vm.prank(arranger);
78 | conduit.returnFunds(0, 60);
79 |
80 | vm.prank(operator1);
81 | conduit.requestFunds(ilk1, address(asset1), 40, "info");
82 |
83 | assertEq(conduit.availableFunds(address(asset1)), 39);
84 |
85 | vm.startPrank(arranger);
86 |
87 | vm.expectRevert("ArrangerConduit/insufficient-funds");
88 | conduit.returnFunds(1, 40);
89 |
90 | asset1.mint(address(conduit), 1);
91 |
92 | assertEq(conduit.availableFunds(address(asset1)), 40);
93 |
94 | conduit.returnFunds(1, 40);
95 | }
96 |
97 | function test_returnFunds_oneRequest_exact() external {
98 | _depositAndDrawFunds(100);
99 |
100 | vm.prank(operator1);
101 | conduit.requestFunds(ilk1, address(asset1), 100, "info");
102 |
103 | asset1.mint(address(conduit), 100);
104 |
105 | IArrangerConduit.FundRequest memory fundRequest = conduit.getFundRequest(0);
106 |
107 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.PENDING);
108 |
109 | assertEq(fundRequest.asset, address(asset1));
110 | assertEq(fundRequest.ilk, ilk1);
111 | assertEq(fundRequest.amountRequested, 100);
112 | assertEq(fundRequest.amountFilled, 0);
113 | assertEq(fundRequest.info, "info");
114 |
115 | assertEq(asset1.balanceOf(address(conduit)), 100);
116 |
117 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 100);
118 | assertEq(conduit.totalRequestedFunds(address(asset1)), 100);
119 |
120 | assertEq(conduit.withdrawableFunds(address(asset1), ilk1), 0);
121 | assertEq(conduit.totalWithdrawableFunds(address(asset1)), 0);
122 |
123 | assertEq(conduit.availableFunds(address(asset1)), 100);
124 |
125 | _assertInvariants();
126 |
127 | vm.prank(arranger);
128 | conduit.returnFunds(0, 100);
129 |
130 | fundRequest = conduit.getFundRequest(0);
131 |
132 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.COMPLETED);
133 |
134 | assertEq(fundRequest.asset, address(asset1));
135 | assertEq(fundRequest.ilk, ilk1);
136 | assertEq(fundRequest.amountRequested, 100);
137 | assertEq(fundRequest.amountFilled, 100);
138 | assertEq(fundRequest.info, "info");
139 |
140 | assertEq(asset1.balanceOf(address(conduit)), 100);
141 |
142 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 0);
143 | assertEq(conduit.totalRequestedFunds(address(asset1)), 0);
144 |
145 | assertEq(conduit.withdrawableFunds(address(asset1), ilk1), 100);
146 | assertEq(conduit.totalWithdrawableFunds(address(asset1)), 100);
147 |
148 | assertEq(conduit.availableFunds(address(asset1)), 0);
149 |
150 | _assertInvariants();
151 | }
152 |
153 | // NOTE: The above test has proven that returnFunds does not change any other values in the
154 | // FundRequest struct other than amountFilled and status. Therefore, for subsequent tests
155 | // only those two values from the struct will be asserted. `amountRequested` is left in
156 | // for easier auditing.
157 |
158 | function test_returnFunds_oneRequest_under() external {
159 | _depositAndDrawFunds(100);
160 |
161 | vm.prank(operator1);
162 | conduit.requestFunds(ilk1, address(asset1), 100, "info");
163 |
164 | asset1.mint(address(conduit), 40);
165 |
166 | IArrangerConduit.FundRequest memory fundRequest = conduit.getFundRequest(0);
167 |
168 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.PENDING);
169 |
170 | assertEq(fundRequest.amountRequested, 100);
171 | assertEq(fundRequest.amountFilled, 0);
172 |
173 | assertEq(asset1.balanceOf(address(conduit)), 40);
174 |
175 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 100);
176 | assertEq(conduit.totalRequestedFunds(address(asset1)), 100);
177 |
178 | assertEq(conduit.withdrawableFunds(address(asset1), ilk1), 0);
179 | assertEq(conduit.totalWithdrawableFunds(address(asset1)), 0);
180 |
181 | assertEq(conduit.availableFunds(address(asset1)), 40);
182 |
183 | _assertInvariants();
184 |
185 | vm.prank(arranger);
186 | conduit.returnFunds(0, 40);
187 |
188 | fundRequest = conduit.getFundRequest(0);
189 |
190 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.COMPLETED);
191 |
192 | assertEq(fundRequest.amountRequested, 100);
193 | assertEq(fundRequest.amountFilled, 40);
194 |
195 | assertEq(asset1.balanceOf(address(conduit)), 40);
196 |
197 | // Goes to zero because amount is reduced by requestedAmount even on partial fills
198 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 0);
199 | assertEq(conduit.totalRequestedFunds(address(asset1)), 0);
200 |
201 | assertEq(conduit.withdrawableFunds(address(asset1), ilk1), 40);
202 | assertEq(conduit.totalWithdrawableFunds(address(asset1)), 40);
203 |
204 | assertEq(conduit.availableFunds(address(asset1)), 0);
205 |
206 | _assertInvariants();
207 | }
208 |
209 | function test_returnFunds_oneIlk_twoRequests_exact_under() external {
210 | _depositAndDrawFunds(100);
211 |
212 | vm.startPrank(operator1);
213 |
214 | conduit.requestFunds(ilk1, address(asset1), 20, "info");
215 | conduit.requestFunds(ilk1, address(asset1), 80, "info");
216 |
217 | vm.stopPrank();
218 |
219 | asset1.mint(address(conduit), 20);
220 |
221 | IArrangerConduit.FundRequest memory fundRequest = conduit.getFundRequest(0);
222 |
223 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.PENDING);
224 |
225 | assertEq(fundRequest.amountRequested, 20);
226 | assertEq(fundRequest.amountFilled, 0);
227 |
228 | fundRequest = conduit.getFundRequest(1);
229 |
230 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.PENDING);
231 |
232 | assertEq(fundRequest.amountRequested, 80);
233 | assertEq(fundRequest.amountFilled, 0);
234 |
235 | assertEq(asset1.balanceOf(address(conduit)), 20);
236 |
237 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 100);
238 | assertEq(conduit.totalRequestedFunds(address(asset1)), 100);
239 |
240 | assertEq(conduit.withdrawableFunds(address(asset1), ilk1), 0);
241 | assertEq(conduit.totalWithdrawableFunds(address(asset1)), 0);
242 |
243 | assertEq(conduit.availableFunds(address(asset1)), 20);
244 |
245 | _assertInvariants();
246 |
247 | vm.prank(arranger);
248 | conduit.returnFunds(0, 20);
249 |
250 | fundRequest = conduit.getFundRequest(0);
251 |
252 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.COMPLETED);
253 |
254 | assertEq(fundRequest.amountRequested, 20);
255 | assertEq(fundRequest.amountFilled, 20);
256 |
257 | assertEq(asset1.balanceOf(address(conduit)), 20);
258 |
259 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 80);
260 | assertEq(conduit.totalRequestedFunds(address(asset1)), 80);
261 |
262 | assertEq(conduit.withdrawableFunds(address(asset1), ilk1), 20);
263 | assertEq(conduit.totalWithdrawableFunds(address(asset1)), 20);
264 |
265 | assertEq(conduit.availableFunds(address(asset1)), 0);
266 |
267 | _assertInvariants();
268 |
269 | asset1.mint(address(conduit), 40);
270 |
271 | vm.prank(arranger);
272 | conduit.returnFunds(1, 40);
273 |
274 | fundRequest = conduit.getFundRequest(1);
275 |
276 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.COMPLETED);
277 |
278 | assertEq(fundRequest.amountRequested, 80);
279 | assertEq(fundRequest.amountFilled, 40);
280 |
281 | assertEq(asset1.balanceOf(address(conduit)), 60);
282 |
283 | // Goes to zero because amount is reduced by requestedAmount even on partial fills
284 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 0);
285 | assertEq(conduit.totalRequestedFunds(address(asset1)), 0);
286 |
287 | assertEq(conduit.withdrawableFunds(address(asset1), ilk1), 60);
288 | assertEq(conduit.totalWithdrawableFunds(address(asset1)), 60);
289 |
290 | assertEq(conduit.availableFunds(address(asset1)), 0);
291 |
292 | _assertInvariants();
293 | }
294 |
295 | function test_returnFunds_twoIlks_twoRequests_under_over() external {
296 | asset1.mint(buffer1, 40);
297 | asset1.mint(buffer2, 60);
298 |
299 | vm.prank(operator1);
300 | conduit.deposit(ilk1, address(asset1), 40);
301 |
302 | vm.prank(operator2);
303 | conduit.deposit(ilk2, address(asset1), 60);
304 |
305 | vm.prank(arranger);
306 | conduit.drawFunds(address(asset1), broker1, 100);
307 |
308 | vm.prank(operator1);
309 | conduit.requestFunds(ilk1, address(asset1), 40, "info");
310 |
311 | vm.prank(operator2);
312 | conduit.requestFunds(ilk2, address(asset1), 60, "info");
313 |
314 | asset1.mint(address(conduit), 20);
315 |
316 | IArrangerConduit.FundRequest memory fundRequest = conduit.getFundRequest(0);
317 |
318 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.PENDING);
319 |
320 | assertEq(fundRequest.amountRequested, 40);
321 | assertEq(fundRequest.amountFilled, 0);
322 |
323 | assertEq(asset1.balanceOf(address(conduit)), 20);
324 |
325 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 40);
326 | assertEq(conduit.requestedFunds(address(asset1), ilk2), 60);
327 | assertEq(conduit.totalRequestedFunds(address(asset1)), 100);
328 |
329 | assertEq(conduit.withdrawableFunds(address(asset1), ilk1), 0);
330 | assertEq(conduit.withdrawableFunds(address(asset1), ilk2), 0);
331 | assertEq(conduit.totalWithdrawableFunds(address(asset1)), 0);
332 |
333 | assertEq(conduit.availableFunds(address(asset1)), 20);
334 |
335 | _assertInvariants();
336 |
337 | vm.prank(arranger);
338 | conduit.returnFunds(0, 20);
339 |
340 | fundRequest = conduit.getFundRequest(0);
341 |
342 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.COMPLETED);
343 |
344 | assertEq(fundRequest.amountRequested, 40);
345 | assertEq(fundRequest.amountFilled, 20);
346 |
347 | assertEq(asset1.balanceOf(address(conduit)), 20);
348 |
349 | // Gets reduced by full ilk1 request
350 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 0);
351 | assertEq(conduit.requestedFunds(address(asset1), ilk2), 60);
352 | assertEq(conduit.totalRequestedFunds(address(asset1)), 60);
353 |
354 | assertEq(conduit.withdrawableFunds(address(asset1), ilk1), 20);
355 | assertEq(conduit.withdrawableFunds(address(asset1), ilk2), 0);
356 | assertEq(conduit.totalWithdrawableFunds(address(asset1)), 20);
357 |
358 | assertEq(conduit.availableFunds(address(asset1)), 0);
359 |
360 | _assertInvariants();
361 |
362 | asset1.mint(address(conduit), 80);
363 |
364 | vm.prank(arranger);
365 | conduit.returnFunds(1, 80);
366 |
367 | fundRequest = conduit.getFundRequest(1);
368 |
369 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.COMPLETED);
370 |
371 | assertEq(fundRequest.amountRequested, 60);
372 | assertEq(fundRequest.amountFilled, 80);
373 |
374 | assertEq(asset1.balanceOf(address(conduit)), 100);
375 |
376 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 0);
377 | assertEq(conduit.requestedFunds(address(asset1), ilk2), 0);
378 | assertEq(conduit.totalRequestedFunds(address(asset1)), 0);
379 |
380 | assertEq(conduit.withdrawableFunds(address(asset1), ilk1), 20);
381 | assertEq(conduit.withdrawableFunds(address(asset1), ilk2), 80);
382 | assertEq(conduit.totalWithdrawableFunds(address(asset1)), 100);
383 |
384 | assertEq(conduit.availableFunds(address(asset1)), 0);
385 |
386 | _assertInvariants();
387 | }
388 |
389 | // NOTE: This test performs one bulk transfer of assets at the beginning to battle test
390 | // accounting under that scenario.
391 | function test_returnFunds_twoIlks_twoAssets_outOfOrder_over_under_under_under() external {
392 | asset1.mint(buffer1, 40);
393 | asset1.mint(buffer2, 60);
394 | asset2.mint(buffer1, 100);
395 | asset2.mint(buffer2, 300);
396 |
397 | vm.startPrank(operator1);
398 |
399 | conduit.deposit(ilk1, address(asset1), 40);
400 | conduit.deposit(ilk1, address(asset2), 100);
401 |
402 | vm.stopPrank();
403 |
404 | vm.startPrank(operator2);
405 |
406 | conduit.deposit(ilk2, address(asset1), 60);
407 | conduit.deposit(ilk2, address(asset2), 300);
408 |
409 | vm.startPrank(arranger);
410 |
411 | conduit.drawFunds(address(asset1), broker1, 100);
412 | conduit.drawFunds(address(asset2), broker2, 400);
413 |
414 | vm.stopPrank();
415 |
416 | vm.prank(operator1);
417 | conduit.requestFunds(ilk1, address(asset1), 40, "info");
418 |
419 | vm.prank(operator2);
420 | conduit.requestFunds(ilk2, address(asset1), 60, "info");
421 |
422 | vm.prank(operator1);
423 | conduit.requestFunds(ilk1, address(asset2), 100, "info");
424 |
425 | vm.prank(operator2);
426 | conduit.requestFunds(ilk2, address(asset2), 300, "info");
427 |
428 | /**************************************/
429 | /*** Before state for all positions ***/
430 | /**************************************/
431 |
432 | // Ilk 1 asset 1
433 |
434 | IArrangerConduit.FundRequest memory fundRequest = conduit.getFundRequest(0);
435 |
436 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.PENDING);
437 |
438 | assertEq(fundRequest.amountRequested, 40);
439 | assertEq(fundRequest.amountFilled, 0);
440 |
441 | // Ilk 2 asset 1
442 |
443 | fundRequest = conduit.getFundRequest(1);
444 |
445 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.PENDING);
446 |
447 | assertEq(fundRequest.amountRequested, 60);
448 | assertEq(fundRequest.amountFilled, 0);
449 |
450 | // Ilk 1 asset 2
451 |
452 | fundRequest = conduit.getFundRequest(2);
453 |
454 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.PENDING);
455 |
456 | assertEq(fundRequest.amountRequested, 100);
457 | assertEq(fundRequest.amountFilled, 0);
458 |
459 | // Ilk 2 asset 2
460 |
461 | fundRequest = conduit.getFundRequest(3);
462 |
463 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.PENDING);
464 |
465 | assertEq(fundRequest.amountRequested, 300);
466 | assertEq(fundRequest.amountFilled, 0);
467 |
468 | // Mint all the funds requested into the conduit, they will not correspond to the amounts
469 | // used in the returnFunds function.
470 | asset1.mint(address(conduit), 100);
471 | asset2.mint(address(conduit), 400);
472 |
473 | // Balance assertions are maintained throughout the test to demonstrate no change.
474 | assertEq(asset1.balanceOf(address(conduit)), 100);
475 | assertEq(asset2.balanceOf(address(conduit)), 400);
476 |
477 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 40);
478 | assertEq(conduit.requestedFunds(address(asset1), ilk2), 60);
479 | assertEq(conduit.requestedFunds(address(asset2), ilk1), 100);
480 | assertEq(conduit.requestedFunds(address(asset2), ilk2), 300);
481 |
482 | assertEq(conduit.totalRequestedFunds(address(asset1)), 100);
483 | assertEq(conduit.totalRequestedFunds(address(asset2)), 400);
484 |
485 | assertEq(conduit.withdrawableFunds(address(asset1), ilk1), 0);
486 | assertEq(conduit.withdrawableFunds(address(asset1), ilk2), 0);
487 | assertEq(conduit.withdrawableFunds(address(asset2), ilk1), 0);
488 | assertEq(conduit.withdrawableFunds(address(asset2), ilk2), 0);
489 |
490 | assertEq(conduit.totalWithdrawableFunds(address(asset1)), 0);
491 | assertEq(conduit.totalWithdrawableFunds(address(asset2)), 0);
492 |
493 | assertEq(conduit.availableFunds(address(asset1)), 100);
494 | assertEq(conduit.availableFunds(address(asset2)), 400);
495 |
496 | _assertInvariants();
497 |
498 | /**************************************************************************/
499 | /*** Return funds for FundRequest 2 BEFORE FundRequest 0 (Over request) ***/
500 | /**************************************************************************/
501 |
502 | vm.prank(arranger);
503 | conduit.returnFunds(1, 70);
504 |
505 | // Assert that request 0 is untouched
506 |
507 | fundRequest = conduit.getFundRequest(0);
508 |
509 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.PENDING);
510 |
511 | assertEq(fundRequest.amountRequested, 40);
512 | assertEq(fundRequest.amountFilled, 0);
513 |
514 | fundRequest = conduit.getFundRequest(1);
515 |
516 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.COMPLETED);
517 |
518 | assertEq(fundRequest.amountRequested, 60);
519 | assertEq(fundRequest.amountFilled, 70);
520 |
521 | assertEq(asset1.balanceOf(address(conduit)), 100);
522 | assertEq(asset2.balanceOf(address(conduit)), 400);
523 |
524 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 40);
525 | assertEq(conduit.requestedFunds(address(asset1), ilk2), 0);
526 | assertEq(conduit.requestedFunds(address(asset2), ilk1), 100);
527 | assertEq(conduit.requestedFunds(address(asset2), ilk2), 300);
528 |
529 | assertEq(conduit.totalRequestedFunds(address(asset1)), 40);
530 | assertEq(conduit.totalRequestedFunds(address(asset2)), 400);
531 |
532 | assertEq(conduit.withdrawableFunds(address(asset1), ilk1), 0);
533 | assertEq(conduit.withdrawableFunds(address(asset1), ilk2), 70);
534 | assertEq(conduit.withdrawableFunds(address(asset2), ilk1), 0);
535 | assertEq(conduit.withdrawableFunds(address(asset2), ilk2), 0);
536 |
537 | assertEq(conduit.totalWithdrawableFunds(address(asset1)), 70);
538 | assertEq(conduit.totalWithdrawableFunds(address(asset2)), 0);
539 |
540 | assertEq(conduit.availableFunds(address(asset1)), 30);
541 | assertEq(conduit.availableFunds(address(asset2)), 400);
542 |
543 | _assertInvariants();
544 |
545 | /***************************************************************************/
546 | /*** Return funds for FundRequest 3 BEFORE FundRequest 2 (Under request) ***/
547 | /***************************************************************************/
548 |
549 | vm.prank(arranger);
550 | conduit.returnFunds(3, 150);
551 |
552 | // Assert that request 2 is untouched
553 |
554 | fundRequest = conduit.getFundRequest(2);
555 |
556 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.PENDING);
557 |
558 | assertEq(fundRequest.amountRequested, 100);
559 | assertEq(fundRequest.amountFilled, 0);
560 |
561 | fundRequest = conduit.getFundRequest(3);
562 |
563 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.COMPLETED);
564 |
565 | assertEq(fundRequest.amountRequested, 300);
566 | assertEq(fundRequest.amountFilled, 150);
567 |
568 | assertEq(asset1.balanceOf(address(conduit)), 100);
569 | assertEq(asset2.balanceOf(address(conduit)), 400);
570 |
571 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 40);
572 | assertEq(conduit.requestedFunds(address(asset1), ilk2), 0);
573 | assertEq(conduit.requestedFunds(address(asset2), ilk1), 100);
574 | assertEq(conduit.requestedFunds(address(asset2), ilk2), 0);
575 |
576 | assertEq(conduit.totalRequestedFunds(address(asset1)), 40);
577 | assertEq(conduit.totalRequestedFunds(address(asset2)), 100);
578 |
579 | assertEq(conduit.withdrawableFunds(address(asset1), ilk1), 0);
580 | assertEq(conduit.withdrawableFunds(address(asset1), ilk2), 70);
581 | assertEq(conduit.withdrawableFunds(address(asset2), ilk1), 0);
582 | assertEq(conduit.withdrawableFunds(address(asset2), ilk2), 150);
583 |
584 | assertEq(conduit.totalWithdrawableFunds(address(asset1)), 70);
585 | assertEq(conduit.totalWithdrawableFunds(address(asset2)), 150);
586 |
587 | assertEq(conduit.availableFunds(address(asset1)), 30);
588 | assertEq(conduit.availableFunds(address(asset2)), 250);
589 |
590 | _assertInvariants();
591 |
592 | /******************************************************/
593 | /*** Return funds for FundRequest 0 (Under request) ***/
594 | /******************************************************/
595 |
596 | vm.prank(arranger);
597 | conduit.returnFunds(0, 30);
598 |
599 | fundRequest = conduit.getFundRequest(0);
600 |
601 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.COMPLETED);
602 |
603 | assertEq(fundRequest.amountRequested, 40);
604 | assertEq(fundRequest.amountFilled, 30);
605 |
606 | assertEq(asset1.balanceOf(address(conduit)), 100);
607 | assertEq(asset2.balanceOf(address(conduit)), 400);
608 |
609 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 0);
610 | assertEq(conduit.requestedFunds(address(asset1), ilk2), 0);
611 | assertEq(conduit.requestedFunds(address(asset2), ilk1), 100);
612 | assertEq(conduit.requestedFunds(address(asset2), ilk2), 0);
613 |
614 | assertEq(conduit.totalRequestedFunds(address(asset1)), 0);
615 | assertEq(conduit.totalRequestedFunds(address(asset2)), 100);
616 |
617 | assertEq(conduit.withdrawableFunds(address(asset1), ilk1), 30);
618 | assertEq(conduit.withdrawableFunds(address(asset1), ilk2), 70);
619 | assertEq(conduit.withdrawableFunds(address(asset2), ilk1), 0);
620 | assertEq(conduit.withdrawableFunds(address(asset2), ilk2), 150);
621 |
622 | assertEq(conduit.totalWithdrawableFunds(address(asset1)), 100);
623 | assertEq(conduit.totalWithdrawableFunds(address(asset2)), 150);
624 |
625 | _assertInvariants();
626 |
627 | assertEq(conduit.availableFunds(address(asset1)), 0);
628 | assertEq(conduit.availableFunds(address(asset2)), 250);
629 |
630 | /******************************************************/
631 | /*** Return funds for FundRequest 2 (Under request) ***/
632 | /******************************************************/
633 |
634 | vm.prank(arranger);
635 | conduit.returnFunds(2, 60);
636 |
637 | fundRequest = conduit.getFundRequest(2);
638 |
639 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.COMPLETED);
640 |
641 | assertEq(fundRequest.amountRequested, 100);
642 | assertEq(fundRequest.amountFilled, 60);
643 |
644 | assertEq(asset1.balanceOf(address(conduit)), 100);
645 | assertEq(asset2.balanceOf(address(conduit)), 400);
646 |
647 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 0);
648 | assertEq(conduit.requestedFunds(address(asset1), ilk2), 0);
649 | assertEq(conduit.requestedFunds(address(asset2), ilk1), 0);
650 | assertEq(conduit.requestedFunds(address(asset2), ilk2), 0);
651 |
652 | assertEq(conduit.totalRequestedFunds(address(asset1)), 0);
653 | assertEq(conduit.totalRequestedFunds(address(asset2)), 0);
654 |
655 | assertEq(conduit.withdrawableFunds(address(asset1), ilk1), 30);
656 | assertEq(conduit.withdrawableFunds(address(asset1), ilk2), 70);
657 | assertEq(conduit.withdrawableFunds(address(asset2), ilk1), 60);
658 | assertEq(conduit.withdrawableFunds(address(asset2), ilk2), 150);
659 |
660 | assertEq(conduit.totalWithdrawableFunds(address(asset1)), 100);
661 | assertEq(conduit.totalWithdrawableFunds(address(asset2)), 210);
662 |
663 | assertEq(conduit.availableFunds(address(asset1)), 0);
664 | assertEq(conduit.availableFunds(address(asset2)), 190);
665 |
666 | _assertInvariants();
667 | }
668 |
669 | function test_returnFundsWithoutDrawing() external {
670 | asset1.mint(buffer1, 100);
671 |
672 | vm.startPrank(operator1);
673 |
674 | conduit.deposit(ilk1, address(asset1), 100);
675 | conduit.requestFunds(ilk1, address(asset1), 100, "info");
676 |
677 | vm.stopPrank();
678 |
679 | IArrangerConduit.FundRequest memory fundRequest = conduit.getFundRequest(0);
680 |
681 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.PENDING);
682 |
683 | assertEq(fundRequest.asset, address(asset1));
684 | assertEq(fundRequest.ilk, ilk1);
685 | assertEq(fundRequest.amountRequested, 100);
686 | assertEq(fundRequest.amountFilled, 0);
687 | assertEq(fundRequest.info, "info");
688 |
689 | assertEq(asset1.balanceOf(address(conduit)), 100);
690 |
691 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 100);
692 | assertEq(conduit.totalRequestedFunds(address(asset1)), 100);
693 |
694 | assertEq(conduit.withdrawableFunds(address(asset1), ilk1), 0);
695 | assertEq(conduit.totalWithdrawableFunds(address(asset1)), 0);
696 |
697 | assertEq(conduit.availableFunds(address(asset1)), 100);
698 |
699 | _assertInvariants();
700 |
701 | vm.prank(arranger);
702 | conduit.returnFunds(0, 100);
703 |
704 | fundRequest = conduit.getFundRequest(0);
705 |
706 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.COMPLETED);
707 |
708 | assertEq(fundRequest.asset, address(asset1));
709 | assertEq(fundRequest.ilk, ilk1);
710 | assertEq(fundRequest.amountRequested, 100);
711 | assertEq(fundRequest.amountFilled, 100);
712 | assertEq(fundRequest.info, "info");
713 |
714 | assertEq(asset1.balanceOf(address(conduit)), 100);
715 |
716 | assertEq(conduit.requestedFunds(address(asset1), ilk1), 0);
717 | assertEq(conduit.totalRequestedFunds(address(asset1)), 0);
718 |
719 | assertEq(conduit.withdrawableFunds(address(asset1), ilk1), 100);
720 | assertEq(conduit.totalWithdrawableFunds(address(asset1)), 100);
721 |
722 | assertEq(conduit.availableFunds(address(asset1)), 0);
723 |
724 | _assertInvariants();
725 | }
726 |
727 | }
728 |
--------------------------------------------------------------------------------
/test/arranger-conduit/ViewFunctions.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 | pragma solidity ^0.8.13;
3 |
4 | import { IArrangerConduit } from "../../src/interfaces/IArrangerConduit.sol";
5 |
6 | import { ArrangerConduitHarness } from "./ArrangerConduitHarness.sol";
7 |
8 | import "./ConduitTestBase.sol";
9 |
10 | contract ArrangerConduit_DrawableFundsTest is ConduitAssetTestBase {
11 |
12 | ArrangerConduitHarness conduitHarness;
13 |
14 | function setUp() public override {
15 | UpgradeableProxy conduitProxy = new UpgradeableProxy();
16 | ArrangerConduitHarness conduitImplementation = new ArrangerConduitHarness();
17 |
18 | conduitProxy.setImplementation(address(conduitImplementation));
19 |
20 | conduitHarness = ArrangerConduitHarness(address(conduitProxy));
21 | }
22 |
23 | function testFuzz_drawableFunds(
24 | uint256 mintAmount1,
25 | uint256 withdrawableFundsAmount1,
26 | uint256 mintAmount2,
27 | uint256 withdrawableFundsAmount2
28 | )
29 | external
30 | {
31 | // `withdrawableFunds` can never be higher than balance
32 | mintAmount1 = _bound(mintAmount1, withdrawableFundsAmount1, type(uint256).max);
33 | mintAmount2 = _bound(mintAmount2, withdrawableFundsAmount2, type(uint256).max);
34 |
35 | asset1.mint(address(conduitHarness), mintAmount1);
36 | asset2.mint(address(conduitHarness), mintAmount2);
37 |
38 | conduitHarness.__setTotalWithdrawableFunds(address(asset1), withdrawableFundsAmount1);
39 | conduitHarness.__setTotalWithdrawableFunds(address(asset2), withdrawableFundsAmount2);
40 |
41 | assertEq(
42 | conduitHarness.availableFunds(address(asset1)),
43 | asset1.balanceOf(address(conduitHarness)) - withdrawableFundsAmount1
44 | );
45 | assertEq(
46 | conduitHarness.availableFunds(address(asset2)),
47 | asset2.balanceOf(address(conduitHarness)) - withdrawableFundsAmount2
48 | );
49 | }
50 |
51 | }
52 |
53 | contract ArrangerConduit_GetFundRequestTest is ConduitAssetTestBase {
54 |
55 | function test_getFundRequest() external {
56 | asset1.mint(buffer1, 100);
57 |
58 | vm.startPrank(operator1);
59 |
60 | conduit.deposit(ilk1, address(asset1), 100);
61 |
62 | conduit.requestFunds(ilk1, address(asset1), 100, "info");
63 |
64 | IArrangerConduit.FundRequest memory fundRequest = conduit.getFundRequest(0);
65 |
66 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.PENDING);
67 |
68 | assertEq(fundRequest.asset, address(asset1));
69 | assertEq(fundRequest.ilk, ilk1);
70 | assertEq(fundRequest.amountRequested, 100);
71 | assertEq(fundRequest.amountFilled, 0);
72 | assertEq(fundRequest.info, "info");
73 |
74 | conduit.requestFunds(ilk1, address(asset1), 200, "info2");
75 |
76 | fundRequest = conduit.getFundRequest(1);
77 |
78 | assertTrue(fundRequest.status == IArrangerConduit.StatusEnum.PENDING);
79 |
80 | assertEq(fundRequest.asset, address(asset1));
81 | assertEq(fundRequest.ilk, ilk1);
82 | assertEq(fundRequest.amountRequested, 200);
83 | assertEq(fundRequest.amountFilled, 0);
84 | assertEq(fundRequest.info, "info2");
85 | }
86 |
87 | }
88 |
89 | contract ArrangerConduit_GetFundRequestsLengthTest is ConduitAssetTestBase {
90 |
91 | function test_getFundRequestsLength() external {
92 | asset1.mint(buffer1, 100);
93 |
94 | vm.startPrank(operator1);
95 |
96 | conduit.deposit(ilk1, address(asset1), 100);
97 |
98 | assertEq(conduit.getFundRequestsLength(), 0);
99 |
100 | conduit.requestFunds(ilk1, address(asset1), 100, "info");
101 |
102 | assertEq(conduit.getFundRequestsLength(), 1);
103 |
104 | conduit.requestFunds(ilk1, address(asset1), 100, "info");
105 |
106 | assertEq(conduit.getFundRequestsLength(), 2);
107 |
108 | vm.stopPrank();
109 |
110 | vm.startPrank(arranger);
111 |
112 | conduit.drawFunds(address(asset1), broker1, 100);
113 | asset1.mint(address(conduit), 100);
114 | conduit.returnFunds(0, 40);
115 |
116 | assertEq(conduit.getFundRequestsLength(), 2); // Returning funds does not change length
117 | }
118 |
119 | }
120 |
121 | contract ArrangerConduit_IsCancelableTest is ConduitAssetTestBase {
122 |
123 | ArrangerConduitHarness conduitHarness;
124 |
125 | function setUp() public override {
126 | super.setUp();
127 |
128 | ArrangerConduitHarness conduitImplementation = new ArrangerConduitHarness();
129 |
130 | conduitProxy.setImplementation(address(conduitImplementation));
131 |
132 | conduitHarness = ArrangerConduitHarness(address(conduitProxy));
133 | }
134 |
135 | function test_isCancelable() external {
136 | asset1.mint(buffer1, 100);
137 |
138 | vm.startPrank(operator1);
139 |
140 | conduitHarness.deposit(ilk1, address(asset1), 100);
141 |
142 | conduitHarness.requestFunds(ilk1, address(asset1), 100, "info");
143 |
144 | IArrangerConduit.FundRequest memory fundRequest = conduitHarness.getFundRequest(0);
145 |
146 | assertEq(uint256(fundRequest.status), uint256(IArrangerConduit.StatusEnum.PENDING));
147 |
148 | assertEq(conduitHarness.isCancelable(0), true);
149 |
150 | conduitHarness.__setFundRequestStatus(0, IArrangerConduit.StatusEnum.CANCELLED);
151 |
152 | assertEq(conduitHarness.isCancelable(0), false);
153 |
154 | conduitHarness.__setFundRequestStatus(0, IArrangerConduit.StatusEnum.COMPLETED);
155 |
156 | assertEq(conduitHarness.isCancelable(0), false);
157 | }
158 |
159 | }
160 |
161 | contract ArrangerConduit_MaxDepositTests is ConduitTestBase {
162 |
163 | function testFuzz_maxDepositTest(bytes32 ilk, address asset) external {
164 | assertEq(conduit.maxDeposit(ilk, asset), type(uint256).max);
165 | }
166 |
167 | }
168 |
169 | contract ArrangerConduit_MaxWithdrawTest is ConduitAssetTestBase {
170 |
171 | ArrangerConduitHarness conduitHarness;
172 |
173 | function setUp() public override {
174 | UpgradeableProxy conduitProxy = new UpgradeableProxy();
175 | ArrangerConduitHarness conduitImplementation = new ArrangerConduitHarness();
176 |
177 | conduitProxy.setImplementation(address(conduitImplementation));
178 |
179 | conduitHarness = ArrangerConduitHarness(address(conduitProxy));
180 | }
181 |
182 | function testFuzz_maxWithdraw(
183 | address asset1_,
184 | address asset2_,
185 | uint256 amount1,
186 | uint256 amount2,
187 | uint256 amount3
188 | )
189 | external
190 | {
191 | vm.assume(asset1_ != asset2_);
192 |
193 | conduitHarness.__setWithdrawableFunds(ilk1, asset1_, amount1);
194 | conduitHarness.__setWithdrawableFunds(ilk1, asset2_, amount2);
195 |
196 | assertEq(conduitHarness.maxWithdraw(ilk1, asset1_), amount1);
197 | assertEq(conduitHarness.maxWithdraw(ilk1, asset2_), amount2);
198 |
199 | amount3 = _bound(amount3, 0, type(uint256).max - amount1);
200 |
201 | conduitHarness.__setWithdrawableFunds(ilk1, asset1_, amount1 + amount3);
202 |
203 | assertEq(conduitHarness.maxWithdraw(ilk1, asset1_), amount1 + amount3);
204 | }
205 |
206 | }
207 |
--------------------------------------------------------------------------------
/test/arranger-conduit/Withdraw.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 | pragma solidity ^0.8.13;
3 |
4 | import "./ConduitTestBase.sol";
5 |
6 | contract ArrangerConduit_WithdrawTests is ConduitAssetTestBase {
7 |
8 | function test_withdraw_noIlkAuth() external {
9 | asset1.mint(buffer1, 100);
10 |
11 | vm.startPrank(operator1);
12 |
13 | conduit.deposit(ilk1, address(asset1), 100);
14 | conduit.requestFunds(ilk1, address(asset1), 100, "info");
15 |
16 | vm.stopPrank();
17 |
18 | vm.expectRevert("ArrangerConduit/not-authorized");
19 | conduit.withdraw(ilk1, address(asset1), 100);
20 | }
21 |
22 | function test_withdraw_noBufferRegistered() external {
23 | _depositAndDrawFunds(100);
24 |
25 | vm.prank(operator1);
26 | conduit.requestFunds(ilk1, address(asset1), 100, "info");
27 |
28 | asset1.mint(address(conduit), 100);
29 |
30 | vm.prank(arranger);
31 | conduit.returnFunds(0, 100);
32 |
33 | registry.file(ilk1, "buffer", address(0));
34 |
35 | vm.prank(operator1);
36 | vm.expectRevert("ArrangerConduit/no-buffer-registered");
37 | conduit.withdraw(ilk1, address(asset1), 100);
38 |
39 | registry.file(ilk1, "buffer", operator1);
40 |
41 | vm.prank(operator1);
42 | conduit.withdraw(ilk1, address(asset1), 100);
43 | }
44 |
45 | function test_withdraw_moreThanWithdrawable() external {
46 | _depositAndDrawFunds(100);
47 |
48 | vm.prank(operator1);
49 | conduit.requestFunds(ilk1, address(asset1), 100, "info");
50 |
51 | asset1.mint(address(conduit), 100);
52 |
53 | vm.prank(arranger);
54 | conduit.returnFunds(0, 100);
55 |
56 | assertEq(asset1.balanceOf(address(conduit)), 100);
57 | assertEq(asset1.balanceOf(buffer1), 0);
58 |
59 | assertEq(conduit.withdrawableFunds(address(asset1), ilk1), 100);
60 | assertEq(conduit.totalWithdrawableFunds(address(asset1)), 100);
61 | assertEq(conduit.withdrawals(address(asset1), ilk1), 0);
62 | assertEq(conduit.totalWithdrawals(address(asset1)), 0);
63 |
64 | // Try to withdraw 200 when only 100 is available
65 | // (using instead of 101 to show its not because of rounding)
66 | vm.prank(operator1);
67 | uint256 amount = conduit.withdraw(ilk1, address(asset1), 200);
68 |
69 | assertEq(amount, 100); // Receive max, which is 100
70 |
71 | assertEq(asset1.balanceOf(address(conduit)), 0);
72 | assertEq(asset1.balanceOf(buffer1), 100);
73 |
74 | assertEq(conduit.withdrawableFunds(address(asset1), ilk1), 0);
75 | assertEq(conduit.totalWithdrawableFunds(address(asset1)), 0);
76 | assertEq(conduit.withdrawals(address(asset1), ilk1), 100);
77 | assertEq(conduit.totalWithdrawals(address(asset1)), 100);
78 | }
79 |
80 | function test_withdraw_singleIlk() external {
81 | _depositAndDrawFunds(100);
82 |
83 | vm.prank(operator1);
84 | conduit.requestFunds(ilk1, address(asset1), 100, "info");
85 |
86 | asset1.mint(address(conduit), 100);
87 |
88 | vm.prank(arranger);
89 | conduit.returnFunds(0, 100);
90 |
91 | assertEq(asset1.balanceOf(address(conduit)), 100);
92 | assertEq(asset1.balanceOf(buffer1), 0);
93 |
94 | assertEq(conduit.withdrawableFunds(address(asset1), ilk1), 100);
95 | assertEq(conduit.totalWithdrawableFunds(address(asset1)), 100);
96 | assertEq(conduit.withdrawals(address(asset1), ilk1), 0);
97 | assertEq(conduit.totalWithdrawals(address(asset1)), 0);
98 |
99 | vm.prank(operator1);
100 | uint256 amount = conduit.withdraw(ilk1, address(asset1), 100);
101 |
102 | assertEq(amount, 100);
103 |
104 | assertEq(asset1.balanceOf(address(conduit)), 0);
105 | assertEq(asset1.balanceOf(buffer1), 100);
106 |
107 | assertEq(conduit.withdrawableFunds(address(asset1), ilk1), 0);
108 | assertEq(conduit.totalWithdrawableFunds(address(asset1)), 0);
109 | assertEq(conduit.withdrawals(address(asset1), ilk1), 100);
110 | assertEq(conduit.totalWithdrawals(address(asset1)), 100);
111 | }
112 |
113 | function test_withdraw_multiIlk_over_under_partial_full_full() external {
114 | _depositAndDrawFunds(100);
115 | _depositAndDrawFunds(asset1, operator2, buffer2, broker1, ilk2, 400);
116 |
117 | vm.prank(operator1);
118 | conduit.requestFunds(ilk1, address(asset1), 100, "info");
119 |
120 | vm.prank(operator2);
121 | conduit.requestFunds(ilk2, address(asset1), 400, "info");
122 |
123 | asset1.mint(address(conduit), 500);
124 |
125 | vm.startPrank(arranger);
126 |
127 | conduit.returnFunds(0, 200); // Over by 100
128 | conduit.returnFunds(1, 300); // Under by 100
129 |
130 | vm.stopPrank();
131 |
132 | assertEq(asset1.balanceOf(address(conduit)), 500);
133 | assertEq(asset1.balanceOf(buffer1), 0);
134 | assertEq(asset1.balanceOf(buffer2), 0);
135 |
136 | assertEq(conduit.withdrawableFunds(address(asset1), ilk1), 200);
137 | assertEq(conduit.withdrawableFunds(address(asset1), ilk2), 300);
138 | assertEq(conduit.totalWithdrawableFunds(address(asset1)), 500);
139 | assertEq(conduit.withdrawals(address(asset1), ilk1), 0);
140 | assertEq(conduit.withdrawals(address(asset1), ilk2), 0);
141 | assertEq(conduit.totalWithdrawals(address(asset1)), 0);
142 |
143 | // Partial withdraw ilk1 1
144 |
145 | vm.prank(operator1);
146 | uint256 amount = conduit.withdraw(ilk1, address(asset1), 50);
147 |
148 | assertEq(amount, 50);
149 |
150 | assertEq(asset1.balanceOf(address(conduit)), 450);
151 | assertEq(asset1.balanceOf(buffer1), 50);
152 | assertEq(asset1.balanceOf(buffer2), 0);
153 |
154 | assertEq(conduit.withdrawableFunds(address(asset1), ilk1), 150);
155 | assertEq(conduit.withdrawableFunds(address(asset1), ilk2), 300);
156 | assertEq(conduit.totalWithdrawableFunds(address(asset1)), 450);
157 | assertEq(conduit.withdrawals(address(asset1), ilk1), 50);
158 | assertEq(conduit.withdrawals(address(asset1), ilk2), 0);
159 | assertEq(conduit.totalWithdrawals(address(asset1)), 50);
160 |
161 | // Finish withdraw ilk1 1
162 |
163 | vm.prank(operator1);
164 | amount = conduit.withdraw(ilk1, address(asset1), 150);
165 |
166 | assertEq(amount, 150);
167 |
168 | assertEq(asset1.balanceOf(address(conduit)), 300);
169 | assertEq(asset1.balanceOf(buffer1), 200);
170 | assertEq(asset1.balanceOf(buffer2), 0);
171 |
172 | assertEq(conduit.withdrawableFunds(address(asset1), ilk1), 0);
173 | assertEq(conduit.withdrawableFunds(address(asset1), ilk2), 300);
174 | assertEq(conduit.totalWithdrawableFunds(address(asset1)), 300);
175 | assertEq(conduit.withdrawals(address(asset1), ilk1), 200);
176 | assertEq(conduit.withdrawals(address(asset1), ilk2), 0);
177 | assertEq(conduit.totalWithdrawals(address(asset1)), 200);
178 |
179 | // Full withdraw ilk1 2
180 |
181 | vm.prank(operator2);
182 | amount = conduit.withdraw(ilk2, address(asset1), 300);
183 |
184 | assertEq(amount, 300);
185 |
186 | assertEq(asset1.balanceOf(address(conduit)), 0);
187 | assertEq(asset1.balanceOf(buffer1), 200);
188 | assertEq(asset1.balanceOf(buffer2), 300);
189 |
190 | assertEq(conduit.withdrawableFunds(address(asset1), ilk1), 0);
191 | assertEq(conduit.withdrawableFunds(address(asset1), ilk2), 0);
192 | assertEq(conduit.totalWithdrawableFunds(address(asset1)), 0);
193 | assertEq(conduit.withdrawals(address(asset1), ilk1), 200);
194 | assertEq(conduit.withdrawals(address(asset1), ilk2), 300);
195 | assertEq(conduit.totalWithdrawals(address(asset1)), 500);
196 | }
197 |
198 | }
199 |
--------------------------------------------------------------------------------
/test/arranger-conduit/invariants/BoundedInvariants.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 | pragma solidity ^0.8.13;
3 |
4 | import "forge-std/Test.sol";
5 |
6 | import { InvariantTestBase } from "./InvariantTestBase.t.sol";
7 |
8 | import { ArrangerBase, ArrangerBounded } from "./handlers/Arranger.sol";
9 | import { OperatorBase, OperatorBounded } from "./handlers/Operator.sol";
10 | import { TransfererBase, TransfererBounded } from "./handlers/Transferer.sol";
11 |
12 | // These invariants assume external transfers into the contract, so balance-based
13 | // invariants are asserted with with gt/lt checks
14 | contract InvariantTest is InvariantTestBase {
15 |
16 | function setUp() public override {
17 | super.setUp();
18 |
19 | // Get string from env and hash for comparison
20 | bytes32 foundryProfile
21 | = keccak256(abi.encodePacked(vm.envOr("FOUNDRY_PROFILE", string(""))));
22 |
23 | address conduit_ = address(conduit);
24 |
25 | // Set up testing suite with bounded handlers unless otherwise specified.
26 | // This is necessary because unbounded testing requires specific env configuration.
27 | if (
28 | foundryProfile == keccak256(abi.encodePacked("unbounded")) ||
29 | foundryProfile == keccak256(abi.encodePacked("unbounded-ci"))
30 | ) {
31 | arrangerHandler = address(new ArrangerBase(conduit_, address(this)));
32 | operatorHandler1 = address(new OperatorBase(conduit_, ilks[0], address(this)));
33 | operatorHandler2 = address(new OperatorBase(conduit_, ilks[1], address(this)));
34 | operatorHandler3 = address(new OperatorBase(conduit_, ilks[2], address(this)));
35 | transfererHandler = address(new TransfererBase(conduit_, address(this)));
36 | } else {
37 | arrangerHandler = address(new ArrangerBounded(conduit_, address(this)));
38 | operatorHandler1 = address(new OperatorBounded(conduit_, ilks[0], address(this)));
39 | operatorHandler2 = address(new OperatorBounded(conduit_, ilks[1], address(this)));
40 | operatorHandler3 = address(new OperatorBounded(conduit_, ilks[2], address(this)));
41 | transfererHandler = address(new TransfererBounded(conduit_, address(this)));
42 | }
43 |
44 | super.configureHandlers();
45 | }
46 |
47 | function invariant_A_B_C_D() external { assert_invariant_A_B_C_D(); }
48 | function invariant_E() external { assert_invariant_E(); }
49 | function invariant_F() external { assert_invariant_F(); }
50 | function invariant_G() external { assert_invariant_G(); }
51 | function invariant_H() external { assert_invariant_H(); }
52 |
53 | }
54 |
55 |
--------------------------------------------------------------------------------
/test/arranger-conduit/invariants/InvariantTestBase.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 | pragma solidity ^0.8.13;
3 |
4 | import "forge-std/Test.sol";
5 |
6 | import { AllocatorRegistry } from "dss-allocator/AllocatorRegistry.sol";
7 | import { AllocatorRoles } from "dss-allocator/AllocatorRoles.sol";
8 |
9 | import { MockERC20 } from "erc20-helpers/MockERC20.sol";
10 |
11 | import { UpgradeableProxy } from "upgradeable-proxy/UpgradeableProxy.sol";
12 |
13 | import { ArrangerConduit } from "../../../src/ArrangerConduit.sol";
14 |
15 | import { IArrangerHandlerLike, ITransfererHandlerLike } from "./interfaces/Interfaces.sol";
16 |
17 | contract InvariantTestBase is Test {
18 |
19 | uint8 ROLE = 0;
20 |
21 | uint256 NUM_ASSETS = 3;
22 | uint256 NUM_ILKS = 3;
23 |
24 | address public arrangerHandler;
25 | address public operatorHandler1;
26 | address public operatorHandler2;
27 | address public operatorHandler3;
28 | address public transfererHandler;
29 |
30 | address[] public assets;
31 | address[] public brokers;
32 |
33 | bytes32[] public ilks;
34 |
35 | ArrangerConduit public conduit;
36 |
37 | AllocatorRegistry public registry = new AllocatorRegistry();
38 | AllocatorRoles public roles = new AllocatorRoles();
39 | ArrangerConduit public conduitImplementation = new ArrangerConduit();
40 | UpgradeableProxy public conduitProxy = new UpgradeableProxy();
41 |
42 | function setUp() public virtual {
43 | conduitProxy.setImplementation(address(conduitImplementation));
44 |
45 | conduit = ArrangerConduit(address(conduitProxy));
46 |
47 | for (uint256 i; i < NUM_ASSETS; i++) {
48 | uint8 decimals = uint8(_bound(uint256(keccak256(abi.encodePacked(bytes32(i)))), 4, 18));
49 |
50 | assets.push(address(new MockERC20("asset", "ASSET", decimals)));
51 | address broker = makeAddr(string.concat("ilk", vm.toString(ilks.length)));
52 | brokers.push(broker);
53 | conduit.setBroker(broker, assets[i], true); // TODO: Use handler
54 | }
55 |
56 | for (uint256 i; i < NUM_ILKS; i++) {
57 | ilks.push(bytes32(bytes(string.concat("ilk", vm.toString(i)))));
58 | }
59 |
60 | conduit.file("registry", address(registry));
61 | conduit.file("roles", address(roles));
62 | }
63 |
64 | // NOTE: Requires handlers to be deployed in child contract
65 | function configureHandlers() internal {
66 | _setupOperatorRole(ilks[0], operatorHandler1);
67 | _setupOperatorRole(ilks[1], operatorHandler2);
68 | _setupOperatorRole(ilks[2], operatorHandler3);
69 |
70 | conduit.file("arranger", arrangerHandler);
71 |
72 | // NOTE: Buffer == operator here, should change with broader integration testing
73 | registry.file(ilks[0], "buffer", operatorHandler1);
74 | registry.file(ilks[1], "buffer", operatorHandler2);
75 | registry.file(ilks[2], "buffer", operatorHandler3);
76 |
77 | targetContract(arrangerHandler);
78 | targetContract(operatorHandler1);
79 | targetContract(operatorHandler2);
80 | targetContract(operatorHandler3);
81 | targetContract(transfererHandler);
82 | }
83 |
84 | /**********************************************************************************************/
85 | /*** Invariant Assertion Helpers ***/
86 | /**********************************************************************************************/
87 |
88 | function assert_invariant_A_B_C_D() internal {
89 | for (uint256 i = 0; i < assets.length; i++) {
90 | uint256 sumDeposits;
91 | uint256 sumRequestedFunds;
92 | uint256 sumWithdrawableFunds;
93 | uint256 sumWithdrawals;
94 |
95 | for (uint256 j = 0; j < ilks.length; j++) {
96 | sumDeposits += conduit.deposits(assets[i], ilks[j]);
97 | sumRequestedFunds += conduit.requestedFunds(assets[i], ilks[j]);
98 | sumWithdrawableFunds += conduit.withdrawableFunds(assets[i], ilks[j]);
99 | sumWithdrawals += conduit.withdrawals(assets[i], ilks[j]);
100 | }
101 |
102 | assertEq(conduit.totalDeposits(assets[i]), sumDeposits);
103 | assertEq(conduit.totalRequestedFunds(assets[i]), sumRequestedFunds);
104 | assertEq(conduit.totalWithdrawableFunds(assets[i]), sumWithdrawableFunds);
105 | assertEq(conduit.totalWithdrawals(assets[i]), sumWithdrawals);
106 | }
107 | }
108 |
109 | function assert_invariant_E() internal {
110 | for (uint256 i = 0; i < assets.length; i++) {
111 | MockERC20 asset = MockERC20(assets[i]);
112 |
113 | assertGe(asset.balanceOf(address(conduit)), conduit.totalWithdrawableFunds(assets[i]));
114 | }
115 | }
116 |
117 | function assert_invariant_F() internal {
118 | IArrangerHandlerLike arrangerHandler_ = IArrangerHandlerLike(arrangerHandler);
119 | for (uint256 i = 0; i < assets.length; i++) {
120 | assertEq(
121 | conduit.totalWithdrawableFunds(assets[i]),
122 | arrangerHandler_.returnedFunds(assets[i]) - conduit.totalWithdrawals(assets[i])
123 | );
124 | }
125 | }
126 |
127 | // NOTE: Interesting finding, if there are transfers and returnFunds calls before
128 | // the first deposit, there can be a situation where returnedFunds > totalDeposits
129 | // very early in a sequence.
130 | // NOTE: Had to add a transferredFunds ghost variable because drawnFunds can actually be higher
131 | // than totalDeposits and returnedFunds if transfer + draw happens early enough.
132 | function assert_invariant_G() internal {
133 | IArrangerHandlerLike arrangerHandler_ = IArrangerHandlerLike(arrangerHandler);
134 | ITransfererHandlerLike transfererHandler_ = ITransfererHandlerLike(transfererHandler);
135 |
136 | for (uint256 i = 0; i < assets.length; i++) {
137 | uint256 netBalance =
138 | conduit.totalDeposits(assets[i])
139 | + arrangerHandler_.returnedFunds(assets[i])
140 | + transfererHandler_.transferredFunds(assets[i])
141 | - arrangerHandler_.drawnFunds(assets[i])
142 | - conduit.totalWithdrawals(assets[i]);
143 |
144 | assertEq(MockERC20(assets[i]).balanceOf(address(conduit)), netBalance);
145 | }
146 | }
147 |
148 | function assert_invariant_H() internal {
149 | for (uint256 i = 0; i < assets.length; i++) {
150 | assertEq(
151 | MockERC20(assets[i]).balanceOf(address(conduit)),
152 | conduit.availableFunds(assets[i]) + conduit.totalWithdrawableFunds(assets[i])
153 | );
154 | }
155 | }
156 |
157 | /**********************************************************************************************/
158 | /*** View Functions ***/
159 | /**********************************************************************************************/
160 |
161 | function getAssetsLength() public view returns (uint256) {
162 | return assets.length;
163 | }
164 |
165 | function getBrokersLength() public view returns (uint256) {
166 | return brokers.length;
167 | }
168 |
169 | function getIlksLength() public view returns (uint256) {
170 | return ilks.length;
171 | }
172 |
173 | /**********************************************************************************************/
174 | /*** Utility Functions ***/
175 | /**********************************************************************************************/
176 |
177 | function _setupOperatorRole(bytes32 ilk_, address operator_) internal {
178 | // Ensure address(this) can always set for a new ilk
179 | roles.setIlkAdmin(ilk_, address(this));
180 |
181 | roles.setUserRole(ilk_, operator_, ROLE, true);
182 |
183 | address conduit_ = address(conduit);
184 |
185 | roles.setRoleAction(ilk_, ROLE, conduit_, conduit.deposit.selector, true);
186 | roles.setRoleAction(ilk_, ROLE, conduit_, conduit.withdraw.selector, true);
187 | roles.setRoleAction(ilk_, ROLE, conduit_, conduit.requestFunds.selector, true);
188 | roles.setRoleAction(ilk_, ROLE, conduit_, conduit.cancelFundRequest.selector, true);
189 | }
190 |
191 | }
192 |
--------------------------------------------------------------------------------
/test/arranger-conduit/invariants/handlers/Arranger.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 | pragma solidity ^0.8.13;
3 |
4 | import "forge-std/Test.sol";
5 |
6 | import { MockERC20 } from "erc20-helpers/MockERC20.sol";
7 |
8 | import { ArrangerConduit, HandlerBase } from "./HandlerBase.sol";
9 |
10 | contract ArrangerBase is HandlerBase, Test {
11 |
12 | mapping(address => uint256) public drawnFunds; // Ghost variable for drawn funds
13 | mapping(address => uint256) public returnedFunds; // Ghost variable for returned funds
14 |
15 | constructor(address arrangerConduit_, address testContract_)
16 | HandlerBase(arrangerConduit_, testContract_) {}
17 |
18 | function drawFunds(uint256 indexSeed, uint256 amount) public virtual {
19 | address asset = _getAsset(indexSeed);
20 | address broker = _getBroker(indexSeed);
21 |
22 | arrangerConduit.drawFunds(asset, broker, amount);
23 |
24 | drawnFunds[asset] += amount;
25 | }
26 |
27 | function returnFunds(uint256 indexSeed, uint256 amount) public virtual {
28 | // Get a random fundRequestId
29 | uint256 fundRequestId = indexSeed % arrangerConduit.getFundRequestsLength();
30 |
31 | arrangerConduit.returnFunds(fundRequestId, amount);
32 |
33 | // Add to the returnedFunds ghost var for the fundRequest's asset
34 | returnedFunds[arrangerConduit.getFundRequest(fundRequestId).asset] += amount;
35 | }
36 |
37 | }
38 |
39 | contract ArrangerBounded is ArrangerBase {
40 |
41 | constructor(address arrangerConduit_, address testContract_)
42 | ArrangerBase(arrangerConduit_, testContract_) {}
43 |
44 | function drawFunds(uint256 indexSeed, uint256 amount) public virtual override {
45 | // Draw funds for an amount between zero and the full available amount
46 | amount = _bound(amount, 0, arrangerConduit.availableFunds(_getAsset(indexSeed)));
47 | super.drawFunds(indexSeed, amount);
48 | }
49 |
50 | function returnFunds(uint256 indexSeed, uint256 amount) public virtual override {
51 | if (arrangerConduit.getFundRequestsLength() == 0) return;
52 |
53 | ( bool active, uint256 fundRequestId ) = _getActiveFundRequestId(indexSeed);
54 |
55 | if (!active) return; // Only returnFunds for active fund requests
56 |
57 | ArrangerConduit.FundRequest memory fundRequest
58 | = arrangerConduit.getFundRequest(fundRequestId);
59 |
60 | // Get exposure to amounts above and below the requested amount
61 | amount = _bound(amount, 0, fundRequest.amountRequested * 2);
62 |
63 | MockERC20(fundRequest.asset).mint(address(arrangerConduit), amount);
64 |
65 | // NOTE: Not using `super.returnFunds` because IDs are derived in different ways
66 | arrangerConduit.returnFunds(fundRequestId, amount);
67 |
68 | returnedFunds[arrangerConduit.getFundRequest(fundRequestId).asset] += amount;
69 | }
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/test/arranger-conduit/invariants/handlers/HandlerBase.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 | pragma solidity ^0.8.13;
3 |
4 | import { ArrangerConduit } from "../../../../src/ArrangerConduit.sol";
5 |
6 | import { InvariantTestBase } from "../InvariantTestBase.t.sol";
7 |
8 | contract HandlerBase {
9 |
10 | ArrangerConduit public arrangerConduit;
11 | InvariantTestBase public testContract;
12 |
13 | constructor(address arrangerConduit_, address testContract_) {
14 | arrangerConduit = ArrangerConduit(arrangerConduit_);
15 | testContract = InvariantTestBase(testContract_);
16 | }
17 |
18 | // TODO: Investigate persisting in storage
19 | function _getActiveFundRequestIds()
20 | internal view returns (uint256[] memory activeFundRequestIds)
21 | {
22 | uint256 fundRequestsLength = arrangerConduit.getFundRequestsLength();
23 |
24 | activeFundRequestIds = new uint256[](fundRequestsLength);
25 |
26 | uint256 activeFundRequestsCount;
27 |
28 | // Iterate through all fundRequests and make a new array of activeFundRequestIds
29 | for (uint256 i = 0; i < fundRequestsLength; i++) {
30 | ArrangerConduit.FundRequest memory fundRequest = arrangerConduit.getFundRequest(i);
31 |
32 | // If status == PENDING
33 | if (uint256(fundRequest.status) == uint256(1)) {
34 | activeFundRequestIds[activeFundRequestsCount] = i;
35 | activeFundRequestsCount++;
36 | }
37 | }
38 |
39 | // Adjust the activeFundRequestIds array to the correct size, removing empty elements
40 | assembly {
41 | mstore(activeFundRequestIds, activeFundRequestsCount)
42 | }
43 | }
44 |
45 | // NOTE: Duplicating code in the function because its more efficient than
46 | // calling _getActiveFundRequestIds() and then iterating again
47 | function _getActiveFundRequestIdsForIlk(bytes32 ilk)
48 | internal view returns (uint256[] memory activeFundRequestIds)
49 | {
50 | uint256 fundRequestsLength = arrangerConduit.getFundRequestsLength();
51 |
52 | activeFundRequestIds = new uint256[](fundRequestsLength);
53 |
54 | uint256 activeFundRequestsCount;
55 |
56 | // Iterate through all fundRequests and make a new array of activeFundRequestIds
57 | for (uint256 i = 0; i < fundRequestsLength; i++) {
58 | ArrangerConduit.FundRequest memory fundRequest = arrangerConduit.getFundRequest(i);
59 |
60 | // If status == PENDING and matches ilk
61 | if (uint256(fundRequest.status) == uint256(1) && fundRequest.ilk == ilk) {
62 | activeFundRequestIds[activeFundRequestsCount] = i;
63 | activeFundRequestsCount++;
64 | }
65 | }
66 |
67 | // Adjust the activeFundRequestIds array to the correct size, removing empty elements
68 | assembly {
69 | mstore(activeFundRequestIds, activeFundRequestsCount)
70 | }
71 | }
72 |
73 | function _getAsset(uint256 indexSeed) internal view returns (address asset) {
74 | // Get a random asset from the actively used assets using a unique seed
75 | asset = testContract.assets(_hash(indexSeed, "asset") % testContract.getAssetsLength());
76 | }
77 |
78 | function _getBroker(uint256 indexSeed) internal view returns (address broker) {
79 | // Get a random broker from the actively used brokers using a unique seed
80 | broker = testContract.brokers(_hash(indexSeed, "broker") % testContract.getBrokersLength());
81 | }
82 |
83 | function _getIlk(uint256 indexSeed) internal view returns (bytes32 ilk) {
84 | // Get a random ilk from the actively used ilks using a unique seed
85 | ilk = testContract.ilks(_hash(indexSeed, "ilk") % testContract.getIlksLength());
86 | }
87 |
88 | function _getActiveFundRequestId(uint256 indexSeed)
89 | internal view returns (bool active, uint256 fundRequestId)
90 | {
91 | uint256[] memory activeFundRequests = _getActiveFundRequestIds();
92 |
93 | if (activeFundRequests.length == 0) return (active, 0); // Return false
94 |
95 | active = true;
96 |
97 | // Pick a random fund request from list of active fundRequests
98 | uint256 seed = _hash(indexSeed, "activeFundRequest");
99 | fundRequestId = activeFundRequests[seed % activeFundRequests.length];
100 | }
101 |
102 | function _hash(uint256 number_, string memory salt) internal pure returns (uint256 hash_) {
103 | hash_ = uint256(keccak256(abi.encode(number_, salt)));
104 | }
105 |
106 | }
107 |
--------------------------------------------------------------------------------
/test/arranger-conduit/invariants/handlers/Operator.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 | pragma solidity ^0.8.13;
3 |
4 | import "forge-std/Test.sol";
5 |
6 | import { MockERC20 } from "erc20-helpers/MockERC20.sol";
7 |
8 | import { HandlerBase } from "./HandlerBase.sol";
9 |
10 | contract OperatorBase is HandlerBase, Test {
11 |
12 | bytes32 ilk;
13 |
14 | constructor(address arrangerConduit_, bytes32 ilk_, address testContract_)
15 | HandlerBase(arrangerConduit_, testContract_)
16 | {
17 | ilk = ilk_;
18 | }
19 |
20 | function cancelFundRequest(uint256 indexSeed) public virtual {
21 | uint256 fundRequestId = indexSeed % arrangerConduit.getFundRequestsLength();
22 |
23 | arrangerConduit.cancelFundRequest(fundRequestId);
24 | }
25 |
26 | function deposit(uint256 indexSeed, uint256 amount) public virtual {
27 | address asset = _getAsset(indexSeed);
28 |
29 | arrangerConduit.deposit(ilk , asset, amount);
30 | }
31 |
32 | function requestFunds(uint256 indexSeed, uint256 amount, string memory info) public virtual {
33 | address asset = _getAsset(indexSeed);
34 |
35 | arrangerConduit.requestFunds(ilk, asset, amount, info);
36 | }
37 |
38 | function withdraw(uint256 indexSeed, uint256 amount) public virtual {
39 | address asset = _getAsset(indexSeed);
40 |
41 | arrangerConduit.withdraw(ilk, asset, amount);
42 | }
43 |
44 | }
45 |
46 | contract OperatorBounded is OperatorBase {
47 |
48 | constructor(address arrangerConduit_, bytes32 ilk_, address testContract_)
49 | OperatorBase(arrangerConduit_, ilk_, testContract_) {}
50 |
51 | function cancelFundRequest(uint256 indexSeed) public virtual override {
52 | uint256[] memory activeFundRequestIds = _getActiveFundRequestIdsForIlk(ilk);
53 |
54 | if (activeFundRequestIds.length == 0) return;
55 |
56 | uint256 fundRequestId = activeFundRequestIds[indexSeed % activeFundRequestIds.length];
57 |
58 | // NOTE: Not using super because id is obtained differently
59 | arrangerConduit.cancelFundRequest(fundRequestId);
60 | }
61 |
62 | function deposit(uint256 indexSeed, uint256 amount) public virtual override {
63 | amount = _bound(amount, 0, 1e45);
64 |
65 | address asset = _getAsset(indexSeed);
66 |
67 | MockERC20(asset).mint(address(this), amount);
68 | MockERC20(asset).approve(address(arrangerConduit), amount);
69 |
70 | super.deposit(indexSeed, amount);
71 | }
72 |
73 | function requestFunds(uint256 indexSeed, uint256 amount, string memory info)
74 | public virtual override
75 | {
76 | amount = _bound(amount, 0, 1e45);
77 |
78 | super.requestFunds(indexSeed, amount, info);
79 | }
80 |
81 | function withdraw(uint256 indexSeed, uint256 amount) public virtual override {
82 | address asset = _getAsset(indexSeed);
83 |
84 | amount = _bound(amount, 0, arrangerConduit.withdrawableFunds(asset, ilk));
85 |
86 | super.withdraw(indexSeed, amount);
87 | }
88 |
89 | }
90 |
--------------------------------------------------------------------------------
/test/arranger-conduit/invariants/handlers/Transferer.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 | pragma solidity ^0.8.13;
3 |
4 | import "forge-std/Test.sol";
5 |
6 | import { MockERC20 } from "erc20-helpers/MockERC20.sol";
7 |
8 | import { HandlerBase } from "./HandlerBase.sol";
9 |
10 | contract TransfererBase is HandlerBase, Test {
11 |
12 | mapping(address => uint256) public transferredFunds;
13 |
14 | constructor(address arrangerConduit_, address testContract_)
15 | HandlerBase(arrangerConduit_, testContract_) {}
16 |
17 | function transfer(uint256 indexSeed, uint256 amount) public virtual {
18 | address asset = _getAsset(indexSeed);
19 |
20 | MockERC20(asset).transfer(address(arrangerConduit), amount);
21 |
22 | transferredFunds[asset] += amount;
23 | }
24 |
25 | }
26 |
27 | contract TransfererBounded is TransfererBase {
28 |
29 | constructor(address arrangerConduit_, address testContract_)
30 | TransfererBase(arrangerConduit_, testContract_) {}
31 |
32 | function transfer(uint256 indexSeed, uint256 amount) public virtual override {
33 | address asset = _getAsset(indexSeed);
34 | amount = _bound(amount, 0, 1e30);
35 |
36 | MockERC20(asset).mint(address(this), amount);
37 |
38 | // NOTE: Have to use indexSeed again so function is overridden
39 | super.transfer(indexSeed, amount);
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/test/arranger-conduit/invariants/interfaces/Interfaces.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 | pragma solidity ^0.8.13;
3 |
4 | interface IArrangerHandlerLike {
5 |
6 | function drawnFunds(address asset) external view returns (uint256 drawnFunds_);
7 |
8 | function returnedFunds(address asset) external view returns (uint256 returnedFunds_);
9 |
10 | }
11 |
12 | interface ITransfererHandlerLike {
13 |
14 | function transferredFunds(address asset) external view returns (uint256 transferredFunds_);
15 |
16 | }
17 |
--------------------------------------------------------------------------------