From 58899f1452e90e869e74531b6fc4b46149a4e161 Mon Sep 17 00:00:00 2001 From: Stefan Ionescu Date: Fri, 8 Oct 2021 00:08:55 +0100 Subject: [PATCH] Add uni v3 utils --- .gitattributes | 2 +- .gitignore | 2 +- .gitmodules | 6 +- .travis.yml | 24 +- LICENSE | 1348 ++++---- Makefile | 8 +- README.md | 30 +- src/SAFESaviourRegistry.sol | 222 +- src/SaviourCRatioSetter.sol | 180 +- src/integrations/compound/CErc20.sol | 416 +-- src/integrations/compound/CToken.sol | 2872 ++++++++--------- .../compound/CTokenInterfaces.sol | 608 ++-- src/integrations/compound/ComptrollerG2.sol | 2124 ++++++------ .../compound/ComptrollerInterface.sol | 142 +- .../compound/ComptrollerStorage.sol | 290 +- src/integrations/compound/EIP20Interface.sol | 124 +- .../compound/EIP20NonStandardInterface.sol | 140 +- src/integrations/compound/ErrorReporter.sol | 414 +-- src/integrations/compound/Exponential.sol | 364 +-- .../compound/ExponentialNoError.sol | 390 +-- .../compound/InterestRateModel.sol | 60 +- src/integrations/compound/PriceOracle.sol | 32 +- src/integrations/compound/Unitroller.sol | 300 +- .../compound/WhitePaperInterestRateModel.sol | 166 +- .../UniswapV2LiquidityManager.sol | 230 +- .../UniswapV3LiquidityManager.sol | 46 - .../uniswap/swappers/UniswapV2Swapper.sol | 268 +- .../uniswap/uni-v2/UniswapV2Factory.sol | 90 +- .../uniswap/uni-v2/UniswapV2Pair.sol | 564 ++-- .../uniswap/uni-v2/UniswapV2Router02.sol | 786 ++--- .../uniswap/uni-v2/interfaces/IERC20.sol | 34 +- .../uni-v2/interfaces/IUniswapV2Callee.sol | 10 +- .../uni-v2/interfaces/IUniswapV2Factory.sol | 34 +- .../uni-v2/interfaces/IUniswapV2Pair.sol | 104 +- .../uni-v2/interfaces/IUniswapV2Router01.sol | 190 +- .../uni-v2/interfaces/IUniswapV2Router02.sol | 88 +- .../uniswap/uni-v2/interfaces/IWETH.sol | 14 +- .../uniswap/uni-v2/libs/TransferHelper.sol | 58 +- .../uniswap/uni-v2/libs/UniswapV2Library.sol | 170 +- .../uni-v2/libs/UniswapV2OracleLibrary.sol | 66 +- .../uniswap/uni-v3/FixedPoint128.sol | 8 + src/integrations/uniswap/uni-v3/FullMath.sol | 107 + .../uniswap/uni-v3/UniswapV3FeeCalculator.sol | 73 + src/interfaces/CTokenLike.sol | 26 +- src/interfaces/CoinJoinLike.sol | 14 +- src/interfaces/CollateralJoinLike.sol | 20 +- src/interfaces/ERC20Like.sol | 22 +- src/interfaces/GebSafeManagerLike.sol | 14 +- src/interfaces/LiquidationEngineLike.sol | 10 +- src/interfaces/OracleRelayerLike.sol | 14 +- src/interfaces/PriceFeedLike.sol | 14 +- src/interfaces/SAFEEngineLike.sol | 52 +- src/interfaces/SAFESaviourRegistryLike.sol | 10 +- src/interfaces/SafeSaviourLike.sol | 182 +- src/interfaces/SaviourCRatioSetterLike.sol | 174 +- src/interfaces/SwapManagerLike.sol | 26 +- src/interfaces/TaxCollectorLike.sol | 10 +- .../UniswapLiquidityManagerLike.sol | 16 - ...niswapV3NonFungiblePositionManagerLike.sol | 35 + src/interfaces/UniswapV3PoolLike.sol | 22 + src/interfaces/YVaultLike.sol | 16 +- src/math/BabylonianMath.sol | 42 +- src/math/CarefulMath.sol | 170 +- src/math/FixedPointMath.sol | 152 +- src/math/Math.sol | 90 +- src/math/SafeMath.sol | 318 +- src/math/UQ112x112.sol | 40 +- .../CompoundSystemCoinSafeSaviour.sol | 922 +++--- .../GeneralTokenReserveSafeSaviour.sol | 800 ++--- ...rlyingUniswapV2CustomCRatioSafeSaviour.sol | 1294 ++++---- .../NativeUnderlyingUniswapV2SafeSaviour.sol | 1332 ++++---- .../NativeUnderlyingUniswapV3SafeSaviour.sol | 1312 ++++---- .../SystemCoinUniswapV2SafeSaviour.sol | 1374 ++++---- .../SystemCoinUniswapV3SafeSaviour.sol | 1308 ++++---- src/saviours/YearnSystemCoinSafeSaviour.sol | 918 +++--- src/test/CompoundSystemCoinSafeSaviour.t.sol | 2142 ++++++------ .../GeneralTokenReserveSafeSaviourTest.t.sol | 1460 ++++----- ...yingUniswapV2CustomCRatioSafeSaviour.t.sol | 2444 +++++++------- ...NativeUnderlyingUniswapV2SafeSaviour.t.sol | 2696 ++++++++-------- src/test/SAFESaviourRegistry.t.sol | 186 +- src/test/SaviourCRatioSetter.t.sol | 240 +- src/test/SystemCoinUniswapV2SafeSaviour.t.sol | 2636 +++++++-------- src/test/UniswapV2LiquidityManager.t.sol | 400 +-- src/test/UniswapV2Swapper.t.sol | 396 +-- src/utils/ReentrancyGuard.sol | 124 +- 85 files changed, 18430 insertions(+), 18247 deletions(-) delete mode 100644 src/integrations/uniswap/liquidity-managers/UniswapV3LiquidityManager.sol create mode 100644 src/integrations/uniswap/uni-v3/FixedPoint128.sol create mode 100644 src/integrations/uniswap/uni-v3/FullMath.sol create mode 100644 src/integrations/uniswap/uni-v3/UniswapV3FeeCalculator.sol delete mode 100644 src/interfaces/UniswapLiquidityManagerLike.sol create mode 100644 src/interfaces/UniswapV3NonFungiblePositionManagerLike.sol create mode 100644 src/interfaces/UniswapV3PoolLike.sol diff --git a/.gitattributes b/.gitattributes index 52031de..64dbca9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -*.sol linguist-language=Solidity +*.sol linguist-language=Solidity diff --git a/.gitignore b/.gitignore index e2e7327..8ea9843 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -/out +/out diff --git a/.gitmodules b/.gitmodules index 1060971..06db822 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "lib/geb-safe-manager"] - path = lib/geb-safe-manager - url = https://github.com/reflexer-labs/geb-safe-manager.git +[submodule "lib/geb-safe-manager"] + path = lib/geb-safe-manager + url = https://github.com/reflexer-labs/geb-safe-manager.git diff --git a/.travis.yml b/.travis.yml index 2539c8b..e72151a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,12 @@ -os: - - linux -language: nix -nix: 2.3.6 -before_install: - - nix-env -iA nixpkgs.cachix - - echo "trusted-users = root travis" | sudo tee -a /etc/nix/nix.conf && sudo pkill nix-daemon - - cachix use maker - - git clone --recursive https://github.com/dapphub/dapptools $HOME/.dapp/dapptools - - nix-env -f https://github.com/makerdao/makerpkgs/tarball/master -iA dappPkgsVersions.hevm-0_41_0.dapp -script: - - dapp --use solc:0.6.7 test +os: + - linux +language: nix +nix: 2.3.6 +before_install: + - nix-env -iA nixpkgs.cachix + - echo "trusted-users = root travis" | sudo tee -a /etc/nix/nix.conf && sudo pkill nix-daemon + - cachix use maker + - git clone --recursive https://github.com/dapphub/dapptools $HOME/.dapp/dapptools + - nix-env -f https://github.com/makerdao/makerpkgs/tarball/master -iA dappPkgsVersions.hevm-0_41_0.dapp +script: + - dapp --use solc:0.6.7 test diff --git a/LICENSE b/LICENSE index f288702..3877ae0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,674 +1,674 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/Makefile b/Makefile index 21f7f70..3f4862a 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -all :; dapp build -clean :; dapp clean -test :; dapp test -deploy :; dapp create SafeSaviourLike +all :; dapp build +clean :; dapp clean +test :; dapp test +deploy :; dapp create SafeSaviourLike diff --git a/README.md b/README.md index 74b1fe0..bcf6939 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ -# GEB Safe Saviours - -This repository contains several SAFE saviours that can be attached to GEB Safes and protect them from liquidation. - -For more details on what saviours are and how they generally work, read the [official documentation](https://docs.reflexer.finance/integrations/safe-protection). - -# Saviour Types - -- **CompoundSystemCoinSafeSaviour**: this saviour lends system coins on a Compound like market and repays a Safe's debt when it's liquidated -- **GeneralTokenReserveSafeSaviour**: this saviour uses collateral to top up a Safe and save it -- **NativeUnderlyingUniswapV2SafeSaviour**: this saviour withdraws liquidity from Uniswap V2 and repays debt and/or tops up a Safe in order to save it -- **NativeUnderlyingUniswapV3SafeSaviour**: this saviour withdraws liquidity from Uniswap V3 and repays debt and/or tops up a Safe in order to save it -- **SystemCoinUniswapV2SafeSaviour**: this saviour withdraws liquidity from Uniswap V2, swaps one of the tokens for the Safe's collateral and repays debt and/or tops up the Safe in order to save it -- **SystemCoinUniswapV3SafeSaviour**: this saviour withdraws liquidity from Uniswap V3, swaps one of the tokens for the Safe's collateral and repays debt and/or tops up the Safe in order to save it -- **YearnSystemCoinSafeSaviour**: this saviour lends system coins in a Yearn strategy vault and repays a Safe's debt when it's liquidated +# GEB Safe Saviours + +This repository contains several SAFE saviours that can be attached to GEB Safes and protect them from liquidation. + +For more details on what saviours are and how they generally work, read the [official documentation](https://docs.reflexer.finance/integrations/safe-protection). + +# Saviour Types + +- **CompoundSystemCoinSafeSaviour**: this saviour lends system coins on a Compound like market and repays a Safe's debt when it's liquidated +- **GeneralTokenReserveSafeSaviour**: this saviour uses collateral to top up a Safe and save it +- **NativeUnderlyingUniswapV2SafeSaviour**: this saviour withdraws liquidity from Uniswap V2 and repays debt and/or tops up a Safe in order to save it +- **NativeUnderlyingUniswapV3SafeSaviour**: this saviour withdraws liquidity from Uniswap V3 and repays debt and/or tops up a Safe in order to save it +- **SystemCoinUniswapV2SafeSaviour**: this saviour withdraws liquidity from Uniswap V2, swaps one of the tokens for the Safe's collateral and repays debt and/or tops up the Safe in order to save it +- **SystemCoinUniswapV3SafeSaviour**: this saviour withdraws liquidity from Uniswap V3, swaps one of the tokens for the Safe's collateral and repays debt and/or tops up the Safe in order to save it +- **YearnSystemCoinSafeSaviour**: this saviour lends system coins in a Yearn strategy vault and repays a Safe's debt when it's liquidated diff --git a/src/SAFESaviourRegistry.sol b/src/SAFESaviourRegistry.sol index 3e50f80..0022a88 100644 --- a/src/SAFESaviourRegistry.sol +++ b/src/SAFESaviourRegistry.sol @@ -1,111 +1,111 @@ -pragma solidity ^0.6.7; - -contract SAFESaviourRegistry { - // --- Auth --- - mapping (address => uint256) public authorizedAccounts; - /** - * @notice Add auth to an account - * @param account Account to add auth to - */ - function addAuthorization(address account) external isAuthorized { - authorizedAccounts[account] = 1; - emit AddAuthorization(account); - } - /** - * @notice Remove auth from an account - * @param account Account to remove auth from - */ - function removeAuthorization(address account) external isAuthorized { - authorizedAccounts[account] = 0; - emit RemoveAuthorization(account); - } - /** - * @notice Checks whether msg.sender can call an authed function - **/ - modifier isAuthorized { - require(authorizedAccounts[msg.sender] == 1, "SAFESaviourRegistry/account-not-authorized"); - _; - } - - // --- Other Modifiers --- - modifier isSaviour { - require(saviours[msg.sender] == 1, "SAFESaviourRegistry/not-a-saviour"); - _; - } - - // --- Variables --- - // Minimum amount of time that needs to elapse for a specific SAFE to be saved again - uint256 public saveCooldown; - - // Timestamp for the last time when a specific SAFE has been saved - mapping(bytes32 => mapping(address => uint256)) public lastSaveTime; - - // Whitelisted saviours - mapping(address => uint256) public saviours; - - // --- Events --- - event AddAuthorization(address account); - event RemoveAuthorization(address account); - event ModifyParameters(bytes32 parameter, uint256 val); - event ToggleSaviour(address saviour, uint256 whitelistState); - event MarkSave(bytes32 indexed collateralType, address indexed safeHandler); - - constructor(uint256 saveCooldown_) public { - require(saveCooldown_ > 0, "SAFESaviourRegistry/null-save-cooldown"); - authorizedAccounts[msg.sender] = 1; - saveCooldown = saveCooldown_; - emit ModifyParameters("saveCooldown", saveCooldown_); - } - - // --- Boolean Logic --- - function either(bool x, bool y) internal pure returns (bool z) { - assembly{ z := or(x, y)} - } - - // --- Math --- - function addition(uint256 x, uint256 y) internal pure returns (uint256 z) { - require((z = x + y) >= x, "SAFESaviourRegistry/add-uint-uint-overflow"); - } - - // --- Administration --- - /* - * @notice Change the saveCooldown value - * @param parameter Name of the parameter to change - * @param val The new value for the param - */ - function modifyParameters(bytes32 parameter, uint256 val) external isAuthorized { - require(val > 0, "SAFESaviourRegistry/null-val"); - if (parameter == "saveCooldown") { - saveCooldown = val; - } else revert("SAFESaviourRegistry/modify-unrecognized-param"); - emit ModifyParameters(parameter, val); - } - /* - * @notice Whitelist/blacklist a saviour contract - * @param saviour The saviour contract to whitelist/blacklist - */ - function toggleSaviour(address saviour) external isAuthorized { - if (saviours[saviour] == 0) { - saviours[saviour] = 1; - } else { - saviours[saviour] = 0; - } - emit ToggleSaviour(saviour, saviours[saviour]); - } - - // --- Core Logic --- - /* - * @notice Mark a new SAFE as just having been saved - * @param collateralType The collateral type backing the SAFE - * @param safeHandler The SAFE's handler - */ - function markSave(bytes32 collateralType, address safeHandler) external isSaviour { - require( - either(lastSaveTime[collateralType][safeHandler] == 0, - addition(lastSaveTime[collateralType][safeHandler], saveCooldown) < now), - "SAFESaviourRegistry/wait-more-to-save" - ); - lastSaveTime[collateralType][safeHandler] = now; - emit MarkSave(collateralType, safeHandler); - } -} +pragma solidity ^0.6.7; + +contract SAFESaviourRegistry { + // --- Auth --- + mapping (address => uint256) public authorizedAccounts; + /** + * @notice Add auth to an account + * @param account Account to add auth to + */ + function addAuthorization(address account) external isAuthorized { + authorizedAccounts[account] = 1; + emit AddAuthorization(account); + } + /** + * @notice Remove auth from an account + * @param account Account to remove auth from + */ + function removeAuthorization(address account) external isAuthorized { + authorizedAccounts[account] = 0; + emit RemoveAuthorization(account); + } + /** + * @notice Checks whether msg.sender can call an authed function + **/ + modifier isAuthorized { + require(authorizedAccounts[msg.sender] == 1, "SAFESaviourRegistry/account-not-authorized"); + _; + } + + // --- Other Modifiers --- + modifier isSaviour { + require(saviours[msg.sender] == 1, "SAFESaviourRegistry/not-a-saviour"); + _; + } + + // --- Variables --- + // Minimum amount of time that needs to elapse for a specific SAFE to be saved again + uint256 public saveCooldown; + + // Timestamp for the last time when a specific SAFE has been saved + mapping(bytes32 => mapping(address => uint256)) public lastSaveTime; + + // Whitelisted saviours + mapping(address => uint256) public saviours; + + // --- Events --- + event AddAuthorization(address account); + event RemoveAuthorization(address account); + event ModifyParameters(bytes32 parameter, uint256 val); + event ToggleSaviour(address saviour, uint256 whitelistState); + event MarkSave(bytes32 indexed collateralType, address indexed safeHandler); + + constructor(uint256 saveCooldown_) public { + require(saveCooldown_ > 0, "SAFESaviourRegistry/null-save-cooldown"); + authorizedAccounts[msg.sender] = 1; + saveCooldown = saveCooldown_; + emit ModifyParameters("saveCooldown", saveCooldown_); + } + + // --- Boolean Logic --- + function either(bool x, bool y) internal pure returns (bool z) { + assembly{ z := or(x, y)} + } + + // --- Math --- + function addition(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x + y) >= x, "SAFESaviourRegistry/add-uint-uint-overflow"); + } + + // --- Administration --- + /* + * @notice Change the saveCooldown value + * @param parameter Name of the parameter to change + * @param val The new value for the param + */ + function modifyParameters(bytes32 parameter, uint256 val) external isAuthorized { + require(val > 0, "SAFESaviourRegistry/null-val"); + if (parameter == "saveCooldown") { + saveCooldown = val; + } else revert("SAFESaviourRegistry/modify-unrecognized-param"); + emit ModifyParameters(parameter, val); + } + /* + * @notice Whitelist/blacklist a saviour contract + * @param saviour The saviour contract to whitelist/blacklist + */ + function toggleSaviour(address saviour) external isAuthorized { + if (saviours[saviour] == 0) { + saviours[saviour] = 1; + } else { + saviours[saviour] = 0; + } + emit ToggleSaviour(saviour, saviours[saviour]); + } + + // --- Core Logic --- + /* + * @notice Mark a new SAFE as just having been saved + * @param collateralType The collateral type backing the SAFE + * @param safeHandler The SAFE's handler + */ + function markSave(bytes32 collateralType, address safeHandler) external isSaviour { + require( + either(lastSaveTime[collateralType][safeHandler] == 0, + addition(lastSaveTime[collateralType][safeHandler], saveCooldown) < now), + "SAFESaviourRegistry/wait-more-to-save" + ); + lastSaveTime[collateralType][safeHandler] = now; + emit MarkSave(collateralType, safeHandler); + } +} diff --git a/src/SaviourCRatioSetter.sol b/src/SaviourCRatioSetter.sol index 3da9972..62ac22c 100644 --- a/src/SaviourCRatioSetter.sol +++ b/src/SaviourCRatioSetter.sol @@ -1,90 +1,90 @@ -pragma solidity 0.6.7; - -import "./interfaces/SaviourCRatioSetterLike.sol"; -import "./math/SafeMath.sol"; - -contract SaviourCRatioSetter is SafeMath, SaviourCRatioSetterLike { - constructor( - address oracleRelayer_, - address safeManager_ - ) public { - require(oracleRelayer_ != address(0), "SaviourCRatioSetter/null-oracle-relayer"); - require(safeManager_ != address(0), "SaviourCRatioSetter/null-safe-manager"); - - authorizedAccounts[msg.sender] = 1; - - oracleRelayer = OracleRelayerLike(oracleRelayer_); - safeManager = GebSafeManagerLike(safeManager_); - - oracleRelayer.redemptionPrice(); - - emit AddAuthorization(msg.sender); - emit ModifyParameters("oracleRelayer", oracleRelayer_); - } - - // --- Administration --- - /** - * @notice Modify an address param - * @param parameter The name of the parameter - * @param data New address for the parameter - */ - function modifyParameters(bytes32 parameter, address data) external isAuthorized { - require(data != address(0), "SaviourCRatioSetter/null-data"); - - if (parameter == "oracleRelayer") { - oracleRelayer = OracleRelayerLike(data); - oracleRelayer.redemptionPrice(); - } - else revert("SaviourCRatioSetter/modify-unrecognized-param"); - } - /** - * @notice Set the default desired CRatio for a specific collateral type - * @param collateralType The name of the collateral type to set the default CRatio for - * @param cRatio New default collateralization ratio - */ - function setDefaultCRatio(bytes32 collateralType, uint256 cRatio) external override isAuthorized { - uint256 scaledLiquidationRatio = oracleRelayer.liquidationCRatio(collateralType) / CRATIO_SCALE_DOWN; - - require(scaledLiquidationRatio > 0, "SaviourCRatioSetter/invalid-scaled-liq-ratio"); - require(both(cRatio > scaledLiquidationRatio, cRatio <= MAX_CRATIO), "SaviourCRatioSetter/invalid-default-desired-cratio"); - - defaultDesiredCollateralizationRatios[collateralType] = cRatio; - - emit SetDefaultCRatio(collateralType, cRatio); - } - /* - * @notify Set the minimum CRatio that every Safe must take into account when setting a desired CRatio - * @param collateralType The collateral type for which to set the min desired CRatio - * @param cRatio The min desired CRatio to set for collateralType - */ - function setMinDesiredCollateralizationRatio(bytes32 collateralType, uint256 cRatio) external override isAuthorized { - require(cRatio < MAX_CRATIO, "SaviourCRatioSetter/invalid-min-cratio"); - minDesiredCollateralizationRatios[collateralType] = cRatio; - emit SetMinDesiredCollateralizationRatio(collateralType, cRatio); - } - // --- Adjust Cover Preferences --- - /* - * @notice Sets the collateralization ratio that a SAFE should have after it's saved - * @dev Only an address that controls the SAFE inside GebSafeManager can call this - * @param collateralType The collateral type used in the safe - * @param safeID The ID of the SAFE to set the desired CRatio for. This ID should be registered inside GebSafeManager - * @param cRatio The collateralization ratio to set - */ - function setDesiredCollateralizationRatio(bytes32 collateralType, uint256 safeID, uint256 cRatio) - external override controlsSAFE(msg.sender, safeID) { - uint256 scaledLiquidationRatio = oracleRelayer.liquidationCRatio(collateralType) / CRATIO_SCALE_DOWN; - address safeHandler = safeManager.safes(safeID); - - require(scaledLiquidationRatio > 0, "SaviourCRatioSetter/invalid-scaled-liq-ratio"); - require(either(cRatio >= minDesiredCollateralizationRatios[collateralType], cRatio == 0), "SaviourCRatioSetter/invalid-min-ratio"); - require(cRatio <= MAX_CRATIO, "SaviourCRatioSetter/exceeds-max-cratio"); - - if (cRatio > 0) { - require(scaledLiquidationRatio < cRatio, "SaviourCRatioSetter/invalid-desired-cratio"); - } - - desiredCollateralizationRatios[collateralType][safeHandler] = cRatio; - - emit SetDesiredCollateralizationRatio(msg.sender, collateralType, safeID, safeHandler, cRatio); - } -} +pragma solidity 0.6.7; + +import "./interfaces/SaviourCRatioSetterLike.sol"; +import "./math/SafeMath.sol"; + +contract SaviourCRatioSetter is SafeMath, SaviourCRatioSetterLike { + constructor( + address oracleRelayer_, + address safeManager_ + ) public { + require(oracleRelayer_ != address(0), "SaviourCRatioSetter/null-oracle-relayer"); + require(safeManager_ != address(0), "SaviourCRatioSetter/null-safe-manager"); + + authorizedAccounts[msg.sender] = 1; + + oracleRelayer = OracleRelayerLike(oracleRelayer_); + safeManager = GebSafeManagerLike(safeManager_); + + oracleRelayer.redemptionPrice(); + + emit AddAuthorization(msg.sender); + emit ModifyParameters("oracleRelayer", oracleRelayer_); + } + + // --- Administration --- + /** + * @notice Modify an address param + * @param parameter The name of the parameter + * @param data New address for the parameter + */ + function modifyParameters(bytes32 parameter, address data) external isAuthorized { + require(data != address(0), "SaviourCRatioSetter/null-data"); + + if (parameter == "oracleRelayer") { + oracleRelayer = OracleRelayerLike(data); + oracleRelayer.redemptionPrice(); + } + else revert("SaviourCRatioSetter/modify-unrecognized-param"); + } + /** + * @notice Set the default desired CRatio for a specific collateral type + * @param collateralType The name of the collateral type to set the default CRatio for + * @param cRatio New default collateralization ratio + */ + function setDefaultCRatio(bytes32 collateralType, uint256 cRatio) external override isAuthorized { + uint256 scaledLiquidationRatio = oracleRelayer.liquidationCRatio(collateralType) / CRATIO_SCALE_DOWN; + + require(scaledLiquidationRatio > 0, "SaviourCRatioSetter/invalid-scaled-liq-ratio"); + require(both(cRatio > scaledLiquidationRatio, cRatio <= MAX_CRATIO), "SaviourCRatioSetter/invalid-default-desired-cratio"); + + defaultDesiredCollateralizationRatios[collateralType] = cRatio; + + emit SetDefaultCRatio(collateralType, cRatio); + } + /* + * @notify Set the minimum CRatio that every Safe must take into account when setting a desired CRatio + * @param collateralType The collateral type for which to set the min desired CRatio + * @param cRatio The min desired CRatio to set for collateralType + */ + function setMinDesiredCollateralizationRatio(bytes32 collateralType, uint256 cRatio) external override isAuthorized { + require(cRatio < MAX_CRATIO, "SaviourCRatioSetter/invalid-min-cratio"); + minDesiredCollateralizationRatios[collateralType] = cRatio; + emit SetMinDesiredCollateralizationRatio(collateralType, cRatio); + } + // --- Adjust Cover Preferences --- + /* + * @notice Sets the collateralization ratio that a SAFE should have after it's saved + * @dev Only an address that controls the SAFE inside GebSafeManager can call this + * @param collateralType The collateral type used in the safe + * @param safeID The ID of the SAFE to set the desired CRatio for. This ID should be registered inside GebSafeManager + * @param cRatio The collateralization ratio to set + */ + function setDesiredCollateralizationRatio(bytes32 collateralType, uint256 safeID, uint256 cRatio) + external override controlsSAFE(msg.sender, safeID) { + uint256 scaledLiquidationRatio = oracleRelayer.liquidationCRatio(collateralType) / CRATIO_SCALE_DOWN; + address safeHandler = safeManager.safes(safeID); + + require(scaledLiquidationRatio > 0, "SaviourCRatioSetter/invalid-scaled-liq-ratio"); + require(either(cRatio >= minDesiredCollateralizationRatios[collateralType], cRatio == 0), "SaviourCRatioSetter/invalid-min-ratio"); + require(cRatio <= MAX_CRATIO, "SaviourCRatioSetter/exceeds-max-cratio"); + + if (cRatio > 0) { + require(scaledLiquidationRatio < cRatio, "SaviourCRatioSetter/invalid-desired-cratio"); + } + + desiredCollateralizationRatios[collateralType][safeHandler] = cRatio; + + emit SetDesiredCollateralizationRatio(msg.sender, collateralType, safeID, safeHandler, cRatio); + } +} diff --git a/src/integrations/compound/CErc20.sol b/src/integrations/compound/CErc20.sol index 3e7ddb3..d6f48b4 100644 --- a/src/integrations/compound/CErc20.sol +++ b/src/integrations/compound/CErc20.sol @@ -1,208 +1,208 @@ -pragma solidity ^0.6.7; - -import "./CToken.sol"; - -/** - * @title Compound's CErc20 Contract - * @notice CTokens which wrap an EIP-20 underlying - * @author Compound - */ -contract CErc20 is CToken, CErc20Interface { - /** - * @notice Initialize the new money market - * @param underlying_ The address of the underlying asset - * @param comptroller_ The address of the Comptroller - * @param interestRateModel_ The address of the interest rate model - * @param initialExchangeRateMantissa_ The initial exchange rate, scaled by 1e18 - * @param name_ ERC-20 name of this token - * @param symbol_ ERC-20 symbol of this token - * @param decimals_ ERC-20 decimal precision of this token - */ - function initialize(address underlying_, - ComptrollerInterface comptroller_, - InterestRateModel interestRateModel_, - uint initialExchangeRateMantissa_, - string memory name_, - string memory symbol_, - uint8 decimals_) public { - // CToken initialize does the bulk of the work - super.initialize(comptroller_, interestRateModel_, initialExchangeRateMantissa_, name_, symbol_, decimals_); - - // Set underlying and sanity check it - underlying = underlying_; - EIP20Interface(underlying).totalSupply(); - } - - /*** User Interface ***/ - - /** - * @notice Sender supplies assets into the market and receives cTokens in exchange - * @dev Accrues interest whether or not the operation succeeds, unless reverted - * @param mintAmount The amount of the underlying asset to supply - * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) - */ - function mint(uint mintAmount) override external returns (uint) { - (uint err,) = mintInternal(mintAmount); - return err; - } - - /** - * @notice Sender redeems cTokens in exchange for the underlying asset - * @dev Accrues interest whether or not the operation succeeds, unless reverted - * @param redeemTokens The number of cTokens to redeem into underlying - * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) - */ - function redeem(uint redeemTokens) override external returns (uint) { - return redeemInternal(redeemTokens); - } - - /** - * @notice Sender redeems cTokens in exchange for a specified amount of underlying asset - * @dev Accrues interest whether or not the operation succeeds, unless reverted - * @param redeemAmount The amount of underlying to redeem - * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) - */ - function redeemUnderlying(uint redeemAmount) override external returns (uint) { - return redeemUnderlyingInternal(redeemAmount); - } - - /** - * @notice Sender borrows assets from the protocol to their own address - * @param borrowAmount The amount of the underlying asset to borrow - * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) - */ - function borrow(uint borrowAmount) override external returns (uint) { - return borrowInternal(borrowAmount); - } - - /** - * @notice Sender repays their own borrow - * @param repayAmount The amount to repay - * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) - */ - function repayBorrow(uint repayAmount) override external returns (uint) { - (uint err,) = repayBorrowInternal(repayAmount); - return err; - } - - /** - * @notice Sender repays a borrow belonging to borrower - * @param borrower the account with the debt being payed off - * @param repayAmount The amount to repay - * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) - */ - function repayBorrowBehalf(address borrower, uint repayAmount) override external returns (uint) { - (uint err,) = repayBorrowBehalfInternal(borrower, repayAmount); - return err; - } - - /** - * @notice The sender liquidates the borrowers collateral. - * The collateral seized is transferred to the liquidator. - * @param borrower The borrower of this cToken to be liquidated - * @param repayAmount The amount of the underlying borrowed asset to repay - * @param cTokenCollateral The market in which to seize collateral from the borrower - * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) - */ - function liquidateBorrow(address borrower, uint repayAmount, CTokenInterface cTokenCollateral) override external returns (uint) { - (uint err,) = liquidateBorrowInternal(borrower, repayAmount, cTokenCollateral); - return err; - } - - /** - * @notice A public function to sweep accidental ERC-20 transfers to this contract. Tokens are sent to admin (timelock) - * @param token The address of the ERC-20 token to sweep - */ - function sweepToken(EIP20NonStandardInterface token) override external { - require(address(token) != underlying, "CErc20::sweepToken: can not sweep underlying token"); - uint256 balance = token.balanceOf(address(this)); - token.transfer(admin, balance); - } - - /** - * @notice The sender adds to reserves. - * @param addAmount The amount fo underlying token to add as reserves - * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) - */ - function _addReserves(uint addAmount) override external returns (uint) { - return _addReservesInternal(addAmount); - } - - /*** Safe Token ***/ - - /** - * @notice Gets balance of this contract in terms of the underlying - * @dev This excludes the value of the current message, if any - * @return The quantity of underlying tokens owned by this contract - */ - function getCashPrior() override internal view returns (uint) { - EIP20Interface token = EIP20Interface(underlying); - return token.balanceOf(address(this)); - } - - /** - * @dev Similar to EIP20 transfer, except it handles a False result from `transferFrom` and reverts in that case. - * This will revert due to insufficient balance or insufficient allowance. - * This function returns the actual amount received, - * which may be less than `amount` if there is a fee attached to the transfer. - * - * Note: This wrapper safely handles non-standard ERC-20 tokens that do not return a value. - * See here: https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca - */ - function doTransferIn(address from, uint amount) override internal returns (uint) { - EIP20NonStandardInterface token = EIP20NonStandardInterface(underlying); - uint balanceBefore = EIP20Interface(underlying).balanceOf(address(this)); - token.transferFrom(from, address(this), amount); - - bool success; - assembly { - switch returndatasize() - case 0 { // This is a non-standard ERC-20 - success := not(0) // set success to true - } - case 32 { // This is a compliant ERC-20 - returndatacopy(0, 0, 32) - success := mload(0) // Set `success = returndata` of external call - } - default { // This is an excessively non-compliant ERC-20, revert. - revert(0, 0) - } - } - require(success, "TOKEN_TRANSFER_IN_FAILED"); - - // Calculate the amount that was *actually* transferred - uint balanceAfter = EIP20Interface(underlying).balanceOf(address(this)); - require(balanceAfter >= balanceBefore, "TOKEN_TRANSFER_IN_OVERFLOW"); - return balanceAfter - balanceBefore; // underflow already checked above, just subtract - } - - /** - * @dev Similar to EIP20 transfer, except it handles a False success from `transfer` and returns an explanatory - * error code rather than reverting. If caller has not called checked protocol's balance, this may revert due to - * insufficient cash held in this contract. If caller has checked protocol's balance prior to this call, and verified - * it is >= amount, this should not revert in normal conditions. - * - * Note: This wrapper safely handles non-standard ERC-20 tokens that do not return a value. - * See here: https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca - */ - function doTransferOut(address payable to, uint amount) override internal { - EIP20NonStandardInterface token = EIP20NonStandardInterface(underlying); - token.transfer(to, amount); - - bool success; - assembly { - switch returndatasize() - case 0 { // This is a non-standard ERC-20 - success := not(0) // set success to true - } - case 32 { // This is a complaint ERC-20 - returndatacopy(0, 0, 32) - success := mload(0) // Set `success = returndata` of external call - } - default { // This is an excessively non-compliant ERC-20, revert. - revert(0, 0) - } - } - require(success, "TOKEN_TRANSFER_OUT_FAILED"); - } -} +pragma solidity ^0.6.7; + +import "./CToken.sol"; + +/** + * @title Compound's CErc20 Contract + * @notice CTokens which wrap an EIP-20 underlying + * @author Compound + */ +contract CErc20 is CToken, CErc20Interface { + /** + * @notice Initialize the new money market + * @param underlying_ The address of the underlying asset + * @param comptroller_ The address of the Comptroller + * @param interestRateModel_ The address of the interest rate model + * @param initialExchangeRateMantissa_ The initial exchange rate, scaled by 1e18 + * @param name_ ERC-20 name of this token + * @param symbol_ ERC-20 symbol of this token + * @param decimals_ ERC-20 decimal precision of this token + */ + function initialize(address underlying_, + ComptrollerInterface comptroller_, + InterestRateModel interestRateModel_, + uint initialExchangeRateMantissa_, + string memory name_, + string memory symbol_, + uint8 decimals_) public { + // CToken initialize does the bulk of the work + super.initialize(comptroller_, interestRateModel_, initialExchangeRateMantissa_, name_, symbol_, decimals_); + + // Set underlying and sanity check it + underlying = underlying_; + EIP20Interface(underlying).totalSupply(); + } + + /*** User Interface ***/ + + /** + * @notice Sender supplies assets into the market and receives cTokens in exchange + * @dev Accrues interest whether or not the operation succeeds, unless reverted + * @param mintAmount The amount of the underlying asset to supply + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function mint(uint mintAmount) override external returns (uint) { + (uint err,) = mintInternal(mintAmount); + return err; + } + + /** + * @notice Sender redeems cTokens in exchange for the underlying asset + * @dev Accrues interest whether or not the operation succeeds, unless reverted + * @param redeemTokens The number of cTokens to redeem into underlying + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function redeem(uint redeemTokens) override external returns (uint) { + return redeemInternal(redeemTokens); + } + + /** + * @notice Sender redeems cTokens in exchange for a specified amount of underlying asset + * @dev Accrues interest whether or not the operation succeeds, unless reverted + * @param redeemAmount The amount of underlying to redeem + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function redeemUnderlying(uint redeemAmount) override external returns (uint) { + return redeemUnderlyingInternal(redeemAmount); + } + + /** + * @notice Sender borrows assets from the protocol to their own address + * @param borrowAmount The amount of the underlying asset to borrow + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function borrow(uint borrowAmount) override external returns (uint) { + return borrowInternal(borrowAmount); + } + + /** + * @notice Sender repays their own borrow + * @param repayAmount The amount to repay + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function repayBorrow(uint repayAmount) override external returns (uint) { + (uint err,) = repayBorrowInternal(repayAmount); + return err; + } + + /** + * @notice Sender repays a borrow belonging to borrower + * @param borrower the account with the debt being payed off + * @param repayAmount The amount to repay + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function repayBorrowBehalf(address borrower, uint repayAmount) override external returns (uint) { + (uint err,) = repayBorrowBehalfInternal(borrower, repayAmount); + return err; + } + + /** + * @notice The sender liquidates the borrowers collateral. + * The collateral seized is transferred to the liquidator. + * @param borrower The borrower of this cToken to be liquidated + * @param repayAmount The amount of the underlying borrowed asset to repay + * @param cTokenCollateral The market in which to seize collateral from the borrower + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function liquidateBorrow(address borrower, uint repayAmount, CTokenInterface cTokenCollateral) override external returns (uint) { + (uint err,) = liquidateBorrowInternal(borrower, repayAmount, cTokenCollateral); + return err; + } + + /** + * @notice A public function to sweep accidental ERC-20 transfers to this contract. Tokens are sent to admin (timelock) + * @param token The address of the ERC-20 token to sweep + */ + function sweepToken(EIP20NonStandardInterface token) override external { + require(address(token) != underlying, "CErc20::sweepToken: can not sweep underlying token"); + uint256 balance = token.balanceOf(address(this)); + token.transfer(admin, balance); + } + + /** + * @notice The sender adds to reserves. + * @param addAmount The amount fo underlying token to add as reserves + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function _addReserves(uint addAmount) override external returns (uint) { + return _addReservesInternal(addAmount); + } + + /*** Safe Token ***/ + + /** + * @notice Gets balance of this contract in terms of the underlying + * @dev This excludes the value of the current message, if any + * @return The quantity of underlying tokens owned by this contract + */ + function getCashPrior() override internal view returns (uint) { + EIP20Interface token = EIP20Interface(underlying); + return token.balanceOf(address(this)); + } + + /** + * @dev Similar to EIP20 transfer, except it handles a False result from `transferFrom` and reverts in that case. + * This will revert due to insufficient balance or insufficient allowance. + * This function returns the actual amount received, + * which may be less than `amount` if there is a fee attached to the transfer. + * + * Note: This wrapper safely handles non-standard ERC-20 tokens that do not return a value. + * See here: https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca + */ + function doTransferIn(address from, uint amount) override internal returns (uint) { + EIP20NonStandardInterface token = EIP20NonStandardInterface(underlying); + uint balanceBefore = EIP20Interface(underlying).balanceOf(address(this)); + token.transferFrom(from, address(this), amount); + + bool success; + assembly { + switch returndatasize() + case 0 { // This is a non-standard ERC-20 + success := not(0) // set success to true + } + case 32 { // This is a compliant ERC-20 + returndatacopy(0, 0, 32) + success := mload(0) // Set `success = returndata` of external call + } + default { // This is an excessively non-compliant ERC-20, revert. + revert(0, 0) + } + } + require(success, "TOKEN_TRANSFER_IN_FAILED"); + + // Calculate the amount that was *actually* transferred + uint balanceAfter = EIP20Interface(underlying).balanceOf(address(this)); + require(balanceAfter >= balanceBefore, "TOKEN_TRANSFER_IN_OVERFLOW"); + return balanceAfter - balanceBefore; // underflow already checked above, just subtract + } + + /** + * @dev Similar to EIP20 transfer, except it handles a False success from `transfer` and returns an explanatory + * error code rather than reverting. If caller has not called checked protocol's balance, this may revert due to + * insufficient cash held in this contract. If caller has checked protocol's balance prior to this call, and verified + * it is >= amount, this should not revert in normal conditions. + * + * Note: This wrapper safely handles non-standard ERC-20 tokens that do not return a value. + * See here: https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca + */ + function doTransferOut(address payable to, uint amount) override internal { + EIP20NonStandardInterface token = EIP20NonStandardInterface(underlying); + token.transfer(to, amount); + + bool success; + assembly { + switch returndatasize() + case 0 { // This is a non-standard ERC-20 + success := not(0) // set success to true + } + case 32 { // This is a complaint ERC-20 + returndatacopy(0, 0, 32) + success := mload(0) // Set `success = returndata` of external call + } + default { // This is an excessively non-compliant ERC-20, revert. + revert(0, 0) + } + } + require(success, "TOKEN_TRANSFER_OUT_FAILED"); + } +} diff --git a/src/integrations/compound/CToken.sol b/src/integrations/compound/CToken.sol index f5c26cb..375083c 100644 --- a/src/integrations/compound/CToken.sol +++ b/src/integrations/compound/CToken.sol @@ -1,1436 +1,1436 @@ -pragma solidity ^0.6.7; - -import "./ComptrollerInterface.sol"; -import "./CTokenInterfaces.sol"; -import "./ErrorReporter.sol"; -import "./Exponential.sol"; -import "./EIP20Interface.sol"; -import "./InterestRateModel.sol"; - -/** - * @title Compound's CToken Contract - * @notice Abstract base for CTokens - * @author Compound - */ - -// NOTE: admin checks are commented for the sake of easy testing - -contract CToken is CTokenInterface, Exponential, TokenErrorReporter { - /** - * @notice Initialize the money market - * @param comptroller_ The address of the Comptroller - * @param interestRateModel_ The address of the interest rate model - * @param initialExchangeRateMantissa_ The initial exchange rate, scaled by 1e18 - * @param name_ EIP-20 name of this token - * @param symbol_ EIP-20 symbol of this token - * @param decimals_ EIP-20 decimal precision of this token - */ - function initialize(ComptrollerInterface comptroller_, - InterestRateModel interestRateModel_, - uint initialExchangeRateMantissa_, - string memory name_, - string memory symbol_, - uint8 decimals_) public { - // require(msg.sender == admin, "only admin may initialize the market"); - require(accrualBlockNumber == 0 && borrowIndex == 0, "market may only be initialized once"); - - // Set initial exchange rate - initialExchangeRateMantissa = initialExchangeRateMantissa_; - require(initialExchangeRateMantissa > 0, "initial exchange rate must be greater than zero."); - - // Set the comptroller - uint err = _setComptroller(comptroller_); - require(err == uint(Error.NO_ERROR), "setting comptroller failed"); - - // Initialize block number and borrow index (block number mocks depend on comptroller being set) - accrualBlockNumber = getBlockNumber(); - borrowIndex = mantissaOne; - - // Set the interest rate model (depends on block number / borrow index) - err = _setInterestRateModelFresh(interestRateModel_); - require(err == uint(Error.NO_ERROR), "setting interest rate model failed"); - - name = name_; - symbol = symbol_; - decimals = decimals_; - - // The counter starts true to prevent changing it from zero to non-zero (i.e. smaller cost/refund) - _notEntered = true; - } - - /** - * @notice Transfer `tokens` tokens from `src` to `dst` by `spender` - * @dev Called by both `transfer` and `transferFrom` internally - * @param spender The address of the account performing the transfer - * @param src The address of the source account - * @param dst The address of the destination account - * @param tokens The number of tokens to transfer - * @return Whether or not the transfer succeeded - */ - function transferTokens(address spender, address src, address dst, uint tokens) internal returns (uint) { - /* Fail if transfer not allowed */ - uint allowed = comptroller.transferAllowed(address(this), src, dst, tokens); - if (allowed != 0) { - return failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.TRANSFER_COMPTROLLER_REJECTION, allowed); - } - - /* Do not allow self-transfers */ - if (src == dst) { - return fail(Error.BAD_INPUT, FailureInfo.TRANSFER_NOT_ALLOWED); - } - - /* Get the allowance, infinite for the account owner */ - uint startingAllowance = 0; - if (spender == src) { - startingAllowance = uint(-1); - } else { - startingAllowance = transferAllowances[src][spender]; - } - - /* Do the calculations, checking for {under,over}flow */ - MathError mathErr; - uint allowanceNew; - uint srcTokensNew; - uint dstTokensNew; - - (mathErr, allowanceNew) = subUInt(startingAllowance, tokens); - if (mathErr != MathError.NO_ERROR) { - return fail(Error.MATH_ERROR, FailureInfo.TRANSFER_NOT_ALLOWED); - } - - (mathErr, srcTokensNew) = subUInt(accountTokens[src], tokens); - if (mathErr != MathError.NO_ERROR) { - return fail(Error.MATH_ERROR, FailureInfo.TRANSFER_NOT_ENOUGH); - } - - (mathErr, dstTokensNew) = addUInt(accountTokens[dst], tokens); - if (mathErr != MathError.NO_ERROR) { - return fail(Error.MATH_ERROR, FailureInfo.TRANSFER_TOO_MUCH); - } - - ///////////////////////// - // EFFECTS & INTERACTIONS - // (No safe failures beyond this point) - - accountTokens[src] = srcTokensNew; - accountTokens[dst] = dstTokensNew; - - /* Eat some of the allowance (if necessary) */ - if (startingAllowance != uint(-1)) { - transferAllowances[src][spender] = allowanceNew; - } - - /* We emit a Transfer event */ - emit Transfer(src, dst, tokens); - - // unused function - // comptroller.transferVerify(address(this), src, dst, tokens); - - return uint(Error.NO_ERROR); - } - - /** - * @notice Transfer `amount` tokens from `msg.sender` to `dst` - * @param dst The address of the destination account - * @param amount The number of tokens to transfer - * @return Whether or not the transfer succeeded - */ - function transfer(address dst, uint256 amount) override external nonReentrant returns (bool) { - return transferTokens(msg.sender, msg.sender, dst, amount) == uint(Error.NO_ERROR); - } - - /** - * @notice Transfer `amount` tokens from `src` to `dst` - * @param src The address of the source account - * @param dst The address of the destination account - * @param amount The number of tokens to transfer - * @return Whether or not the transfer succeeded - */ - function transferFrom(address src, address dst, uint256 amount) override external nonReentrant returns (bool) { - return transferTokens(msg.sender, src, dst, amount) == uint(Error.NO_ERROR); - } - - /** - * @notice Approve `spender` to transfer up to `amount` from `src` - * @dev This will overwrite the approval amount for `spender` - * and is subject to issues noted [here](https://eips.ethereum.org/EIPS/eip-20#approve) - * @param spender The address of the account which may transfer tokens - * @param amount The number of tokens that are approved (-1 means infinite) - * @return Whether or not the approval succeeded - */ - function approve(address spender, uint256 amount) override external returns (bool) { - address src = msg.sender; - transferAllowances[src][spender] = amount; - emit Approval(src, spender, amount); - return true; - } - - /** - * @notice Get the current allowance from `owner` for `spender` - * @param owner The address of the account which owns the tokens to be spent - * @param spender The address of the account which may transfer tokens - * @return The number of tokens allowed to be spent (-1 means infinite) - */ - function allowance(address owner, address spender) override external view returns (uint256) { - return transferAllowances[owner][spender]; - } - - /** - * @notice Get the token balance of the `owner` - * @param owner The address of the account to query - * @return The number of tokens owned by `owner` - */ - function balanceOf(address owner) override external view returns (uint256) { - return accountTokens[owner]; - } - - /** - * @notice Get the underlying balance of the `owner` - * @dev This also accrues interest in a transaction - * @param owner The address of the account to query - * @return The amount of underlying owned by `owner` - */ - function balanceOfUnderlying(address owner) override external returns (uint) { - Exp memory exchangeRate = Exp({mantissa: exchangeRateCurrent()}); - (MathError mErr, uint balance) = mulScalarTruncate(exchangeRate, accountTokens[owner]); - require(mErr == MathError.NO_ERROR, "balance could not be calculated"); - return balance; - } - - /** - * @notice Get a snapshot of the account's balances, and the cached exchange rate - * @dev This is used by comptroller to more efficiently perform liquidity checks. - * @param account Address of the account to snapshot - * @return (possible error, token balance, borrow balance, exchange rate mantissa) - */ - function getAccountSnapshot(address account) override external view returns (uint, uint, uint, uint) { - uint cTokenBalance = accountTokens[account]; - uint borrowBalance; - uint exchangeRateMantissa; - - MathError mErr; - - (mErr, borrowBalance) = borrowBalanceStoredInternal(account); - if (mErr != MathError.NO_ERROR) { - return (uint(Error.MATH_ERROR), 0, 0, 0); - } - - (mErr, exchangeRateMantissa) = exchangeRateStoredInternal(); - if (mErr != MathError.NO_ERROR) { - return (uint(Error.MATH_ERROR), 0, 0, 0); - } - - return (uint(Error.NO_ERROR), cTokenBalance, borrowBalance, exchangeRateMantissa); - } - - /** - * @dev Function to simply retrieve block number - * This exists mainly for inheriting test contracts to stub this result. - */ - function getBlockNumber() internal view returns (uint) { - return block.number; - } - - /** - * @notice Returns the current per-block borrow interest rate for this cToken - * @return The borrow interest rate per block, scaled by 1e18 - */ - function borrowRatePerBlock() override external view returns (uint) { - return interestRateModel.getBorrowRate(getCashPrior(), totalBorrows, totalReserves); - } - - /** - * @notice Returns the current per-block supply interest rate for this cToken - * @return The supply interest rate per block, scaled by 1e18 - */ - function supplyRatePerBlock() override external view returns (uint) { - return interestRateModel.getSupplyRate(getCashPrior(), totalBorrows, totalReserves, reserveFactorMantissa); - } - - /** - * @notice Returns the current total borrows plus accrued interest - * @return The total borrows with interest - */ - function totalBorrowsCurrent() override external nonReentrant returns (uint) { - require(accrueInterest() == uint(Error.NO_ERROR), "accrue interest failed"); - return totalBorrows; - } - - /** - * @notice Accrue interest to updated borrowIndex and then calculate account's borrow balance using the updated borrowIndex - * @param account The address whose balance should be calculated after updating borrowIndex - * @return The calculated balance - */ - function borrowBalanceCurrent(address account) override external nonReentrant returns (uint) { - require(accrueInterest() == uint(Error.NO_ERROR), "accrue interest failed"); - return borrowBalanceStored(account); - } - - /** - * @notice Return the borrow balance of account based on stored data - * @param account The address whose balance should be calculated - * @return The calculated balance - */ - function borrowBalanceStored(address account) override public view returns (uint) { - (MathError err, uint result) = borrowBalanceStoredInternal(account); - require(err == MathError.NO_ERROR, "borrowBalanceStored: borrowBalanceStoredInternal failed"); - return result; - } - - /** - * @notice Return the borrow balance of account based on stored data - * @param account The address whose balance should be calculated - * @return (error code, the calculated balance or 0 if error code is non-zero) - */ - function borrowBalanceStoredInternal(address account) internal view returns (MathError, uint) { - /* Note: we do not assert that the market is up to date */ - MathError mathErr; - uint principalTimesIndex; - uint result; - - /* Get borrowBalance and borrowIndex */ - BorrowSnapshot storage borrowSnapshot = accountBorrows[account]; - - /* If borrowBalance = 0 then borrowIndex is likely also 0. - * Rather than failing the calculation with a division by 0, we immediately return 0 in this case. - */ - if (borrowSnapshot.principal == 0) { - return (MathError.NO_ERROR, 0); - } - - /* Calculate new borrow balance using the interest index: - * recentBorrowBalance = borrower.borrowBalance * market.borrowIndex / borrower.borrowIndex - */ - (mathErr, principalTimesIndex) = mulUInt(borrowSnapshot.principal, borrowIndex); - if (mathErr != MathError.NO_ERROR) { - return (mathErr, 0); - } - - (mathErr, result) = divUInt(principalTimesIndex, borrowSnapshot.interestIndex); - if (mathErr != MathError.NO_ERROR) { - return (mathErr, 0); - } - - return (MathError.NO_ERROR, result); - } - - /** - * @notice Accrue interest then return the up-to-date exchange rate - * @return Calculated exchange rate scaled by 1e18 - */ - function exchangeRateCurrent() override public nonReentrant returns (uint) { - require(accrueInterest() == uint(Error.NO_ERROR), "accrue interest failed"); - return exchangeRateStored(); - } - - /** - * @notice Calculates the exchange rate from the underlying to the CToken - * @dev This function does not accrue interest before calculating the exchange rate - * @return Calculated exchange rate scaled by 1e18 - */ - function exchangeRateStored() override public view returns (uint) { - (MathError err, uint result) = exchangeRateStoredInternal(); - require(err == MathError.NO_ERROR, "exchangeRateStored: exchangeRateStoredInternal failed"); - return result; - } - - /** - * @notice Calculates the exchange rate from the underlying to the CToken - * @dev This function does not accrue interest before calculating the exchange rate - * @return (error code, calculated exchange rate scaled by 1e18) - */ - function exchangeRateStoredInternal() internal view returns (MathError, uint) { - uint _totalSupply = totalSupply; - if (_totalSupply == 0) { - /* - * If there are no tokens minted: - * exchangeRate = initialExchangeRate - */ - return (MathError.NO_ERROR, initialExchangeRateMantissa); - } else { - /* - * Otherwise: - * exchangeRate = (totalCash + totalBorrows - totalReserves) / totalSupply - */ - uint totalCash = getCashPrior(); - uint cashPlusBorrowsMinusReserves; - Exp memory exchangeRate; - MathError mathErr; - - (mathErr, cashPlusBorrowsMinusReserves) = addThenSubUInt(totalCash, totalBorrows, totalReserves); - if (mathErr != MathError.NO_ERROR) { - return (mathErr, 0); - } - - (mathErr, exchangeRate) = getExp(cashPlusBorrowsMinusReserves, _totalSupply); - if (mathErr != MathError.NO_ERROR) { - return (mathErr, 0); - } - - return (MathError.NO_ERROR, exchangeRate.mantissa); - } - } - - /** - * @notice Get cash balance of this cToken in the underlying asset - * @return The quantity of underlying asset owned by this contract - */ - function getCash() override external view returns (uint) { - return getCashPrior(); - } - - /** - * @notice Applies accrued interest to total borrows and reserves - * @dev This calculates interest accrued from the last checkpointed block - * up to the current block and writes new checkpoint to storage. - */ - function accrueInterest() override public returns (uint) { - /* Remember the initial block number */ - uint currentBlockNumber = getBlockNumber(); - uint accrualBlockNumberPrior = accrualBlockNumber; - - /* Short-circuit accumulating 0 interest */ - if (accrualBlockNumberPrior == currentBlockNumber) { - return uint(Error.NO_ERROR); - } - - /* Read the previous values out of storage */ - uint cashPrior = getCashPrior(); - uint borrowsPrior = totalBorrows; - uint reservesPrior = totalReserves; - uint borrowIndexPrior = borrowIndex; - - /* Calculate the current borrow interest rate */ - uint borrowRateMantissa = interestRateModel.getBorrowRate(cashPrior, borrowsPrior, reservesPrior); - require(borrowRateMantissa <= borrowRateMaxMantissa, "borrow rate is absurdly high"); - - /* Calculate the number of blocks elapsed since the last accrual */ - (MathError mathErr, uint blockDelta) = subUInt(currentBlockNumber, accrualBlockNumberPrior); - require(mathErr == MathError.NO_ERROR, "could not calculate block delta"); - - /* - * Calculate the interest accumulated into borrows and reserves and the new index: - * simpleInterestFactor = borrowRate * blockDelta - * interestAccumulated = simpleInterestFactor * totalBorrows - * totalBorrowsNew = interestAccumulated + totalBorrows - * totalReservesNew = interestAccumulated * reserveFactor + totalReserves - * borrowIndexNew = simpleInterestFactor * borrowIndex + borrowIndex - */ - - Exp memory simpleInterestFactor; - uint interestAccumulated; - uint totalBorrowsNew; - uint totalReservesNew; - uint borrowIndexNew; - - (mathErr, simpleInterestFactor) = mulScalar(Exp({mantissa: borrowRateMantissa}), blockDelta); - if (mathErr != MathError.NO_ERROR) { - return failOpaque(Error.MATH_ERROR, FailureInfo.ACCRUE_INTEREST_SIMPLE_INTEREST_FACTOR_CALCULATION_FAILED, uint(mathErr)); - } - - (mathErr, interestAccumulated) = mulScalarTruncate(simpleInterestFactor, borrowsPrior); - if (mathErr != MathError.NO_ERROR) { - return failOpaque(Error.MATH_ERROR, FailureInfo.ACCRUE_INTEREST_ACCUMULATED_INTEREST_CALCULATION_FAILED, uint(mathErr)); - } - - (mathErr, totalBorrowsNew) = addUInt(interestAccumulated, borrowsPrior); - if (mathErr != MathError.NO_ERROR) { - return failOpaque(Error.MATH_ERROR, FailureInfo.ACCRUE_INTEREST_NEW_TOTAL_BORROWS_CALCULATION_FAILED, uint(mathErr)); - } - - (mathErr, totalReservesNew) = mulScalarTruncateAddUInt(Exp({mantissa: reserveFactorMantissa}), interestAccumulated, reservesPrior); - if (mathErr != MathError.NO_ERROR) { - return failOpaque(Error.MATH_ERROR, FailureInfo.ACCRUE_INTEREST_NEW_TOTAL_RESERVES_CALCULATION_FAILED, uint(mathErr)); - } - - (mathErr, borrowIndexNew) = mulScalarTruncateAddUInt(simpleInterestFactor, borrowIndexPrior, borrowIndexPrior); - if (mathErr != MathError.NO_ERROR) { - return failOpaque(Error.MATH_ERROR, FailureInfo.ACCRUE_INTEREST_NEW_BORROW_INDEX_CALCULATION_FAILED, uint(mathErr)); - } - - ///////////////////////// - // EFFECTS & INTERACTIONS - // (No safe failures beyond this point) - - /* We write the previously calculated values into storage */ - accrualBlockNumber = currentBlockNumber; - borrowIndex = borrowIndexNew; - totalBorrows = totalBorrowsNew; - totalReserves = totalReservesNew; - - /* We emit an AccrueInterest event */ - emit AccrueInterest(cashPrior, interestAccumulated, borrowIndexNew, totalBorrowsNew); - - return uint(Error.NO_ERROR); - } - - /** - * @notice Sender supplies assets into the market and receives cTokens in exchange - * @dev Accrues interest whether or not the operation succeeds, unless reverted - * @param mintAmount The amount of the underlying asset to supply - * @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual mint amount. - */ - function mintInternal(uint mintAmount) internal nonReentrant returns (uint, uint) { - uint error = accrueInterest(); - if (error != uint(Error.NO_ERROR)) { - // accrueInterest emits logs on errors, but we still want to log the fact that an attempted borrow failed - return (fail(Error(error), FailureInfo.MINT_ACCRUE_INTEREST_FAILED), 0); - } - // mintFresh emits the actual Mint event if successful and logs on errors, so we don't need to - return mintFresh(msg.sender, mintAmount); - } - - struct MintLocalVars { - Error err; - MathError mathErr; - uint exchangeRateMantissa; - uint mintTokens; - uint totalSupplyNew; - uint accountTokensNew; - uint actualMintAmount; - } - - /** - * @notice User supplies assets into the market and receives cTokens in exchange - * @dev Assumes interest has already been accrued up to the current block - * @param minter The address of the account which is supplying the assets - * @param mintAmount The amount of the underlying asset to supply - * @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual mint amount. - */ - function mintFresh(address minter, uint mintAmount) internal returns (uint, uint) { - /* Fail if mint not allowed */ - uint allowed = comptroller.mintAllowed(address(this), minter, mintAmount); - if (allowed != 0) { - return (failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.MINT_COMPTROLLER_REJECTION, allowed), 0); - } - - /* Verify market's block number equals current block number */ - if (accrualBlockNumber != getBlockNumber()) { - return (fail(Error.MARKET_NOT_FRESH, FailureInfo.MINT_FRESHNESS_CHECK), 0); - } - - MintLocalVars memory vars; - - (vars.mathErr, vars.exchangeRateMantissa) = exchangeRateStoredInternal(); - if (vars.mathErr != MathError.NO_ERROR) { - return (failOpaque(Error.MATH_ERROR, FailureInfo.MINT_EXCHANGE_RATE_READ_FAILED, uint(vars.mathErr)), 0); - } - - ///////////////////////// - // EFFECTS & INTERACTIONS - // (No safe failures beyond this point) - - /* - * We call `doTransferIn` for the minter and the mintAmount. - * Note: The cToken must handle variations between ERC-20 and ETH underlying. - * `doTransferIn` reverts if anything goes wrong, since we can't be sure if - * side-effects occurred. The function returns the amount actually transferred, - * in case of a fee. On success, the cToken holds an additional `actualMintAmount` - * of cash. - */ - vars.actualMintAmount = doTransferIn(minter, mintAmount); - - /* - * We get the current exchange rate and calculate the number of cTokens to be minted: - * mintTokens = actualMintAmount / exchangeRate - */ - - (vars.mathErr, vars.mintTokens) = divScalarByExpTruncate(vars.actualMintAmount, Exp({mantissa: vars.exchangeRateMantissa})); - require(vars.mathErr == MathError.NO_ERROR, "MINT_EXCHANGE_CALCULATION_FAILED"); - - /* - * We calculate the new total supply of cTokens and minter token balance, checking for overflow: - * totalSupplyNew = totalSupply + mintTokens - * accountTokensNew = accountTokens[minter] + mintTokens - */ - (vars.mathErr, vars.totalSupplyNew) = addUInt(totalSupply, vars.mintTokens); - require(vars.mathErr == MathError.NO_ERROR, "MINT_NEW_TOTAL_SUPPLY_CALCULATION_FAILED"); - - (vars.mathErr, vars.accountTokensNew) = addUInt(accountTokens[minter], vars.mintTokens); - require(vars.mathErr == MathError.NO_ERROR, "MINT_NEW_ACCOUNT_BALANCE_CALCULATION_FAILED"); - - /* We write previously calculated values into storage */ - totalSupply = vars.totalSupplyNew; - accountTokens[minter] = vars.accountTokensNew; - - /* We emit a Mint event, and a Transfer event */ - emit Mint(minter, vars.actualMintAmount, vars.mintTokens); - emit Transfer(address(this), minter, vars.mintTokens); - - /* We call the defense hook */ - // unused function - // comptroller.mintVerify(address(this), minter, vars.actualMintAmount, vars.mintTokens); - - return (uint(Error.NO_ERROR), vars.actualMintAmount); - } - - /** - * @notice Sender redeems cTokens in exchange for the underlying asset - * @dev Accrues interest whether or not the operation succeeds, unless reverted - * @param redeemTokens The number of cTokens to redeem into underlying - * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) - */ - function redeemInternal(uint redeemTokens) internal nonReentrant returns (uint) { - uint error = accrueInterest(); - if (error != uint(Error.NO_ERROR)) { - // accrueInterest emits logs on errors, but we still want to log the fact that an attempted redeem failed - return fail(Error(error), FailureInfo.REDEEM_ACCRUE_INTEREST_FAILED); - } - // redeemFresh emits redeem-specific logs on errors, so we don't need to - return redeemFresh(msg.sender, redeemTokens, 0); - } - - /** - * @notice Sender redeems cTokens in exchange for a specified amount of underlying asset - * @dev Accrues interest whether or not the operation succeeds, unless reverted - * @param redeemAmount The amount of underlying to receive from redeeming cTokens - * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) - */ - function redeemUnderlyingInternal(uint redeemAmount) internal nonReentrant returns (uint) { - uint error = accrueInterest(); - if (error != uint(Error.NO_ERROR)) { - // accrueInterest emits logs on errors, but we still want to log the fact that an attempted redeem failed - return fail(Error(error), FailureInfo.REDEEM_ACCRUE_INTEREST_FAILED); - } - // redeemFresh emits redeem-specific logs on errors, so we don't need to - return redeemFresh(msg.sender, 0, redeemAmount); - } - - struct RedeemLocalVars { - Error err; - MathError mathErr; - uint exchangeRateMantissa; - uint redeemTokens; - uint redeemAmount; - uint totalSupplyNew; - uint accountTokensNew; - } - - /** - * @notice User redeems cTokens in exchange for the underlying asset - * @dev Assumes interest has already been accrued up to the current block - * @param redeemer The address of the account which is redeeming the tokens - * @param redeemTokensIn The number of cTokens to redeem into underlying (only one of redeemTokensIn or redeemAmountIn may be non-zero) - * @param redeemAmountIn The number of underlying tokens to receive from redeeming cTokens (only one of redeemTokensIn or redeemAmountIn may be non-zero) - * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) - */ - function redeemFresh(address payable redeemer, uint redeemTokensIn, uint redeemAmountIn) internal returns (uint) { - require(redeemTokensIn == 0 || redeemAmountIn == 0, "one of redeemTokensIn or redeemAmountIn must be zero"); - - RedeemLocalVars memory vars; - - /* exchangeRate = invoke Exchange Rate Stored() */ - (vars.mathErr, vars.exchangeRateMantissa) = exchangeRateStoredInternal(); - if (vars.mathErr != MathError.NO_ERROR) { - return failOpaque(Error.MATH_ERROR, FailureInfo.REDEEM_EXCHANGE_RATE_READ_FAILED, uint(vars.mathErr)); - } - - /* If redeemTokensIn > 0: */ - if (redeemTokensIn > 0) { - /* - * We calculate the exchange rate and the amount of underlying to be redeemed: - * redeemTokens = redeemTokensIn - * redeemAmount = redeemTokensIn x exchangeRateCurrent - */ - vars.redeemTokens = redeemTokensIn; - - (vars.mathErr, vars.redeemAmount) = mulScalarTruncate(Exp({mantissa: vars.exchangeRateMantissa}), redeemTokensIn); - if (vars.mathErr != MathError.NO_ERROR) { - return failOpaque(Error.MATH_ERROR, FailureInfo.REDEEM_EXCHANGE_TOKENS_CALCULATION_FAILED, uint(vars.mathErr)); - } - } else { - /* - * We get the current exchange rate and calculate the amount to be redeemed: - * redeemTokens = redeemAmountIn / exchangeRate - * redeemAmount = redeemAmountIn - */ - - (vars.mathErr, vars.redeemTokens) = divScalarByExpTruncate(redeemAmountIn, Exp({mantissa: vars.exchangeRateMantissa})); - if (vars.mathErr != MathError.NO_ERROR) { - return failOpaque(Error.MATH_ERROR, FailureInfo.REDEEM_EXCHANGE_AMOUNT_CALCULATION_FAILED, uint(vars.mathErr)); - } - - vars.redeemAmount = redeemAmountIn; - } - - /* Fail if redeem not allowed */ - uint allowed = comptroller.redeemAllowed(address(this), redeemer, vars.redeemTokens); - if (allowed != 0) { - return failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.REDEEM_COMPTROLLER_REJECTION, allowed); - } - - /* Verify market's block number equals current block number */ - if (accrualBlockNumber != getBlockNumber()) { - return fail(Error.MARKET_NOT_FRESH, FailureInfo.REDEEM_FRESHNESS_CHECK); - } - - /* - * We calculate the new total supply and redeemer balance, checking for underflow: - * totalSupplyNew = totalSupply - redeemTokens - * accountTokensNew = accountTokens[redeemer] - redeemTokens - */ - (vars.mathErr, vars.totalSupplyNew) = subUInt(totalSupply, vars.redeemTokens); - if (vars.mathErr != MathError.NO_ERROR) { - return failOpaque(Error.MATH_ERROR, FailureInfo.REDEEM_NEW_TOTAL_SUPPLY_CALCULATION_FAILED, uint(vars.mathErr)); - } - - (vars.mathErr, vars.accountTokensNew) = subUInt(accountTokens[redeemer], vars.redeemTokens); - if (vars.mathErr != MathError.NO_ERROR) { - return failOpaque(Error.MATH_ERROR, FailureInfo.REDEEM_NEW_ACCOUNT_BALANCE_CALCULATION_FAILED, uint(vars.mathErr)); - } - - /* Fail gracefully if protocol has insufficient cash */ - if (getCashPrior() < vars.redeemAmount) { - return fail(Error.TOKEN_INSUFFICIENT_CASH, FailureInfo.REDEEM_TRANSFER_OUT_NOT_POSSIBLE); - } - - ///////////////////////// - // EFFECTS & INTERACTIONS - // (No safe failures beyond this point) - - /* - * We invoke doTransferOut for the redeemer and the redeemAmount. - * Note: The cToken must handle variations between ERC-20 and ETH underlying. - * On success, the cToken has redeemAmount less of cash. - * doTransferOut reverts if anything goes wrong, since we can't be sure if side effects occurred. - */ - doTransferOut(redeemer, vars.redeemAmount); - - /* We write previously calculated values into storage */ - totalSupply = vars.totalSupplyNew; - accountTokens[redeemer] = vars.accountTokensNew; - - /* We emit a Transfer event, and a Redeem event */ - emit Transfer(redeemer, address(this), vars.redeemTokens); - emit Redeem(redeemer, vars.redeemAmount, vars.redeemTokens); - - /* We call the defense hook */ - comptroller.redeemVerify(address(this), redeemer, vars.redeemAmount, vars.redeemTokens); - - return uint(Error.NO_ERROR); - } - - /** - * @notice Sender borrows assets from the protocol to their own address - * @param borrowAmount The amount of the underlying asset to borrow - * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) - */ - function borrowInternal(uint borrowAmount) internal nonReentrant returns (uint) { - uint error = accrueInterest(); - if (error != uint(Error.NO_ERROR)) { - // accrueInterest emits logs on errors, but we still want to log the fact that an attempted borrow failed - return fail(Error(error), FailureInfo.BORROW_ACCRUE_INTEREST_FAILED); - } - // borrowFresh emits borrow-specific logs on errors, so we don't need to - return borrowFresh(msg.sender, borrowAmount); - } - - struct BorrowLocalVars { - MathError mathErr; - uint accountBorrows; - uint accountBorrowsNew; - uint totalBorrowsNew; - } - - /** - * @notice Users borrow assets from the protocol to their own address - * @param borrowAmount The amount of the underlying asset to borrow - * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) - */ - function borrowFresh(address payable borrower, uint borrowAmount) internal returns (uint) { - /* Fail if borrow not allowed */ - uint allowed = comptroller.borrowAllowed(address(this), borrower, borrowAmount); - if (allowed != 0) { - return failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.BORROW_COMPTROLLER_REJECTION, allowed); - } - - /* Verify market's block number equals current block number */ - if (accrualBlockNumber != getBlockNumber()) { - return fail(Error.MARKET_NOT_FRESH, FailureInfo.BORROW_FRESHNESS_CHECK); - } - - /* Fail gracefully if protocol has insufficient underlying cash */ - if (getCashPrior() < borrowAmount) { - return fail(Error.TOKEN_INSUFFICIENT_CASH, FailureInfo.BORROW_CASH_NOT_AVAILABLE); - } - - BorrowLocalVars memory vars; - - /* - * We calculate the new borrower and total borrow balances, failing on overflow: - * accountBorrowsNew = accountBorrows + borrowAmount - * totalBorrowsNew = totalBorrows + borrowAmount - */ - (vars.mathErr, vars.accountBorrows) = borrowBalanceStoredInternal(borrower); - if (vars.mathErr != MathError.NO_ERROR) { - return failOpaque(Error.MATH_ERROR, FailureInfo.BORROW_ACCUMULATED_BALANCE_CALCULATION_FAILED, uint(vars.mathErr)); - } - - (vars.mathErr, vars.accountBorrowsNew) = addUInt(vars.accountBorrows, borrowAmount); - if (vars.mathErr != MathError.NO_ERROR) { - return failOpaque(Error.MATH_ERROR, FailureInfo.BORROW_NEW_ACCOUNT_BORROW_BALANCE_CALCULATION_FAILED, uint(vars.mathErr)); - } - - (vars.mathErr, vars.totalBorrowsNew) = addUInt(totalBorrows, borrowAmount); - if (vars.mathErr != MathError.NO_ERROR) { - return failOpaque(Error.MATH_ERROR, FailureInfo.BORROW_NEW_TOTAL_BALANCE_CALCULATION_FAILED, uint(vars.mathErr)); - } - - ///////////////////////// - // EFFECTS & INTERACTIONS - // (No safe failures beyond this point) - - /* - * We invoke doTransferOut for the borrower and the borrowAmount. - * Note: The cToken must handle variations between ERC-20 and ETH underlying. - * On success, the cToken borrowAmount less of cash. - * doTransferOut reverts if anything goes wrong, since we can't be sure if side effects occurred. - */ - doTransferOut(borrower, borrowAmount); - - /* We write the previously calculated values into storage */ - accountBorrows[borrower].principal = vars.accountBorrowsNew; - accountBorrows[borrower].interestIndex = borrowIndex; - totalBorrows = vars.totalBorrowsNew; - - /* We emit a Borrow event */ - emit Borrow(borrower, borrowAmount, vars.accountBorrowsNew, vars.totalBorrowsNew); - - /* We call the defense hook */ - // unused function - // comptroller.borrowVerify(address(this), borrower, borrowAmount); - - return uint(Error.NO_ERROR); - } - - /** - * @notice Sender repays their own borrow - * @param repayAmount The amount to repay - * @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual repayment amount. - */ - function repayBorrowInternal(uint repayAmount) internal nonReentrant returns (uint, uint) { - uint error = accrueInterest(); - if (error != uint(Error.NO_ERROR)) { - // accrueInterest emits logs on errors, but we still want to log the fact that an attempted borrow failed - return (fail(Error(error), FailureInfo.REPAY_BORROW_ACCRUE_INTEREST_FAILED), 0); - } - // repayBorrowFresh emits repay-borrow-specific logs on errors, so we don't need to - return repayBorrowFresh(msg.sender, msg.sender, repayAmount); - } - - /** - * @notice Sender repays a borrow belonging to borrower - * @param borrower the account with the debt being payed off - * @param repayAmount The amount to repay - * @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual repayment amount. - */ - function repayBorrowBehalfInternal(address borrower, uint repayAmount) internal nonReentrant returns (uint, uint) { - uint error = accrueInterest(); - if (error != uint(Error.NO_ERROR)) { - // accrueInterest emits logs on errors, but we still want to log the fact that an attempted borrow failed - return (fail(Error(error), FailureInfo.REPAY_BEHALF_ACCRUE_INTEREST_FAILED), 0); - } - // repayBorrowFresh emits repay-borrow-specific logs on errors, so we don't need to - return repayBorrowFresh(msg.sender, borrower, repayAmount); - } - - struct RepayBorrowLocalVars { - Error err; - MathError mathErr; - uint repayAmount; - uint borrowerIndex; - uint accountBorrows; - uint accountBorrowsNew; - uint totalBorrowsNew; - uint actualRepayAmount; - } - - /** - * @notice Borrows are repaid by another user (possibly the borrower). - * @param payer the account paying off the borrow - * @param borrower the account with the debt being payed off - * @param repayAmount the amount of undelrying tokens being returned - * @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual repayment amount. - */ - function repayBorrowFresh(address payer, address borrower, uint repayAmount) internal returns (uint, uint) { - /* Fail if repayBorrow not allowed */ - uint allowed = comptroller.repayBorrowAllowed(address(this), payer, borrower, repayAmount); - if (allowed != 0) { - return (failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.REPAY_BORROW_COMPTROLLER_REJECTION, allowed), 0); - } - - /* Verify market's block number equals current block number */ - if (accrualBlockNumber != getBlockNumber()) { - return (fail(Error.MARKET_NOT_FRESH, FailureInfo.REPAY_BORROW_FRESHNESS_CHECK), 0); - } - - RepayBorrowLocalVars memory vars; - - /* We remember the original borrowerIndex for verification purposes */ - vars.borrowerIndex = accountBorrows[borrower].interestIndex; - - /* We fetch the amount the borrower owes, with accumulated interest */ - (vars.mathErr, vars.accountBorrows) = borrowBalanceStoredInternal(borrower); - if (vars.mathErr != MathError.NO_ERROR) { - return (failOpaque(Error.MATH_ERROR, FailureInfo.REPAY_BORROW_ACCUMULATED_BALANCE_CALCULATION_FAILED, uint(vars.mathErr)), 0); - } - - /* If repayAmount == -1, repayAmount = accountBorrows */ - if (repayAmount == uint(-1)) { - vars.repayAmount = vars.accountBorrows; - } else { - vars.repayAmount = repayAmount; - } - - ///////////////////////// - // EFFECTS & INTERACTIONS - // (No safe failures beyond this point) - - /* - * We call doTransferIn for the payer and the repayAmount - * Note: The cToken must handle variations between ERC-20 and ETH underlying. - * On success, the cToken holds an additional repayAmount of cash. - * doTransferIn reverts if anything goes wrong, since we can't be sure if side effects occurred. - * it returns the amount actually transferred, in case of a fee. - */ - vars.actualRepayAmount = doTransferIn(payer, vars.repayAmount); - - /* - * We calculate the new borrower and total borrow balances, failing on underflow: - * accountBorrowsNew = accountBorrows - actualRepayAmount - * totalBorrowsNew = totalBorrows - actualRepayAmount - */ - (vars.mathErr, vars.accountBorrowsNew) = subUInt(vars.accountBorrows, vars.actualRepayAmount); - require(vars.mathErr == MathError.NO_ERROR, "REPAY_BORROW_NEW_ACCOUNT_BORROW_BALANCE_CALCULATION_FAILED"); - - (vars.mathErr, vars.totalBorrowsNew) = subUInt(totalBorrows, vars.actualRepayAmount); - require(vars.mathErr == MathError.NO_ERROR, "REPAY_BORROW_NEW_TOTAL_BALANCE_CALCULATION_FAILED"); - - /* We write the previously calculated values into storage */ - accountBorrows[borrower].principal = vars.accountBorrowsNew; - accountBorrows[borrower].interestIndex = borrowIndex; - totalBorrows = vars.totalBorrowsNew; - - /* We emit a RepayBorrow event */ - emit RepayBorrow(payer, borrower, vars.actualRepayAmount, vars.accountBorrowsNew, vars.totalBorrowsNew); - - /* We call the defense hook */ - // unused function - // comptroller.repayBorrowVerify(address(this), payer, borrower, vars.actualRepayAmount, vars.borrowerIndex); - - return (uint(Error.NO_ERROR), vars.actualRepayAmount); - } - - /** - * @notice The sender liquidates the borrowers collateral. - * The collateral seized is transferred to the liquidator. - * @param borrower The borrower of this cToken to be liquidated - * @param cTokenCollateral The market in which to seize collateral from the borrower - * @param repayAmount The amount of the underlying borrowed asset to repay - * @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual repayment amount. - */ - function liquidateBorrowInternal(address borrower, uint repayAmount, CTokenInterface cTokenCollateral) internal nonReentrant returns (uint, uint) { - uint error = accrueInterest(); - if (error != uint(Error.NO_ERROR)) { - // accrueInterest emits logs on errors, but we still want to log the fact that an attempted liquidation failed - return (fail(Error(error), FailureInfo.LIQUIDATE_ACCRUE_BORROW_INTEREST_FAILED), 0); - } - - error = cTokenCollateral.accrueInterest(); - if (error != uint(Error.NO_ERROR)) { - // accrueInterest emits logs on errors, but we still want to log the fact that an attempted liquidation failed - return (fail(Error(error), FailureInfo.LIQUIDATE_ACCRUE_COLLATERAL_INTEREST_FAILED), 0); - } - - // liquidateBorrowFresh emits borrow-specific logs on errors, so we don't need to - return liquidateBorrowFresh(msg.sender, borrower, repayAmount, cTokenCollateral); - } - - /** - * @notice The liquidator liquidates the borrowers collateral. - * The collateral seized is transferred to the liquidator. - * @param borrower The borrower of this cToken to be liquidated - * @param liquidator The address repaying the borrow and seizing collateral - * @param cTokenCollateral The market in which to seize collateral from the borrower - * @param repayAmount The amount of the underlying borrowed asset to repay - * @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual repayment amount. - */ - function liquidateBorrowFresh(address liquidator, address borrower, uint repayAmount, CTokenInterface cTokenCollateral) internal returns (uint, uint) { - /* Fail if liquidate not allowed */ - uint allowed = comptroller.liquidateBorrowAllowed(address(this), address(cTokenCollateral), liquidator, borrower, repayAmount); - if (allowed != 0) { - return (failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.LIQUIDATE_COMPTROLLER_REJECTION, allowed), 0); - } - - /* Verify market's block number equals current block number */ - if (accrualBlockNumber != getBlockNumber()) { - return (fail(Error.MARKET_NOT_FRESH, FailureInfo.LIQUIDATE_FRESHNESS_CHECK), 0); - } - - /* Verify cTokenCollateral market's block number equals current block number */ - if (cTokenCollateral.accrualBlockNumber() != getBlockNumber()) { - return (fail(Error.MARKET_NOT_FRESH, FailureInfo.LIQUIDATE_COLLATERAL_FRESHNESS_CHECK), 0); - } - - /* Fail if borrower = liquidator */ - if (borrower == liquidator) { - return (fail(Error.INVALID_ACCOUNT_PAIR, FailureInfo.LIQUIDATE_LIQUIDATOR_IS_BORROWER), 0); - } - - /* Fail if repayAmount = 0 */ - if (repayAmount == 0) { - return (fail(Error.INVALID_CLOSE_AMOUNT_REQUESTED, FailureInfo.LIQUIDATE_CLOSE_AMOUNT_IS_ZERO), 0); - } - - /* Fail if repayAmount = -1 */ - if (repayAmount == uint(-1)) { - return (fail(Error.INVALID_CLOSE_AMOUNT_REQUESTED, FailureInfo.LIQUIDATE_CLOSE_AMOUNT_IS_UINT_MAX), 0); - } - - - /* Fail if repayBorrow fails */ - (uint repayBorrowError, uint actualRepayAmount) = repayBorrowFresh(liquidator, borrower, repayAmount); - if (repayBorrowError != uint(Error.NO_ERROR)) { - return (fail(Error(repayBorrowError), FailureInfo.LIQUIDATE_REPAY_BORROW_FRESH_FAILED), 0); - } - - ///////////////////////// - // EFFECTS & INTERACTIONS - // (No safe failures beyond this point) - - /* We calculate the number of collateral tokens that will be seized */ - (uint amountSeizeError, uint seizeTokens) = comptroller.liquidateCalculateSeizeTokens(address(this), address(cTokenCollateral), actualRepayAmount); - require(amountSeizeError == uint(Error.NO_ERROR), "LIQUIDATE_COMPTROLLER_CALCULATE_AMOUNT_SEIZE_FAILED"); - - /* Revert if borrower collateral token balance < seizeTokens */ - require(cTokenCollateral.balanceOf(borrower) >= seizeTokens, "LIQUIDATE_SEIZE_TOO_MUCH"); - - // If this is also the collateral, run seizeInternal to avoid re-entrancy, otherwise make an external call - uint seizeError; - if (address(cTokenCollateral) == address(this)) { - seizeError = seizeInternal(address(this), liquidator, borrower, seizeTokens); - } else { - seizeError = cTokenCollateral.seize(liquidator, borrower, seizeTokens); - } - - /* Revert if seize tokens fails (since we cannot be sure of side effects) */ - require(seizeError == uint(Error.NO_ERROR), "token seizure failed"); - - /* We emit a LiquidateBorrow event */ - emit LiquidateBorrow(liquidator, borrower, actualRepayAmount, address(cTokenCollateral), seizeTokens); - - /* We call the defense hook */ - // unused function - // comptroller.liquidateBorrowVerify(address(this), address(cTokenCollateral), liquidator, borrower, actualRepayAmount, seizeTokens); - - return (uint(Error.NO_ERROR), actualRepayAmount); - } - - /** - * @notice Transfers collateral tokens (this market) to the liquidator. - * @dev Will fail unless called by another cToken during the process of liquidation. - * Its absolutely critical to use msg.sender as the borrowed cToken and not a parameter. - * @param liquidator The account receiving seized collateral - * @param borrower The account having collateral seized - * @param seizeTokens The number of cTokens to seize - * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) - */ - function seize(address liquidator, address borrower, uint seizeTokens) override external nonReentrant returns (uint) { - return seizeInternal(msg.sender, liquidator, borrower, seizeTokens); - } - - /** - * @notice Transfers collateral tokens (this market) to the liquidator. - * @dev Called only during an in-kind liquidation, or by liquidateBorrow during the liquidation of another CToken. - * Its absolutely critical to use msg.sender as the seizer cToken and not a parameter. - * @param seizerToken The contract seizing the collateral (i.e. borrowed cToken) - * @param liquidator The account receiving seized collateral - * @param borrower The account having collateral seized - * @param seizeTokens The number of cTokens to seize - * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) - */ - function seizeInternal(address seizerToken, address liquidator, address borrower, uint seizeTokens) internal returns (uint) { - /* Fail if seize not allowed */ - uint allowed = comptroller.seizeAllowed(address(this), seizerToken, liquidator, borrower, seizeTokens); - if (allowed != 0) { - return failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.LIQUIDATE_SEIZE_COMPTROLLER_REJECTION, allowed); - } - - /* Fail if borrower = liquidator */ - if (borrower == liquidator) { - return fail(Error.INVALID_ACCOUNT_PAIR, FailureInfo.LIQUIDATE_SEIZE_LIQUIDATOR_IS_BORROWER); - } - - MathError mathErr; - uint borrowerTokensNew; - uint liquidatorTokensNew; - - /* - * We calculate the new borrower and liquidator token balances, failing on underflow/overflow: - * borrowerTokensNew = accountTokens[borrower] - seizeTokens - * liquidatorTokensNew = accountTokens[liquidator] + seizeTokens - */ - (mathErr, borrowerTokensNew) = subUInt(accountTokens[borrower], seizeTokens); - if (mathErr != MathError.NO_ERROR) { - return failOpaque(Error.MATH_ERROR, FailureInfo.LIQUIDATE_SEIZE_BALANCE_DECREMENT_FAILED, uint(mathErr)); - } - - (mathErr, liquidatorTokensNew) = addUInt(accountTokens[liquidator], seizeTokens); - if (mathErr != MathError.NO_ERROR) { - return failOpaque(Error.MATH_ERROR, FailureInfo.LIQUIDATE_SEIZE_BALANCE_INCREMENT_FAILED, uint(mathErr)); - } - - ///////////////////////// - // EFFECTS & INTERACTIONS - // (No safe failures beyond this point) - - /* We write the previously calculated values into storage */ - accountTokens[borrower] = borrowerTokensNew; - accountTokens[liquidator] = liquidatorTokensNew; - - /* Emit a Transfer event */ - emit Transfer(borrower, liquidator, seizeTokens); - - /* We call the defense hook */ - // unused function - // comptroller.seizeVerify(address(this), seizerToken, liquidator, borrower, seizeTokens); - - return uint(Error.NO_ERROR); - } - - - /*** Admin Functions ***/ - - /** - * @notice Begins transfer of admin rights. The newPendingAdmin must call `_acceptAdmin` to finalize the transfer. - * @dev Admin function to begin change of admin. The newPendingAdmin must call `_acceptAdmin` to finalize the transfer. - * @param newPendingAdmin New pending admin. - * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) - */ - function _setPendingAdmin(address payable newPendingAdmin) override external returns (uint) { - // Check caller = admin - /* if (msg.sender != admin) { - return fail(Error.UNAUTHORIZED, FailureInfo.SET_PENDING_ADMIN_OWNER_CHECK); - } */ - - // Save current value, if any, for inclusion in log - address oldPendingAdmin = pendingAdmin; - - // Store pendingAdmin with value newPendingAdmin - pendingAdmin = newPendingAdmin; - - // Emit NewPendingAdmin(oldPendingAdmin, newPendingAdmin) - emit NewPendingAdmin(oldPendingAdmin, newPendingAdmin); - - return uint(Error.NO_ERROR); - } - - /** - * @notice Accepts transfer of admin rights. msg.sender must be pendingAdmin - * @dev Admin function for pending admin to accept role and update admin - * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) - */ - function _acceptAdmin() override external returns (uint) { - // Check caller is pendingAdmin and pendingAdmin ≠ address(0) - /* if (msg.sender != pendingAdmin || msg.sender == address(0)) { - return fail(Error.UNAUTHORIZED, FailureInfo.ACCEPT_ADMIN_PENDING_ADMIN_CHECK); - } */ - - // Save current values for inclusion in log - address oldAdmin = admin; - address oldPendingAdmin = pendingAdmin; - - // Store admin with value pendingAdmin - admin = pendingAdmin; - - // Clear the pending value - pendingAdmin = address(0); - - emit NewAdmin(oldAdmin, admin); - emit NewPendingAdmin(oldPendingAdmin, pendingAdmin); - - return uint(Error.NO_ERROR); - } - - /** - * @notice Sets a new comptroller for the market - * @dev Admin function to set a new comptroller - * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) - */ - function _setComptroller(ComptrollerInterface newComptroller) override public returns (uint) { - // Check caller is admin - /* if (msg.sender != admin) { - return fail(Error.UNAUTHORIZED, FailureInfo.SET_COMPTROLLER_OWNER_CHECK); - } */ - - ComptrollerInterface oldComptroller = comptroller; - // Ensure invoke comptroller.isComptroller() returns true - require(newComptroller.isComptroller(), "marker method returned false"); - - // Set market's comptroller to newComptroller - comptroller = newComptroller; - - // Emit NewComptroller(oldComptroller, newComptroller) - emit NewComptroller(oldComptroller, newComptroller); - - return uint(Error.NO_ERROR); - } - - /** - * @notice accrues interest and sets a new reserve factor for the protocol using _setReserveFactorFresh - * @dev Admin function to accrue interest and set a new reserve factor - * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) - */ - function _setReserveFactor(uint newReserveFactorMantissa) override external nonReentrant returns (uint) { - uint error = accrueInterest(); - if (error != uint(Error.NO_ERROR)) { - // accrueInterest emits logs on errors, but on top of that we want to log the fact that an attempted reserve factor change failed. - return fail(Error(error), FailureInfo.SET_RESERVE_FACTOR_ACCRUE_INTEREST_FAILED); - } - // _setReserveFactorFresh emits reserve-factor-specific logs on errors, so we don't need to. - return _setReserveFactorFresh(newReserveFactorMantissa); - } - - /** - * @notice Sets a new reserve factor for the protocol (*requires fresh interest accrual) - * @dev Admin function to set a new reserve factor - * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) - */ - function _setReserveFactorFresh(uint newReserveFactorMantissa) internal returns (uint) { - // Check caller is admin - /* if (msg.sender != admin) { - return fail(Error.UNAUTHORIZED, FailureInfo.SET_RESERVE_FACTOR_ADMIN_CHECK); - } */ - - // Verify market's block number equals current block number - if (accrualBlockNumber != getBlockNumber()) { - return fail(Error.MARKET_NOT_FRESH, FailureInfo.SET_RESERVE_FACTOR_FRESH_CHECK); - } - - // Check newReserveFactor ≤ maxReserveFactor - if (newReserveFactorMantissa > reserveFactorMaxMantissa) { - return fail(Error.BAD_INPUT, FailureInfo.SET_RESERVE_FACTOR_BOUNDS_CHECK); - } - - uint oldReserveFactorMantissa = reserveFactorMantissa; - reserveFactorMantissa = newReserveFactorMantissa; - - emit NewReserveFactor(oldReserveFactorMantissa, newReserveFactorMantissa); - - return uint(Error.NO_ERROR); - } - - /** - * @notice Accrues interest and reduces reserves by transferring from msg.sender - * @param addAmount Amount of addition to reserves - * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) - */ - function _addReservesInternal(uint addAmount) internal nonReentrant returns (uint) { - uint error = accrueInterest(); - if (error != uint(Error.NO_ERROR)) { - // accrueInterest emits logs on errors, but on top of that we want to log the fact that an attempted reduce reserves failed. - return fail(Error(error), FailureInfo.ADD_RESERVES_ACCRUE_INTEREST_FAILED); - } - - // _addReservesFresh emits reserve-addition-specific logs on errors, so we don't need to. - (error, ) = _addReservesFresh(addAmount); - return error; - } - - /** - * @notice Add reserves by transferring from caller - * @dev Requires fresh interest accrual - * @param addAmount Amount of addition to reserves - * @return (uint, uint) An error code (0=success, otherwise a failure (see ErrorReporter.sol for details)) and the actual amount added, net token fees - */ - function _addReservesFresh(uint addAmount) internal returns (uint, uint) { - // totalReserves + actualAddAmount - uint totalReservesNew; - uint actualAddAmount; - - // We fail gracefully unless market's block number equals current block number - if (accrualBlockNumber != getBlockNumber()) { - return (fail(Error.MARKET_NOT_FRESH, FailureInfo.ADD_RESERVES_FRESH_CHECK), actualAddAmount); - } - - ///////////////////////// - // EFFECTS & INTERACTIONS - // (No safe failures beyond this point) - - /* - * We call doTransferIn for the caller and the addAmount - * Note: The cToken must handle variations between ERC-20 and ETH underlying. - * On success, the cToken holds an additional addAmount of cash. - * doTransferIn reverts if anything goes wrong, since we can't be sure if side effects occurred. - * it returns the amount actually transferred, in case of a fee. - */ - - actualAddAmount = doTransferIn(msg.sender, addAmount); - - totalReservesNew = totalReserves + actualAddAmount; - - /* Revert on overflow */ - require(totalReservesNew >= totalReserves, "add reserves unexpected overflow"); - - // Store reserves[n+1] = reserves[n] + actualAddAmount - totalReserves = totalReservesNew; - - /* Emit NewReserves(admin, actualAddAmount, reserves[n+1]) */ - emit ReservesAdded(msg.sender, actualAddAmount, totalReservesNew); - - /* Return (NO_ERROR, actualAddAmount) */ - return (uint(Error.NO_ERROR), actualAddAmount); - } - - - /** - * @notice Accrues interest and reduces reserves by transferring to admin - * @param reduceAmount Amount of reduction to reserves - * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) - */ - function _reduceReserves(uint reduceAmount) override external nonReentrant returns (uint) { - uint error = accrueInterest(); - if (error != uint(Error.NO_ERROR)) { - // accrueInterest emits logs on errors, but on top of that we want to log the fact that an attempted reduce reserves failed. - return fail(Error(error), FailureInfo.REDUCE_RESERVES_ACCRUE_INTEREST_FAILED); - } - // _reduceReservesFresh emits reserve-reduction-specific logs on errors, so we don't need to. - return _reduceReservesFresh(reduceAmount); - } - - /** - * @notice Reduces reserves by transferring to admin - * @dev Requires fresh interest accrual - * @param reduceAmount Amount of reduction to reserves - * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) - */ - function _reduceReservesFresh(uint reduceAmount) internal returns (uint) { - // totalReserves - reduceAmount - uint totalReservesNew; - - // Check caller is admin - /* if (msg.sender != admin) { - return fail(Error.UNAUTHORIZED, FailureInfo.REDUCE_RESERVES_ADMIN_CHECK); - } */ - - // We fail gracefully unless market's block number equals current block number - if (accrualBlockNumber != getBlockNumber()) { - return fail(Error.MARKET_NOT_FRESH, FailureInfo.REDUCE_RESERVES_FRESH_CHECK); - } - - // Fail gracefully if protocol has insufficient underlying cash - if (getCashPrior() < reduceAmount) { - return fail(Error.TOKEN_INSUFFICIENT_CASH, FailureInfo.REDUCE_RESERVES_CASH_NOT_AVAILABLE); - } - - // Check reduceAmount ≤ reserves[n] (totalReserves) - if (reduceAmount > totalReserves) { - return fail(Error.BAD_INPUT, FailureInfo.REDUCE_RESERVES_VALIDATION); - } - - ///////////////////////// - // EFFECTS & INTERACTIONS - // (No safe failures beyond this point) - - totalReservesNew = totalReserves - reduceAmount; - // We checked reduceAmount <= totalReserves above, so this should never revert. - require(totalReservesNew <= totalReserves, "reduce reserves unexpected underflow"); - - // Store reserves[n+1] = reserves[n] - reduceAmount - totalReserves = totalReservesNew; - - // doTransferOut reverts if anything goes wrong, since we can't be sure if side effects occurred. - doTransferOut(admin, reduceAmount); - - emit ReservesReduced(admin, reduceAmount, totalReservesNew); - - return uint(Error.NO_ERROR); - } - - /** - * @notice accrues interest and updates the interest rate model using _setInterestRateModelFresh - * @dev Admin function to accrue interest and update the interest rate model - * @param newInterestRateModel the new interest rate model to use - * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) - */ - function _setInterestRateModel(InterestRateModel newInterestRateModel) override public returns (uint) { - uint error = accrueInterest(); - if (error != uint(Error.NO_ERROR)) { - // accrueInterest emits logs on errors, but on top of that we want to log the fact that an attempted change of interest rate model failed - return fail(Error(error), FailureInfo.SET_INTEREST_RATE_MODEL_ACCRUE_INTEREST_FAILED); - } - // _setInterestRateModelFresh emits interest-rate-model-update-specific logs on errors, so we don't need to. - return _setInterestRateModelFresh(newInterestRateModel); - } - - /** - * @notice updates the interest rate model (*requires fresh interest accrual) - * @dev Admin function to update the interest rate model - * @param newInterestRateModel the new interest rate model to use - * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) - */ - function _setInterestRateModelFresh(InterestRateModel newInterestRateModel) internal returns (uint) { - - // Used to store old model for use in the event that is emitted on success - InterestRateModel oldInterestRateModel; - - // Check caller is admin - /* if (msg.sender != admin) { - return fail(Error.UNAUTHORIZED, FailureInfo.SET_INTEREST_RATE_MODEL_OWNER_CHECK); - } */ - - // We fail gracefully unless market's block number equals current block number - if (accrualBlockNumber != getBlockNumber()) { - return fail(Error.MARKET_NOT_FRESH, FailureInfo.SET_INTEREST_RATE_MODEL_FRESH_CHECK); - } - - // Track the market's current interest rate model - oldInterestRateModel = interestRateModel; - - // Ensure invoke newInterestRateModel.isInterestRateModel() returns true - require(newInterestRateModel.isInterestRateModel(), "marker method returned false"); - - // Set the interest rate model to newInterestRateModel - interestRateModel = newInterestRateModel; - - // Emit NewMarketInterestRateModel(oldInterestRateModel, newInterestRateModel) - emit NewMarketInterestRateModel(oldInterestRateModel, newInterestRateModel); - - return uint(Error.NO_ERROR); - } - - /*** Safe Token ***/ - - /** - * @notice Gets balance of this contract in terms of the underlying - * @dev This excludes the value of the current message, if any - * @return The quantity of underlying owned by this contract - */ - function getCashPrior() virtual internal view returns (uint) {} - - /** - * @dev Performs a transfer in, reverting upon failure. Returns the amount actually transferred to the protocol, in case of a fee. - * This may revert due to insufficient balance or insufficient allowance. - */ - function doTransferIn(address from, uint amount) virtual internal returns (uint) {} - - /** - * @dev Performs a transfer out, ideally returning an explanatory error code upon failure tather than reverting. - * If caller has not called checked protocol's balance, may revert due to insufficient cash held in the contract. - * If caller has checked protocol's balance, and verified it is >= amount, this should not revert in normal conditions. - */ - function doTransferOut(address payable to, uint amount) virtual internal {} - - - /*** Reentrancy Guard ***/ - - /** - * @dev Prevents a contract from calling itself, directly or indirectly. - */ - modifier nonReentrant() { - require(_notEntered, "re-entered"); - _notEntered = false; - _; - _notEntered = true; // get a gas-refund post-Istanbul - } -} +pragma solidity ^0.6.7; + +import "./ComptrollerInterface.sol"; +import "./CTokenInterfaces.sol"; +import "./ErrorReporter.sol"; +import "./Exponential.sol"; +import "./EIP20Interface.sol"; +import "./InterestRateModel.sol"; + +/** + * @title Compound's CToken Contract + * @notice Abstract base for CTokens + * @author Compound + */ + +// NOTE: admin checks are commented for the sake of easy testing + +contract CToken is CTokenInterface, Exponential, TokenErrorReporter { + /** + * @notice Initialize the money market + * @param comptroller_ The address of the Comptroller + * @param interestRateModel_ The address of the interest rate model + * @param initialExchangeRateMantissa_ The initial exchange rate, scaled by 1e18 + * @param name_ EIP-20 name of this token + * @param symbol_ EIP-20 symbol of this token + * @param decimals_ EIP-20 decimal precision of this token + */ + function initialize(ComptrollerInterface comptroller_, + InterestRateModel interestRateModel_, + uint initialExchangeRateMantissa_, + string memory name_, + string memory symbol_, + uint8 decimals_) public { + // require(msg.sender == admin, "only admin may initialize the market"); + require(accrualBlockNumber == 0 && borrowIndex == 0, "market may only be initialized once"); + + // Set initial exchange rate + initialExchangeRateMantissa = initialExchangeRateMantissa_; + require(initialExchangeRateMantissa > 0, "initial exchange rate must be greater than zero."); + + // Set the comptroller + uint err = _setComptroller(comptroller_); + require(err == uint(Error.NO_ERROR), "setting comptroller failed"); + + // Initialize block number and borrow index (block number mocks depend on comptroller being set) + accrualBlockNumber = getBlockNumber(); + borrowIndex = mantissaOne; + + // Set the interest rate model (depends on block number / borrow index) + err = _setInterestRateModelFresh(interestRateModel_); + require(err == uint(Error.NO_ERROR), "setting interest rate model failed"); + + name = name_; + symbol = symbol_; + decimals = decimals_; + + // The counter starts true to prevent changing it from zero to non-zero (i.e. smaller cost/refund) + _notEntered = true; + } + + /** + * @notice Transfer `tokens` tokens from `src` to `dst` by `spender` + * @dev Called by both `transfer` and `transferFrom` internally + * @param spender The address of the account performing the transfer + * @param src The address of the source account + * @param dst The address of the destination account + * @param tokens The number of tokens to transfer + * @return Whether or not the transfer succeeded + */ + function transferTokens(address spender, address src, address dst, uint tokens) internal returns (uint) { + /* Fail if transfer not allowed */ + uint allowed = comptroller.transferAllowed(address(this), src, dst, tokens); + if (allowed != 0) { + return failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.TRANSFER_COMPTROLLER_REJECTION, allowed); + } + + /* Do not allow self-transfers */ + if (src == dst) { + return fail(Error.BAD_INPUT, FailureInfo.TRANSFER_NOT_ALLOWED); + } + + /* Get the allowance, infinite for the account owner */ + uint startingAllowance = 0; + if (spender == src) { + startingAllowance = uint(-1); + } else { + startingAllowance = transferAllowances[src][spender]; + } + + /* Do the calculations, checking for {under,over}flow */ + MathError mathErr; + uint allowanceNew; + uint srcTokensNew; + uint dstTokensNew; + + (mathErr, allowanceNew) = subUInt(startingAllowance, tokens); + if (mathErr != MathError.NO_ERROR) { + return fail(Error.MATH_ERROR, FailureInfo.TRANSFER_NOT_ALLOWED); + } + + (mathErr, srcTokensNew) = subUInt(accountTokens[src], tokens); + if (mathErr != MathError.NO_ERROR) { + return fail(Error.MATH_ERROR, FailureInfo.TRANSFER_NOT_ENOUGH); + } + + (mathErr, dstTokensNew) = addUInt(accountTokens[dst], tokens); + if (mathErr != MathError.NO_ERROR) { + return fail(Error.MATH_ERROR, FailureInfo.TRANSFER_TOO_MUCH); + } + + ///////////////////////// + // EFFECTS & INTERACTIONS + // (No safe failures beyond this point) + + accountTokens[src] = srcTokensNew; + accountTokens[dst] = dstTokensNew; + + /* Eat some of the allowance (if necessary) */ + if (startingAllowance != uint(-1)) { + transferAllowances[src][spender] = allowanceNew; + } + + /* We emit a Transfer event */ + emit Transfer(src, dst, tokens); + + // unused function + // comptroller.transferVerify(address(this), src, dst, tokens); + + return uint(Error.NO_ERROR); + } + + /** + * @notice Transfer `amount` tokens from `msg.sender` to `dst` + * @param dst The address of the destination account + * @param amount The number of tokens to transfer + * @return Whether or not the transfer succeeded + */ + function transfer(address dst, uint256 amount) override external nonReentrant returns (bool) { + return transferTokens(msg.sender, msg.sender, dst, amount) == uint(Error.NO_ERROR); + } + + /** + * @notice Transfer `amount` tokens from `src` to `dst` + * @param src The address of the source account + * @param dst The address of the destination account + * @param amount The number of tokens to transfer + * @return Whether or not the transfer succeeded + */ + function transferFrom(address src, address dst, uint256 amount) override external nonReentrant returns (bool) { + return transferTokens(msg.sender, src, dst, amount) == uint(Error.NO_ERROR); + } + + /** + * @notice Approve `spender` to transfer up to `amount` from `src` + * @dev This will overwrite the approval amount for `spender` + * and is subject to issues noted [here](https://eips.ethereum.org/EIPS/eip-20#approve) + * @param spender The address of the account which may transfer tokens + * @param amount The number of tokens that are approved (-1 means infinite) + * @return Whether or not the approval succeeded + */ + function approve(address spender, uint256 amount) override external returns (bool) { + address src = msg.sender; + transferAllowances[src][spender] = amount; + emit Approval(src, spender, amount); + return true; + } + + /** + * @notice Get the current allowance from `owner` for `spender` + * @param owner The address of the account which owns the tokens to be spent + * @param spender The address of the account which may transfer tokens + * @return The number of tokens allowed to be spent (-1 means infinite) + */ + function allowance(address owner, address spender) override external view returns (uint256) { + return transferAllowances[owner][spender]; + } + + /** + * @notice Get the token balance of the `owner` + * @param owner The address of the account to query + * @return The number of tokens owned by `owner` + */ + function balanceOf(address owner) override external view returns (uint256) { + return accountTokens[owner]; + } + + /** + * @notice Get the underlying balance of the `owner` + * @dev This also accrues interest in a transaction + * @param owner The address of the account to query + * @return The amount of underlying owned by `owner` + */ + function balanceOfUnderlying(address owner) override external returns (uint) { + Exp memory exchangeRate = Exp({mantissa: exchangeRateCurrent()}); + (MathError mErr, uint balance) = mulScalarTruncate(exchangeRate, accountTokens[owner]); + require(mErr == MathError.NO_ERROR, "balance could not be calculated"); + return balance; + } + + /** + * @notice Get a snapshot of the account's balances, and the cached exchange rate + * @dev This is used by comptroller to more efficiently perform liquidity checks. + * @param account Address of the account to snapshot + * @return (possible error, token balance, borrow balance, exchange rate mantissa) + */ + function getAccountSnapshot(address account) override external view returns (uint, uint, uint, uint) { + uint cTokenBalance = accountTokens[account]; + uint borrowBalance; + uint exchangeRateMantissa; + + MathError mErr; + + (mErr, borrowBalance) = borrowBalanceStoredInternal(account); + if (mErr != MathError.NO_ERROR) { + return (uint(Error.MATH_ERROR), 0, 0, 0); + } + + (mErr, exchangeRateMantissa) = exchangeRateStoredInternal(); + if (mErr != MathError.NO_ERROR) { + return (uint(Error.MATH_ERROR), 0, 0, 0); + } + + return (uint(Error.NO_ERROR), cTokenBalance, borrowBalance, exchangeRateMantissa); + } + + /** + * @dev Function to simply retrieve block number + * This exists mainly for inheriting test contracts to stub this result. + */ + function getBlockNumber() internal view returns (uint) { + return block.number; + } + + /** + * @notice Returns the current per-block borrow interest rate for this cToken + * @return The borrow interest rate per block, scaled by 1e18 + */ + function borrowRatePerBlock() override external view returns (uint) { + return interestRateModel.getBorrowRate(getCashPrior(), totalBorrows, totalReserves); + } + + /** + * @notice Returns the current per-block supply interest rate for this cToken + * @return The supply interest rate per block, scaled by 1e18 + */ + function supplyRatePerBlock() override external view returns (uint) { + return interestRateModel.getSupplyRate(getCashPrior(), totalBorrows, totalReserves, reserveFactorMantissa); + } + + /** + * @notice Returns the current total borrows plus accrued interest + * @return The total borrows with interest + */ + function totalBorrowsCurrent() override external nonReentrant returns (uint) { + require(accrueInterest() == uint(Error.NO_ERROR), "accrue interest failed"); + return totalBorrows; + } + + /** + * @notice Accrue interest to updated borrowIndex and then calculate account's borrow balance using the updated borrowIndex + * @param account The address whose balance should be calculated after updating borrowIndex + * @return The calculated balance + */ + function borrowBalanceCurrent(address account) override external nonReentrant returns (uint) { + require(accrueInterest() == uint(Error.NO_ERROR), "accrue interest failed"); + return borrowBalanceStored(account); + } + + /** + * @notice Return the borrow balance of account based on stored data + * @param account The address whose balance should be calculated + * @return The calculated balance + */ + function borrowBalanceStored(address account) override public view returns (uint) { + (MathError err, uint result) = borrowBalanceStoredInternal(account); + require(err == MathError.NO_ERROR, "borrowBalanceStored: borrowBalanceStoredInternal failed"); + return result; + } + + /** + * @notice Return the borrow balance of account based on stored data + * @param account The address whose balance should be calculated + * @return (error code, the calculated balance or 0 if error code is non-zero) + */ + function borrowBalanceStoredInternal(address account) internal view returns (MathError, uint) { + /* Note: we do not assert that the market is up to date */ + MathError mathErr; + uint principalTimesIndex; + uint result; + + /* Get borrowBalance and borrowIndex */ + BorrowSnapshot storage borrowSnapshot = accountBorrows[account]; + + /* If borrowBalance = 0 then borrowIndex is likely also 0. + * Rather than failing the calculation with a division by 0, we immediately return 0 in this case. + */ + if (borrowSnapshot.principal == 0) { + return (MathError.NO_ERROR, 0); + } + + /* Calculate new borrow balance using the interest index: + * recentBorrowBalance = borrower.borrowBalance * market.borrowIndex / borrower.borrowIndex + */ + (mathErr, principalTimesIndex) = mulUInt(borrowSnapshot.principal, borrowIndex); + if (mathErr != MathError.NO_ERROR) { + return (mathErr, 0); + } + + (mathErr, result) = divUInt(principalTimesIndex, borrowSnapshot.interestIndex); + if (mathErr != MathError.NO_ERROR) { + return (mathErr, 0); + } + + return (MathError.NO_ERROR, result); + } + + /** + * @notice Accrue interest then return the up-to-date exchange rate + * @return Calculated exchange rate scaled by 1e18 + */ + function exchangeRateCurrent() override public nonReentrant returns (uint) { + require(accrueInterest() == uint(Error.NO_ERROR), "accrue interest failed"); + return exchangeRateStored(); + } + + /** + * @notice Calculates the exchange rate from the underlying to the CToken + * @dev This function does not accrue interest before calculating the exchange rate + * @return Calculated exchange rate scaled by 1e18 + */ + function exchangeRateStored() override public view returns (uint) { + (MathError err, uint result) = exchangeRateStoredInternal(); + require(err == MathError.NO_ERROR, "exchangeRateStored: exchangeRateStoredInternal failed"); + return result; + } + + /** + * @notice Calculates the exchange rate from the underlying to the CToken + * @dev This function does not accrue interest before calculating the exchange rate + * @return (error code, calculated exchange rate scaled by 1e18) + */ + function exchangeRateStoredInternal() internal view returns (MathError, uint) { + uint _totalSupply = totalSupply; + if (_totalSupply == 0) { + /* + * If there are no tokens minted: + * exchangeRate = initialExchangeRate + */ + return (MathError.NO_ERROR, initialExchangeRateMantissa); + } else { + /* + * Otherwise: + * exchangeRate = (totalCash + totalBorrows - totalReserves) / totalSupply + */ + uint totalCash = getCashPrior(); + uint cashPlusBorrowsMinusReserves; + Exp memory exchangeRate; + MathError mathErr; + + (mathErr, cashPlusBorrowsMinusReserves) = addThenSubUInt(totalCash, totalBorrows, totalReserves); + if (mathErr != MathError.NO_ERROR) { + return (mathErr, 0); + } + + (mathErr, exchangeRate) = getExp(cashPlusBorrowsMinusReserves, _totalSupply); + if (mathErr != MathError.NO_ERROR) { + return (mathErr, 0); + } + + return (MathError.NO_ERROR, exchangeRate.mantissa); + } + } + + /** + * @notice Get cash balance of this cToken in the underlying asset + * @return The quantity of underlying asset owned by this contract + */ + function getCash() override external view returns (uint) { + return getCashPrior(); + } + + /** + * @notice Applies accrued interest to total borrows and reserves + * @dev This calculates interest accrued from the last checkpointed block + * up to the current block and writes new checkpoint to storage. + */ + function accrueInterest() override public returns (uint) { + /* Remember the initial block number */ + uint currentBlockNumber = getBlockNumber(); + uint accrualBlockNumberPrior = accrualBlockNumber; + + /* Short-circuit accumulating 0 interest */ + if (accrualBlockNumberPrior == currentBlockNumber) { + return uint(Error.NO_ERROR); + } + + /* Read the previous values out of storage */ + uint cashPrior = getCashPrior(); + uint borrowsPrior = totalBorrows; + uint reservesPrior = totalReserves; + uint borrowIndexPrior = borrowIndex; + + /* Calculate the current borrow interest rate */ + uint borrowRateMantissa = interestRateModel.getBorrowRate(cashPrior, borrowsPrior, reservesPrior); + require(borrowRateMantissa <= borrowRateMaxMantissa, "borrow rate is absurdly high"); + + /* Calculate the number of blocks elapsed since the last accrual */ + (MathError mathErr, uint blockDelta) = subUInt(currentBlockNumber, accrualBlockNumberPrior); + require(mathErr == MathError.NO_ERROR, "could not calculate block delta"); + + /* + * Calculate the interest accumulated into borrows and reserves and the new index: + * simpleInterestFactor = borrowRate * blockDelta + * interestAccumulated = simpleInterestFactor * totalBorrows + * totalBorrowsNew = interestAccumulated + totalBorrows + * totalReservesNew = interestAccumulated * reserveFactor + totalReserves + * borrowIndexNew = simpleInterestFactor * borrowIndex + borrowIndex + */ + + Exp memory simpleInterestFactor; + uint interestAccumulated; + uint totalBorrowsNew; + uint totalReservesNew; + uint borrowIndexNew; + + (mathErr, simpleInterestFactor) = mulScalar(Exp({mantissa: borrowRateMantissa}), blockDelta); + if (mathErr != MathError.NO_ERROR) { + return failOpaque(Error.MATH_ERROR, FailureInfo.ACCRUE_INTEREST_SIMPLE_INTEREST_FACTOR_CALCULATION_FAILED, uint(mathErr)); + } + + (mathErr, interestAccumulated) = mulScalarTruncate(simpleInterestFactor, borrowsPrior); + if (mathErr != MathError.NO_ERROR) { + return failOpaque(Error.MATH_ERROR, FailureInfo.ACCRUE_INTEREST_ACCUMULATED_INTEREST_CALCULATION_FAILED, uint(mathErr)); + } + + (mathErr, totalBorrowsNew) = addUInt(interestAccumulated, borrowsPrior); + if (mathErr != MathError.NO_ERROR) { + return failOpaque(Error.MATH_ERROR, FailureInfo.ACCRUE_INTEREST_NEW_TOTAL_BORROWS_CALCULATION_FAILED, uint(mathErr)); + } + + (mathErr, totalReservesNew) = mulScalarTruncateAddUInt(Exp({mantissa: reserveFactorMantissa}), interestAccumulated, reservesPrior); + if (mathErr != MathError.NO_ERROR) { + return failOpaque(Error.MATH_ERROR, FailureInfo.ACCRUE_INTEREST_NEW_TOTAL_RESERVES_CALCULATION_FAILED, uint(mathErr)); + } + + (mathErr, borrowIndexNew) = mulScalarTruncateAddUInt(simpleInterestFactor, borrowIndexPrior, borrowIndexPrior); + if (mathErr != MathError.NO_ERROR) { + return failOpaque(Error.MATH_ERROR, FailureInfo.ACCRUE_INTEREST_NEW_BORROW_INDEX_CALCULATION_FAILED, uint(mathErr)); + } + + ///////////////////////// + // EFFECTS & INTERACTIONS + // (No safe failures beyond this point) + + /* We write the previously calculated values into storage */ + accrualBlockNumber = currentBlockNumber; + borrowIndex = borrowIndexNew; + totalBorrows = totalBorrowsNew; + totalReserves = totalReservesNew; + + /* We emit an AccrueInterest event */ + emit AccrueInterest(cashPrior, interestAccumulated, borrowIndexNew, totalBorrowsNew); + + return uint(Error.NO_ERROR); + } + + /** + * @notice Sender supplies assets into the market and receives cTokens in exchange + * @dev Accrues interest whether or not the operation succeeds, unless reverted + * @param mintAmount The amount of the underlying asset to supply + * @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual mint amount. + */ + function mintInternal(uint mintAmount) internal nonReentrant returns (uint, uint) { + uint error = accrueInterest(); + if (error != uint(Error.NO_ERROR)) { + // accrueInterest emits logs on errors, but we still want to log the fact that an attempted borrow failed + return (fail(Error(error), FailureInfo.MINT_ACCRUE_INTEREST_FAILED), 0); + } + // mintFresh emits the actual Mint event if successful and logs on errors, so we don't need to + return mintFresh(msg.sender, mintAmount); + } + + struct MintLocalVars { + Error err; + MathError mathErr; + uint exchangeRateMantissa; + uint mintTokens; + uint totalSupplyNew; + uint accountTokensNew; + uint actualMintAmount; + } + + /** + * @notice User supplies assets into the market and receives cTokens in exchange + * @dev Assumes interest has already been accrued up to the current block + * @param minter The address of the account which is supplying the assets + * @param mintAmount The amount of the underlying asset to supply + * @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual mint amount. + */ + function mintFresh(address minter, uint mintAmount) internal returns (uint, uint) { + /* Fail if mint not allowed */ + uint allowed = comptroller.mintAllowed(address(this), minter, mintAmount); + if (allowed != 0) { + return (failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.MINT_COMPTROLLER_REJECTION, allowed), 0); + } + + /* Verify market's block number equals current block number */ + if (accrualBlockNumber != getBlockNumber()) { + return (fail(Error.MARKET_NOT_FRESH, FailureInfo.MINT_FRESHNESS_CHECK), 0); + } + + MintLocalVars memory vars; + + (vars.mathErr, vars.exchangeRateMantissa) = exchangeRateStoredInternal(); + if (vars.mathErr != MathError.NO_ERROR) { + return (failOpaque(Error.MATH_ERROR, FailureInfo.MINT_EXCHANGE_RATE_READ_FAILED, uint(vars.mathErr)), 0); + } + + ///////////////////////// + // EFFECTS & INTERACTIONS + // (No safe failures beyond this point) + + /* + * We call `doTransferIn` for the minter and the mintAmount. + * Note: The cToken must handle variations between ERC-20 and ETH underlying. + * `doTransferIn` reverts if anything goes wrong, since we can't be sure if + * side-effects occurred. The function returns the amount actually transferred, + * in case of a fee. On success, the cToken holds an additional `actualMintAmount` + * of cash. + */ + vars.actualMintAmount = doTransferIn(minter, mintAmount); + + /* + * We get the current exchange rate and calculate the number of cTokens to be minted: + * mintTokens = actualMintAmount / exchangeRate + */ + + (vars.mathErr, vars.mintTokens) = divScalarByExpTruncate(vars.actualMintAmount, Exp({mantissa: vars.exchangeRateMantissa})); + require(vars.mathErr == MathError.NO_ERROR, "MINT_EXCHANGE_CALCULATION_FAILED"); + + /* + * We calculate the new total supply of cTokens and minter token balance, checking for overflow: + * totalSupplyNew = totalSupply + mintTokens + * accountTokensNew = accountTokens[minter] + mintTokens + */ + (vars.mathErr, vars.totalSupplyNew) = addUInt(totalSupply, vars.mintTokens); + require(vars.mathErr == MathError.NO_ERROR, "MINT_NEW_TOTAL_SUPPLY_CALCULATION_FAILED"); + + (vars.mathErr, vars.accountTokensNew) = addUInt(accountTokens[minter], vars.mintTokens); + require(vars.mathErr == MathError.NO_ERROR, "MINT_NEW_ACCOUNT_BALANCE_CALCULATION_FAILED"); + + /* We write previously calculated values into storage */ + totalSupply = vars.totalSupplyNew; + accountTokens[minter] = vars.accountTokensNew; + + /* We emit a Mint event, and a Transfer event */ + emit Mint(minter, vars.actualMintAmount, vars.mintTokens); + emit Transfer(address(this), minter, vars.mintTokens); + + /* We call the defense hook */ + // unused function + // comptroller.mintVerify(address(this), minter, vars.actualMintAmount, vars.mintTokens); + + return (uint(Error.NO_ERROR), vars.actualMintAmount); + } + + /** + * @notice Sender redeems cTokens in exchange for the underlying asset + * @dev Accrues interest whether or not the operation succeeds, unless reverted + * @param redeemTokens The number of cTokens to redeem into underlying + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function redeemInternal(uint redeemTokens) internal nonReentrant returns (uint) { + uint error = accrueInterest(); + if (error != uint(Error.NO_ERROR)) { + // accrueInterest emits logs on errors, but we still want to log the fact that an attempted redeem failed + return fail(Error(error), FailureInfo.REDEEM_ACCRUE_INTEREST_FAILED); + } + // redeemFresh emits redeem-specific logs on errors, so we don't need to + return redeemFresh(msg.sender, redeemTokens, 0); + } + + /** + * @notice Sender redeems cTokens in exchange for a specified amount of underlying asset + * @dev Accrues interest whether or not the operation succeeds, unless reverted + * @param redeemAmount The amount of underlying to receive from redeeming cTokens + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function redeemUnderlyingInternal(uint redeemAmount) internal nonReentrant returns (uint) { + uint error = accrueInterest(); + if (error != uint(Error.NO_ERROR)) { + // accrueInterest emits logs on errors, but we still want to log the fact that an attempted redeem failed + return fail(Error(error), FailureInfo.REDEEM_ACCRUE_INTEREST_FAILED); + } + // redeemFresh emits redeem-specific logs on errors, so we don't need to + return redeemFresh(msg.sender, 0, redeemAmount); + } + + struct RedeemLocalVars { + Error err; + MathError mathErr; + uint exchangeRateMantissa; + uint redeemTokens; + uint redeemAmount; + uint totalSupplyNew; + uint accountTokensNew; + } + + /** + * @notice User redeems cTokens in exchange for the underlying asset + * @dev Assumes interest has already been accrued up to the current block + * @param redeemer The address of the account which is redeeming the tokens + * @param redeemTokensIn The number of cTokens to redeem into underlying (only one of redeemTokensIn or redeemAmountIn may be non-zero) + * @param redeemAmountIn The number of underlying tokens to receive from redeeming cTokens (only one of redeemTokensIn or redeemAmountIn may be non-zero) + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function redeemFresh(address payable redeemer, uint redeemTokensIn, uint redeemAmountIn) internal returns (uint) { + require(redeemTokensIn == 0 || redeemAmountIn == 0, "one of redeemTokensIn or redeemAmountIn must be zero"); + + RedeemLocalVars memory vars; + + /* exchangeRate = invoke Exchange Rate Stored() */ + (vars.mathErr, vars.exchangeRateMantissa) = exchangeRateStoredInternal(); + if (vars.mathErr != MathError.NO_ERROR) { + return failOpaque(Error.MATH_ERROR, FailureInfo.REDEEM_EXCHANGE_RATE_READ_FAILED, uint(vars.mathErr)); + } + + /* If redeemTokensIn > 0: */ + if (redeemTokensIn > 0) { + /* + * We calculate the exchange rate and the amount of underlying to be redeemed: + * redeemTokens = redeemTokensIn + * redeemAmount = redeemTokensIn x exchangeRateCurrent + */ + vars.redeemTokens = redeemTokensIn; + + (vars.mathErr, vars.redeemAmount) = mulScalarTruncate(Exp({mantissa: vars.exchangeRateMantissa}), redeemTokensIn); + if (vars.mathErr != MathError.NO_ERROR) { + return failOpaque(Error.MATH_ERROR, FailureInfo.REDEEM_EXCHANGE_TOKENS_CALCULATION_FAILED, uint(vars.mathErr)); + } + } else { + /* + * We get the current exchange rate and calculate the amount to be redeemed: + * redeemTokens = redeemAmountIn / exchangeRate + * redeemAmount = redeemAmountIn + */ + + (vars.mathErr, vars.redeemTokens) = divScalarByExpTruncate(redeemAmountIn, Exp({mantissa: vars.exchangeRateMantissa})); + if (vars.mathErr != MathError.NO_ERROR) { + return failOpaque(Error.MATH_ERROR, FailureInfo.REDEEM_EXCHANGE_AMOUNT_CALCULATION_FAILED, uint(vars.mathErr)); + } + + vars.redeemAmount = redeemAmountIn; + } + + /* Fail if redeem not allowed */ + uint allowed = comptroller.redeemAllowed(address(this), redeemer, vars.redeemTokens); + if (allowed != 0) { + return failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.REDEEM_COMPTROLLER_REJECTION, allowed); + } + + /* Verify market's block number equals current block number */ + if (accrualBlockNumber != getBlockNumber()) { + return fail(Error.MARKET_NOT_FRESH, FailureInfo.REDEEM_FRESHNESS_CHECK); + } + + /* + * We calculate the new total supply and redeemer balance, checking for underflow: + * totalSupplyNew = totalSupply - redeemTokens + * accountTokensNew = accountTokens[redeemer] - redeemTokens + */ + (vars.mathErr, vars.totalSupplyNew) = subUInt(totalSupply, vars.redeemTokens); + if (vars.mathErr != MathError.NO_ERROR) { + return failOpaque(Error.MATH_ERROR, FailureInfo.REDEEM_NEW_TOTAL_SUPPLY_CALCULATION_FAILED, uint(vars.mathErr)); + } + + (vars.mathErr, vars.accountTokensNew) = subUInt(accountTokens[redeemer], vars.redeemTokens); + if (vars.mathErr != MathError.NO_ERROR) { + return failOpaque(Error.MATH_ERROR, FailureInfo.REDEEM_NEW_ACCOUNT_BALANCE_CALCULATION_FAILED, uint(vars.mathErr)); + } + + /* Fail gracefully if protocol has insufficient cash */ + if (getCashPrior() < vars.redeemAmount) { + return fail(Error.TOKEN_INSUFFICIENT_CASH, FailureInfo.REDEEM_TRANSFER_OUT_NOT_POSSIBLE); + } + + ///////////////////////// + // EFFECTS & INTERACTIONS + // (No safe failures beyond this point) + + /* + * We invoke doTransferOut for the redeemer and the redeemAmount. + * Note: The cToken must handle variations between ERC-20 and ETH underlying. + * On success, the cToken has redeemAmount less of cash. + * doTransferOut reverts if anything goes wrong, since we can't be sure if side effects occurred. + */ + doTransferOut(redeemer, vars.redeemAmount); + + /* We write previously calculated values into storage */ + totalSupply = vars.totalSupplyNew; + accountTokens[redeemer] = vars.accountTokensNew; + + /* We emit a Transfer event, and a Redeem event */ + emit Transfer(redeemer, address(this), vars.redeemTokens); + emit Redeem(redeemer, vars.redeemAmount, vars.redeemTokens); + + /* We call the defense hook */ + comptroller.redeemVerify(address(this), redeemer, vars.redeemAmount, vars.redeemTokens); + + return uint(Error.NO_ERROR); + } + + /** + * @notice Sender borrows assets from the protocol to their own address + * @param borrowAmount The amount of the underlying asset to borrow + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function borrowInternal(uint borrowAmount) internal nonReentrant returns (uint) { + uint error = accrueInterest(); + if (error != uint(Error.NO_ERROR)) { + // accrueInterest emits logs on errors, but we still want to log the fact that an attempted borrow failed + return fail(Error(error), FailureInfo.BORROW_ACCRUE_INTEREST_FAILED); + } + // borrowFresh emits borrow-specific logs on errors, so we don't need to + return borrowFresh(msg.sender, borrowAmount); + } + + struct BorrowLocalVars { + MathError mathErr; + uint accountBorrows; + uint accountBorrowsNew; + uint totalBorrowsNew; + } + + /** + * @notice Users borrow assets from the protocol to their own address + * @param borrowAmount The amount of the underlying asset to borrow + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function borrowFresh(address payable borrower, uint borrowAmount) internal returns (uint) { + /* Fail if borrow not allowed */ + uint allowed = comptroller.borrowAllowed(address(this), borrower, borrowAmount); + if (allowed != 0) { + return failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.BORROW_COMPTROLLER_REJECTION, allowed); + } + + /* Verify market's block number equals current block number */ + if (accrualBlockNumber != getBlockNumber()) { + return fail(Error.MARKET_NOT_FRESH, FailureInfo.BORROW_FRESHNESS_CHECK); + } + + /* Fail gracefully if protocol has insufficient underlying cash */ + if (getCashPrior() < borrowAmount) { + return fail(Error.TOKEN_INSUFFICIENT_CASH, FailureInfo.BORROW_CASH_NOT_AVAILABLE); + } + + BorrowLocalVars memory vars; + + /* + * We calculate the new borrower and total borrow balances, failing on overflow: + * accountBorrowsNew = accountBorrows + borrowAmount + * totalBorrowsNew = totalBorrows + borrowAmount + */ + (vars.mathErr, vars.accountBorrows) = borrowBalanceStoredInternal(borrower); + if (vars.mathErr != MathError.NO_ERROR) { + return failOpaque(Error.MATH_ERROR, FailureInfo.BORROW_ACCUMULATED_BALANCE_CALCULATION_FAILED, uint(vars.mathErr)); + } + + (vars.mathErr, vars.accountBorrowsNew) = addUInt(vars.accountBorrows, borrowAmount); + if (vars.mathErr != MathError.NO_ERROR) { + return failOpaque(Error.MATH_ERROR, FailureInfo.BORROW_NEW_ACCOUNT_BORROW_BALANCE_CALCULATION_FAILED, uint(vars.mathErr)); + } + + (vars.mathErr, vars.totalBorrowsNew) = addUInt(totalBorrows, borrowAmount); + if (vars.mathErr != MathError.NO_ERROR) { + return failOpaque(Error.MATH_ERROR, FailureInfo.BORROW_NEW_TOTAL_BALANCE_CALCULATION_FAILED, uint(vars.mathErr)); + } + + ///////////////////////// + // EFFECTS & INTERACTIONS + // (No safe failures beyond this point) + + /* + * We invoke doTransferOut for the borrower and the borrowAmount. + * Note: The cToken must handle variations between ERC-20 and ETH underlying. + * On success, the cToken borrowAmount less of cash. + * doTransferOut reverts if anything goes wrong, since we can't be sure if side effects occurred. + */ + doTransferOut(borrower, borrowAmount); + + /* We write the previously calculated values into storage */ + accountBorrows[borrower].principal = vars.accountBorrowsNew; + accountBorrows[borrower].interestIndex = borrowIndex; + totalBorrows = vars.totalBorrowsNew; + + /* We emit a Borrow event */ + emit Borrow(borrower, borrowAmount, vars.accountBorrowsNew, vars.totalBorrowsNew); + + /* We call the defense hook */ + // unused function + // comptroller.borrowVerify(address(this), borrower, borrowAmount); + + return uint(Error.NO_ERROR); + } + + /** + * @notice Sender repays their own borrow + * @param repayAmount The amount to repay + * @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual repayment amount. + */ + function repayBorrowInternal(uint repayAmount) internal nonReentrant returns (uint, uint) { + uint error = accrueInterest(); + if (error != uint(Error.NO_ERROR)) { + // accrueInterest emits logs on errors, but we still want to log the fact that an attempted borrow failed + return (fail(Error(error), FailureInfo.REPAY_BORROW_ACCRUE_INTEREST_FAILED), 0); + } + // repayBorrowFresh emits repay-borrow-specific logs on errors, so we don't need to + return repayBorrowFresh(msg.sender, msg.sender, repayAmount); + } + + /** + * @notice Sender repays a borrow belonging to borrower + * @param borrower the account with the debt being payed off + * @param repayAmount The amount to repay + * @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual repayment amount. + */ + function repayBorrowBehalfInternal(address borrower, uint repayAmount) internal nonReentrant returns (uint, uint) { + uint error = accrueInterest(); + if (error != uint(Error.NO_ERROR)) { + // accrueInterest emits logs on errors, but we still want to log the fact that an attempted borrow failed + return (fail(Error(error), FailureInfo.REPAY_BEHALF_ACCRUE_INTEREST_FAILED), 0); + } + // repayBorrowFresh emits repay-borrow-specific logs on errors, so we don't need to + return repayBorrowFresh(msg.sender, borrower, repayAmount); + } + + struct RepayBorrowLocalVars { + Error err; + MathError mathErr; + uint repayAmount; + uint borrowerIndex; + uint accountBorrows; + uint accountBorrowsNew; + uint totalBorrowsNew; + uint actualRepayAmount; + } + + /** + * @notice Borrows are repaid by another user (possibly the borrower). + * @param payer the account paying off the borrow + * @param borrower the account with the debt being payed off + * @param repayAmount the amount of undelrying tokens being returned + * @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual repayment amount. + */ + function repayBorrowFresh(address payer, address borrower, uint repayAmount) internal returns (uint, uint) { + /* Fail if repayBorrow not allowed */ + uint allowed = comptroller.repayBorrowAllowed(address(this), payer, borrower, repayAmount); + if (allowed != 0) { + return (failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.REPAY_BORROW_COMPTROLLER_REJECTION, allowed), 0); + } + + /* Verify market's block number equals current block number */ + if (accrualBlockNumber != getBlockNumber()) { + return (fail(Error.MARKET_NOT_FRESH, FailureInfo.REPAY_BORROW_FRESHNESS_CHECK), 0); + } + + RepayBorrowLocalVars memory vars; + + /* We remember the original borrowerIndex for verification purposes */ + vars.borrowerIndex = accountBorrows[borrower].interestIndex; + + /* We fetch the amount the borrower owes, with accumulated interest */ + (vars.mathErr, vars.accountBorrows) = borrowBalanceStoredInternal(borrower); + if (vars.mathErr != MathError.NO_ERROR) { + return (failOpaque(Error.MATH_ERROR, FailureInfo.REPAY_BORROW_ACCUMULATED_BALANCE_CALCULATION_FAILED, uint(vars.mathErr)), 0); + } + + /* If repayAmount == -1, repayAmount = accountBorrows */ + if (repayAmount == uint(-1)) { + vars.repayAmount = vars.accountBorrows; + } else { + vars.repayAmount = repayAmount; + } + + ///////////////////////// + // EFFECTS & INTERACTIONS + // (No safe failures beyond this point) + + /* + * We call doTransferIn for the payer and the repayAmount + * Note: The cToken must handle variations between ERC-20 and ETH underlying. + * On success, the cToken holds an additional repayAmount of cash. + * doTransferIn reverts if anything goes wrong, since we can't be sure if side effects occurred. + * it returns the amount actually transferred, in case of a fee. + */ + vars.actualRepayAmount = doTransferIn(payer, vars.repayAmount); + + /* + * We calculate the new borrower and total borrow balances, failing on underflow: + * accountBorrowsNew = accountBorrows - actualRepayAmount + * totalBorrowsNew = totalBorrows - actualRepayAmount + */ + (vars.mathErr, vars.accountBorrowsNew) = subUInt(vars.accountBorrows, vars.actualRepayAmount); + require(vars.mathErr == MathError.NO_ERROR, "REPAY_BORROW_NEW_ACCOUNT_BORROW_BALANCE_CALCULATION_FAILED"); + + (vars.mathErr, vars.totalBorrowsNew) = subUInt(totalBorrows, vars.actualRepayAmount); + require(vars.mathErr == MathError.NO_ERROR, "REPAY_BORROW_NEW_TOTAL_BALANCE_CALCULATION_FAILED"); + + /* We write the previously calculated values into storage */ + accountBorrows[borrower].principal = vars.accountBorrowsNew; + accountBorrows[borrower].interestIndex = borrowIndex; + totalBorrows = vars.totalBorrowsNew; + + /* We emit a RepayBorrow event */ + emit RepayBorrow(payer, borrower, vars.actualRepayAmount, vars.accountBorrowsNew, vars.totalBorrowsNew); + + /* We call the defense hook */ + // unused function + // comptroller.repayBorrowVerify(address(this), payer, borrower, vars.actualRepayAmount, vars.borrowerIndex); + + return (uint(Error.NO_ERROR), vars.actualRepayAmount); + } + + /** + * @notice The sender liquidates the borrowers collateral. + * The collateral seized is transferred to the liquidator. + * @param borrower The borrower of this cToken to be liquidated + * @param cTokenCollateral The market in which to seize collateral from the borrower + * @param repayAmount The amount of the underlying borrowed asset to repay + * @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual repayment amount. + */ + function liquidateBorrowInternal(address borrower, uint repayAmount, CTokenInterface cTokenCollateral) internal nonReentrant returns (uint, uint) { + uint error = accrueInterest(); + if (error != uint(Error.NO_ERROR)) { + // accrueInterest emits logs on errors, but we still want to log the fact that an attempted liquidation failed + return (fail(Error(error), FailureInfo.LIQUIDATE_ACCRUE_BORROW_INTEREST_FAILED), 0); + } + + error = cTokenCollateral.accrueInterest(); + if (error != uint(Error.NO_ERROR)) { + // accrueInterest emits logs on errors, but we still want to log the fact that an attempted liquidation failed + return (fail(Error(error), FailureInfo.LIQUIDATE_ACCRUE_COLLATERAL_INTEREST_FAILED), 0); + } + + // liquidateBorrowFresh emits borrow-specific logs on errors, so we don't need to + return liquidateBorrowFresh(msg.sender, borrower, repayAmount, cTokenCollateral); + } + + /** + * @notice The liquidator liquidates the borrowers collateral. + * The collateral seized is transferred to the liquidator. + * @param borrower The borrower of this cToken to be liquidated + * @param liquidator The address repaying the borrow and seizing collateral + * @param cTokenCollateral The market in which to seize collateral from the borrower + * @param repayAmount The amount of the underlying borrowed asset to repay + * @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual repayment amount. + */ + function liquidateBorrowFresh(address liquidator, address borrower, uint repayAmount, CTokenInterface cTokenCollateral) internal returns (uint, uint) { + /* Fail if liquidate not allowed */ + uint allowed = comptroller.liquidateBorrowAllowed(address(this), address(cTokenCollateral), liquidator, borrower, repayAmount); + if (allowed != 0) { + return (failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.LIQUIDATE_COMPTROLLER_REJECTION, allowed), 0); + } + + /* Verify market's block number equals current block number */ + if (accrualBlockNumber != getBlockNumber()) { + return (fail(Error.MARKET_NOT_FRESH, FailureInfo.LIQUIDATE_FRESHNESS_CHECK), 0); + } + + /* Verify cTokenCollateral market's block number equals current block number */ + if (cTokenCollateral.accrualBlockNumber() != getBlockNumber()) { + return (fail(Error.MARKET_NOT_FRESH, FailureInfo.LIQUIDATE_COLLATERAL_FRESHNESS_CHECK), 0); + } + + /* Fail if borrower = liquidator */ + if (borrower == liquidator) { + return (fail(Error.INVALID_ACCOUNT_PAIR, FailureInfo.LIQUIDATE_LIQUIDATOR_IS_BORROWER), 0); + } + + /* Fail if repayAmount = 0 */ + if (repayAmount == 0) { + return (fail(Error.INVALID_CLOSE_AMOUNT_REQUESTED, FailureInfo.LIQUIDATE_CLOSE_AMOUNT_IS_ZERO), 0); + } + + /* Fail if repayAmount = -1 */ + if (repayAmount == uint(-1)) { + return (fail(Error.INVALID_CLOSE_AMOUNT_REQUESTED, FailureInfo.LIQUIDATE_CLOSE_AMOUNT_IS_UINT_MAX), 0); + } + + + /* Fail if repayBorrow fails */ + (uint repayBorrowError, uint actualRepayAmount) = repayBorrowFresh(liquidator, borrower, repayAmount); + if (repayBorrowError != uint(Error.NO_ERROR)) { + return (fail(Error(repayBorrowError), FailureInfo.LIQUIDATE_REPAY_BORROW_FRESH_FAILED), 0); + } + + ///////////////////////// + // EFFECTS & INTERACTIONS + // (No safe failures beyond this point) + + /* We calculate the number of collateral tokens that will be seized */ + (uint amountSeizeError, uint seizeTokens) = comptroller.liquidateCalculateSeizeTokens(address(this), address(cTokenCollateral), actualRepayAmount); + require(amountSeizeError == uint(Error.NO_ERROR), "LIQUIDATE_COMPTROLLER_CALCULATE_AMOUNT_SEIZE_FAILED"); + + /* Revert if borrower collateral token balance < seizeTokens */ + require(cTokenCollateral.balanceOf(borrower) >= seizeTokens, "LIQUIDATE_SEIZE_TOO_MUCH"); + + // If this is also the collateral, run seizeInternal to avoid re-entrancy, otherwise make an external call + uint seizeError; + if (address(cTokenCollateral) == address(this)) { + seizeError = seizeInternal(address(this), liquidator, borrower, seizeTokens); + } else { + seizeError = cTokenCollateral.seize(liquidator, borrower, seizeTokens); + } + + /* Revert if seize tokens fails (since we cannot be sure of side effects) */ + require(seizeError == uint(Error.NO_ERROR), "token seizure failed"); + + /* We emit a LiquidateBorrow event */ + emit LiquidateBorrow(liquidator, borrower, actualRepayAmount, address(cTokenCollateral), seizeTokens); + + /* We call the defense hook */ + // unused function + // comptroller.liquidateBorrowVerify(address(this), address(cTokenCollateral), liquidator, borrower, actualRepayAmount, seizeTokens); + + return (uint(Error.NO_ERROR), actualRepayAmount); + } + + /** + * @notice Transfers collateral tokens (this market) to the liquidator. + * @dev Will fail unless called by another cToken during the process of liquidation. + * Its absolutely critical to use msg.sender as the borrowed cToken and not a parameter. + * @param liquidator The account receiving seized collateral + * @param borrower The account having collateral seized + * @param seizeTokens The number of cTokens to seize + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function seize(address liquidator, address borrower, uint seizeTokens) override external nonReentrant returns (uint) { + return seizeInternal(msg.sender, liquidator, borrower, seizeTokens); + } + + /** + * @notice Transfers collateral tokens (this market) to the liquidator. + * @dev Called only during an in-kind liquidation, or by liquidateBorrow during the liquidation of another CToken. + * Its absolutely critical to use msg.sender as the seizer cToken and not a parameter. + * @param seizerToken The contract seizing the collateral (i.e. borrowed cToken) + * @param liquidator The account receiving seized collateral + * @param borrower The account having collateral seized + * @param seizeTokens The number of cTokens to seize + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function seizeInternal(address seizerToken, address liquidator, address borrower, uint seizeTokens) internal returns (uint) { + /* Fail if seize not allowed */ + uint allowed = comptroller.seizeAllowed(address(this), seizerToken, liquidator, borrower, seizeTokens); + if (allowed != 0) { + return failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.LIQUIDATE_SEIZE_COMPTROLLER_REJECTION, allowed); + } + + /* Fail if borrower = liquidator */ + if (borrower == liquidator) { + return fail(Error.INVALID_ACCOUNT_PAIR, FailureInfo.LIQUIDATE_SEIZE_LIQUIDATOR_IS_BORROWER); + } + + MathError mathErr; + uint borrowerTokensNew; + uint liquidatorTokensNew; + + /* + * We calculate the new borrower and liquidator token balances, failing on underflow/overflow: + * borrowerTokensNew = accountTokens[borrower] - seizeTokens + * liquidatorTokensNew = accountTokens[liquidator] + seizeTokens + */ + (mathErr, borrowerTokensNew) = subUInt(accountTokens[borrower], seizeTokens); + if (mathErr != MathError.NO_ERROR) { + return failOpaque(Error.MATH_ERROR, FailureInfo.LIQUIDATE_SEIZE_BALANCE_DECREMENT_FAILED, uint(mathErr)); + } + + (mathErr, liquidatorTokensNew) = addUInt(accountTokens[liquidator], seizeTokens); + if (mathErr != MathError.NO_ERROR) { + return failOpaque(Error.MATH_ERROR, FailureInfo.LIQUIDATE_SEIZE_BALANCE_INCREMENT_FAILED, uint(mathErr)); + } + + ///////////////////////// + // EFFECTS & INTERACTIONS + // (No safe failures beyond this point) + + /* We write the previously calculated values into storage */ + accountTokens[borrower] = borrowerTokensNew; + accountTokens[liquidator] = liquidatorTokensNew; + + /* Emit a Transfer event */ + emit Transfer(borrower, liquidator, seizeTokens); + + /* We call the defense hook */ + // unused function + // comptroller.seizeVerify(address(this), seizerToken, liquidator, borrower, seizeTokens); + + return uint(Error.NO_ERROR); + } + + + /*** Admin Functions ***/ + + /** + * @notice Begins transfer of admin rights. The newPendingAdmin must call `_acceptAdmin` to finalize the transfer. + * @dev Admin function to begin change of admin. The newPendingAdmin must call `_acceptAdmin` to finalize the transfer. + * @param newPendingAdmin New pending admin. + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function _setPendingAdmin(address payable newPendingAdmin) override external returns (uint) { + // Check caller = admin + /* if (msg.sender != admin) { + return fail(Error.UNAUTHORIZED, FailureInfo.SET_PENDING_ADMIN_OWNER_CHECK); + } */ + + // Save current value, if any, for inclusion in log + address oldPendingAdmin = pendingAdmin; + + // Store pendingAdmin with value newPendingAdmin + pendingAdmin = newPendingAdmin; + + // Emit NewPendingAdmin(oldPendingAdmin, newPendingAdmin) + emit NewPendingAdmin(oldPendingAdmin, newPendingAdmin); + + return uint(Error.NO_ERROR); + } + + /** + * @notice Accepts transfer of admin rights. msg.sender must be pendingAdmin + * @dev Admin function for pending admin to accept role and update admin + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function _acceptAdmin() override external returns (uint) { + // Check caller is pendingAdmin and pendingAdmin ≠ address(0) + /* if (msg.sender != pendingAdmin || msg.sender == address(0)) { + return fail(Error.UNAUTHORIZED, FailureInfo.ACCEPT_ADMIN_PENDING_ADMIN_CHECK); + } */ + + // Save current values for inclusion in log + address oldAdmin = admin; + address oldPendingAdmin = pendingAdmin; + + // Store admin with value pendingAdmin + admin = pendingAdmin; + + // Clear the pending value + pendingAdmin = address(0); + + emit NewAdmin(oldAdmin, admin); + emit NewPendingAdmin(oldPendingAdmin, pendingAdmin); + + return uint(Error.NO_ERROR); + } + + /** + * @notice Sets a new comptroller for the market + * @dev Admin function to set a new comptroller + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function _setComptroller(ComptrollerInterface newComptroller) override public returns (uint) { + // Check caller is admin + /* if (msg.sender != admin) { + return fail(Error.UNAUTHORIZED, FailureInfo.SET_COMPTROLLER_OWNER_CHECK); + } */ + + ComptrollerInterface oldComptroller = comptroller; + // Ensure invoke comptroller.isComptroller() returns true + require(newComptroller.isComptroller(), "marker method returned false"); + + // Set market's comptroller to newComptroller + comptroller = newComptroller; + + // Emit NewComptroller(oldComptroller, newComptroller) + emit NewComptroller(oldComptroller, newComptroller); + + return uint(Error.NO_ERROR); + } + + /** + * @notice accrues interest and sets a new reserve factor for the protocol using _setReserveFactorFresh + * @dev Admin function to accrue interest and set a new reserve factor + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function _setReserveFactor(uint newReserveFactorMantissa) override external nonReentrant returns (uint) { + uint error = accrueInterest(); + if (error != uint(Error.NO_ERROR)) { + // accrueInterest emits logs on errors, but on top of that we want to log the fact that an attempted reserve factor change failed. + return fail(Error(error), FailureInfo.SET_RESERVE_FACTOR_ACCRUE_INTEREST_FAILED); + } + // _setReserveFactorFresh emits reserve-factor-specific logs on errors, so we don't need to. + return _setReserveFactorFresh(newReserveFactorMantissa); + } + + /** + * @notice Sets a new reserve factor for the protocol (*requires fresh interest accrual) + * @dev Admin function to set a new reserve factor + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function _setReserveFactorFresh(uint newReserveFactorMantissa) internal returns (uint) { + // Check caller is admin + /* if (msg.sender != admin) { + return fail(Error.UNAUTHORIZED, FailureInfo.SET_RESERVE_FACTOR_ADMIN_CHECK); + } */ + + // Verify market's block number equals current block number + if (accrualBlockNumber != getBlockNumber()) { + return fail(Error.MARKET_NOT_FRESH, FailureInfo.SET_RESERVE_FACTOR_FRESH_CHECK); + } + + // Check newReserveFactor ≤ maxReserveFactor + if (newReserveFactorMantissa > reserveFactorMaxMantissa) { + return fail(Error.BAD_INPUT, FailureInfo.SET_RESERVE_FACTOR_BOUNDS_CHECK); + } + + uint oldReserveFactorMantissa = reserveFactorMantissa; + reserveFactorMantissa = newReserveFactorMantissa; + + emit NewReserveFactor(oldReserveFactorMantissa, newReserveFactorMantissa); + + return uint(Error.NO_ERROR); + } + + /** + * @notice Accrues interest and reduces reserves by transferring from msg.sender + * @param addAmount Amount of addition to reserves + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function _addReservesInternal(uint addAmount) internal nonReentrant returns (uint) { + uint error = accrueInterest(); + if (error != uint(Error.NO_ERROR)) { + // accrueInterest emits logs on errors, but on top of that we want to log the fact that an attempted reduce reserves failed. + return fail(Error(error), FailureInfo.ADD_RESERVES_ACCRUE_INTEREST_FAILED); + } + + // _addReservesFresh emits reserve-addition-specific logs on errors, so we don't need to. + (error, ) = _addReservesFresh(addAmount); + return error; + } + + /** + * @notice Add reserves by transferring from caller + * @dev Requires fresh interest accrual + * @param addAmount Amount of addition to reserves + * @return (uint, uint) An error code (0=success, otherwise a failure (see ErrorReporter.sol for details)) and the actual amount added, net token fees + */ + function _addReservesFresh(uint addAmount) internal returns (uint, uint) { + // totalReserves + actualAddAmount + uint totalReservesNew; + uint actualAddAmount; + + // We fail gracefully unless market's block number equals current block number + if (accrualBlockNumber != getBlockNumber()) { + return (fail(Error.MARKET_NOT_FRESH, FailureInfo.ADD_RESERVES_FRESH_CHECK), actualAddAmount); + } + + ///////////////////////// + // EFFECTS & INTERACTIONS + // (No safe failures beyond this point) + + /* + * We call doTransferIn for the caller and the addAmount + * Note: The cToken must handle variations between ERC-20 and ETH underlying. + * On success, the cToken holds an additional addAmount of cash. + * doTransferIn reverts if anything goes wrong, since we can't be sure if side effects occurred. + * it returns the amount actually transferred, in case of a fee. + */ + + actualAddAmount = doTransferIn(msg.sender, addAmount); + + totalReservesNew = totalReserves + actualAddAmount; + + /* Revert on overflow */ + require(totalReservesNew >= totalReserves, "add reserves unexpected overflow"); + + // Store reserves[n+1] = reserves[n] + actualAddAmount + totalReserves = totalReservesNew; + + /* Emit NewReserves(admin, actualAddAmount, reserves[n+1]) */ + emit ReservesAdded(msg.sender, actualAddAmount, totalReservesNew); + + /* Return (NO_ERROR, actualAddAmount) */ + return (uint(Error.NO_ERROR), actualAddAmount); + } + + + /** + * @notice Accrues interest and reduces reserves by transferring to admin + * @param reduceAmount Amount of reduction to reserves + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function _reduceReserves(uint reduceAmount) override external nonReentrant returns (uint) { + uint error = accrueInterest(); + if (error != uint(Error.NO_ERROR)) { + // accrueInterest emits logs on errors, but on top of that we want to log the fact that an attempted reduce reserves failed. + return fail(Error(error), FailureInfo.REDUCE_RESERVES_ACCRUE_INTEREST_FAILED); + } + // _reduceReservesFresh emits reserve-reduction-specific logs on errors, so we don't need to. + return _reduceReservesFresh(reduceAmount); + } + + /** + * @notice Reduces reserves by transferring to admin + * @dev Requires fresh interest accrual + * @param reduceAmount Amount of reduction to reserves + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function _reduceReservesFresh(uint reduceAmount) internal returns (uint) { + // totalReserves - reduceAmount + uint totalReservesNew; + + // Check caller is admin + /* if (msg.sender != admin) { + return fail(Error.UNAUTHORIZED, FailureInfo.REDUCE_RESERVES_ADMIN_CHECK); + } */ + + // We fail gracefully unless market's block number equals current block number + if (accrualBlockNumber != getBlockNumber()) { + return fail(Error.MARKET_NOT_FRESH, FailureInfo.REDUCE_RESERVES_FRESH_CHECK); + } + + // Fail gracefully if protocol has insufficient underlying cash + if (getCashPrior() < reduceAmount) { + return fail(Error.TOKEN_INSUFFICIENT_CASH, FailureInfo.REDUCE_RESERVES_CASH_NOT_AVAILABLE); + } + + // Check reduceAmount ≤ reserves[n] (totalReserves) + if (reduceAmount > totalReserves) { + return fail(Error.BAD_INPUT, FailureInfo.REDUCE_RESERVES_VALIDATION); + } + + ///////////////////////// + // EFFECTS & INTERACTIONS + // (No safe failures beyond this point) + + totalReservesNew = totalReserves - reduceAmount; + // We checked reduceAmount <= totalReserves above, so this should never revert. + require(totalReservesNew <= totalReserves, "reduce reserves unexpected underflow"); + + // Store reserves[n+1] = reserves[n] - reduceAmount + totalReserves = totalReservesNew; + + // doTransferOut reverts if anything goes wrong, since we can't be sure if side effects occurred. + doTransferOut(admin, reduceAmount); + + emit ReservesReduced(admin, reduceAmount, totalReservesNew); + + return uint(Error.NO_ERROR); + } + + /** + * @notice accrues interest and updates the interest rate model using _setInterestRateModelFresh + * @dev Admin function to accrue interest and update the interest rate model + * @param newInterestRateModel the new interest rate model to use + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function _setInterestRateModel(InterestRateModel newInterestRateModel) override public returns (uint) { + uint error = accrueInterest(); + if (error != uint(Error.NO_ERROR)) { + // accrueInterest emits logs on errors, but on top of that we want to log the fact that an attempted change of interest rate model failed + return fail(Error(error), FailureInfo.SET_INTEREST_RATE_MODEL_ACCRUE_INTEREST_FAILED); + } + // _setInterestRateModelFresh emits interest-rate-model-update-specific logs on errors, so we don't need to. + return _setInterestRateModelFresh(newInterestRateModel); + } + + /** + * @notice updates the interest rate model (*requires fresh interest accrual) + * @dev Admin function to update the interest rate model + * @param newInterestRateModel the new interest rate model to use + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function _setInterestRateModelFresh(InterestRateModel newInterestRateModel) internal returns (uint) { + + // Used to store old model for use in the event that is emitted on success + InterestRateModel oldInterestRateModel; + + // Check caller is admin + /* if (msg.sender != admin) { + return fail(Error.UNAUTHORIZED, FailureInfo.SET_INTEREST_RATE_MODEL_OWNER_CHECK); + } */ + + // We fail gracefully unless market's block number equals current block number + if (accrualBlockNumber != getBlockNumber()) { + return fail(Error.MARKET_NOT_FRESH, FailureInfo.SET_INTEREST_RATE_MODEL_FRESH_CHECK); + } + + // Track the market's current interest rate model + oldInterestRateModel = interestRateModel; + + // Ensure invoke newInterestRateModel.isInterestRateModel() returns true + require(newInterestRateModel.isInterestRateModel(), "marker method returned false"); + + // Set the interest rate model to newInterestRateModel + interestRateModel = newInterestRateModel; + + // Emit NewMarketInterestRateModel(oldInterestRateModel, newInterestRateModel) + emit NewMarketInterestRateModel(oldInterestRateModel, newInterestRateModel); + + return uint(Error.NO_ERROR); + } + + /*** Safe Token ***/ + + /** + * @notice Gets balance of this contract in terms of the underlying + * @dev This excludes the value of the current message, if any + * @return The quantity of underlying owned by this contract + */ + function getCashPrior() virtual internal view returns (uint) {} + + /** + * @dev Performs a transfer in, reverting upon failure. Returns the amount actually transferred to the protocol, in case of a fee. + * This may revert due to insufficient balance or insufficient allowance. + */ + function doTransferIn(address from, uint amount) virtual internal returns (uint) {} + + /** + * @dev Performs a transfer out, ideally returning an explanatory error code upon failure tather than reverting. + * If caller has not called checked protocol's balance, may revert due to insufficient cash held in the contract. + * If caller has checked protocol's balance, and verified it is >= amount, this should not revert in normal conditions. + */ + function doTransferOut(address payable to, uint amount) virtual internal {} + + + /*** Reentrancy Guard ***/ + + /** + * @dev Prevents a contract from calling itself, directly or indirectly. + */ + modifier nonReentrant() { + require(_notEntered, "re-entered"); + _notEntered = false; + _; + _notEntered = true; // get a gas-refund post-Istanbul + } +} diff --git a/src/integrations/compound/CTokenInterfaces.sol b/src/integrations/compound/CTokenInterfaces.sol index 431f5ad..3487930 100644 --- a/src/integrations/compound/CTokenInterfaces.sol +++ b/src/integrations/compound/CTokenInterfaces.sol @@ -1,304 +1,304 @@ -pragma solidity ^0.6.7; - -import "./ComptrollerInterface.sol"; -import "./InterestRateModel.sol"; -import "./EIP20NonStandardInterface.sol"; - -contract CTokenStorage { - /** - * @dev Guard variable for re-entrancy checks - */ - bool internal _notEntered; - - /** - * @notice EIP-20 token name for this token - */ - string public name; - - /** - * @notice EIP-20 token symbol for this token - */ - string public symbol; - - /** - * @notice EIP-20 token decimals for this token - */ - uint8 public decimals; - - /** - * @notice Maximum borrow rate that can ever be applied (.0005% / block) - */ - - uint internal constant borrowRateMaxMantissa = 0.0005e16; - - /** - * @notice Maximum fraction of interest that can be set aside for reserves - */ - uint internal constant reserveFactorMaxMantissa = 1e18; - - /** - * @notice Administrator for this contract - */ - address payable public admin; - - /** - * @notice Pending administrator for this contract - */ - address payable public pendingAdmin; - - /** - * @notice Contract which oversees inter-cToken operations - */ - ComptrollerInterface public comptroller; - - /** - * @notice Model which tells what the current interest rate should be - */ - InterestRateModel public interestRateModel; - - /** - * @notice Initial exchange rate used when minting the first CTokens (used when totalSupply = 0) - */ - uint internal initialExchangeRateMantissa; - - /** - * @notice Fraction of interest currently set aside for reserves - */ - uint public reserveFactorMantissa; - - /** - * @notice Block number that interest was last accrued at - */ - uint public accrualBlockNumber; - - /** - * @notice Accumulator of the total earned interest rate since the opening of the market - */ - uint public borrowIndex; - - /** - * @notice Total amount of outstanding borrows of the underlying in this market - */ - uint public totalBorrows; - - /** - * @notice Total amount of reserves of the underlying held in this market - */ - uint public totalReserves; - - /** - * @notice Total number of tokens in circulation - */ - uint public totalSupply; - - /** - * @notice Official record of token balances for each account - */ - mapping (address => uint) internal accountTokens; - - /** - * @notice Approved token transfer amounts on behalf of others - */ - mapping (address => mapping (address => uint)) internal transferAllowances; - - /** - * @notice Container for borrow balance information - * @member principal Total balance (with accrued interest), after applying the most recent balance-changing action - * @member interestIndex Global borrowIndex as of the most recent balance-changing action - */ - struct BorrowSnapshot { - uint principal; - uint interestIndex; - } - - /** - * @notice Mapping of account addresses to outstanding borrow balances - */ - mapping(address => BorrowSnapshot) internal accountBorrows; -} - -contract CTokenInterface is CTokenStorage { - /** - * @notice Indicator that this is a CToken contract (for inspection) - */ - bool public constant isCToken = true; - - - /*** Market Events ***/ - - /** - * @notice Event emitted when interest is accrued - */ - event AccrueInterest(uint cashPrior, uint interestAccumulated, uint borrowIndex, uint totalBorrows); - - /** - * @notice Event emitted when tokens are minted - */ - event Mint(address minter, uint mintAmount, uint mintTokens); - - /** - * @notice Event emitted when tokens are redeemed - */ - event Redeem(address redeemer, uint redeemAmount, uint redeemTokens); - - /** - * @notice Event emitted when underlying is borrowed - */ - event Borrow(address borrower, uint borrowAmount, uint accountBorrows, uint totalBorrows); - - /** - * @notice Event emitted when a borrow is repaid - */ - event RepayBorrow(address payer, address borrower, uint repayAmount, uint accountBorrows, uint totalBorrows); - - /** - * @notice Event emitted when a borrow is liquidated - */ - event LiquidateBorrow(address liquidator, address borrower, uint repayAmount, address cTokenCollateral, uint seizeTokens); - - - /*** Admin Events ***/ - - /** - * @notice Event emitted when pendingAdmin is changed - */ - event NewPendingAdmin(address oldPendingAdmin, address newPendingAdmin); - - /** - * @notice Event emitted when pendingAdmin is accepted, which means admin is updated - */ - event NewAdmin(address oldAdmin, address newAdmin); - - /** - * @notice Event emitted when comptroller is changed - */ - event NewComptroller(ComptrollerInterface oldComptroller, ComptrollerInterface newComptroller); - - /** - * @notice Event emitted when interestRateModel is changed - */ - event NewMarketInterestRateModel(InterestRateModel oldInterestRateModel, InterestRateModel newInterestRateModel); - - /** - * @notice Event emitted when the reserve factor is changed - */ - event NewReserveFactor(uint oldReserveFactorMantissa, uint newReserveFactorMantissa); - - /** - * @notice Event emitted when the reserves are added - */ - event ReservesAdded(address benefactor, uint addAmount, uint newTotalReserves); - - /** - * @notice Event emitted when the reserves are reduced - */ - event ReservesReduced(address admin, uint reduceAmount, uint newTotalReserves); - - /** - * @notice EIP20 Transfer event - */ - event Transfer(address indexed from, address indexed to, uint amount); - - /** - * @notice EIP20 Approval event - */ - event Approval(address indexed owner, address indexed spender, uint amount); - - /** - * @notice Failure event - */ - event Failure(uint error, uint info, uint detail); - - - /*** User Interface ***/ - - function transfer(address dst, uint amount) virtual external returns (bool) {} - function transferFrom(address src, address dst, uint amount) virtual external returns (bool) {} - function approve(address spender, uint amount) virtual external returns (bool) {} - function allowance(address owner, address spender) virtual external view returns (uint) {} - function balanceOf(address owner) virtual external view returns (uint) {} - function balanceOfUnderlying(address owner) virtual external returns (uint) {} - function getAccountSnapshot(address account) virtual external view returns (uint, uint, uint, uint) {} - function borrowRatePerBlock() virtual external view returns (uint) {} - function supplyRatePerBlock() virtual external view returns (uint) {} - function totalBorrowsCurrent() virtual external returns (uint) {} - function borrowBalanceCurrent(address account) virtual external returns (uint) {} - function borrowBalanceStored(address account) virtual public view returns (uint) {} - function exchangeRateCurrent() virtual public returns (uint) {} - function exchangeRateStored() virtual public view returns (uint) {} - function getCash() virtual external view returns (uint) {} - function accrueInterest() virtual public returns (uint) {} - function seize(address liquidator, address borrower, uint seizeTokens) virtual external returns (uint) {} - - - /*** Admin Functions ***/ - - function _setPendingAdmin(address payable newPendingAdmin) virtual external returns (uint) {} - function _acceptAdmin() virtual external returns (uint) {} - function _setComptroller(ComptrollerInterface newComptroller) virtual public returns (uint) {} - function _setReserveFactor(uint newReserveFactorMantissa) virtual external returns (uint) {} - function _reduceReserves(uint reduceAmount) virtual external returns (uint) {} - function _setInterestRateModel(InterestRateModel newInterestRateModel) virtual public returns (uint) {} -} - -contract CErc20Storage { - /** - * @notice Underlying asset for this CToken - */ - address public underlying; -} - -abstract contract CErc20Interface is CErc20Storage { - - /*** User Interface ***/ - - function mint(uint mintAmount) virtual external returns (uint) {} - function redeem(uint redeemTokens) virtual external returns (uint) {} - function redeemUnderlying(uint redeemAmount) virtual external returns (uint) {} - function borrow(uint borrowAmount) virtual external returns (uint) {} - function repayBorrow(uint repayAmount) virtual external returns (uint) {} - function repayBorrowBehalf(address borrower, uint repayAmount) virtual external returns (uint) {} - function liquidateBorrow(address borrower, uint repayAmount, CTokenInterface cTokenCollateral) virtual external returns (uint) {} - function sweepToken(EIP20NonStandardInterface token) virtual external {} - - - /*** Admin Functions ***/ - - function _addReserves(uint addAmount) virtual external returns (uint); -} - -contract CDelegationStorage { - /** - * @notice Implementation address for this contract - */ - address public implementation; -} - -contract CDelegatorInterface is CDelegationStorage { - /** - * @notice Emitted when implementation is changed - */ - event NewImplementation(address oldImplementation, address newImplementation); - - /** - * @notice Called by the admin to update the implementation of the delegator - * @param implementation_ The address of the new implementation for delegation - * @param allowResign Flag to indicate whether to call _resignImplementation on the old implementation - * @param becomeImplementationData The encoded bytes data to be passed to _becomeImplementation - */ - function _setImplementation(address implementation_, bool allowResign, bytes memory becomeImplementationData) virtual public {} -} - -contract CDelegateInterface is CDelegationStorage { - /** - * @notice Called by the delegator on a delegate to initialize it for duty - * @dev Should revert if any issues arise which make it unfit for delegation - * @param data The encoded bytes data for any initialization - */ - function _becomeImplementation(bytes memory data) virtual public {} - - /**admin - * @notice Called by the delegator on a delegate to forfeit its responsibility - */ - function _resignImplementation() virtual public {} -} +pragma solidity ^0.6.7; + +import "./ComptrollerInterface.sol"; +import "./InterestRateModel.sol"; +import "./EIP20NonStandardInterface.sol"; + +contract CTokenStorage { + /** + * @dev Guard variable for re-entrancy checks + */ + bool internal _notEntered; + + /** + * @notice EIP-20 token name for this token + */ + string public name; + + /** + * @notice EIP-20 token symbol for this token + */ + string public symbol; + + /** + * @notice EIP-20 token decimals for this token + */ + uint8 public decimals; + + /** + * @notice Maximum borrow rate that can ever be applied (.0005% / block) + */ + + uint internal constant borrowRateMaxMantissa = 0.0005e16; + + /** + * @notice Maximum fraction of interest that can be set aside for reserves + */ + uint internal constant reserveFactorMaxMantissa = 1e18; + + /** + * @notice Administrator for this contract + */ + address payable public admin; + + /** + * @notice Pending administrator for this contract + */ + address payable public pendingAdmin; + + /** + * @notice Contract which oversees inter-cToken operations + */ + ComptrollerInterface public comptroller; + + /** + * @notice Model which tells what the current interest rate should be + */ + InterestRateModel public interestRateModel; + + /** + * @notice Initial exchange rate used when minting the first CTokens (used when totalSupply = 0) + */ + uint internal initialExchangeRateMantissa; + + /** + * @notice Fraction of interest currently set aside for reserves + */ + uint public reserveFactorMantissa; + + /** + * @notice Block number that interest was last accrued at + */ + uint public accrualBlockNumber; + + /** + * @notice Accumulator of the total earned interest rate since the opening of the market + */ + uint public borrowIndex; + + /** + * @notice Total amount of outstanding borrows of the underlying in this market + */ + uint public totalBorrows; + + /** + * @notice Total amount of reserves of the underlying held in this market + */ + uint public totalReserves; + + /** + * @notice Total number of tokens in circulation + */ + uint public totalSupply; + + /** + * @notice Official record of token balances for each account + */ + mapping (address => uint) internal accountTokens; + + /** + * @notice Approved token transfer amounts on behalf of others + */ + mapping (address => mapping (address => uint)) internal transferAllowances; + + /** + * @notice Container for borrow balance information + * @member principal Total balance (with accrued interest), after applying the most recent balance-changing action + * @member interestIndex Global borrowIndex as of the most recent balance-changing action + */ + struct BorrowSnapshot { + uint principal; + uint interestIndex; + } + + /** + * @notice Mapping of account addresses to outstanding borrow balances + */ + mapping(address => BorrowSnapshot) internal accountBorrows; +} + +contract CTokenInterface is CTokenStorage { + /** + * @notice Indicator that this is a CToken contract (for inspection) + */ + bool public constant isCToken = true; + + + /*** Market Events ***/ + + /** + * @notice Event emitted when interest is accrued + */ + event AccrueInterest(uint cashPrior, uint interestAccumulated, uint borrowIndex, uint totalBorrows); + + /** + * @notice Event emitted when tokens are minted + */ + event Mint(address minter, uint mintAmount, uint mintTokens); + + /** + * @notice Event emitted when tokens are redeemed + */ + event Redeem(address redeemer, uint redeemAmount, uint redeemTokens); + + /** + * @notice Event emitted when underlying is borrowed + */ + event Borrow(address borrower, uint borrowAmount, uint accountBorrows, uint totalBorrows); + + /** + * @notice Event emitted when a borrow is repaid + */ + event RepayBorrow(address payer, address borrower, uint repayAmount, uint accountBorrows, uint totalBorrows); + + /** + * @notice Event emitted when a borrow is liquidated + */ + event LiquidateBorrow(address liquidator, address borrower, uint repayAmount, address cTokenCollateral, uint seizeTokens); + + + /*** Admin Events ***/ + + /** + * @notice Event emitted when pendingAdmin is changed + */ + event NewPendingAdmin(address oldPendingAdmin, address newPendingAdmin); + + /** + * @notice Event emitted when pendingAdmin is accepted, which means admin is updated + */ + event NewAdmin(address oldAdmin, address newAdmin); + + /** + * @notice Event emitted when comptroller is changed + */ + event NewComptroller(ComptrollerInterface oldComptroller, ComptrollerInterface newComptroller); + + /** + * @notice Event emitted when interestRateModel is changed + */ + event NewMarketInterestRateModel(InterestRateModel oldInterestRateModel, InterestRateModel newInterestRateModel); + + /** + * @notice Event emitted when the reserve factor is changed + */ + event NewReserveFactor(uint oldReserveFactorMantissa, uint newReserveFactorMantissa); + + /** + * @notice Event emitted when the reserves are added + */ + event ReservesAdded(address benefactor, uint addAmount, uint newTotalReserves); + + /** + * @notice Event emitted when the reserves are reduced + */ + event ReservesReduced(address admin, uint reduceAmount, uint newTotalReserves); + + /** + * @notice EIP20 Transfer event + */ + event Transfer(address indexed from, address indexed to, uint amount); + + /** + * @notice EIP20 Approval event + */ + event Approval(address indexed owner, address indexed spender, uint amount); + + /** + * @notice Failure event + */ + event Failure(uint error, uint info, uint detail); + + + /*** User Interface ***/ + + function transfer(address dst, uint amount) virtual external returns (bool) {} + function transferFrom(address src, address dst, uint amount) virtual external returns (bool) {} + function approve(address spender, uint amount) virtual external returns (bool) {} + function allowance(address owner, address spender) virtual external view returns (uint) {} + function balanceOf(address owner) virtual external view returns (uint) {} + function balanceOfUnderlying(address owner) virtual external returns (uint) {} + function getAccountSnapshot(address account) virtual external view returns (uint, uint, uint, uint) {} + function borrowRatePerBlock() virtual external view returns (uint) {} + function supplyRatePerBlock() virtual external view returns (uint) {} + function totalBorrowsCurrent() virtual external returns (uint) {} + function borrowBalanceCurrent(address account) virtual external returns (uint) {} + function borrowBalanceStored(address account) virtual public view returns (uint) {} + function exchangeRateCurrent() virtual public returns (uint) {} + function exchangeRateStored() virtual public view returns (uint) {} + function getCash() virtual external view returns (uint) {} + function accrueInterest() virtual public returns (uint) {} + function seize(address liquidator, address borrower, uint seizeTokens) virtual external returns (uint) {} + + + /*** Admin Functions ***/ + + function _setPendingAdmin(address payable newPendingAdmin) virtual external returns (uint) {} + function _acceptAdmin() virtual external returns (uint) {} + function _setComptroller(ComptrollerInterface newComptroller) virtual public returns (uint) {} + function _setReserveFactor(uint newReserveFactorMantissa) virtual external returns (uint) {} + function _reduceReserves(uint reduceAmount) virtual external returns (uint) {} + function _setInterestRateModel(InterestRateModel newInterestRateModel) virtual public returns (uint) {} +} + +contract CErc20Storage { + /** + * @notice Underlying asset for this CToken + */ + address public underlying; +} + +abstract contract CErc20Interface is CErc20Storage { + + /*** User Interface ***/ + + function mint(uint mintAmount) virtual external returns (uint) {} + function redeem(uint redeemTokens) virtual external returns (uint) {} + function redeemUnderlying(uint redeemAmount) virtual external returns (uint) {} + function borrow(uint borrowAmount) virtual external returns (uint) {} + function repayBorrow(uint repayAmount) virtual external returns (uint) {} + function repayBorrowBehalf(address borrower, uint repayAmount) virtual external returns (uint) {} + function liquidateBorrow(address borrower, uint repayAmount, CTokenInterface cTokenCollateral) virtual external returns (uint) {} + function sweepToken(EIP20NonStandardInterface token) virtual external {} + + + /*** Admin Functions ***/ + + function _addReserves(uint addAmount) virtual external returns (uint); +} + +contract CDelegationStorage { + /** + * @notice Implementation address for this contract + */ + address public implementation; +} + +contract CDelegatorInterface is CDelegationStorage { + /** + * @notice Emitted when implementation is changed + */ + event NewImplementation(address oldImplementation, address newImplementation); + + /** + * @notice Called by the admin to update the implementation of the delegator + * @param implementation_ The address of the new implementation for delegation + * @param allowResign Flag to indicate whether to call _resignImplementation on the old implementation + * @param becomeImplementationData The encoded bytes data to be passed to _becomeImplementation + */ + function _setImplementation(address implementation_, bool allowResign, bytes memory becomeImplementationData) virtual public {} +} + +contract CDelegateInterface is CDelegationStorage { + /** + * @notice Called by the delegator on a delegate to initialize it for duty + * @dev Should revert if any issues arise which make it unfit for delegation + * @param data The encoded bytes data for any initialization + */ + function _becomeImplementation(bytes memory data) virtual public {} + + /**admin + * @notice Called by the delegator on a delegate to forfeit its responsibility + */ + function _resignImplementation() virtual public {} +} diff --git a/src/integrations/compound/ComptrollerG2.sol b/src/integrations/compound/ComptrollerG2.sol index e870388..144e3c2 100644 --- a/src/integrations/compound/ComptrollerG2.sol +++ b/src/integrations/compound/ComptrollerG2.sol @@ -1,1062 +1,1062 @@ -pragma solidity ^0.6.7; - -import "./CToken.sol"; -import "./ErrorReporter.sol"; -import "./Exponential.sol"; -import "./PriceOracle.sol"; -import "./ComptrollerInterface.sol"; -import "./ComptrollerStorage.sol"; -import "./Unitroller.sol"; - -/** - * @title Compound's Comptroller Contract - * @author Compound - */ -contract ComptrollerG2 is ComptrollerV2Storage, ComptrollerInterface, ComptrollerErrorReporter, Exponential { - /** - * @notice Emitted when an admin supports a market - */ - event MarketListed(CToken cToken); - - /** - * @notice Emitted when an account enters a market - */ - event MarketEntered(CToken cToken, address account); - - /** - * @notice Emitted when an account exits a market - */ - event MarketExited(CToken cToken, address account); - - /** - * @notice Emitted when close factor is changed by admin - */ - event NewCloseFactor(uint oldCloseFactorMantissa, uint newCloseFactorMantissa); - - /** - * @notice Emitted when a collateral factor is changed by admin - */ - event NewCollateralFactor(CToken cToken, uint oldCollateralFactorMantissa, uint newCollateralFactorMantissa); - - /** - * @notice Emitted when liquidation incentive is changed by admin - */ - event NewLiquidationIncentive(uint oldLiquidationIncentiveMantissa, uint newLiquidationIncentiveMantissa); - - /** - * @notice Emitted when maxAssets is changed by admin - */ - event NewMaxAssets(uint oldMaxAssets, uint newMaxAssets); - - /** - * @notice Emitted when price oracle is changed - */ - event NewPriceOracle(PriceOracle oldPriceOracle, PriceOracle newPriceOracle); - - /** - * @notice Emitted when pause guardian is changed - */ - event NewPauseGuardian(address oldPauseGuardian, address newPauseGuardian); - - /** - * @notice Emitted when an action is paused globally - */ - event ActionPaused(string action, bool pauseState); - - /** - * @notice Emitted when an action is paused on a market - */ - event ActionPaused(CToken cToken, string action, bool pauseState); - - // closeFactorMantissa must be strictly greater than this value - uint internal constant closeFactorMinMantissa = 0.05e18; // 0.05 - - // closeFactorMantissa must not exceed this value - uint internal constant closeFactorMaxMantissa = 0.9e18; // 0.9 - - // No collateralFactorMantissa may exceed this value - uint internal constant collateralFactorMaxMantissa = 0.9e18; // 0.9 - - // liquidationIncentiveMantissa must be no less than this value - uint internal constant liquidationIncentiveMinMantissa = 1.0e18; // 1.0 - - // liquidationIncentiveMantissa must be no greater than this value - uint internal constant liquidationIncentiveMaxMantissa = 1.5e18; // 1.5 - - constructor() public { - admin = msg.sender; - } - - /*** Assets You Are In ***/ - - /** - * @notice Returns the assets an account has entered - * @param account The address of the account to pull assets for - * @return A dynamic list with the assets the account has entered - */ - function getAssetsIn(address account) external view returns (CToken[] memory) { - CToken[] memory assetsIn = accountAssets[account]; - - return assetsIn; - } - - /** - * @notice Returns whether the given account is entered in the given asset - * @param account The address of the account to check - * @param cToken The cToken to check - * @return True if the account is in the asset, otherwise false. - */ - function checkMembership(address account, CToken cToken) external view returns (bool) { - return markets[address(cToken)].accountMembership[account]; - } - - /** - * @notice Add assets to be included in account liquidity calculation - * @param cTokens The list of addresses of the cToken markets to be enabled - * @return Success indicator for whether each corresponding market was entered - */ - function enterMarkets(address[] memory cTokens) override public returns (uint[] memory) { - uint len = cTokens.length; - - uint[] memory results = new uint[](len); - for (uint i = 0; i < len; i++) { - CToken cToken = CToken(cTokens[i]); - - results[i] = uint(addToMarketInternal(cToken, msg.sender)); - } - - return results; - } - - /** - * @notice Add the market to the borrower's "assets in" for liquidity calculations - * @param cToken The market to enter - * @param borrower The address of the account to modify - * @return Success indicator for whether the market was entered - */ - function addToMarketInternal(CToken cToken, address borrower) internal returns (Error) { - Market storage marketToJoin = markets[address(cToken)]; - - if (!marketToJoin.isListed) { - // market is not listed, cannot join - return Error.MARKET_NOT_LISTED; - } - - if (marketToJoin.accountMembership[borrower] == true) { - // already joined - return Error.NO_ERROR; - } - - if (accountAssets[borrower].length >= maxAssets) { - // no space, cannot join - return Error.TOO_MANY_ASSETS; - } - - // survived the gauntlet, add to list - // NOTE: we store these somewhat redundantly as a significant optimization - // this avoids having to iterate through the list for the most common use cases - // that is, only when we need to perform liquidity checks - // and not whenever we want to check if an account is in a particular market - marketToJoin.accountMembership[borrower] = true; - accountAssets[borrower].push(cToken); - - emit MarketEntered(cToken, borrower); - - return Error.NO_ERROR; - } - - /** - * @notice Removes asset from sender's account liquidity calculation - * @dev Sender must not have an outstanding borrow balance in the asset, - * or be providing neccessary collateral for an outstanding borrow. - * @param cTokenAddress The address of the asset to be removed - * @return Whether or not the account successfully exited the market - */ - function exitMarket(address cTokenAddress) override external returns (uint) { - CToken cToken = CToken(cTokenAddress); - /* Get sender tokensHeld and amountOwed underlying from the cToken */ - (uint oErr, uint tokensHeld, uint amountOwed, ) = cToken.getAccountSnapshot(msg.sender); - require(oErr == 0, "exitMarket: getAccountSnapshot failed"); // semi-opaque error code - - /* Fail if the sender has a borrow balance */ - if (amountOwed != 0) { - return fail(Error.NONZERO_BORROW_BALANCE, FailureInfo.EXIT_MARKET_BALANCE_OWED); - } - - /* Fail if the sender is not permitted to redeem all of their tokens */ - uint allowed = redeemAllowedInternal(cTokenAddress, msg.sender, tokensHeld); - if (allowed != 0) { - return failOpaque(Error.REJECTION, FailureInfo.EXIT_MARKET_REJECTION, allowed); - } - - Market storage marketToExit = markets[address(cToken)]; - - /* Return true if the sender is not already ‘in’ the market */ - if (!marketToExit.accountMembership[msg.sender]) { - return uint(Error.NO_ERROR); - } - - /* Set cToken account membership to false */ - delete marketToExit.accountMembership[msg.sender]; - - /* Delete cToken from the account’s list of assets */ - // load into memory for faster iteration - CToken[] memory userAssetList = accountAssets[msg.sender]; - uint len = userAssetList.length; - uint assetIndex = len; - for (uint i = 0; i < len; i++) { - if (userAssetList[i] == cToken) { - assetIndex = i; - break; - } - } - - // We *must* have found the asset in the list or our redundant data structure is broken - assert(assetIndex < len); - - // copy last item in list to location of item to be removed, reduce length by 1 - CToken[] storage storedList = accountAssets[msg.sender]; - storedList[assetIndex] = storedList[storedList.length - 1]; - storedList.pop(); - - emit MarketExited(cToken, msg.sender); - - return uint(Error.NO_ERROR); - } - - /*** Policy Hooks ***/ - - /** - * @notice Checks if the account should be allowed to mint tokens in the given market - * @param cToken The market to verify the mint against - * @param minter The account which would get the minted tokens - * @param mintAmount The amount of underlying being supplied to the market in exchange for tokens - * @return 0 if the mint is allowed, otherwise a semi-opaque error code (See ErrorReporter.sol) - */ - function mintAllowed(address cToken, address minter, uint mintAmount) override external returns (uint) { - // Pausing is a very serious situation - we revert to sound the alarms - require(!mintGuardianPaused[cToken], "mint is paused"); - - // Shh - currently unused - minter; - mintAmount; - - if (!markets[cToken].isListed) { - return uint(Error.MARKET_NOT_LISTED); - } - - // *may include Policy Hook-type checks - - return uint(Error.NO_ERROR); - } - - /** - * @notice Validates mint and reverts on rejection. May emit logs. - * @param cToken Asset being minted - * @param minter The address minting the tokens - * @param actualMintAmount The amount of the underlying asset being minted - * @param mintTokens The number of tokens being minted - */ - function mintVerify(address cToken, address minter, uint actualMintAmount, uint mintTokens) override external { - // Shh - currently unused - cToken; - minter; - actualMintAmount; - mintTokens; - - // Shh - we don't ever want this hook to be marked pure - if (false) { - maxAssets = maxAssets; - } - } - - /** - * @notice Checks if the account should be allowed to redeem tokens in the given market - * @param cToken The market to verify the redeem against - * @param redeemer The account which would redeem the tokens - * @param redeemTokens The number of cTokens to exchange for the underlying asset in the market - * @return 0 if the redeem is allowed, otherwise a semi-opaque error code (See ErrorReporter.sol) - */ - function redeemAllowed(address cToken, address redeemer, uint redeemTokens) override external returns (uint) { - return redeemAllowedInternal(cToken, redeemer, redeemTokens); - } - - function redeemAllowedInternal(address cToken, address redeemer, uint redeemTokens) internal view returns (uint) { - if (!markets[cToken].isListed) { - return uint(Error.MARKET_NOT_LISTED); - } - - // *may include Policy Hook-type checks - - /* If the redeemer is not 'in' the market, then we can bypass the liquidity check */ - if (!markets[cToken].accountMembership[redeemer]) { - return uint(Error.NO_ERROR); - } - - /* Otherwise, perform a hypothetical liquidity check to guard against shortfall */ - (Error err, , uint shortfall) = getHypotheticalAccountLiquidityInternal(redeemer, CToken(cToken), redeemTokens, 0); - if (err != Error.NO_ERROR) { - return uint(err); - } - if (shortfall > 0) { - return uint(Error.INSUFFICIENT_LIQUIDITY); - } - - return uint(Error.NO_ERROR); - } - - /** - * @notice Validates redeem and reverts on rejection. May emit logs. - * @param cToken Asset being redeemed - * @param redeemer The address redeeming the tokens - * @param redeemAmount The amount of the underlying asset being redeemed - * @param redeemTokens The number of tokens being redeemed - */ - function redeemVerify(address cToken, address redeemer, uint redeemAmount, uint redeemTokens) override external { - // Shh - currently unused - cToken; - redeemer; - - // Require tokens is zero or amount is also zero - if (redeemTokens == 0 && redeemAmount > 0) { - revert("redeemTokens zero"); - } - } - - /** - * @notice Checks if the account should be allowed to borrow the underlying asset of the given market - * @param cToken The market to verify the borrow against - * @param borrower The account which would borrow the asset - * @param borrowAmount The amount of underlying the account would borrow - * @return 0 if the borrow is allowed, otherwise a semi-opaque error code (See ErrorReporter.sol) - */ - function borrowAllowed(address cToken, address borrower, uint borrowAmount) override external returns (uint) { - // Pausing is a very serious situation - we revert to sound the alarms - require(!borrowGuardianPaused[cToken], "borrow is paused"); - - if (!markets[cToken].isListed) { - return uint(Error.MARKET_NOT_LISTED); - } - - // *may include Policy Hook-type checks - - if (!markets[cToken].accountMembership[borrower]) { - // only cTokens may call borrowAllowed if borrower not in market - require(msg.sender == cToken, "sender must be cToken"); - - // attempt to add borrower to the market - Error err = addToMarketInternal(CToken(msg.sender), borrower); - if (err != Error.NO_ERROR) { - return uint(err); - } - - // it should be impossible to break the important invariant - assert(markets[cToken].accountMembership[borrower]); - } - - if (oracle.getUnderlyingPrice(CToken(cToken)) == 0) { - return uint(Error.PRICE_ERROR); - } - - (Error err, , uint shortfall) = getHypotheticalAccountLiquidityInternal(borrower, CToken(cToken), 0, borrowAmount); - if (err != Error.NO_ERROR) { - return uint(err); - } - if (shortfall > 0) { - return uint(Error.INSUFFICIENT_LIQUIDITY); - } - - return uint(Error.NO_ERROR); - } - - /** - * @notice Validates borrow and reverts on rejection. May emit logs. - * @param cToken Asset whose underlying is being borrowed - * @param borrower The address borrowing the underlying - * @param borrowAmount The amount of the underlying asset requested to borrow - */ - function borrowVerify(address cToken, address borrower, uint borrowAmount) override external { - // Shh - currently unused - cToken; - borrower; - borrowAmount; - - // Shh - we don't ever want this hook to be marked pure - if (false) { - maxAssets = maxAssets; - } - } - - /** - * @notice Checks if the account should be allowed to repay a borrow in the given market - * @param cToken The market to verify the repay against - * @param payer The account which would repay the asset - * @param borrower The account which would borrowed the asset - * @param repayAmount The amount of the underlying asset the account would repay - * @return 0 if the repay is allowed, otherwise a semi-opaque error code (See ErrorReporter.sol) - */ - function repayBorrowAllowed( - address cToken, - address payer, - address borrower, - uint repayAmount) override external returns (uint) { - // Shh - currently unused - payer; - borrower; - repayAmount; - - if (!markets[cToken].isListed) { - return uint(Error.MARKET_NOT_LISTED); - } - - // *may include Policy Hook-type checks - - return uint(Error.NO_ERROR); - } - - /** - * @notice Validates repayBorrow and reverts on rejection. May emit logs. - * @param cToken Asset being repaid - * @param payer The address repaying the borrow - * @param borrower The address of the borrower - * @param actualRepayAmount The amount of underlying being repaid - */ - function repayBorrowVerify( - address cToken, - address payer, - address borrower, - uint actualRepayAmount, - uint borrowerIndex) override external { - // Shh - currently unused - cToken; - payer; - borrower; - actualRepayAmount; - borrowerIndex; - - // Shh - we don't ever want this hook to be marked pure - if (false) { - maxAssets = maxAssets; - } - } - - /** - * @notice Checks if the liquidation should be allowed to occur - * @param cTokenBorrowed Asset which was borrowed by the borrower - * @param cTokenCollateral Asset which was used as collateral and will be seized - * @param liquidator The address repaying the borrow and seizing the collateral - * @param borrower The address of the borrower - * @param repayAmount The amount of underlying being repaid - */ - function liquidateBorrowAllowed( - address cTokenBorrowed, - address cTokenCollateral, - address liquidator, - address borrower, - uint repayAmount) override external returns (uint) { - // Shh - currently unused - liquidator; - - if (!markets[cTokenBorrowed].isListed || !markets[cTokenCollateral].isListed) { - return uint(Error.MARKET_NOT_LISTED); - } - - // *may include Policy Hook-type checks - - /* The borrower must have shortfall in order to be liquidatable */ - (Error err, , uint shortfall) = getAccountLiquidityInternal(borrower); - if (err != Error.NO_ERROR) { - return uint(err); - } - if (shortfall == 0) { - return uint(Error.INSUFFICIENT_SHORTFALL); - } - - /* The liquidator may not repay more than what is allowed by the closeFactor */ - uint borrowBalance = CToken(cTokenBorrowed).borrowBalanceStored(borrower); - (MathError mathErr, uint maxClose) = mulScalarTruncate(Exp({mantissa: closeFactorMantissa}), borrowBalance); - if (mathErr != MathError.NO_ERROR) { - return uint(Error.MATH_ERROR); - } - if (repayAmount > maxClose) { - return uint(Error.TOO_MUCH_REPAY); - } - - return uint(Error.NO_ERROR); - } - - /** - * @notice Validates liquidateBorrow and reverts on rejection. May emit logs. - * @param cTokenBorrowed Asset which was borrowed by the borrower - * @param cTokenCollateral Asset which was used as collateral and will be seized - * @param liquidator The address repaying the borrow and seizing the collateral - * @param borrower The address of the borrower - * @param actualRepayAmount The amount of underlying being repaid - */ - function liquidateBorrowVerify( - address cTokenBorrowed, - address cTokenCollateral, - address liquidator, - address borrower, - uint actualRepayAmount, - uint seizeTokens) override external { - // Shh - currently unused - cTokenBorrowed; - cTokenCollateral; - liquidator; - borrower; - actualRepayAmount; - seizeTokens; - - // Shh - we don't ever want this hook to be marked pure - if (false) { - maxAssets = maxAssets; - } - } - - /** - * @notice Checks if the seizing of assets should be allowed to occur - * @param cTokenCollateral Asset which was used as collateral and will be seized - * @param cTokenBorrowed Asset which was borrowed by the borrower - * @param liquidator The address repaying the borrow and seizing the collateral - * @param borrower The address of the borrower - * @param seizeTokens The number of collateral tokens to seize - */ - function seizeAllowed( - address cTokenCollateral, - address cTokenBorrowed, - address liquidator, - address borrower, - uint seizeTokens) override external returns (uint) { - // Pausing is a very serious situation - we revert to sound the alarms - require(!seizeGuardianPaused, "seize is paused"); - - // Shh - currently unused - liquidator; - borrower; - seizeTokens; - - if (!markets[cTokenCollateral].isListed || !markets[cTokenBorrowed].isListed) { - return uint(Error.MARKET_NOT_LISTED); - } - - if (CToken(cTokenCollateral).comptroller() != CToken(cTokenBorrowed).comptroller()) { - return uint(Error.COMPTROLLER_MISMATCH); - } - - // *may include Policy Hook-type checks - - return uint(Error.NO_ERROR); - } - - /** - * @notice Validates seize and reverts on rejection. May emit logs. - * @param cTokenCollateral Asset which was used as collateral and will be seized - * @param cTokenBorrowed Asset which was borrowed by the borrower - * @param liquidator The address repaying the borrow and seizing the collateral - * @param borrower The address of the borrower - * @param seizeTokens The number of collateral tokens to seize - */ - function seizeVerify( - address cTokenCollateral, - address cTokenBorrowed, - address liquidator, - address borrower, - uint seizeTokens) override external { - // Shh - currently unused - cTokenCollateral; - cTokenBorrowed; - liquidator; - borrower; - seizeTokens; - - // Shh - we don't ever want this hook to be marked pure - if (false) { - maxAssets = maxAssets; - } - } - - /** - * @notice Checks if the account should be allowed to transfer tokens in the given market - * @param cToken The market to verify the transfer against - * @param src The account which sources the tokens - * @param dst The account which receives the tokens - * @param transferTokens The number of cTokens to transfer - * @return 0 if the transfer is allowed, otherwise a semi-opaque error code (See ErrorReporter.sol) - */ - function transferAllowed(address cToken, address src, address dst, uint transferTokens) override external returns (uint) { - // Pausing is a very serious situation - we revert to sound the alarms - require(!transferGuardianPaused, "transfer is paused"); - - // Shh - currently unused - dst; - - // *may include Policy Hook-type checks - - // Currently the only consideration is whether or not - // the src is allowed to redeem this many tokens - return redeemAllowedInternal(cToken, src, transferTokens); - } - - /** - * @notice Validates transfer and reverts on rejection. May emit logs. - * @param cToken Asset being transferred - * @param src The account which sources the tokens - * @param dst The account which receives the tokens - * @param transferTokens The number of cTokens to transfer - */ - function transferVerify(address cToken, address src, address dst, uint transferTokens) override external { - // Shh - currently unused - cToken; - src; - dst; - transferTokens; - - // Shh - we don't ever want this hook to be marked pure - if (false) { - maxAssets = maxAssets; - } - } - - /*** Liquidity/Liquidation Calculations ***/ - - /** - * @dev Local vars for avoiding stack-depth limits in calculating account liquidity. - * Note that `cTokenBalance` is the number of cTokens the account owns in the market, - * whereas `borrowBalance` is the amount of underlying that the account has borrowed. - */ - struct AccountLiquidityLocalVars { - uint sumCollateral; - uint sumBorrowPlusEffects; - uint cTokenBalance; - uint borrowBalance; - uint exchangeRateMantissa; - uint oraclePriceMantissa; - Exp collateralFactor; - Exp exchangeRate; - Exp oraclePrice; - Exp tokensToEther; - } - - /** - * @notice Determine the current account liquidity wrt collateral requirements - * @return (possible error code (semi-opaque), - account liquidity in excess of collateral requirements, - * account shortfall below collateral requirements) - */ - function getAccountLiquidity(address account) public view returns (uint, uint, uint) { - (Error err, uint liquidity, uint shortfall) = getHypotheticalAccountLiquidityInternal(account, CToken(0), 0, 0); - - return (uint(err), liquidity, shortfall); - } - - /** - * @notice Determine the current account liquidity wrt collateral requirements - * @return (possible error code, - account liquidity in excess of collateral requirements, - * account shortfall below collateral requirements) - */ - function getAccountLiquidityInternal(address account) internal view returns (Error, uint, uint) { - return getHypotheticalAccountLiquidityInternal(account, CToken(0), 0, 0); - } - - /** - * @notice Determine what the account liquidity would be if the given amounts were redeemed/borrowed - * @param cTokenModify The market to hypothetically redeem/borrow in - * @param account The account to determine liquidity for - * @param redeemTokens The number of tokens to hypothetically redeem - * @param borrowAmount The amount of underlying to hypothetically borrow - * @return (possible error code (semi-opaque), - hypothetical account liquidity in excess of collateral requirements, - * hypothetical account shortfall below collateral requirements) - */ - function getHypotheticalAccountLiquidity( - address account, - address cTokenModify, - uint redeemTokens, - uint borrowAmount) public view returns (uint, uint, uint) { - (Error err, uint liquidity, uint shortfall) = getHypotheticalAccountLiquidityInternal(account, CToken(cTokenModify), redeemTokens, borrowAmount); - return (uint(err), liquidity, shortfall); - } - - /** - * @notice Determine what the account liquidity would be if the given amounts were redeemed/borrowed - * @param cTokenModify The market to hypothetically redeem/borrow in - * @param account The account to determine liquidity for - * @param redeemTokens The number of tokens to hypothetically redeem - * @param borrowAmount The amount of underlying to hypothetically borrow - * @dev Note that we calculate the exchangeRateStored for each collateral cToken using stored data, - * without calculating accumulated interest. - * @return (possible error code, - hypothetical account liquidity in excess of collateral requirements, - * hypothetical account shortfall below collateral requirements) - */ - function getHypotheticalAccountLiquidityInternal( - address account, - CToken cTokenModify, - uint redeemTokens, - uint borrowAmount) internal view returns (Error, uint, uint) { - - AccountLiquidityLocalVars memory vars; // Holds all our calculation results - uint oErr; - MathError mErr; - - // For each asset the account is in - CToken[] memory assets = accountAssets[account]; - for (uint i = 0; i < assets.length; i++) { - CToken asset = assets[i]; - - // Read the balances and exchange rate from the cToken - (oErr, vars.cTokenBalance, vars.borrowBalance, vars.exchangeRateMantissa) = asset.getAccountSnapshot(account); - if (oErr != 0) { // semi-opaque error code, we assume NO_ERROR == 0 is invariant between upgrades - return (Error.SNAPSHOT_ERROR, 0, 0); - } - vars.collateralFactor = Exp({mantissa: markets[address(asset)].collateralFactorMantissa}); - vars.exchangeRate = Exp({mantissa: vars.exchangeRateMantissa}); - - // Get the normalized price of the asset - vars.oraclePriceMantissa = oracle.getUnderlyingPrice(asset); - if (vars.oraclePriceMantissa == 0) { - return (Error.PRICE_ERROR, 0, 0); - } - vars.oraclePrice = Exp({mantissa: vars.oraclePriceMantissa}); - - // Pre-compute a conversion factor from tokens -> ether (normalized price value) - (mErr, vars.tokensToEther) = mulExp3(vars.collateralFactor, vars.exchangeRate, vars.oraclePrice); - if (mErr != MathError.NO_ERROR) { - return (Error.MATH_ERROR, 0, 0); - } - - // sumCollateral += tokensToEther * cTokenBalance - (mErr, vars.sumCollateral) = mulScalarTruncateAddUInt(vars.tokensToEther, vars.cTokenBalance, vars.sumCollateral); - if (mErr != MathError.NO_ERROR) { - return (Error.MATH_ERROR, 0, 0); - } - - // sumBorrowPlusEffects += oraclePrice * borrowBalance - (mErr, vars.sumBorrowPlusEffects) = mulScalarTruncateAddUInt(vars.oraclePrice, vars.borrowBalance, vars.sumBorrowPlusEffects); - if (mErr != MathError.NO_ERROR) { - return (Error.MATH_ERROR, 0, 0); - } - - // Calculate effects of interacting with cTokenModify - if (asset == cTokenModify) { - // redeem effect - // sumBorrowPlusEffects += tokensToEther * redeemTokens - (mErr, vars.sumBorrowPlusEffects) = mulScalarTruncateAddUInt(vars.tokensToEther, redeemTokens, vars.sumBorrowPlusEffects); - if (mErr != MathError.NO_ERROR) { - return (Error.MATH_ERROR, 0, 0); - } - - // borrow effect - // sumBorrowPlusEffects += oraclePrice * borrowAmount - (mErr, vars.sumBorrowPlusEffects) = mulScalarTruncateAddUInt(vars.oraclePrice, borrowAmount, vars.sumBorrowPlusEffects); - if (mErr != MathError.NO_ERROR) { - return (Error.MATH_ERROR, 0, 0); - } - } - } - - // These are safe, as the underflow condition is checked first - if (vars.sumCollateral > vars.sumBorrowPlusEffects) { - return (Error.NO_ERROR, vars.sumCollateral - vars.sumBorrowPlusEffects, 0); - } else { - return (Error.NO_ERROR, 0, vars.sumBorrowPlusEffects - vars.sumCollateral); - } - } - - /** - * @notice Calculate number of tokens of collateral asset to seize given an underlying amount - * @dev Used in liquidation (called in cToken.liquidateBorrowFresh) - * @param cTokenBorrowed The address of the borrowed cToken - * @param cTokenCollateral The address of the collateral cToken - * @param actualRepayAmount The amount of cTokenBorrowed underlying to convert into cTokenCollateral tokens - * @return (errorCode, number of cTokenCollateral tokens to be seized in a liquidation) - */ - function liquidateCalculateSeizeTokens(address cTokenBorrowed, address cTokenCollateral, uint actualRepayAmount) - override external view returns (uint, uint) { - /* Read oracle prices for borrowed and collateral markets */ - uint priceBorrowedMantissa = oracle.getUnderlyingPrice(CToken(cTokenBorrowed)); - uint priceCollateralMantissa = oracle.getUnderlyingPrice(CToken(cTokenCollateral)); - if (priceBorrowedMantissa == 0 || priceCollateralMantissa == 0) { - return (uint(Error.PRICE_ERROR), 0); - } - - /* - * Get the exchange rate and calculate the number of collateral tokens to seize: - * seizeAmount = actualRepayAmount * liquidationIncentive * priceBorrowed / priceCollateral - * seizeTokens = seizeAmount / exchangeRate - * = actualRepayAmount * (liquidationIncentive * priceBorrowed) / (priceCollateral * exchangeRate) - */ - uint exchangeRateMantissa = CToken(cTokenCollateral).exchangeRateStored(); // Note: reverts on error - uint seizeTokens; - Exp memory numerator; - Exp memory denominator; - Exp memory ratio; - MathError mathErr; - - (mathErr, numerator) = mulExp(liquidationIncentiveMantissa, priceBorrowedMantissa); - if (mathErr != MathError.NO_ERROR) { - return (uint(Error.MATH_ERROR), 0); - } - - (mathErr, denominator) = mulExp(priceCollateralMantissa, exchangeRateMantissa); - if (mathErr != MathError.NO_ERROR) { - return (uint(Error.MATH_ERROR), 0); - } - - (mathErr, ratio) = divExp(numerator, denominator); - if (mathErr != MathError.NO_ERROR) { - return (uint(Error.MATH_ERROR), 0); - } - - (mathErr, seizeTokens) = mulScalarTruncate(ratio, actualRepayAmount); - if (mathErr != MathError.NO_ERROR) { - return (uint(Error.MATH_ERROR), 0); - } - - return (uint(Error.NO_ERROR), seizeTokens); - } - - /*** Admin Functions ***/ - - /** - * @notice Sets a new price oracle for the comptroller - * @dev Admin function to set a new price oracle - * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) - */ - function _setPriceOracle(PriceOracle newOracle) public returns (uint) { - // Check caller is admin - if (msg.sender != admin) { - return fail(Error.UNAUTHORIZED, FailureInfo.SET_PRICE_ORACLE_OWNER_CHECK); - } - - // Track the old oracle for the comptroller - PriceOracle oldOracle = oracle; - - // Set comptroller's oracle to newOracle - oracle = newOracle; - - // Emit NewPriceOracle(oldOracle, newOracle) - emit NewPriceOracle(oldOracle, newOracle); - - return uint(Error.NO_ERROR); - } - - /** - * @notice Sets the closeFactor used when liquidating borrows - * @dev Admin function to set closeFactor - * @param newCloseFactorMantissa New close factor, scaled by 1e18 - * @return uint 0=success, otherwise a failure. (See ErrorReporter for details) - */ - function _setCloseFactor(uint newCloseFactorMantissa) external returns (uint256) { - // Check caller is admin - if (msg.sender != admin) { - return fail(Error.UNAUTHORIZED, FailureInfo.SET_CLOSE_FACTOR_OWNER_CHECK); - } - - Exp memory newCloseFactorExp = Exp({mantissa: newCloseFactorMantissa}); - Exp memory lowLimit = Exp({mantissa: closeFactorMinMantissa}); - if (lessThanOrEqualExp(newCloseFactorExp, lowLimit)) { - return fail(Error.INVALID_CLOSE_FACTOR, FailureInfo.SET_CLOSE_FACTOR_VALIDATION); - } - - Exp memory highLimit = Exp({mantissa: closeFactorMaxMantissa}); - if (lessThanExp(highLimit, newCloseFactorExp)) { - return fail(Error.INVALID_CLOSE_FACTOR, FailureInfo.SET_CLOSE_FACTOR_VALIDATION); - } - - uint oldCloseFactorMantissa = closeFactorMantissa; - closeFactorMantissa = newCloseFactorMantissa; - emit NewCloseFactor(oldCloseFactorMantissa, closeFactorMantissa); - - return uint(Error.NO_ERROR); - } - - /** - * @notice Sets the collateralFactor for a market - * @dev Admin function to set per-market collateralFactor - * @param cToken The market to set the factor on - * @param newCollateralFactorMantissa The new collateral factor, scaled by 1e18 - * @return uint 0=success, otherwise a failure. (See ErrorReporter for details) - */ - function _setCollateralFactor(CToken cToken, uint newCollateralFactorMantissa) external returns (uint256) { - // Check caller is admin - if (msg.sender != admin) { - return fail(Error.UNAUTHORIZED, FailureInfo.SET_COLLATERAL_FACTOR_OWNER_CHECK); - } - - // Verify market is listed - Market storage market = markets[address(cToken)]; - if (!market.isListed) { - return fail(Error.MARKET_NOT_LISTED, FailureInfo.SET_COLLATERAL_FACTOR_NO_EXISTS); - } - - Exp memory newCollateralFactorExp = Exp({mantissa: newCollateralFactorMantissa}); - - // Check collateral factor <= 0.9 - Exp memory highLimit = Exp({mantissa: collateralFactorMaxMantissa}); - if (lessThanExp(highLimit, newCollateralFactorExp)) { - return fail(Error.INVALID_COLLATERAL_FACTOR, FailureInfo.SET_COLLATERAL_FACTOR_VALIDATION); - } - - // If collateral factor != 0, fail if price == 0 - if (newCollateralFactorMantissa != 0 && oracle.getUnderlyingPrice(cToken) == 0) { - return fail(Error.PRICE_ERROR, FailureInfo.SET_COLLATERAL_FACTOR_WITHOUT_PRICE); - } - - // Set market's collateral factor to new collateral factor, remember old value - uint oldCollateralFactorMantissa = market.collateralFactorMantissa; - market.collateralFactorMantissa = newCollateralFactorMantissa; - - // Emit event with asset, old collateral factor, and new collateral factor - emit NewCollateralFactor(cToken, oldCollateralFactorMantissa, newCollateralFactorMantissa); - - return uint(Error.NO_ERROR); - } - - /** - * @notice Sets maxAssets which controls how many markets can be entered - * @dev Admin function to set maxAssets - * @param newMaxAssets New max assets - * @return uint 0=success, otherwise a failure. (See ErrorReporter for details) - */ - function _setMaxAssets(uint newMaxAssets) external returns (uint) { - // Check caller is admin - if (msg.sender != admin) { - return fail(Error.UNAUTHORIZED, FailureInfo.SET_MAX_ASSETS_OWNER_CHECK); - } - - uint oldMaxAssets = maxAssets; - maxAssets = newMaxAssets; - emit NewMaxAssets(oldMaxAssets, newMaxAssets); - - return uint(Error.NO_ERROR); - } - - /** - * @notice Sets liquidationIncentive - * @dev Admin function to set liquidationIncentive - * @param newLiquidationIncentiveMantissa New liquidationIncentive scaled by 1e18 - * @return uint 0=success, otherwise a failure. (See ErrorReporter for details) - */ - function _setLiquidationIncentive(uint newLiquidationIncentiveMantissa) external returns (uint) { - // Check caller is admin - if (msg.sender != admin) { - return fail(Error.UNAUTHORIZED, FailureInfo.SET_LIQUIDATION_INCENTIVE_OWNER_CHECK); - } - - // Check de-scaled min <= newLiquidationIncentive <= max - Exp memory newLiquidationIncentive = Exp({mantissa: newLiquidationIncentiveMantissa}); - Exp memory minLiquidationIncentive = Exp({mantissa: liquidationIncentiveMinMantissa}); - if (lessThanExp(newLiquidationIncentive, minLiquidationIncentive)) { - return fail(Error.INVALID_LIQUIDATION_INCENTIVE, FailureInfo.SET_LIQUIDATION_INCENTIVE_VALIDATION); - } - - Exp memory maxLiquidationIncentive = Exp({mantissa: liquidationIncentiveMaxMantissa}); - if (lessThanExp(maxLiquidationIncentive, newLiquidationIncentive)) { - return fail(Error.INVALID_LIQUIDATION_INCENTIVE, FailureInfo.SET_LIQUIDATION_INCENTIVE_VALIDATION); - } - - // Save current value for use in log - uint oldLiquidationIncentiveMantissa = liquidationIncentiveMantissa; - - // Set liquidation incentive to new incentive - liquidationIncentiveMantissa = newLiquidationIncentiveMantissa; - - // Emit event with old incentive, new incentive - emit NewLiquidationIncentive(oldLiquidationIncentiveMantissa, newLiquidationIncentiveMantissa); - - return uint(Error.NO_ERROR); - } - - /** - * @notice Add the market to the markets mapping and set it as listed - * @dev Admin function to set isListed and add support for the market - * @param cToken The address of the market (token) to list - * @return uint 0=success, otherwise a failure. (See enum Error for details) - */ - function _supportMarket(CToken cToken) external returns (uint) { - if (msg.sender != admin) { - return fail(Error.UNAUTHORIZED, FailureInfo.SUPPORT_MARKET_OWNER_CHECK); - } - - if (markets[address(cToken)].isListed) { - return fail(Error.MARKET_ALREADY_LISTED, FailureInfo.SUPPORT_MARKET_EXISTS); - } - - cToken.isCToken(); // Sanity check to make sure its really a CToken - - markets[address(cToken)] = Market({isListed: true, isComped: false, collateralFactorMantissa: 0}); - emit MarketListed(cToken); - - return uint(Error.NO_ERROR); - } - - /** - * @notice Admin function to change the Pause Guardian - * @param newPauseGuardian The address of the new Pause Guardian - * @return uint 0=success, otherwise a failure. (See enum Error for details) - */ - function _setPauseGuardian(address newPauseGuardian) public returns (uint) { - if (msg.sender != admin) { - return fail(Error.UNAUTHORIZED, FailureInfo.SET_PAUSE_GUARDIAN_OWNER_CHECK); - } - - // Save current value for inclusion in log - address oldPauseGuardian = pauseGuardian; - - // Store pauseGuardian with value newPauseGuardian - pauseGuardian = newPauseGuardian; - - // Emit NewPauseGuardian(OldPauseGuardian, NewPauseGuardian) - emit NewPauseGuardian(oldPauseGuardian, pauseGuardian); - - return uint(Error.NO_ERROR); - } - - function _setMintPaused(CToken cToken, bool state) public returns (bool) { - require(markets[address(cToken)].isListed, "cannot pause a market that is not listed"); - require(msg.sender == pauseGuardian || msg.sender == admin, "only pause guardian and admin can pause"); - require(msg.sender == admin || state == true, "only admin can unpause"); - - mintGuardianPaused[address(cToken)] = state; - emit ActionPaused(cToken, "Mint", state); - return state; - } - - function _setBorrowPaused(CToken cToken, bool state) public returns (bool) { - require(markets[address(cToken)].isListed, "cannot pause a market that is not listed"); - require(msg.sender == pauseGuardian || msg.sender == admin, "only pause guardian and admin can pause"); - require(msg.sender == admin || state == true, "only admin can unpause"); - - borrowGuardianPaused[address(cToken)] = state; - emit ActionPaused(cToken, "Borrow", state); - return state; - } - - function _setTransferPaused(bool state) public returns (bool) { - require(msg.sender == pauseGuardian || msg.sender == admin, "only pause guardian and admin can pause"); - require(msg.sender == admin || state == true, "only admin can unpause"); - - transferGuardianPaused = state; - emit ActionPaused("Transfer", state); - return state; - } - - function _setSeizePaused(bool state) public returns (bool) { - require(msg.sender == pauseGuardian || msg.sender == admin, "only pause guardian and admin can pause"); - require(msg.sender == admin || state == true, "only admin can unpause"); - - seizeGuardianPaused = state; - emit ActionPaused("Seize", state); - return state; - } - - function _become(Unitroller unitroller) public { - require(msg.sender == unitroller.admin(), "only unitroller admin can change brains"); - - uint changeStatus = unitroller._acceptImplementation(); - require(changeStatus == 0, "change not authorized"); - } -} +pragma solidity ^0.6.7; + +import "./CToken.sol"; +import "./ErrorReporter.sol"; +import "./Exponential.sol"; +import "./PriceOracle.sol"; +import "./ComptrollerInterface.sol"; +import "./ComptrollerStorage.sol"; +import "./Unitroller.sol"; + +/** + * @title Compound's Comptroller Contract + * @author Compound + */ +contract ComptrollerG2 is ComptrollerV2Storage, ComptrollerInterface, ComptrollerErrorReporter, Exponential { + /** + * @notice Emitted when an admin supports a market + */ + event MarketListed(CToken cToken); + + /** + * @notice Emitted when an account enters a market + */ + event MarketEntered(CToken cToken, address account); + + /** + * @notice Emitted when an account exits a market + */ + event MarketExited(CToken cToken, address account); + + /** + * @notice Emitted when close factor is changed by admin + */ + event NewCloseFactor(uint oldCloseFactorMantissa, uint newCloseFactorMantissa); + + /** + * @notice Emitted when a collateral factor is changed by admin + */ + event NewCollateralFactor(CToken cToken, uint oldCollateralFactorMantissa, uint newCollateralFactorMantissa); + + /** + * @notice Emitted when liquidation incentive is changed by admin + */ + event NewLiquidationIncentive(uint oldLiquidationIncentiveMantissa, uint newLiquidationIncentiveMantissa); + + /** + * @notice Emitted when maxAssets is changed by admin + */ + event NewMaxAssets(uint oldMaxAssets, uint newMaxAssets); + + /** + * @notice Emitted when price oracle is changed + */ + event NewPriceOracle(PriceOracle oldPriceOracle, PriceOracle newPriceOracle); + + /** + * @notice Emitted when pause guardian is changed + */ + event NewPauseGuardian(address oldPauseGuardian, address newPauseGuardian); + + /** + * @notice Emitted when an action is paused globally + */ + event ActionPaused(string action, bool pauseState); + + /** + * @notice Emitted when an action is paused on a market + */ + event ActionPaused(CToken cToken, string action, bool pauseState); + + // closeFactorMantissa must be strictly greater than this value + uint internal constant closeFactorMinMantissa = 0.05e18; // 0.05 + + // closeFactorMantissa must not exceed this value + uint internal constant closeFactorMaxMantissa = 0.9e18; // 0.9 + + // No collateralFactorMantissa may exceed this value + uint internal constant collateralFactorMaxMantissa = 0.9e18; // 0.9 + + // liquidationIncentiveMantissa must be no less than this value + uint internal constant liquidationIncentiveMinMantissa = 1.0e18; // 1.0 + + // liquidationIncentiveMantissa must be no greater than this value + uint internal constant liquidationIncentiveMaxMantissa = 1.5e18; // 1.5 + + constructor() public { + admin = msg.sender; + } + + /*** Assets You Are In ***/ + + /** + * @notice Returns the assets an account has entered + * @param account The address of the account to pull assets for + * @return A dynamic list with the assets the account has entered + */ + function getAssetsIn(address account) external view returns (CToken[] memory) { + CToken[] memory assetsIn = accountAssets[account]; + + return assetsIn; + } + + /** + * @notice Returns whether the given account is entered in the given asset + * @param account The address of the account to check + * @param cToken The cToken to check + * @return True if the account is in the asset, otherwise false. + */ + function checkMembership(address account, CToken cToken) external view returns (bool) { + return markets[address(cToken)].accountMembership[account]; + } + + /** + * @notice Add assets to be included in account liquidity calculation + * @param cTokens The list of addresses of the cToken markets to be enabled + * @return Success indicator for whether each corresponding market was entered + */ + function enterMarkets(address[] memory cTokens) override public returns (uint[] memory) { + uint len = cTokens.length; + + uint[] memory results = new uint[](len); + for (uint i = 0; i < len; i++) { + CToken cToken = CToken(cTokens[i]); + + results[i] = uint(addToMarketInternal(cToken, msg.sender)); + } + + return results; + } + + /** + * @notice Add the market to the borrower's "assets in" for liquidity calculations + * @param cToken The market to enter + * @param borrower The address of the account to modify + * @return Success indicator for whether the market was entered + */ + function addToMarketInternal(CToken cToken, address borrower) internal returns (Error) { + Market storage marketToJoin = markets[address(cToken)]; + + if (!marketToJoin.isListed) { + // market is not listed, cannot join + return Error.MARKET_NOT_LISTED; + } + + if (marketToJoin.accountMembership[borrower] == true) { + // already joined + return Error.NO_ERROR; + } + + if (accountAssets[borrower].length >= maxAssets) { + // no space, cannot join + return Error.TOO_MANY_ASSETS; + } + + // survived the gauntlet, add to list + // NOTE: we store these somewhat redundantly as a significant optimization + // this avoids having to iterate through the list for the most common use cases + // that is, only when we need to perform liquidity checks + // and not whenever we want to check if an account is in a particular market + marketToJoin.accountMembership[borrower] = true; + accountAssets[borrower].push(cToken); + + emit MarketEntered(cToken, borrower); + + return Error.NO_ERROR; + } + + /** + * @notice Removes asset from sender's account liquidity calculation + * @dev Sender must not have an outstanding borrow balance in the asset, + * or be providing neccessary collateral for an outstanding borrow. + * @param cTokenAddress The address of the asset to be removed + * @return Whether or not the account successfully exited the market + */ + function exitMarket(address cTokenAddress) override external returns (uint) { + CToken cToken = CToken(cTokenAddress); + /* Get sender tokensHeld and amountOwed underlying from the cToken */ + (uint oErr, uint tokensHeld, uint amountOwed, ) = cToken.getAccountSnapshot(msg.sender); + require(oErr == 0, "exitMarket: getAccountSnapshot failed"); // semi-opaque error code + + /* Fail if the sender has a borrow balance */ + if (amountOwed != 0) { + return fail(Error.NONZERO_BORROW_BALANCE, FailureInfo.EXIT_MARKET_BALANCE_OWED); + } + + /* Fail if the sender is not permitted to redeem all of their tokens */ + uint allowed = redeemAllowedInternal(cTokenAddress, msg.sender, tokensHeld); + if (allowed != 0) { + return failOpaque(Error.REJECTION, FailureInfo.EXIT_MARKET_REJECTION, allowed); + } + + Market storage marketToExit = markets[address(cToken)]; + + /* Return true if the sender is not already ‘in’ the market */ + if (!marketToExit.accountMembership[msg.sender]) { + return uint(Error.NO_ERROR); + } + + /* Set cToken account membership to false */ + delete marketToExit.accountMembership[msg.sender]; + + /* Delete cToken from the account’s list of assets */ + // load into memory for faster iteration + CToken[] memory userAssetList = accountAssets[msg.sender]; + uint len = userAssetList.length; + uint assetIndex = len; + for (uint i = 0; i < len; i++) { + if (userAssetList[i] == cToken) { + assetIndex = i; + break; + } + } + + // We *must* have found the asset in the list or our redundant data structure is broken + assert(assetIndex < len); + + // copy last item in list to location of item to be removed, reduce length by 1 + CToken[] storage storedList = accountAssets[msg.sender]; + storedList[assetIndex] = storedList[storedList.length - 1]; + storedList.pop(); + + emit MarketExited(cToken, msg.sender); + + return uint(Error.NO_ERROR); + } + + /*** Policy Hooks ***/ + + /** + * @notice Checks if the account should be allowed to mint tokens in the given market + * @param cToken The market to verify the mint against + * @param minter The account which would get the minted tokens + * @param mintAmount The amount of underlying being supplied to the market in exchange for tokens + * @return 0 if the mint is allowed, otherwise a semi-opaque error code (See ErrorReporter.sol) + */ + function mintAllowed(address cToken, address minter, uint mintAmount) override external returns (uint) { + // Pausing is a very serious situation - we revert to sound the alarms + require(!mintGuardianPaused[cToken], "mint is paused"); + + // Shh - currently unused + minter; + mintAmount; + + if (!markets[cToken].isListed) { + return uint(Error.MARKET_NOT_LISTED); + } + + // *may include Policy Hook-type checks + + return uint(Error.NO_ERROR); + } + + /** + * @notice Validates mint and reverts on rejection. May emit logs. + * @param cToken Asset being minted + * @param minter The address minting the tokens + * @param actualMintAmount The amount of the underlying asset being minted + * @param mintTokens The number of tokens being minted + */ + function mintVerify(address cToken, address minter, uint actualMintAmount, uint mintTokens) override external { + // Shh - currently unused + cToken; + minter; + actualMintAmount; + mintTokens; + + // Shh - we don't ever want this hook to be marked pure + if (false) { + maxAssets = maxAssets; + } + } + + /** + * @notice Checks if the account should be allowed to redeem tokens in the given market + * @param cToken The market to verify the redeem against + * @param redeemer The account which would redeem the tokens + * @param redeemTokens The number of cTokens to exchange for the underlying asset in the market + * @return 0 if the redeem is allowed, otherwise a semi-opaque error code (See ErrorReporter.sol) + */ + function redeemAllowed(address cToken, address redeemer, uint redeemTokens) override external returns (uint) { + return redeemAllowedInternal(cToken, redeemer, redeemTokens); + } + + function redeemAllowedInternal(address cToken, address redeemer, uint redeemTokens) internal view returns (uint) { + if (!markets[cToken].isListed) { + return uint(Error.MARKET_NOT_LISTED); + } + + // *may include Policy Hook-type checks + + /* If the redeemer is not 'in' the market, then we can bypass the liquidity check */ + if (!markets[cToken].accountMembership[redeemer]) { + return uint(Error.NO_ERROR); + } + + /* Otherwise, perform a hypothetical liquidity check to guard against shortfall */ + (Error err, , uint shortfall) = getHypotheticalAccountLiquidityInternal(redeemer, CToken(cToken), redeemTokens, 0); + if (err != Error.NO_ERROR) { + return uint(err); + } + if (shortfall > 0) { + return uint(Error.INSUFFICIENT_LIQUIDITY); + } + + return uint(Error.NO_ERROR); + } + + /** + * @notice Validates redeem and reverts on rejection. May emit logs. + * @param cToken Asset being redeemed + * @param redeemer The address redeeming the tokens + * @param redeemAmount The amount of the underlying asset being redeemed + * @param redeemTokens The number of tokens being redeemed + */ + function redeemVerify(address cToken, address redeemer, uint redeemAmount, uint redeemTokens) override external { + // Shh - currently unused + cToken; + redeemer; + + // Require tokens is zero or amount is also zero + if (redeemTokens == 0 && redeemAmount > 0) { + revert("redeemTokens zero"); + } + } + + /** + * @notice Checks if the account should be allowed to borrow the underlying asset of the given market + * @param cToken The market to verify the borrow against + * @param borrower The account which would borrow the asset + * @param borrowAmount The amount of underlying the account would borrow + * @return 0 if the borrow is allowed, otherwise a semi-opaque error code (See ErrorReporter.sol) + */ + function borrowAllowed(address cToken, address borrower, uint borrowAmount) override external returns (uint) { + // Pausing is a very serious situation - we revert to sound the alarms + require(!borrowGuardianPaused[cToken], "borrow is paused"); + + if (!markets[cToken].isListed) { + return uint(Error.MARKET_NOT_LISTED); + } + + // *may include Policy Hook-type checks + + if (!markets[cToken].accountMembership[borrower]) { + // only cTokens may call borrowAllowed if borrower not in market + require(msg.sender == cToken, "sender must be cToken"); + + // attempt to add borrower to the market + Error err = addToMarketInternal(CToken(msg.sender), borrower); + if (err != Error.NO_ERROR) { + return uint(err); + } + + // it should be impossible to break the important invariant + assert(markets[cToken].accountMembership[borrower]); + } + + if (oracle.getUnderlyingPrice(CToken(cToken)) == 0) { + return uint(Error.PRICE_ERROR); + } + + (Error err, , uint shortfall) = getHypotheticalAccountLiquidityInternal(borrower, CToken(cToken), 0, borrowAmount); + if (err != Error.NO_ERROR) { + return uint(err); + } + if (shortfall > 0) { + return uint(Error.INSUFFICIENT_LIQUIDITY); + } + + return uint(Error.NO_ERROR); + } + + /** + * @notice Validates borrow and reverts on rejection. May emit logs. + * @param cToken Asset whose underlying is being borrowed + * @param borrower The address borrowing the underlying + * @param borrowAmount The amount of the underlying asset requested to borrow + */ + function borrowVerify(address cToken, address borrower, uint borrowAmount) override external { + // Shh - currently unused + cToken; + borrower; + borrowAmount; + + // Shh - we don't ever want this hook to be marked pure + if (false) { + maxAssets = maxAssets; + } + } + + /** + * @notice Checks if the account should be allowed to repay a borrow in the given market + * @param cToken The market to verify the repay against + * @param payer The account which would repay the asset + * @param borrower The account which would borrowed the asset + * @param repayAmount The amount of the underlying asset the account would repay + * @return 0 if the repay is allowed, otherwise a semi-opaque error code (See ErrorReporter.sol) + */ + function repayBorrowAllowed( + address cToken, + address payer, + address borrower, + uint repayAmount) override external returns (uint) { + // Shh - currently unused + payer; + borrower; + repayAmount; + + if (!markets[cToken].isListed) { + return uint(Error.MARKET_NOT_LISTED); + } + + // *may include Policy Hook-type checks + + return uint(Error.NO_ERROR); + } + + /** + * @notice Validates repayBorrow and reverts on rejection. May emit logs. + * @param cToken Asset being repaid + * @param payer The address repaying the borrow + * @param borrower The address of the borrower + * @param actualRepayAmount The amount of underlying being repaid + */ + function repayBorrowVerify( + address cToken, + address payer, + address borrower, + uint actualRepayAmount, + uint borrowerIndex) override external { + // Shh - currently unused + cToken; + payer; + borrower; + actualRepayAmount; + borrowerIndex; + + // Shh - we don't ever want this hook to be marked pure + if (false) { + maxAssets = maxAssets; + } + } + + /** + * @notice Checks if the liquidation should be allowed to occur + * @param cTokenBorrowed Asset which was borrowed by the borrower + * @param cTokenCollateral Asset which was used as collateral and will be seized + * @param liquidator The address repaying the borrow and seizing the collateral + * @param borrower The address of the borrower + * @param repayAmount The amount of underlying being repaid + */ + function liquidateBorrowAllowed( + address cTokenBorrowed, + address cTokenCollateral, + address liquidator, + address borrower, + uint repayAmount) override external returns (uint) { + // Shh - currently unused + liquidator; + + if (!markets[cTokenBorrowed].isListed || !markets[cTokenCollateral].isListed) { + return uint(Error.MARKET_NOT_LISTED); + } + + // *may include Policy Hook-type checks + + /* The borrower must have shortfall in order to be liquidatable */ + (Error err, , uint shortfall) = getAccountLiquidityInternal(borrower); + if (err != Error.NO_ERROR) { + return uint(err); + } + if (shortfall == 0) { + return uint(Error.INSUFFICIENT_SHORTFALL); + } + + /* The liquidator may not repay more than what is allowed by the closeFactor */ + uint borrowBalance = CToken(cTokenBorrowed).borrowBalanceStored(borrower); + (MathError mathErr, uint maxClose) = mulScalarTruncate(Exp({mantissa: closeFactorMantissa}), borrowBalance); + if (mathErr != MathError.NO_ERROR) { + return uint(Error.MATH_ERROR); + } + if (repayAmount > maxClose) { + return uint(Error.TOO_MUCH_REPAY); + } + + return uint(Error.NO_ERROR); + } + + /** + * @notice Validates liquidateBorrow and reverts on rejection. May emit logs. + * @param cTokenBorrowed Asset which was borrowed by the borrower + * @param cTokenCollateral Asset which was used as collateral and will be seized + * @param liquidator The address repaying the borrow and seizing the collateral + * @param borrower The address of the borrower + * @param actualRepayAmount The amount of underlying being repaid + */ + function liquidateBorrowVerify( + address cTokenBorrowed, + address cTokenCollateral, + address liquidator, + address borrower, + uint actualRepayAmount, + uint seizeTokens) override external { + // Shh - currently unused + cTokenBorrowed; + cTokenCollateral; + liquidator; + borrower; + actualRepayAmount; + seizeTokens; + + // Shh - we don't ever want this hook to be marked pure + if (false) { + maxAssets = maxAssets; + } + } + + /** + * @notice Checks if the seizing of assets should be allowed to occur + * @param cTokenCollateral Asset which was used as collateral and will be seized + * @param cTokenBorrowed Asset which was borrowed by the borrower + * @param liquidator The address repaying the borrow and seizing the collateral + * @param borrower The address of the borrower + * @param seizeTokens The number of collateral tokens to seize + */ + function seizeAllowed( + address cTokenCollateral, + address cTokenBorrowed, + address liquidator, + address borrower, + uint seizeTokens) override external returns (uint) { + // Pausing is a very serious situation - we revert to sound the alarms + require(!seizeGuardianPaused, "seize is paused"); + + // Shh - currently unused + liquidator; + borrower; + seizeTokens; + + if (!markets[cTokenCollateral].isListed || !markets[cTokenBorrowed].isListed) { + return uint(Error.MARKET_NOT_LISTED); + } + + if (CToken(cTokenCollateral).comptroller() != CToken(cTokenBorrowed).comptroller()) { + return uint(Error.COMPTROLLER_MISMATCH); + } + + // *may include Policy Hook-type checks + + return uint(Error.NO_ERROR); + } + + /** + * @notice Validates seize and reverts on rejection. May emit logs. + * @param cTokenCollateral Asset which was used as collateral and will be seized + * @param cTokenBorrowed Asset which was borrowed by the borrower + * @param liquidator The address repaying the borrow and seizing the collateral + * @param borrower The address of the borrower + * @param seizeTokens The number of collateral tokens to seize + */ + function seizeVerify( + address cTokenCollateral, + address cTokenBorrowed, + address liquidator, + address borrower, + uint seizeTokens) override external { + // Shh - currently unused + cTokenCollateral; + cTokenBorrowed; + liquidator; + borrower; + seizeTokens; + + // Shh - we don't ever want this hook to be marked pure + if (false) { + maxAssets = maxAssets; + } + } + + /** + * @notice Checks if the account should be allowed to transfer tokens in the given market + * @param cToken The market to verify the transfer against + * @param src The account which sources the tokens + * @param dst The account which receives the tokens + * @param transferTokens The number of cTokens to transfer + * @return 0 if the transfer is allowed, otherwise a semi-opaque error code (See ErrorReporter.sol) + */ + function transferAllowed(address cToken, address src, address dst, uint transferTokens) override external returns (uint) { + // Pausing is a very serious situation - we revert to sound the alarms + require(!transferGuardianPaused, "transfer is paused"); + + // Shh - currently unused + dst; + + // *may include Policy Hook-type checks + + // Currently the only consideration is whether or not + // the src is allowed to redeem this many tokens + return redeemAllowedInternal(cToken, src, transferTokens); + } + + /** + * @notice Validates transfer and reverts on rejection. May emit logs. + * @param cToken Asset being transferred + * @param src The account which sources the tokens + * @param dst The account which receives the tokens + * @param transferTokens The number of cTokens to transfer + */ + function transferVerify(address cToken, address src, address dst, uint transferTokens) override external { + // Shh - currently unused + cToken; + src; + dst; + transferTokens; + + // Shh - we don't ever want this hook to be marked pure + if (false) { + maxAssets = maxAssets; + } + } + + /*** Liquidity/Liquidation Calculations ***/ + + /** + * @dev Local vars for avoiding stack-depth limits in calculating account liquidity. + * Note that `cTokenBalance` is the number of cTokens the account owns in the market, + * whereas `borrowBalance` is the amount of underlying that the account has borrowed. + */ + struct AccountLiquidityLocalVars { + uint sumCollateral; + uint sumBorrowPlusEffects; + uint cTokenBalance; + uint borrowBalance; + uint exchangeRateMantissa; + uint oraclePriceMantissa; + Exp collateralFactor; + Exp exchangeRate; + Exp oraclePrice; + Exp tokensToEther; + } + + /** + * @notice Determine the current account liquidity wrt collateral requirements + * @return (possible error code (semi-opaque), + account liquidity in excess of collateral requirements, + * account shortfall below collateral requirements) + */ + function getAccountLiquidity(address account) public view returns (uint, uint, uint) { + (Error err, uint liquidity, uint shortfall) = getHypotheticalAccountLiquidityInternal(account, CToken(0), 0, 0); + + return (uint(err), liquidity, shortfall); + } + + /** + * @notice Determine the current account liquidity wrt collateral requirements + * @return (possible error code, + account liquidity in excess of collateral requirements, + * account shortfall below collateral requirements) + */ + function getAccountLiquidityInternal(address account) internal view returns (Error, uint, uint) { + return getHypotheticalAccountLiquidityInternal(account, CToken(0), 0, 0); + } + + /** + * @notice Determine what the account liquidity would be if the given amounts were redeemed/borrowed + * @param cTokenModify The market to hypothetically redeem/borrow in + * @param account The account to determine liquidity for + * @param redeemTokens The number of tokens to hypothetically redeem + * @param borrowAmount The amount of underlying to hypothetically borrow + * @return (possible error code (semi-opaque), + hypothetical account liquidity in excess of collateral requirements, + * hypothetical account shortfall below collateral requirements) + */ + function getHypotheticalAccountLiquidity( + address account, + address cTokenModify, + uint redeemTokens, + uint borrowAmount) public view returns (uint, uint, uint) { + (Error err, uint liquidity, uint shortfall) = getHypotheticalAccountLiquidityInternal(account, CToken(cTokenModify), redeemTokens, borrowAmount); + return (uint(err), liquidity, shortfall); + } + + /** + * @notice Determine what the account liquidity would be if the given amounts were redeemed/borrowed + * @param cTokenModify The market to hypothetically redeem/borrow in + * @param account The account to determine liquidity for + * @param redeemTokens The number of tokens to hypothetically redeem + * @param borrowAmount The amount of underlying to hypothetically borrow + * @dev Note that we calculate the exchangeRateStored for each collateral cToken using stored data, + * without calculating accumulated interest. + * @return (possible error code, + hypothetical account liquidity in excess of collateral requirements, + * hypothetical account shortfall below collateral requirements) + */ + function getHypotheticalAccountLiquidityInternal( + address account, + CToken cTokenModify, + uint redeemTokens, + uint borrowAmount) internal view returns (Error, uint, uint) { + + AccountLiquidityLocalVars memory vars; // Holds all our calculation results + uint oErr; + MathError mErr; + + // For each asset the account is in + CToken[] memory assets = accountAssets[account]; + for (uint i = 0; i < assets.length; i++) { + CToken asset = assets[i]; + + // Read the balances and exchange rate from the cToken + (oErr, vars.cTokenBalance, vars.borrowBalance, vars.exchangeRateMantissa) = asset.getAccountSnapshot(account); + if (oErr != 0) { // semi-opaque error code, we assume NO_ERROR == 0 is invariant between upgrades + return (Error.SNAPSHOT_ERROR, 0, 0); + } + vars.collateralFactor = Exp({mantissa: markets[address(asset)].collateralFactorMantissa}); + vars.exchangeRate = Exp({mantissa: vars.exchangeRateMantissa}); + + // Get the normalized price of the asset + vars.oraclePriceMantissa = oracle.getUnderlyingPrice(asset); + if (vars.oraclePriceMantissa == 0) { + return (Error.PRICE_ERROR, 0, 0); + } + vars.oraclePrice = Exp({mantissa: vars.oraclePriceMantissa}); + + // Pre-compute a conversion factor from tokens -> ether (normalized price value) + (mErr, vars.tokensToEther) = mulExp3(vars.collateralFactor, vars.exchangeRate, vars.oraclePrice); + if (mErr != MathError.NO_ERROR) { + return (Error.MATH_ERROR, 0, 0); + } + + // sumCollateral += tokensToEther * cTokenBalance + (mErr, vars.sumCollateral) = mulScalarTruncateAddUInt(vars.tokensToEther, vars.cTokenBalance, vars.sumCollateral); + if (mErr != MathError.NO_ERROR) { + return (Error.MATH_ERROR, 0, 0); + } + + // sumBorrowPlusEffects += oraclePrice * borrowBalance + (mErr, vars.sumBorrowPlusEffects) = mulScalarTruncateAddUInt(vars.oraclePrice, vars.borrowBalance, vars.sumBorrowPlusEffects); + if (mErr != MathError.NO_ERROR) { + return (Error.MATH_ERROR, 0, 0); + } + + // Calculate effects of interacting with cTokenModify + if (asset == cTokenModify) { + // redeem effect + // sumBorrowPlusEffects += tokensToEther * redeemTokens + (mErr, vars.sumBorrowPlusEffects) = mulScalarTruncateAddUInt(vars.tokensToEther, redeemTokens, vars.sumBorrowPlusEffects); + if (mErr != MathError.NO_ERROR) { + return (Error.MATH_ERROR, 0, 0); + } + + // borrow effect + // sumBorrowPlusEffects += oraclePrice * borrowAmount + (mErr, vars.sumBorrowPlusEffects) = mulScalarTruncateAddUInt(vars.oraclePrice, borrowAmount, vars.sumBorrowPlusEffects); + if (mErr != MathError.NO_ERROR) { + return (Error.MATH_ERROR, 0, 0); + } + } + } + + // These are safe, as the underflow condition is checked first + if (vars.sumCollateral > vars.sumBorrowPlusEffects) { + return (Error.NO_ERROR, vars.sumCollateral - vars.sumBorrowPlusEffects, 0); + } else { + return (Error.NO_ERROR, 0, vars.sumBorrowPlusEffects - vars.sumCollateral); + } + } + + /** + * @notice Calculate number of tokens of collateral asset to seize given an underlying amount + * @dev Used in liquidation (called in cToken.liquidateBorrowFresh) + * @param cTokenBorrowed The address of the borrowed cToken + * @param cTokenCollateral The address of the collateral cToken + * @param actualRepayAmount The amount of cTokenBorrowed underlying to convert into cTokenCollateral tokens + * @return (errorCode, number of cTokenCollateral tokens to be seized in a liquidation) + */ + function liquidateCalculateSeizeTokens(address cTokenBorrowed, address cTokenCollateral, uint actualRepayAmount) + override external view returns (uint, uint) { + /* Read oracle prices for borrowed and collateral markets */ + uint priceBorrowedMantissa = oracle.getUnderlyingPrice(CToken(cTokenBorrowed)); + uint priceCollateralMantissa = oracle.getUnderlyingPrice(CToken(cTokenCollateral)); + if (priceBorrowedMantissa == 0 || priceCollateralMantissa == 0) { + return (uint(Error.PRICE_ERROR), 0); + } + + /* + * Get the exchange rate and calculate the number of collateral tokens to seize: + * seizeAmount = actualRepayAmount * liquidationIncentive * priceBorrowed / priceCollateral + * seizeTokens = seizeAmount / exchangeRate + * = actualRepayAmount * (liquidationIncentive * priceBorrowed) / (priceCollateral * exchangeRate) + */ + uint exchangeRateMantissa = CToken(cTokenCollateral).exchangeRateStored(); // Note: reverts on error + uint seizeTokens; + Exp memory numerator; + Exp memory denominator; + Exp memory ratio; + MathError mathErr; + + (mathErr, numerator) = mulExp(liquidationIncentiveMantissa, priceBorrowedMantissa); + if (mathErr != MathError.NO_ERROR) { + return (uint(Error.MATH_ERROR), 0); + } + + (mathErr, denominator) = mulExp(priceCollateralMantissa, exchangeRateMantissa); + if (mathErr != MathError.NO_ERROR) { + return (uint(Error.MATH_ERROR), 0); + } + + (mathErr, ratio) = divExp(numerator, denominator); + if (mathErr != MathError.NO_ERROR) { + return (uint(Error.MATH_ERROR), 0); + } + + (mathErr, seizeTokens) = mulScalarTruncate(ratio, actualRepayAmount); + if (mathErr != MathError.NO_ERROR) { + return (uint(Error.MATH_ERROR), 0); + } + + return (uint(Error.NO_ERROR), seizeTokens); + } + + /*** Admin Functions ***/ + + /** + * @notice Sets a new price oracle for the comptroller + * @dev Admin function to set a new price oracle + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function _setPriceOracle(PriceOracle newOracle) public returns (uint) { + // Check caller is admin + if (msg.sender != admin) { + return fail(Error.UNAUTHORIZED, FailureInfo.SET_PRICE_ORACLE_OWNER_CHECK); + } + + // Track the old oracle for the comptroller + PriceOracle oldOracle = oracle; + + // Set comptroller's oracle to newOracle + oracle = newOracle; + + // Emit NewPriceOracle(oldOracle, newOracle) + emit NewPriceOracle(oldOracle, newOracle); + + return uint(Error.NO_ERROR); + } + + /** + * @notice Sets the closeFactor used when liquidating borrows + * @dev Admin function to set closeFactor + * @param newCloseFactorMantissa New close factor, scaled by 1e18 + * @return uint 0=success, otherwise a failure. (See ErrorReporter for details) + */ + function _setCloseFactor(uint newCloseFactorMantissa) external returns (uint256) { + // Check caller is admin + if (msg.sender != admin) { + return fail(Error.UNAUTHORIZED, FailureInfo.SET_CLOSE_FACTOR_OWNER_CHECK); + } + + Exp memory newCloseFactorExp = Exp({mantissa: newCloseFactorMantissa}); + Exp memory lowLimit = Exp({mantissa: closeFactorMinMantissa}); + if (lessThanOrEqualExp(newCloseFactorExp, lowLimit)) { + return fail(Error.INVALID_CLOSE_FACTOR, FailureInfo.SET_CLOSE_FACTOR_VALIDATION); + } + + Exp memory highLimit = Exp({mantissa: closeFactorMaxMantissa}); + if (lessThanExp(highLimit, newCloseFactorExp)) { + return fail(Error.INVALID_CLOSE_FACTOR, FailureInfo.SET_CLOSE_FACTOR_VALIDATION); + } + + uint oldCloseFactorMantissa = closeFactorMantissa; + closeFactorMantissa = newCloseFactorMantissa; + emit NewCloseFactor(oldCloseFactorMantissa, closeFactorMantissa); + + return uint(Error.NO_ERROR); + } + + /** + * @notice Sets the collateralFactor for a market + * @dev Admin function to set per-market collateralFactor + * @param cToken The market to set the factor on + * @param newCollateralFactorMantissa The new collateral factor, scaled by 1e18 + * @return uint 0=success, otherwise a failure. (See ErrorReporter for details) + */ + function _setCollateralFactor(CToken cToken, uint newCollateralFactorMantissa) external returns (uint256) { + // Check caller is admin + if (msg.sender != admin) { + return fail(Error.UNAUTHORIZED, FailureInfo.SET_COLLATERAL_FACTOR_OWNER_CHECK); + } + + // Verify market is listed + Market storage market = markets[address(cToken)]; + if (!market.isListed) { + return fail(Error.MARKET_NOT_LISTED, FailureInfo.SET_COLLATERAL_FACTOR_NO_EXISTS); + } + + Exp memory newCollateralFactorExp = Exp({mantissa: newCollateralFactorMantissa}); + + // Check collateral factor <= 0.9 + Exp memory highLimit = Exp({mantissa: collateralFactorMaxMantissa}); + if (lessThanExp(highLimit, newCollateralFactorExp)) { + return fail(Error.INVALID_COLLATERAL_FACTOR, FailureInfo.SET_COLLATERAL_FACTOR_VALIDATION); + } + + // If collateral factor != 0, fail if price == 0 + if (newCollateralFactorMantissa != 0 && oracle.getUnderlyingPrice(cToken) == 0) { + return fail(Error.PRICE_ERROR, FailureInfo.SET_COLLATERAL_FACTOR_WITHOUT_PRICE); + } + + // Set market's collateral factor to new collateral factor, remember old value + uint oldCollateralFactorMantissa = market.collateralFactorMantissa; + market.collateralFactorMantissa = newCollateralFactorMantissa; + + // Emit event with asset, old collateral factor, and new collateral factor + emit NewCollateralFactor(cToken, oldCollateralFactorMantissa, newCollateralFactorMantissa); + + return uint(Error.NO_ERROR); + } + + /** + * @notice Sets maxAssets which controls how many markets can be entered + * @dev Admin function to set maxAssets + * @param newMaxAssets New max assets + * @return uint 0=success, otherwise a failure. (See ErrorReporter for details) + */ + function _setMaxAssets(uint newMaxAssets) external returns (uint) { + // Check caller is admin + if (msg.sender != admin) { + return fail(Error.UNAUTHORIZED, FailureInfo.SET_MAX_ASSETS_OWNER_CHECK); + } + + uint oldMaxAssets = maxAssets; + maxAssets = newMaxAssets; + emit NewMaxAssets(oldMaxAssets, newMaxAssets); + + return uint(Error.NO_ERROR); + } + + /** + * @notice Sets liquidationIncentive + * @dev Admin function to set liquidationIncentive + * @param newLiquidationIncentiveMantissa New liquidationIncentive scaled by 1e18 + * @return uint 0=success, otherwise a failure. (See ErrorReporter for details) + */ + function _setLiquidationIncentive(uint newLiquidationIncentiveMantissa) external returns (uint) { + // Check caller is admin + if (msg.sender != admin) { + return fail(Error.UNAUTHORIZED, FailureInfo.SET_LIQUIDATION_INCENTIVE_OWNER_CHECK); + } + + // Check de-scaled min <= newLiquidationIncentive <= max + Exp memory newLiquidationIncentive = Exp({mantissa: newLiquidationIncentiveMantissa}); + Exp memory minLiquidationIncentive = Exp({mantissa: liquidationIncentiveMinMantissa}); + if (lessThanExp(newLiquidationIncentive, minLiquidationIncentive)) { + return fail(Error.INVALID_LIQUIDATION_INCENTIVE, FailureInfo.SET_LIQUIDATION_INCENTIVE_VALIDATION); + } + + Exp memory maxLiquidationIncentive = Exp({mantissa: liquidationIncentiveMaxMantissa}); + if (lessThanExp(maxLiquidationIncentive, newLiquidationIncentive)) { + return fail(Error.INVALID_LIQUIDATION_INCENTIVE, FailureInfo.SET_LIQUIDATION_INCENTIVE_VALIDATION); + } + + // Save current value for use in log + uint oldLiquidationIncentiveMantissa = liquidationIncentiveMantissa; + + // Set liquidation incentive to new incentive + liquidationIncentiveMantissa = newLiquidationIncentiveMantissa; + + // Emit event with old incentive, new incentive + emit NewLiquidationIncentive(oldLiquidationIncentiveMantissa, newLiquidationIncentiveMantissa); + + return uint(Error.NO_ERROR); + } + + /** + * @notice Add the market to the markets mapping and set it as listed + * @dev Admin function to set isListed and add support for the market + * @param cToken The address of the market (token) to list + * @return uint 0=success, otherwise a failure. (See enum Error for details) + */ + function _supportMarket(CToken cToken) external returns (uint) { + if (msg.sender != admin) { + return fail(Error.UNAUTHORIZED, FailureInfo.SUPPORT_MARKET_OWNER_CHECK); + } + + if (markets[address(cToken)].isListed) { + return fail(Error.MARKET_ALREADY_LISTED, FailureInfo.SUPPORT_MARKET_EXISTS); + } + + cToken.isCToken(); // Sanity check to make sure its really a CToken + + markets[address(cToken)] = Market({isListed: true, isComped: false, collateralFactorMantissa: 0}); + emit MarketListed(cToken); + + return uint(Error.NO_ERROR); + } + + /** + * @notice Admin function to change the Pause Guardian + * @param newPauseGuardian The address of the new Pause Guardian + * @return uint 0=success, otherwise a failure. (See enum Error for details) + */ + function _setPauseGuardian(address newPauseGuardian) public returns (uint) { + if (msg.sender != admin) { + return fail(Error.UNAUTHORIZED, FailureInfo.SET_PAUSE_GUARDIAN_OWNER_CHECK); + } + + // Save current value for inclusion in log + address oldPauseGuardian = pauseGuardian; + + // Store pauseGuardian with value newPauseGuardian + pauseGuardian = newPauseGuardian; + + // Emit NewPauseGuardian(OldPauseGuardian, NewPauseGuardian) + emit NewPauseGuardian(oldPauseGuardian, pauseGuardian); + + return uint(Error.NO_ERROR); + } + + function _setMintPaused(CToken cToken, bool state) public returns (bool) { + require(markets[address(cToken)].isListed, "cannot pause a market that is not listed"); + require(msg.sender == pauseGuardian || msg.sender == admin, "only pause guardian and admin can pause"); + require(msg.sender == admin || state == true, "only admin can unpause"); + + mintGuardianPaused[address(cToken)] = state; + emit ActionPaused(cToken, "Mint", state); + return state; + } + + function _setBorrowPaused(CToken cToken, bool state) public returns (bool) { + require(markets[address(cToken)].isListed, "cannot pause a market that is not listed"); + require(msg.sender == pauseGuardian || msg.sender == admin, "only pause guardian and admin can pause"); + require(msg.sender == admin || state == true, "only admin can unpause"); + + borrowGuardianPaused[address(cToken)] = state; + emit ActionPaused(cToken, "Borrow", state); + return state; + } + + function _setTransferPaused(bool state) public returns (bool) { + require(msg.sender == pauseGuardian || msg.sender == admin, "only pause guardian and admin can pause"); + require(msg.sender == admin || state == true, "only admin can unpause"); + + transferGuardianPaused = state; + emit ActionPaused("Transfer", state); + return state; + } + + function _setSeizePaused(bool state) public returns (bool) { + require(msg.sender == pauseGuardian || msg.sender == admin, "only pause guardian and admin can pause"); + require(msg.sender == admin || state == true, "only admin can unpause"); + + seizeGuardianPaused = state; + emit ActionPaused("Seize", state); + return state; + } + + function _become(Unitroller unitroller) public { + require(msg.sender == unitroller.admin(), "only unitroller admin can change brains"); + + uint changeStatus = unitroller._acceptImplementation(); + require(changeStatus == 0, "change not authorized"); + } +} diff --git a/src/integrations/compound/ComptrollerInterface.sol b/src/integrations/compound/ComptrollerInterface.sol index 363fc7e..8a1bed8 100644 --- a/src/integrations/compound/ComptrollerInterface.sol +++ b/src/integrations/compound/ComptrollerInterface.sol @@ -1,71 +1,71 @@ -pragma solidity ^0.6.7; - -abstract contract ComptrollerInterface { - /// @notice Indicator that this is a Comptroller contract (for inspection) - bool public constant isComptroller = true; - - /*** Assets You Are In ***/ - - function enterMarkets(address[] calldata cTokens) virtual external returns (uint[] memory) {} - function exitMarket(address cToken) virtual external returns (uint) {} - - /*** Policy Hooks ***/ - - function mintAllowed(address cToken, address minter, uint mintAmount) virtual external returns (uint) {} - function mintVerify(address cToken, address minter, uint mintAmount, uint mintTokens) virtual external {} - - function redeemAllowed(address cToken, address redeemer, uint redeemTokens) virtual external returns (uint) {} - function redeemVerify(address cToken, address redeemer, uint redeemAmount, uint redeemTokens) virtual external {} - - function borrowAllowed(address cToken, address borrower, uint borrowAmount) virtual external returns (uint) {} - function borrowVerify(address cToken, address borrower, uint borrowAmount) virtual external {} - - function repayBorrowAllowed( - address cToken, - address payer, - address borrower, - uint repayAmount) virtual external returns (uint) {} - function repayBorrowVerify( - address cToken, - address payer, - address borrower, - uint repayAmount, - uint borrowerIndex) virtual external {} - - function liquidateBorrowAllowed( - address cTokenBorrowed, - address cTokenCollateral, - address liquidator, - address borrower, - uint repayAmount) virtual external returns (uint) {} - function liquidateBorrowVerify( - address cTokenBorrowed, - address cTokenCollateral, - address liquidator, - address borrower, - uint repayAmount, - uint seizeTokens) virtual external {} - - function seizeAllowed( - address cTokenCollateral, - address cTokenBorrowed, - address liquidator, - address borrower, - uint seizeTokens) virtual external returns (uint) {} - function seizeVerify( - address cTokenCollateral, - address cTokenBorrowed, - address liquidator, - address borrower, - uint seizeTokens) virtual external {} - - function transferAllowed(address cToken, address src, address dst, uint transferTokens) virtual external returns (uint) {} - function transferVerify(address cToken, address src, address dst, uint transferTokens) virtual external {} - - /*** Liquidity/Liquidation Calculations ***/ - - function liquidateCalculateSeizeTokens( - address cTokenBorrowed, - address cTokenCollateral, - uint repayAmount) virtual external view returns (uint, uint) {} -} +pragma solidity ^0.6.7; + +abstract contract ComptrollerInterface { + /// @notice Indicator that this is a Comptroller contract (for inspection) + bool public constant isComptroller = true; + + /*** Assets You Are In ***/ + + function enterMarkets(address[] calldata cTokens) virtual external returns (uint[] memory) {} + function exitMarket(address cToken) virtual external returns (uint) {} + + /*** Policy Hooks ***/ + + function mintAllowed(address cToken, address minter, uint mintAmount) virtual external returns (uint) {} + function mintVerify(address cToken, address minter, uint mintAmount, uint mintTokens) virtual external {} + + function redeemAllowed(address cToken, address redeemer, uint redeemTokens) virtual external returns (uint) {} + function redeemVerify(address cToken, address redeemer, uint redeemAmount, uint redeemTokens) virtual external {} + + function borrowAllowed(address cToken, address borrower, uint borrowAmount) virtual external returns (uint) {} + function borrowVerify(address cToken, address borrower, uint borrowAmount) virtual external {} + + function repayBorrowAllowed( + address cToken, + address payer, + address borrower, + uint repayAmount) virtual external returns (uint) {} + function repayBorrowVerify( + address cToken, + address payer, + address borrower, + uint repayAmount, + uint borrowerIndex) virtual external {} + + function liquidateBorrowAllowed( + address cTokenBorrowed, + address cTokenCollateral, + address liquidator, + address borrower, + uint repayAmount) virtual external returns (uint) {} + function liquidateBorrowVerify( + address cTokenBorrowed, + address cTokenCollateral, + address liquidator, + address borrower, + uint repayAmount, + uint seizeTokens) virtual external {} + + function seizeAllowed( + address cTokenCollateral, + address cTokenBorrowed, + address liquidator, + address borrower, + uint seizeTokens) virtual external returns (uint) {} + function seizeVerify( + address cTokenCollateral, + address cTokenBorrowed, + address liquidator, + address borrower, + uint seizeTokens) virtual external {} + + function transferAllowed(address cToken, address src, address dst, uint transferTokens) virtual external returns (uint) {} + function transferVerify(address cToken, address src, address dst, uint transferTokens) virtual external {} + + /*** Liquidity/Liquidation Calculations ***/ + + function liquidateCalculateSeizeTokens( + address cTokenBorrowed, + address cTokenCollateral, + uint repayAmount) virtual external view returns (uint, uint) {} +} diff --git a/src/integrations/compound/ComptrollerStorage.sol b/src/integrations/compound/ComptrollerStorage.sol index 70bea5d..cdc2e76 100644 --- a/src/integrations/compound/ComptrollerStorage.sol +++ b/src/integrations/compound/ComptrollerStorage.sol @@ -1,145 +1,145 @@ -pragma solidity ^0.6.7; - -import "./CToken.sol"; -import "./PriceOracle.sol"; - -contract UnitrollerAdminStorage { - /** - * @notice Administrator for this contract - */ - address public admin; - - /** - * @notice Pending administrator for this contract - */ - address public pendingAdmin; - - /** - * @notice Active brains of Unitroller - */ - address public comptrollerImplementation; - - /** - * @notice Pending brains of Unitroller - */ - address public pendingComptrollerImplementation; -} - -contract ComptrollerV1Storage is UnitrollerAdminStorage { - - /** - * @notice Oracle which gives the price of any given asset - */ - PriceOracle public oracle; - - /** - * @notice Multiplier used to calculate the maximum repayAmount when liquidating a borrow - */ - uint public closeFactorMantissa; - - /** - * @notice Multiplier representing the discount on collateral that a liquidator receives - */ - uint public liquidationIncentiveMantissa; - - /** - * @notice Max number of assets a single account can participate in (borrow or use as collateral) - */ - uint public maxAssets; - - /** - * @notice Per-account mapping of "assets you are in", capped by maxAssets - */ - mapping(address => CToken[]) public accountAssets; - -} - -contract ComptrollerV2Storage is ComptrollerV1Storage { - struct Market { - /// @notice Whether or not this market is listed - bool isListed; - - /** - * @notice Multiplier representing the most one can borrow against their collateral in this market. - * For instance, 0.9 to allow borrowing 90% of collateral value. - * Must be between 0 and 1, and stored as a mantissa. - */ - uint collateralFactorMantissa; - - /// @notice Per-market mapping of "accounts in this asset" - mapping(address => bool) accountMembership; - - /// @notice Whether or not this market receives COMP - bool isComped; - } - - /** - * @notice Official mapping of cTokens -> Market metadata - * @dev Used e.g. to determine if a market is supported - */ - mapping(address => Market) public markets; - - - /** - * @notice The Pause Guardian can pause certain actions as a safety mechanism. - * Actions which allow users to remove their own assets cannot be paused. - * Liquidation / seizing / transfer can only be paused globally, not by market. - */ - address public pauseGuardian; - bool public _mintGuardianPaused; - bool public _borrowGuardianPaused; - bool public transferGuardianPaused; - bool public seizeGuardianPaused; - mapping(address => bool) public mintGuardianPaused; - mapping(address => bool) public borrowGuardianPaused; -} - -contract ComptrollerV3Storage is ComptrollerV2Storage { - struct CompMarketState { - /// @notice The market's last updated compBorrowIndex or compSupplyIndex - uint224 index; - - /// @notice The block number the index was last updated at - uint32 block; - } - - /// @notice A list of all markets - CToken[] public allMarkets; - - /// @notice The rate at which the flywheel distributes COMP, per block - uint public compRate; - - /// @notice The portion of compRate that each market currently receives - mapping(address => uint) public compSpeeds; - - /// @notice The COMP market supply state for each market - mapping(address => CompMarketState) public compSupplyState; - - /// @notice The COMP market borrow state for each market - mapping(address => CompMarketState) public compBorrowState; - - /// @notice The COMP borrow index for each market for each supplier as of the last time they accrued COMP - mapping(address => mapping(address => uint)) public compSupplierIndex; - - /// @notice The COMP borrow index for each market for each borrower as of the last time they accrued COMP - mapping(address => mapping(address => uint)) public compBorrowerIndex; - - /// @notice The COMP accrued but not yet transferred to each user - mapping(address => uint) public compAccrued; -} - -contract ComptrollerV4Storage is ComptrollerV3Storage { - // @notice The borrowCapGuardian can set borrowCaps to any number for any market. Lowering the borrow cap could disable borrowing on the given market. - address public borrowCapGuardian; - - // @notice Borrow caps enforced by borrowAllowed for each cToken address. Defaults to zero which corresponds to unlimited borrowing. - mapping(address => uint) public borrowCaps; -} - -contract ComptrollerV5Storage is ComptrollerV4Storage { - /// @notice The portion of COMP that each contributor receives per block - mapping(address => uint) public compContributorSpeeds; - - /// @notice Last block at which a contributor's COMP rewards have been allocated - mapping(address => uint) public lastContributorBlock; -} +pragma solidity ^0.6.7; + +import "./CToken.sol"; +import "./PriceOracle.sol"; + +contract UnitrollerAdminStorage { + /** + * @notice Administrator for this contract + */ + address public admin; + + /** + * @notice Pending administrator for this contract + */ + address public pendingAdmin; + + /** + * @notice Active brains of Unitroller + */ + address public comptrollerImplementation; + + /** + * @notice Pending brains of Unitroller + */ + address public pendingComptrollerImplementation; +} + +contract ComptrollerV1Storage is UnitrollerAdminStorage { + + /** + * @notice Oracle which gives the price of any given asset + */ + PriceOracle public oracle; + + /** + * @notice Multiplier used to calculate the maximum repayAmount when liquidating a borrow + */ + uint public closeFactorMantissa; + + /** + * @notice Multiplier representing the discount on collateral that a liquidator receives + */ + uint public liquidationIncentiveMantissa; + + /** + * @notice Max number of assets a single account can participate in (borrow or use as collateral) + */ + uint public maxAssets; + + /** + * @notice Per-account mapping of "assets you are in", capped by maxAssets + */ + mapping(address => CToken[]) public accountAssets; + +} + +contract ComptrollerV2Storage is ComptrollerV1Storage { + struct Market { + /// @notice Whether or not this market is listed + bool isListed; + + /** + * @notice Multiplier representing the most one can borrow against their collateral in this market. + * For instance, 0.9 to allow borrowing 90% of collateral value. + * Must be between 0 and 1, and stored as a mantissa. + */ + uint collateralFactorMantissa; + + /// @notice Per-market mapping of "accounts in this asset" + mapping(address => bool) accountMembership; + + /// @notice Whether or not this market receives COMP + bool isComped; + } + + /** + * @notice Official mapping of cTokens -> Market metadata + * @dev Used e.g. to determine if a market is supported + */ + mapping(address => Market) public markets; + + + /** + * @notice The Pause Guardian can pause certain actions as a safety mechanism. + * Actions which allow users to remove their own assets cannot be paused. + * Liquidation / seizing / transfer can only be paused globally, not by market. + */ + address public pauseGuardian; + bool public _mintGuardianPaused; + bool public _borrowGuardianPaused; + bool public transferGuardianPaused; + bool public seizeGuardianPaused; + mapping(address => bool) public mintGuardianPaused; + mapping(address => bool) public borrowGuardianPaused; +} + +contract ComptrollerV3Storage is ComptrollerV2Storage { + struct CompMarketState { + /// @notice The market's last updated compBorrowIndex or compSupplyIndex + uint224 index; + + /// @notice The block number the index was last updated at + uint32 block; + } + + /// @notice A list of all markets + CToken[] public allMarkets; + + /// @notice The rate at which the flywheel distributes COMP, per block + uint public compRate; + + /// @notice The portion of compRate that each market currently receives + mapping(address => uint) public compSpeeds; + + /// @notice The COMP market supply state for each market + mapping(address => CompMarketState) public compSupplyState; + + /// @notice The COMP market borrow state for each market + mapping(address => CompMarketState) public compBorrowState; + + /// @notice The COMP borrow index for each market for each supplier as of the last time they accrued COMP + mapping(address => mapping(address => uint)) public compSupplierIndex; + + /// @notice The COMP borrow index for each market for each borrower as of the last time they accrued COMP + mapping(address => mapping(address => uint)) public compBorrowerIndex; + + /// @notice The COMP accrued but not yet transferred to each user + mapping(address => uint) public compAccrued; +} + +contract ComptrollerV4Storage is ComptrollerV3Storage { + // @notice The borrowCapGuardian can set borrowCaps to any number for any market. Lowering the borrow cap could disable borrowing on the given market. + address public borrowCapGuardian; + + // @notice Borrow caps enforced by borrowAllowed for each cToken address. Defaults to zero which corresponds to unlimited borrowing. + mapping(address => uint) public borrowCaps; +} + +contract ComptrollerV5Storage is ComptrollerV4Storage { + /// @notice The portion of COMP that each contributor receives per block + mapping(address => uint) public compContributorSpeeds; + + /// @notice Last block at which a contributor's COMP rewards have been allocated + mapping(address => uint) public lastContributorBlock; +} diff --git a/src/integrations/compound/EIP20Interface.sol b/src/integrations/compound/EIP20Interface.sol index 41a4d5a..876f0eb 100644 --- a/src/integrations/compound/EIP20Interface.sol +++ b/src/integrations/compound/EIP20Interface.sol @@ -1,62 +1,62 @@ -pragma solidity ^0.6.7; - -/** - * @title ERC 20 Token Standard Interface - * https://eips.ethereum.org/EIPS/eip-20 - */ -interface EIP20Interface { - function name() external view returns (string memory); - function symbol() external view returns (string memory); - function decimals() external view returns (uint8); - - /** - * @notice Get the total number of tokens in circulation - * @return The supply of tokens - */ - function totalSupply() external view returns (uint256); - - /** - * @notice Gets the balance of the specified address - * @param owner The address from which the balance will be retrieved - * @return balance The balance - */ - function balanceOf(address owner) external view returns (uint256 balance); - - /** - * @notice Transfer `amount` tokens from `msg.sender` to `dst` - * @param dst The address of the destination account - * @param amount The number of tokens to transfer - * @return success Whether or not the transfer succeeded - */ - function transfer(address dst, uint256 amount) external returns (bool success); - - /** - * @notice Transfer `amount` tokens from `src` to `dst` - * @param src The address of the source account - * @param dst The address of the destination account - * @param amount The number of tokens to transfer - * @return success Whether or not the transfer succeeded - */ - function transferFrom(address src, address dst, uint256 amount) external returns (bool success); - - /** - * @notice Approve `spender` to transfer up to `amount` from `src` - * @dev This will overwrite the approval amount for `spender` - * and is subject to issues noted [here](https://eips.ethereum.org/EIPS/eip-20#approve) - * @param spender The address of the account which may transfer tokens - * @param amount The number of tokens that are approved (-1 means infinite) - * @return success Whether or not the approval succeeded - */ - function approve(address spender, uint256 amount) external returns (bool success); - - /** - * @notice Get the current allowance from `owner` for `spender` - * @param owner The address of the account which owns the tokens to be spent - * @param spender The address of the account which may transfer tokens - * @return remaining The number of tokens allowed to be spent (-1 means infinite) - */ - function allowance(address owner, address spender) external view returns (uint256 remaining); - - event Transfer(address indexed from, address indexed to, uint256 amount); - event Approval(address indexed owner, address indexed spender, uint256 amount); -} +pragma solidity ^0.6.7; + +/** + * @title ERC 20 Token Standard Interface + * https://eips.ethereum.org/EIPS/eip-20 + */ +interface EIP20Interface { + function name() external view returns (string memory); + function symbol() external view returns (string memory); + function decimals() external view returns (uint8); + + /** + * @notice Get the total number of tokens in circulation + * @return The supply of tokens + */ + function totalSupply() external view returns (uint256); + + /** + * @notice Gets the balance of the specified address + * @param owner The address from which the balance will be retrieved + * @return balance The balance + */ + function balanceOf(address owner) external view returns (uint256 balance); + + /** + * @notice Transfer `amount` tokens from `msg.sender` to `dst` + * @param dst The address of the destination account + * @param amount The number of tokens to transfer + * @return success Whether or not the transfer succeeded + */ + function transfer(address dst, uint256 amount) external returns (bool success); + + /** + * @notice Transfer `amount` tokens from `src` to `dst` + * @param src The address of the source account + * @param dst The address of the destination account + * @param amount The number of tokens to transfer + * @return success Whether or not the transfer succeeded + */ + function transferFrom(address src, address dst, uint256 amount) external returns (bool success); + + /** + * @notice Approve `spender` to transfer up to `amount` from `src` + * @dev This will overwrite the approval amount for `spender` + * and is subject to issues noted [here](https://eips.ethereum.org/EIPS/eip-20#approve) + * @param spender The address of the account which may transfer tokens + * @param amount The number of tokens that are approved (-1 means infinite) + * @return success Whether or not the approval succeeded + */ + function approve(address spender, uint256 amount) external returns (bool success); + + /** + * @notice Get the current allowance from `owner` for `spender` + * @param owner The address of the account which owns the tokens to be spent + * @param spender The address of the account which may transfer tokens + * @return remaining The number of tokens allowed to be spent (-1 means infinite) + */ + function allowance(address owner, address spender) external view returns (uint256 remaining); + + event Transfer(address indexed from, address indexed to, uint256 amount); + event Approval(address indexed owner, address indexed spender, uint256 amount); +} diff --git a/src/integrations/compound/EIP20NonStandardInterface.sol b/src/integrations/compound/EIP20NonStandardInterface.sol index 53cad5b..b316c6e 100644 --- a/src/integrations/compound/EIP20NonStandardInterface.sol +++ b/src/integrations/compound/EIP20NonStandardInterface.sol @@ -1,70 +1,70 @@ -pragma solidity ^0.6.7; - -/** - * @title EIP20NonStandardInterface - * @dev Version of ERC20 with no return values for `transfer` and `transferFrom` - * See https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca - */ -interface EIP20NonStandardInterface { - - /** - * @notice Get the total number of tokens in circulation - * @return The supply of tokens - */ - function totalSupply() external view returns (uint256); - - /** - * @notice Gets the balance of the specified address - * @param owner The address from which the balance will be retrieved - * @return balance The balance - */ - function balanceOf(address owner) external view returns (uint256 balance); - - /// - /// !!!!!!!!!!!!!! - /// !!! NOTICE !!! `transfer` does not return a value, in violation of the ERC-20 specification - /// !!!!!!!!!!!!!! - /// - - /** - * @notice Transfer `amount` tokens from `msg.sender` to `dst` - * @param dst The address of the destination account - * @param amount The number of tokens to transfer - */ - function transfer(address dst, uint256 amount) external; - - /// - /// !!!!!!!!!!!!!! - /// !!! NOTICE !!! `transferFrom` does not return a value, in violation of the ERC-20 specification - /// !!!!!!!!!!!!!! - /// - - /** - * @notice Transfer `amount` tokens from `src` to `dst` - * @param src The address of the source account - * @param dst The address of the destination account - * @param amount The number of tokens to transfer - */ - function transferFrom(address src, address dst, uint256 amount) external; - - /** - * @notice Approve `spender` to transfer up to `amount` from `src` - * @dev This will overwrite the approval amount for `spender` - * and is subject to issues noted [here](https://eips.ethereum.org/EIPS/eip-20#approve) - * @param spender The address of the account which may transfer tokens - * @param amount The number of tokens that are approved - * @return success Whether or not the approval succeeded - */ - function approve(address spender, uint256 amount) external returns (bool success); - - /** - * @notice Get the current allowance from `owner` for `spender` - * @param owner The address of the account which owns the tokens to be spent - * @param spender The address of the account which may transfer tokens - * @return remaining The number of tokens allowed to be spent - */ - function allowance(address owner, address spender) external view returns (uint256 remaining); - - event Transfer(address indexed from, address indexed to, uint256 amount); - event Approval(address indexed owner, address indexed spender, uint256 amount); -} +pragma solidity ^0.6.7; + +/** + * @title EIP20NonStandardInterface + * @dev Version of ERC20 with no return values for `transfer` and `transferFrom` + * See https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca + */ +interface EIP20NonStandardInterface { + + /** + * @notice Get the total number of tokens in circulation + * @return The supply of tokens + */ + function totalSupply() external view returns (uint256); + + /** + * @notice Gets the balance of the specified address + * @param owner The address from which the balance will be retrieved + * @return balance The balance + */ + function balanceOf(address owner) external view returns (uint256 balance); + + /// + /// !!!!!!!!!!!!!! + /// !!! NOTICE !!! `transfer` does not return a value, in violation of the ERC-20 specification + /// !!!!!!!!!!!!!! + /// + + /** + * @notice Transfer `amount` tokens from `msg.sender` to `dst` + * @param dst The address of the destination account + * @param amount The number of tokens to transfer + */ + function transfer(address dst, uint256 amount) external; + + /// + /// !!!!!!!!!!!!!! + /// !!! NOTICE !!! `transferFrom` does not return a value, in violation of the ERC-20 specification + /// !!!!!!!!!!!!!! + /// + + /** + * @notice Transfer `amount` tokens from `src` to `dst` + * @param src The address of the source account + * @param dst The address of the destination account + * @param amount The number of tokens to transfer + */ + function transferFrom(address src, address dst, uint256 amount) external; + + /** + * @notice Approve `spender` to transfer up to `amount` from `src` + * @dev This will overwrite the approval amount for `spender` + * and is subject to issues noted [here](https://eips.ethereum.org/EIPS/eip-20#approve) + * @param spender The address of the account which may transfer tokens + * @param amount The number of tokens that are approved + * @return success Whether or not the approval succeeded + */ + function approve(address spender, uint256 amount) external returns (bool success); + + /** + * @notice Get the current allowance from `owner` for `spender` + * @param owner The address of the account which owns the tokens to be spent + * @param spender The address of the account which may transfer tokens + * @return remaining The number of tokens allowed to be spent + */ + function allowance(address owner, address spender) external view returns (uint256 remaining); + + event Transfer(address indexed from, address indexed to, uint256 amount); + event Approval(address indexed owner, address indexed spender, uint256 amount); +} diff --git a/src/integrations/compound/ErrorReporter.sol b/src/integrations/compound/ErrorReporter.sol index 8970e84..1889ecc 100644 --- a/src/integrations/compound/ErrorReporter.sol +++ b/src/integrations/compound/ErrorReporter.sol @@ -1,207 +1,207 @@ -pragma solidity ^0.6.7; - -contract ComptrollerErrorReporter { - enum Error { - NO_ERROR, - UNAUTHORIZED, - COMPTROLLER_MISMATCH, - INSUFFICIENT_SHORTFALL, - INSUFFICIENT_LIQUIDITY, - INVALID_CLOSE_FACTOR, - INVALID_COLLATERAL_FACTOR, - INVALID_LIQUIDATION_INCENTIVE, - MARKET_NOT_ENTERED, // no longer possible - MARKET_NOT_LISTED, - MARKET_ALREADY_LISTED, - MATH_ERROR, - NONZERO_BORROW_BALANCE, - PRICE_ERROR, - REJECTION, - SNAPSHOT_ERROR, - TOO_MANY_ASSETS, - TOO_MUCH_REPAY - } - - enum FailureInfo { - ACCEPT_ADMIN_PENDING_ADMIN_CHECK, - ACCEPT_PENDING_IMPLEMENTATION_ADDRESS_CHECK, - EXIT_MARKET_BALANCE_OWED, - EXIT_MARKET_REJECTION, - SET_CLOSE_FACTOR_OWNER_CHECK, - SET_CLOSE_FACTOR_VALIDATION, - SET_COLLATERAL_FACTOR_OWNER_CHECK, - SET_COLLATERAL_FACTOR_NO_EXISTS, - SET_COLLATERAL_FACTOR_VALIDATION, - SET_COLLATERAL_FACTOR_WITHOUT_PRICE, - SET_IMPLEMENTATION_OWNER_CHECK, - SET_LIQUIDATION_INCENTIVE_OWNER_CHECK, - SET_LIQUIDATION_INCENTIVE_VALIDATION, - SET_MAX_ASSETS_OWNER_CHECK, - SET_PENDING_ADMIN_OWNER_CHECK, - SET_PENDING_IMPLEMENTATION_OWNER_CHECK, - SET_PRICE_ORACLE_OWNER_CHECK, - SUPPORT_MARKET_EXISTS, - SUPPORT_MARKET_OWNER_CHECK, - SET_PAUSE_GUARDIAN_OWNER_CHECK - } - - /** - * @dev `error` corresponds to enum Error; `info` corresponds to enum FailureInfo, and `detail` is an arbitrary - * contract-specific code that enables us to report opaque error codes from upgradeable contracts. - **/ - event Failure(uint error, uint info, uint detail); - - /** - * @dev use this when reporting a known error from the money market or a non-upgradeable collaborator - */ - function fail(Error err, FailureInfo info) internal returns (uint) { - emit Failure(uint(err), uint(info), 0); - - return uint(err); - } - - /** - * @dev use this when reporting an opaque error from an upgradeable collaborator contract - */ - function failOpaque(Error err, FailureInfo info, uint opaqueError) internal returns (uint) { - emit Failure(uint(err), uint(info), opaqueError); - - return uint(err); - } -} - -contract TokenErrorReporter { - enum Error { - NO_ERROR, - UNAUTHORIZED, - BAD_INPUT, - COMPTROLLER_REJECTION, - COMPTROLLER_CALCULATION_ERROR, - INTEREST_RATE_MODEL_ERROR, - INVALID_ACCOUNT_PAIR, - INVALID_CLOSE_AMOUNT_REQUESTED, - INVALID_COLLATERAL_FACTOR, - MATH_ERROR, - MARKET_NOT_FRESH, - MARKET_NOT_LISTED, - TOKEN_INSUFFICIENT_ALLOWANCE, - TOKEN_INSUFFICIENT_BALANCE, - TOKEN_INSUFFICIENT_CASH, - TOKEN_TRANSFER_IN_FAILED, - TOKEN_TRANSFER_OUT_FAILED - } - - /* - * Note: FailureInfo (but not Error) is kept in alphabetical order - * This is because FailureInfo grows significantly faster, and - * the order of Error has some meaning, while the order of FailureInfo - * is entirely arbitrary. - */ - enum FailureInfo { - ACCEPT_ADMIN_PENDING_ADMIN_CHECK, - ACCRUE_INTEREST_ACCUMULATED_INTEREST_CALCULATION_FAILED, - ACCRUE_INTEREST_BORROW_RATE_CALCULATION_FAILED, - ACCRUE_INTEREST_NEW_BORROW_INDEX_CALCULATION_FAILED, - ACCRUE_INTEREST_NEW_TOTAL_BORROWS_CALCULATION_FAILED, - ACCRUE_INTEREST_NEW_TOTAL_RESERVES_CALCULATION_FAILED, - ACCRUE_INTEREST_SIMPLE_INTEREST_FACTOR_CALCULATION_FAILED, - BORROW_ACCUMULATED_BALANCE_CALCULATION_FAILED, - BORROW_ACCRUE_INTEREST_FAILED, - BORROW_CASH_NOT_AVAILABLE, - BORROW_FRESHNESS_CHECK, - BORROW_NEW_TOTAL_BALANCE_CALCULATION_FAILED, - BORROW_NEW_ACCOUNT_BORROW_BALANCE_CALCULATION_FAILED, - BORROW_MARKET_NOT_LISTED, - BORROW_COMPTROLLER_REJECTION, - LIQUIDATE_ACCRUE_BORROW_INTEREST_FAILED, - LIQUIDATE_ACCRUE_COLLATERAL_INTEREST_FAILED, - LIQUIDATE_COLLATERAL_FRESHNESS_CHECK, - LIQUIDATE_COMPTROLLER_REJECTION, - LIQUIDATE_COMPTROLLER_CALCULATE_AMOUNT_SEIZE_FAILED, - LIQUIDATE_CLOSE_AMOUNT_IS_UINT_MAX, - LIQUIDATE_CLOSE_AMOUNT_IS_ZERO, - LIQUIDATE_FRESHNESS_CHECK, - LIQUIDATE_LIQUIDATOR_IS_BORROWER, - LIQUIDATE_REPAY_BORROW_FRESH_FAILED, - LIQUIDATE_SEIZE_BALANCE_INCREMENT_FAILED, - LIQUIDATE_SEIZE_BALANCE_DECREMENT_FAILED, - LIQUIDATE_SEIZE_COMPTROLLER_REJECTION, - LIQUIDATE_SEIZE_LIQUIDATOR_IS_BORROWER, - LIQUIDATE_SEIZE_TOO_MUCH, - MINT_ACCRUE_INTEREST_FAILED, - MINT_COMPTROLLER_REJECTION, - MINT_EXCHANGE_CALCULATION_FAILED, - MINT_EXCHANGE_RATE_READ_FAILED, - MINT_FRESHNESS_CHECK, - MINT_NEW_ACCOUNT_BALANCE_CALCULATION_FAILED, - MINT_NEW_TOTAL_SUPPLY_CALCULATION_FAILED, - MINT_TRANSFER_IN_FAILED, - MINT_TRANSFER_IN_NOT_POSSIBLE, - REDEEM_ACCRUE_INTEREST_FAILED, - REDEEM_COMPTROLLER_REJECTION, - REDEEM_EXCHANGE_TOKENS_CALCULATION_FAILED, - REDEEM_EXCHANGE_AMOUNT_CALCULATION_FAILED, - REDEEM_EXCHANGE_RATE_READ_FAILED, - REDEEM_FRESHNESS_CHECK, - REDEEM_NEW_ACCOUNT_BALANCE_CALCULATION_FAILED, - REDEEM_NEW_TOTAL_SUPPLY_CALCULATION_FAILED, - REDEEM_TRANSFER_OUT_NOT_POSSIBLE, - REDUCE_RESERVES_ACCRUE_INTEREST_FAILED, - REDUCE_RESERVES_ADMIN_CHECK, - REDUCE_RESERVES_CASH_NOT_AVAILABLE, - REDUCE_RESERVES_FRESH_CHECK, - REDUCE_RESERVES_VALIDATION, - REPAY_BEHALF_ACCRUE_INTEREST_FAILED, - REPAY_BORROW_ACCRUE_INTEREST_FAILED, - REPAY_BORROW_ACCUMULATED_BALANCE_CALCULATION_FAILED, - REPAY_BORROW_COMPTROLLER_REJECTION, - REPAY_BORROW_FRESHNESS_CHECK, - REPAY_BORROW_NEW_ACCOUNT_BORROW_BALANCE_CALCULATION_FAILED, - REPAY_BORROW_NEW_TOTAL_BALANCE_CALCULATION_FAILED, - REPAY_BORROW_TRANSFER_IN_NOT_POSSIBLE, - SET_COLLATERAL_FACTOR_OWNER_CHECK, - SET_COLLATERAL_FACTOR_VALIDATION, - SET_COMPTROLLER_OWNER_CHECK, - SET_INTEREST_RATE_MODEL_ACCRUE_INTEREST_FAILED, - SET_INTEREST_RATE_MODEL_FRESH_CHECK, - SET_INTEREST_RATE_MODEL_OWNER_CHECK, - SET_MAX_ASSETS_OWNER_CHECK, - SET_ORACLE_MARKET_NOT_LISTED, - SET_PENDING_ADMIN_OWNER_CHECK, - SET_RESERVE_FACTOR_ACCRUE_INTEREST_FAILED, - SET_RESERVE_FACTOR_ADMIN_CHECK, - SET_RESERVE_FACTOR_FRESH_CHECK, - SET_RESERVE_FACTOR_BOUNDS_CHECK, - TRANSFER_COMPTROLLER_REJECTION, - TRANSFER_NOT_ALLOWED, - TRANSFER_NOT_ENOUGH, - TRANSFER_TOO_MUCH, - ADD_RESERVES_ACCRUE_INTEREST_FAILED, - ADD_RESERVES_FRESH_CHECK, - ADD_RESERVES_TRANSFER_IN_NOT_POSSIBLE - } - - /** - * @dev `error` corresponds to enum Error; `info` corresponds to enum FailureInfo, and `detail` is an arbitrary - * contract-specific code that enables us to report opaque error codes from upgradeable contracts. - **/ - event Failure(uint error, uint info, uint detail); - - /** - * @dev use this when reporting a known error from the money market or a non-upgradeable collaborator - */ - function fail(Error err, FailureInfo info) internal returns (uint) { - emit Failure(uint(err), uint(info), 0); - - return uint(err); - } - - /** - * @dev use this when reporting an opaque error from an upgradeable collaborator contract - */ - function failOpaque(Error err, FailureInfo info, uint opaqueError) internal returns (uint) { - emit Failure(uint(err), uint(info), opaqueError); - - return uint(err); - } -} +pragma solidity ^0.6.7; + +contract ComptrollerErrorReporter { + enum Error { + NO_ERROR, + UNAUTHORIZED, + COMPTROLLER_MISMATCH, + INSUFFICIENT_SHORTFALL, + INSUFFICIENT_LIQUIDITY, + INVALID_CLOSE_FACTOR, + INVALID_COLLATERAL_FACTOR, + INVALID_LIQUIDATION_INCENTIVE, + MARKET_NOT_ENTERED, // no longer possible + MARKET_NOT_LISTED, + MARKET_ALREADY_LISTED, + MATH_ERROR, + NONZERO_BORROW_BALANCE, + PRICE_ERROR, + REJECTION, + SNAPSHOT_ERROR, + TOO_MANY_ASSETS, + TOO_MUCH_REPAY + } + + enum FailureInfo { + ACCEPT_ADMIN_PENDING_ADMIN_CHECK, + ACCEPT_PENDING_IMPLEMENTATION_ADDRESS_CHECK, + EXIT_MARKET_BALANCE_OWED, + EXIT_MARKET_REJECTION, + SET_CLOSE_FACTOR_OWNER_CHECK, + SET_CLOSE_FACTOR_VALIDATION, + SET_COLLATERAL_FACTOR_OWNER_CHECK, + SET_COLLATERAL_FACTOR_NO_EXISTS, + SET_COLLATERAL_FACTOR_VALIDATION, + SET_COLLATERAL_FACTOR_WITHOUT_PRICE, + SET_IMPLEMENTATION_OWNER_CHECK, + SET_LIQUIDATION_INCENTIVE_OWNER_CHECK, + SET_LIQUIDATION_INCENTIVE_VALIDATION, + SET_MAX_ASSETS_OWNER_CHECK, + SET_PENDING_ADMIN_OWNER_CHECK, + SET_PENDING_IMPLEMENTATION_OWNER_CHECK, + SET_PRICE_ORACLE_OWNER_CHECK, + SUPPORT_MARKET_EXISTS, + SUPPORT_MARKET_OWNER_CHECK, + SET_PAUSE_GUARDIAN_OWNER_CHECK + } + + /** + * @dev `error` corresponds to enum Error; `info` corresponds to enum FailureInfo, and `detail` is an arbitrary + * contract-specific code that enables us to report opaque error codes from upgradeable contracts. + **/ + event Failure(uint error, uint info, uint detail); + + /** + * @dev use this when reporting a known error from the money market or a non-upgradeable collaborator + */ + function fail(Error err, FailureInfo info) internal returns (uint) { + emit Failure(uint(err), uint(info), 0); + + return uint(err); + } + + /** + * @dev use this when reporting an opaque error from an upgradeable collaborator contract + */ + function failOpaque(Error err, FailureInfo info, uint opaqueError) internal returns (uint) { + emit Failure(uint(err), uint(info), opaqueError); + + return uint(err); + } +} + +contract TokenErrorReporter { + enum Error { + NO_ERROR, + UNAUTHORIZED, + BAD_INPUT, + COMPTROLLER_REJECTION, + COMPTROLLER_CALCULATION_ERROR, + INTEREST_RATE_MODEL_ERROR, + INVALID_ACCOUNT_PAIR, + INVALID_CLOSE_AMOUNT_REQUESTED, + INVALID_COLLATERAL_FACTOR, + MATH_ERROR, + MARKET_NOT_FRESH, + MARKET_NOT_LISTED, + TOKEN_INSUFFICIENT_ALLOWANCE, + TOKEN_INSUFFICIENT_BALANCE, + TOKEN_INSUFFICIENT_CASH, + TOKEN_TRANSFER_IN_FAILED, + TOKEN_TRANSFER_OUT_FAILED + } + + /* + * Note: FailureInfo (but not Error) is kept in alphabetical order + * This is because FailureInfo grows significantly faster, and + * the order of Error has some meaning, while the order of FailureInfo + * is entirely arbitrary. + */ + enum FailureInfo { + ACCEPT_ADMIN_PENDING_ADMIN_CHECK, + ACCRUE_INTEREST_ACCUMULATED_INTEREST_CALCULATION_FAILED, + ACCRUE_INTEREST_BORROW_RATE_CALCULATION_FAILED, + ACCRUE_INTEREST_NEW_BORROW_INDEX_CALCULATION_FAILED, + ACCRUE_INTEREST_NEW_TOTAL_BORROWS_CALCULATION_FAILED, + ACCRUE_INTEREST_NEW_TOTAL_RESERVES_CALCULATION_FAILED, + ACCRUE_INTEREST_SIMPLE_INTEREST_FACTOR_CALCULATION_FAILED, + BORROW_ACCUMULATED_BALANCE_CALCULATION_FAILED, + BORROW_ACCRUE_INTEREST_FAILED, + BORROW_CASH_NOT_AVAILABLE, + BORROW_FRESHNESS_CHECK, + BORROW_NEW_TOTAL_BALANCE_CALCULATION_FAILED, + BORROW_NEW_ACCOUNT_BORROW_BALANCE_CALCULATION_FAILED, + BORROW_MARKET_NOT_LISTED, + BORROW_COMPTROLLER_REJECTION, + LIQUIDATE_ACCRUE_BORROW_INTEREST_FAILED, + LIQUIDATE_ACCRUE_COLLATERAL_INTEREST_FAILED, + LIQUIDATE_COLLATERAL_FRESHNESS_CHECK, + LIQUIDATE_COMPTROLLER_REJECTION, + LIQUIDATE_COMPTROLLER_CALCULATE_AMOUNT_SEIZE_FAILED, + LIQUIDATE_CLOSE_AMOUNT_IS_UINT_MAX, + LIQUIDATE_CLOSE_AMOUNT_IS_ZERO, + LIQUIDATE_FRESHNESS_CHECK, + LIQUIDATE_LIQUIDATOR_IS_BORROWER, + LIQUIDATE_REPAY_BORROW_FRESH_FAILED, + LIQUIDATE_SEIZE_BALANCE_INCREMENT_FAILED, + LIQUIDATE_SEIZE_BALANCE_DECREMENT_FAILED, + LIQUIDATE_SEIZE_COMPTROLLER_REJECTION, + LIQUIDATE_SEIZE_LIQUIDATOR_IS_BORROWER, + LIQUIDATE_SEIZE_TOO_MUCH, + MINT_ACCRUE_INTEREST_FAILED, + MINT_COMPTROLLER_REJECTION, + MINT_EXCHANGE_CALCULATION_FAILED, + MINT_EXCHANGE_RATE_READ_FAILED, + MINT_FRESHNESS_CHECK, + MINT_NEW_ACCOUNT_BALANCE_CALCULATION_FAILED, + MINT_NEW_TOTAL_SUPPLY_CALCULATION_FAILED, + MINT_TRANSFER_IN_FAILED, + MINT_TRANSFER_IN_NOT_POSSIBLE, + REDEEM_ACCRUE_INTEREST_FAILED, + REDEEM_COMPTROLLER_REJECTION, + REDEEM_EXCHANGE_TOKENS_CALCULATION_FAILED, + REDEEM_EXCHANGE_AMOUNT_CALCULATION_FAILED, + REDEEM_EXCHANGE_RATE_READ_FAILED, + REDEEM_FRESHNESS_CHECK, + REDEEM_NEW_ACCOUNT_BALANCE_CALCULATION_FAILED, + REDEEM_NEW_TOTAL_SUPPLY_CALCULATION_FAILED, + REDEEM_TRANSFER_OUT_NOT_POSSIBLE, + REDUCE_RESERVES_ACCRUE_INTEREST_FAILED, + REDUCE_RESERVES_ADMIN_CHECK, + REDUCE_RESERVES_CASH_NOT_AVAILABLE, + REDUCE_RESERVES_FRESH_CHECK, + REDUCE_RESERVES_VALIDATION, + REPAY_BEHALF_ACCRUE_INTEREST_FAILED, + REPAY_BORROW_ACCRUE_INTEREST_FAILED, + REPAY_BORROW_ACCUMULATED_BALANCE_CALCULATION_FAILED, + REPAY_BORROW_COMPTROLLER_REJECTION, + REPAY_BORROW_FRESHNESS_CHECK, + REPAY_BORROW_NEW_ACCOUNT_BORROW_BALANCE_CALCULATION_FAILED, + REPAY_BORROW_NEW_TOTAL_BALANCE_CALCULATION_FAILED, + REPAY_BORROW_TRANSFER_IN_NOT_POSSIBLE, + SET_COLLATERAL_FACTOR_OWNER_CHECK, + SET_COLLATERAL_FACTOR_VALIDATION, + SET_COMPTROLLER_OWNER_CHECK, + SET_INTEREST_RATE_MODEL_ACCRUE_INTEREST_FAILED, + SET_INTEREST_RATE_MODEL_FRESH_CHECK, + SET_INTEREST_RATE_MODEL_OWNER_CHECK, + SET_MAX_ASSETS_OWNER_CHECK, + SET_ORACLE_MARKET_NOT_LISTED, + SET_PENDING_ADMIN_OWNER_CHECK, + SET_RESERVE_FACTOR_ACCRUE_INTEREST_FAILED, + SET_RESERVE_FACTOR_ADMIN_CHECK, + SET_RESERVE_FACTOR_FRESH_CHECK, + SET_RESERVE_FACTOR_BOUNDS_CHECK, + TRANSFER_COMPTROLLER_REJECTION, + TRANSFER_NOT_ALLOWED, + TRANSFER_NOT_ENOUGH, + TRANSFER_TOO_MUCH, + ADD_RESERVES_ACCRUE_INTEREST_FAILED, + ADD_RESERVES_FRESH_CHECK, + ADD_RESERVES_TRANSFER_IN_NOT_POSSIBLE + } + + /** + * @dev `error` corresponds to enum Error; `info` corresponds to enum FailureInfo, and `detail` is an arbitrary + * contract-specific code that enables us to report opaque error codes from upgradeable contracts. + **/ + event Failure(uint error, uint info, uint detail); + + /** + * @dev use this when reporting a known error from the money market or a non-upgradeable collaborator + */ + function fail(Error err, FailureInfo info) internal returns (uint) { + emit Failure(uint(err), uint(info), 0); + + return uint(err); + } + + /** + * @dev use this when reporting an opaque error from an upgradeable collaborator contract + */ + function failOpaque(Error err, FailureInfo info, uint opaqueError) internal returns (uint) { + emit Failure(uint(err), uint(info), opaqueError); + + return uint(err); + } +} diff --git a/src/integrations/compound/Exponential.sol b/src/integrations/compound/Exponential.sol index 22fd72a..8aceaec 100644 --- a/src/integrations/compound/Exponential.sol +++ b/src/integrations/compound/Exponential.sol @@ -1,182 +1,182 @@ -pragma solidity ^0.6.7; - -import "../../math/CarefulMath.sol"; -import "./ExponentialNoError.sol"; - -/** - * @title Exponential module for storing fixed-precision decimals - * @author Compound - * @dev Legacy contract for compatibility reasons with existing contracts that still use MathError - * @notice Exp is a struct which stores decimals with a fixed precision of 18 decimal places. - * Thus, if we wanted to store the 5.1, mantissa would store 5.1e18. That is: - * `Exp({mantissa: 5100000000000000000})`. - */ -contract Exponential is CarefulMath, ExponentialNoError { - /** - * @dev Creates an exponential from numerator and denominator values. - * Note: Returns an error if (`num` * 10e18) > MAX_INT, - * or if `denom` is zero. - */ - function getExp(uint num, uint denom) pure internal returns (MathError, Exp memory) { - (MathError err0, uint scaledNumerator) = mulUInt(num, expScale); - if (err0 != MathError.NO_ERROR) { - return (err0, Exp({mantissa: 0})); - } - - (MathError err1, uint rational) = divUInt(scaledNumerator, denom); - if (err1 != MathError.NO_ERROR) { - return (err1, Exp({mantissa: 0})); - } - - return (MathError.NO_ERROR, Exp({mantissa: rational})); - } - - /** - * @dev Adds two exponentials, returning a new exponential. - */ - function addExp(Exp memory a, Exp memory b) pure internal returns (MathError, Exp memory) { - (MathError error, uint result) = addUInt(a.mantissa, b.mantissa); - - return (error, Exp({mantissa: result})); - } - - /** - * @dev Subtracts two exponentials, returning a new exponential. - */ - function subExp(Exp memory a, Exp memory b) pure internal returns (MathError, Exp memory) { - (MathError error, uint result) = subUInt(a.mantissa, b.mantissa); - - return (error, Exp({mantissa: result})); - } - - /** - * @dev Multiply an Exp by a scalar, returning a new Exp. - */ - function mulScalar(Exp memory a, uint scalar) pure internal returns (MathError, Exp memory) { - (MathError err0, uint scaledMantissa) = mulUInt(a.mantissa, scalar); - if (err0 != MathError.NO_ERROR) { - return (err0, Exp({mantissa: 0})); - } - - return (MathError.NO_ERROR, Exp({mantissa: scaledMantissa})); - } - - /** - * @dev Multiply an Exp by a scalar, then truncate to return an unsigned integer. - */ - function mulScalarTruncate(Exp memory a, uint scalar) pure internal returns (MathError, uint) { - (MathError err, Exp memory product) = mulScalar(a, scalar); - if (err != MathError.NO_ERROR) { - return (err, 0); - } - - return (MathError.NO_ERROR, truncate(product)); - } - - /** - * @dev Multiply an Exp by a scalar, truncate, then add an to an unsigned integer, returning an unsigned integer. - */ - function mulScalarTruncateAddUInt(Exp memory a, uint scalar, uint addend) pure internal returns (MathError, uint) { - (MathError err, Exp memory product) = mulScalar(a, scalar); - if (err != MathError.NO_ERROR) { - return (err, 0); - } - - return addUInt(truncate(product), addend); - } - - /** - * @dev Divide an Exp by a scalar, returning a new Exp. - */ - function divScalar(Exp memory a, uint scalar) pure internal returns (MathError, Exp memory) { - (MathError err0, uint descaledMantissa) = divUInt(a.mantissa, scalar); - if (err0 != MathError.NO_ERROR) { - return (err0, Exp({mantissa: 0})); - } - - return (MathError.NO_ERROR, Exp({mantissa: descaledMantissa})); - } - - /** - * @dev Divide a scalar by an Exp, returning a new Exp. - */ - function divScalarByExp(uint scalar, Exp memory divisor) pure internal returns (MathError, Exp memory) { - /* - We are doing this as: - getExp(mulUInt(expScale, scalar), divisor.mantissa) - How it works: - Exp = a / b; - Scalar = s; - `s / (a / b)` = `b * s / a` and since for an Exp `a = mantissa, b = expScale` - */ - (MathError err0, uint numerator) = mulUInt(expScale, scalar); - if (err0 != MathError.NO_ERROR) { - return (err0, Exp({mantissa: 0})); - } - return getExp(numerator, divisor.mantissa); - } - - /** - * @dev Divide a scalar by an Exp, then truncate to return an unsigned integer. - */ - function divScalarByExpTruncate(uint scalar, Exp memory divisor) pure internal returns (MathError, uint) { - (MathError err, Exp memory fraction) = divScalarByExp(scalar, divisor); - if (err != MathError.NO_ERROR) { - return (err, 0); - } - - return (MathError.NO_ERROR, truncate(fraction)); - } - - /** - * @dev Multiplies two exponentials, returning a new exponential. - */ - function mulExp(Exp memory a, Exp memory b) pure internal returns (MathError, Exp memory) { - - (MathError err0, uint doubleScaledProduct) = mulUInt(a.mantissa, b.mantissa); - if (err0 != MathError.NO_ERROR) { - return (err0, Exp({mantissa: 0})); - } - - // We add half the scale before dividing so that we get rounding instead of truncation. - // See "Listing 6" and text above it at https://accu.org/index.php/journals/1717 - // Without this change, a result like 6.6...e-19 will be truncated to 0 instead of being rounded to 1e-18. - (MathError err1, uint doubleScaledProductWithHalfScale) = addUInt(halfExpScale, doubleScaledProduct); - if (err1 != MathError.NO_ERROR) { - return (err1, Exp({mantissa: 0})); - } - - (MathError err2, uint product) = divUInt(doubleScaledProductWithHalfScale, expScale); - // The only error `div` can return is MathError.DIVISION_BY_ZERO but we control `expScale` and it is not zero. - assert(err2 == MathError.NO_ERROR); - - return (MathError.NO_ERROR, Exp({mantissa: product})); - } - - /** - * @dev Multiplies two exponentials given their mantissas, returning a new exponential. - */ - function mulExp(uint a, uint b) pure internal returns (MathError, Exp memory) { - return mulExp(Exp({mantissa: a}), Exp({mantissa: b})); - } - - /** - * @dev Multiplies three exponentials, returning a new exponential. - */ - function mulExp3(Exp memory a, Exp memory b, Exp memory c) pure internal returns (MathError, Exp memory) { - (MathError err, Exp memory ab) = mulExp(a, b); - if (err != MathError.NO_ERROR) { - return (err, ab); - } - return mulExp(ab, c); - } - - /** - * @dev Divides two exponentials, returning a new exponential. - * (a/scale) / (b/scale) = (a/scale) * (scale/b) = a/b, - * which we can scale as an Exp by calling getExp(a.mantissa, b.mantissa) - */ - function divExp(Exp memory a, Exp memory b) pure internal returns (MathError, Exp memory) { - return getExp(a.mantissa, b.mantissa); - } -} +pragma solidity ^0.6.7; + +import "../../math/CarefulMath.sol"; +import "./ExponentialNoError.sol"; + +/** + * @title Exponential module for storing fixed-precision decimals + * @author Compound + * @dev Legacy contract for compatibility reasons with existing contracts that still use MathError + * @notice Exp is a struct which stores decimals with a fixed precision of 18 decimal places. + * Thus, if we wanted to store the 5.1, mantissa would store 5.1e18. That is: + * `Exp({mantissa: 5100000000000000000})`. + */ +contract Exponential is CarefulMath, ExponentialNoError { + /** + * @dev Creates an exponential from numerator and denominator values. + * Note: Returns an error if (`num` * 10e18) > MAX_INT, + * or if `denom` is zero. + */ + function getExp(uint num, uint denom) pure internal returns (MathError, Exp memory) { + (MathError err0, uint scaledNumerator) = mulUInt(num, expScale); + if (err0 != MathError.NO_ERROR) { + return (err0, Exp({mantissa: 0})); + } + + (MathError err1, uint rational) = divUInt(scaledNumerator, denom); + if (err1 != MathError.NO_ERROR) { + return (err1, Exp({mantissa: 0})); + } + + return (MathError.NO_ERROR, Exp({mantissa: rational})); + } + + /** + * @dev Adds two exponentials, returning a new exponential. + */ + function addExp(Exp memory a, Exp memory b) pure internal returns (MathError, Exp memory) { + (MathError error, uint result) = addUInt(a.mantissa, b.mantissa); + + return (error, Exp({mantissa: result})); + } + + /** + * @dev Subtracts two exponentials, returning a new exponential. + */ + function subExp(Exp memory a, Exp memory b) pure internal returns (MathError, Exp memory) { + (MathError error, uint result) = subUInt(a.mantissa, b.mantissa); + + return (error, Exp({mantissa: result})); + } + + /** + * @dev Multiply an Exp by a scalar, returning a new Exp. + */ + function mulScalar(Exp memory a, uint scalar) pure internal returns (MathError, Exp memory) { + (MathError err0, uint scaledMantissa) = mulUInt(a.mantissa, scalar); + if (err0 != MathError.NO_ERROR) { + return (err0, Exp({mantissa: 0})); + } + + return (MathError.NO_ERROR, Exp({mantissa: scaledMantissa})); + } + + /** + * @dev Multiply an Exp by a scalar, then truncate to return an unsigned integer. + */ + function mulScalarTruncate(Exp memory a, uint scalar) pure internal returns (MathError, uint) { + (MathError err, Exp memory product) = mulScalar(a, scalar); + if (err != MathError.NO_ERROR) { + return (err, 0); + } + + return (MathError.NO_ERROR, truncate(product)); + } + + /** + * @dev Multiply an Exp by a scalar, truncate, then add an to an unsigned integer, returning an unsigned integer. + */ + function mulScalarTruncateAddUInt(Exp memory a, uint scalar, uint addend) pure internal returns (MathError, uint) { + (MathError err, Exp memory product) = mulScalar(a, scalar); + if (err != MathError.NO_ERROR) { + return (err, 0); + } + + return addUInt(truncate(product), addend); + } + + /** + * @dev Divide an Exp by a scalar, returning a new Exp. + */ + function divScalar(Exp memory a, uint scalar) pure internal returns (MathError, Exp memory) { + (MathError err0, uint descaledMantissa) = divUInt(a.mantissa, scalar); + if (err0 != MathError.NO_ERROR) { + return (err0, Exp({mantissa: 0})); + } + + return (MathError.NO_ERROR, Exp({mantissa: descaledMantissa})); + } + + /** + * @dev Divide a scalar by an Exp, returning a new Exp. + */ + function divScalarByExp(uint scalar, Exp memory divisor) pure internal returns (MathError, Exp memory) { + /* + We are doing this as: + getExp(mulUInt(expScale, scalar), divisor.mantissa) + How it works: + Exp = a / b; + Scalar = s; + `s / (a / b)` = `b * s / a` and since for an Exp `a = mantissa, b = expScale` + */ + (MathError err0, uint numerator) = mulUInt(expScale, scalar); + if (err0 != MathError.NO_ERROR) { + return (err0, Exp({mantissa: 0})); + } + return getExp(numerator, divisor.mantissa); + } + + /** + * @dev Divide a scalar by an Exp, then truncate to return an unsigned integer. + */ + function divScalarByExpTruncate(uint scalar, Exp memory divisor) pure internal returns (MathError, uint) { + (MathError err, Exp memory fraction) = divScalarByExp(scalar, divisor); + if (err != MathError.NO_ERROR) { + return (err, 0); + } + + return (MathError.NO_ERROR, truncate(fraction)); + } + + /** + * @dev Multiplies two exponentials, returning a new exponential. + */ + function mulExp(Exp memory a, Exp memory b) pure internal returns (MathError, Exp memory) { + + (MathError err0, uint doubleScaledProduct) = mulUInt(a.mantissa, b.mantissa); + if (err0 != MathError.NO_ERROR) { + return (err0, Exp({mantissa: 0})); + } + + // We add half the scale before dividing so that we get rounding instead of truncation. + // See "Listing 6" and text above it at https://accu.org/index.php/journals/1717 + // Without this change, a result like 6.6...e-19 will be truncated to 0 instead of being rounded to 1e-18. + (MathError err1, uint doubleScaledProductWithHalfScale) = addUInt(halfExpScale, doubleScaledProduct); + if (err1 != MathError.NO_ERROR) { + return (err1, Exp({mantissa: 0})); + } + + (MathError err2, uint product) = divUInt(doubleScaledProductWithHalfScale, expScale); + // The only error `div` can return is MathError.DIVISION_BY_ZERO but we control `expScale` and it is not zero. + assert(err2 == MathError.NO_ERROR); + + return (MathError.NO_ERROR, Exp({mantissa: product})); + } + + /** + * @dev Multiplies two exponentials given their mantissas, returning a new exponential. + */ + function mulExp(uint a, uint b) pure internal returns (MathError, Exp memory) { + return mulExp(Exp({mantissa: a}), Exp({mantissa: b})); + } + + /** + * @dev Multiplies three exponentials, returning a new exponential. + */ + function mulExp3(Exp memory a, Exp memory b, Exp memory c) pure internal returns (MathError, Exp memory) { + (MathError err, Exp memory ab) = mulExp(a, b); + if (err != MathError.NO_ERROR) { + return (err, ab); + } + return mulExp(ab, c); + } + + /** + * @dev Divides two exponentials, returning a new exponential. + * (a/scale) / (b/scale) = (a/scale) * (scale/b) = a/b, + * which we can scale as an Exp by calling getExp(a.mantissa, b.mantissa) + */ + function divExp(Exp memory a, Exp memory b) pure internal returns (MathError, Exp memory) { + return getExp(a.mantissa, b.mantissa); + } +} diff --git a/src/integrations/compound/ExponentialNoError.sol b/src/integrations/compound/ExponentialNoError.sol index 67e3115..2558752 100644 --- a/src/integrations/compound/ExponentialNoError.sol +++ b/src/integrations/compound/ExponentialNoError.sol @@ -1,195 +1,195 @@ -pragma solidity ^0.6.7; - -/** - * @title Exponential module for storing fixed-precision decimals - * @author Compound - * @notice Exp is a struct which stores decimals with a fixed precision of 18 decimal places. - * Thus, if we wanted to store the 5.1, mantissa would store 5.1e18. That is: - * `Exp({mantissa: 5100000000000000000})`. - */ -contract ExponentialNoError { - uint constant expScale = 1e18; - uint constant doubleScale = 1e36; - uint constant halfExpScale = expScale/2; - uint constant mantissaOne = expScale; - - struct Exp { - uint mantissa; - } - - struct Double { - uint mantissa; - } - - /** - * @dev Truncates the given exp to a whole number value. - * For example, truncate(Exp{mantissa: 15 * expScale}) = 15 - */ - function truncate(Exp memory exp) pure internal returns (uint) { - // Note: We are not using careful math here as we're performing a division that cannot fail - return exp.mantissa / expScale; - } - - /** - * @dev Multiply an Exp by a scalar, then truncate to return an unsigned integer. - */ - function mul_ScalarTruncate(Exp memory a, uint scalar) pure internal returns (uint) { - Exp memory product = mul_(a, scalar); - return truncate(product); - } - - /** - * @dev Multiply an Exp by a scalar, truncate, then add an to an unsigned integer, returning an unsigned integer. - */ - function mul_ScalarTruncateAddUInt(Exp memory a, uint scalar, uint addend) pure internal returns (uint) { - Exp memory product = mul_(a, scalar); - return add_(truncate(product), addend); - } - - /** - * @dev Checks if first Exp is less than second Exp. - */ - function lessThanExp(Exp memory left, Exp memory right) pure internal returns (bool) { - return left.mantissa < right.mantissa; - } - - /** - * @dev Checks if left Exp <= right Exp. - */ - function lessThanOrEqualExp(Exp memory left, Exp memory right) pure internal returns (bool) { - return left.mantissa <= right.mantissa; - } - - /** - * @dev Checks if left Exp > right Exp. - */ - function greaterThanExp(Exp memory left, Exp memory right) pure internal returns (bool) { - return left.mantissa > right.mantissa; - } - - /** - * @dev returns true if Exp is exactly zero - */ - function isZeroExp(Exp memory value) pure internal returns (bool) { - return value.mantissa == 0; - } - - function safe224(uint n, string memory errorMessage) pure internal returns (uint224) { - require(n < 2**224, errorMessage); - return uint224(n); - } - - function safe32(uint n, string memory errorMessage) pure internal returns (uint32) { - require(n < 2**32, errorMessage); - return uint32(n); - } - - function add_(Exp memory a, Exp memory b) pure internal returns (Exp memory) { - return Exp({mantissa: add_(a.mantissa, b.mantissa)}); - } - - function add_(Double memory a, Double memory b) pure internal returns (Double memory) { - return Double({mantissa: add_(a.mantissa, b.mantissa)}); - } - - function add_(uint a, uint b) pure internal returns (uint) { - return add_(a, b, "addition overflow"); - } - - function add_(uint a, uint b, string memory errorMessage) pure internal returns (uint) { - uint c = a + b; - require(c >= a, errorMessage); - return c; - } - - function sub_(Exp memory a, Exp memory b) pure internal returns (Exp memory) { - return Exp({mantissa: sub_(a.mantissa, b.mantissa)}); - } - - function sub_(Double memory a, Double memory b) pure internal returns (Double memory) { - return Double({mantissa: sub_(a.mantissa, b.mantissa)}); - } - - function sub_(uint a, uint b) pure internal returns (uint) { - return sub_(a, b, "subtraction underflow"); - } - - function sub_(uint a, uint b, string memory errorMessage) pure internal returns (uint) { - require(b <= a, errorMessage); - return a - b; - } - - function mul_(Exp memory a, Exp memory b) pure internal returns (Exp memory) { - return Exp({mantissa: mul_(a.mantissa, b.mantissa) / expScale}); - } - - function mul_(Exp memory a, uint b) pure internal returns (Exp memory) { - return Exp({mantissa: mul_(a.mantissa, b)}); - } - - function mul_(uint a, Exp memory b) pure internal returns (uint) { - return mul_(a, b.mantissa) / expScale; - } - - function mul_(Double memory a, Double memory b) pure internal returns (Double memory) { - return Double({mantissa: mul_(a.mantissa, b.mantissa) / doubleScale}); - } - - function mul_(Double memory a, uint b) pure internal returns (Double memory) { - return Double({mantissa: mul_(a.mantissa, b)}); - } - - function mul_(uint a, Double memory b) pure internal returns (uint) { - return mul_(a, b.mantissa) / doubleScale; - } - - function mul_(uint a, uint b) pure internal returns (uint) { - return mul_(a, b, "multiplication overflow"); - } - - function mul_(uint a, uint b, string memory errorMessage) pure internal returns (uint) { - if (a == 0 || b == 0) { - return 0; - } - uint c = a * b; - require(c / a == b, errorMessage); - return c; - } - - function div_(Exp memory a, Exp memory b) pure internal returns (Exp memory) { - return Exp({mantissa: div_(mul_(a.mantissa, expScale), b.mantissa)}); - } - - function div_(Exp memory a, uint b) pure internal returns (Exp memory) { - return Exp({mantissa: div_(a.mantissa, b)}); - } - - function div_(uint a, Exp memory b) pure internal returns (uint) { - return div_(mul_(a, expScale), b.mantissa); - } - - function div_(Double memory a, Double memory b) pure internal returns (Double memory) { - return Double({mantissa: div_(mul_(a.mantissa, doubleScale), b.mantissa)}); - } - - function div_(Double memory a, uint b) pure internal returns (Double memory) { - return Double({mantissa: div_(a.mantissa, b)}); - } - - function div_(uint a, Double memory b) pure internal returns (uint) { - return div_(mul_(a, doubleScale), b.mantissa); - } - - function div_(uint a, uint b) pure internal returns (uint) { - return div_(a, b, "divide by zero"); - } - - function div_(uint a, uint b, string memory errorMessage) pure internal returns (uint) { - require(b > 0, errorMessage); - return a / b; - } - - function fraction(uint a, uint b) pure internal returns (Double memory) { - return Double({mantissa: div_(mul_(a, doubleScale), b)}); - } -} +pragma solidity ^0.6.7; + +/** + * @title Exponential module for storing fixed-precision decimals + * @author Compound + * @notice Exp is a struct which stores decimals with a fixed precision of 18 decimal places. + * Thus, if we wanted to store the 5.1, mantissa would store 5.1e18. That is: + * `Exp({mantissa: 5100000000000000000})`. + */ +contract ExponentialNoError { + uint constant expScale = 1e18; + uint constant doubleScale = 1e36; + uint constant halfExpScale = expScale/2; + uint constant mantissaOne = expScale; + + struct Exp { + uint mantissa; + } + + struct Double { + uint mantissa; + } + + /** + * @dev Truncates the given exp to a whole number value. + * For example, truncate(Exp{mantissa: 15 * expScale}) = 15 + */ + function truncate(Exp memory exp) pure internal returns (uint) { + // Note: We are not using careful math here as we're performing a division that cannot fail + return exp.mantissa / expScale; + } + + /** + * @dev Multiply an Exp by a scalar, then truncate to return an unsigned integer. + */ + function mul_ScalarTruncate(Exp memory a, uint scalar) pure internal returns (uint) { + Exp memory product = mul_(a, scalar); + return truncate(product); + } + + /** + * @dev Multiply an Exp by a scalar, truncate, then add an to an unsigned integer, returning an unsigned integer. + */ + function mul_ScalarTruncateAddUInt(Exp memory a, uint scalar, uint addend) pure internal returns (uint) { + Exp memory product = mul_(a, scalar); + return add_(truncate(product), addend); + } + + /** + * @dev Checks if first Exp is less than second Exp. + */ + function lessThanExp(Exp memory left, Exp memory right) pure internal returns (bool) { + return left.mantissa < right.mantissa; + } + + /** + * @dev Checks if left Exp <= right Exp. + */ + function lessThanOrEqualExp(Exp memory left, Exp memory right) pure internal returns (bool) { + return left.mantissa <= right.mantissa; + } + + /** + * @dev Checks if left Exp > right Exp. + */ + function greaterThanExp(Exp memory left, Exp memory right) pure internal returns (bool) { + return left.mantissa > right.mantissa; + } + + /** + * @dev returns true if Exp is exactly zero + */ + function isZeroExp(Exp memory value) pure internal returns (bool) { + return value.mantissa == 0; + } + + function safe224(uint n, string memory errorMessage) pure internal returns (uint224) { + require(n < 2**224, errorMessage); + return uint224(n); + } + + function safe32(uint n, string memory errorMessage) pure internal returns (uint32) { + require(n < 2**32, errorMessage); + return uint32(n); + } + + function add_(Exp memory a, Exp memory b) pure internal returns (Exp memory) { + return Exp({mantissa: add_(a.mantissa, b.mantissa)}); + } + + function add_(Double memory a, Double memory b) pure internal returns (Double memory) { + return Double({mantissa: add_(a.mantissa, b.mantissa)}); + } + + function add_(uint a, uint b) pure internal returns (uint) { + return add_(a, b, "addition overflow"); + } + + function add_(uint a, uint b, string memory errorMessage) pure internal returns (uint) { + uint c = a + b; + require(c >= a, errorMessage); + return c; + } + + function sub_(Exp memory a, Exp memory b) pure internal returns (Exp memory) { + return Exp({mantissa: sub_(a.mantissa, b.mantissa)}); + } + + function sub_(Double memory a, Double memory b) pure internal returns (Double memory) { + return Double({mantissa: sub_(a.mantissa, b.mantissa)}); + } + + function sub_(uint a, uint b) pure internal returns (uint) { + return sub_(a, b, "subtraction underflow"); + } + + function sub_(uint a, uint b, string memory errorMessage) pure internal returns (uint) { + require(b <= a, errorMessage); + return a - b; + } + + function mul_(Exp memory a, Exp memory b) pure internal returns (Exp memory) { + return Exp({mantissa: mul_(a.mantissa, b.mantissa) / expScale}); + } + + function mul_(Exp memory a, uint b) pure internal returns (Exp memory) { + return Exp({mantissa: mul_(a.mantissa, b)}); + } + + function mul_(uint a, Exp memory b) pure internal returns (uint) { + return mul_(a, b.mantissa) / expScale; + } + + function mul_(Double memory a, Double memory b) pure internal returns (Double memory) { + return Double({mantissa: mul_(a.mantissa, b.mantissa) / doubleScale}); + } + + function mul_(Double memory a, uint b) pure internal returns (Double memory) { + return Double({mantissa: mul_(a.mantissa, b)}); + } + + function mul_(uint a, Double memory b) pure internal returns (uint) { + return mul_(a, b.mantissa) / doubleScale; + } + + function mul_(uint a, uint b) pure internal returns (uint) { + return mul_(a, b, "multiplication overflow"); + } + + function mul_(uint a, uint b, string memory errorMessage) pure internal returns (uint) { + if (a == 0 || b == 0) { + return 0; + } + uint c = a * b; + require(c / a == b, errorMessage); + return c; + } + + function div_(Exp memory a, Exp memory b) pure internal returns (Exp memory) { + return Exp({mantissa: div_(mul_(a.mantissa, expScale), b.mantissa)}); + } + + function div_(Exp memory a, uint b) pure internal returns (Exp memory) { + return Exp({mantissa: div_(a.mantissa, b)}); + } + + function div_(uint a, Exp memory b) pure internal returns (uint) { + return div_(mul_(a, expScale), b.mantissa); + } + + function div_(Double memory a, Double memory b) pure internal returns (Double memory) { + return Double({mantissa: div_(mul_(a.mantissa, doubleScale), b.mantissa)}); + } + + function div_(Double memory a, uint b) pure internal returns (Double memory) { + return Double({mantissa: div_(a.mantissa, b)}); + } + + function div_(uint a, Double memory b) pure internal returns (uint) { + return div_(mul_(a, doubleScale), b.mantissa); + } + + function div_(uint a, uint b) pure internal returns (uint) { + return div_(a, b, "divide by zero"); + } + + function div_(uint a, uint b, string memory errorMessage) pure internal returns (uint) { + require(b > 0, errorMessage); + return a / b; + } + + function fraction(uint a, uint b) pure internal returns (Double memory) { + return Double({mantissa: div_(mul_(a, doubleScale), b)}); + } +} diff --git a/src/integrations/compound/InterestRateModel.sol b/src/integrations/compound/InterestRateModel.sol index 192ff20..2e1ca64 100644 --- a/src/integrations/compound/InterestRateModel.sol +++ b/src/integrations/compound/InterestRateModel.sol @@ -1,30 +1,30 @@ -pragma solidity ^0.6.7; - -/** - * @title Compound's InterestRateModel Interface - * @author Compound - */ -abstract contract InterestRateModel { - /// @notice Indicator that this is an InterestRateModel contract (for inspection) - bool public constant isInterestRateModel = true; - - /** - * @notice Calculates the current borrow interest rate per block - * @param cash The total amount of cash the market has - * @param borrows The total amount of borrows the market has outstanding - * @param reserves The total amount of reserves the market has - * @return The borrow rate per block (as a percentage, and scaled by 1e18) - */ - function getBorrowRate(uint cash, uint borrows, uint reserves) virtual external view returns (uint) {} - - /** - * @notice Calculates the current supply interest rate per block - * @param cash The total amount of cash the market has - * @param borrows The total amount of borrows the market has outstanding - * @param reserves The total amount of reserves the market has - * @param reserveFactorMantissa The current reserve factor the market has - * @return The supply rate per block (as a percentage, and scaled by 1e18) - */ - function getSupplyRate(uint cash, uint borrows, uint reserves, uint reserveFactorMantissa) virtual external view returns (uint) {} - -} +pragma solidity ^0.6.7; + +/** + * @title Compound's InterestRateModel Interface + * @author Compound + */ +abstract contract InterestRateModel { + /// @notice Indicator that this is an InterestRateModel contract (for inspection) + bool public constant isInterestRateModel = true; + + /** + * @notice Calculates the current borrow interest rate per block + * @param cash The total amount of cash the market has + * @param borrows The total amount of borrows the market has outstanding + * @param reserves The total amount of reserves the market has + * @return The borrow rate per block (as a percentage, and scaled by 1e18) + */ + function getBorrowRate(uint cash, uint borrows, uint reserves) virtual external view returns (uint) {} + + /** + * @notice Calculates the current supply interest rate per block + * @param cash The total amount of cash the market has + * @param borrows The total amount of borrows the market has outstanding + * @param reserves The total amount of reserves the market has + * @param reserveFactorMantissa The current reserve factor the market has + * @return The supply rate per block (as a percentage, and scaled by 1e18) + */ + function getSupplyRate(uint cash, uint borrows, uint reserves, uint reserveFactorMantissa) virtual external view returns (uint) {} + +} diff --git a/src/integrations/compound/PriceOracle.sol b/src/integrations/compound/PriceOracle.sol index 7d7f37b..8eb2949 100644 --- a/src/integrations/compound/PriceOracle.sol +++ b/src/integrations/compound/PriceOracle.sol @@ -1,16 +1,16 @@ -pragma solidity ^0.6.7; - -import "./CToken.sol"; - -abstract contract PriceOracle { - /// @notice Indicator that this is a PriceOracle contract (for inspection) - bool public constant isPriceOracle = true; - - /** - * @notice Get the underlying price of a cToken asset - * @param cToken The cToken to get the underlying price of - * @return The underlying asset price mantissa (scaled by 1e18). - * Zero means the price is unavailable. - */ - function getUnderlyingPrice(CToken cToken) virtual external view returns (uint); -} +pragma solidity ^0.6.7; + +import "./CToken.sol"; + +abstract contract PriceOracle { + /// @notice Indicator that this is a PriceOracle contract (for inspection) + bool public constant isPriceOracle = true; + + /** + * @notice Get the underlying price of a cToken asset + * @param cToken The cToken to get the underlying price of + * @return The underlying asset price mantissa (scaled by 1e18). + * Zero means the price is unavailable. + */ + function getUnderlyingPrice(CToken cToken) virtual external view returns (uint); +} diff --git a/src/integrations/compound/Unitroller.sol b/src/integrations/compound/Unitroller.sol index 8779e6b..ac5dc89 100644 --- a/src/integrations/compound/Unitroller.sol +++ b/src/integrations/compound/Unitroller.sol @@ -1,150 +1,150 @@ -pragma solidity ^0.6.7; - -import "./ErrorReporter.sol"; -import "./ComptrollerStorage.sol"; -/** - * @title ComptrollerCore - * @dev Storage for the comptroller is at this address, while execution is delegated to the `comptrollerImplementation`. - * CTokens should reference this contract as their comptroller. - */ -contract Unitroller is UnitrollerAdminStorage, ComptrollerErrorReporter { - - /** - * @notice Emitted when pendingComptrollerImplementation is changed - */ - event NewPendingImplementation(address oldPendingImplementation, address newPendingImplementation); - - /** - * @notice Emitted when pendingComptrollerImplementation is accepted, which means comptroller implementation is updated - */ - event NewImplementation(address oldImplementation, address newImplementation); - - /** - * @notice Emitted when pendingAdmin is changed - */ - event NewPendingAdmin(address oldPendingAdmin, address newPendingAdmin); - - /** - * @notice Emitted when pendingAdmin is accepted, which means admin is updated - */ - event NewAdmin(address oldAdmin, address newAdmin); - - constructor() public { - // Set admin to caller - admin = msg.sender; - } - - /*** Admin Functions ***/ - function _setPendingImplementation(address newPendingImplementation) public returns (uint) { - - if (msg.sender != admin) { - return fail(Error.UNAUTHORIZED, FailureInfo.SET_PENDING_IMPLEMENTATION_OWNER_CHECK); - } - - address oldPendingImplementation = pendingComptrollerImplementation; - - pendingComptrollerImplementation = newPendingImplementation; - - emit NewPendingImplementation(oldPendingImplementation, pendingComptrollerImplementation); - - return uint(Error.NO_ERROR); - } - - /** - * @notice Accepts new implementation of comptroller. msg.sender must be pendingImplementation - * @dev Admin function for new implementation to accept it's role as implementation - * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) - */ - function _acceptImplementation() public returns (uint) { - // Check caller is pendingImplementation and pendingImplementation ≠ address(0) - if (msg.sender != pendingComptrollerImplementation || pendingComptrollerImplementation == address(0)) { - return fail(Error.UNAUTHORIZED, FailureInfo.ACCEPT_PENDING_IMPLEMENTATION_ADDRESS_CHECK); - } - - // Save current values for inclusion in log - address oldImplementation = comptrollerImplementation; - address oldPendingImplementation = pendingComptrollerImplementation; - - comptrollerImplementation = pendingComptrollerImplementation; - - pendingComptrollerImplementation = address(0); - - emit NewImplementation(oldImplementation, comptrollerImplementation); - emit NewPendingImplementation(oldPendingImplementation, pendingComptrollerImplementation); - - return uint(Error.NO_ERROR); - } - - - /** - * @notice Begins transfer of admin rights. The newPendingAdmin must call `_acceptAdmin` to finalize the transfer. - * @dev Admin function to begin change of admin. The newPendingAdmin must call `_acceptAdmin` to finalize the transfer. - * @param newPendingAdmin New pending admin. - * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) - */ - function _setPendingAdmin(address newPendingAdmin) public returns (uint) { - // Check caller = admin - if (msg.sender != admin) { - return fail(Error.UNAUTHORIZED, FailureInfo.SET_PENDING_ADMIN_OWNER_CHECK); - } - - // Save current value, if any, for inclusion in log - address oldPendingAdmin = pendingAdmin; - - // Store pendingAdmin with value newPendingAdmin - pendingAdmin = newPendingAdmin; - - // Emit NewPendingAdmin(oldPendingAdmin, newPendingAdmin) - emit NewPendingAdmin(oldPendingAdmin, newPendingAdmin); - - return uint(Error.NO_ERROR); - } - - /** - * @notice Accepts transfer of admin rights. msg.sender must be pendingAdmin - * @dev Admin function for pending admin to accept role and update admin - * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) - */ - function _acceptAdmin() public returns (uint) { - // Check caller is pendingAdmin and pendingAdmin ≠ address(0) - if (msg.sender != pendingAdmin || msg.sender == address(0)) { - return fail(Error.UNAUTHORIZED, FailureInfo.ACCEPT_ADMIN_PENDING_ADMIN_CHECK); - } - - // Save current values for inclusion in log - address oldAdmin = admin; - address oldPendingAdmin = pendingAdmin; - - // Store admin with value pendingAdmin - admin = pendingAdmin; - - // Clear the pending value - pendingAdmin = address(0); - - emit NewAdmin(oldAdmin, admin); - emit NewPendingAdmin(oldPendingAdmin, pendingAdmin); - - return uint(Error.NO_ERROR); - } - - /** - * @dev Delegates execution to an implementation contract. - * It returns to the external caller whatever the implementation returns - * or forwards reverts. - */ - fallback() external { - // delegate all other functions to current implementation - (bool success, ) = comptrollerImplementation.delegatecall(msg.data); - - assembly { - let free_mem_ptr := mload(0x40) - returndatacopy(free_mem_ptr, 0, returndatasize()) - - switch success - case 0 { revert(free_mem_ptr, returndatasize()) } - default { return(free_mem_ptr, returndatasize()) } - } - } - - receive() payable external {} -} +pragma solidity ^0.6.7; + +import "./ErrorReporter.sol"; +import "./ComptrollerStorage.sol"; +/** + * @title ComptrollerCore + * @dev Storage for the comptroller is at this address, while execution is delegated to the `comptrollerImplementation`. + * CTokens should reference this contract as their comptroller. + */ +contract Unitroller is UnitrollerAdminStorage, ComptrollerErrorReporter { + + /** + * @notice Emitted when pendingComptrollerImplementation is changed + */ + event NewPendingImplementation(address oldPendingImplementation, address newPendingImplementation); + + /** + * @notice Emitted when pendingComptrollerImplementation is accepted, which means comptroller implementation is updated + */ + event NewImplementation(address oldImplementation, address newImplementation); + + /** + * @notice Emitted when pendingAdmin is changed + */ + event NewPendingAdmin(address oldPendingAdmin, address newPendingAdmin); + + /** + * @notice Emitted when pendingAdmin is accepted, which means admin is updated + */ + event NewAdmin(address oldAdmin, address newAdmin); + + constructor() public { + // Set admin to caller + admin = msg.sender; + } + + /*** Admin Functions ***/ + function _setPendingImplementation(address newPendingImplementation) public returns (uint) { + + if (msg.sender != admin) { + return fail(Error.UNAUTHORIZED, FailureInfo.SET_PENDING_IMPLEMENTATION_OWNER_CHECK); + } + + address oldPendingImplementation = pendingComptrollerImplementation; + + pendingComptrollerImplementation = newPendingImplementation; + + emit NewPendingImplementation(oldPendingImplementation, pendingComptrollerImplementation); + + return uint(Error.NO_ERROR); + } + + /** + * @notice Accepts new implementation of comptroller. msg.sender must be pendingImplementation + * @dev Admin function for new implementation to accept it's role as implementation + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function _acceptImplementation() public returns (uint) { + // Check caller is pendingImplementation and pendingImplementation ≠ address(0) + if (msg.sender != pendingComptrollerImplementation || pendingComptrollerImplementation == address(0)) { + return fail(Error.UNAUTHORIZED, FailureInfo.ACCEPT_PENDING_IMPLEMENTATION_ADDRESS_CHECK); + } + + // Save current values for inclusion in log + address oldImplementation = comptrollerImplementation; + address oldPendingImplementation = pendingComptrollerImplementation; + + comptrollerImplementation = pendingComptrollerImplementation; + + pendingComptrollerImplementation = address(0); + + emit NewImplementation(oldImplementation, comptrollerImplementation); + emit NewPendingImplementation(oldPendingImplementation, pendingComptrollerImplementation); + + return uint(Error.NO_ERROR); + } + + + /** + * @notice Begins transfer of admin rights. The newPendingAdmin must call `_acceptAdmin` to finalize the transfer. + * @dev Admin function to begin change of admin. The newPendingAdmin must call `_acceptAdmin` to finalize the transfer. + * @param newPendingAdmin New pending admin. + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function _setPendingAdmin(address newPendingAdmin) public returns (uint) { + // Check caller = admin + if (msg.sender != admin) { + return fail(Error.UNAUTHORIZED, FailureInfo.SET_PENDING_ADMIN_OWNER_CHECK); + } + + // Save current value, if any, for inclusion in log + address oldPendingAdmin = pendingAdmin; + + // Store pendingAdmin with value newPendingAdmin + pendingAdmin = newPendingAdmin; + + // Emit NewPendingAdmin(oldPendingAdmin, newPendingAdmin) + emit NewPendingAdmin(oldPendingAdmin, newPendingAdmin); + + return uint(Error.NO_ERROR); + } + + /** + * @notice Accepts transfer of admin rights. msg.sender must be pendingAdmin + * @dev Admin function for pending admin to accept role and update admin + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function _acceptAdmin() public returns (uint) { + // Check caller is pendingAdmin and pendingAdmin ≠ address(0) + if (msg.sender != pendingAdmin || msg.sender == address(0)) { + return fail(Error.UNAUTHORIZED, FailureInfo.ACCEPT_ADMIN_PENDING_ADMIN_CHECK); + } + + // Save current values for inclusion in log + address oldAdmin = admin; + address oldPendingAdmin = pendingAdmin; + + // Store admin with value pendingAdmin + admin = pendingAdmin; + + // Clear the pending value + pendingAdmin = address(0); + + emit NewAdmin(oldAdmin, admin); + emit NewPendingAdmin(oldPendingAdmin, pendingAdmin); + + return uint(Error.NO_ERROR); + } + + /** + * @dev Delegates execution to an implementation contract. + * It returns to the external caller whatever the implementation returns + * or forwards reverts. + */ + fallback() external { + // delegate all other functions to current implementation + (bool success, ) = comptrollerImplementation.delegatecall(msg.data); + + assembly { + let free_mem_ptr := mload(0x40) + returndatacopy(free_mem_ptr, 0, returndatasize()) + + switch success + case 0 { revert(free_mem_ptr, returndatasize()) } + default { return(free_mem_ptr, returndatasize()) } + } + } + + receive() payable external {} +} diff --git a/src/integrations/compound/WhitePaperInterestRateModel.sol b/src/integrations/compound/WhitePaperInterestRateModel.sol index c47a138..593598b 100644 --- a/src/integrations/compound/WhitePaperInterestRateModel.sol +++ b/src/integrations/compound/WhitePaperInterestRateModel.sol @@ -1,83 +1,83 @@ -pragma solidity ^0.6.7; - -import "./InterestRateModel.sol"; -import "../../math/SafeMath.sol"; - -/** - * @title Compound's WhitePaperInterestRateModel Contract - * @author Compound - * @notice The parameterized model described in section 2.4 of the original Compound Protocol whitepaper - */ -contract WhitePaperInterestRateModel is InterestRateModel, SafeMath { - event NewInterestParams(uint baseRatePerBlock, uint multiplierPerBlock); - - /** - * @notice The approximate number of blocks per year that is assumed by the interest rate model - */ - uint public constant blocksPerYear = 2102400; - - /** - * @notice The multiplier of utilization rate that gives the slope of the interest rate - */ - uint public multiplierPerBlock; - - /** - * @notice The base interest rate which is the y-intercept when utilization rate is 0 - */ - uint public baseRatePerBlock; - - /** - * @notice Construct an interest rate model - * @param baseRatePerYear The approximate target base APR, as a mantissa (scaled by 1e18) - * @param multiplierPerYear The rate of increase in interest rate wrt utilization (scaled by 1e18) - */ - constructor(uint baseRatePerYear, uint multiplierPerYear) public { - baseRatePerBlock = div(baseRatePerYear, blocksPerYear); - multiplierPerBlock = div(multiplierPerYear, blocksPerYear); - - emit NewInterestParams(baseRatePerBlock, multiplierPerBlock); - } - - /** - * @notice Calculates the utilization rate of the market: `borrows / (cash + borrows - reserves)` - * @param cash The amount of cash in the market - * @param borrows The amount of borrows in the market - * @param reserves The amount of reserves in the market (currently unused) - * @return The utilization rate as a mantissa between [0, 1e18] - */ - function utilizationRate(uint cash, uint borrows, uint reserves) public pure returns (uint) { - // Utilization rate is 0 when there are no borrows - if (borrows == 0) { - return 0; - } - - return div(mul(borrows, 1e18), (sub(add(cash, borrows), reserves))); - } - - /** - * @notice Calculates the current borrow rate per block, with the error code expected by the market - * @param cash The amount of cash in the market - * @param borrows The amount of borrows in the market - * @param reserves The amount of reserves in the market - * @return The borrow rate percentage per block as a mantissa (scaled by 1e18) - */ - function getBorrowRate(uint cash, uint borrows, uint reserves) override public view returns (uint) { - uint ur = utilizationRate(cash, borrows, reserves); - return add(div(mul(ur, multiplierPerBlock), 1e18), baseRatePerBlock); - } - - /** - * @notice Calculates the current supply rate per block - * @param cash The amount of cash in the market - * @param borrows The amount of borrows in the market - * @param reserves The amount of reserves in the market - * @param reserveFactorMantissa The current reserve factor for the market - * @return The supply rate percentage per block as a mantissa (scaled by 1e18) - */ - function getSupplyRate(uint cash, uint borrows, uint reserves, uint reserveFactorMantissa) override public view returns (uint) { - uint oneMinusReserveFactor = sub(uint(1e18), reserveFactorMantissa); - uint borrowRate = getBorrowRate(cash, borrows, reserves); - uint rateToPool = div(mul(borrowRate, oneMinusReserveFactor), 1e18); - return div(mul(utilizationRate(cash, borrows, reserves), rateToPool), 1e18); - } -} +pragma solidity ^0.6.7; + +import "./InterestRateModel.sol"; +import "../../math/SafeMath.sol"; + +/** + * @title Compound's WhitePaperInterestRateModel Contract + * @author Compound + * @notice The parameterized model described in section 2.4 of the original Compound Protocol whitepaper + */ +contract WhitePaperInterestRateModel is InterestRateModel, SafeMath { + event NewInterestParams(uint baseRatePerBlock, uint multiplierPerBlock); + + /** + * @notice The approximate number of blocks per year that is assumed by the interest rate model + */ + uint public constant blocksPerYear = 2102400; + + /** + * @notice The multiplier of utilization rate that gives the slope of the interest rate + */ + uint public multiplierPerBlock; + + /** + * @notice The base interest rate which is the y-intercept when utilization rate is 0 + */ + uint public baseRatePerBlock; + + /** + * @notice Construct an interest rate model + * @param baseRatePerYear The approximate target base APR, as a mantissa (scaled by 1e18) + * @param multiplierPerYear The rate of increase in interest rate wrt utilization (scaled by 1e18) + */ + constructor(uint baseRatePerYear, uint multiplierPerYear) public { + baseRatePerBlock = div(baseRatePerYear, blocksPerYear); + multiplierPerBlock = div(multiplierPerYear, blocksPerYear); + + emit NewInterestParams(baseRatePerBlock, multiplierPerBlock); + } + + /** + * @notice Calculates the utilization rate of the market: `borrows / (cash + borrows - reserves)` + * @param cash The amount of cash in the market + * @param borrows The amount of borrows in the market + * @param reserves The amount of reserves in the market (currently unused) + * @return The utilization rate as a mantissa between [0, 1e18] + */ + function utilizationRate(uint cash, uint borrows, uint reserves) public pure returns (uint) { + // Utilization rate is 0 when there are no borrows + if (borrows == 0) { + return 0; + } + + return div(mul(borrows, 1e18), (sub(add(cash, borrows), reserves))); + } + + /** + * @notice Calculates the current borrow rate per block, with the error code expected by the market + * @param cash The amount of cash in the market + * @param borrows The amount of borrows in the market + * @param reserves The amount of reserves in the market + * @return The borrow rate percentage per block as a mantissa (scaled by 1e18) + */ + function getBorrowRate(uint cash, uint borrows, uint reserves) override public view returns (uint) { + uint ur = utilizationRate(cash, borrows, reserves); + return add(div(mul(ur, multiplierPerBlock), 1e18), baseRatePerBlock); + } + + /** + * @notice Calculates the current supply rate per block + * @param cash The amount of cash in the market + * @param borrows The amount of borrows in the market + * @param reserves The amount of reserves in the market + * @param reserveFactorMantissa The current reserve factor for the market + * @return The supply rate percentage per block as a mantissa (scaled by 1e18) + */ + function getSupplyRate(uint cash, uint borrows, uint reserves, uint reserveFactorMantissa) override public view returns (uint) { + uint oneMinusReserveFactor = sub(uint(1e18), reserveFactorMantissa); + uint borrowRate = getBorrowRate(cash, borrows, reserves); + uint rateToPool = div(mul(borrowRate, oneMinusReserveFactor), 1e18); + return div(mul(utilizationRate(cash, borrows, reserves), rateToPool), 1e18); + } +} diff --git a/src/integrations/uniswap/liquidity-managers/UniswapV2LiquidityManager.sol b/src/integrations/uniswap/liquidity-managers/UniswapV2LiquidityManager.sol index b5e9aeb..87e9a92 100644 --- a/src/integrations/uniswap/liquidity-managers/UniswapV2LiquidityManager.sol +++ b/src/integrations/uniswap/liquidity-managers/UniswapV2LiquidityManager.sol @@ -1,115 +1,115 @@ -pragma solidity 0.6.7; - -import "../uni-v2/interfaces/IUniswapV2Pair.sol"; -import "../uni-v2/interfaces/IUniswapV2Router02.sol"; - -import "../../../math/SafeMath.sol"; - -import "../../../interfaces/ERC20Like.sol"; -import "../../../interfaces/UniswapLiquidityManagerLike.sol"; - -contract UniswapV2LiquidityManager is UniswapLiquidityManagerLike, SafeMath { - // The Uniswap v2 pair handled by this contract - IUniswapV2Pair public pair; - // The official Uniswap v2 router V2 - IUniswapV2Router02 public router; - - constructor(address pair_, address router_) public { - require(pair_ != address(0), "UniswapV2LiquidityManager/null-pair"); - require(router_ != address(0), "UniswapV2LiquidityManager/null-router"); - pair = IUniswapV2Pair(pair_); - router = IUniswapV2Router02(router_); - } - - // --- Boolean Logic --- - function either(bool x, bool y) internal pure returns (bool z) { - assembly{ z := or(x, y)} - } - - // --- Public Getters --- - /* - * @notice Return the amount of token0 tokens that someone would get back by burning a specific amount of LP tokens - * @param liquidityAmount The amount of LP tokens to burn - * @return The amount of token0 tokens that someone would get back - */ - function getToken0FromLiquidity(uint256 liquidityAmount) public override view returns (uint256) { - if (liquidityAmount == 0) return 0; - - (uint256 totalSupply, uint256 cumulativeLPBalance) = getSupplyAndCumulativeLiquidity(liquidityAmount); - if (either(liquidityAmount == 0, cumulativeLPBalance > totalSupply)) return 0; - - return mul(cumulativeLPBalance, ERC20Like(pair.token0()).balanceOf(address(pair))) / totalSupply; - } - /* - * @notice Return the amount of token1 tokens that someone would get back by burning a specific amount of LP tokens - * @param liquidityAmount The amount of LP tokens to burn - * @return The amount of token1 tokens that someone would get back - */ - function getToken1FromLiquidity(uint256 liquidityAmount) public override view returns (uint256) { - if (liquidityAmount == 0) return 0; - - (uint256 totalSupply, uint256 cumulativeLPBalance) = getSupplyAndCumulativeLiquidity(liquidityAmount); - if (either(liquidityAmount == 0, cumulativeLPBalance > totalSupply)) return 0; - - return mul(cumulativeLPBalance, ERC20Like(pair.token1()).balanceOf(address(pair))) / totalSupply; - } - /* - * @notice Return the amount of LP tokens needed to withdraw a specific amount of token0 tokens - * @param token0Amount The amount of token0 tokens from which to determine the amount of LP tokens - * @return The amount of LP tokens needed to withdraw a specific amount of token0 tokens - */ - function getLiquidityFromToken0(uint256 token0Amount) public override view returns (uint256) { - if (either(token0Amount == 0, ERC20Like(address(pair.token0())).balanceOf(address(pair)) < token0Amount)) return 0; - return div(mul(token0Amount, pair.totalSupply()), ERC20Like(pair.token0()).balanceOf(address(pair))); - } - /* - * @notice Return the amount of LP tokens needed to withdraw a specific amount of token1 tokens - * @param token1Amount The amount of token1 tokens from which to determine the amount of LP tokens - * @return The amount of LP tokens needed to withdraw a specific amount of token1 tokens - */ - function getLiquidityFromToken1(uint256 token1Amount) public override view returns (uint256) { - if (either(token1Amount == 0, ERC20Like(address(pair.token1())).balanceOf(address(pair)) < token1Amount)) return 0; - return div(mul(token1Amount, pair.totalSupply()), ERC20Like(pair.token1()).balanceOf(address(pair))); - } - - // --- Internal Getters --- - /* - * @notice Internal view function that returns the total supply of LP tokens in the 'pair' as well as the LP - * token balance of the pair contract itself if it were to have liquidityAmount extra tokens - * @param liquidityAmount The amount of LP tokens that would be burned - * @return The total supply of LP tokens in the 'pair' as well as the LP token balance - * of the pair contract itself if it were to have liquidityAmount extra tokens - */ - function getSupplyAndCumulativeLiquidity(uint256 liquidityAmount) internal view returns (uint256, uint256) { - return (pair.totalSupply(), add(pair.balanceOf(address(pair)), liquidityAmount)); - } - - // --- Liquidity Removal Logic --- - /* - * @notice Remove liquidity from the Uniswap pool - * @param liquidity The amount of LP tokens to burn - * @param amount0Min The min amount of token0 requested - * @param amount1Min The min amount of token1 requested - * @param to The address that receives token0 and token1 tokens after liquidity is removed - * @return The amounts of token0 and token1 tokens returned - */ - function removeLiquidity( - uint256 liquidity, - uint128 amount0Min, - uint128 amount1Min, - address to - ) public override returns (uint256 amount0, uint256 amount1) { - require(to != address(0), "UniswapV2LiquidityManager/null-dst"); - pair.transferFrom(msg.sender, address(this), liquidity); - pair.approve(address(router), liquidity); - (amount0, amount1) = router.removeLiquidity( - pair.token0(), - pair.token1(), - liquidity, - uint(amount0Min), - uint(amount1Min), - to, - now - ); - } -} +pragma solidity 0.6.7; + +import "../uni-v2/interfaces/IUniswapV2Pair.sol"; +import "../uni-v2/interfaces/IUniswapV2Router02.sol"; + +import "../../../math/SafeMath.sol"; + +import "../../../interfaces/ERC20Like.sol"; +import "../../../interfaces/UniswapLiquidityManagerLike.sol"; + +contract UniswapV2LiquidityManager is UniswapLiquidityManagerLike, SafeMath { + // The Uniswap v2 pair handled by this contract + IUniswapV2Pair public pair; + // The official Uniswap v2 router V2 + IUniswapV2Router02 public router; + + constructor(address pair_, address router_) public { + require(pair_ != address(0), "UniswapV2LiquidityManager/null-pair"); + require(router_ != address(0), "UniswapV2LiquidityManager/null-router"); + pair = IUniswapV2Pair(pair_); + router = IUniswapV2Router02(router_); + } + + // --- Boolean Logic --- + function either(bool x, bool y) internal pure returns (bool z) { + assembly{ z := or(x, y)} + } + + // --- Public Getters --- + /* + * @notice Return the amount of token0 tokens that someone would get back by burning a specific amount of LP tokens + * @param liquidityAmount The amount of LP tokens to burn + * @return The amount of token0 tokens that someone would get back + */ + function getToken0FromLiquidity(uint256 liquidityAmount) public override view returns (uint256) { + if (liquidityAmount == 0) return 0; + + (uint256 totalSupply, uint256 cumulativeLPBalance) = getSupplyAndCumulativeLiquidity(liquidityAmount); + if (either(liquidityAmount == 0, cumulativeLPBalance > totalSupply)) return 0; + + return mul(cumulativeLPBalance, ERC20Like(pair.token0()).balanceOf(address(pair))) / totalSupply; + } + /* + * @notice Return the amount of token1 tokens that someone would get back by burning a specific amount of LP tokens + * @param liquidityAmount The amount of LP tokens to burn + * @return The amount of token1 tokens that someone would get back + */ + function getToken1FromLiquidity(uint256 liquidityAmount) public override view returns (uint256) { + if (liquidityAmount == 0) return 0; + + (uint256 totalSupply, uint256 cumulativeLPBalance) = getSupplyAndCumulativeLiquidity(liquidityAmount); + if (either(liquidityAmount == 0, cumulativeLPBalance > totalSupply)) return 0; + + return mul(cumulativeLPBalance, ERC20Like(pair.token1()).balanceOf(address(pair))) / totalSupply; + } + /* + * @notice Return the amount of LP tokens needed to withdraw a specific amount of token0 tokens + * @param token0Amount The amount of token0 tokens from which to determine the amount of LP tokens + * @return The amount of LP tokens needed to withdraw a specific amount of token0 tokens + */ + function getLiquidityFromToken0(uint256 token0Amount) public override view returns (uint256) { + if (either(token0Amount == 0, ERC20Like(address(pair.token0())).balanceOf(address(pair)) < token0Amount)) return 0; + return div(mul(token0Amount, pair.totalSupply()), ERC20Like(pair.token0()).balanceOf(address(pair))); + } + /* + * @notice Return the amount of LP tokens needed to withdraw a specific amount of token1 tokens + * @param token1Amount The amount of token1 tokens from which to determine the amount of LP tokens + * @return The amount of LP tokens needed to withdraw a specific amount of token1 tokens + */ + function getLiquidityFromToken1(uint256 token1Amount) public override view returns (uint256) { + if (either(token1Amount == 0, ERC20Like(address(pair.token1())).balanceOf(address(pair)) < token1Amount)) return 0; + return div(mul(token1Amount, pair.totalSupply()), ERC20Like(pair.token1()).balanceOf(address(pair))); + } + + // --- Internal Getters --- + /* + * @notice Internal view function that returns the total supply of LP tokens in the 'pair' as well as the LP + * token balance of the pair contract itself if it were to have liquidityAmount extra tokens + * @param liquidityAmount The amount of LP tokens that would be burned + * @return The total supply of LP tokens in the 'pair' as well as the LP token balance + * of the pair contract itself if it were to have liquidityAmount extra tokens + */ + function getSupplyAndCumulativeLiquidity(uint256 liquidityAmount) internal view returns (uint256, uint256) { + return (pair.totalSupply(), add(pair.balanceOf(address(pair)), liquidityAmount)); + } + + // --- Liquidity Removal Logic --- + /* + * @notice Remove liquidity from the Uniswap pool + * @param liquidity The amount of LP tokens to burn + * @param amount0Min The min amount of token0 requested + * @param amount1Min The min amount of token1 requested + * @param to The address that receives token0 and token1 tokens after liquidity is removed + * @return The amounts of token0 and token1 tokens returned + */ + function removeLiquidity( + uint256 liquidity, + uint128 amount0Min, + uint128 amount1Min, + address to + ) public override returns (uint256 amount0, uint256 amount1) { + require(to != address(0), "UniswapV2LiquidityManager/null-dst"); + pair.transferFrom(msg.sender, address(this), liquidity); + pair.approve(address(router), liquidity); + (amount0, amount1) = router.removeLiquidity( + pair.token0(), + pair.token1(), + liquidity, + uint(amount0Min), + uint(amount1Min), + to, + now + ); + } +} diff --git a/src/integrations/uniswap/liquidity-managers/UniswapV3LiquidityManager.sol b/src/integrations/uniswap/liquidity-managers/UniswapV3LiquidityManager.sol deleted file mode 100644 index 9880ecc..0000000 --- a/src/integrations/uniswap/liquidity-managers/UniswapV3LiquidityManager.sol +++ /dev/null @@ -1,46 +0,0 @@ -pragma solidity 0.6.7; - -import "../../../math/SafeMath.sol"; - -abstract contract GebUniswapV3LiquidityManager { - function getToken0FromLiquidity(uint256) virtual public view returns (uint256); - function getToken1FromLiquidity(uint256) virtual public view returns (uint256); - function getLiquidityFromToken0(uint256) virtual public view returns (uint256); - function getLiquidityFromToken1(uint256) virtual public view returns (uint256); - function transferFrom(address, address, uint256) virtual public returns (bool); - function withdraw(uint256, address, uint128, uint128) virtual external returns (uint256, uint256); -} - -contract UniswapV3LiquidityManager is SafeMath { - GebUniswapV3LiquidityManager public gebLiquidityManager; - - constructor(address gebLiquidityManager_) public { - require(gebLiquidityManager_ != address(0), "UniswapV3LiquidityManager/null-manager"); - gebLiquidityManager = GebUniswapV3LiquidityManager(gebLiquidityManager_); - } - - // --- Liquidity Removal Logic --- - /* - * @notice Remove liquidity from the Uniswap pool - * @param liquidity The amount of LP tokens to burn - * @param amount0Min The min amount of token0 requested - * @param amount1Min The min amount of token1 requested - * @param to The address that receives token0 and token1 tokens after liquidity is removed - * @return The amounts of token0 and token1 tokens returned - */ - function removeLiquidity( - uint256 liquidity, - uint128 amount0Min, - uint128 amount1Min, - address to - ) public returns (uint256 amount0, uint256 amount1) { - require(to != address(0), "UniswapV3LiquidityManager/null-dst"); - gebLiquidityManager.transferFrom(msg.sender, address(this), liquidity); - (amount0, amount1) = gebLiquidityManager.withdraw( - liquidity, - to, - amount0Min, - amount1Min - ); - } -} diff --git a/src/integrations/uniswap/swappers/UniswapV2Swapper.sol b/src/integrations/uniswap/swappers/UniswapV2Swapper.sol index 8c3e27c..56831a9 100644 --- a/src/integrations/uniswap/swappers/UniswapV2Swapper.sol +++ b/src/integrations/uniswap/swappers/UniswapV2Swapper.sol @@ -1,134 +1,134 @@ -pragma solidity 0.6.7; - -import "../uni-v2/interfaces/IUniswapV2Router02.sol"; -import "../uni-v2/interfaces/IUniswapV2Factory.sol"; -import "../uni-v2/interfaces/IUniswapV2Pair.sol"; - -import "../../../math/SafeMath.sol"; - -import "../../../interfaces/ERC20Like.sol"; -import "../../../interfaces/SwapManagerLike.sol"; - -import "../../../utils/ReentrancyGuard.sol"; - -contract UniswapV2Swapper is ReentrancyGuard, SwapManagerLike { - // --- Variables --- - // The official Uniswap v2 router V2 - IUniswapV2Router02 public router; - // The official Uniswap v2 factory - IUniswapV2Factory public factory; - // Array of tokens to be swapped - address[] tokenPath; - - constructor(address factory_, address router_) public { - require(factory_ != address(0), "UniswapV2Swapper/null-factory"); - require(router_ != address(0), "UniswapV2Swapper/null-router"); - - factory = IUniswapV2Factory(factory_); - router = IUniswapV2Router02(router_); - } - - // --- Math --- - function uniAddition(uint x, uint y) internal pure returns (uint z) { - require((z = x + y) >= x, 'UniswapV2Swapper/add-overflow'); - } - function uniSubtract(uint x, uint y) internal pure returns (uint z) { - require((z = x - y) <= x, 'UniswapV2Swapper/sub-underflow'); - } - function uniMultiply(uint x, uint y) internal pure returns (uint z) { - require(y == 0 || (z = x * y) / y == x, 'UniswapV2Swapper/mul-overflow'); - } - - // --- Boolean Logic --- - function both(bool x, bool y) internal pure returns (bool z) { - assembly{ z := and(x, y)} - } - - // --- Core Logic --- - /* - * @notice Swap from one token to another - * @param tokenIn The token that's being sold - * @param tokenOut The token that's being bought - * @param amountIn The amount of tokens being sold - * @param amountOutMin The minimum amount of tokens being bought - * @param to The address that will receive the bought tokens - */ - function swap( - address tokenIn, - address tokenOut, - uint amountIn, - uint amountOutMin, - address to - ) external override nonReentrant returns (uint256 amountOut) { - require(amountIn > 0, "UniswapV2Swapper/null-amount-in"); - require(to != address(0), "UniswapV2Swapper/null-dst"); - - ERC20Like(tokenIn).transferFrom(msg.sender, address(this), amountIn); - ERC20Like(tokenIn).approve(address(router), amountIn); - - tokenPath.push(tokenIn); - tokenPath.push(tokenOut); - - router.swapExactTokensForTokens(amountIn, 1, tokenPath, to, now); - delete(tokenPath); - } - - // --- Public Getters --- - /* - * @notice Return the amount of tokens bought given a specific pair and an amount of tokens being sold - * @param tokenIn The token that's being sold - * @param tokenOut The token that's being bought - * @param amountIn The amount of tokens being sold - * @return amountOut The amount of tokens that can be bought - */ - function getAmountOut(address tokenIn, address tokenOut, uint amountIn) public override view returns (uint256 amountOut) { - require(amountIn > 0, 'UniswapV2Swapper/null-amount-in'); - - (uint reserveIn, uint reserveOut) = getReserves(tokenIn, tokenOut); - require(both(reserveIn > 0, reserveOut > 0), 'UniswapV2Swapper/insufficient-liquidity'); - - uint amountInWithFee = uniMultiply(amountIn, 997); - uint numerator = uniMultiply(amountInWithFee, reserveOut); - uint denominator = uniAddition(uniMultiply(reserveIn, 1000), amountInWithFee); - amountOut = numerator / denominator; - } - /* - * @notice Return the length of the tokenPath array - */ - function getTokenPathLength() public view returns (uint256) { - return tokenPath.length; - } - - // --- Internal Logic --- - /* - * @notice Returns sorted token addresses, used to handle return values from pairs sorted in this order - * @param tokenA One of the tokens in a pair - * @param tokenB A second token in a pair - */ - function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) { - require(tokenA != tokenB, 'UniswapV2Swapper/identical-tokens'); - (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); - require(token0 != address(0), 'UniswapV2Swapper/null-token'); - } - - /* - * @notice Return a pair address given two tokens - * @param tokenA One of the tokens in a pair - * @param tokenB A second token in a pair - */ - function pairFor(address tokenA, address tokenB) internal view returns (address pair) { - (address token0, address token1) = sortTokens(tokenA, tokenB); - return factory.getPair(tokenA, tokenB); - } - - /* - * @notice Fetches and sorts the reserves for a pair - * @param tokenA One of the tokens in a pair - * @param tokenB A second token in a pair - */ - function getReserves(address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) { - (address token0,) = sortTokens(tokenA, tokenB); - (uint reserve0, uint reserve1,) = IUniswapV2Pair(factory.getPair(tokenA, tokenB)).getReserves(); - (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0); - } -} +pragma solidity 0.6.7; + +import "../uni-v2/interfaces/IUniswapV2Router02.sol"; +import "../uni-v2/interfaces/IUniswapV2Factory.sol"; +import "../uni-v2/interfaces/IUniswapV2Pair.sol"; + +import "../../../math/SafeMath.sol"; + +import "../../../interfaces/ERC20Like.sol"; +import "../../../interfaces/SwapManagerLike.sol"; + +import "../../../utils/ReentrancyGuard.sol"; + +contract UniswapV2Swapper is ReentrancyGuard, SwapManagerLike { + // --- Variables --- + // The official Uniswap v2 router V2 + IUniswapV2Router02 public router; + // The official Uniswap v2 factory + IUniswapV2Factory public factory; + // Array of tokens to be swapped + address[] tokenPath; + + constructor(address factory_, address router_) public { + require(factory_ != address(0), "UniswapV2Swapper/null-factory"); + require(router_ != address(0), "UniswapV2Swapper/null-router"); + + factory = IUniswapV2Factory(factory_); + router = IUniswapV2Router02(router_); + } + + // --- Math --- + function uniAddition(uint x, uint y) internal pure returns (uint z) { + require((z = x + y) >= x, 'UniswapV2Swapper/add-overflow'); + } + function uniSubtract(uint x, uint y) internal pure returns (uint z) { + require((z = x - y) <= x, 'UniswapV2Swapper/sub-underflow'); + } + function uniMultiply(uint x, uint y) internal pure returns (uint z) { + require(y == 0 || (z = x * y) / y == x, 'UniswapV2Swapper/mul-overflow'); + } + + // --- Boolean Logic --- + function both(bool x, bool y) internal pure returns (bool z) { + assembly{ z := and(x, y)} + } + + // --- Core Logic --- + /* + * @notice Swap from one token to another + * @param tokenIn The token that's being sold + * @param tokenOut The token that's being bought + * @param amountIn The amount of tokens being sold + * @param amountOutMin The minimum amount of tokens being bought + * @param to The address that will receive the bought tokens + */ + function swap( + address tokenIn, + address tokenOut, + uint amountIn, + uint amountOutMin, + address to + ) external override nonReentrant returns (uint256 amountOut) { + require(amountIn > 0, "UniswapV2Swapper/null-amount-in"); + require(to != address(0), "UniswapV2Swapper/null-dst"); + + ERC20Like(tokenIn).transferFrom(msg.sender, address(this), amountIn); + ERC20Like(tokenIn).approve(address(router), amountIn); + + tokenPath.push(tokenIn); + tokenPath.push(tokenOut); + + router.swapExactTokensForTokens(amountIn, 1, tokenPath, to, now); + delete(tokenPath); + } + + // --- Public Getters --- + /* + * @notice Return the amount of tokens bought given a specific pair and an amount of tokens being sold + * @param tokenIn The token that's being sold + * @param tokenOut The token that's being bought + * @param amountIn The amount of tokens being sold + * @return amountOut The amount of tokens that can be bought + */ + function getAmountOut(address tokenIn, address tokenOut, uint amountIn) public override view returns (uint256 amountOut) { + require(amountIn > 0, 'UniswapV2Swapper/null-amount-in'); + + (uint reserveIn, uint reserveOut) = getReserves(tokenIn, tokenOut); + require(both(reserveIn > 0, reserveOut > 0), 'UniswapV2Swapper/insufficient-liquidity'); + + uint amountInWithFee = uniMultiply(amountIn, 997); + uint numerator = uniMultiply(amountInWithFee, reserveOut); + uint denominator = uniAddition(uniMultiply(reserveIn, 1000), amountInWithFee); + amountOut = numerator / denominator; + } + /* + * @notice Return the length of the tokenPath array + */ + function getTokenPathLength() public view returns (uint256) { + return tokenPath.length; + } + + // --- Internal Logic --- + /* + * @notice Returns sorted token addresses, used to handle return values from pairs sorted in this order + * @param tokenA One of the tokens in a pair + * @param tokenB A second token in a pair + */ + function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) { + require(tokenA != tokenB, 'UniswapV2Swapper/identical-tokens'); + (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); + require(token0 != address(0), 'UniswapV2Swapper/null-token'); + } + + /* + * @notice Return a pair address given two tokens + * @param tokenA One of the tokens in a pair + * @param tokenB A second token in a pair + */ + function pairFor(address tokenA, address tokenB) internal view returns (address pair) { + (address token0, address token1) = sortTokens(tokenA, tokenB); + return factory.getPair(tokenA, tokenB); + } + + /* + * @notice Fetches and sorts the reserves for a pair + * @param tokenA One of the tokens in a pair + * @param tokenB A second token in a pair + */ + function getReserves(address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) { + (address token0,) = sortTokens(tokenA, tokenB); + (uint reserve0, uint reserve1,) = IUniswapV2Pair(factory.getPair(tokenA, tokenB)).getReserves(); + (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0); + } +} diff --git a/src/integrations/uniswap/uni-v2/UniswapV2Factory.sol b/src/integrations/uniswap/uni-v2/UniswapV2Factory.sol index 557aaa7..dfc11a4 100644 --- a/src/integrations/uniswap/uni-v2/UniswapV2Factory.sol +++ b/src/integrations/uniswap/uni-v2/UniswapV2Factory.sol @@ -1,45 +1,45 @@ -pragma solidity 0.6.7; - -import './interfaces/IUniswapV2Factory.sol'; -import './UniswapV2Pair.sol'; - -contract UniswapV2Factory is IUniswapV2Factory { - address override public feeTo; - address override public feeToSetter; - - mapping(address => mapping(address => address)) override public getPair; - address[] override public allPairs; - - event PairCreated(address indexed token0, address indexed token1, address pair, uint); - - constructor(address _feeToSetter) public { - feeToSetter = _feeToSetter; - } - - function allPairsLength() override external view returns (uint) { - return allPairs.length; - } - - function createPair(address tokenA, address tokenB) override external returns (address pair) { - require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES'); - (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); - require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS'); - require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient - pair = address(new UniswapV2Pair()); - IUniswapV2Pair(pair).initialize(token0, token1); - getPair[token0][token1] = pair; - getPair[token1][token0] = pair; // populate mapping in the reverse direction - allPairs.push(pair); - emit PairCreated(token0, token1, pair, allPairs.length); - } - - function setFeeTo(address _feeTo) override external { - require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN'); - feeTo = _feeTo; - } - - function setFeeToSetter(address _feeToSetter) override external { - require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN'); - feeToSetter = _feeToSetter; - } -} +pragma solidity 0.6.7; + +import './interfaces/IUniswapV2Factory.sol'; +import './UniswapV2Pair.sol'; + +contract UniswapV2Factory is IUniswapV2Factory { + address override public feeTo; + address override public feeToSetter; + + mapping(address => mapping(address => address)) override public getPair; + address[] override public allPairs; + + event PairCreated(address indexed token0, address indexed token1, address pair, uint); + + constructor(address _feeToSetter) public { + feeToSetter = _feeToSetter; + } + + function allPairsLength() override external view returns (uint) { + return allPairs.length; + } + + function createPair(address tokenA, address tokenB) override external returns (address pair) { + require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES'); + (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); + require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS'); + require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient + pair = address(new UniswapV2Pair()); + IUniswapV2Pair(pair).initialize(token0, token1); + getPair[token0][token1] = pair; + getPair[token1][token0] = pair; // populate mapping in the reverse direction + allPairs.push(pair); + emit PairCreated(token0, token1, pair, allPairs.length); + } + + function setFeeTo(address _feeTo) override external { + require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN'); + feeTo = _feeTo; + } + + function setFeeToSetter(address _feeToSetter) override external { + require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN'); + feeToSetter = _feeToSetter; + } +} diff --git a/src/integrations/uniswap/uni-v2/UniswapV2Pair.sol b/src/integrations/uniswap/uni-v2/UniswapV2Pair.sol index ac95cfc..bbfa8bc 100644 --- a/src/integrations/uniswap/uni-v2/UniswapV2Pair.sol +++ b/src/integrations/uniswap/uni-v2/UniswapV2Pair.sol @@ -1,282 +1,282 @@ -pragma solidity 0.6.7; - -import './interfaces/IUniswapV2Pair.sol'; -import '../../../math/Math.sol'; -import '../../../math/SafeMath.sol'; -import '../../../math/UQ112x112.sol'; -import './interfaces/IERC20.sol'; -import './interfaces/IUniswapV2Factory.sol'; -import './interfaces/IUniswapV2Callee.sol'; - -contract UniswapV2Pair is UQ112x112, Math, SafeMath { - // --- Local Vars --- - uint public constant MINIMUM_LIQUIDITY = 10**3; - bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)'))); - - address public factory; - address public token0; - address public token1; - - uint112 private reserve0; // uses single storage slot, accessible via getReserves - uint112 private reserve1; // uses single storage slot, accessible via getReserves - uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves - - uint public price0CumulativeLast; - uint public price1CumulativeLast; - uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event - - uint private unlocked = 1; - modifier lock() { - require(unlocked == 1, 'UniswapV2: LOCKED'); - unlocked = 0; - _; - unlocked = 1; - } - - event Mint(address indexed sender, uint amount0, uint amount1); - event Burn(address indexed sender, uint amount0, uint amount1, address indexed to); - event Swap( - address indexed sender, - uint amount0In, - uint amount1In, - uint amount0Out, - uint amount1Out, - address indexed to - ); - event Sync(uint112 reserve0, uint112 reserve1); - - // --- Inherited Vars --- - string public constant name = 'Uniswap V2'; - string public constant symbol = 'UNI-V2'; - uint8 public constant decimals = 18; - uint public totalSupply; - mapping(address => uint) public balanceOf; - mapping(address => mapping(address => uint)) public allowance; - - bytes32 public DOMAIN_SEPARATOR; - // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); - bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; - mapping(address => uint) public nonces; - - event Approval(address indexed owner, address indexed spender, uint value); - event Transfer(address indexed from, address indexed to, uint value); - - constructor() public { - factory = msg.sender; - uint chainId = 1; - DOMAIN_SEPARATOR = keccak256( - abi.encode( - keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'), - keccak256(bytes(name)), - keccak256(bytes('1')), - chainId, - address(this) - ) - ); - } - - // --- Inherited --- - function _mint(address to, uint value) internal { - totalSupply = add(totalSupply, value); - balanceOf[to] = add(balanceOf[to], value); - emit Transfer(address(0), to, value); - } - - function _burn(address from, uint value) internal { - balanceOf[from] = sub(balanceOf[from], value); - totalSupply = sub(totalSupply, value); - emit Transfer(from, address(0), value); - } - - function _approve(address owner, address spender, uint value) private { - allowance[owner][spender] = value; - emit Approval(owner, spender, value); - } - - function _transfer(address from, address to, uint value) private { - balanceOf[from] = sub(balanceOf[from], value); - balanceOf[to] = add(balanceOf[to], value); - emit Transfer(from, to, value); - } - - function approve(address spender, uint value) public returns (bool) { - _approve(msg.sender, spender, value); - return true; - } - - function transfer(address to, uint value) public returns (bool) { - _transfer(msg.sender, to, value); - return true; - } - - function transferFrom(address from, address to, uint value) public returns (bool) { - if (allowance[from][msg.sender] != uint(-1)) { - allowance[from][msg.sender] = sub(allowance[from][msg.sender], value); - } - _transfer(from, to, value); - return true; - } - - // --- Local --- - function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external { - require(deadline >= block.timestamp, 'UniswapV2: EXPIRED'); - bytes32 digest = keccak256( - abi.encodePacked( - '\x19\x01', - DOMAIN_SEPARATOR, - keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)) - ) - ); - address recoveredAddress = ecrecover(digest, v, r, s); - require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE'); - _approve(owner, spender, value); - } - - function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) { - _reserve0 = reserve0; - _reserve1 = reserve1; - _blockTimestampLast = blockTimestampLast; - } - - function _safeTransfer(address token, address to, uint value) private { - (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value)); - require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED'); - } - - // called once by the factory at time of deployment - function initialize(address _token0, address _token1) external { - require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check - token0 = _token0; - token1 = _token1; - } - - // update reserves and, on the first call per block, price accumulators - function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private { - require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW'); - uint32 blockTimestamp = uint32(block.timestamp % 2**32); - uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired - if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) { - // * never overflows, and + overflow is desired - price0CumulativeLast += uint(uqdiv(encode(_reserve1), _reserve0)) * timeElapsed; - price1CumulativeLast += uint(uqdiv(encode(_reserve0), _reserve1)) * timeElapsed; - } - reserve0 = uint112(balance0); - reserve1 = uint112(balance1); - blockTimestampLast = blockTimestamp; - emit Sync(reserve0, reserve1); - } - - // if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k) - function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) { - address feeTo = IUniswapV2Factory(factory).feeTo(); - feeOn = feeTo != address(0); - uint _kLast = kLast; // gas savings - if (feeOn) { - if (_kLast != 0) { - uint rootK = sqrt(mul(uint(_reserve0), _reserve1)); - uint rootKLast = sqrt(_kLast); - if (rootK > rootKLast) { - uint numerator = mul(totalSupply, sub(rootK, rootKLast)); - uint denominator = add(mul(rootK, 5), rootKLast); - uint liquidity = numerator / denominator; - if (liquidity > 0) _mint(feeTo, liquidity); - } - } - } else if (_kLast != 0) { - kLast = 0; - } - } - - // this low-level function should be called from a contract which performs important safety checks - function mint(address to) external lock returns (uint liquidity) { - (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings - uint balance0 = IERC20(token0).balanceOf(address(this)); - uint balance1 = IERC20(token1).balanceOf(address(this)); - uint amount0 = sub(balance0, _reserve0); - uint amount1 = sub(balance1, _reserve1); - - bool feeOn = _mintFee(_reserve0, _reserve1); - uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee - if (_totalSupply == 0) { - liquidity = sub(sqrt(mul(amount0, amount1)), MINIMUM_LIQUIDITY); - _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens - } else { - liquidity = min(mul(amount0, _totalSupply) / _reserve0, mul(amount1, _totalSupply) / _reserve1); - } - require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED'); - _mint(to, liquidity); - - _update(balance0, balance1, _reserve0, _reserve1); - if (feeOn) kLast = mul(uint(reserve0), reserve1); // reserve0 and reserve1 are up-to-date - emit Mint(msg.sender, amount0, amount1); - } - - // this low-level function should be called from a contract which performs important safety checks - function burn(address to) external lock returns (uint amount0, uint amount1) { - (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings - address _token0 = token0; // gas savings - address _token1 = token1; // gas savings - uint balance0 = IERC20(_token0).balanceOf(address(this)); - uint balance1 = IERC20(_token1).balanceOf(address(this)); - uint liquidity = balanceOf[address(this)]; - - bool feeOn = _mintFee(_reserve0, _reserve1); - uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee - amount0 = mul(liquidity, balance0) / _totalSupply; // using balances ensures pro-rata distribution - amount1 = mul(liquidity, balance1) / _totalSupply; // using balances ensures pro-rata distribution - require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED'); - _burn(address(this), liquidity); - _safeTransfer(_token0, to, amount0); - _safeTransfer(_token1, to, amount1); - balance0 = IERC20(_token0).balanceOf(address(this)); - balance1 = IERC20(_token1).balanceOf(address(this)); - - _update(balance0, balance1, _reserve0, _reserve1); - if (feeOn) kLast = mul(uint(reserve0), reserve1); // reserve0 and reserve1 are up-to-date - emit Burn(msg.sender, amount0, amount1, to); - } - - // this low-level function should be called from a contract which performs important safety checks - function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock { - require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT'); - (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings - require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY'); - - uint balance0; - uint balance1; - { // scope for _token{0,1}, avoids stack too deep errors - address _token0 = token0; - address _token1 = token1; - require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO'); - if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens - if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens - if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data); - balance0 = IERC20(_token0).balanceOf(address(this)); - balance1 = IERC20(_token1).balanceOf(address(this)); - } - uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0; - uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0; - require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT'); - { // scope for reserve{0,1}Adjusted, avoids stack too deep errors - uint balance0Adjusted = sub(mul(balance0, 1000), mul(amount0In, 3)); - uint balance1Adjusted = sub(mul(balance1, 1000), mul(amount1In, 3)); - require(mul(balance0Adjusted, balance1Adjusted) >= mul(mul(uint(_reserve0), _reserve1), 1000**2), 'UniswapV2: K'); - } - - _update(balance0, balance1, _reserve0, _reserve1); - emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to); - } - - // force balances to match reserves - function skim(address to) external lock { - address _token0 = token0; // gas savings - address _token1 = token1; // gas savings - _safeTransfer(_token0, to, sub(IERC20(_token0).balanceOf(address(this)), reserve0)); - _safeTransfer(_token1, to, sub(IERC20(_token1).balanceOf(address(this)), reserve1)); - } - - // force reserves to match balances - function sync() external lock { - _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1); - } -} +pragma solidity 0.6.7; + +import './interfaces/IUniswapV2Pair.sol'; +import '../../../math/Math.sol'; +import '../../../math/SafeMath.sol'; +import '../../../math/UQ112x112.sol'; +import './interfaces/IERC20.sol'; +import './interfaces/IUniswapV2Factory.sol'; +import './interfaces/IUniswapV2Callee.sol'; + +contract UniswapV2Pair is UQ112x112, Math, SafeMath { + // --- Local Vars --- + uint public constant MINIMUM_LIQUIDITY = 10**3; + bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)'))); + + address public factory; + address public token0; + address public token1; + + uint112 private reserve0; // uses single storage slot, accessible via getReserves + uint112 private reserve1; // uses single storage slot, accessible via getReserves + uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves + + uint public price0CumulativeLast; + uint public price1CumulativeLast; + uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event + + uint private unlocked = 1; + modifier lock() { + require(unlocked == 1, 'UniswapV2: LOCKED'); + unlocked = 0; + _; + unlocked = 1; + } + + event Mint(address indexed sender, uint amount0, uint amount1); + event Burn(address indexed sender, uint amount0, uint amount1, address indexed to); + event Swap( + address indexed sender, + uint amount0In, + uint amount1In, + uint amount0Out, + uint amount1Out, + address indexed to + ); + event Sync(uint112 reserve0, uint112 reserve1); + + // --- Inherited Vars --- + string public constant name = 'Uniswap V2'; + string public constant symbol = 'UNI-V2'; + uint8 public constant decimals = 18; + uint public totalSupply; + mapping(address => uint) public balanceOf; + mapping(address => mapping(address => uint)) public allowance; + + bytes32 public DOMAIN_SEPARATOR; + // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; + mapping(address => uint) public nonces; + + event Approval(address indexed owner, address indexed spender, uint value); + event Transfer(address indexed from, address indexed to, uint value); + + constructor() public { + factory = msg.sender; + uint chainId = 1; + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'), + keccak256(bytes(name)), + keccak256(bytes('1')), + chainId, + address(this) + ) + ); + } + + // --- Inherited --- + function _mint(address to, uint value) internal { + totalSupply = add(totalSupply, value); + balanceOf[to] = add(balanceOf[to], value); + emit Transfer(address(0), to, value); + } + + function _burn(address from, uint value) internal { + balanceOf[from] = sub(balanceOf[from], value); + totalSupply = sub(totalSupply, value); + emit Transfer(from, address(0), value); + } + + function _approve(address owner, address spender, uint value) private { + allowance[owner][spender] = value; + emit Approval(owner, spender, value); + } + + function _transfer(address from, address to, uint value) private { + balanceOf[from] = sub(balanceOf[from], value); + balanceOf[to] = add(balanceOf[to], value); + emit Transfer(from, to, value); + } + + function approve(address spender, uint value) public returns (bool) { + _approve(msg.sender, spender, value); + return true; + } + + function transfer(address to, uint value) public returns (bool) { + _transfer(msg.sender, to, value); + return true; + } + + function transferFrom(address from, address to, uint value) public returns (bool) { + if (allowance[from][msg.sender] != uint(-1)) { + allowance[from][msg.sender] = sub(allowance[from][msg.sender], value); + } + _transfer(from, to, value); + return true; + } + + // --- Local --- + function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external { + require(deadline >= block.timestamp, 'UniswapV2: EXPIRED'); + bytes32 digest = keccak256( + abi.encodePacked( + '\x19\x01', + DOMAIN_SEPARATOR, + keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)) + ) + ); + address recoveredAddress = ecrecover(digest, v, r, s); + require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE'); + _approve(owner, spender, value); + } + + function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) { + _reserve0 = reserve0; + _reserve1 = reserve1; + _blockTimestampLast = blockTimestampLast; + } + + function _safeTransfer(address token, address to, uint value) private { + (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value)); + require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED'); + } + + // called once by the factory at time of deployment + function initialize(address _token0, address _token1) external { + require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check + token0 = _token0; + token1 = _token1; + } + + // update reserves and, on the first call per block, price accumulators + function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private { + require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW'); + uint32 blockTimestamp = uint32(block.timestamp % 2**32); + uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired + if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) { + // * never overflows, and + overflow is desired + price0CumulativeLast += uint(uqdiv(encode(_reserve1), _reserve0)) * timeElapsed; + price1CumulativeLast += uint(uqdiv(encode(_reserve0), _reserve1)) * timeElapsed; + } + reserve0 = uint112(balance0); + reserve1 = uint112(balance1); + blockTimestampLast = blockTimestamp; + emit Sync(reserve0, reserve1); + } + + // if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k) + function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) { + address feeTo = IUniswapV2Factory(factory).feeTo(); + feeOn = feeTo != address(0); + uint _kLast = kLast; // gas savings + if (feeOn) { + if (_kLast != 0) { + uint rootK = sqrt(mul(uint(_reserve0), _reserve1)); + uint rootKLast = sqrt(_kLast); + if (rootK > rootKLast) { + uint numerator = mul(totalSupply, sub(rootK, rootKLast)); + uint denominator = add(mul(rootK, 5), rootKLast); + uint liquidity = numerator / denominator; + if (liquidity > 0) _mint(feeTo, liquidity); + } + } + } else if (_kLast != 0) { + kLast = 0; + } + } + + // this low-level function should be called from a contract which performs important safety checks + function mint(address to) external lock returns (uint liquidity) { + (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings + uint balance0 = IERC20(token0).balanceOf(address(this)); + uint balance1 = IERC20(token1).balanceOf(address(this)); + uint amount0 = sub(balance0, _reserve0); + uint amount1 = sub(balance1, _reserve1); + + bool feeOn = _mintFee(_reserve0, _reserve1); + uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee + if (_totalSupply == 0) { + liquidity = sub(sqrt(mul(amount0, amount1)), MINIMUM_LIQUIDITY); + _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens + } else { + liquidity = min(mul(amount0, _totalSupply) / _reserve0, mul(amount1, _totalSupply) / _reserve1); + } + require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED'); + _mint(to, liquidity); + + _update(balance0, balance1, _reserve0, _reserve1); + if (feeOn) kLast = mul(uint(reserve0), reserve1); // reserve0 and reserve1 are up-to-date + emit Mint(msg.sender, amount0, amount1); + } + + // this low-level function should be called from a contract which performs important safety checks + function burn(address to) external lock returns (uint amount0, uint amount1) { + (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings + address _token0 = token0; // gas savings + address _token1 = token1; // gas savings + uint balance0 = IERC20(_token0).balanceOf(address(this)); + uint balance1 = IERC20(_token1).balanceOf(address(this)); + uint liquidity = balanceOf[address(this)]; + + bool feeOn = _mintFee(_reserve0, _reserve1); + uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee + amount0 = mul(liquidity, balance0) / _totalSupply; // using balances ensures pro-rata distribution + amount1 = mul(liquidity, balance1) / _totalSupply; // using balances ensures pro-rata distribution + require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED'); + _burn(address(this), liquidity); + _safeTransfer(_token0, to, amount0); + _safeTransfer(_token1, to, amount1); + balance0 = IERC20(_token0).balanceOf(address(this)); + balance1 = IERC20(_token1).balanceOf(address(this)); + + _update(balance0, balance1, _reserve0, _reserve1); + if (feeOn) kLast = mul(uint(reserve0), reserve1); // reserve0 and reserve1 are up-to-date + emit Burn(msg.sender, amount0, amount1, to); + } + + // this low-level function should be called from a contract which performs important safety checks + function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock { + require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT'); + (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings + require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY'); + + uint balance0; + uint balance1; + { // scope for _token{0,1}, avoids stack too deep errors + address _token0 = token0; + address _token1 = token1; + require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO'); + if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens + if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens + if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data); + balance0 = IERC20(_token0).balanceOf(address(this)); + balance1 = IERC20(_token1).balanceOf(address(this)); + } + uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0; + uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0; + require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT'); + { // scope for reserve{0,1}Adjusted, avoids stack too deep errors + uint balance0Adjusted = sub(mul(balance0, 1000), mul(amount0In, 3)); + uint balance1Adjusted = sub(mul(balance1, 1000), mul(amount1In, 3)); + require(mul(balance0Adjusted, balance1Adjusted) >= mul(mul(uint(_reserve0), _reserve1), 1000**2), 'UniswapV2: K'); + } + + _update(balance0, balance1, _reserve0, _reserve1); + emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to); + } + + // force balances to match reserves + function skim(address to) external lock { + address _token0 = token0; // gas savings + address _token1 = token1; // gas savings + _safeTransfer(_token0, to, sub(IERC20(_token0).balanceOf(address(this)), reserve0)); + _safeTransfer(_token1, to, sub(IERC20(_token1).balanceOf(address(this)), reserve1)); + } + + // force reserves to match balances + function sync() external lock { + _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1); + } +} diff --git a/src/integrations/uniswap/uni-v2/UniswapV2Router02.sol b/src/integrations/uniswap/uni-v2/UniswapV2Router02.sol index 94b4ca5..65170ab 100644 --- a/src/integrations/uniswap/uni-v2/UniswapV2Router02.sol +++ b/src/integrations/uniswap/uni-v2/UniswapV2Router02.sol @@ -1,393 +1,393 @@ -pragma solidity 0.6.7; - -import './interfaces/IUniswapV2Factory.sol'; -import './libs/TransferHelper.sol'; - -import './libs/UniswapV2Library.sol'; -import '../../../math/SafeMath.sol'; -import './interfaces/IERC20.sol'; -import './interfaces/IWETH.sol'; - -contract UniswapV2Router02 is SafeMath, UniswapV2Library { - - address public immutable factory; - address public immutable WETH; - - modifier ensure(uint deadline) { - require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED'); - _; - } - - constructor(address _factory, address _WETH) public { - factory = _factory; - WETH = _WETH; - } - - receive() external payable { - assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract - } - - // **** ADD LIQUIDITY **** - function _addLiquidity( - address tokenA, - address tokenB, - uint amountADesired, - uint amountBDesired, - uint amountAMin, - uint amountBMin - ) internal virtual returns (uint amountA, uint amountB) { - // create the pair if it doesn't exist yet - if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) { - IUniswapV2Factory(factory).createPair(tokenA, tokenB); - } - (uint reserveA, uint reserveB) = getReserves(factory, tokenA, tokenB); - if (reserveA == 0 && reserveB == 0) { - (amountA, amountB) = (amountADesired, amountBDesired); - } else { - uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB); - if (amountBOptimal <= amountBDesired) { - require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT'); - (amountA, amountB) = (amountADesired, amountBOptimal); - } else { - uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA); - assert(amountAOptimal <= amountADesired); - require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT'); - (amountA, amountB) = (amountAOptimal, amountBDesired); - } - } - } - function addLiquidity( - address tokenA, - address tokenB, - uint amountADesired, - uint amountBDesired, - uint amountAMin, - uint amountBMin, - address to, - uint deadline - ) external virtual ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) { - (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin); - address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); - TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA); - TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB); - liquidity = IUniswapV2Pair(pair).mint(to); - } - function addLiquidityETH( - address token, - uint amountTokenDesired, - uint amountTokenMin, - uint amountETHMin, - address to, - uint deadline - ) external virtual payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) { - (amountToken, amountETH) = _addLiquidity( - token, - WETH, - amountTokenDesired, - msg.value, - amountTokenMin, - amountETHMin - ); - address pair = UniswapV2Library.pairFor(factory, token, WETH); - TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken); - IWETH(WETH).deposit{value: amountETH}(); - assert(IWETH(WETH).transfer(pair, amountETH)); - liquidity = IUniswapV2Pair(pair).mint(to); - // refund dust eth, if any - if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH); - } - - // **** REMOVE LIQUIDITY **** - function removeLiquidity( - address tokenA, - address tokenB, - uint liquidity, - uint amountAMin, - uint amountBMin, - address to, - uint deadline - ) public virtual ensure(deadline) returns (uint amountA, uint amountB) { - address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); - IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair - (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to); - (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB); - (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0); - require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT'); - require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT'); - } - function removeLiquidityETH( - address token, - uint liquidity, - uint amountTokenMin, - uint amountETHMin, - address to, - uint deadline - ) public virtual ensure(deadline) returns (uint amountToken, uint amountETH) { - (amountToken, amountETH) = removeLiquidity( - token, - WETH, - liquidity, - amountTokenMin, - amountETHMin, - address(this), - deadline - ); - TransferHelper.safeTransfer(token, to, amountToken); - IWETH(WETH).withdraw(amountETH); - TransferHelper.safeTransferETH(to, amountETH); - } - function removeLiquidityWithPermit( - address tokenA, - address tokenB, - uint liquidity, - uint amountAMin, - uint amountBMin, - address to, - uint deadline, - bool approveMax, uint8 v, bytes32 r, bytes32 s - ) external virtual returns (uint amountA, uint amountB) { - address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); - uint value = approveMax ? uint(-1) : liquidity; - IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s); - (amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline); - } - function removeLiquidityETHWithPermit( - address token, - uint liquidity, - uint amountTokenMin, - uint amountETHMin, - address to, - uint deadline, - bool approveMax, uint8 v, bytes32 r, bytes32 s - ) external virtual returns (uint amountToken, uint amountETH) { - address pair = UniswapV2Library.pairFor(factory, token, WETH); - uint value = approveMax ? uint(-1) : liquidity; - IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s); - (amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline); - } - - // **** REMOVE LIQUIDITY (supporting fee-on-transfer tokens) **** - function removeLiquidityETHSupportingFeeOnTransferTokens( - address token, - uint liquidity, - uint amountTokenMin, - uint amountETHMin, - address to, - uint deadline - ) public virtual ensure(deadline) returns (uint amountETH) { - (, amountETH) = removeLiquidity( - token, - WETH, - liquidity, - amountTokenMin, - amountETHMin, - address(this), - deadline - ); - TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this))); - IWETH(WETH).withdraw(amountETH); - TransferHelper.safeTransferETH(to, amountETH); - } - function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens( - address token, - uint liquidity, - uint amountTokenMin, - uint amountETHMin, - address to, - uint deadline, - bool approveMax, uint8 v, bytes32 r, bytes32 s - ) external virtual returns (uint amountETH) { - address pair = UniswapV2Library.pairFor(factory, token, WETH); - uint value = approveMax ? uint(-1) : liquidity; - IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s); - amountETH = removeLiquidityETHSupportingFeeOnTransferTokens( - token, liquidity, amountTokenMin, amountETHMin, to, deadline - ); - } - - // **** SWAP **** - // requires the initial amount to have already been sent to the first pair - function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual { - for (uint i; i < path.length - 1; i++) { - (address input, address output) = (path[i], path[i + 1]); - (address token0,) = UniswapV2Library.sortTokens(input, output); - uint amountOut = amounts[i + 1]; - (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0)); - address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to; - IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap( - amount0Out, amount1Out, to, new bytes(0) - ); - } - } - function swapExactTokensForTokens( - uint amountIn, - uint amountOutMin, - address[] calldata path, - address to, - uint deadline - ) external virtual ensure(deadline) returns (uint[] memory amounts) { - amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path); - require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); - TransferHelper.safeTransferFrom( - path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] - ); - _swap(amounts, path, to); - } - function swapTokensForExactTokens( - uint amountOut, - uint amountInMax, - address[] calldata path, - address to, - uint deadline - ) external virtual ensure(deadline) returns (uint[] memory amounts) { - amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path); - require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT'); - TransferHelper.safeTransferFrom( - path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] - ); - _swap(amounts, path, to); - } - function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) - external - virtual - payable - ensure(deadline) - returns (uint[] memory amounts) - { - require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH'); - amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path); - require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); - IWETH(WETH).deposit{value: amounts[0]}(); - assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0])); - _swap(amounts, path, to); - } - function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline) - external - virtual - ensure(deadline) - returns (uint[] memory amounts) - { - require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH'); - amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path); - require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT'); - TransferHelper.safeTransferFrom( - path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] - ); - _swap(amounts, path, address(this)); - IWETH(WETH).withdraw(amounts[amounts.length - 1]); - TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]); - } - function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) - external - virtual - ensure(deadline) - returns (uint[] memory amounts) - { - require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH'); - amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path); - require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); - TransferHelper.safeTransferFrom( - path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] - ); - _swap(amounts, path, address(this)); - IWETH(WETH).withdraw(amounts[amounts.length - 1]); - TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]); - } - function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline) - external - virtual - payable - ensure(deadline) - returns (uint[] memory amounts) - { - require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH'); - amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path); - require(amounts[0] <= msg.value, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT'); - IWETH(WETH).deposit{value: amounts[0]}(); - assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0])); - _swap(amounts, path, to); - // refund dust eth, if any - if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]); - } - - // **** SWAP (supporting fee-on-transfer tokens) **** - // requires the initial amount to have already been sent to the first pair - function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual { - for (uint i; i < path.length - 1; i++) { - (address input, address output) = (path[i], path[i + 1]); - (address token0,) = UniswapV2Library.sortTokens(input, output); - IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)); - uint amountInput; - uint amountOutput; - { // scope to avoid stack too deep errors - (uint reserve0, uint reserve1,) = pair.getReserves(); - (uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0); - amountInput = sub(IERC20(input).balanceOf(address(pair)), reserveInput); - amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput); - } - (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0)); - address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to; - pair.swap(amount0Out, amount1Out, to, new bytes(0)); - } - } - function swapExactTokensForTokensSupportingFeeOnTransferTokens( - uint amountIn, - uint amountOutMin, - address[] calldata path, - address to, - uint deadline - ) external virtual ensure(deadline) { - TransferHelper.safeTransferFrom( - path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn - ); - uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to); - _swapSupportingFeeOnTransferTokens(path, to); - require( - sub(IERC20(path[path.length - 1]).balanceOf(to), balanceBefore) >= amountOutMin, - 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT' - ); - } - function swapExactETHForTokensSupportingFeeOnTransferTokens( - uint amountOutMin, - address[] calldata path, - address to, - uint deadline - ) - external - virtual - payable - ensure(deadline) - { - require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH'); - uint amountIn = msg.value; - IWETH(WETH).deposit{value: amountIn}(); - assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn)); - uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to); - _swapSupportingFeeOnTransferTokens(path, to); - require( - sub(IERC20(path[path.length - 1]).balanceOf(to), balanceBefore) >= amountOutMin, - 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT' - ); - } - function swapExactTokensForETHSupportingFeeOnTransferTokens( - uint amountIn, - uint amountOutMin, - address[] calldata path, - address to, - uint deadline - ) - external - virtual - ensure(deadline) - { - require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH'); - TransferHelper.safeTransferFrom( - path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn - ); - _swapSupportingFeeOnTransferTokens(path, address(this)); - uint amountOut = IERC20(WETH).balanceOf(address(this)); - require(amountOut >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); - IWETH(WETH).withdraw(amountOut); - TransferHelper.safeTransferETH(to, amountOut); - } -} +pragma solidity 0.6.7; + +import './interfaces/IUniswapV2Factory.sol'; +import './libs/TransferHelper.sol'; + +import './libs/UniswapV2Library.sol'; +import '../../../math/SafeMath.sol'; +import './interfaces/IERC20.sol'; +import './interfaces/IWETH.sol'; + +contract UniswapV2Router02 is SafeMath, UniswapV2Library { + + address public immutable factory; + address public immutable WETH; + + modifier ensure(uint deadline) { + require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED'); + _; + } + + constructor(address _factory, address _WETH) public { + factory = _factory; + WETH = _WETH; + } + + receive() external payable { + assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract + } + + // **** ADD LIQUIDITY **** + function _addLiquidity( + address tokenA, + address tokenB, + uint amountADesired, + uint amountBDesired, + uint amountAMin, + uint amountBMin + ) internal virtual returns (uint amountA, uint amountB) { + // create the pair if it doesn't exist yet + if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) { + IUniswapV2Factory(factory).createPair(tokenA, tokenB); + } + (uint reserveA, uint reserveB) = getReserves(factory, tokenA, tokenB); + if (reserveA == 0 && reserveB == 0) { + (amountA, amountB) = (amountADesired, amountBDesired); + } else { + uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB); + if (amountBOptimal <= amountBDesired) { + require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT'); + (amountA, amountB) = (amountADesired, amountBOptimal); + } else { + uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA); + assert(amountAOptimal <= amountADesired); + require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT'); + (amountA, amountB) = (amountAOptimal, amountBDesired); + } + } + } + function addLiquidity( + address tokenA, + address tokenB, + uint amountADesired, + uint amountBDesired, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) external virtual ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) { + (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin); + address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); + TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA); + TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB); + liquidity = IUniswapV2Pair(pair).mint(to); + } + function addLiquidityETH( + address token, + uint amountTokenDesired, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) external virtual payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) { + (amountToken, amountETH) = _addLiquidity( + token, + WETH, + amountTokenDesired, + msg.value, + amountTokenMin, + amountETHMin + ); + address pair = UniswapV2Library.pairFor(factory, token, WETH); + TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken); + IWETH(WETH).deposit{value: amountETH}(); + assert(IWETH(WETH).transfer(pair, amountETH)); + liquidity = IUniswapV2Pair(pair).mint(to); + // refund dust eth, if any + if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH); + } + + // **** REMOVE LIQUIDITY **** + function removeLiquidity( + address tokenA, + address tokenB, + uint liquidity, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) public virtual ensure(deadline) returns (uint amountA, uint amountB) { + address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); + IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair + (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to); + (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB); + (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0); + require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT'); + require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT'); + } + function removeLiquidityETH( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) public virtual ensure(deadline) returns (uint amountToken, uint amountETH) { + (amountToken, amountETH) = removeLiquidity( + token, + WETH, + liquidity, + amountTokenMin, + amountETHMin, + address(this), + deadline + ); + TransferHelper.safeTransfer(token, to, amountToken); + IWETH(WETH).withdraw(amountETH); + TransferHelper.safeTransferETH(to, amountETH); + } + function removeLiquidityWithPermit( + address tokenA, + address tokenB, + uint liquidity, + uint amountAMin, + uint amountBMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external virtual returns (uint amountA, uint amountB) { + address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); + uint value = approveMax ? uint(-1) : liquidity; + IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s); + (amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline); + } + function removeLiquidityETHWithPermit( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external virtual returns (uint amountToken, uint amountETH) { + address pair = UniswapV2Library.pairFor(factory, token, WETH); + uint value = approveMax ? uint(-1) : liquidity; + IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s); + (amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline); + } + + // **** REMOVE LIQUIDITY (supporting fee-on-transfer tokens) **** + function removeLiquidityETHSupportingFeeOnTransferTokens( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) public virtual ensure(deadline) returns (uint amountETH) { + (, amountETH) = removeLiquidity( + token, + WETH, + liquidity, + amountTokenMin, + amountETHMin, + address(this), + deadline + ); + TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this))); + IWETH(WETH).withdraw(amountETH); + TransferHelper.safeTransferETH(to, amountETH); + } + function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external virtual returns (uint amountETH) { + address pair = UniswapV2Library.pairFor(factory, token, WETH); + uint value = approveMax ? uint(-1) : liquidity; + IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s); + amountETH = removeLiquidityETHSupportingFeeOnTransferTokens( + token, liquidity, amountTokenMin, amountETHMin, to, deadline + ); + } + + // **** SWAP **** + // requires the initial amount to have already been sent to the first pair + function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual { + for (uint i; i < path.length - 1; i++) { + (address input, address output) = (path[i], path[i + 1]); + (address token0,) = UniswapV2Library.sortTokens(input, output); + uint amountOut = amounts[i + 1]; + (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0)); + address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to; + IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap( + amount0Out, amount1Out, to, new bytes(0) + ); + } + } + function swapExactTokensForTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external virtual ensure(deadline) returns (uint[] memory amounts) { + amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path); + require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); + TransferHelper.safeTransferFrom( + path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] + ); + _swap(amounts, path, to); + } + function swapTokensForExactTokens( + uint amountOut, + uint amountInMax, + address[] calldata path, + address to, + uint deadline + ) external virtual ensure(deadline) returns (uint[] memory amounts) { + amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path); + require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT'); + TransferHelper.safeTransferFrom( + path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] + ); + _swap(amounts, path, to); + } + function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) + external + virtual + payable + ensure(deadline) + returns (uint[] memory amounts) + { + require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH'); + amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path); + require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); + IWETH(WETH).deposit{value: amounts[0]}(); + assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0])); + _swap(amounts, path, to); + } + function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline) + external + virtual + ensure(deadline) + returns (uint[] memory amounts) + { + require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH'); + amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path); + require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT'); + TransferHelper.safeTransferFrom( + path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] + ); + _swap(amounts, path, address(this)); + IWETH(WETH).withdraw(amounts[amounts.length - 1]); + TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]); + } + function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) + external + virtual + ensure(deadline) + returns (uint[] memory amounts) + { + require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH'); + amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path); + require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); + TransferHelper.safeTransferFrom( + path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] + ); + _swap(amounts, path, address(this)); + IWETH(WETH).withdraw(amounts[amounts.length - 1]); + TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]); + } + function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline) + external + virtual + payable + ensure(deadline) + returns (uint[] memory amounts) + { + require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH'); + amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path); + require(amounts[0] <= msg.value, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT'); + IWETH(WETH).deposit{value: amounts[0]}(); + assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0])); + _swap(amounts, path, to); + // refund dust eth, if any + if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]); + } + + // **** SWAP (supporting fee-on-transfer tokens) **** + // requires the initial amount to have already been sent to the first pair + function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual { + for (uint i; i < path.length - 1; i++) { + (address input, address output) = (path[i], path[i + 1]); + (address token0,) = UniswapV2Library.sortTokens(input, output); + IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)); + uint amountInput; + uint amountOutput; + { // scope to avoid stack too deep errors + (uint reserve0, uint reserve1,) = pair.getReserves(); + (uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0); + amountInput = sub(IERC20(input).balanceOf(address(pair)), reserveInput); + amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput); + } + (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0)); + address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to; + pair.swap(amount0Out, amount1Out, to, new bytes(0)); + } + } + function swapExactTokensForTokensSupportingFeeOnTransferTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external virtual ensure(deadline) { + TransferHelper.safeTransferFrom( + path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn + ); + uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to); + _swapSupportingFeeOnTransferTokens(path, to); + require( + sub(IERC20(path[path.length - 1]).balanceOf(to), balanceBefore) >= amountOutMin, + 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT' + ); + } + function swapExactETHForTokensSupportingFeeOnTransferTokens( + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) + external + virtual + payable + ensure(deadline) + { + require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH'); + uint amountIn = msg.value; + IWETH(WETH).deposit{value: amountIn}(); + assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn)); + uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to); + _swapSupportingFeeOnTransferTokens(path, to); + require( + sub(IERC20(path[path.length - 1]).balanceOf(to), balanceBefore) >= amountOutMin, + 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT' + ); + } + function swapExactTokensForETHSupportingFeeOnTransferTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) + external + virtual + ensure(deadline) + { + require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH'); + TransferHelper.safeTransferFrom( + path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn + ); + _swapSupportingFeeOnTransferTokens(path, address(this)); + uint amountOut = IERC20(WETH).balanceOf(address(this)); + require(amountOut >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); + IWETH(WETH).withdraw(amountOut); + TransferHelper.safeTransferETH(to, amountOut); + } +} diff --git a/src/integrations/uniswap/uni-v2/interfaces/IERC20.sol b/src/integrations/uniswap/uni-v2/interfaces/IERC20.sol index baad0f6..a682bd3 100644 --- a/src/integrations/uniswap/uni-v2/interfaces/IERC20.sol +++ b/src/integrations/uniswap/uni-v2/interfaces/IERC20.sol @@ -1,17 +1,17 @@ -pragma solidity 0.6.7; - -interface IERC20 { - event Approval(address indexed owner, address indexed spender, uint value); - event Transfer(address indexed from, address indexed to, uint value); - - function name() external view returns (string memory); - function symbol() external view returns (string memory); - function decimals() external view returns (uint8); - function totalSupply() external view returns (uint); - function balanceOf(address owner) external view returns (uint); - function allowance(address owner, address spender) external view returns (uint); - - function approve(address spender, uint value) external returns (bool); - function transfer(address to, uint value) external returns (bool); - function transferFrom(address from, address to, uint value) external returns (bool); -} +pragma solidity 0.6.7; + +interface IERC20 { + event Approval(address indexed owner, address indexed spender, uint value); + event Transfer(address indexed from, address indexed to, uint value); + + function name() external view returns (string memory); + function symbol() external view returns (string memory); + function decimals() external view returns (uint8); + function totalSupply() external view returns (uint); + function balanceOf(address owner) external view returns (uint); + function allowance(address owner, address spender) external view returns (uint); + + function approve(address spender, uint value) external returns (bool); + function transfer(address to, uint value) external returns (bool); + function transferFrom(address from, address to, uint value) external returns (bool); +} diff --git a/src/integrations/uniswap/uni-v2/interfaces/IUniswapV2Callee.sol b/src/integrations/uniswap/uni-v2/interfaces/IUniswapV2Callee.sol index 7ac2c4d..7c5d816 100644 --- a/src/integrations/uniswap/uni-v2/interfaces/IUniswapV2Callee.sol +++ b/src/integrations/uniswap/uni-v2/interfaces/IUniswapV2Callee.sol @@ -1,5 +1,5 @@ -pragma solidity 0.6.7; - -interface IUniswapV2Callee { - function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external; -} +pragma solidity 0.6.7; + +interface IUniswapV2Callee { + function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external; +} diff --git a/src/integrations/uniswap/uni-v2/interfaces/IUniswapV2Factory.sol b/src/integrations/uniswap/uni-v2/interfaces/IUniswapV2Factory.sol index daefbb9..5a41fb2 100644 --- a/src/integrations/uniswap/uni-v2/interfaces/IUniswapV2Factory.sol +++ b/src/integrations/uniswap/uni-v2/interfaces/IUniswapV2Factory.sol @@ -1,17 +1,17 @@ -pragma solidity 0.6.7; - -interface IUniswapV2Factory { - event PairCreated(address indexed token0, address indexed token1, address pair, uint); - - function feeTo() external view returns (address); - function feeToSetter() external view returns (address); - - function getPair(address tokenA, address tokenB) external view returns (address pair); - function allPairs(uint) external view returns (address pair); - function allPairsLength() external view returns (uint); - - function createPair(address tokenA, address tokenB) external returns (address pair); - - function setFeeTo(address) external; - function setFeeToSetter(address) external; -} +pragma solidity 0.6.7; + +interface IUniswapV2Factory { + event PairCreated(address indexed token0, address indexed token1, address pair, uint); + + function feeTo() external view returns (address); + function feeToSetter() external view returns (address); + + function getPair(address tokenA, address tokenB) external view returns (address pair); + function allPairs(uint) external view returns (address pair); + function allPairsLength() external view returns (uint); + + function createPair(address tokenA, address tokenB) external returns (address pair); + + function setFeeTo(address) external; + function setFeeToSetter(address) external; +} diff --git a/src/integrations/uniswap/uni-v2/interfaces/IUniswapV2Pair.sol b/src/integrations/uniswap/uni-v2/interfaces/IUniswapV2Pair.sol index cd0ce6d..2285feb 100644 --- a/src/integrations/uniswap/uni-v2/interfaces/IUniswapV2Pair.sol +++ b/src/integrations/uniswap/uni-v2/interfaces/IUniswapV2Pair.sol @@ -1,52 +1,52 @@ -pragma solidity 0.6.7; - -interface IUniswapV2Pair { - event Approval(address indexed owner, address indexed spender, uint value); - event Transfer(address indexed from, address indexed to, uint value); - - function name() external pure returns (string memory); - function symbol() external pure returns (string memory); - function decimals() external pure returns (uint8); - function totalSupply() external view returns (uint); - function balanceOf(address owner) external view returns (uint); - function allowance(address owner, address spender) external view returns (uint); - - function approve(address spender, uint value) external returns (bool); - function transfer(address to, uint value) external returns (bool); - function transferFrom(address from, address to, uint value) external returns (bool); - - function DOMAIN_SEPARATOR() external view returns (bytes32); - function PERMIT_TYPEHASH() external pure returns (bytes32); - function nonces(address owner) external view returns (uint); - - function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external; - - event Mint(address indexed sender, uint amount0, uint amount1); - event Burn(address indexed sender, uint amount0, uint amount1, address indexed to); - event Swap( - address indexed sender, - uint amount0In, - uint amount1In, - uint amount0Out, - uint amount1Out, - address indexed to - ); - event Sync(uint112 reserve0, uint112 reserve1); - - function MINIMUM_LIQUIDITY() external pure returns (uint); - function factory() external view returns (address); - function token0() external view returns (address); - function token1() external view returns (address); - function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); - function price0CumulativeLast() external view returns (uint); - function price1CumulativeLast() external view returns (uint); - function kLast() external view returns (uint); - - function mint(address to) external returns (uint liquidity); - function burn(address to) external returns (uint amount0, uint amount1); - function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external; - function skim(address to) external; - function sync() external; - - function initialize(address, address) external; -} +pragma solidity 0.6.7; + +interface IUniswapV2Pair { + event Approval(address indexed owner, address indexed spender, uint value); + event Transfer(address indexed from, address indexed to, uint value); + + function name() external pure returns (string memory); + function symbol() external pure returns (string memory); + function decimals() external pure returns (uint8); + function totalSupply() external view returns (uint); + function balanceOf(address owner) external view returns (uint); + function allowance(address owner, address spender) external view returns (uint); + + function approve(address spender, uint value) external returns (bool); + function transfer(address to, uint value) external returns (bool); + function transferFrom(address from, address to, uint value) external returns (bool); + + function DOMAIN_SEPARATOR() external view returns (bytes32); + function PERMIT_TYPEHASH() external pure returns (bytes32); + function nonces(address owner) external view returns (uint); + + function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external; + + event Mint(address indexed sender, uint amount0, uint amount1); + event Burn(address indexed sender, uint amount0, uint amount1, address indexed to); + event Swap( + address indexed sender, + uint amount0In, + uint amount1In, + uint amount0Out, + uint amount1Out, + address indexed to + ); + event Sync(uint112 reserve0, uint112 reserve1); + + function MINIMUM_LIQUIDITY() external pure returns (uint); + function factory() external view returns (address); + function token0() external view returns (address); + function token1() external view returns (address); + function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); + function price0CumulativeLast() external view returns (uint); + function price1CumulativeLast() external view returns (uint); + function kLast() external view returns (uint); + + function mint(address to) external returns (uint liquidity); + function burn(address to) external returns (uint amount0, uint amount1); + function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external; + function skim(address to) external; + function sync() external; + + function initialize(address, address) external; +} diff --git a/src/integrations/uniswap/uni-v2/interfaces/IUniswapV2Router01.sol b/src/integrations/uniswap/uni-v2/interfaces/IUniswapV2Router01.sol index 0566cb8..9e3315f 100644 --- a/src/integrations/uniswap/uni-v2/interfaces/IUniswapV2Router01.sol +++ b/src/integrations/uniswap/uni-v2/interfaces/IUniswapV2Router01.sol @@ -1,95 +1,95 @@ -pragma solidity 0.6.7; - -interface IUniswapV2Router01 { - function factory() external pure returns (address); - function WETH() external pure returns (address); - - function addLiquidity( - address tokenA, - address tokenB, - uint amountADesired, - uint amountBDesired, - uint amountAMin, - uint amountBMin, - address to, - uint deadline - ) external returns (uint amountA, uint amountB, uint liquidity); - function addLiquidityETH( - address token, - uint amountTokenDesired, - uint amountTokenMin, - uint amountETHMin, - address to, - uint deadline - ) external payable returns (uint amountToken, uint amountETH, uint liquidity); - function removeLiquidity( - address tokenA, - address tokenB, - uint liquidity, - uint amountAMin, - uint amountBMin, - address to, - uint deadline - ) external returns (uint amountA, uint amountB); - function removeLiquidityETH( - address token, - uint liquidity, - uint amountTokenMin, - uint amountETHMin, - address to, - uint deadline - ) external returns (uint amountToken, uint amountETH); - function removeLiquidityWithPermit( - address tokenA, - address tokenB, - uint liquidity, - uint amountAMin, - uint amountBMin, - address to, - uint deadline, - bool approveMax, uint8 v, bytes32 r, bytes32 s - ) external returns (uint amountA, uint amountB); - function removeLiquidityETHWithPermit( - address token, - uint liquidity, - uint amountTokenMin, - uint amountETHMin, - address to, - uint deadline, - bool approveMax, uint8 v, bytes32 r, bytes32 s - ) external returns (uint amountToken, uint amountETH); - function swapExactTokensForTokens( - uint amountIn, - uint amountOutMin, - address[] calldata path, - address to, - uint deadline - ) external returns (uint[] memory amounts); - function swapTokensForExactTokens( - uint amountOut, - uint amountInMax, - address[] calldata path, - address to, - uint deadline - ) external returns (uint[] memory amounts); - function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) - external - payable - returns (uint[] memory amounts); - function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline) - external - returns (uint[] memory amounts); - function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) - external - returns (uint[] memory amounts); - function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline) - external - payable - returns (uint[] memory amounts); - - function quote(uint amountA, uint reserveA, uint reserveB) external pure returns (uint amountB); - function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) external pure returns (uint amountOut); - function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) external pure returns (uint amountIn); - function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts); - function getAmountsIn(uint amountOut, address[] calldata path) external view returns (uint[] memory amounts); -} +pragma solidity 0.6.7; + +interface IUniswapV2Router01 { + function factory() external pure returns (address); + function WETH() external pure returns (address); + + function addLiquidity( + address tokenA, + address tokenB, + uint amountADesired, + uint amountBDesired, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) external returns (uint amountA, uint amountB, uint liquidity); + function addLiquidityETH( + address token, + uint amountTokenDesired, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) external payable returns (uint amountToken, uint amountETH, uint liquidity); + function removeLiquidity( + address tokenA, + address tokenB, + uint liquidity, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) external returns (uint amountA, uint amountB); + function removeLiquidityETH( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) external returns (uint amountToken, uint amountETH); + function removeLiquidityWithPermit( + address tokenA, + address tokenB, + uint liquidity, + uint amountAMin, + uint amountBMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external returns (uint amountA, uint amountB); + function removeLiquidityETHWithPermit( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external returns (uint amountToken, uint amountETH); + function swapExactTokensForTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external returns (uint[] memory amounts); + function swapTokensForExactTokens( + uint amountOut, + uint amountInMax, + address[] calldata path, + address to, + uint deadline + ) external returns (uint[] memory amounts); + function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) + external + payable + returns (uint[] memory amounts); + function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline) + external + returns (uint[] memory amounts); + function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) + external + returns (uint[] memory amounts); + function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline) + external + payable + returns (uint[] memory amounts); + + function quote(uint amountA, uint reserveA, uint reserveB) external pure returns (uint amountB); + function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) external pure returns (uint amountOut); + function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) external pure returns (uint amountIn); + function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts); + function getAmountsIn(uint amountOut, address[] calldata path) external view returns (uint[] memory amounts); +} diff --git a/src/integrations/uniswap/uni-v2/interfaces/IUniswapV2Router02.sol b/src/integrations/uniswap/uni-v2/interfaces/IUniswapV2Router02.sol index 61a01dd..6284c22 100644 --- a/src/integrations/uniswap/uni-v2/interfaces/IUniswapV2Router02.sol +++ b/src/integrations/uniswap/uni-v2/interfaces/IUniswapV2Router02.sol @@ -1,44 +1,44 @@ -pragma solidity 0.6.7; - -import './IUniswapV2Router01.sol'; - -interface IUniswapV2Router02 is IUniswapV2Router01 { - function removeLiquidityETHSupportingFeeOnTransferTokens( - address token, - uint liquidity, - uint amountTokenMin, - uint amountETHMin, - address to, - uint deadline - ) external returns (uint amountETH); - function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens( - address token, - uint liquidity, - uint amountTokenMin, - uint amountETHMin, - address to, - uint deadline, - bool approveMax, uint8 v, bytes32 r, bytes32 s - ) external returns (uint amountETH); - - function swapExactTokensForTokensSupportingFeeOnTransferTokens( - uint amountIn, - uint amountOutMin, - address[] calldata path, - address to, - uint deadline - ) external; - function swapExactETHForTokensSupportingFeeOnTransferTokens( - uint amountOutMin, - address[] calldata path, - address to, - uint deadline - ) external payable; - function swapExactTokensForETHSupportingFeeOnTransferTokens( - uint amountIn, - uint amountOutMin, - address[] calldata path, - address to, - uint deadline - ) external; -} +pragma solidity 0.6.7; + +import './IUniswapV2Router01.sol'; + +interface IUniswapV2Router02 is IUniswapV2Router01 { + function removeLiquidityETHSupportingFeeOnTransferTokens( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) external returns (uint amountETH); + function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external returns (uint amountETH); + + function swapExactTokensForTokensSupportingFeeOnTransferTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external; + function swapExactETHForTokensSupportingFeeOnTransferTokens( + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external payable; + function swapExactTokensForETHSupportingFeeOnTransferTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external; +} diff --git a/src/integrations/uniswap/uni-v2/interfaces/IWETH.sol b/src/integrations/uniswap/uni-v2/interfaces/IWETH.sol index 3c42d35..82ea7bf 100644 --- a/src/integrations/uniswap/uni-v2/interfaces/IWETH.sol +++ b/src/integrations/uniswap/uni-v2/interfaces/IWETH.sol @@ -1,7 +1,7 @@ -pragma solidity 0.6.7; - -interface IWETH { - function deposit() external payable; - function transfer(address to, uint value) external returns (bool); - function withdraw(uint) external; -} +pragma solidity 0.6.7; + +interface IWETH { + function deposit() external payable; + function transfer(address to, uint value) external returns (bool); + function withdraw(uint) external; +} diff --git a/src/integrations/uniswap/uni-v2/libs/TransferHelper.sol b/src/integrations/uniswap/uni-v2/libs/TransferHelper.sol index 56905fc..87a1133 100644 --- a/src/integrations/uniswap/uni-v2/libs/TransferHelper.sol +++ b/src/integrations/uniswap/uni-v2/libs/TransferHelper.sol @@ -1,29 +1,29 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -pragma solidity 0.6.7; - -// helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return true/false -library TransferHelper { - function safeApprove(address token, address to, uint value) internal { - // bytes4(keccak256(bytes('approve(address,uint256)'))); - (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value)); - require(success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper: APPROVE_FAILED'); - } - - function safeTransfer(address token, address to, uint value) internal { - // bytes4(keccak256(bytes('transfer(address,uint256)'))); - (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value)); - require(success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper: TRANSFER_FAILED'); - } - - function safeTransferFrom(address token, address from, address to, uint value) internal { - // bytes4(keccak256(bytes('transferFrom(address,address,uint256)'))); - (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value)); - require(success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper: TRANSFER_FROM_FAILED'); - } - - function safeTransferETH(address to, uint value) internal { - (bool success,) = to.call{value:value}(new bytes(0)); - require(success, 'TransferHelper: ETH_TRANSFER_FAILED'); - } -} +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity 0.6.7; + +// helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return true/false +library TransferHelper { + function safeApprove(address token, address to, uint value) internal { + // bytes4(keccak256(bytes('approve(address,uint256)'))); + (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value)); + require(success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper: APPROVE_FAILED'); + } + + function safeTransfer(address token, address to, uint value) internal { + // bytes4(keccak256(bytes('transfer(address,uint256)'))); + (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value)); + require(success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper: TRANSFER_FAILED'); + } + + function safeTransferFrom(address token, address from, address to, uint value) internal { + // bytes4(keccak256(bytes('transferFrom(address,address,uint256)'))); + (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value)); + require(success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper: TRANSFER_FROM_FAILED'); + } + + function safeTransferETH(address to, uint value) internal { + (bool success,) = to.call{value:value}(new bytes(0)); + require(success, 'TransferHelper: ETH_TRANSFER_FAILED'); + } +} diff --git a/src/integrations/uniswap/uni-v2/libs/UniswapV2Library.sol b/src/integrations/uniswap/uni-v2/libs/UniswapV2Library.sol index 47c97ff..96d72cd 100644 --- a/src/integrations/uniswap/uni-v2/libs/UniswapV2Library.sol +++ b/src/integrations/uniswap/uni-v2/libs/UniswapV2Library.sol @@ -1,85 +1,85 @@ -pragma solidity 0.6.7; - -import '../interfaces/IUniswapV2Pair.sol'; -import '../interfaces/IUniswapV2Factory.sol'; - -contract UniswapV2Library { - // --- Math --- - function uniAddition(uint x, uint y) internal pure returns (uint z) { - require((z = x + y) >= x, 'UniswapV2Library: add-overflow'); - } - function uniSubtract(uint x, uint y) internal pure returns (uint z) { - require((z = x - y) <= x, 'UniswapV2Library: sub-underflow'); - } - function uniMultiply(uint x, uint y) internal pure returns (uint z) { - require(y == 0 || (z = x * y) / y == x, 'UniswapV2Library: mul-overflow'); - } - - // returns sorted token addresses, used to handle return values from pairs sorted in this order - function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) { - require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES'); - (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); - require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS'); - } - - // Modified Uniswap function to work with dapp.tools (CREATE2 throws) - function pairFor(address factory, address tokenA, address tokenB) internal view returns (address pair) { - (address token0, address token1) = sortTokens(tokenA, tokenB); - return IUniswapV2Factory(factory).getPair(tokenA, tokenB); - } - - // fetches and sorts the reserves for a pair; modified from the initial Uniswap version in order to work with dapp.tools - function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) { - (address token0,) = sortTokens(tokenA, tokenB); - (uint reserve0, uint reserve1,) = IUniswapV2Pair(IUniswapV2Factory(factory).getPair(tokenA, tokenB)).getReserves(); - (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0); - } - - // Given some amount of an asset and pair reserves, returns an equivalent amount of the other asset - function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) { - require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT'); - require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); - amountB = uniMultiply(amountA, reserveB) / reserveA; - } - - // Given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset - function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) { - require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT'); - require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); - uint amountInWithFee = uniMultiply(amountIn, 997); - uint numerator = uniMultiply(amountInWithFee, reserveOut); - uint denominator = uniAddition(uniMultiply(reserveIn, 1000), amountInWithFee); - amountOut = numerator / denominator; - } - - // given an output amount of an asset and pair reserves, returns a required input amount of the other asset - function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) { - require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT'); - require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); - uint numerator = uniMultiply(uniMultiply(reserveIn, amountOut), 1000); - uint denominator = uniMultiply(uniSubtract(reserveOut, amountOut), 997); - amountIn = uniAddition((numerator / denominator), 1); - } - - // performs chained getAmountOut calculations on any number of pairs - function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) { - require(path.length >= 2, 'UniswapV2Library: INVALID_PATH'); - amounts = new uint[](path.length); - amounts[0] = amountIn; - for (uint i; i < path.length - 1; i++) { - (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]); - amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut); - } - } - - // performs chained getAmountIn calculations on any number of pairs - function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) { - require(path.length >= 2, 'UniswapV2Library: INVALID_PATH'); - amounts = new uint[](path.length); - amounts[amounts.length - 1] = amountOut; - for (uint i = path.length - 1; i > 0; i--) { - (uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]); - amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut); - } - } -} +pragma solidity 0.6.7; + +import '../interfaces/IUniswapV2Pair.sol'; +import '../interfaces/IUniswapV2Factory.sol'; + +contract UniswapV2Library { + // --- Math --- + function uniAddition(uint x, uint y) internal pure returns (uint z) { + require((z = x + y) >= x, 'UniswapV2Library: add-overflow'); + } + function uniSubtract(uint x, uint y) internal pure returns (uint z) { + require((z = x - y) <= x, 'UniswapV2Library: sub-underflow'); + } + function uniMultiply(uint x, uint y) internal pure returns (uint z) { + require(y == 0 || (z = x * y) / y == x, 'UniswapV2Library: mul-overflow'); + } + + // returns sorted token addresses, used to handle return values from pairs sorted in this order + function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) { + require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES'); + (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); + require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS'); + } + + // Modified Uniswap function to work with dapp.tools (CREATE2 throws) + function pairFor(address factory, address tokenA, address tokenB) internal view returns (address pair) { + (address token0, address token1) = sortTokens(tokenA, tokenB); + return IUniswapV2Factory(factory).getPair(tokenA, tokenB); + } + + // fetches and sorts the reserves for a pair; modified from the initial Uniswap version in order to work with dapp.tools + function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) { + (address token0,) = sortTokens(tokenA, tokenB); + (uint reserve0, uint reserve1,) = IUniswapV2Pair(IUniswapV2Factory(factory).getPair(tokenA, tokenB)).getReserves(); + (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0); + } + + // Given some amount of an asset and pair reserves, returns an equivalent amount of the other asset + function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) { + require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT'); + require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); + amountB = uniMultiply(amountA, reserveB) / reserveA; + } + + // Given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset + function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) { + require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT'); + require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); + uint amountInWithFee = uniMultiply(amountIn, 997); + uint numerator = uniMultiply(amountInWithFee, reserveOut); + uint denominator = uniAddition(uniMultiply(reserveIn, 1000), amountInWithFee); + amountOut = numerator / denominator; + } + + // given an output amount of an asset and pair reserves, returns a required input amount of the other asset + function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) { + require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT'); + require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); + uint numerator = uniMultiply(uniMultiply(reserveIn, amountOut), 1000); + uint denominator = uniMultiply(uniSubtract(reserveOut, amountOut), 997); + amountIn = uniAddition((numerator / denominator), 1); + } + + // performs chained getAmountOut calculations on any number of pairs + function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) { + require(path.length >= 2, 'UniswapV2Library: INVALID_PATH'); + amounts = new uint[](path.length); + amounts[0] = amountIn; + for (uint i; i < path.length - 1; i++) { + (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]); + amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut); + } + } + + // performs chained getAmountIn calculations on any number of pairs + function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) { + require(path.length >= 2, 'UniswapV2Library: INVALID_PATH'); + amounts = new uint[](path.length); + amounts[amounts.length - 1] = amountOut; + for (uint i = path.length - 1; i > 0; i--) { + (uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]); + amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut); + } + } +} diff --git a/src/integrations/uniswap/uni-v2/libs/UniswapV2OracleLibrary.sol b/src/integrations/uniswap/uni-v2/libs/UniswapV2OracleLibrary.sol index ef9530e..eb1e464 100644 --- a/src/integrations/uniswap/uni-v2/libs/UniswapV2OracleLibrary.sol +++ b/src/integrations/uniswap/uni-v2/libs/UniswapV2OracleLibrary.sol @@ -1,33 +1,33 @@ -pragma solidity 0.6.7; - -import '../interfaces/IUniswapV2Pair.sol'; -import '../../../../math/FixedPointMath.sol'; - -// Contract with helper methods for oracles that are concerned with computing average prices -contract UniswapV2OracleLibrary is FixedPointMath { - // Helper function that returns the current block timestamp within the range of uint32, i.e. [0, 2**32 - 1] - function currentBlockTimestamp() internal view returns (uint32) { - return uint32(block.timestamp % 2 ** 32); - } - - // Produces the cumulative price using counterfactuals to save gas and avoid a call to sync. - function currentCumulativePrices( - address pair - ) internal view returns (uint price0Cumulative, uint price1Cumulative, uint32 blockTimestamp) { - blockTimestamp = currentBlockTimestamp(); - price0Cumulative = IUniswapV2Pair(pair).price0CumulativeLast(); - price1Cumulative = IUniswapV2Pair(pair).price1CumulativeLast(); - - // if time has elapsed since the last update on the pair, mock the accumulated price values - (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast) = IUniswapV2Pair(pair).getReserves(); - if (blockTimestampLast != blockTimestamp) { - // subtraction overflow is desired - uint32 timeElapsed = blockTimestamp - blockTimestampLast; - // addition overflow is desired - // counterfactual - price0Cumulative += uint(frac(reserve1, reserve0)._x) * timeElapsed; - // counterfactual - price1Cumulative += uint(frac(reserve0, reserve1)._x) * timeElapsed; - } - } -} +pragma solidity 0.6.7; + +import '../interfaces/IUniswapV2Pair.sol'; +import '../../../../math/FixedPointMath.sol'; + +// Contract with helper methods for oracles that are concerned with computing average prices +contract UniswapV2OracleLibrary is FixedPointMath { + // Helper function that returns the current block timestamp within the range of uint32, i.e. [0, 2**32 - 1] + function currentBlockTimestamp() internal view returns (uint32) { + return uint32(block.timestamp % 2 ** 32); + } + + // Produces the cumulative price using counterfactuals to save gas and avoid a call to sync. + function currentCumulativePrices( + address pair + ) internal view returns (uint price0Cumulative, uint price1Cumulative, uint32 blockTimestamp) { + blockTimestamp = currentBlockTimestamp(); + price0Cumulative = IUniswapV2Pair(pair).price0CumulativeLast(); + price1Cumulative = IUniswapV2Pair(pair).price1CumulativeLast(); + + // if time has elapsed since the last update on the pair, mock the accumulated price values + (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast) = IUniswapV2Pair(pair).getReserves(); + if (blockTimestampLast != blockTimestamp) { + // subtraction overflow is desired + uint32 timeElapsed = blockTimestamp - blockTimestampLast; + // addition overflow is desired + // counterfactual + price0Cumulative += uint(frac(reserve1, reserve0)._x) * timeElapsed; + // counterfactual + price1Cumulative += uint(frac(reserve0, reserve1)._x) * timeElapsed; + } + } +} diff --git a/src/integrations/uniswap/uni-v3/FixedPoint128.sol b/src/integrations/uniswap/uni-v3/FixedPoint128.sol new file mode 100644 index 0000000..0582e52 --- /dev/null +++ b/src/integrations/uniswap/uni-v3/FixedPoint128.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.4.0; + +/// @title FixedPoint128 +/// @notice A library for handling binary fixed point numbers, see https://en.wikipedia.org/wiki/Q_(number_format) +library FixedPoint128 { + uint256 internal constant Q128 = 0x100000000000000000000000000000000; +} diff --git a/src/integrations/uniswap/uni-v3/FullMath.sol b/src/integrations/uniswap/uni-v3/FullMath.sol new file mode 100644 index 0000000..8803e56 --- /dev/null +++ b/src/integrations/uniswap/uni-v3/FullMath.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.4.0; + +/// @title Contains 512-bit math functions +/// @notice Facilitates multiplication and division that can have overflow of an intermediate value without any loss of precision +/// @dev Handles "phantom overflow" i.e., allows multiplication and division where an intermediate value overflows 256 bits +library FullMath { + /// @notice Calculates floor(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 + /// @param a The multiplicand + /// @param b The multiplier + /// @param denominator The divisor + /// @return result The 256-bit result + /// @dev Credit to Remco Bloemen under MIT license https://xn--2-umb.com/21/muldiv + function mulDiv( + uint256 a, + uint256 b, + uint256 denominator + ) internal pure returns (uint256 result) { + // 512-bit multiply [prod1 prod0] = a * b + // Compute the product mod 2**256 and mod 2**256 - 1 + // then use the Chinese Remainder Theorem to reconstruct + // the 512 bit result. The result is stored in two 256 + // variables such that product = prod1 * 2**256 + prod0 + uint256 prod0; // Least significant 256 bits of the product + uint256 prod1; // Most significant 256 bits of the product + assembly { + let mm := mulmod(a, b, not(0)) + prod0 := mul(a, b) + prod1 := sub(sub(mm, prod0), lt(mm, prod0)) + } + + // Handle non-overflow cases, 256 by 256 division + if (prod1 == 0) { + require(denominator > 0); + assembly { + result := div(prod0, denominator) + } + return result; + } + + // Make sure the result is less than 2**256. + // Also prevents denominator == 0 + require(denominator > prod1); + + /////////////////////////////////////////////// + // 512 by 256 division. + /////////////////////////////////////////////// + + // Make division exact by subtracting the remainder from [prod1 prod0] + // Compute remainder using mulmod + uint256 remainder; + assembly { + remainder := mulmod(a, b, denominator) + } + // Subtract 256 bit number from 512 bit number + assembly { + prod1 := sub(prod1, gt(remainder, prod0)) + prod0 := sub(prod0, remainder) + } + + // Factor powers of two out of denominator + // Compute largest power of two divisor of denominator. + // Always >= 1. + uint256 twos = -denominator & denominator; + // Divide denominator by power of two + assembly { + denominator := div(denominator, twos) + } + + // Divide [prod1 prod0] by the factors of two + assembly { + prod0 := div(prod0, twos) + } + // Shift in bits from prod1 into prod0. For this we need + // to flip `twos` such that it is 2**256 / twos. + // If twos is zero, then it becomes one + assembly { + twos := add(div(sub(0, twos), twos), 1) + } + prod0 |= prod1 * twos; + + // Invert denominator mod 2**256 + // Now that denominator is an odd number, it has an inverse + // modulo 2**256 such that denominator * inv = 1 mod 2**256. + // Compute the inverse by starting with a seed that is correct + // correct for four bits. That is, denominator * inv = 1 mod 2**4 + uint256 inv = (3 * denominator) ^ 2; + // Now use Newton-Raphson iteration to improve the precision. + // Thanks to Hensel's lifting lemma, this also works in modular + // arithmetic, doubling the correct bits in each step. + inv *= 2 - denominator * inv; // inverse mod 2**8 + inv *= 2 - denominator * inv; // inverse mod 2**16 + inv *= 2 - denominator * inv; // inverse mod 2**32 + inv *= 2 - denominator * inv; // inverse mod 2**64 + inv *= 2 - denominator * inv; // inverse mod 2**128 + inv *= 2 - denominator * inv; // inverse mod 2**256 + + // Because the division is now exact we can divide by multiplying + // with the modular inverse of denominator. This will give us the + // correct result modulo 2**256. Since the precoditions guarantee + // that the outcome is less than 2**256, this is the final result. + // We don't need to compute the high bits of the result and prod1 + // is no longer required. + result = prod0 * inv; + return result; + } +} diff --git a/src/integrations/uniswap/uni-v3/UniswapV3FeeCalculator.sol b/src/integrations/uniswap/uni-v3/UniswapV3FeeCalculator.sol new file mode 100644 index 0000000..36f2a41 --- /dev/null +++ b/src/integrations/uniswap/uni-v3/UniswapV3FeeCalculator.sol @@ -0,0 +1,73 @@ +pragma solidity 0.6.7; + +import "../../../interfaces/UniswapV3NonFungiblePositionManagerLike.sol"; +import "../../../interfaces/UniswapV3PoolLike.sol"; + +import "./FixedPoint128.sol"; +import "./FullMath.sol"; + +contract UniswapV3FeeCalculator { + // --- Variables --- + // Uniswap v3 position manager + UniswapV3NonFungiblePositionManagerLike public positionManager; + + constructor( + address positionManager_ + ) public { + require(positionManager_ != address(0), "UniswapV3FeeCalculator/null-position-manager"); + positionManager = UniswapV3NonFungiblePositionManagerLike(positionManager_); + } + + // --- Core Logic --- + /** + * @notice Return the amount of uncollected fees for a specific position + * @param pool Address of the pool associated with this position + * @param tokenId The ID of the position in the manager + */ + function getUncollectedFees( + address pool, + uint256 tokenId + ) + external + view + returns (uint256, uint256) + { + ( ,,,,, + int24 tickLower, + int24 tickUpper, + uint128 liquidity, + uint256 oldFeeGrowthInside0LastX128, + uint256 oldFeeGrowthInside1LastX128, + , + ) + = positionManager.positions(tokenId); + IUniswapV3Pool pool = UniswapV3PoolLike(pool); + + if (liquidity > 0) { + uint256 amount0; + uint256 amount1; + + (, uint256 latestFeeGrowthInside0LastX128, uint256 latestFeeGrowthInside1LastX128, , ) = + pool.positions(PositionKey.compute(address(positionManager), tickLower, tickUpper)); + + amount0 = uint256( + FullMath.mulDiv( + latestFeeGrowthInside0LastX128 - oldFeeGrowthInside0LastX128, + liquidity, + FixedPoint128.Q128 + ) + ); + amount1 = uint256( + FullMath.mulDiv( + latestFeeGrowthInside1LastX128 - oldFeeGrowthInside1LastX128, + liquidity, + FixedPoint128.Q128 + ) + ); + + return (amount0, amount1); + } + + return (0, 0); + } +} diff --git a/src/interfaces/CTokenLike.sol b/src/interfaces/CTokenLike.sol index b59eba1..62a8b6a 100644 --- a/src/interfaces/CTokenLike.sol +++ b/src/interfaces/CTokenLike.sol @@ -1,13 +1,13 @@ -pragma solidity 0.6.7; - -abstract contract CTokenLike { - function mint(uint256) virtual external returns (uint256); - function exchangeRateStored() virtual public view returns (uint); - function exchangeRateCurrent() virtual external returns (uint256); - function redeem(uint256) virtual external returns (uint256); - function redeemUnderlying(uint256) virtual external returns (uint256); - function isCToken() virtual external returns (bool); - function balanceOfUnderlying(address) virtual external returns (uint); - function balanceOf(address) virtual external returns (uint256); - function approve(address, uint256) virtual external returns (bool); -} +pragma solidity 0.6.7; + +abstract contract CTokenLike { + function mint(uint256) virtual external returns (uint256); + function exchangeRateStored() virtual public view returns (uint); + function exchangeRateCurrent() virtual external returns (uint256); + function redeem(uint256) virtual external returns (uint256); + function redeemUnderlying(uint256) virtual external returns (uint256); + function isCToken() virtual external returns (bool); + function balanceOfUnderlying(address) virtual external returns (uint); + function balanceOf(address) virtual external returns (uint256); + function approve(address, uint256) virtual external returns (bool); +} diff --git a/src/interfaces/CoinJoinLike.sol b/src/interfaces/CoinJoinLike.sol index fcf2e72..e21a81e 100644 --- a/src/interfaces/CoinJoinLike.sol +++ b/src/interfaces/CoinJoinLike.sol @@ -1,7 +1,7 @@ -pragma solidity 0.6.7; - -abstract contract CoinJoinLike { - function systemCoin() virtual public view returns (address); - function safeEngine() virtual public view returns (address); - function join(address, uint256) virtual external; -} +pragma solidity 0.6.7; + +abstract contract CoinJoinLike { + function systemCoin() virtual public view returns (address); + function safeEngine() virtual public view returns (address); + function join(address, uint256) virtual external; +} diff --git a/src/interfaces/CollateralJoinLike.sol b/src/interfaces/CollateralJoinLike.sol index 0c7268e..58c2a2d 100644 --- a/src/interfaces/CollateralJoinLike.sol +++ b/src/interfaces/CollateralJoinLike.sol @@ -1,10 +1,10 @@ -pragma solidity ^0.6.7; - -abstract contract CollateralJoinLike { - function safeEngine() virtual public view returns (address); - function collateralType() virtual public view returns (bytes32); - function collateral() virtual public view returns (address); - function decimals() virtual public view returns (uint256); - function contractEnabled() virtual public view returns (uint256); - function join(address, uint256) virtual external; -} +pragma solidity ^0.6.7; + +abstract contract CollateralJoinLike { + function safeEngine() virtual public view returns (address); + function collateralType() virtual public view returns (bytes32); + function collateral() virtual public view returns (address); + function decimals() virtual public view returns (uint256); + function contractEnabled() virtual public view returns (uint256); + function join(address, uint256) virtual external; +} diff --git a/src/interfaces/ERC20Like.sol b/src/interfaces/ERC20Like.sol index 9fc880e..741d4f8 100644 --- a/src/interfaces/ERC20Like.sol +++ b/src/interfaces/ERC20Like.sol @@ -1,11 +1,11 @@ -pragma solidity ^0.6.7; - -abstract contract ERC20Like { - function approve(address guy, uint wad) virtual public returns (bool); - function transfer(address dst, uint wad) virtual public returns (bool); - function balanceOf(address) virtual external view returns (uint256); - function transferFrom(address src, address dst, uint wad) - virtual - public - returns (bool); -} +pragma solidity ^0.6.7; + +abstract contract ERC20Like { + function approve(address guy, uint wad) virtual public returns (bool); + function transfer(address dst, uint wad) virtual public returns (bool); + function balanceOf(address) virtual external view returns (uint256); + function transferFrom(address src, address dst, uint wad) + virtual + public + returns (bool); +} diff --git a/src/interfaces/GebSafeManagerLike.sol b/src/interfaces/GebSafeManagerLike.sol index 3010528..51ade79 100644 --- a/src/interfaces/GebSafeManagerLike.sol +++ b/src/interfaces/GebSafeManagerLike.sol @@ -1,7 +1,7 @@ -pragma solidity ^0.6.7; - -abstract contract GebSafeManagerLike { - function safes(uint256) virtual public view returns (address); - function ownsSAFE(uint256) virtual public view returns (address); - function safeCan(address,uint256,address) virtual public view returns (uint256); -} +pragma solidity ^0.6.7; + +abstract contract GebSafeManagerLike { + function safes(uint256) virtual public view returns (address); + function ownsSAFE(uint256) virtual public view returns (address); + function safeCan(address,uint256,address) virtual public view returns (uint256); +} diff --git a/src/interfaces/LiquidationEngineLike.sol b/src/interfaces/LiquidationEngineLike.sol index c8d8288..f5279a0 100644 --- a/src/interfaces/LiquidationEngineLike.sol +++ b/src/interfaces/LiquidationEngineLike.sol @@ -1,5 +1,5 @@ -pragma solidity ^0.6.7; - -abstract contract LiquidationEngineLike { - function safeSaviours(address) virtual public view returns (uint256); -} +pragma solidity ^0.6.7; + +abstract contract LiquidationEngineLike { + function safeSaviours(address) virtual public view returns (uint256); +} diff --git a/src/interfaces/OracleRelayerLike.sol b/src/interfaces/OracleRelayerLike.sol index 7496197..213f30d 100644 --- a/src/interfaces/OracleRelayerLike.sol +++ b/src/interfaces/OracleRelayerLike.sol @@ -1,7 +1,7 @@ -pragma solidity ^0.6.7; - -abstract contract OracleRelayerLike { - function collateralTypes(bytes32) virtual public view returns (address, uint256, uint256); - function liquidationCRatio(bytes32) virtual public view returns (uint256); - function redemptionPrice() virtual public returns (uint256); -} +pragma solidity ^0.6.7; + +abstract contract OracleRelayerLike { + function collateralTypes(bytes32) virtual public view returns (address, uint256, uint256); + function liquidationCRatio(bytes32) virtual public view returns (uint256); + function redemptionPrice() virtual public returns (uint256); +} diff --git a/src/interfaces/PriceFeedLike.sol b/src/interfaces/PriceFeedLike.sol index 225abde..6d50c24 100644 --- a/src/interfaces/PriceFeedLike.sol +++ b/src/interfaces/PriceFeedLike.sol @@ -1,7 +1,7 @@ -pragma solidity ^0.6.7; - -abstract contract PriceFeedLike { - function priceSource() virtual public view returns (address); - function read() virtual public view returns (uint256); - function getResultWithValidity() virtual external view returns (uint256,bool); -} +pragma solidity ^0.6.7; + +abstract contract PriceFeedLike { + function priceSource() virtual public view returns (address); + function read() virtual public view returns (uint256); + function getResultWithValidity() virtual external view returns (uint256,bool); +} diff --git a/src/interfaces/SAFEEngineLike.sol b/src/interfaces/SAFEEngineLike.sol index aef2a1a..6ec5ceb 100644 --- a/src/interfaces/SAFEEngineLike.sol +++ b/src/interfaces/SAFEEngineLike.sol @@ -1,26 +1,26 @@ -pragma solidity ^0.6.7; - -abstract contract SAFEEngineLike { - function approveSAFEModification(address) virtual external; - function safeRights(address,address) virtual public view returns (uint256); - function collateralTypes(bytes32) virtual public view returns ( - uint256 debtAmount, // [wad] - uint256 accumulatedRate, // [ray] - uint256 safetyPrice, // [ray] - uint256 debtCeiling, // [rad] - uint256 debtFloor, // [rad] - uint256 liquidationPrice // [ray] - ); - function safes(bytes32,address) virtual public view returns ( - uint256 lockedCollateral, // [wad] - uint256 generatedDebt // [wad] - ); - function modifySAFECollateralization( - bytes32 collateralType, - address safe, - address collateralSource, - address debtDestination, - int256 deltaCollateral, // [wad] - int256 deltaDebt // [wad] - ) virtual external; -} +pragma solidity ^0.6.7; + +abstract contract SAFEEngineLike { + function approveSAFEModification(address) virtual external; + function safeRights(address,address) virtual public view returns (uint256); + function collateralTypes(bytes32) virtual public view returns ( + uint256 debtAmount, // [wad] + uint256 accumulatedRate, // [ray] + uint256 safetyPrice, // [ray] + uint256 debtCeiling, // [rad] + uint256 debtFloor, // [rad] + uint256 liquidationPrice // [ray] + ); + function safes(bytes32,address) virtual public view returns ( + uint256 lockedCollateral, // [wad] + uint256 generatedDebt // [wad] + ); + function modifySAFECollateralization( + bytes32 collateralType, + address safe, + address collateralSource, + address debtDestination, + int256 deltaCollateral, // [wad] + int256 deltaDebt // [wad] + ) virtual external; +} diff --git a/src/interfaces/SAFESaviourRegistryLike.sol b/src/interfaces/SAFESaviourRegistryLike.sol index 1a60847..7eca514 100644 --- a/src/interfaces/SAFESaviourRegistryLike.sol +++ b/src/interfaces/SAFESaviourRegistryLike.sol @@ -1,5 +1,5 @@ -pragma solidity ^0.6.7; - -abstract contract SAFESaviourRegistryLike { - function markSave(bytes32 collateralType, address safeHandler) virtual external; -} +pragma solidity ^0.6.7; + +abstract contract SAFESaviourRegistryLike { + function markSave(bytes32 collateralType, address safeHandler) virtual external; +} diff --git a/src/interfaces/SafeSaviourLike.sol b/src/interfaces/SafeSaviourLike.sol index 87d1999..fd274ca 100644 --- a/src/interfaces/SafeSaviourLike.sol +++ b/src/interfaces/SafeSaviourLike.sol @@ -1,91 +1,91 @@ -// Copyright (C) 2020 Reflexer Labs, INC - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -pragma solidity ^0.6.7; - -import "./CollateralJoinLike.sol"; -import "./CoinJoinLike.sol"; -import "./OracleRelayerLike.sol"; -import "./SAFEEngineLike.sol"; -import "./LiquidationEngineLike.sol"; -import "./PriceFeedLike.sol"; -import "./ERC20Like.sol"; -import "./GebSafeManagerLike.sol"; -import "./TaxCollectorLike.sol"; -import "./SAFESaviourRegistryLike.sol"; - -import "../utils/ReentrancyGuard.sol"; - -abstract contract SafeSaviourLike is ReentrancyGuard { - // Checks whether a saviour contract has been approved by governance in the LiquidationEngine - modifier liquidationEngineApproved(address saviour) { - require(liquidationEngine.safeSaviours(saviour) == 1, "SafeSaviour/not-approved-in-liquidation-engine"); - _; - } - // Checks whether someone controls a safe handler inside the GebSafeManager - modifier controlsSAFE(address owner, uint256 safeID) { - require(owner != address(0), "SafeSaviour/null-owner"); - require(either(owner == safeManager.ownsSAFE(safeID), safeManager.safeCan(safeManager.ownsSAFE(safeID), safeID, owner) == 1), "SafeSaviour/not-owning-safe"); - - _; - } - - // --- Variables --- - LiquidationEngineLike public liquidationEngine; - TaxCollectorLike public taxCollector; - OracleRelayerLike public oracleRelayer; - GebSafeManagerLike public safeManager; - SAFEEngineLike public safeEngine; - SAFESaviourRegistryLike public saviourRegistry; - - // The amount of tokens the keeper gets in exchange for the gas spent to save a SAFE - uint256 public keeperPayout; // [wad] - // The minimum fiat value that the keeper must get in exchange for saving a SAFE - uint256 public minKeeperPayoutValue; // [wad] - /* - The proportion between the keeperPayout (if it's in collateral) and the amount of collateral or debt that's in a SAFE to be saved. - Alternatively, it can be the proportion between the fiat value of keeperPayout and the fiat value of the profit that a keeper - could make if a SAFE is liquidated right now. It ensures there's no incentive to intentionally put a SAFE underwater and then - save it just to make a profit that's greater than the one from participating in collateral auctions - */ - uint256 public payoutToSAFESize; - - // --- Constants --- - uint256 public constant ONE = 1; - uint256 public constant HUNDRED = 100; - uint256 public constant THOUSAND = 1000; - uint256 public constant WAD_COMPLEMENT = 10**9; - uint256 public constant WAD = 10**18; - uint256 public constant RAY = 10**27; - uint256 public constant MAX_UINT = uint(-1); - - // --- Boolean Logic --- - function both(bool x, bool y) internal pure returns (bool z) { - assembly{ z := and(x, y) } - } - function either(bool x, bool y) internal pure returns (bool z) { - assembly{ z := or(x, y)} - } - - // --- Events --- - event SaveSAFE(address indexed keeper, bytes32 indexed collateralType, address indexed safeHandler, uint256 collateralAddedOrDebtRepaid); - - // --- Functions to Implement --- - function saveSAFE(address,bytes32,address) virtual external returns (bool,uint256,uint256); - function getKeeperPayoutValue() virtual public returns (uint256); - function keeperPayoutExceedsMinValue() virtual public returns (bool); - function canSave(bytes32,address) virtual external returns (bool); - function tokenAmountUsedToSave(bytes32,address) virtual public returns (uint256); -} +// Copyright (C) 2020 Reflexer Labs, INC + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity ^0.6.7; + +import "./CollateralJoinLike.sol"; +import "./CoinJoinLike.sol"; +import "./OracleRelayerLike.sol"; +import "./SAFEEngineLike.sol"; +import "./LiquidationEngineLike.sol"; +import "./PriceFeedLike.sol"; +import "./ERC20Like.sol"; +import "./GebSafeManagerLike.sol"; +import "./TaxCollectorLike.sol"; +import "./SAFESaviourRegistryLike.sol"; + +import "../utils/ReentrancyGuard.sol"; + +abstract contract SafeSaviourLike is ReentrancyGuard { + // Checks whether a saviour contract has been approved by governance in the LiquidationEngine + modifier liquidationEngineApproved(address saviour) { + require(liquidationEngine.safeSaviours(saviour) == 1, "SafeSaviour/not-approved-in-liquidation-engine"); + _; + } + // Checks whether someone controls a safe handler inside the GebSafeManager + modifier controlsSAFE(address owner, uint256 safeID) { + require(owner != address(0), "SafeSaviour/null-owner"); + require(either(owner == safeManager.ownsSAFE(safeID), safeManager.safeCan(safeManager.ownsSAFE(safeID), safeID, owner) == 1), "SafeSaviour/not-owning-safe"); + + _; + } + + // --- Variables --- + LiquidationEngineLike public liquidationEngine; + TaxCollectorLike public taxCollector; + OracleRelayerLike public oracleRelayer; + GebSafeManagerLike public safeManager; + SAFEEngineLike public safeEngine; + SAFESaviourRegistryLike public saviourRegistry; + + // The amount of tokens the keeper gets in exchange for the gas spent to save a SAFE + uint256 public keeperPayout; // [wad] + // The minimum fiat value that the keeper must get in exchange for saving a SAFE + uint256 public minKeeperPayoutValue; // [wad] + /* + The proportion between the keeperPayout (if it's in collateral) and the amount of collateral or debt that's in a SAFE to be saved. + Alternatively, it can be the proportion between the fiat value of keeperPayout and the fiat value of the profit that a keeper + could make if a SAFE is liquidated right now. It ensures there's no incentive to intentionally put a SAFE underwater and then + save it just to make a profit that's greater than the one from participating in collateral auctions + */ + uint256 public payoutToSAFESize; + + // --- Constants --- + uint256 public constant ONE = 1; + uint256 public constant HUNDRED = 100; + uint256 public constant THOUSAND = 1000; + uint256 public constant WAD_COMPLEMENT = 10**9; + uint256 public constant WAD = 10**18; + uint256 public constant RAY = 10**27; + uint256 public constant MAX_UINT = uint(-1); + + // --- Boolean Logic --- + function both(bool x, bool y) internal pure returns (bool z) { + assembly{ z := and(x, y) } + } + function either(bool x, bool y) internal pure returns (bool z) { + assembly{ z := or(x, y)} + } + + // --- Events --- + event SaveSAFE(address indexed keeper, bytes32 indexed collateralType, address indexed safeHandler, uint256 collateralAddedOrDebtRepaid); + + // --- Functions to Implement --- + function saveSAFE(address,bytes32,address) virtual external returns (bool,uint256,uint256); + function getKeeperPayoutValue() virtual public returns (uint256); + function keeperPayoutExceedsMinValue() virtual public returns (bool); + function canSave(bytes32,address) virtual external returns (bool); + function tokenAmountUsedToSave(bytes32,address) virtual public returns (uint256); +} diff --git a/src/interfaces/SaviourCRatioSetterLike.sol b/src/interfaces/SaviourCRatioSetterLike.sol index 2d2df04..77d8c63 100644 --- a/src/interfaces/SaviourCRatioSetterLike.sol +++ b/src/interfaces/SaviourCRatioSetterLike.sol @@ -1,87 +1,87 @@ -pragma solidity 0.6.7; - -import "./OracleRelayerLike.sol"; -import "./GebSafeManagerLike.sol"; - -import "../utils/ReentrancyGuard.sol"; - -abstract contract SaviourCRatioSetterLike is ReentrancyGuard { - // --- Auth --- - mapping (address => uint256) public authorizedAccounts; - /** - * @notice Add auth to an account - * @param account Account to add auth to - */ - function addAuthorization(address account) external isAuthorized { - authorizedAccounts[account] = 1; - emit AddAuthorization(account); - } - /** - * @notice Remove auth from an account - * @param account Account to remove auth from - */ - function removeAuthorization(address account) external isAuthorized { - authorizedAccounts[account] = 0; - emit RemoveAuthorization(account); - } - /** - * @notice Checks whether msg.sender can call an authed function - **/ - modifier isAuthorized { - require(authorizedAccounts[msg.sender] == 1, "SaviourCRatioSetter/account-not-authorized"); - _; - } - - // Checks whether someone controls a safe handler inside the GebSafeManager - modifier controlsSAFE(address owner, uint256 safeID) { - require(owner != address(0), "SaviourCRatioSetter/null-owner"); - require(either(owner == safeManager.ownsSAFE(safeID), safeManager.safeCan(safeManager.ownsSAFE(safeID), safeID, owner) == 1), "SaviourCRatioSetter/not-owning-safe"); - - _; - } - - // --- Variables --- - OracleRelayerLike public oracleRelayer; - GebSafeManagerLike public safeManager; - - // Default desired cratio for each individual collateral type - mapping(bytes32 => uint256) public defaultDesiredCollateralizationRatios; - // Minimum bound for the desired cratio for each collateral type - mapping(bytes32 => uint256) public minDesiredCollateralizationRatios; - // Desired CRatios for each SAFE after they're saved - mapping(bytes32 => mapping(address => uint256)) public desiredCollateralizationRatios; - - // --- Constants --- - uint256 public constant MAX_CRATIO = 1000; - uint256 public constant CRATIO_SCALE_DOWN = 10**25; - - // --- Boolean Logic --- - function both(bool x, bool y) internal pure returns (bool z) { - assembly{ z := and(x, y) } - } - function either(bool x, bool y) internal pure returns (bool z) { - assembly{ z := or(x, y)} - } - - // --- Events --- - event AddAuthorization(address account); - event RemoveAuthorization(address account); - event ModifyParameters(bytes32 indexed parameter, address data); - event SetDefaultCRatio(bytes32 indexed collateralType, uint256 cRatio); - event SetMinDesiredCollateralizationRatio( - bytes32 indexed collateralType, - uint256 cRatio - ); - event SetDesiredCollateralizationRatio( - address indexed caller, - bytes32 indexed collateralType, - uint256 safeID, - address indexed safeHandler, - uint256 cRatio - ); - - // --- Functions --- - function setDefaultCRatio(bytes32, uint256) virtual external; - function setMinDesiredCollateralizationRatio(bytes32 collateralType, uint256 cRatio) virtual external; - function setDesiredCollateralizationRatio(bytes32 collateralType, uint256 safeID, uint256 cRatio) virtual external; -} +pragma solidity 0.6.7; + +import "./OracleRelayerLike.sol"; +import "./GebSafeManagerLike.sol"; + +import "../utils/ReentrancyGuard.sol"; + +abstract contract SaviourCRatioSetterLike is ReentrancyGuard { + // --- Auth --- + mapping (address => uint256) public authorizedAccounts; + /** + * @notice Add auth to an account + * @param account Account to add auth to + */ + function addAuthorization(address account) external isAuthorized { + authorizedAccounts[account] = 1; + emit AddAuthorization(account); + } + /** + * @notice Remove auth from an account + * @param account Account to remove auth from + */ + function removeAuthorization(address account) external isAuthorized { + authorizedAccounts[account] = 0; + emit RemoveAuthorization(account); + } + /** + * @notice Checks whether msg.sender can call an authed function + **/ + modifier isAuthorized { + require(authorizedAccounts[msg.sender] == 1, "SaviourCRatioSetter/account-not-authorized"); + _; + } + + // Checks whether someone controls a safe handler inside the GebSafeManager + modifier controlsSAFE(address owner, uint256 safeID) { + require(owner != address(0), "SaviourCRatioSetter/null-owner"); + require(either(owner == safeManager.ownsSAFE(safeID), safeManager.safeCan(safeManager.ownsSAFE(safeID), safeID, owner) == 1), "SaviourCRatioSetter/not-owning-safe"); + + _; + } + + // --- Variables --- + OracleRelayerLike public oracleRelayer; + GebSafeManagerLike public safeManager; + + // Default desired cratio for each individual collateral type + mapping(bytes32 => uint256) public defaultDesiredCollateralizationRatios; + // Minimum bound for the desired cratio for each collateral type + mapping(bytes32 => uint256) public minDesiredCollateralizationRatios; + // Desired CRatios for each SAFE after they're saved + mapping(bytes32 => mapping(address => uint256)) public desiredCollateralizationRatios; + + // --- Constants --- + uint256 public constant MAX_CRATIO = 1000; + uint256 public constant CRATIO_SCALE_DOWN = 10**25; + + // --- Boolean Logic --- + function both(bool x, bool y) internal pure returns (bool z) { + assembly{ z := and(x, y) } + } + function either(bool x, bool y) internal pure returns (bool z) { + assembly{ z := or(x, y)} + } + + // --- Events --- + event AddAuthorization(address account); + event RemoveAuthorization(address account); + event ModifyParameters(bytes32 indexed parameter, address data); + event SetDefaultCRatio(bytes32 indexed collateralType, uint256 cRatio); + event SetMinDesiredCollateralizationRatio( + bytes32 indexed collateralType, + uint256 cRatio + ); + event SetDesiredCollateralizationRatio( + address indexed caller, + bytes32 indexed collateralType, + uint256 safeID, + address indexed safeHandler, + uint256 cRatio + ); + + // --- Functions --- + function setDefaultCRatio(bytes32, uint256) virtual external; + function setMinDesiredCollateralizationRatio(bytes32 collateralType, uint256 cRatio) virtual external; + function setDesiredCollateralizationRatio(bytes32 collateralType, uint256 safeID, uint256 cRatio) virtual external; +} diff --git a/src/interfaces/SwapManagerLike.sol b/src/interfaces/SwapManagerLike.sol index ee3a01b..d05d470 100644 --- a/src/interfaces/SwapManagerLike.sol +++ b/src/interfaces/SwapManagerLike.sol @@ -1,13 +1,13 @@ -pragma solidity 0.6.7; - -abstract contract SwapManagerLike { - function swap( - address tokenIn, - address tokenOut, - uint amountIn, - uint amountOutMin, - address to - ) external virtual returns (uint256 amountOut); - - function getAmountOut(address tokenIn, address tokenOut, uint amountIn) public virtual view returns (uint256); -} +pragma solidity 0.6.7; + +abstract contract SwapManagerLike { + function swap( + address tokenIn, + address tokenOut, + uint amountIn, + uint amountOutMin, + address to + ) external virtual returns (uint256 amountOut); + + function getAmountOut(address tokenIn, address tokenOut, uint amountIn) public virtual view returns (uint256); +} diff --git a/src/interfaces/TaxCollectorLike.sol b/src/interfaces/TaxCollectorLike.sol index 0d4d4b8..e8f2e30 100644 --- a/src/interfaces/TaxCollectorLike.sol +++ b/src/interfaces/TaxCollectorLike.sol @@ -1,5 +1,5 @@ -pragma solidity 0.6.7; - -abstract contract TaxCollectorLike { - function taxSingle(bytes32) public virtual returns (uint256); -} +pragma solidity 0.6.7; + +abstract contract TaxCollectorLike { + function taxSingle(bytes32) public virtual returns (uint256); +} diff --git a/src/interfaces/UniswapLiquidityManagerLike.sol b/src/interfaces/UniswapLiquidityManagerLike.sol deleted file mode 100644 index 7ba4c74..0000000 --- a/src/interfaces/UniswapLiquidityManagerLike.sol +++ /dev/null @@ -1,16 +0,0 @@ -pragma solidity 0.6.7; - -abstract contract UniswapLiquidityManagerLike { - function getToken0FromLiquidity(uint256) virtual public view returns (uint256); - function getToken1FromLiquidity(uint256) virtual public view returns (uint256); - - function getLiquidityFromToken0(uint256) virtual public view returns (uint256); - function getLiquidityFromToken1(uint256) virtual public view returns (uint256); - - function removeLiquidity( - uint256 liquidity, - uint128 amount0Min, - uint128 amount1Min, - address to - ) public virtual returns (uint256, uint256); -} diff --git a/src/interfaces/UniswapV3NonFungiblePositionManagerLike.sol b/src/interfaces/UniswapV3NonFungiblePositionManagerLike.sol new file mode 100644 index 0000000..743f574 --- /dev/null +++ b/src/interfaces/UniswapV3NonFungiblePositionManagerLike.sol @@ -0,0 +1,35 @@ +pragma solidity 0.6.7; + +abstract contract UniswapV3NonFungiblePositionManagerLike { + struct CollectParams { + uint256 tokenId; + address recipient; + uint128 amount0Max; + uint128 amount1Max; + } + + function positions(uint256 tokenId) + external + view + virtual + returns ( + uint96 nonce, + address operator, + address token0, + address token1, + uint24 fee, + int24 tickLower, + int24 tickUpper, + uint128 liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 tokensOwed0, + uint128 tokensOwed1 + ); + function collect(CollectParams calldata) + external + payable + virtual + returns (uint256 amount0, uint256 amount1); + function burn(uint256 tokenId) external payable virtual; +} diff --git a/src/interfaces/UniswapV3PoolLike.sol b/src/interfaces/UniswapV3PoolLike.sol new file mode 100644 index 0000000..5a6972b --- /dev/null +++ b/src/interfaces/UniswapV3PoolLike.sol @@ -0,0 +1,22 @@ +pragma solidity 0.6.7; + +abstract contract UniswapV3PoolLike { + /// @notice Returns the information about a position by the position's key + /// @param key The position's key is a hash of a preimage composed by the owner, tickLower and tickUpper + /// @return _liquidity The amount of liquidity in the position, + /// Returns feeGrowthInside0LastX128 fee growth of token0 inside the tick range as of the last mint/burn/poke, + /// Returns feeGrowthInside1LastX128 fee growth of token1 inside the tick range as of the last mint/burn/poke, + /// Returns tokensOwed0 the computed amount of token0 owed to the position as of the last mint/burn/poke, + /// Returns tokensOwed1 the computed amount of token1 owed to the position as of the last mint/burn/poke + function positions(bytes32 key) + external + view + virtual + returns ( + uint128 _liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 tokensOwed0, + uint128 tokensOwed1 + ); +} diff --git a/src/interfaces/YVaultLike.sol b/src/interfaces/YVaultLike.sol index e0767b1..dcee97b 100644 --- a/src/interfaces/YVaultLike.sol +++ b/src/interfaces/YVaultLike.sol @@ -1,8 +1,8 @@ -pragma solidity 0.6.7; - -abstract contract YVaultLike { - function deposit(uint256) virtual external returns (uint256); - function withdraw(uint256) virtual external returns (uint256); - function balanceOf(address) virtual external returns (uint256); - function pricePerShare() virtual external returns (uint256); -} +pragma solidity 0.6.7; + +abstract contract YVaultLike { + function deposit(uint256) virtual external returns (uint256); + function withdraw(uint256) virtual external returns (uint256); + function balanceOf(address) virtual external returns (uint256); + function pricePerShare() virtual external returns (uint256); +} diff --git a/src/math/BabylonianMath.sol b/src/math/BabylonianMath.sol index 26ee7b1..b6d43c0 100644 --- a/src/math/BabylonianMath.sol +++ b/src/math/BabylonianMath.sol @@ -1,21 +1,21 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -pragma solidity 0.6.7; - -// computes square roots using the babylonian method -// https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method -contract BabylonianMath { - function sqrt(uint y) internal pure returns (uint z) { - if (y > 3) { - z = y; - uint x = y / 2 + 1; - while (x < z) { - z = x; - x = (y / x + x) / 2; - } - } else if (y != 0) { - z = 1; - } - // else z = 0 - } -} +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity 0.6.7; + +// computes square roots using the babylonian method +// https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method +contract BabylonianMath { + function sqrt(uint y) internal pure returns (uint z) { + if (y > 3) { + z = y; + uint x = y / 2 + 1; + while (x < z) { + z = x; + x = (y / x + x) / 2; + } + } else if (y != 0) { + z = 1; + } + // else z = 0 + } +} diff --git a/src/math/CarefulMath.sol b/src/math/CarefulMath.sol index ee935c8..ca31e99 100644 --- a/src/math/CarefulMath.sol +++ b/src/math/CarefulMath.sol @@ -1,85 +1,85 @@ -pragma solidity ^0.6.7; - -/** - * @title Careful Math - * @author Compound - * @notice Derived from OpenZeppelin's SafeMath library - * https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/contracts/math/SafeMath.sol - */ -contract CarefulMath { - - /** - * @dev Possible error codes that we can return - */ - enum MathError { - NO_ERROR, - DIVISION_BY_ZERO, - INTEGER_OVERFLOW, - INTEGER_UNDERFLOW - } - - /** - * @dev Multiplies two numbers, returns an error on overflow. - */ - function mulUInt(uint a, uint b) internal pure returns (MathError, uint) { - if (a == 0) { - return (MathError.NO_ERROR, 0); - } - - uint c = a * b; - - if (c / a != b) { - return (MathError.INTEGER_OVERFLOW, 0); - } else { - return (MathError.NO_ERROR, c); - } - } - - /** - * @dev Integer division of two numbers, truncating the quotient. - */ - function divUInt(uint a, uint b) internal pure returns (MathError, uint) { - if (b == 0) { - return (MathError.DIVISION_BY_ZERO, 0); - } - - return (MathError.NO_ERROR, a / b); - } - - /** - * @dev Subtracts two numbers, returns an error on overflow (i.e. if subtrahend is greater than minuend). - */ - function subUInt(uint a, uint b) internal pure returns (MathError, uint) { - if (b <= a) { - return (MathError.NO_ERROR, a - b); - } else { - return (MathError.INTEGER_UNDERFLOW, 0); - } - } - - /** - * @dev Adds two numbers, returns an error on overflow. - */ - function addUInt(uint a, uint b) internal pure returns (MathError, uint) { - uint c = a + b; - - if (c >= a) { - return (MathError.NO_ERROR, c); - } else { - return (MathError.INTEGER_OVERFLOW, 0); - } - } - - /** - * @dev add a and b and then subtract c - */ - function addThenSubUInt(uint a, uint b, uint c) internal pure returns (MathError, uint) { - (MathError err0, uint sum) = addUInt(a, b); - - if (err0 != MathError.NO_ERROR) { - return (err0, 0); - } - - return subUInt(sum, c); - } -} +pragma solidity ^0.6.7; + +/** + * @title Careful Math + * @author Compound + * @notice Derived from OpenZeppelin's SafeMath library + * https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/contracts/math/SafeMath.sol + */ +contract CarefulMath { + + /** + * @dev Possible error codes that we can return + */ + enum MathError { + NO_ERROR, + DIVISION_BY_ZERO, + INTEGER_OVERFLOW, + INTEGER_UNDERFLOW + } + + /** + * @dev Multiplies two numbers, returns an error on overflow. + */ + function mulUInt(uint a, uint b) internal pure returns (MathError, uint) { + if (a == 0) { + return (MathError.NO_ERROR, 0); + } + + uint c = a * b; + + if (c / a != b) { + return (MathError.INTEGER_OVERFLOW, 0); + } else { + return (MathError.NO_ERROR, c); + } + } + + /** + * @dev Integer division of two numbers, truncating the quotient. + */ + function divUInt(uint a, uint b) internal pure returns (MathError, uint) { + if (b == 0) { + return (MathError.DIVISION_BY_ZERO, 0); + } + + return (MathError.NO_ERROR, a / b); + } + + /** + * @dev Subtracts two numbers, returns an error on overflow (i.e. if subtrahend is greater than minuend). + */ + function subUInt(uint a, uint b) internal pure returns (MathError, uint) { + if (b <= a) { + return (MathError.NO_ERROR, a - b); + } else { + return (MathError.INTEGER_UNDERFLOW, 0); + } + } + + /** + * @dev Adds two numbers, returns an error on overflow. + */ + function addUInt(uint a, uint b) internal pure returns (MathError, uint) { + uint c = a + b; + + if (c >= a) { + return (MathError.NO_ERROR, c); + } else { + return (MathError.INTEGER_OVERFLOW, 0); + } + } + + /** + * @dev add a and b and then subtract c + */ + function addThenSubUInt(uint a, uint b, uint c) internal pure returns (MathError, uint) { + (MathError err0, uint sum) = addUInt(a, b); + + if (err0 != MathError.NO_ERROR) { + return (err0, 0); + } + + return subUInt(sum, c); + } +} diff --git a/src/math/FixedPointMath.sol b/src/math/FixedPointMath.sol index 3559a7b..87244d8 100644 --- a/src/math/FixedPointMath.sol +++ b/src/math/FixedPointMath.sol @@ -1,76 +1,76 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -pragma solidity 0.6.7; - -import './BabylonianMath.sol'; - -// Contract for handling binary fixed point numbers (https://en.wikipedia.org/wiki/Q_(number_format)) -contract FixedPointMath is BabylonianMath { - // range: [0, 2**112 - 1] - // resolution: 1 / 2**112 - struct uq112x112 { - uint224 _x; - } - - // range: [0, 2**144 - 1] - // resolution: 1 / 2**112 - struct uq144x112 { - uint _x; - } - - uint8 private constant RESOLUTION = 112; - uint private constant Q112 = uint(1) << RESOLUTION; - uint private constant Q224 = Q112 << RESOLUTION; - - // encode a uint112 as a UQ112x112 - function encode(uint112 x) internal pure returns (uq112x112 memory) { - return uq112x112(uint224(x) << RESOLUTION); - } - - // encodes a uint144 as a UQ144x112 - function encode144(uint144 x) internal pure returns (uq144x112 memory) { - return uq144x112(uint256(x) << RESOLUTION); - } - - // divide a UQ112x112 by a uint112, returning a UQ112x112 - function div(uq112x112 memory self, uint112 x) internal pure returns (uq112x112 memory) { - require(x != 0, 'FixedPoint: DIV_BY_ZERO'); - return uq112x112(self._x / uint224(x)); - } - - // multiply a UQ112x112 by a uint, returning a UQ144x112 - // reverts on overflow - function mul(uq112x112 memory self, uint y) internal pure returns (uq144x112 memory) { - uint z; - require(y == 0 || (z = uint(self._x) * y) / y == uint(self._x), "FixedPoint: MULTIPLICATION_OVERFLOW"); - return uq144x112(z); - } - - // returns a UQ112x112 which represents the ratio of the numerator to the denominator - // equivalent to encode(numerator).divide(denominator) - function frac(uint112 numerator, uint112 denominator) internal pure returns (uq112x112 memory) { - require(denominator > 0, "FixedPoint: DIV_BY_ZERO"); - return uq112x112((uint224(numerator) << RESOLUTION) / denominator); - } - - // decode a UQ112x112 into a uint112 by truncating after the radix point - function decode(uq112x112 memory self) internal pure returns (uint112) { - return uint112(self._x >> RESOLUTION); - } - - // decode a UQ144x112 into a uint144 by truncating after the radix point - function decode144(uq144x112 memory self) internal pure returns (uint144) { - return uint144(self._x >> RESOLUTION); - } - - // take the reciprocal of a UQ112x112 - function reciprocal(uq112x112 memory self) internal pure returns (uq112x112 memory) { - require(self._x != 0, 'FixedPoint: ZERO_RECIPROCAL'); - return uq112x112(uint224(Q224 / self._x)); - } - - // square root of a UQ112x112 - function sqrt(uq112x112 memory self) internal pure returns (uq112x112 memory) { - return uq112x112(uint224(super.sqrt(uint256(self._x)) << 56)); - } -} +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity 0.6.7; + +import './BabylonianMath.sol'; + +// Contract for handling binary fixed point numbers (https://en.wikipedia.org/wiki/Q_(number_format)) +contract FixedPointMath is BabylonianMath { + // range: [0, 2**112 - 1] + // resolution: 1 / 2**112 + struct uq112x112 { + uint224 _x; + } + + // range: [0, 2**144 - 1] + // resolution: 1 / 2**112 + struct uq144x112 { + uint _x; + } + + uint8 private constant RESOLUTION = 112; + uint private constant Q112 = uint(1) << RESOLUTION; + uint private constant Q224 = Q112 << RESOLUTION; + + // encode a uint112 as a UQ112x112 + function encode(uint112 x) internal pure returns (uq112x112 memory) { + return uq112x112(uint224(x) << RESOLUTION); + } + + // encodes a uint144 as a UQ144x112 + function encode144(uint144 x) internal pure returns (uq144x112 memory) { + return uq144x112(uint256(x) << RESOLUTION); + } + + // divide a UQ112x112 by a uint112, returning a UQ112x112 + function div(uq112x112 memory self, uint112 x) internal pure returns (uq112x112 memory) { + require(x != 0, 'FixedPoint: DIV_BY_ZERO'); + return uq112x112(self._x / uint224(x)); + } + + // multiply a UQ112x112 by a uint, returning a UQ144x112 + // reverts on overflow + function mul(uq112x112 memory self, uint y) internal pure returns (uq144x112 memory) { + uint z; + require(y == 0 || (z = uint(self._x) * y) / y == uint(self._x), "FixedPoint: MULTIPLICATION_OVERFLOW"); + return uq144x112(z); + } + + // returns a UQ112x112 which represents the ratio of the numerator to the denominator + // equivalent to encode(numerator).divide(denominator) + function frac(uint112 numerator, uint112 denominator) internal pure returns (uq112x112 memory) { + require(denominator > 0, "FixedPoint: DIV_BY_ZERO"); + return uq112x112((uint224(numerator) << RESOLUTION) / denominator); + } + + // decode a UQ112x112 into a uint112 by truncating after the radix point + function decode(uq112x112 memory self) internal pure returns (uint112) { + return uint112(self._x >> RESOLUTION); + } + + // decode a UQ144x112 into a uint144 by truncating after the radix point + function decode144(uq144x112 memory self) internal pure returns (uint144) { + return uint144(self._x >> RESOLUTION); + } + + // take the reciprocal of a UQ112x112 + function reciprocal(uq112x112 memory self) internal pure returns (uq112x112 memory) { + require(self._x != 0, 'FixedPoint: ZERO_RECIPROCAL'); + return uq112x112(uint224(Q224 / self._x)); + } + + // square root of a UQ112x112 + function sqrt(uq112x112 memory self) internal pure returns (uq112x112 memory) { + return uq112x112(uint224(super.sqrt(uint256(self._x)) << 56)); + } +} diff --git a/src/math/Math.sol b/src/math/Math.sol index 3ab6a87..89634c6 100644 --- a/src/math/Math.sol +++ b/src/math/Math.sol @@ -1,45 +1,45 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.6.0 <0.8.0; - -/** - * @dev Standard math utilities missing in the Solidity language. - */ -contract Math { - /** - * @dev Returns the largest of two numbers. - */ - function max(uint256 a, uint256 b) internal pure returns (uint256) { - return a >= b ? a : b; - } - - /** - * @dev Returns the smallest of two numbers. - */ - function min(uint256 a, uint256 b) internal pure returns (uint256) { - return a < b ? a : b; - } - - /** - * @dev Returns the average of two numbers. The result is rounded towards - * zero. - */ - function average(uint256 a, uint256 b) internal pure returns (uint256) { - // (a + b) / 2 can overflow, so we distribute - return (a / 2) + (b / 2) + ((a % 2 + b % 2) / 2); - } - - // babylonian method (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method) - function sqrt(uint y) internal pure returns (uint z) { - if (y > 3) { - z = y; - uint x = y / 2 + 1; - while (x < z) { - z = x; - x = (y / x + x) / 2; - } - } else if (y != 0) { - z = 1; - } - } -} +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.0 <0.8.0; + +/** + * @dev Standard math utilities missing in the Solidity language. + */ +contract Math { + /** + * @dev Returns the largest of two numbers. + */ + function max(uint256 a, uint256 b) internal pure returns (uint256) { + return a >= b ? a : b; + } + + /** + * @dev Returns the smallest of two numbers. + */ + function min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } + + /** + * @dev Returns the average of two numbers. The result is rounded towards + * zero. + */ + function average(uint256 a, uint256 b) internal pure returns (uint256) { + // (a + b) / 2 can overflow, so we distribute + return (a / 2) + (b / 2) + ((a % 2 + b % 2) / 2); + } + + // babylonian method (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method) + function sqrt(uint y) internal pure returns (uint z) { + if (y > 3) { + z = y; + uint x = y / 2 + 1; + while (x < z) { + z = x; + x = (y / x + x) / 2; + } + } else if (y != 0) { + z = 1; + } + } +} diff --git a/src/math/SafeMath.sol b/src/math/SafeMath.sol index 9da1b91..81dd273 100644 --- a/src/math/SafeMath.sol +++ b/src/math/SafeMath.sol @@ -1,159 +1,159 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.6.0 <0.8.0; - -/** - * @dev Wrappers over Solidity's arithmetic operations with added overflow - * checks. - * - * Arithmetic operations in Solidity wrap on overflow. This can easily result - * in bugs, because programmers usually assume that an overflow raises an - * error, which is the standard behavior in high level programming languages. - * `SafeMath` restores this intuition by reverting the transaction when an - * operation overflows. - * - * Using this library instead of the unchecked operations eliminates an entire - * class of bugs, so it's recommended to use it always. - */ -contract SafeMath { - /** - * @dev Returns the addition of two unsigned integers, reverting on - * overflow. - * - * Counterpart to Solidity's `+` operator. - * - * Requirements: - * - * - Addition cannot overflow. - */ - function add(uint256 a, uint256 b) internal pure returns (uint256) { - uint256 c = a + b; - require(c >= a, "SafeMath: addition overflow"); - - return c; - } - - /** - * @dev Returns the subtraction of two unsigned integers, reverting on - * overflow (when the result is negative). - * - * Counterpart to Solidity's `-` operator. - * - * Requirements: - * - * - Subtraction cannot overflow. - */ - function sub(uint256 a, uint256 b) internal pure returns (uint256) { - return sub(a, b, "SafeMath: subtraction overflow"); - } - - /** - * @dev Returns the subtraction of two unsigned integers, reverting with custom message on - * overflow (when the result is negative). - * - * Counterpart to Solidity's `-` operator. - * - * Requirements: - * - * - Subtraction cannot overflow. - */ - function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { - require(b <= a, errorMessage); - uint256 c = a - b; - - return c; - } - - /** - * @dev Returns the multiplication of two unsigned integers, reverting on - * overflow. - * - * Counterpart to Solidity's `*` operator. - * - * Requirements: - * - * - Multiplication cannot overflow. - */ - function mul(uint256 a, uint256 b) internal pure returns (uint256) { - // Gas optimization: this is cheaper than requiring 'a' not being zero, but the - // benefit is lost if 'b' is also tested. - // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 - if (a == 0) { - return 0; - } - - uint256 c = a * b; - require(c / a == b, "SafeMath: multiplication overflow"); - - return c; - } - - /** - * @dev Returns the integer division of two unsigned integers. Reverts on - * division by zero. The result is rounded towards zero. - * - * Counterpart to Solidity's `/` operator. Note: this function uses a - * `revert` opcode (which leaves remaining gas untouched) while Solidity - * uses an invalid opcode to revert (consuming all remaining gas). - * - * Requirements: - * - * - The divisor cannot be zero. - */ - function div(uint256 a, uint256 b) internal pure returns (uint256) { - return div(a, b, "SafeMath: division by zero"); - } - - /** - * @dev Returns the integer division of two unsigned integers. Reverts with custom message on - * division by zero. The result is rounded towards zero. - * - * Counterpart to Solidity's `/` operator. Note: this function uses a - * `revert` opcode (which leaves remaining gas untouched) while Solidity - * uses an invalid opcode to revert (consuming all remaining gas). - * - * Requirements: - * - * - The divisor cannot be zero. - */ - function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { - require(b > 0, errorMessage); - uint256 c = a / b; - // assert(a == b * c + a % b); // There is no case in which this doesn't hold - - return c; - } - - /** - * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), - * Reverts when dividing by zero. - * - * Counterpart to Solidity's `%` operator. This function uses a `revert` - * opcode (which leaves remaining gas untouched) while Solidity uses an - * invalid opcode to revert (consuming all remaining gas). - * - * Requirements: - * - * - The divisor cannot be zero. - */ - function mod(uint256 a, uint256 b) internal pure returns (uint256) { - return mod(a, b, "SafeMath: modulo by zero"); - } - - /** - * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), - * Reverts with custom message when dividing by zero. - * - * Counterpart to Solidity's `%` operator. This function uses a `revert` - * opcode (which leaves remaining gas untouched) while Solidity uses an - * invalid opcode to revert (consuming all remaining gas). - * - * Requirements: - * - * - The divisor cannot be zero. - */ - function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { - require(b != 0, errorMessage); - return a % b; - } -} +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.0 <0.8.0; + +/** + * @dev Wrappers over Solidity's arithmetic operations with added overflow + * checks. + * + * Arithmetic operations in Solidity wrap on overflow. This can easily result + * in bugs, because programmers usually assume that an overflow raises an + * error, which is the standard behavior in high level programming languages. + * `SafeMath` restores this intuition by reverting the transaction when an + * operation overflows. + * + * Using this library instead of the unchecked operations eliminates an entire + * class of bugs, so it's recommended to use it always. + */ +contract SafeMath { + /** + * @dev Returns the addition of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `+` operator. + * + * Requirements: + * + * - Addition cannot overflow. + */ + function add(uint256 a, uint256 b) internal pure returns (uint256) { + uint256 c = a + b; + require(c >= a, "SafeMath: addition overflow"); + + return c; + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting on + * overflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * + * - Subtraction cannot overflow. + */ + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + return sub(a, b, "SafeMath: subtraction overflow"); + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting with custom message on + * overflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * + * - Subtraction cannot overflow. + */ + function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { + require(b <= a, errorMessage); + uint256 c = a - b; + + return c; + } + + /** + * @dev Returns the multiplication of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `*` operator. + * + * Requirements: + * + * - Multiplication cannot overflow. + */ + function mul(uint256 a, uint256 b) internal pure returns (uint256) { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) { + return 0; + } + + uint256 c = a * b; + require(c / a == b, "SafeMath: multiplication overflow"); + + return c; + } + + /** + * @dev Returns the integer division of two unsigned integers. Reverts on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function div(uint256 a, uint256 b) internal pure returns (uint256) { + return div(a, b, "SafeMath: division by zero"); + } + + /** + * @dev Returns the integer division of two unsigned integers. Reverts with custom message on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { + require(b > 0, errorMessage); + uint256 c = a / b; + // assert(a == b * c + a % b); // There is no case in which this doesn't hold + + return c; + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * Reverts when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function mod(uint256 a, uint256 b) internal pure returns (uint256) { + return mod(a, b, "SafeMath: modulo by zero"); + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * Reverts with custom message when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { + require(b != 0, errorMessage); + return a % b; + } +} diff --git a/src/math/UQ112x112.sol b/src/math/UQ112x112.sol index 8a740ae..b43972c 100644 --- a/src/math/UQ112x112.sol +++ b/src/math/UQ112x112.sol @@ -1,20 +1,20 @@ -pragma solidity 0.6.7; - -// a library for handling binary fixed point numbers (https://en.wikipedia.org/wiki/Q_(number_format)) - -// range: [0, 2**112 - 1] -// resolution: 1 / 2**112 - -contract UQ112x112 { - uint224 constant Q112 = 2**112; - - // encode a uint112 as a UQ112x112 - function encode(uint112 y) internal pure returns (uint224 z) { - z = uint224(y) * Q112; // never overflows - } - - // divide a UQ112x112 by a uint112, returning a UQ112x112 - function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) { - z = x / uint224(y); - } -} +pragma solidity 0.6.7; + +// a library for handling binary fixed point numbers (https://en.wikipedia.org/wiki/Q_(number_format)) + +// range: [0, 2**112 - 1] +// resolution: 1 / 2**112 + +contract UQ112x112 { + uint224 constant Q112 = 2**112; + + // encode a uint112 as a UQ112x112 + function encode(uint112 y) internal pure returns (uint224 z) { + z = uint224(y) * Q112; // never overflows + } + + // divide a UQ112x112 by a uint112, returning a UQ112x112 + function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) { + z = x / uint224(y); + } +} diff --git a/src/saviours/CompoundSystemCoinSafeSaviour.sol b/src/saviours/CompoundSystemCoinSafeSaviour.sol index c5e8693..2ae712d 100644 --- a/src/saviours/CompoundSystemCoinSafeSaviour.sol +++ b/src/saviours/CompoundSystemCoinSafeSaviour.sol @@ -1,461 +1,461 @@ -// Copyright (C) 2021 Reflexer Labs, INC - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -pragma solidity 0.6.7; - -import "../interfaces/CTokenLike.sol"; -import "../interfaces/SaviourCRatioSetterLike.sol"; -import "../interfaces/SafeSaviourLike.sol"; -import "../math/SafeMath.sol"; - -contract CompoundSystemCoinSafeSaviour is SafeMath, SafeSaviourLike { - // --- Auth --- - mapping (address => uint256) public authorizedAccounts; - /** - * @notice Add auth to an account - * @param account Account to add auth to - */ - function addAuthorization(address account) external isAuthorized { - authorizedAccounts[account] = 1; - emit AddAuthorization(account); - } - /** - * @notice Remove auth from an account - * @param account Account to remove auth from - */ - function removeAuthorization(address account) external isAuthorized { - authorizedAccounts[account] = 0; - emit RemoveAuthorization(account); - } - /** - * @notice Checks whether msg.sender can call an authed function - **/ - modifier isAuthorized { - require(authorizedAccounts[msg.sender] == 1, "CompoundSystemCoinSafeSaviour/account-not-authorized"); - _; - } - - mapping (address => uint256) public allowedUsers; - /** - * @notice Allow a user to deposit assets - * @param usr User to whitelist - */ - function allowUser(address usr) external isAuthorized { - allowedUsers[usr] = 1; - emit AllowUser(usr); - } - /** - * @notice Disallow a user from depositing assets - * @param usr User to disallow - */ - function disallowUser(address usr) external isAuthorized { - allowedUsers[usr] = 0; - emit DisallowUser(usr); - } - /** - * @notice Checks whether an address is an allowed user - **/ - modifier isAllowed { - require( - either(restrictUsage == 0, both(restrictUsage == 1, allowedUsers[msg.sender] == 1)), - "CompoundSystemCoinSafeSaviour/account-not-allowed" - ); - _; - } - - // --- Variables --- - // Flag that tells whether usage of the contract is restricted to allowed users - uint256 public restrictUsage; - - // Amount of cTokens currently protecting each position - mapping(bytes32 => mapping(address => uint256)) public cTokenCover; - // The cToken address - CTokenLike public cToken; - // The ERC20 system coin - ERC20Like public systemCoin; - // The system coin join contract - CoinJoinLike public coinJoin; - // Oracle providing the system coin price feed - PriceFeedLike public systemCoinOrcl; - // Contract that defines desired CRatios for each Safe after it is saved - SaviourCRatioSetterLike public cRatioSetter; - - // --- Events --- - event AddAuthorization(address account); - event RemoveAuthorization(address account); - event AllowUser(address usr); - event DisallowUser(address usr); - event ModifyParameters(bytes32 indexed parameter, uint256 val); - event ModifyParameters(bytes32 indexed parameter, address data); - event Deposit( - address indexed caller, - bytes32 collateralType, - address indexed safeHandler, - uint256 systemCoinAmount, - uint256 cTokenAmount - ); - event Withdraw( - address indexed caller, - bytes32 collateralType, - address indexed safeHandler, - address dst, - uint256 systemCoinAmount, - uint256 cTokenAmount - ); - - constructor( - address coinJoin_, - address cRatioSetter_, - address systemCoinOrcl_, - address liquidationEngine_, - address taxCollector_, - address oracleRelayer_, - address safeManager_, - address saviourRegistry_, - address cToken_, - uint256 keeperPayout_, - uint256 minKeeperPayoutValue_ - ) public { - require(coinJoin_ != address(0), "CompoundSystemCoinSafeSaviour/null-coin-join"); - require(cRatioSetter_ != address(0), "CompoundSystemCoinSafeSaviour/null-cratio-setter"); - require(systemCoinOrcl_ != address(0), "CompoundSystemCoinSafeSaviour/null-system-coin-oracle"); - require(oracleRelayer_ != address(0), "CompoundSystemCoinSafeSaviour/null-oracle-relayer"); - require(taxCollector_ != address(0), "CompoundSystemCoinSafeSaviour/null-tax-collector"); - require(liquidationEngine_ != address(0), "CompoundSystemCoinSafeSaviour/null-liquidation-engine"); - require(safeManager_ != address(0), "CompoundSystemCoinSafeSaviour/null-safe-manager"); - require(saviourRegistry_ != address(0), "CompoundSystemCoinSafeSaviour/null-saviour-registry"); - require(cToken_ != address(0), "CompoundSystemCoinSafeSaviour/null-c-token"); - require(keeperPayout_ > 0, "CompoundSystemCoinSafeSaviour/invalid-keeper-payout"); - require(minKeeperPayoutValue_ > 0, "CompoundSystemCoinSafeSaviour/invalid-min-payout-value"); - - authorizedAccounts[msg.sender] = 1; - - keeperPayout = keeperPayout_; - minKeeperPayoutValue = minKeeperPayoutValue_; - - coinJoin = CoinJoinLike(coinJoin_); - cRatioSetter = SaviourCRatioSetterLike(cRatioSetter_); - liquidationEngine = LiquidationEngineLike(liquidationEngine_); - taxCollector = TaxCollectorLike(taxCollector_); - oracleRelayer = OracleRelayerLike(oracleRelayer_); - systemCoinOrcl = PriceFeedLike(systemCoinOrcl_); - systemCoin = ERC20Like(coinJoin.systemCoin()); - safeEngine = SAFEEngineLike(coinJoin.safeEngine()); - safeManager = GebSafeManagerLike(safeManager_); - saviourRegistry = SAFESaviourRegistryLike(saviourRegistry_); - cToken = CTokenLike(cToken_); - - systemCoinOrcl.read(); - systemCoinOrcl.getResultWithValidity(); - oracleRelayer.redemptionPrice(); - - require(cToken.isCToken(), "CompoundSystemCoinSafeSaviour/not-c-token"); - require(address(safeEngine) != address(0), "CompoundSystemCoinSafeSaviour/null-safe-engine"); - require(address(systemCoin) != address(0), "CompoundSystemCoinSafeSaviour/null-sys-coin"); - - emit AddAuthorization(msg.sender); - emit ModifyParameters("keeperPayout", keeperPayout); - emit ModifyParameters("minKeeperPayoutValue", minKeeperPayoutValue); - emit ModifyParameters("liquidationEngine", liquidationEngine_); - emit ModifyParameters("taxCollector", taxCollector_); - emit ModifyParameters("oracleRelayer", oracleRelayer_); - emit ModifyParameters("systemCoinOrcl", systemCoinOrcl_); - } - - // --- Administration --- - /** - * @notice Modify an uint256 param - * @param parameter The name of the parameter - * @param val New value for the parameter - */ - function modifyParameters(bytes32 parameter, uint256 val) external isAuthorized { - if (parameter == "keeperPayout") { - require(val > 0, "CompoundSystemCoinSafeSaviour/null-payout"); - keeperPayout = val; - } - else if (parameter == "minKeeperPayoutValue") { - require(val > 0, "CompoundSystemCoinSafeSaviour/null-min-payout"); - minKeeperPayoutValue = val; - } - else if (parameter == "restrictUsage") { - require(val <= 1, "CompoundSystemCoinSafeSaviour/invalid-restriction"); - restrictUsage = val; - } - else revert("CompoundSystemCoinSafeSaviour/modify-unrecognized-param"); - emit ModifyParameters(parameter, val); - } - /** - * @notice Modify an address param - * @param parameter The name of the parameter - * @param data New address for the parameter - */ - function modifyParameters(bytes32 parameter, address data) external isAuthorized { - require(data != address(0), "CompoundSystemCoinSafeSaviour/null-data"); - - if (parameter == "systemCoinOrcl") { - systemCoinOrcl = PriceFeedLike(data); - systemCoinOrcl.read(); - systemCoinOrcl.getResultWithValidity(); - } - else if (parameter == "oracleRelayer") { - oracleRelayer = OracleRelayerLike(data); - oracleRelayer.redemptionPrice(); - } - else if (parameter == "liquidationEngine") { - liquidationEngine = LiquidationEngineLike(data); - } - else if (parameter == "taxCollector") { - taxCollector = TaxCollectorLike(data); - } - else revert("CompoundSystemCoinSafeSaviour/modify-unrecognized-param"); - emit ModifyParameters(parameter, data); - } - - // --- Adding/Withdrawing Cover --- - /* - * @notice Deposit system coins in the contract and lend them on Compound in order to provide cover for a specific - * SAFE controlled by the SAFE Manager - * @param collateralType The collateral type used in the SAFE - * @param safeID The ID of the SAFE to protect. This ID should be registered inside GebSafeManager - * @param systemCoinAmount The amount of system coins to deposit - */ - function deposit(bytes32 collateralType, uint256 safeID, uint256 systemCoinAmount) - external isAllowed() liquidationEngineApproved(address(this)) nonReentrant { - uint256 defaultCRatio = cRatioSetter.defaultDesiredCollateralizationRatios(collateralType); - require(systemCoinAmount > 0, "CompoundSystemCoinSafeSaviour/null-system-coin-amount"); - require(defaultCRatio > 0, "CompoundSystemCoinSafeSaviour/collateral-not-set"); - - // Check that the SAFE exists inside GebSafeManager - address safeHandler = safeManager.safes(safeID); - require(safeHandler != address(0), "CompoundSystemCoinSafeSaviour/null-handler"); - - // Check that the SAFE has debt - (, uint256 safeDebt) = safeEngine.safes(collateralType, safeHandler); - require(safeDebt > 0, "CompoundSystemCoinSafeSaviour/safe-does-not-have-debt"); - - // Lend on Compound - uint256 currentCTokenBalance = cToken.balanceOf(address(this)); - systemCoin.transferFrom(msg.sender, address(this), systemCoinAmount); - systemCoin.approve(address(cToken), systemCoinAmount); - require(cToken.mint(systemCoinAmount) == 0, "CompoundSystemCoinSafeSaviour/cannot-mint-ctoken"); - - // Update the cToken balance used to cover the SAFE - cTokenCover[collateralType][safeHandler] = - add(cTokenCover[collateralType][safeHandler], sub(cToken.balanceOf(address(this)), currentCTokenBalance)); - - emit Deposit(msg.sender, collateralType, safeHandler, systemCoinAmount, sub(cToken.balanceOf(address(this)), currentCTokenBalance)); - } - - /* - * @notice Withdraw system coins from the contract by exiting your Compound lending position - * @dev Only an address that controls the SAFE inside GebSafeManager can call this - * @param safeID The ID of the SAFE to remove cover from. This ID should be registered inside GebSafeManager - * @param cTokenAmount The amount of cTokens to use and redeem system coins from Compound - * @param dst The address that will receive the withdrawn system coins - */ - function withdraw(bytes32 collateralType, uint256 safeID, uint256 cTokenAmount, address dst) - external controlsSAFE(msg.sender, safeID) nonReentrant { - require(cTokenAmount > 0, "CompoundSystemCoinSafeSaviour/null-cToken-amount"); - - // Fetch the handler from the SAFE manager - address safeHandler = safeManager.safes(safeID); - require(cTokenCover[collateralType][safeHandler] >= cTokenAmount, "CompoundSystemCoinSafeSaviour/not-enough-to-redeem"); - - // Redeem system coins from Compound and transfer them to the caller - uint256 currentSystemCoinAmount = systemCoin.balanceOf(address(this)); - cTokenCover[collateralType][safeHandler] = sub(cTokenCover[collateralType][safeHandler], cTokenAmount); - require(cToken.redeem(cTokenAmount) == 0, "CompoundSystemCoinSafeSaviour/cannot-redeem-ctoken"); - - uint256 amountTransferred = sub(systemCoin.balanceOf(address(this)), currentSystemCoinAmount); - systemCoin.transfer(dst, amountTransferred); - - emit Withdraw( - msg.sender, - collateralType, - safeHandler, - dst, - amountTransferred, - cTokenAmount - ); - } - - // --- Saving Logic --- - /* - * @notice Saves a SAFE by repaying some of its debt (using cTokens) - * @dev Only the LiquidationEngine can call this - * @param keeper The keeper that called LiquidationEngine.liquidateSAFE and that should be rewarded for spending gas to save a SAFE - * @param collateralType The collateral type backing the SAFE that's being liquidated - * @param safeHandler The handler of the SAFE that's being liquidated - * @return Whether the SAFE has been saved, the amount of debt repaid as well as the amount of - * system coins sent to the keeper as their payment - */ - function saveSAFE(address keeper, bytes32 collateralType, address safeHandler) override external returns (bool, uint256, uint256) { - require(address(liquidationEngine) == msg.sender, "CompoundSystemCoinSafeSaviour/caller-not-liquidation-engine"); - require(keeper != address(0), "CompoundSystemCoinSafeSaviour/null-keeper-address"); - - if (both(both(collateralType == "", safeHandler == address(0)), keeper == address(liquidationEngine))) { - return (true, uint(-1), uint(-1)); - } - - // Check that the fiat value of the keeper payout is high enough - require(keeperPayoutExceedsMinValue(), "CompoundSystemCoinSafeSaviour/small-keeper-payout-value"); - - // Tax the collateral - taxCollector.taxSingle(collateralType); - - // Compute and check the validity of the amount of cTokens used to save the SAFE - uint256 tokenAmountUsed = tokenAmountUsedToSave(collateralType, safeHandler); - require(both(tokenAmountUsed != MAX_UINT, tokenAmountUsed != 0), "CompoundSystemCoinSafeSaviour/invalid-tokens-used-to-save"); - - // Check that there are enough cTokens added to cover both the keeper's payout and the amount used to save the SAFE - uint256 keeperCTokenPayout = div(mul(keeperPayout, WAD), cToken.exchangeRateStored()); - require(cTokenCover[collateralType][safeHandler] >= add(keeperCTokenPayout, tokenAmountUsed), "CompoundSystemCoinSafeSaviour/not-enough-cover-deposited"); - - // Update the remaining cover - cTokenCover[collateralType][safeHandler] = sub(cTokenCover[collateralType][safeHandler], add(keeperCTokenPayout, tokenAmountUsed)); - - // Mark the SAFE in the registry as just having been saved - saviourRegistry.markSave(collateralType, safeHandler); - - // Get system coins back from Compound - uint256 currentSystemCoinAmount = systemCoin.balanceOf(address(this)); - require(cToken.redeem(add(keeperCTokenPayout, tokenAmountUsed)) == 0, "CompoundSystemCoinSafeSaviour/cannot-redeem-ctoken"); - uint256 systemCoinsToRepay = sub(sub(systemCoin.balanceOf(address(this)), currentSystemCoinAmount), keeperPayout); - - // Approve the coin join contract to take system coins and repay debt - systemCoin.approve(address(coinJoin), 0); - systemCoin.approve(address(coinJoin), systemCoinsToRepay); - - // Join system coins in the system and repay the SAFE's debt - { - coinJoin.join(address(this), systemCoinsToRepay); - uint256 nonAdjustedSystemCoinsToRepay = div(mul(systemCoinsToRepay, RAY), getAccumulatedRate(collateralType)); - - safeEngine.modifySAFECollateralization( - collateralType, - safeHandler, - address(0), - address(this), - int256(0), - -int256(nonAdjustedSystemCoinsToRepay) - ); - } - - // Send the fee to the keeper - systemCoin.transfer(keeper, keeperPayout); - - // Emit an event - emit SaveSAFE(keeper, collateralType, safeHandler, tokenAmountUsed); - - return (true, tokenAmountUsed, keeperPayout); - } - - // --- Getters --- - /* - * @notice Compute whether the value of keeperPayout system coins is higher than or equal to minKeeperPayoutValue - * @dev Used to determine whether it's worth it for the keeper to save the SAFE in exchange for keeperPayout system coins - * @return A bool representing whether the value of keeperPayout system coins is >= minKeeperPayoutValue - */ - function keeperPayoutExceedsMinValue() override public returns (bool) { - (uint256 priceFeedValue, bool hasValidValue) = systemCoinOrcl.getResultWithValidity(); - - if (either(!hasValidValue, priceFeedValue == 0)) { - return false; - } - - return (minKeeperPayoutValue <= mul(keeperPayout, priceFeedValue) / WAD); - } - /* - * @notice Return the current value of the keeper payout - */ - function getKeeperPayoutValue() override public returns (uint256) { - (uint256 priceFeedValue, bool hasValidValue) = systemCoinOrcl.getResultWithValidity(); - - if (either(!hasValidValue, priceFeedValue == 0)) { - return 0; - } - - return mul(keeperPayout, priceFeedValue) / WAD; - } - /* - * @notice Determine whether a SAFE can be saved with the current amount of cTokens deposited as cover for it - * @param safeHandler The handler of the SAFE which the function takes into account - * @return Whether the SAFE can be saved or not - */ - function canSave(bytes32 collateralType, address safeHandler) override external returns (bool) { - uint256 tokenAmountUsed = tokenAmountUsedToSave(collateralType, safeHandler); - - if (either(tokenAmountUsed == MAX_UINT, tokenAmountUsed == 0)) { - return false; - } - - uint256 keeperCTokenPayout = div(mul(keeperPayout, WAD), cToken.exchangeRateStored()); - return (cTokenCover[collateralType][safeHandler] >= add(tokenAmountUsed, keeperCTokenPayout)); - } - /* - * @notice Calculate the amount of cTokens used to save a SAFE and bring its CRatio to the desired level - * @param collateralType The SAFE collateral type - * @param safeHandler The handler of the SAFE which the function takes into account - * @return The amount of cTokens used to save the SAFE and bring its CRatio to the desired level - */ - function tokenAmountUsedToSave(bytes32 collateralType, address safeHandler) override public returns (uint256) { - if (cTokenCover[collateralType][safeHandler] == 0) return 0; - - (uint256 depositedCollateralToken, uint256 safeDebt) = safeEngine.safes(collateralType, safeHandler); - (address ethFSM,,) = oracleRelayer.collateralTypes(collateralType); - if (ethFSM == address(0)) return MAX_UINT; - - (uint256 priceFeedValue, bool hasValidValue) = PriceFeedLike(ethFSM).getResultWithValidity(); - - // If the SAFE doesn't have debt, if the price feed is faulty or if the default desired CRatio is null, abort - uint256 defaultCRatio = cRatioSetter.defaultDesiredCollateralizationRatios(collateralType); - if (either(either(safeDebt == 0, either(priceFeedValue == 0, !hasValidValue)), defaultCRatio == 0)) { - return MAX_UINT; - } - - // Calculate the amount of debt that needs to be repaid so the SAFE gets to the target CRatio - uint256 targetCRatio = (cRatioSetter.desiredCollateralizationRatios(collateralType, safeHandler) == 0) ? - defaultCRatio : cRatioSetter.desiredCollateralizationRatios(collateralType, safeHandler); - - uint256 targetDebtAmount = mul( - mul(HUNDRED, mul(depositedCollateralToken, priceFeedValue) / WAD) / targetCRatio, RAY - ) / oracleRelayer.redemptionPrice(); - - // If you need to repay more than the amount of debt in the SAFE (or all the debt), return 0 - if (either(targetDebtAmount >= safeDebt, debtBelowFloor(collateralType, targetDebtAmount))) { - return 0; - } else { - safeDebt = mul(safeDebt, getAccumulatedRate(collateralType)) / RAY; - return div(mul(sub(safeDebt, targetDebtAmount), WAD), cToken.exchangeRateCurrent()); - } - } - /* - * @notify Returns whether a target debt amount is below the debt floor of a specific collateral type - * @param collateralType The collateral type whose floor we compare against - * @param targetDebtAmount The target debt amount for a SAFE that has collateralType collateral in it - */ - function debtBelowFloor(bytes32 collateralType, uint256 targetDebtAmount) public view returns (bool) { - (, , , , uint256 debtFloor, ) = safeEngine.collateralTypes(collateralType); - return (mul(targetDebtAmount, RAY) < debtFloor); - } - /* - * @notify Get the accumulated interest rate for a specific collateral type - * @param The collateral type for which to retrieve the rate - */ - function getAccumulatedRate(bytes32 collateralType) - public view returns (uint256 accumulatedRate) { - (, accumulatedRate, , , , ) = safeEngine.collateralTypes(collateralType); - } -} +// Copyright (C) 2021 Reflexer Labs, INC + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity 0.6.7; + +import "../interfaces/CTokenLike.sol"; +import "../interfaces/SaviourCRatioSetterLike.sol"; +import "../interfaces/SafeSaviourLike.sol"; +import "../math/SafeMath.sol"; + +contract CompoundSystemCoinSafeSaviour is SafeMath, SafeSaviourLike { + // --- Auth --- + mapping (address => uint256) public authorizedAccounts; + /** + * @notice Add auth to an account + * @param account Account to add auth to + */ + function addAuthorization(address account) external isAuthorized { + authorizedAccounts[account] = 1; + emit AddAuthorization(account); + } + /** + * @notice Remove auth from an account + * @param account Account to remove auth from + */ + function removeAuthorization(address account) external isAuthorized { + authorizedAccounts[account] = 0; + emit RemoveAuthorization(account); + } + /** + * @notice Checks whether msg.sender can call an authed function + **/ + modifier isAuthorized { + require(authorizedAccounts[msg.sender] == 1, "CompoundSystemCoinSafeSaviour/account-not-authorized"); + _; + } + + mapping (address => uint256) public allowedUsers; + /** + * @notice Allow a user to deposit assets + * @param usr User to whitelist + */ + function allowUser(address usr) external isAuthorized { + allowedUsers[usr] = 1; + emit AllowUser(usr); + } + /** + * @notice Disallow a user from depositing assets + * @param usr User to disallow + */ + function disallowUser(address usr) external isAuthorized { + allowedUsers[usr] = 0; + emit DisallowUser(usr); + } + /** + * @notice Checks whether an address is an allowed user + **/ + modifier isAllowed { + require( + either(restrictUsage == 0, both(restrictUsage == 1, allowedUsers[msg.sender] == 1)), + "CompoundSystemCoinSafeSaviour/account-not-allowed" + ); + _; + } + + // --- Variables --- + // Flag that tells whether usage of the contract is restricted to allowed users + uint256 public restrictUsage; + + // Amount of cTokens currently protecting each position + mapping(bytes32 => mapping(address => uint256)) public cTokenCover; + // The cToken address + CTokenLike public cToken; + // The ERC20 system coin + ERC20Like public systemCoin; + // The system coin join contract + CoinJoinLike public coinJoin; + // Oracle providing the system coin price feed + PriceFeedLike public systemCoinOrcl; + // Contract that defines desired CRatios for each Safe after it is saved + SaviourCRatioSetterLike public cRatioSetter; + + // --- Events --- + event AddAuthorization(address account); + event RemoveAuthorization(address account); + event AllowUser(address usr); + event DisallowUser(address usr); + event ModifyParameters(bytes32 indexed parameter, uint256 val); + event ModifyParameters(bytes32 indexed parameter, address data); + event Deposit( + address indexed caller, + bytes32 collateralType, + address indexed safeHandler, + uint256 systemCoinAmount, + uint256 cTokenAmount + ); + event Withdraw( + address indexed caller, + bytes32 collateralType, + address indexed safeHandler, + address dst, + uint256 systemCoinAmount, + uint256 cTokenAmount + ); + + constructor( + address coinJoin_, + address cRatioSetter_, + address systemCoinOrcl_, + address liquidationEngine_, + address taxCollector_, + address oracleRelayer_, + address safeManager_, + address saviourRegistry_, + address cToken_, + uint256 keeperPayout_, + uint256 minKeeperPayoutValue_ + ) public { + require(coinJoin_ != address(0), "CompoundSystemCoinSafeSaviour/null-coin-join"); + require(cRatioSetter_ != address(0), "CompoundSystemCoinSafeSaviour/null-cratio-setter"); + require(systemCoinOrcl_ != address(0), "CompoundSystemCoinSafeSaviour/null-system-coin-oracle"); + require(oracleRelayer_ != address(0), "CompoundSystemCoinSafeSaviour/null-oracle-relayer"); + require(taxCollector_ != address(0), "CompoundSystemCoinSafeSaviour/null-tax-collector"); + require(liquidationEngine_ != address(0), "CompoundSystemCoinSafeSaviour/null-liquidation-engine"); + require(safeManager_ != address(0), "CompoundSystemCoinSafeSaviour/null-safe-manager"); + require(saviourRegistry_ != address(0), "CompoundSystemCoinSafeSaviour/null-saviour-registry"); + require(cToken_ != address(0), "CompoundSystemCoinSafeSaviour/null-c-token"); + require(keeperPayout_ > 0, "CompoundSystemCoinSafeSaviour/invalid-keeper-payout"); + require(minKeeperPayoutValue_ > 0, "CompoundSystemCoinSafeSaviour/invalid-min-payout-value"); + + authorizedAccounts[msg.sender] = 1; + + keeperPayout = keeperPayout_; + minKeeperPayoutValue = minKeeperPayoutValue_; + + coinJoin = CoinJoinLike(coinJoin_); + cRatioSetter = SaviourCRatioSetterLike(cRatioSetter_); + liquidationEngine = LiquidationEngineLike(liquidationEngine_); + taxCollector = TaxCollectorLike(taxCollector_); + oracleRelayer = OracleRelayerLike(oracleRelayer_); + systemCoinOrcl = PriceFeedLike(systemCoinOrcl_); + systemCoin = ERC20Like(coinJoin.systemCoin()); + safeEngine = SAFEEngineLike(coinJoin.safeEngine()); + safeManager = GebSafeManagerLike(safeManager_); + saviourRegistry = SAFESaviourRegistryLike(saviourRegistry_); + cToken = CTokenLike(cToken_); + + systemCoinOrcl.read(); + systemCoinOrcl.getResultWithValidity(); + oracleRelayer.redemptionPrice(); + + require(cToken.isCToken(), "CompoundSystemCoinSafeSaviour/not-c-token"); + require(address(safeEngine) != address(0), "CompoundSystemCoinSafeSaviour/null-safe-engine"); + require(address(systemCoin) != address(0), "CompoundSystemCoinSafeSaviour/null-sys-coin"); + + emit AddAuthorization(msg.sender); + emit ModifyParameters("keeperPayout", keeperPayout); + emit ModifyParameters("minKeeperPayoutValue", minKeeperPayoutValue); + emit ModifyParameters("liquidationEngine", liquidationEngine_); + emit ModifyParameters("taxCollector", taxCollector_); + emit ModifyParameters("oracleRelayer", oracleRelayer_); + emit ModifyParameters("systemCoinOrcl", systemCoinOrcl_); + } + + // --- Administration --- + /** + * @notice Modify an uint256 param + * @param parameter The name of the parameter + * @param val New value for the parameter + */ + function modifyParameters(bytes32 parameter, uint256 val) external isAuthorized { + if (parameter == "keeperPayout") { + require(val > 0, "CompoundSystemCoinSafeSaviour/null-payout"); + keeperPayout = val; + } + else if (parameter == "minKeeperPayoutValue") { + require(val > 0, "CompoundSystemCoinSafeSaviour/null-min-payout"); + minKeeperPayoutValue = val; + } + else if (parameter == "restrictUsage") { + require(val <= 1, "CompoundSystemCoinSafeSaviour/invalid-restriction"); + restrictUsage = val; + } + else revert("CompoundSystemCoinSafeSaviour/modify-unrecognized-param"); + emit ModifyParameters(parameter, val); + } + /** + * @notice Modify an address param + * @param parameter The name of the parameter + * @param data New address for the parameter + */ + function modifyParameters(bytes32 parameter, address data) external isAuthorized { + require(data != address(0), "CompoundSystemCoinSafeSaviour/null-data"); + + if (parameter == "systemCoinOrcl") { + systemCoinOrcl = PriceFeedLike(data); + systemCoinOrcl.read(); + systemCoinOrcl.getResultWithValidity(); + } + else if (parameter == "oracleRelayer") { + oracleRelayer = OracleRelayerLike(data); + oracleRelayer.redemptionPrice(); + } + else if (parameter == "liquidationEngine") { + liquidationEngine = LiquidationEngineLike(data); + } + else if (parameter == "taxCollector") { + taxCollector = TaxCollectorLike(data); + } + else revert("CompoundSystemCoinSafeSaviour/modify-unrecognized-param"); + emit ModifyParameters(parameter, data); + } + + // --- Adding/Withdrawing Cover --- + /* + * @notice Deposit system coins in the contract and lend them on Compound in order to provide cover for a specific + * SAFE controlled by the SAFE Manager + * @param collateralType The collateral type used in the SAFE + * @param safeID The ID of the SAFE to protect. This ID should be registered inside GebSafeManager + * @param systemCoinAmount The amount of system coins to deposit + */ + function deposit(bytes32 collateralType, uint256 safeID, uint256 systemCoinAmount) + external isAllowed() liquidationEngineApproved(address(this)) nonReentrant { + uint256 defaultCRatio = cRatioSetter.defaultDesiredCollateralizationRatios(collateralType); + require(systemCoinAmount > 0, "CompoundSystemCoinSafeSaviour/null-system-coin-amount"); + require(defaultCRatio > 0, "CompoundSystemCoinSafeSaviour/collateral-not-set"); + + // Check that the SAFE exists inside GebSafeManager + address safeHandler = safeManager.safes(safeID); + require(safeHandler != address(0), "CompoundSystemCoinSafeSaviour/null-handler"); + + // Check that the SAFE has debt + (, uint256 safeDebt) = safeEngine.safes(collateralType, safeHandler); + require(safeDebt > 0, "CompoundSystemCoinSafeSaviour/safe-does-not-have-debt"); + + // Lend on Compound + uint256 currentCTokenBalance = cToken.balanceOf(address(this)); + systemCoin.transferFrom(msg.sender, address(this), systemCoinAmount); + systemCoin.approve(address(cToken), systemCoinAmount); + require(cToken.mint(systemCoinAmount) == 0, "CompoundSystemCoinSafeSaviour/cannot-mint-ctoken"); + + // Update the cToken balance used to cover the SAFE + cTokenCover[collateralType][safeHandler] = + add(cTokenCover[collateralType][safeHandler], sub(cToken.balanceOf(address(this)), currentCTokenBalance)); + + emit Deposit(msg.sender, collateralType, safeHandler, systemCoinAmount, sub(cToken.balanceOf(address(this)), currentCTokenBalance)); + } + + /* + * @notice Withdraw system coins from the contract by exiting your Compound lending position + * @dev Only an address that controls the SAFE inside GebSafeManager can call this + * @param safeID The ID of the SAFE to remove cover from. This ID should be registered inside GebSafeManager + * @param cTokenAmount The amount of cTokens to use and redeem system coins from Compound + * @param dst The address that will receive the withdrawn system coins + */ + function withdraw(bytes32 collateralType, uint256 safeID, uint256 cTokenAmount, address dst) + external controlsSAFE(msg.sender, safeID) nonReentrant { + require(cTokenAmount > 0, "CompoundSystemCoinSafeSaviour/null-cToken-amount"); + + // Fetch the handler from the SAFE manager + address safeHandler = safeManager.safes(safeID); + require(cTokenCover[collateralType][safeHandler] >= cTokenAmount, "CompoundSystemCoinSafeSaviour/not-enough-to-redeem"); + + // Redeem system coins from Compound and transfer them to the caller + uint256 currentSystemCoinAmount = systemCoin.balanceOf(address(this)); + cTokenCover[collateralType][safeHandler] = sub(cTokenCover[collateralType][safeHandler], cTokenAmount); + require(cToken.redeem(cTokenAmount) == 0, "CompoundSystemCoinSafeSaviour/cannot-redeem-ctoken"); + + uint256 amountTransferred = sub(systemCoin.balanceOf(address(this)), currentSystemCoinAmount); + systemCoin.transfer(dst, amountTransferred); + + emit Withdraw( + msg.sender, + collateralType, + safeHandler, + dst, + amountTransferred, + cTokenAmount + ); + } + + // --- Saving Logic --- + /* + * @notice Saves a SAFE by repaying some of its debt (using cTokens) + * @dev Only the LiquidationEngine can call this + * @param keeper The keeper that called LiquidationEngine.liquidateSAFE and that should be rewarded for spending gas to save a SAFE + * @param collateralType The collateral type backing the SAFE that's being liquidated + * @param safeHandler The handler of the SAFE that's being liquidated + * @return Whether the SAFE has been saved, the amount of debt repaid as well as the amount of + * system coins sent to the keeper as their payment + */ + function saveSAFE(address keeper, bytes32 collateralType, address safeHandler) override external returns (bool, uint256, uint256) { + require(address(liquidationEngine) == msg.sender, "CompoundSystemCoinSafeSaviour/caller-not-liquidation-engine"); + require(keeper != address(0), "CompoundSystemCoinSafeSaviour/null-keeper-address"); + + if (both(both(collateralType == "", safeHandler == address(0)), keeper == address(liquidationEngine))) { + return (true, uint(-1), uint(-1)); + } + + // Check that the fiat value of the keeper payout is high enough + require(keeperPayoutExceedsMinValue(), "CompoundSystemCoinSafeSaviour/small-keeper-payout-value"); + + // Tax the collateral + taxCollector.taxSingle(collateralType); + + // Compute and check the validity of the amount of cTokens used to save the SAFE + uint256 tokenAmountUsed = tokenAmountUsedToSave(collateralType, safeHandler); + require(both(tokenAmountUsed != MAX_UINT, tokenAmountUsed != 0), "CompoundSystemCoinSafeSaviour/invalid-tokens-used-to-save"); + + // Check that there are enough cTokens added to cover both the keeper's payout and the amount used to save the SAFE + uint256 keeperCTokenPayout = div(mul(keeperPayout, WAD), cToken.exchangeRateStored()); + require(cTokenCover[collateralType][safeHandler] >= add(keeperCTokenPayout, tokenAmountUsed), "CompoundSystemCoinSafeSaviour/not-enough-cover-deposited"); + + // Update the remaining cover + cTokenCover[collateralType][safeHandler] = sub(cTokenCover[collateralType][safeHandler], add(keeperCTokenPayout, tokenAmountUsed)); + + // Mark the SAFE in the registry as just having been saved + saviourRegistry.markSave(collateralType, safeHandler); + + // Get system coins back from Compound + uint256 currentSystemCoinAmount = systemCoin.balanceOf(address(this)); + require(cToken.redeem(add(keeperCTokenPayout, tokenAmountUsed)) == 0, "CompoundSystemCoinSafeSaviour/cannot-redeem-ctoken"); + uint256 systemCoinsToRepay = sub(sub(systemCoin.balanceOf(address(this)), currentSystemCoinAmount), keeperPayout); + + // Approve the coin join contract to take system coins and repay debt + systemCoin.approve(address(coinJoin), 0); + systemCoin.approve(address(coinJoin), systemCoinsToRepay); + + // Join system coins in the system and repay the SAFE's debt + { + coinJoin.join(address(this), systemCoinsToRepay); + uint256 nonAdjustedSystemCoinsToRepay = div(mul(systemCoinsToRepay, RAY), getAccumulatedRate(collateralType)); + + safeEngine.modifySAFECollateralization( + collateralType, + safeHandler, + address(0), + address(this), + int256(0), + -int256(nonAdjustedSystemCoinsToRepay) + ); + } + + // Send the fee to the keeper + systemCoin.transfer(keeper, keeperPayout); + + // Emit an event + emit SaveSAFE(keeper, collateralType, safeHandler, tokenAmountUsed); + + return (true, tokenAmountUsed, keeperPayout); + } + + // --- Getters --- + /* + * @notice Compute whether the value of keeperPayout system coins is higher than or equal to minKeeperPayoutValue + * @dev Used to determine whether it's worth it for the keeper to save the SAFE in exchange for keeperPayout system coins + * @return A bool representing whether the value of keeperPayout system coins is >= minKeeperPayoutValue + */ + function keeperPayoutExceedsMinValue() override public returns (bool) { + (uint256 priceFeedValue, bool hasValidValue) = systemCoinOrcl.getResultWithValidity(); + + if (either(!hasValidValue, priceFeedValue == 0)) { + return false; + } + + return (minKeeperPayoutValue <= mul(keeperPayout, priceFeedValue) / WAD); + } + /* + * @notice Return the current value of the keeper payout + */ + function getKeeperPayoutValue() override public returns (uint256) { + (uint256 priceFeedValue, bool hasValidValue) = systemCoinOrcl.getResultWithValidity(); + + if (either(!hasValidValue, priceFeedValue == 0)) { + return 0; + } + + return mul(keeperPayout, priceFeedValue) / WAD; + } + /* + * @notice Determine whether a SAFE can be saved with the current amount of cTokens deposited as cover for it + * @param safeHandler The handler of the SAFE which the function takes into account + * @return Whether the SAFE can be saved or not + */ + function canSave(bytes32 collateralType, address safeHandler) override external returns (bool) { + uint256 tokenAmountUsed = tokenAmountUsedToSave(collateralType, safeHandler); + + if (either(tokenAmountUsed == MAX_UINT, tokenAmountUsed == 0)) { + return false; + } + + uint256 keeperCTokenPayout = div(mul(keeperPayout, WAD), cToken.exchangeRateStored()); + return (cTokenCover[collateralType][safeHandler] >= add(tokenAmountUsed, keeperCTokenPayout)); + } + /* + * @notice Calculate the amount of cTokens used to save a SAFE and bring its CRatio to the desired level + * @param collateralType The SAFE collateral type + * @param safeHandler The handler of the SAFE which the function takes into account + * @return The amount of cTokens used to save the SAFE and bring its CRatio to the desired level + */ + function tokenAmountUsedToSave(bytes32 collateralType, address safeHandler) override public returns (uint256) { + if (cTokenCover[collateralType][safeHandler] == 0) return 0; + + (uint256 depositedCollateralToken, uint256 safeDebt) = safeEngine.safes(collateralType, safeHandler); + (address ethFSM,,) = oracleRelayer.collateralTypes(collateralType); + if (ethFSM == address(0)) return MAX_UINT; + + (uint256 priceFeedValue, bool hasValidValue) = PriceFeedLike(ethFSM).getResultWithValidity(); + + // If the SAFE doesn't have debt, if the price feed is faulty or if the default desired CRatio is null, abort + uint256 defaultCRatio = cRatioSetter.defaultDesiredCollateralizationRatios(collateralType); + if (either(either(safeDebt == 0, either(priceFeedValue == 0, !hasValidValue)), defaultCRatio == 0)) { + return MAX_UINT; + } + + // Calculate the amount of debt that needs to be repaid so the SAFE gets to the target CRatio + uint256 targetCRatio = (cRatioSetter.desiredCollateralizationRatios(collateralType, safeHandler) == 0) ? + defaultCRatio : cRatioSetter.desiredCollateralizationRatios(collateralType, safeHandler); + + uint256 targetDebtAmount = mul( + mul(HUNDRED, mul(depositedCollateralToken, priceFeedValue) / WAD) / targetCRatio, RAY + ) / oracleRelayer.redemptionPrice(); + + // If you need to repay more than the amount of debt in the SAFE (or all the debt), return 0 + if (either(targetDebtAmount >= safeDebt, debtBelowFloor(collateralType, targetDebtAmount))) { + return 0; + } else { + safeDebt = mul(safeDebt, getAccumulatedRate(collateralType)) / RAY; + return div(mul(sub(safeDebt, targetDebtAmount), WAD), cToken.exchangeRateCurrent()); + } + } + /* + * @notify Returns whether a target debt amount is below the debt floor of a specific collateral type + * @param collateralType The collateral type whose floor we compare against + * @param targetDebtAmount The target debt amount for a SAFE that has collateralType collateral in it + */ + function debtBelowFloor(bytes32 collateralType, uint256 targetDebtAmount) public view returns (bool) { + (, , , , uint256 debtFloor, ) = safeEngine.collateralTypes(collateralType); + return (mul(targetDebtAmount, RAY) < debtFloor); + } + /* + * @notify Get the accumulated interest rate for a specific collateral type + * @param The collateral type for which to retrieve the rate + */ + function getAccumulatedRate(bytes32 collateralType) + public view returns (uint256 accumulatedRate) { + (, accumulatedRate, , , , ) = safeEngine.collateralTypes(collateralType); + } +} diff --git a/src/saviours/GeneralTokenReserveSafeSaviour.sol b/src/saviours/GeneralTokenReserveSafeSaviour.sol index 8fd3352..2ca4bff 100644 --- a/src/saviours/GeneralTokenReserveSafeSaviour.sol +++ b/src/saviours/GeneralTokenReserveSafeSaviour.sol @@ -1,400 +1,400 @@ -// Copyright (C) 2020 Reflexer Labs, INC - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -pragma solidity ^0.6.7; - -import "../interfaces/SaviourCRatioSetterLike.sol"; -import "../interfaces/SafeSaviourLike.sol"; -import "../math/SafeMath.sol"; - -contract GeneralTokenReserveSafeSaviour is SafeMath, SafeSaviourLike { - // --- Auth --- - mapping (address => uint256) public authorizedAccounts; - /** - * @notice Add auth to an account - * @param account Account to add auth to - */ - function addAuthorization(address account) external isAuthorized { - authorizedAccounts[account] = 1; - emit AddAuthorization(account); - } - /** - * @notice Remove auth from an account - * @param account Account to remove auth from - */ - function removeAuthorization(address account) external isAuthorized { - authorizedAccounts[account] = 0; - emit RemoveAuthorization(account); - } - /** - * @notice Checks whether msg.sender can call an authed function - **/ - modifier isAuthorized { - require(authorizedAccounts[msg.sender] == 1, "GeneralTokenReserveSafeSaviour/account-not-authorized"); - _; - } - - mapping (address => uint256) public allowedUsers; - /** - * @notice Allow a user to deposit assets - * @param usr User to whitelist - */ - function allowUser(address usr) external isAuthorized { - allowedUsers[usr] = 1; - emit AllowUser(usr); - } - /** - * @notice Disallow a user from depositing assets - * @param usr User to disallow - */ - function disallowUser(address usr) external isAuthorized { - allowedUsers[usr] = 0; - emit DisallowUser(usr); - } - /** - * @notice Checks whether an address is an allowed user - **/ - modifier isAllowed { - require( - either(restrictUsage == 0, both(restrictUsage == 1, allowedUsers[msg.sender] == 1)), - "GeneralTokenReserveSafeSaviour/account-not-allowed" - ); - _; - } - - // --- Variables --- - // Flag that tells whether usage of the contract is restricted to allowed users - uint256 public restrictUsage; - - // Amount of collateral deposited to cover each SAFE - mapping(address => uint256) public collateralTokenCover; - // The collateral join contract for adding collateral in the system - CollateralJoinLike public collateralJoin; - // The collateral token - ERC20Like public collateralToken; - // Contract that defines desired CRatios for each Safe after it is saved - SaviourCRatioSetterLike public cRatioSetter; - - // --- Events --- - event AddAuthorization(address account); - event RemoveAuthorization(address account); - event AllowUser(address usr); - event DisallowUser(address usr); - event ModifyParameters(bytes32 indexed parameter, address data); - event ModifyParameters(bytes32 indexed parameter, uint256 data); - event Deposit(address indexed caller, address indexed safeHandler, uint256 amount); - event Withdraw(address indexed caller, address indexed safeHandler, address dst, uint256 amount); - - constructor( - address cRatioSetter_, - address collateralJoin_, - address liquidationEngine_, - address taxCollector_, - address oracleRelayer_, - address safeManager_, - address saviourRegistry_, - uint256 keeperPayout_, - uint256 minKeeperPayoutValue_, - uint256 payoutToSAFESize_ - ) public { - require(cRatioSetter_ != address(0), "GeneralTokenReserveSafeSaviour/null-cratio-setter"); - require(collateralJoin_ != address(0), "GeneralTokenReserveSafeSaviour/null-collateral-join"); - require(liquidationEngine_ != address(0), "GeneralTokenReserveSafeSaviour/null-liquidation-engine"); - require(taxCollector_ != address(0), "GeneralTokenReserveSafeSaviour/null-tax-collector"); - require(oracleRelayer_ != address(0), "GeneralTokenReserveSafeSaviour/null-oracle-relayer"); - require(safeManager_ != address(0), "GeneralTokenReserveSafeSaviour/null-safe-manager"); - require(saviourRegistry_ != address(0), "GeneralTokenReserveSafeSaviour/null-saviour-registry"); - require(keeperPayout_ > 0, "GeneralTokenReserveSafeSaviour/invalid-keeper-payout"); - require(payoutToSAFESize_ > 1, "GeneralTokenReserveSafeSaviour/invalid-payout-to-safe-size"); - require(minKeeperPayoutValue_ > 0, "GeneralTokenReserveSafeSaviour/invalid-min-payout-value"); - - authorizedAccounts[msg.sender] = 1; - - keeperPayout = keeperPayout_; - payoutToSAFESize = payoutToSAFESize_; - minKeeperPayoutValue = minKeeperPayoutValue_; - - cRatioSetter = SaviourCRatioSetterLike(cRatioSetter_); - liquidationEngine = LiquidationEngineLike(liquidationEngine_); - taxCollector = TaxCollectorLike(taxCollector_); - collateralJoin = CollateralJoinLike(collateralJoin_); - oracleRelayer = OracleRelayerLike(oracleRelayer_); - safeEngine = SAFEEngineLike(collateralJoin.safeEngine()); - safeManager = GebSafeManagerLike(safeManager_); - saviourRegistry = SAFESaviourRegistryLike(saviourRegistry_); - collateralToken = ERC20Like(collateralJoin.collateral()); - - require(address(safeEngine) != address(0), "GeneralTokenReserveSafeSaviour/null-safe-engine"); - require(collateralJoin.decimals() == 18, "GeneralTokenReserveSafeSaviour/invalid-join-decimals"); - require(collateralJoin.contractEnabled() == 1, "GeneralTokenReserveSafeSaviour/join-disabled"); - - emit AddAuthorization(msg.sender); - emit ModifyParameters("keeperPayout", keeperPayout); - emit ModifyParameters("minKeeperPayoutValue", minKeeperPayoutValue); - emit ModifyParameters("cRatioSetter", cRatioSetter_); - emit ModifyParameters("taxCollector", taxCollector_); - emit ModifyParameters("liquidationEngine", liquidationEngine_); - emit ModifyParameters("oracleRelayer", oracleRelayer_); - } - - // --- Administration --- - /** - * @notice Modify an uint256 param - * @param parameter The name of the parameter - * @param val New value for the parameter - */ - function modifyParameters(bytes32 parameter, uint256 val) external isAuthorized { - if (parameter == "keeperPayout") { - require(val > 0, "GeneralTokenReserveSafeSaviour/null-payout"); - keeperPayout = val; - } - else if (parameter == "minKeeperPayoutValue") { - require(val > 0, "GeneralTokenReserveSafeSaviour/null-min-payout"); - minKeeperPayoutValue = val; - } - else if (parameter == "restrictUsage") { - require(val <= 1, "GeneralTokenReserveSafeSaviour/invalid-restriction"); - restrictUsage = val; - } - else revert("GeneralTokenReserveSafeSaviour/modify-unrecognized-param"); - emit ModifyParameters(parameter, val); - } - /** - * @notice Modify an address param - * @param parameter The name of the parameter - * @param data New address for the parameter - */ - function modifyParameters(bytes32 parameter, address data) external isAuthorized { - require(data != address(0), "GeneralTokenReserveSafeSaviour/null-data"); - - if (parameter == "oracleRelayer") { - oracleRelayer = OracleRelayerLike(data); - oracleRelayer.redemptionPrice(); - } - else if (parameter == "liquidationEngine") { - liquidationEngine = LiquidationEngineLike(data); - } - else if (parameter == "taxCollector") { - taxCollector = TaxCollectorLike(data); - } - else revert("GeneralTokenReserveSafeSaviour/modify-unrecognized-param"); - emit ModifyParameters(parameter, data); - } - - // --- Adding/Withdrawing Cover --- - /* - * @notice Deposit collateralToken in the contract in order to provide cover for a specific SAFE controlled by the SAFE Manager - * @param safeID The ID of the SAFE to protect. This ID should be registered inside GebSafeManager - * @param collateralTokenAmount The amount of collateralToken to deposit - */ - function deposit(uint256 safeID, uint256 collateralTokenAmount) external isAllowed() liquidationEngineApproved(address(this)) nonReentrant { - require(collateralTokenAmount > 0, "GeneralTokenReserveSafeSaviour/null-collateralToken-amount"); - - // Check that the SAFE exists inside GebSafeManager - address safeHandler = safeManager.safes(safeID); - require(safeHandler != address(0), "GeneralTokenReserveSafeSaviour/null-handler"); - - // Check that the SAFE has debt - (, uint256 safeDebt) = - SAFEEngineLike(collateralJoin.safeEngine()).safes(collateralJoin.collateralType(), safeHandler); - require(safeDebt > 0, "GeneralTokenReserveSafeSaviour/safe-does-not-have-debt"); - - // Update the collateralToken balance used to cover the SAFE and transfer collateralToken to this contract - collateralTokenCover[safeHandler] = add(collateralTokenCover[safeHandler], collateralTokenAmount); - require(collateralToken.transferFrom(msg.sender, address(this), collateralTokenAmount), "GeneralTokenReserveSafeSaviour/could-not-transfer-collateralToken"); - - emit Deposit(msg.sender, safeHandler, collateralTokenAmount); - } - /* - * @notice Withdraw collateralToken from the contract and provide less cover for a SAFE - * @dev Only an address that controls the SAFE inside GebSafeManager can call this - * @param safeID The ID of the SAFE to remove cover from. This ID should be registered inside GebSafeManager - * @param collateralTokenAmount The amount of collateralToken to withdraw - * @param dst The address that will receive the withdrawn tokens - */ - function withdraw(uint256 safeID, uint256 collateralTokenAmount, address dst) external controlsSAFE(msg.sender, safeID) nonReentrant { - require(collateralTokenAmount > 0, "GeneralTokenReserveSafeSaviour/null-collateralToken-amount"); - - // Fetch the handler from the SAFE manager - address safeHandler = safeManager.safes(safeID); - require(collateralTokenCover[safeHandler] >= collateralTokenAmount, "GeneralTokenReserveSafeSaviour/not-enough-to-withdraw"); - - // Withdraw cover and transfer collateralToken to the caller - collateralTokenCover[safeHandler] = sub(collateralTokenCover[safeHandler], collateralTokenAmount); - collateralToken.transfer(dst, collateralTokenAmount); - - emit Withdraw(msg.sender, safeHandler, dst, collateralTokenAmount); - } - - // --- Saving Logic --- - /* - * @notice Saves a SAFE by adding more collateralToken into it - * @dev Only the LiquidationEngine can call this - * @param keeper The keeper that called LiquidationEngine.liquidateSAFE and that should be rewarded for spending gas to save a SAFE - * @param collateralType The collateral type backing the SAFE that's being liquidated - * @param safeHandler The handler of the SAFE that's being liquidated - * @return Whether the SAFE has been saved, the amount of collateralToken added in the SAFE as well as the amount of - * collateralToken sent to the keeper as their payment - */ - function saveSAFE(address keeper, bytes32 collateralType, address safeHandler) override external returns (bool, uint256, uint256) { - require(address(liquidationEngine) == msg.sender, "GeneralTokenReserveSafeSaviour/caller-not-liquidation-engine"); - require(keeper != address(0), "GeneralTokenReserveSafeSaviour/null-keeper-address"); - - if (both(both(collateralType == "", safeHandler == address(0)), keeper == address(liquidationEngine))) { - return (true, uint(-1), uint(-1)); - } - - require(collateralType == collateralJoin.collateralType(), "GeneralTokenReserveSafeSaviour/invalid-collateral-type"); - - // Check that the fiat value of the keeper payout is high enough - require(keeperPayoutExceedsMinValue(), "GeneralTokenReserveSafeSaviour/small-keeper-payout-value"); - - // Tax the collateral type - taxCollector.taxSingle(collateralType); - - // Check that the amount of collateral locked in the safe is bigger than the keeper's payout - (uint256 safeLockedCollateral,) = - SAFEEngineLike(collateralJoin.safeEngine()).safes(collateralJoin.collateralType(), safeHandler); - require(safeLockedCollateral >= mul(keeperPayout, payoutToSAFESize), "GeneralTokenReserveSafeSaviour/tiny-safe"); - - // Compute and check the validity of the amount of collateralToken used to save the SAFE - uint256 tokenAmountUsed = tokenAmountUsedToSave(collateralJoin.collateralType(), safeHandler); - require(both(tokenAmountUsed != MAX_UINT, tokenAmountUsed != 0), "GeneralTokenReserveSafeSaviour/invalid-tokens-used-to-save"); - - // Check that there's enough collateralToken added as to cover both the keeper's payout and the amount used to save the SAFE - require(collateralTokenCover[safeHandler] >= add(keeperPayout, tokenAmountUsed), "GeneralTokenReserveSafeSaviour/not-enough-cover-deposited"); - - // Update the remaining cover - collateralTokenCover[safeHandler] = sub(collateralTokenCover[safeHandler], add(keeperPayout, tokenAmountUsed)); - - // Mark the SAFE in the registry as just being saved - saviourRegistry.markSave(collateralType, safeHandler); - - // Approve collateralToken to the collateral join contract - collateralToken.approve(address(collateralJoin), 0); - collateralToken.approve(address(collateralJoin), tokenAmountUsed); - - // Join collateralToken in the system and add it in the saved SAFE - collateralJoin.join(address(this), tokenAmountUsed); - safeEngine.modifySAFECollateralization( - collateralJoin.collateralType(), - safeHandler, - address(this), - address(0), - int256(tokenAmountUsed), - int256(0) - ); - - // Send the fee to the keeper - collateralToken.transfer(keeper, keeperPayout); - - // Emit an event - emit SaveSAFE(keeper, collateralType, safeHandler, tokenAmountUsed); - - return (true, tokenAmountUsed, keeperPayout); - } - - // --- Getters --- - /* - * @notice Compute whether the value of keeperPayout collateralToken is higher than or equal to minKeeperPayoutValue - * @dev Used to determine whether it's worth it for the keeper to save the SAFE in exchange for keeperPayout collateralToken - * @return A bool representing whether the value of keeperPayout collateralToken is >= minKeeperPayoutValue - */ - function keeperPayoutExceedsMinValue() override public returns (bool) { - (address ethFSM,,) = oracleRelayer.collateralTypes(collateralJoin.collateralType()); - (uint256 priceFeedValue, bool hasValidValue) = PriceFeedLike(PriceFeedLike(ethFSM).priceSource()).getResultWithValidity(); - - if (either(!hasValidValue, priceFeedValue == 0)) { - return false; - } - - return (minKeeperPayoutValue <= mul(keeperPayout, priceFeedValue) / WAD); - } - /* - * @notice Return the current value of the keeper payout - */ - function getKeeperPayoutValue() override public returns (uint256) { - (address ethFSM,,) = oracleRelayer.collateralTypes(collateralJoin.collateralType()); - (uint256 priceFeedValue, bool hasValidValue) = PriceFeedLike(PriceFeedLike(ethFSM).priceSource()).getResultWithValidity(); - - if (either(!hasValidValue, priceFeedValue == 0)) { - return 0; - } - - return mul(keeperPayout, priceFeedValue) / WAD; - } - /* - * @notice Determine whether a SAFE can be saved with the current amount of collateralToken deposited as cover for it - * @param collateralType The SAFE collateral type (ignored in this implementation) - * @param safeHandler The handler of the SAFE which the function takes into account - * @return Whether the SAFE can be saved or not - */ - function canSave(bytes32, address safeHandler) override external returns (bool) { - uint256 tokenAmountUsed = tokenAmountUsedToSave(collateralJoin.collateralType(), safeHandler); - - if (either(tokenAmountUsed == MAX_UINT, tokenAmountUsed == 0)) { - return false; - } - - return (collateralTokenCover[safeHandler] >= add(tokenAmountUsed, keeperPayout)); - } - /* - * @notice Calculate the amount of collateralToken used to save a SAFE and bring its CRatio to the desired level - * @param collateralType The SAFE collateral type (ignored in this implementation) - * @param safeHandler The handler of the SAFE which the function takes into account - * @return The amount of collateralToken used to save the SAFE and bring its CRatio to the desired level - */ - function tokenAmountUsedToSave(bytes32, address safeHandler) override public returns (uint256 tokenAmountUsed) { - (uint256 depositedCollateralToken, uint256 safeDebt) = - SAFEEngineLike(collateralJoin.safeEngine()).safes(collateralJoin.collateralType(), safeHandler); - (address ethFSM,,) = oracleRelayer.collateralTypes(collateralJoin.collateralType()); - if (ethFSM == address(0)) return MAX_UINT; - (uint256 priceFeedValue, bool hasValidValue) = PriceFeedLike(ethFSM).getResultWithValidity(); - - // If the SAFE doesn't have debt, if the price feed is faulty or if the default desired CRatio is null, abort - uint256 defaultCRatio = cRatioSetter.defaultDesiredCollateralizationRatios(collateralJoin.collateralType()); - if (either(either(safeDebt == 0, either(priceFeedValue == 0, !hasValidValue)), defaultCRatio == 0)) { - tokenAmountUsed = MAX_UINT; - return MAX_UINT; - } - - // Calculate the value of the debt equivalent to the value of the collateralToken that would need to be in the SAFE after it's saved - uint256 targetCRatio = (cRatioSetter.desiredCollateralizationRatios(collateralJoin.collateralType(), safeHandler) == 0) ? - defaultCRatio : cRatioSetter.desiredCollateralizationRatios(collateralJoin.collateralType(), safeHandler); - uint256 scaledDownDebtValue = mul( - mul(oracleRelayer.redemptionPrice(), safeDebt) / RAY, getAccumulatedRate(collateralJoin.collateralType()) - ) / RAY; - scaledDownDebtValue = mul(add(scaledDownDebtValue, ONE), targetCRatio) / HUNDRED; - - // Compute the amount of collateralToken the SAFE needs to get to the desired CRatio - uint256 collateralTokenAmountNeeded = mul(scaledDownDebtValue, WAD) / priceFeedValue; - - // If the amount of collateralToken needed is lower than the amount that's currently in the SAFE, return 0 - if (collateralTokenAmountNeeded <= depositedCollateralToken) { - return 0; - } else { - // Otherwise return the delta - return sub(collateralTokenAmountNeeded, depositedCollateralToken); - } - } - /* - * @notify Get the accumulated interest rate for a specific collateral type - * @param The collateral type for which to retrieve the rate - */ - function getAccumulatedRate(bytes32 collateralType) - public view returns (uint256 accumulatedRate) { - (, accumulatedRate, , , , ) = safeEngine.collateralTypes(collateralType); - } -} +// Copyright (C) 2020 Reflexer Labs, INC + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity ^0.6.7; + +import "../interfaces/SaviourCRatioSetterLike.sol"; +import "../interfaces/SafeSaviourLike.sol"; +import "../math/SafeMath.sol"; + +contract GeneralTokenReserveSafeSaviour is SafeMath, SafeSaviourLike { + // --- Auth --- + mapping (address => uint256) public authorizedAccounts; + /** + * @notice Add auth to an account + * @param account Account to add auth to + */ + function addAuthorization(address account) external isAuthorized { + authorizedAccounts[account] = 1; + emit AddAuthorization(account); + } + /** + * @notice Remove auth from an account + * @param account Account to remove auth from + */ + function removeAuthorization(address account) external isAuthorized { + authorizedAccounts[account] = 0; + emit RemoveAuthorization(account); + } + /** + * @notice Checks whether msg.sender can call an authed function + **/ + modifier isAuthorized { + require(authorizedAccounts[msg.sender] == 1, "GeneralTokenReserveSafeSaviour/account-not-authorized"); + _; + } + + mapping (address => uint256) public allowedUsers; + /** + * @notice Allow a user to deposit assets + * @param usr User to whitelist + */ + function allowUser(address usr) external isAuthorized { + allowedUsers[usr] = 1; + emit AllowUser(usr); + } + /** + * @notice Disallow a user from depositing assets + * @param usr User to disallow + */ + function disallowUser(address usr) external isAuthorized { + allowedUsers[usr] = 0; + emit DisallowUser(usr); + } + /** + * @notice Checks whether an address is an allowed user + **/ + modifier isAllowed { + require( + either(restrictUsage == 0, both(restrictUsage == 1, allowedUsers[msg.sender] == 1)), + "GeneralTokenReserveSafeSaviour/account-not-allowed" + ); + _; + } + + // --- Variables --- + // Flag that tells whether usage of the contract is restricted to allowed users + uint256 public restrictUsage; + + // Amount of collateral deposited to cover each SAFE + mapping(address => uint256) public collateralTokenCover; + // The collateral join contract for adding collateral in the system + CollateralJoinLike public collateralJoin; + // The collateral token + ERC20Like public collateralToken; + // Contract that defines desired CRatios for each Safe after it is saved + SaviourCRatioSetterLike public cRatioSetter; + + // --- Events --- + event AddAuthorization(address account); + event RemoveAuthorization(address account); + event AllowUser(address usr); + event DisallowUser(address usr); + event ModifyParameters(bytes32 indexed parameter, address data); + event ModifyParameters(bytes32 indexed parameter, uint256 data); + event Deposit(address indexed caller, address indexed safeHandler, uint256 amount); + event Withdraw(address indexed caller, address indexed safeHandler, address dst, uint256 amount); + + constructor( + address cRatioSetter_, + address collateralJoin_, + address liquidationEngine_, + address taxCollector_, + address oracleRelayer_, + address safeManager_, + address saviourRegistry_, + uint256 keeperPayout_, + uint256 minKeeperPayoutValue_, + uint256 payoutToSAFESize_ + ) public { + require(cRatioSetter_ != address(0), "GeneralTokenReserveSafeSaviour/null-cratio-setter"); + require(collateralJoin_ != address(0), "GeneralTokenReserveSafeSaviour/null-collateral-join"); + require(liquidationEngine_ != address(0), "GeneralTokenReserveSafeSaviour/null-liquidation-engine"); + require(taxCollector_ != address(0), "GeneralTokenReserveSafeSaviour/null-tax-collector"); + require(oracleRelayer_ != address(0), "GeneralTokenReserveSafeSaviour/null-oracle-relayer"); + require(safeManager_ != address(0), "GeneralTokenReserveSafeSaviour/null-safe-manager"); + require(saviourRegistry_ != address(0), "GeneralTokenReserveSafeSaviour/null-saviour-registry"); + require(keeperPayout_ > 0, "GeneralTokenReserveSafeSaviour/invalid-keeper-payout"); + require(payoutToSAFESize_ > 1, "GeneralTokenReserveSafeSaviour/invalid-payout-to-safe-size"); + require(minKeeperPayoutValue_ > 0, "GeneralTokenReserveSafeSaviour/invalid-min-payout-value"); + + authorizedAccounts[msg.sender] = 1; + + keeperPayout = keeperPayout_; + payoutToSAFESize = payoutToSAFESize_; + minKeeperPayoutValue = minKeeperPayoutValue_; + + cRatioSetter = SaviourCRatioSetterLike(cRatioSetter_); + liquidationEngine = LiquidationEngineLike(liquidationEngine_); + taxCollector = TaxCollectorLike(taxCollector_); + collateralJoin = CollateralJoinLike(collateralJoin_); + oracleRelayer = OracleRelayerLike(oracleRelayer_); + safeEngine = SAFEEngineLike(collateralJoin.safeEngine()); + safeManager = GebSafeManagerLike(safeManager_); + saviourRegistry = SAFESaviourRegistryLike(saviourRegistry_); + collateralToken = ERC20Like(collateralJoin.collateral()); + + require(address(safeEngine) != address(0), "GeneralTokenReserveSafeSaviour/null-safe-engine"); + require(collateralJoin.decimals() == 18, "GeneralTokenReserveSafeSaviour/invalid-join-decimals"); + require(collateralJoin.contractEnabled() == 1, "GeneralTokenReserveSafeSaviour/join-disabled"); + + emit AddAuthorization(msg.sender); + emit ModifyParameters("keeperPayout", keeperPayout); + emit ModifyParameters("minKeeperPayoutValue", minKeeperPayoutValue); + emit ModifyParameters("cRatioSetter", cRatioSetter_); + emit ModifyParameters("taxCollector", taxCollector_); + emit ModifyParameters("liquidationEngine", liquidationEngine_); + emit ModifyParameters("oracleRelayer", oracleRelayer_); + } + + // --- Administration --- + /** + * @notice Modify an uint256 param + * @param parameter The name of the parameter + * @param val New value for the parameter + */ + function modifyParameters(bytes32 parameter, uint256 val) external isAuthorized { + if (parameter == "keeperPayout") { + require(val > 0, "GeneralTokenReserveSafeSaviour/null-payout"); + keeperPayout = val; + } + else if (parameter == "minKeeperPayoutValue") { + require(val > 0, "GeneralTokenReserveSafeSaviour/null-min-payout"); + minKeeperPayoutValue = val; + } + else if (parameter == "restrictUsage") { + require(val <= 1, "GeneralTokenReserveSafeSaviour/invalid-restriction"); + restrictUsage = val; + } + else revert("GeneralTokenReserveSafeSaviour/modify-unrecognized-param"); + emit ModifyParameters(parameter, val); + } + /** + * @notice Modify an address param + * @param parameter The name of the parameter + * @param data New address for the parameter + */ + function modifyParameters(bytes32 parameter, address data) external isAuthorized { + require(data != address(0), "GeneralTokenReserveSafeSaviour/null-data"); + + if (parameter == "oracleRelayer") { + oracleRelayer = OracleRelayerLike(data); + oracleRelayer.redemptionPrice(); + } + else if (parameter == "liquidationEngine") { + liquidationEngine = LiquidationEngineLike(data); + } + else if (parameter == "taxCollector") { + taxCollector = TaxCollectorLike(data); + } + else revert("GeneralTokenReserveSafeSaviour/modify-unrecognized-param"); + emit ModifyParameters(parameter, data); + } + + // --- Adding/Withdrawing Cover --- + /* + * @notice Deposit collateralToken in the contract in order to provide cover for a specific SAFE controlled by the SAFE Manager + * @param safeID The ID of the SAFE to protect. This ID should be registered inside GebSafeManager + * @param collateralTokenAmount The amount of collateralToken to deposit + */ + function deposit(uint256 safeID, uint256 collateralTokenAmount) external isAllowed() liquidationEngineApproved(address(this)) nonReentrant { + require(collateralTokenAmount > 0, "GeneralTokenReserveSafeSaviour/null-collateralToken-amount"); + + // Check that the SAFE exists inside GebSafeManager + address safeHandler = safeManager.safes(safeID); + require(safeHandler != address(0), "GeneralTokenReserveSafeSaviour/null-handler"); + + // Check that the SAFE has debt + (, uint256 safeDebt) = + SAFEEngineLike(collateralJoin.safeEngine()).safes(collateralJoin.collateralType(), safeHandler); + require(safeDebt > 0, "GeneralTokenReserveSafeSaviour/safe-does-not-have-debt"); + + // Update the collateralToken balance used to cover the SAFE and transfer collateralToken to this contract + collateralTokenCover[safeHandler] = add(collateralTokenCover[safeHandler], collateralTokenAmount); + require(collateralToken.transferFrom(msg.sender, address(this), collateralTokenAmount), "GeneralTokenReserveSafeSaviour/could-not-transfer-collateralToken"); + + emit Deposit(msg.sender, safeHandler, collateralTokenAmount); + } + /* + * @notice Withdraw collateralToken from the contract and provide less cover for a SAFE + * @dev Only an address that controls the SAFE inside GebSafeManager can call this + * @param safeID The ID of the SAFE to remove cover from. This ID should be registered inside GebSafeManager + * @param collateralTokenAmount The amount of collateralToken to withdraw + * @param dst The address that will receive the withdrawn tokens + */ + function withdraw(uint256 safeID, uint256 collateralTokenAmount, address dst) external controlsSAFE(msg.sender, safeID) nonReentrant { + require(collateralTokenAmount > 0, "GeneralTokenReserveSafeSaviour/null-collateralToken-amount"); + + // Fetch the handler from the SAFE manager + address safeHandler = safeManager.safes(safeID); + require(collateralTokenCover[safeHandler] >= collateralTokenAmount, "GeneralTokenReserveSafeSaviour/not-enough-to-withdraw"); + + // Withdraw cover and transfer collateralToken to the caller + collateralTokenCover[safeHandler] = sub(collateralTokenCover[safeHandler], collateralTokenAmount); + collateralToken.transfer(dst, collateralTokenAmount); + + emit Withdraw(msg.sender, safeHandler, dst, collateralTokenAmount); + } + + // --- Saving Logic --- + /* + * @notice Saves a SAFE by adding more collateralToken into it + * @dev Only the LiquidationEngine can call this + * @param keeper The keeper that called LiquidationEngine.liquidateSAFE and that should be rewarded for spending gas to save a SAFE + * @param collateralType The collateral type backing the SAFE that's being liquidated + * @param safeHandler The handler of the SAFE that's being liquidated + * @return Whether the SAFE has been saved, the amount of collateralToken added in the SAFE as well as the amount of + * collateralToken sent to the keeper as their payment + */ + function saveSAFE(address keeper, bytes32 collateralType, address safeHandler) override external returns (bool, uint256, uint256) { + require(address(liquidationEngine) == msg.sender, "GeneralTokenReserveSafeSaviour/caller-not-liquidation-engine"); + require(keeper != address(0), "GeneralTokenReserveSafeSaviour/null-keeper-address"); + + if (both(both(collateralType == "", safeHandler == address(0)), keeper == address(liquidationEngine))) { + return (true, uint(-1), uint(-1)); + } + + require(collateralType == collateralJoin.collateralType(), "GeneralTokenReserveSafeSaviour/invalid-collateral-type"); + + // Check that the fiat value of the keeper payout is high enough + require(keeperPayoutExceedsMinValue(), "GeneralTokenReserveSafeSaviour/small-keeper-payout-value"); + + // Tax the collateral type + taxCollector.taxSingle(collateralType); + + // Check that the amount of collateral locked in the safe is bigger than the keeper's payout + (uint256 safeLockedCollateral,) = + SAFEEngineLike(collateralJoin.safeEngine()).safes(collateralJoin.collateralType(), safeHandler); + require(safeLockedCollateral >= mul(keeperPayout, payoutToSAFESize), "GeneralTokenReserveSafeSaviour/tiny-safe"); + + // Compute and check the validity of the amount of collateralToken used to save the SAFE + uint256 tokenAmountUsed = tokenAmountUsedToSave(collateralJoin.collateralType(), safeHandler); + require(both(tokenAmountUsed != MAX_UINT, tokenAmountUsed != 0), "GeneralTokenReserveSafeSaviour/invalid-tokens-used-to-save"); + + // Check that there's enough collateralToken added as to cover both the keeper's payout and the amount used to save the SAFE + require(collateralTokenCover[safeHandler] >= add(keeperPayout, tokenAmountUsed), "GeneralTokenReserveSafeSaviour/not-enough-cover-deposited"); + + // Update the remaining cover + collateralTokenCover[safeHandler] = sub(collateralTokenCover[safeHandler], add(keeperPayout, tokenAmountUsed)); + + // Mark the SAFE in the registry as just being saved + saviourRegistry.markSave(collateralType, safeHandler); + + // Approve collateralToken to the collateral join contract + collateralToken.approve(address(collateralJoin), 0); + collateralToken.approve(address(collateralJoin), tokenAmountUsed); + + // Join collateralToken in the system and add it in the saved SAFE + collateralJoin.join(address(this), tokenAmountUsed); + safeEngine.modifySAFECollateralization( + collateralJoin.collateralType(), + safeHandler, + address(this), + address(0), + int256(tokenAmountUsed), + int256(0) + ); + + // Send the fee to the keeper + collateralToken.transfer(keeper, keeperPayout); + + // Emit an event + emit SaveSAFE(keeper, collateralType, safeHandler, tokenAmountUsed); + + return (true, tokenAmountUsed, keeperPayout); + } + + // --- Getters --- + /* + * @notice Compute whether the value of keeperPayout collateralToken is higher than or equal to minKeeperPayoutValue + * @dev Used to determine whether it's worth it for the keeper to save the SAFE in exchange for keeperPayout collateralToken + * @return A bool representing whether the value of keeperPayout collateralToken is >= minKeeperPayoutValue + */ + function keeperPayoutExceedsMinValue() override public returns (bool) { + (address ethFSM,,) = oracleRelayer.collateralTypes(collateralJoin.collateralType()); + (uint256 priceFeedValue, bool hasValidValue) = PriceFeedLike(PriceFeedLike(ethFSM).priceSource()).getResultWithValidity(); + + if (either(!hasValidValue, priceFeedValue == 0)) { + return false; + } + + return (minKeeperPayoutValue <= mul(keeperPayout, priceFeedValue) / WAD); + } + /* + * @notice Return the current value of the keeper payout + */ + function getKeeperPayoutValue() override public returns (uint256) { + (address ethFSM,,) = oracleRelayer.collateralTypes(collateralJoin.collateralType()); + (uint256 priceFeedValue, bool hasValidValue) = PriceFeedLike(PriceFeedLike(ethFSM).priceSource()).getResultWithValidity(); + + if (either(!hasValidValue, priceFeedValue == 0)) { + return 0; + } + + return mul(keeperPayout, priceFeedValue) / WAD; + } + /* + * @notice Determine whether a SAFE can be saved with the current amount of collateralToken deposited as cover for it + * @param collateralType The SAFE collateral type (ignored in this implementation) + * @param safeHandler The handler of the SAFE which the function takes into account + * @return Whether the SAFE can be saved or not + */ + function canSave(bytes32, address safeHandler) override external returns (bool) { + uint256 tokenAmountUsed = tokenAmountUsedToSave(collateralJoin.collateralType(), safeHandler); + + if (either(tokenAmountUsed == MAX_UINT, tokenAmountUsed == 0)) { + return false; + } + + return (collateralTokenCover[safeHandler] >= add(tokenAmountUsed, keeperPayout)); + } + /* + * @notice Calculate the amount of collateralToken used to save a SAFE and bring its CRatio to the desired level + * @param collateralType The SAFE collateral type (ignored in this implementation) + * @param safeHandler The handler of the SAFE which the function takes into account + * @return The amount of collateralToken used to save the SAFE and bring its CRatio to the desired level + */ + function tokenAmountUsedToSave(bytes32, address safeHandler) override public returns (uint256 tokenAmountUsed) { + (uint256 depositedCollateralToken, uint256 safeDebt) = + SAFEEngineLike(collateralJoin.safeEngine()).safes(collateralJoin.collateralType(), safeHandler); + (address ethFSM,,) = oracleRelayer.collateralTypes(collateralJoin.collateralType()); + if (ethFSM == address(0)) return MAX_UINT; + (uint256 priceFeedValue, bool hasValidValue) = PriceFeedLike(ethFSM).getResultWithValidity(); + + // If the SAFE doesn't have debt, if the price feed is faulty or if the default desired CRatio is null, abort + uint256 defaultCRatio = cRatioSetter.defaultDesiredCollateralizationRatios(collateralJoin.collateralType()); + if (either(either(safeDebt == 0, either(priceFeedValue == 0, !hasValidValue)), defaultCRatio == 0)) { + tokenAmountUsed = MAX_UINT; + return MAX_UINT; + } + + // Calculate the value of the debt equivalent to the value of the collateralToken that would need to be in the SAFE after it's saved + uint256 targetCRatio = (cRatioSetter.desiredCollateralizationRatios(collateralJoin.collateralType(), safeHandler) == 0) ? + defaultCRatio : cRatioSetter.desiredCollateralizationRatios(collateralJoin.collateralType(), safeHandler); + uint256 scaledDownDebtValue = mul( + mul(oracleRelayer.redemptionPrice(), safeDebt) / RAY, getAccumulatedRate(collateralJoin.collateralType()) + ) / RAY; + scaledDownDebtValue = mul(add(scaledDownDebtValue, ONE), targetCRatio) / HUNDRED; + + // Compute the amount of collateralToken the SAFE needs to get to the desired CRatio + uint256 collateralTokenAmountNeeded = mul(scaledDownDebtValue, WAD) / priceFeedValue; + + // If the amount of collateralToken needed is lower than the amount that's currently in the SAFE, return 0 + if (collateralTokenAmountNeeded <= depositedCollateralToken) { + return 0; + } else { + // Otherwise return the delta + return sub(collateralTokenAmountNeeded, depositedCollateralToken); + } + } + /* + * @notify Get the accumulated interest rate for a specific collateral type + * @param The collateral type for which to retrieve the rate + */ + function getAccumulatedRate(bytes32 collateralType) + public view returns (uint256 accumulatedRate) { + (, accumulatedRate, , , , ) = safeEngine.collateralTypes(collateralType); + } +} diff --git a/src/saviours/NativeUnderlyingUniswapV2CustomCRatioSafeSaviour.sol b/src/saviours/NativeUnderlyingUniswapV2CustomCRatioSafeSaviour.sol index b3646c9..e3ade09 100644 --- a/src/saviours/NativeUnderlyingUniswapV2CustomCRatioSafeSaviour.sol +++ b/src/saviours/NativeUnderlyingUniswapV2CustomCRatioSafeSaviour.sol @@ -1,647 +1,647 @@ -// Copyright (C) 2021 Reflexer Labs, INC - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -pragma solidity 0.6.7; - -import "../interfaces/UniswapLiquidityManagerLike.sol"; -import "../interfaces/SafeSaviourLike.sol"; -import "../math/SafeMath.sol"; -import "../math/Math.sol"; - -contract NativeUnderlyingUniswapV2CustomCRatioSafeSaviour is Math, SafeMath, SafeSaviourLike { - // --- Auth --- - mapping (address => uint256) public authorizedAccounts; - /** - * @notice Add auth to an account - * @param account Account to add auth to - */ - function addAuthorization(address account) external isAuthorized { - authorizedAccounts[account] = 1; - emit AddAuthorization(account); - } - /** - * @notice Remove auth from an account - * @param account Account to remove auth from - */ - function removeAuthorization(address account) external isAuthorized { - authorizedAccounts[account] = 0; - emit RemoveAuthorization(account); - } - /** - * @notice Checks whether msg.sender can call an authed function - **/ - modifier isAuthorized { - require(authorizedAccounts[msg.sender] == 1, "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/account-not-authorized"); - _; - } - - mapping (address => uint256) public allowedUsers; - /** - * @notice Allow a user to deposit assets - * @param usr User to whitelist - */ - function allowUser(address usr) external isAuthorized { - allowedUsers[usr] = 1; - emit AllowUser(usr); - } - /** - * @notice Disallow a user from depositing assets - * @param usr User to disallow - */ - function disallowUser(address usr) external isAuthorized { - allowedUsers[usr] = 0; - emit DisallowUser(usr); - } - /** - * @notice Checks whether an address is an allowed user - **/ - modifier isAllowed { - require( - either(restrictUsage == 0, both(restrictUsage == 1, allowedUsers[msg.sender] == 1)), - "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/account-not-allowed" - ); - _; - } - - // --- Variables --- - // Flag that tells whether usage of the contract is restricted to allowed users - uint256 public restrictUsage; - // Whether the system coin is token0 in the Uniswap pool or not - bool public isSystemCoinToken0; - - // Amount of LP tokens currently protecting each position - mapping(address => uint256) public lpTokenCover; - // Amount of system coin tokens that Safe owners can get back - mapping(address => uint256) public underlyingReserves; - // cRatio threshold for each Safe, below which anyone can call saveSAFE (safeHandler, threshold) - mapping(address => uint) public cRatioThresholds; - - // Liquidity manager contract for Uniswap v2/v3 - UniswapLiquidityManagerLike public liquidityManager; - // The ERC20 system coin - ERC20Like public systemCoin; - // The system coin join contract - CoinJoinLike public coinJoin; - // The collateral join contract for adding collateral in the system - CollateralJoinLike public collateralJoin; - // The LP token - ERC20Like public lpToken; - // The collateral token - ERC20Like public collateralToken; - // Oracle providing the system coin price feed - PriceFeedLike public systemCoinOrcl; - - // --- Events --- - event AddAuthorization(address account); - event RemoveAuthorization(address account); - event AllowUser(address usr); - event DisallowUser(address usr); - event ModifyParameters(bytes32 indexed parameter, uint256 val); - event ModifyParameters(bytes32 indexed parameter, address data); - event Deposit( - address indexed caller, - address indexed safeHandler, - uint256 lpTokenAmount - ); - event Withdraw( - address indexed caller, - address indexed safeHandler, - address dst, - uint256 lpTokenAmount - ); - event GetReserves( - address indexed caller, - address indexed safeHandler, - uint256 systemCoinAmount, - address dst - ); - - constructor( - bool isSystemCoinToken0_, - address coinJoin_, - address collateralJoin_, - address systemCoinOrcl_, - address liquidationEngine_, - address taxCollector_, - address oracleRelayer_, - address safeManager_, - address liquidityManager_, - address lpToken_, - uint256 minKeeperPayoutValue_ - ) public { - require(coinJoin_ != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-coin-join"); - require(collateralJoin_ != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-collateral-join"); - require(systemCoinOrcl_ != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-system-coin-oracle"); - require(oracleRelayer_ != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-oracle-relayer"); - require(liquidationEngine_ != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-liquidation-engine"); - require(taxCollector_ != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-tax-collector"); - require(safeManager_ != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-safe-manager"); - require(liquidityManager_ != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-liq-manager"); - require(lpToken_ != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-lp-token"); - require(minKeeperPayoutValue_ > 0, "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/invalid-min-payout-value"); - - authorizedAccounts[msg.sender] = 1; - - isSystemCoinToken0 = isSystemCoinToken0_; - minKeeperPayoutValue = minKeeperPayoutValue_; - - coinJoin = CoinJoinLike(coinJoin_); - collateralJoin = CollateralJoinLike(collateralJoin_); - liquidationEngine = LiquidationEngineLike(liquidationEngine_); - taxCollector = TaxCollectorLike(taxCollector_); - oracleRelayer = OracleRelayerLike(oracleRelayer_); - systemCoinOrcl = PriceFeedLike(systemCoinOrcl_); - systemCoin = ERC20Like(coinJoin.systemCoin()); - safeEngine = SAFEEngineLike(coinJoin.safeEngine()); - safeManager = GebSafeManagerLike(safeManager_); - liquidityManager = UniswapLiquidityManagerLike(liquidityManager_); - lpToken = ERC20Like(lpToken_); - collateralToken = ERC20Like(collateralJoin.collateral()); - - systemCoinOrcl.getResultWithValidity(); - oracleRelayer.redemptionPrice(); - - require(collateralJoin.contractEnabled() == 1, "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/join-disabled"); - require(address(collateralToken) != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-col-token"); - require(address(safeEngine) != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-safe-engine"); - require(address(systemCoin) != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-sys-coin"); - - emit AddAuthorization(msg.sender); - emit ModifyParameters("minKeeperPayoutValue", minKeeperPayoutValue); - emit ModifyParameters("oracleRelayer", oracleRelayer_); - emit ModifyParameters("taxCollector", taxCollector_); - emit ModifyParameters("systemCoinOrcl", systemCoinOrcl_); - emit ModifyParameters("liquidationEngine", liquidationEngine_); - emit ModifyParameters("liquidityManager", liquidityManager_); - } - - // --- Administration --- - /** - * @notice Modify an uint256 param - * @param parameter The name of the parameter - * @param val New value for the parameter - */ - function modifyParameters(bytes32 parameter, uint256 val) external isAuthorized { - if (parameter == "minKeeperPayoutValue") { - require(val > 0, "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-min-payout"); - minKeeperPayoutValue = val; - } - else if (parameter == "restrictUsage") { - require(val <= 1, "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/invalid-restriction"); - restrictUsage = val; - } - else revert("NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/modify-unrecognized-param"); - emit ModifyParameters(parameter, val); - } - /** - * @notice Modify an address param - * @param parameter The name of the parameter - * @param data New address for the parameter - */ - function modifyParameters(bytes32 parameter, address data) external isAuthorized { - require(data != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-data"); - - if (parameter == "systemCoinOrcl") { - systemCoinOrcl = PriceFeedLike(data); - systemCoinOrcl.getResultWithValidity(); - } - else if (parameter == "oracleRelayer") { - oracleRelayer = OracleRelayerLike(data); - oracleRelayer.redemptionPrice(); - } - else if (parameter == "liquidityManager") { - liquidityManager = UniswapLiquidityManagerLike(data); - } - else if (parameter == "liquidationEngine") { - liquidationEngine = LiquidationEngineLike(data); - } - else if (parameter == "taxCollector") { - taxCollector = TaxCollectorLike(data); - } - else revert("NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/modify-unrecognized-param"); - emit ModifyParameters(parameter, data); - } - - // --- Setting cRatio Threshold --- - /* - * @notice Set cRatio threshold - * @dev Only an address that controls the SAFE inside the SAFE Manager can call this - * @param safeID The ID of the SAFE to set the threshold for. This ID should be registered inside the SAFE Manager - * @param cRatioThreshold The threshold below which the SAFE can be saved - */ - function setCRatioThreshold(uint256 safeID, uint256 cRatioThreshold) external controlsSAFE(msg.sender, safeID) { - address safeHandler = safeManager.safes(safeID); - cRatioThresholds[safeHandler] = cRatioThreshold; - } - // --- Transferring Reserves --- - /* - * @notify Get back system coins that were withdrawn from Uniswap and not used to save a specific SAFE - * @param safeID The ID of the Safe that was previously saved and has leftover system coins that can be withdrawn - * @param dst The address that will receive system coins - */ - function getReserves(uint256 safeID, address dst) external controlsSAFE(msg.sender, safeID) nonReentrant { - address safeHandler = safeManager.safes(safeID); - uint256 systemCoins = underlyingReserves[safeHandler]; - - require(systemCoins > 0, "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/no-reserves"); - underlyingReserves[safeHandler] = 0; - - systemCoin.transfer(dst, systemCoins); - - emit GetReserves(msg.sender, safeHandler, systemCoins, dst); - } - - // --- Adding/Withdrawing Cover --- - /* - * @notice Deposit lpTokenAmount in the contract in order to provide cover for a specific SAFE managed by the SAFE Manager - * @param safeID The ID of the SAFE to protect. This ID should be registered inside GebSafeManager - * @param lpTokenAmount The amount of LP tokens to deposit - * @param threshold cRatio threshold - */ - function deposit(uint256 safeID, uint256 lpTokenAmount) external isAllowed() nonReentrant { - require(lpTokenAmount > 0, "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-lp-amount"); - - // Check that the SAFE exists inside GebSafeManager - address safeHandler = safeManager.safes(safeID); - require(safeHandler != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-handler"); - - // Check that the SAFE has debt - (, uint256 safeDebt) = - SAFEEngineLike(collateralJoin.safeEngine()).safes(collateralJoin.collateralType(), safeHandler); - require(safeDebt > 0, "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/safe-does-not-have-debt"); - - // Update the lpToken balance used to cover the SAFE and transfer tokens to this contract - lpTokenCover[safeHandler] = add(lpTokenCover[safeHandler], lpTokenAmount); - require(lpToken.transferFrom(msg.sender, address(this), lpTokenAmount), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/could-not-transfer-lp"); - - emit Deposit(msg.sender, safeHandler, lpTokenAmount); - } - /* - * @notice Withdraw lpTokenAmount from the contract and provide less cover for a SAFE - * @dev Only an address that controls the SAFE inside the SAFE Manager can call this - * @param safeID The ID of the SAFE to remove cover from. This ID should be registered inside the SAFE Manager - * @param lpTokenAmount The amount of lpToken to withdraw - * @param dst The address that will receive the LP tokens - */ - function withdraw(uint256 safeID, uint256 lpTokenAmount, address dst) external controlsSAFE(msg.sender, safeID) nonReentrant { - require(lpTokenAmount > 0, "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-lp-amount"); - - // Fetch the handler from the SAFE manager - address safeHandler = safeManager.safes(safeID); - require(lpTokenCover[safeHandler] >= lpTokenAmount, "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/not-enough-to-withdraw"); - - // Withdraw cover and transfer collateralToken to the caller - lpTokenCover[safeHandler] = sub(lpTokenCover[safeHandler], lpTokenAmount); - lpToken.transfer(dst, lpTokenAmount); - - emit Withdraw(msg.sender, safeHandler, dst, lpTokenAmount); - } - - // --- Saving Logic --- - /* - * @notice Saves a SAFE by withdrawing liquidity and repaying debt and/or adding more collateral - * @param keeper The keeper that should be rewarded for spending gas to save the SAFE - * @param collateralType The collateral type backing the SAFE that's being liquidated - * @param safeHandler The handler of the SAFE that's being liquidated - * @return Whether the SAFE has been saved, the amount of LP tokens that were used to withdraw liquidity as well as the amount of - * system coins sent to the keeper as their payment (this implementation always returns 0) - */ - function saveSAFE(address keeper, bytes32 collateralType, address safeHandler) override external nonReentrant returns (bool, uint256, uint256) { - require(keeper != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-keeper-address"); - - // Check that this is handling the correct collateral - require(collateralType == collateralJoin.collateralType(), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/invalid-collateral-type"); - - // Check that the SAFE has a non null amount of LP tokens covering it - require(lpTokenCover[safeHandler] > 0, "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-cover"); - - // Tax the collateral - taxCollector.taxSingle(collateralType); - - // calls allowed if safe cRatio is lower than user defined cRatio - require(getSafeCRatio(safeHandler) <= mul(cRatioThresholds[safeHandler], RAY / 100), - "NativeUnderlyingUniswapV2SafeSaviour/safe-above-threshold"); - - // Store cover amount in local var - uint256 totalCover = lpTokenCover[safeHandler]; - delete(lpTokenCover[safeHandler]); - - // Withdraw all liquidity - uint256 sysCoinBalance = systemCoin.balanceOf(address(this)); - - lpToken.approve(address(liquidityManager), totalCover); - liquidityManager.removeLiquidity(totalCover, 0, 0, address(this)); - - // Check after removing liquidity - require( - systemCoin.balanceOf(address(this)) > sysCoinBalance, - "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/faulty-remove-liquidity" - ); - - // Compute how many coins were withdrawn as well as the amount of ETH that's in this contract - sysCoinBalance = sub(systemCoin.balanceOf(address(this)), sysCoinBalance); - uint256 collateralCoinBalance = collateralToken.balanceOf(address(this)); - - // Get the amounts of tokens sent to the keeper as payment - (uint256 keeperSysCoins, uint256 keeperCollateralCoins) = - getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), sysCoinBalance, collateralCoinBalance); - - // There must be tokens that go to the keeper - require(either(keeperSysCoins > 0, keeperCollateralCoins > 0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/cannot-pay-keeper"); - - // Compute how many coins remain after paying the keeper - sysCoinBalance = sub(sysCoinBalance, keeperSysCoins); - collateralCoinBalance = sub(collateralCoinBalance, keeperCollateralCoins); - - // There must be tokens that are used to save the SAFE - require(either(sysCoinBalance > 0, collateralCoinBalance > 0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/cannot-save-safe"); - - // Get the amount of system coins used to repay debt - uint256 safeDebtRepaid = getTokensForSaving(safeHandler, sysCoinBalance); - - // Compute remaining balances of tokens that will go into reserves - sysCoinBalance = sub(sysCoinBalance, safeDebtRepaid); - - // Update reserves - if (sysCoinBalance > 0) { - underlyingReserves[safeHandler] = add( - underlyingReserves[safeHandler], sysCoinBalance - ); - } - - // Save the SAFE - if (safeDebtRepaid > 0) { - // Approve the coin join contract to take system coins and repay debt - systemCoin.approve(address(coinJoin), safeDebtRepaid); - // Calculate the non adjusted system coin amount - uint256 nonAdjustedSystemCoinsToRepay = div(mul(safeDebtRepaid, RAY), getAccumulatedRate(collateralType)); - - // Join system coins in the system and repay the SAFE's debt - coinJoin.join(address(this), safeDebtRepaid); - safeEngine.modifySAFECollateralization( - collateralType, - safeHandler, - address(0), - address(this), - int256(0), - -int256(nonAdjustedSystemCoinsToRepay) - ); - } - - if (collateralCoinBalance > 0) { - // Approve collateralToken to the collateral join contract - collateralToken.approve(address(collateralJoin), collateralCoinBalance); - - // Join collateralToken in the system and add it in the saved SAFE - collateralJoin.join(address(this), collateralCoinBalance); - safeEngine.modifySAFECollateralization( - collateralType, - safeHandler, - address(this), - address(0), - int256(collateralCoinBalance), - int256(0) - ); - } - - // Check that the current cRatio is above the liquidation threshold - require(safeIsAfloat(safeHandler), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/safe-not-saved"); - - // Pay keeper - if (keeperSysCoins > 0) { - systemCoin.transfer(keeper, keeperSysCoins); - } - - if (keeperCollateralCoins > 0) { - collateralToken.transfer(keeper, keeperCollateralCoins); - } - - // Emit an event - emit SaveSAFE(keeper, collateralType, safeHandler, totalCover); - - return (true, totalCover, 0); - } - - // --- Getters --- - /* - * @notify Must be implemented according to the interface although it always returns 0 - */ - function getKeeperPayoutValue() override public returns (uint256) { - return 0; - } - /* - * @notify Must be implemented according to the interface although it always returns false - */ - function keeperPayoutExceedsMinValue() override public returns (bool) { - return false; - } - /* - * @notice Determine whether a SAFE can be saved with the current amount of lpTokenCover deposited as cover for it - * @param safeHandler The handler of the SAFE which the function takes into account - * @return Whether the SAFE can be saved or not - */ - function canSave(bytes32, address safeHandler) override external returns (bool) { - // Fetch the redemption price first - uint256 redemptionPrice = oracleRelayer.redemptionPrice(); - - // Fetch the amount of tokens used to save the SAFE - (uint256 systemCoinAmount, uint256 collateralAmount) = - getLPUnderlying(safeHandler); - - // Get the amounts of tokens sent to the keeper as payment - (uint256 keeperSysCoins, uint256 keeperCollateralCoins) = - getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), systemCoinAmount, collateralAmount); - - // Compute how many coins remain after paying the keeper - systemCoinAmount = sub(systemCoinAmount, keeperSysCoins); - collateralAmount = sub(collateralAmount, keeperCollateralCoins); - - // There must be tokens that can be used to save the SAFE - if (both(systemCoinAmount == 0, collateralAmount == 0)) { - return false; - } - - // Get the amount of system coins used to repay debt - uint256 safeDebtRepaid = getTokensForSaving(safeHandler, systemCoinAmount); - if (safeDebtRepaid > systemCoinAmount) return false; - - // If resulting debt is below the floor or if the SAFE can't be saved, return false - { - (, uint256 accumulatedRate, , , uint256 debtFloor, uint256 liquidationPrice) = - safeEngine.collateralTypes(collateralJoin.collateralType()); - (uint256 safeCollateral, uint256 safeDebt) = safeEngine.safes(collateralJoin.collateralType(), safeHandler); - - uint256 remainingDebt = sub(safeDebt, safeDebtRepaid); - - if (either( - both(mul(remainingDebt, accumulatedRate) < debtFloor, remainingDebt != 0), - mul(add(safeCollateral, collateralAmount), liquidationPrice) < mul(remainingDebt, accumulatedRate) - )) { - return false; - } - } - - // If there are some tokens used to used to repay the keeper, return true - if (either(keeperSysCoins > 0, keeperCollateralCoins > 0)) { - return true; - } - - return false; - } - /* - * @notice Return the total amount of LP tokens covering a specific SAFE - * @param safeHandler The handler of the SAFE which the function takes into account - * @return The total LP token cover for a specific SAFE - */ - function tokenAmountUsedToSave(bytes32, address safeHandler) override public returns (uint256) { - return lpTokenCover[safeHandler]; - } - /* - * @notify Fetch the collateral's price - */ - function getCollateralPrice() public view returns (uint256) { - (address ethFSM,,) = oracleRelayer.collateralTypes(collateralJoin.collateralType()); - if (ethFSM == address(0)) return 0; - - (uint256 priceFeedValue, bool hasValidValue) = PriceFeedLike(ethFSM).getResultWithValidity(); - if (!hasValidValue) return 0; - - return priceFeedValue; - } - /* - * @notify Fetch the system coin's market price - */ - function getSystemCoinMarketPrice() public view returns (uint256) { - (uint256 priceFeedValue, bool hasValidValue) = systemCoinOrcl.getResultWithValidity(); - if (!hasValidValue) return 0; - - return priceFeedValue; - } - /* - * @notify Get the current collateralization ratio of a SAFE - * @param safeHandler The handler/address of the SAFE whose collateralization ratio is retrieved - */ - function getSafeCRatio(address safeHandler) public view returns (uint256) { - bytes32 collateralType = collateralJoin.collateralType(); - (, uint256 accumulatedRate, uint256 safetyPrice, , , ) = safeEngine.collateralTypes(collateralType); - (,, uint256 liquidationCRatio) = oracleRelayer.collateralTypes(collateralJoin.collateralType()); - (uint256 collateralBalance, uint256 debtBalance) = - SAFEEngineLike(collateralJoin.safeEngine()).safes(collateralJoin.collateralType(), safeHandler); - - return div(mul(collateralBalance, mul(safetyPrice, liquidationCRatio)), mul(debtBalance, accumulatedRate)); - } - /* - * @notify Return the amount of system coins and collateral tokens retrieved from the LP position covering a specific SAFE - * @param safeHandler The handler/address of the targeted SAFE - */ - function getLPUnderlying(address safeHandler) public view returns (uint256, uint256) { - uint256 coverAmount = lpTokenCover[safeHandler]; - - if (coverAmount == 0) return (0, 0); - - (uint256 sysCoinsFromLP, uint256 collateralFromLP) = (isSystemCoinToken0) ? - (liquidityManager.getToken0FromLiquidity(coverAmount), liquidityManager.getToken1FromLiquidity(coverAmount)) : - (liquidityManager.getToken1FromLiquidity(coverAmount), liquidityManager.getToken0FromLiquidity(coverAmount)); - - return (sysCoinsFromLP, collateralFromLP); - } - /* - * @notice Return the amount of system coins used to save a SAFE - * @param safeHandler The handler/address of the targeted SAFE - * @param maxSystemCoins Max amount of system coins that can be used to save the SAFE - */ - function getTokensForSaving(address safeHandler, uint256 maxSystemCoins) - public view returns (uint256) { - if (maxSystemCoins == 0) return 0; - - bytes32 collateralType = collateralJoin.collateralType(); - - // Get the SAFE debt - (, uint256 safeDebt) = safeEngine.safes(collateralType, safeHandler); - - if (safeDebt <= maxSystemCoins) { - return safeDebt; - } - - (, uint256 accumulatedRate, , , uint debtFloor, ) = safeEngine.collateralTypes(collateralType); - uint256 adjustedDebt = mul(accumulatedRate, safeDebt); - - if (debtFloor >= adjustedDebt) { - return 0; - } - - uint256 debtToRepay = sub(adjustedDebt, debtFloor) / RAY; - - return min(maxSystemCoins, debtToRepay); - } - /* - * @notice Return the amount of system coins and/or collateral tokens used to pay a keeper - * @param safeHandler The handler/address of the targeted SAFE - * @param redemptionPrice The system coin redemption price used in calculations - * @param sysCoinAmount Amount of system coin available - * @param collateralAmount The amount of collateral tokens that are available - */ - function getKeeperPayoutTokens(address safeHandler, uint256 redemptionPrice, uint256 sysCoinAmount, uint256 collateralAmount) - public view returns (uint256, uint256) { - // Get the system coin and collateral market prices - uint256 collateralPrice = getCollateralPrice(); - uint256 sysCoinMarketPrice = getSystemCoinMarketPrice(); - if (either(collateralPrice == 0, sysCoinMarketPrice == 0)) { - return (0, 0); - } - - // Check if the keeper can get system coins and if yes, compute how many - uint256 keeperSysCoins; - uint256 payoutInSystemCoins = div(mul(minKeeperPayoutValue, WAD), sysCoinMarketPrice); - - if (payoutInSystemCoins <= sysCoinAmount) { - return (payoutInSystemCoins, 0); - } else { - keeperSysCoins = sysCoinAmount; - } - - // Calculate how much collateral the keeper will get - uint256 remainingKeeperPayoutValue = sub(minKeeperPayoutValue, mul(keeperSysCoins, sysCoinMarketPrice) / WAD); - uint256 collateralTokenNeeded = div(mul(remainingKeeperPayoutValue, WAD), collateralPrice); - - // If there are enough collateral tokens retreived from LP in order to pay the keeper, return the token amounts - if (collateralTokenNeeded <= collateralAmount) { - return (keeperSysCoins, collateralTokenNeeded); - } else { - // Otherwise, return zeroes - return (0, 0); - } - } - /* - * @notify Returns whether a SAFE is afloat - * @param safeHandler The handler of the SAFE to verify - */ - function safeIsAfloat(address safeHandler) public view returns (bool) { - (, uint256 accumulatedRate, , , , uint256 liquidationPrice) = safeEngine.collateralTypes(collateralJoin.collateralType()); - (uint256 safeCollateral, uint256 safeDebt) = safeEngine.safes(collateralJoin.collateralType(), safeHandler); - - return ( - mul(safeCollateral, liquidationPrice) > mul(safeDebt, accumulatedRate) - ); - } - /* - * @notify Get the accumulated interest rate for a specific collateral type - * @param The collateral type for which to retrieve the rate - */ - function getAccumulatedRate(bytes32 collateralType) - public view returns (uint256 accumulatedRate) { - (, accumulatedRate, , , , ) = safeEngine.collateralTypes(collateralType); - } -} +// Copyright (C) 2021 Reflexer Labs, INC + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity 0.6.7; + +import "../interfaces/UniswapLiquidityManagerLike.sol"; +import "../interfaces/SafeSaviourLike.sol"; +import "../math/SafeMath.sol"; +import "../math/Math.sol"; + +contract NativeUnderlyingUniswapV2CustomCRatioSafeSaviour is Math, SafeMath, SafeSaviourLike { + // --- Auth --- + mapping (address => uint256) public authorizedAccounts; + /** + * @notice Add auth to an account + * @param account Account to add auth to + */ + function addAuthorization(address account) external isAuthorized { + authorizedAccounts[account] = 1; + emit AddAuthorization(account); + } + /** + * @notice Remove auth from an account + * @param account Account to remove auth from + */ + function removeAuthorization(address account) external isAuthorized { + authorizedAccounts[account] = 0; + emit RemoveAuthorization(account); + } + /** + * @notice Checks whether msg.sender can call an authed function + **/ + modifier isAuthorized { + require(authorizedAccounts[msg.sender] == 1, "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/account-not-authorized"); + _; + } + + mapping (address => uint256) public allowedUsers; + /** + * @notice Allow a user to deposit assets + * @param usr User to whitelist + */ + function allowUser(address usr) external isAuthorized { + allowedUsers[usr] = 1; + emit AllowUser(usr); + } + /** + * @notice Disallow a user from depositing assets + * @param usr User to disallow + */ + function disallowUser(address usr) external isAuthorized { + allowedUsers[usr] = 0; + emit DisallowUser(usr); + } + /** + * @notice Checks whether an address is an allowed user + **/ + modifier isAllowed { + require( + either(restrictUsage == 0, both(restrictUsage == 1, allowedUsers[msg.sender] == 1)), + "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/account-not-allowed" + ); + _; + } + + // --- Variables --- + // Flag that tells whether usage of the contract is restricted to allowed users + uint256 public restrictUsage; + // Whether the system coin is token0 in the Uniswap pool or not + bool public isSystemCoinToken0; + + // Amount of LP tokens currently protecting each position + mapping(address => uint256) public lpTokenCover; + // Amount of system coin tokens that Safe owners can get back + mapping(address => uint256) public underlyingReserves; + // cRatio threshold for each Safe, below which anyone can call saveSAFE (safeHandler, threshold) + mapping(address => uint) public cRatioThresholds; + + // Liquidity manager contract for Uniswap v2/v3 + UniswapLiquidityManagerLike public liquidityManager; + // The ERC20 system coin + ERC20Like public systemCoin; + // The system coin join contract + CoinJoinLike public coinJoin; + // The collateral join contract for adding collateral in the system + CollateralJoinLike public collateralJoin; + // The LP token + ERC20Like public lpToken; + // The collateral token + ERC20Like public collateralToken; + // Oracle providing the system coin price feed + PriceFeedLike public systemCoinOrcl; + + // --- Events --- + event AddAuthorization(address account); + event RemoveAuthorization(address account); + event AllowUser(address usr); + event DisallowUser(address usr); + event ModifyParameters(bytes32 indexed parameter, uint256 val); + event ModifyParameters(bytes32 indexed parameter, address data); + event Deposit( + address indexed caller, + address indexed safeHandler, + uint256 lpTokenAmount + ); + event Withdraw( + address indexed caller, + address indexed safeHandler, + address dst, + uint256 lpTokenAmount + ); + event GetReserves( + address indexed caller, + address indexed safeHandler, + uint256 systemCoinAmount, + address dst + ); + + constructor( + bool isSystemCoinToken0_, + address coinJoin_, + address collateralJoin_, + address systemCoinOrcl_, + address liquidationEngine_, + address taxCollector_, + address oracleRelayer_, + address safeManager_, + address liquidityManager_, + address lpToken_, + uint256 minKeeperPayoutValue_ + ) public { + require(coinJoin_ != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-coin-join"); + require(collateralJoin_ != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-collateral-join"); + require(systemCoinOrcl_ != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-system-coin-oracle"); + require(oracleRelayer_ != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-oracle-relayer"); + require(liquidationEngine_ != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-liquidation-engine"); + require(taxCollector_ != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-tax-collector"); + require(safeManager_ != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-safe-manager"); + require(liquidityManager_ != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-liq-manager"); + require(lpToken_ != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-lp-token"); + require(minKeeperPayoutValue_ > 0, "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/invalid-min-payout-value"); + + authorizedAccounts[msg.sender] = 1; + + isSystemCoinToken0 = isSystemCoinToken0_; + minKeeperPayoutValue = minKeeperPayoutValue_; + + coinJoin = CoinJoinLike(coinJoin_); + collateralJoin = CollateralJoinLike(collateralJoin_); + liquidationEngine = LiquidationEngineLike(liquidationEngine_); + taxCollector = TaxCollectorLike(taxCollector_); + oracleRelayer = OracleRelayerLike(oracleRelayer_); + systemCoinOrcl = PriceFeedLike(systemCoinOrcl_); + systemCoin = ERC20Like(coinJoin.systemCoin()); + safeEngine = SAFEEngineLike(coinJoin.safeEngine()); + safeManager = GebSafeManagerLike(safeManager_); + liquidityManager = UniswapLiquidityManagerLike(liquidityManager_); + lpToken = ERC20Like(lpToken_); + collateralToken = ERC20Like(collateralJoin.collateral()); + + systemCoinOrcl.getResultWithValidity(); + oracleRelayer.redemptionPrice(); + + require(collateralJoin.contractEnabled() == 1, "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/join-disabled"); + require(address(collateralToken) != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-col-token"); + require(address(safeEngine) != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-safe-engine"); + require(address(systemCoin) != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-sys-coin"); + + emit AddAuthorization(msg.sender); + emit ModifyParameters("minKeeperPayoutValue", minKeeperPayoutValue); + emit ModifyParameters("oracleRelayer", oracleRelayer_); + emit ModifyParameters("taxCollector", taxCollector_); + emit ModifyParameters("systemCoinOrcl", systemCoinOrcl_); + emit ModifyParameters("liquidationEngine", liquidationEngine_); + emit ModifyParameters("liquidityManager", liquidityManager_); + } + + // --- Administration --- + /** + * @notice Modify an uint256 param + * @param parameter The name of the parameter + * @param val New value for the parameter + */ + function modifyParameters(bytes32 parameter, uint256 val) external isAuthorized { + if (parameter == "minKeeperPayoutValue") { + require(val > 0, "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-min-payout"); + minKeeperPayoutValue = val; + } + else if (parameter == "restrictUsage") { + require(val <= 1, "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/invalid-restriction"); + restrictUsage = val; + } + else revert("NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/modify-unrecognized-param"); + emit ModifyParameters(parameter, val); + } + /** + * @notice Modify an address param + * @param parameter The name of the parameter + * @param data New address for the parameter + */ + function modifyParameters(bytes32 parameter, address data) external isAuthorized { + require(data != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-data"); + + if (parameter == "systemCoinOrcl") { + systemCoinOrcl = PriceFeedLike(data); + systemCoinOrcl.getResultWithValidity(); + } + else if (parameter == "oracleRelayer") { + oracleRelayer = OracleRelayerLike(data); + oracleRelayer.redemptionPrice(); + } + else if (parameter == "liquidityManager") { + liquidityManager = UniswapLiquidityManagerLike(data); + } + else if (parameter == "liquidationEngine") { + liquidationEngine = LiquidationEngineLike(data); + } + else if (parameter == "taxCollector") { + taxCollector = TaxCollectorLike(data); + } + else revert("NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/modify-unrecognized-param"); + emit ModifyParameters(parameter, data); + } + + // --- Setting cRatio Threshold --- + /* + * @notice Set cRatio threshold + * @dev Only an address that controls the SAFE inside the SAFE Manager can call this + * @param safeID The ID of the SAFE to set the threshold for. This ID should be registered inside the SAFE Manager + * @param cRatioThreshold The threshold below which the SAFE can be saved + */ + function setCRatioThreshold(uint256 safeID, uint256 cRatioThreshold) external controlsSAFE(msg.sender, safeID) { + address safeHandler = safeManager.safes(safeID); + cRatioThresholds[safeHandler] = cRatioThreshold; + } + // --- Transferring Reserves --- + /* + * @notify Get back system coins that were withdrawn from Uniswap and not used to save a specific SAFE + * @param safeID The ID of the Safe that was previously saved and has leftover system coins that can be withdrawn + * @param dst The address that will receive system coins + */ + function getReserves(uint256 safeID, address dst) external controlsSAFE(msg.sender, safeID) nonReentrant { + address safeHandler = safeManager.safes(safeID); + uint256 systemCoins = underlyingReserves[safeHandler]; + + require(systemCoins > 0, "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/no-reserves"); + underlyingReserves[safeHandler] = 0; + + systemCoin.transfer(dst, systemCoins); + + emit GetReserves(msg.sender, safeHandler, systemCoins, dst); + } + + // --- Adding/Withdrawing Cover --- + /* + * @notice Deposit lpTokenAmount in the contract in order to provide cover for a specific SAFE managed by the SAFE Manager + * @param safeID The ID of the SAFE to protect. This ID should be registered inside GebSafeManager + * @param lpTokenAmount The amount of LP tokens to deposit + * @param threshold cRatio threshold + */ + function deposit(uint256 safeID, uint256 lpTokenAmount) external isAllowed() nonReentrant { + require(lpTokenAmount > 0, "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-lp-amount"); + + // Check that the SAFE exists inside GebSafeManager + address safeHandler = safeManager.safes(safeID); + require(safeHandler != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-handler"); + + // Check that the SAFE has debt + (, uint256 safeDebt) = + SAFEEngineLike(collateralJoin.safeEngine()).safes(collateralJoin.collateralType(), safeHandler); + require(safeDebt > 0, "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/safe-does-not-have-debt"); + + // Update the lpToken balance used to cover the SAFE and transfer tokens to this contract + lpTokenCover[safeHandler] = add(lpTokenCover[safeHandler], lpTokenAmount); + require(lpToken.transferFrom(msg.sender, address(this), lpTokenAmount), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/could-not-transfer-lp"); + + emit Deposit(msg.sender, safeHandler, lpTokenAmount); + } + /* + * @notice Withdraw lpTokenAmount from the contract and provide less cover for a SAFE + * @dev Only an address that controls the SAFE inside the SAFE Manager can call this + * @param safeID The ID of the SAFE to remove cover from. This ID should be registered inside the SAFE Manager + * @param lpTokenAmount The amount of lpToken to withdraw + * @param dst The address that will receive the LP tokens + */ + function withdraw(uint256 safeID, uint256 lpTokenAmount, address dst) external controlsSAFE(msg.sender, safeID) nonReentrant { + require(lpTokenAmount > 0, "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-lp-amount"); + + // Fetch the handler from the SAFE manager + address safeHandler = safeManager.safes(safeID); + require(lpTokenCover[safeHandler] >= lpTokenAmount, "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/not-enough-to-withdraw"); + + // Withdraw cover and transfer collateralToken to the caller + lpTokenCover[safeHandler] = sub(lpTokenCover[safeHandler], lpTokenAmount); + lpToken.transfer(dst, lpTokenAmount); + + emit Withdraw(msg.sender, safeHandler, dst, lpTokenAmount); + } + + // --- Saving Logic --- + /* + * @notice Saves a SAFE by withdrawing liquidity and repaying debt and/or adding more collateral + * @param keeper The keeper that should be rewarded for spending gas to save the SAFE + * @param collateralType The collateral type backing the SAFE that's being liquidated + * @param safeHandler The handler of the SAFE that's being liquidated + * @return Whether the SAFE has been saved, the amount of LP tokens that were used to withdraw liquidity as well as the amount of + * system coins sent to the keeper as their payment (this implementation always returns 0) + */ + function saveSAFE(address keeper, bytes32 collateralType, address safeHandler) override external nonReentrant returns (bool, uint256, uint256) { + require(keeper != address(0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-keeper-address"); + + // Check that this is handling the correct collateral + require(collateralType == collateralJoin.collateralType(), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/invalid-collateral-type"); + + // Check that the SAFE has a non null amount of LP tokens covering it + require(lpTokenCover[safeHandler] > 0, "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/null-cover"); + + // Tax the collateral + taxCollector.taxSingle(collateralType); + + // calls allowed if safe cRatio is lower than user defined cRatio + require(getSafeCRatio(safeHandler) <= mul(cRatioThresholds[safeHandler], RAY / 100), + "NativeUnderlyingUniswapV2SafeSaviour/safe-above-threshold"); + + // Store cover amount in local var + uint256 totalCover = lpTokenCover[safeHandler]; + delete(lpTokenCover[safeHandler]); + + // Withdraw all liquidity + uint256 sysCoinBalance = systemCoin.balanceOf(address(this)); + + lpToken.approve(address(liquidityManager), totalCover); + liquidityManager.removeLiquidity(totalCover, 0, 0, address(this)); + + // Check after removing liquidity + require( + systemCoin.balanceOf(address(this)) > sysCoinBalance, + "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/faulty-remove-liquidity" + ); + + // Compute how many coins were withdrawn as well as the amount of ETH that's in this contract + sysCoinBalance = sub(systemCoin.balanceOf(address(this)), sysCoinBalance); + uint256 collateralCoinBalance = collateralToken.balanceOf(address(this)); + + // Get the amounts of tokens sent to the keeper as payment + (uint256 keeperSysCoins, uint256 keeperCollateralCoins) = + getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), sysCoinBalance, collateralCoinBalance); + + // There must be tokens that go to the keeper + require(either(keeperSysCoins > 0, keeperCollateralCoins > 0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/cannot-pay-keeper"); + + // Compute how many coins remain after paying the keeper + sysCoinBalance = sub(sysCoinBalance, keeperSysCoins); + collateralCoinBalance = sub(collateralCoinBalance, keeperCollateralCoins); + + // There must be tokens that are used to save the SAFE + require(either(sysCoinBalance > 0, collateralCoinBalance > 0), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/cannot-save-safe"); + + // Get the amount of system coins used to repay debt + uint256 safeDebtRepaid = getTokensForSaving(safeHandler, sysCoinBalance); + + // Compute remaining balances of tokens that will go into reserves + sysCoinBalance = sub(sysCoinBalance, safeDebtRepaid); + + // Update reserves + if (sysCoinBalance > 0) { + underlyingReserves[safeHandler] = add( + underlyingReserves[safeHandler], sysCoinBalance + ); + } + + // Save the SAFE + if (safeDebtRepaid > 0) { + // Approve the coin join contract to take system coins and repay debt + systemCoin.approve(address(coinJoin), safeDebtRepaid); + // Calculate the non adjusted system coin amount + uint256 nonAdjustedSystemCoinsToRepay = div(mul(safeDebtRepaid, RAY), getAccumulatedRate(collateralType)); + + // Join system coins in the system and repay the SAFE's debt + coinJoin.join(address(this), safeDebtRepaid); + safeEngine.modifySAFECollateralization( + collateralType, + safeHandler, + address(0), + address(this), + int256(0), + -int256(nonAdjustedSystemCoinsToRepay) + ); + } + + if (collateralCoinBalance > 0) { + // Approve collateralToken to the collateral join contract + collateralToken.approve(address(collateralJoin), collateralCoinBalance); + + // Join collateralToken in the system and add it in the saved SAFE + collateralJoin.join(address(this), collateralCoinBalance); + safeEngine.modifySAFECollateralization( + collateralType, + safeHandler, + address(this), + address(0), + int256(collateralCoinBalance), + int256(0) + ); + } + + // Check that the current cRatio is above the liquidation threshold + require(safeIsAfloat(safeHandler), "NativeUnderlyingUniswapV2CustomCRatioSafeSaviour/safe-not-saved"); + + // Pay keeper + if (keeperSysCoins > 0) { + systemCoin.transfer(keeper, keeperSysCoins); + } + + if (keeperCollateralCoins > 0) { + collateralToken.transfer(keeper, keeperCollateralCoins); + } + + // Emit an event + emit SaveSAFE(keeper, collateralType, safeHandler, totalCover); + + return (true, totalCover, 0); + } + + // --- Getters --- + /* + * @notify Must be implemented according to the interface although it always returns 0 + */ + function getKeeperPayoutValue() override public returns (uint256) { + return 0; + } + /* + * @notify Must be implemented according to the interface although it always returns false + */ + function keeperPayoutExceedsMinValue() override public returns (bool) { + return false; + } + /* + * @notice Determine whether a SAFE can be saved with the current amount of lpTokenCover deposited as cover for it + * @param safeHandler The handler of the SAFE which the function takes into account + * @return Whether the SAFE can be saved or not + */ + function canSave(bytes32, address safeHandler) override external returns (bool) { + // Fetch the redemption price first + uint256 redemptionPrice = oracleRelayer.redemptionPrice(); + + // Fetch the amount of tokens used to save the SAFE + (uint256 systemCoinAmount, uint256 collateralAmount) = + getLPUnderlying(safeHandler); + + // Get the amounts of tokens sent to the keeper as payment + (uint256 keeperSysCoins, uint256 keeperCollateralCoins) = + getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), systemCoinAmount, collateralAmount); + + // Compute how many coins remain after paying the keeper + systemCoinAmount = sub(systemCoinAmount, keeperSysCoins); + collateralAmount = sub(collateralAmount, keeperCollateralCoins); + + // There must be tokens that can be used to save the SAFE + if (both(systemCoinAmount == 0, collateralAmount == 0)) { + return false; + } + + // Get the amount of system coins used to repay debt + uint256 safeDebtRepaid = getTokensForSaving(safeHandler, systemCoinAmount); + if (safeDebtRepaid > systemCoinAmount) return false; + + // If resulting debt is below the floor or if the SAFE can't be saved, return false + { + (, uint256 accumulatedRate, , , uint256 debtFloor, uint256 liquidationPrice) = + safeEngine.collateralTypes(collateralJoin.collateralType()); + (uint256 safeCollateral, uint256 safeDebt) = safeEngine.safes(collateralJoin.collateralType(), safeHandler); + + uint256 remainingDebt = sub(safeDebt, safeDebtRepaid); + + if (either( + both(mul(remainingDebt, accumulatedRate) < debtFloor, remainingDebt != 0), + mul(add(safeCollateral, collateralAmount), liquidationPrice) < mul(remainingDebt, accumulatedRate) + )) { + return false; + } + } + + // If there are some tokens used to used to repay the keeper, return true + if (either(keeperSysCoins > 0, keeperCollateralCoins > 0)) { + return true; + } + + return false; + } + /* + * @notice Return the total amount of LP tokens covering a specific SAFE + * @param safeHandler The handler of the SAFE which the function takes into account + * @return The total LP token cover for a specific SAFE + */ + function tokenAmountUsedToSave(bytes32, address safeHandler) override public returns (uint256) { + return lpTokenCover[safeHandler]; + } + /* + * @notify Fetch the collateral's price + */ + function getCollateralPrice() public view returns (uint256) { + (address ethFSM,,) = oracleRelayer.collateralTypes(collateralJoin.collateralType()); + if (ethFSM == address(0)) return 0; + + (uint256 priceFeedValue, bool hasValidValue) = PriceFeedLike(ethFSM).getResultWithValidity(); + if (!hasValidValue) return 0; + + return priceFeedValue; + } + /* + * @notify Fetch the system coin's market price + */ + function getSystemCoinMarketPrice() public view returns (uint256) { + (uint256 priceFeedValue, bool hasValidValue) = systemCoinOrcl.getResultWithValidity(); + if (!hasValidValue) return 0; + + return priceFeedValue; + } + /* + * @notify Get the current collateralization ratio of a SAFE + * @param safeHandler The handler/address of the SAFE whose collateralization ratio is retrieved + */ + function getSafeCRatio(address safeHandler) public view returns (uint256) { + bytes32 collateralType = collateralJoin.collateralType(); + (, uint256 accumulatedRate, uint256 safetyPrice, , , ) = safeEngine.collateralTypes(collateralType); + (,, uint256 liquidationCRatio) = oracleRelayer.collateralTypes(collateralJoin.collateralType()); + (uint256 collateralBalance, uint256 debtBalance) = + SAFEEngineLike(collateralJoin.safeEngine()).safes(collateralJoin.collateralType(), safeHandler); + + return div(mul(collateralBalance, mul(safetyPrice, liquidationCRatio)), mul(debtBalance, accumulatedRate)); + } + /* + * @notify Return the amount of system coins and collateral tokens retrieved from the LP position covering a specific SAFE + * @param safeHandler The handler/address of the targeted SAFE + */ + function getLPUnderlying(address safeHandler) public view returns (uint256, uint256) { + uint256 coverAmount = lpTokenCover[safeHandler]; + + if (coverAmount == 0) return (0, 0); + + (uint256 sysCoinsFromLP, uint256 collateralFromLP) = (isSystemCoinToken0) ? + (liquidityManager.getToken0FromLiquidity(coverAmount), liquidityManager.getToken1FromLiquidity(coverAmount)) : + (liquidityManager.getToken1FromLiquidity(coverAmount), liquidityManager.getToken0FromLiquidity(coverAmount)); + + return (sysCoinsFromLP, collateralFromLP); + } + /* + * @notice Return the amount of system coins used to save a SAFE + * @param safeHandler The handler/address of the targeted SAFE + * @param maxSystemCoins Max amount of system coins that can be used to save the SAFE + */ + function getTokensForSaving(address safeHandler, uint256 maxSystemCoins) + public view returns (uint256) { + if (maxSystemCoins == 0) return 0; + + bytes32 collateralType = collateralJoin.collateralType(); + + // Get the SAFE debt + (, uint256 safeDebt) = safeEngine.safes(collateralType, safeHandler); + + if (safeDebt <= maxSystemCoins) { + return safeDebt; + } + + (, uint256 accumulatedRate, , , uint debtFloor, ) = safeEngine.collateralTypes(collateralType); + uint256 adjustedDebt = mul(accumulatedRate, safeDebt); + + if (debtFloor >= adjustedDebt) { + return 0; + } + + uint256 debtToRepay = sub(adjustedDebt, debtFloor) / RAY; + + return min(maxSystemCoins, debtToRepay); + } + /* + * @notice Return the amount of system coins and/or collateral tokens used to pay a keeper + * @param safeHandler The handler/address of the targeted SAFE + * @param redemptionPrice The system coin redemption price used in calculations + * @param sysCoinAmount Amount of system coin available + * @param collateralAmount The amount of collateral tokens that are available + */ + function getKeeperPayoutTokens(address safeHandler, uint256 redemptionPrice, uint256 sysCoinAmount, uint256 collateralAmount) + public view returns (uint256, uint256) { + // Get the system coin and collateral market prices + uint256 collateralPrice = getCollateralPrice(); + uint256 sysCoinMarketPrice = getSystemCoinMarketPrice(); + if (either(collateralPrice == 0, sysCoinMarketPrice == 0)) { + return (0, 0); + } + + // Check if the keeper can get system coins and if yes, compute how many + uint256 keeperSysCoins; + uint256 payoutInSystemCoins = div(mul(minKeeperPayoutValue, WAD), sysCoinMarketPrice); + + if (payoutInSystemCoins <= sysCoinAmount) { + return (payoutInSystemCoins, 0); + } else { + keeperSysCoins = sysCoinAmount; + } + + // Calculate how much collateral the keeper will get + uint256 remainingKeeperPayoutValue = sub(minKeeperPayoutValue, mul(keeperSysCoins, sysCoinMarketPrice) / WAD); + uint256 collateralTokenNeeded = div(mul(remainingKeeperPayoutValue, WAD), collateralPrice); + + // If there are enough collateral tokens retreived from LP in order to pay the keeper, return the token amounts + if (collateralTokenNeeded <= collateralAmount) { + return (keeperSysCoins, collateralTokenNeeded); + } else { + // Otherwise, return zeroes + return (0, 0); + } + } + /* + * @notify Returns whether a SAFE is afloat + * @param safeHandler The handler of the SAFE to verify + */ + function safeIsAfloat(address safeHandler) public view returns (bool) { + (, uint256 accumulatedRate, , , , uint256 liquidationPrice) = safeEngine.collateralTypes(collateralJoin.collateralType()); + (uint256 safeCollateral, uint256 safeDebt) = safeEngine.safes(collateralJoin.collateralType(), safeHandler); + + return ( + mul(safeCollateral, liquidationPrice) > mul(safeDebt, accumulatedRate) + ); + } + /* + * @notify Get the accumulated interest rate for a specific collateral type + * @param The collateral type for which to retrieve the rate + */ + function getAccumulatedRate(bytes32 collateralType) + public view returns (uint256 accumulatedRate) { + (, accumulatedRate, , , , ) = safeEngine.collateralTypes(collateralType); + } +} diff --git a/src/saviours/NativeUnderlyingUniswapV2SafeSaviour.sol b/src/saviours/NativeUnderlyingUniswapV2SafeSaviour.sol index a7e18dc..75d7fcd 100644 --- a/src/saviours/NativeUnderlyingUniswapV2SafeSaviour.sol +++ b/src/saviours/NativeUnderlyingUniswapV2SafeSaviour.sol @@ -1,666 +1,666 @@ -// Copyright (C) 2021 Reflexer Labs, INC - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -pragma solidity 0.6.7; - -import "../interfaces/UniswapLiquidityManagerLike.sol"; -import "../interfaces/SaviourCRatioSetterLike.sol"; -import "../interfaces/SafeSaviourLike.sol"; -import "../math/SafeMath.sol"; - -contract NativeUnderlyingUniswapV2SafeSaviour is SafeMath, SafeSaviourLike { - // --- Auth --- - mapping (address => uint256) public authorizedAccounts; - /** - * @notice Add auth to an account - * @param account Account to add auth to - */ - function addAuthorization(address account) external isAuthorized { - authorizedAccounts[account] = 1; - emit AddAuthorization(account); - } - /** - * @notice Remove auth from an account - * @param account Account to remove auth from - */ - function removeAuthorization(address account) external isAuthorized { - authorizedAccounts[account] = 0; - emit RemoveAuthorization(account); - } - /** - * @notice Checks whether msg.sender can call an authed function - **/ - modifier isAuthorized { - require(authorizedAccounts[msg.sender] == 1, "NativeUnderlyingUniswapV2SafeSaviour/account-not-authorized"); - _; - } - - mapping (address => uint256) public allowedUsers; - /** - * @notice Allow a user to deposit assets - * @param usr User to whitelist - */ - function allowUser(address usr) external isAuthorized { - allowedUsers[usr] = 1; - emit AllowUser(usr); - } - /** - * @notice Disallow a user from depositing assets - * @param usr User to disallow - */ - function disallowUser(address usr) external isAuthorized { - allowedUsers[usr] = 0; - emit DisallowUser(usr); - } - /** - * @notice Checks whether an address is an allowed user - **/ - modifier isAllowed { - require( - either(restrictUsage == 0, both(restrictUsage == 1, allowedUsers[msg.sender] == 1)), - "NativeUnderlyingUniswapV2SafeSaviour/account-not-allowed" - ); - _; - } - - // --- Structs --- - struct Reserves { - uint256 systemCoins; - uint256 collateralCoins; - } - - // --- Variables --- - // Flag that tells whether usage of the contract is restricted to allowed users - uint256 public restrictUsage; - - // Whether the system coin is token0 in the Uniswap pool or not - bool public isSystemCoinToken0; - // Amount of LP tokens currently protecting each position - mapping(address => uint256) public lpTokenCover; - // Amount of system coin/collateral tokens that Safe owners can get back - mapping(address => Reserves) public underlyingReserves; - // Liquidity manager contract for Uniswap v2/v3 - UniswapLiquidityManagerLike public liquidityManager; - // The ERC20 system coin - ERC20Like public systemCoin; - // The system coin join contract - CoinJoinLike public coinJoin; - // The collateral join contract for adding collateral in the system - CollateralJoinLike public collateralJoin; - // The LP token - ERC20Like public lpToken; - // The collateral token - ERC20Like public collateralToken; - // Oracle providing the system coin price feed - PriceFeedLike public systemCoinOrcl; - // Contract that defines desired CRatios for each Safe after it is saved - SaviourCRatioSetterLike public cRatioSetter; - - // --- Events --- - event AddAuthorization(address account); - event RemoveAuthorization(address account); - event AllowUser(address usr); - event DisallowUser(address usr); - event ModifyParameters(bytes32 indexed parameter, uint256 val); - event ModifyParameters(bytes32 indexed parameter, address data); - event Deposit( - address indexed caller, - address indexed safeHandler, - uint256 lpTokenAmount - ); - event Withdraw( - address indexed caller, - address indexed safeHandler, - address dst, - uint256 lpTokenAmount - ); - event GetReserves( - address indexed caller, - address indexed safeHandler, - uint256 systemCoinAmount, - uint256 collateralAmount, - address dst - ); - - constructor( - bool isSystemCoinToken0_, - address coinJoin_, - address collateralJoin_, - address cRatioSetter_, - address systemCoinOrcl_, - address liquidationEngine_, - address taxCollector_, - address oracleRelayer_, - address safeManager_, - address saviourRegistry_, - address liquidityManager_, - address lpToken_, - uint256 minKeeperPayoutValue_ - ) public { - require(coinJoin_ != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-coin-join"); - require(collateralJoin_ != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-collateral-join"); - require(cRatioSetter_ != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-cratio-setter"); - require(systemCoinOrcl_ != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-system-coin-oracle"); - require(oracleRelayer_ != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-oracle-relayer"); - require(liquidationEngine_ != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-liquidation-engine"); - require(taxCollector_ != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-tax-collector"); - require(safeManager_ != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-safe-manager"); - require(saviourRegistry_ != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-saviour-registry"); - require(liquidityManager_ != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-liq-manager"); - require(lpToken_ != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-lp-token"); - require(minKeeperPayoutValue_ > 0, "NativeUnderlyingUniswapV2SafeSaviour/invalid-min-payout-value"); - - authorizedAccounts[msg.sender] = 1; - - isSystemCoinToken0 = isSystemCoinToken0_; - minKeeperPayoutValue = minKeeperPayoutValue_; - - coinJoin = CoinJoinLike(coinJoin_); - collateralJoin = CollateralJoinLike(collateralJoin_); - cRatioSetter = SaviourCRatioSetterLike(cRatioSetter_); - liquidationEngine = LiquidationEngineLike(liquidationEngine_); - taxCollector = TaxCollectorLike(taxCollector_); - oracleRelayer = OracleRelayerLike(oracleRelayer_); - systemCoinOrcl = PriceFeedLike(systemCoinOrcl_); - systemCoin = ERC20Like(coinJoin.systemCoin()); - safeEngine = SAFEEngineLike(coinJoin.safeEngine()); - safeManager = GebSafeManagerLike(safeManager_); - saviourRegistry = SAFESaviourRegistryLike(saviourRegistry_); - liquidityManager = UniswapLiquidityManagerLike(liquidityManager_); - lpToken = ERC20Like(lpToken_); - collateralToken = ERC20Like(collateralJoin.collateral()); - - systemCoinOrcl.getResultWithValidity(); - oracleRelayer.redemptionPrice(); - - require(collateralJoin.contractEnabled() == 1, "NativeUnderlyingUniswapV2SafeSaviour/join-disabled"); - require(address(collateralToken) != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-col-token"); - require(address(safeEngine) != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-safe-engine"); - require(address(systemCoin) != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-sys-coin"); - - emit AddAuthorization(msg.sender); - emit ModifyParameters("minKeeperPayoutValue", minKeeperPayoutValue); - emit ModifyParameters("oracleRelayer", oracleRelayer_); - emit ModifyParameters("taxCollector", taxCollector_); - emit ModifyParameters("systemCoinOrcl", systemCoinOrcl_); - emit ModifyParameters("liquidationEngine", liquidationEngine_); - emit ModifyParameters("liquidityManager", liquidityManager_); - } - - // --- Administration --- - /** - * @notice Modify an uint256 param - * @param parameter The name of the parameter - * @param val New value for the parameter - */ - function modifyParameters(bytes32 parameter, uint256 val) external isAuthorized { - if (parameter == "minKeeperPayoutValue") { - require(val > 0, "NativeUnderlyingUniswapV2SafeSaviour/null-min-payout"); - minKeeperPayoutValue = val; - } - else if (parameter == "restrictUsage") { - require(val <= 1, "NativeUnderlyingUniswapV2SafeSaviour/invalid-restriction"); - restrictUsage = val; - } - else revert("NativeUnderlyingUniswapV2SafeSaviour/modify-unrecognized-param"); - emit ModifyParameters(parameter, val); - } - /** - * @notice Modify an address param - * @param parameter The name of the parameter - * @param data New address for the parameter - */ - function modifyParameters(bytes32 parameter, address data) external isAuthorized { - require(data != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-data"); - - if (parameter == "systemCoinOrcl") { - systemCoinOrcl = PriceFeedLike(data); - systemCoinOrcl.getResultWithValidity(); - } - else if (parameter == "oracleRelayer") { - oracleRelayer = OracleRelayerLike(data); - oracleRelayer.redemptionPrice(); - } - else if (parameter == "liquidityManager") { - liquidityManager = UniswapLiquidityManagerLike(data); - } - else if (parameter == "liquidationEngine") { - liquidationEngine = LiquidationEngineLike(data); - } - else if (parameter == "taxCollector") { - taxCollector = TaxCollectorLike(data); - } - else revert("NativeUnderlyingUniswapV2SafeSaviour/modify-unrecognized-param"); - emit ModifyParameters(parameter, data); - } - - // --- Transferring Reserves --- - /* - * @notify Get back system coins or collateral tokens that were withdrawn from Uniswap and not used to save a specific SAFE - * @param safeID The ID of the safe that was previously saved and has leftover funds that can be withdrawn - * @param dst The address that will receive - */ - function getReserves(uint256 safeID, address dst) external controlsSAFE(msg.sender, safeID) nonReentrant { - address safeHandler = safeManager.safes(safeID); - (uint256 systemCoins, uint256 collateralCoins) = - (underlyingReserves[safeHandler].systemCoins, underlyingReserves[safeHandler].collateralCoins); - - require(either(systemCoins > 0, collateralCoins > 0), "NativeUnderlyingUniswapV2SafeSaviour/no-reserves"); - delete(underlyingReserves[safeManager.safes(safeID)]); - - if (systemCoins > 0) { - systemCoin.transfer(dst, systemCoins); - } - - if (collateralCoins > 0) { - collateralToken.transfer(dst, collateralCoins); - } - - emit GetReserves(msg.sender, safeHandler, systemCoins, collateralCoins, dst); - } - - // --- Adding/Withdrawing Cover --- - /* - * @notice Deposit lpToken in the contract in order to provide cover for a specific SAFE managed by the SAFE Manager - * @param safeID The ID of the SAFE to protect. This ID should be registered inside GebSafeManager - * @param lpTokenAmount The amount of collateralToken to deposit - */ - function deposit(uint256 safeID, uint256 lpTokenAmount) external isAllowed() liquidationEngineApproved(address(this)) nonReentrant { - require(lpTokenAmount > 0, "NativeUnderlyingUniswapV2SafeSaviour/null-lp-amount"); - - // Check that the SAFE exists inside GebSafeManager - address safeHandler = safeManager.safes(safeID); - require(safeHandler != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-handler"); - - // Check that the SAFE has debt - (, uint256 safeDebt) = - SAFEEngineLike(collateralJoin.safeEngine()).safes(collateralJoin.collateralType(), safeHandler); - require(safeDebt > 0, "NativeUnderlyingUniswapV2SafeSaviour/safe-does-not-have-debt"); - - // Update the lpToken balance used to cover the SAFE and transfer tokens to this contract - lpTokenCover[safeHandler] = add(lpTokenCover[safeHandler], lpTokenAmount); - require(lpToken.transferFrom(msg.sender, address(this), lpTokenAmount), "NativeUnderlyingUniswapV2SafeSaviour/could-not-transfer-lp"); - - emit Deposit(msg.sender, safeHandler, lpTokenAmount); - } - /* - * @notice Withdraw lpToken from the contract and provide less cover for a SAFE - * @dev Only an address that controls the SAFE inside the SAFE Manager can call this - * @param safeID The ID of the SAFE to remove cover from. This ID should be registered inside the SAFE Manager - * @param lpTokenAmount The amount of lpToken to withdraw - * @param dst The address that will receive the LP tokens - */ - function withdraw(uint256 safeID, uint256 lpTokenAmount, address dst) external controlsSAFE(msg.sender, safeID) nonReentrant { - require(lpTokenAmount > 0, "NativeUnderlyingUniswapV2SafeSaviour/null-lp-amount"); - - // Fetch the handler from the SAFE manager - address safeHandler = safeManager.safes(safeID); - require(lpTokenCover[safeHandler] >= lpTokenAmount, "NativeUnderlyingUniswapV2SafeSaviour/not-enough-to-withdraw"); - - // Withdraw cover and transfer collateralToken to the caller - lpTokenCover[safeHandler] = sub(lpTokenCover[safeHandler], lpTokenAmount); - lpToken.transfer(dst, lpTokenAmount); - - emit Withdraw(msg.sender, safeHandler, dst, lpTokenAmount); - } - - // --- Saving Logic --- - /* - * @notice Saves a SAFE by withdrawing liquidity and repaying debt and/or adding more collateral - * @dev Only the LiquidationEngine can call this - * @param keeper The keeper that called LiquidationEngine.liquidateSAFE and that should be rewarded for spending gas to save a SAFE - * @param collateralType The collateral type backing the SAFE that's being liquidated - * @param safeHandler The handler of the SAFE that's being liquidated - * @return Whether the SAFE has been saved, the amount of LP tokens that were used to withdraw liquidity as well as the amount of - * system coins sent to the keeper as their payment (this implementation always returns 0) - */ - function saveSAFE(address keeper, bytes32 collateralType, address safeHandler) override external returns (bool, uint256, uint256) { - require(address(liquidationEngine) == msg.sender, "NativeUnderlyingUniswapV2SafeSaviour/caller-not-liquidation-engine"); - require(keeper != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-keeper-address"); - - if (both(both(collateralType == "", safeHandler == address(0)), keeper == address(liquidationEngine))) { - return (true, uint(-1), uint(-1)); - } - - // Check that this is handling the correct collateral - require(collateralType == collateralJoin.collateralType(), "NativeUnderlyingUniswapV2SafeSaviour/invalid-collateral-type"); - - // Check that the SAFE has a non null amount of LP tokens covering it - require(lpTokenCover[safeHandler] > 0, "NativeUnderlyingUniswapV2SafeSaviour/null-cover"); - - // Tax the collateral - taxCollector.taxSingle(collateralType); - - // Get the amount of tokens used to top up the SAFE - (uint256 safeDebtRepaid, uint256 safeCollateralAdded) = - getTokensForSaving(safeHandler, oracleRelayer.redemptionPrice()); - - // There must be tokens used to save the SAVE - require(either(safeDebtRepaid > 0, safeCollateralAdded > 0), "NativeUnderlyingUniswapV2SafeSaviour/cannot-save-safe"); - - // Get the amounts of tokens sent to the keeper as payment - (uint256 keeperSysCoins, uint256 keeperCollateralCoins) = - getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), safeDebtRepaid, safeCollateralAdded); - - // There must be tokens that go to the keeper - require(either(keeperSysCoins > 0, keeperCollateralCoins > 0), "NativeUnderlyingUniswapV2SafeSaviour/cannot-pay-keeper"); - - // Store cover amount in local var - uint256 totalCover = lpTokenCover[safeHandler]; - delete(lpTokenCover[safeHandler]); - - // Mark the SAFE in the registry as just having been saved - saviourRegistry.markSave(collateralType, safeHandler); - - // Withdraw all liquidity - uint256 sysCoinBalance = systemCoin.balanceOf(address(this)); - uint256 collateralCoinBalance = collateralToken.balanceOf(address(this)); - - lpToken.approve(address(liquidityManager), totalCover); - liquidityManager.removeLiquidity(totalCover, 0, 0, address(this)); - - // Checks after removing liquidity - require( - either(systemCoin.balanceOf(address(this)) > sysCoinBalance, collateralToken.balanceOf(address(this)) > collateralCoinBalance), - "NativeUnderlyingUniswapV2SafeSaviour/faulty-remove-liquidity" - ); - - // Compute remaining balances of tokens that will go into reserves - sysCoinBalance = sub(sub(systemCoin.balanceOf(address(this)), sysCoinBalance), add(safeDebtRepaid, keeperSysCoins)); - collateralCoinBalance = sub( - sub(collateralToken.balanceOf(address(this)), collateralCoinBalance), add(safeCollateralAdded, keeperCollateralCoins) - ); - - // Update reserves - if (sysCoinBalance > 0) { - underlyingReserves[safeHandler].systemCoins = add( - underlyingReserves[safeHandler].systemCoins, sysCoinBalance - ); - } - if (collateralCoinBalance > 0) { - underlyingReserves[safeHandler].collateralCoins = add( - underlyingReserves[safeHandler].collateralCoins, collateralCoinBalance - ); - } - - // Save the SAFE - if (safeDebtRepaid > 0) { - // Approve the coin join contract to take system coins and repay debt - systemCoin.approve(address(coinJoin), safeDebtRepaid); - // Calculate the non adjusted system coin amount - uint256 nonAdjustedSystemCoinsToRepay = div(mul(safeDebtRepaid, RAY), getAccumulatedRate(collateralType)); - - // Join system coins in the system and repay the SAFE's debt - coinJoin.join(address(this), safeDebtRepaid); - safeEngine.modifySAFECollateralization( - collateralType, - safeHandler, - address(0), - address(this), - int256(0), - -int256(nonAdjustedSystemCoinsToRepay) - ); - } - - if (safeCollateralAdded > 0) { - // Approve collateralToken to the collateral join contract - collateralToken.approve(address(collateralJoin), safeCollateralAdded); - - // Join collateralToken in the system and add it in the saved SAFE - collateralJoin.join(address(this), safeCollateralAdded); - safeEngine.modifySAFECollateralization( - collateralType, - safeHandler, - address(this), - address(0), - int256(safeCollateralAdded), - int256(0) - ); - } - - // Pay keeper - if (keeperSysCoins > 0) { - systemCoin.transfer(keeper, keeperSysCoins); - } - - if (keeperCollateralCoins > 0) { - collateralToken.transfer(keeper, keeperCollateralCoins); - } - - // Emit an event - emit SaveSAFE(keeper, collateralType, safeHandler, totalCover); - - return (true, totalCover, 0); - } - - // --- Getters --- - /* - * @notify Must be implemented according to the interface although it always returns 0 - */ - function getKeeperPayoutValue() override public returns (uint256) { - return 0; - } - /* - * @notify Must be implemented according to the interface although it always returns false - */ - function keeperPayoutExceedsMinValue() override public returns (bool) { - return false; - } - /* - * @notice Determine whether a SAFE can be saved with the current amount of lpTokenCover deposited as cover for it - * @param safeHandler The handler of the SAFE which the function takes into account - * @return Whether the SAFE can be saved or not - */ - function canSave(bytes32, address safeHandler) override external returns (bool) { - // Fetch the redemption price first - uint256 redemptionPrice = oracleRelayer.redemptionPrice(); - - // Fetch the amount of tokens used to save the SAFE - (uint256 safeDebtRepaid, uint256 safeCollateralAdded) = - getTokensForSaving(safeHandler, redemptionPrice); - - // Fetch the amount of tokens sent to the keeper - (uint256 keeperSysCoins, uint256 keeperCollateralCoins) = - getKeeperPayoutTokens(safeHandler, redemptionPrice, safeDebtRepaid, safeCollateralAdded); - - // If there are some tokens used to repay the keeper, return true - if (both( - either(safeDebtRepaid > 0, safeCollateralAdded > 0), - either(keeperSysCoins > 0, keeperCollateralCoins > 0) - )) { - return true; - } - - return false; - } - /* - * @notice Return the total amount of LP tokens covering a specific SAFE - * @param collateralType The SAFE collateral type (ignored in this implementation) - * @param safeHandler The handler of the SAFE which the function takes into account - * @return The total LP token cover for a specific SAFE - */ - function tokenAmountUsedToSave(bytes32, address safeHandler) override public returns (uint256) { - return lpTokenCover[safeHandler]; - } - /* - * @notify Fetch the collateral's price - */ - function getCollateralPrice() public view returns (uint256) { - (address ethFSM,,) = oracleRelayer.collateralTypes(collateralJoin.collateralType()); - if (ethFSM == address(0)) return 0; - - (uint256 priceFeedValue, bool hasValidValue) = PriceFeedLike(ethFSM).getResultWithValidity(); - if (!hasValidValue) return 0; - - return priceFeedValue; - } - /* - * @notify Fetch the system coin's market price - */ - function getSystemCoinMarketPrice() public view returns (uint256) { - (uint256 priceFeedValue, bool hasValidValue) = systemCoinOrcl.getResultWithValidity(); - if (!hasValidValue) return 0; - - return priceFeedValue; - } - /* - * @notify Get the target collateralization ratio that a SAFE should have after it's saved - * @param safeHandler The handler/address of the SAFE whose target collateralization ratio is retrieved - */ - function getTargetCRatio(address safeHandler) public view returns (uint256) { - bytes32 collateralType = collateralJoin.collateralType(); - uint256 defaultCRatio = cRatioSetter.defaultDesiredCollateralizationRatios(collateralType); - uint256 targetCRatio = (cRatioSetter.desiredCollateralizationRatios(collateralType, safeHandler) == 0) ? - defaultCRatio : cRatioSetter.desiredCollateralizationRatios(collateralType, safeHandler); - return targetCRatio; - } - /* - * @notify Return the amount of system coins and collateral tokens retrieved from the LP position covering a specific SAFE - * @param safeHandler The handler/address of the targeted SAFE - */ - function getLPUnderlying(address safeHandler) public view returns (uint256, uint256) { - uint256 coverAmount = lpTokenCover[safeHandler]; - - if (coverAmount == 0) return (0, 0); - - (uint256 sysCoinsFromLP, uint256 collateralFromLP) = (isSystemCoinToken0) ? - (liquidityManager.getToken0FromLiquidity(coverAmount), liquidityManager.getToken1FromLiquidity(coverAmount)) : - (liquidityManager.getToken1FromLiquidity(coverAmount), liquidityManager.getToken0FromLiquidity(coverAmount)); - - return (sysCoinsFromLP, collateralFromLP); - } - /* - * @notice Return the amount of system coins and/or collateral tokens used to save a SAFE - * @param safeHandler The handler/address of the targeted SAFE - * @param redemptionPrice The system coin redemption price used in calculations - */ - function getTokensForSaving(address safeHandler, uint256 redemptionPrice) - public view returns (uint256, uint256) { - if (either(lpTokenCover[safeHandler] == 0, redemptionPrice == 0)) { - return (0, 0); - } - - // Get the default CRatio for the SAFE - (uint256 depositedCollateralToken, uint256 safeDebt) = - SAFEEngineLike(collateralJoin.safeEngine()).safes(collateralJoin.collateralType(), safeHandler); - uint256 targetCRatio = getTargetCRatio(safeHandler); - if (either(safeDebt == 0, targetCRatio == 0)) { - return (0, 0); - } - - // Get the collateral market price - uint256 collateralPrice = getCollateralPrice(); - if (collateralPrice == 0) { - return (0, 0); - } - - // Calculate how much debt would need to be repaid - uint256 debtToRepay = mul( - mul(HUNDRED, mul(depositedCollateralToken, collateralPrice) / WAD) / targetCRatio, RAY - ) / redemptionPrice; - - if (either(debtToRepay >= safeDebt, debtBelowFloor(collateralJoin.collateralType(), debtToRepay))) { - return (0, 0); - } - safeDebt = mul(safeDebt, getAccumulatedRate(collateralJoin.collateralType())) / RAY; - debtToRepay = sub(safeDebt, debtToRepay); - - // Calculate underlying amounts received from LP withdrawal - (uint256 sysCoinsFromLP, uint256 collateralFromLP) = getLPUnderlying(safeHandler); - - // Determine total debt to repay; return if the SAFE can be saved solely by repaying debt, continue calculations otherwise - if (sysCoinsFromLP >= debtToRepay) { - return (debtToRepay, 0); - } else { - // Calculate the amount of collateral that would need to be added to the SAFE - uint256 scaledDownDebtValue = mul(add(mul(redemptionPrice, sub(safeDebt, sysCoinsFromLP)) / RAY, ONE), targetCRatio) / HUNDRED; - - uint256 collateralTokenNeeded = div(mul(scaledDownDebtValue, WAD), collateralPrice); - collateralTokenNeeded = (depositedCollateralToken < collateralTokenNeeded) ? - sub(collateralTokenNeeded, depositedCollateralToken) : MAX_UINT; - - // See if there's enough collateral to add to the SAFE in order to save it - if (collateralTokenNeeded <= collateralFromLP) { - return (sysCoinsFromLP, collateralTokenNeeded); - } else { - return (0, 0); - } - } - } - /* - * @notice Return the amount of system coins and/or collateral tokens used to pay a keeper - * @param safeHandler The handler/address of the targeted SAFE - * @param redemptionPrice The system coin redemption price used in calculations - * @param safeDebtRepaid The amount of system coins that are already used to save the targeted SAFE - * @param safeCollateralAdded The amount of collateral tokens that are already used to save the targeted SAFE - */ - function getKeeperPayoutTokens(address safeHandler, uint256 redemptionPrice, uint256 safeDebtRepaid, uint256 safeCollateralAdded) - public view returns (uint256, uint256) { - // Get the system coin and collateral market prices - uint256 collateralPrice = getCollateralPrice(); - uint256 sysCoinMarketPrice = getSystemCoinMarketPrice(); - if (either(collateralPrice == 0, sysCoinMarketPrice == 0)) { - return (0, 0); - } - - // Calculate underlying amounts received from LP withdrawal - (uint256 sysCoinsFromLP, uint256 collateralFromLP) = getLPUnderlying(safeHandler); - - // Check if the keeper can get system coins and if yes, compute how many - uint256 keeperSysCoins; - if (sysCoinsFromLP > safeDebtRepaid) { - uint256 remainingSystemCoins = sub(sysCoinsFromLP, safeDebtRepaid); - uint256 payoutInSystemCoins = div(mul(minKeeperPayoutValue, WAD), sysCoinMarketPrice); - - if (payoutInSystemCoins <= remainingSystemCoins) { - return (payoutInSystemCoins, 0); - } else { - keeperSysCoins = remainingSystemCoins; - } - } - - // Calculate how much collateral the keeper will get - if (collateralFromLP <= safeCollateralAdded) return (0, 0); - - uint256 remainingCollateral = sub(collateralFromLP, safeCollateralAdded); - uint256 remainingKeeperPayoutValue = sub(minKeeperPayoutValue, mul(keeperSysCoins, sysCoinMarketPrice) / WAD); - uint256 collateralTokenNeeded = div(mul(remainingKeeperPayoutValue, WAD), collateralPrice); - - // If there are enough collateral tokens retreived from LP in order to pay the keeper, return the token amounts - if (collateralTokenNeeded <= remainingCollateral) { - return (keeperSysCoins, collateralTokenNeeded); - } else { - // Otherwise, return zeroes - return (0, 0); - } - } - /* - * @notify Returns whether a target debt amount is below the debt floor of a specific collateral type - * @param collateralType The collateral type whose floor we compare against - * @param targetDebtAmount The target debt amount for a SAFE that has collateralType collateral in it - */ - function debtBelowFloor(bytes32 collateralType, uint256 targetDebtAmount) public view returns (bool) { - (, , , , uint256 debtFloor, ) = safeEngine.collateralTypes(collateralType); - return (mul(targetDebtAmount, RAY) < debtFloor); - } - /* - * @notify Get the accumulated interest rate for a specific collateral type - * @param The collateral type for which to retrieve the rate - */ - function getAccumulatedRate(bytes32 collateralType) - public view returns (uint256 accumulatedRate) { - (, accumulatedRate, , , , ) = safeEngine.collateralTypes(collateralType); - } -} +// Copyright (C) 2021 Reflexer Labs, INC + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity 0.6.7; + +import "../interfaces/UniswapLiquidityManagerLike.sol"; +import "../interfaces/SaviourCRatioSetterLike.sol"; +import "../interfaces/SafeSaviourLike.sol"; +import "../math/SafeMath.sol"; + +contract NativeUnderlyingUniswapV2SafeSaviour is SafeMath, SafeSaviourLike { + // --- Auth --- + mapping (address => uint256) public authorizedAccounts; + /** + * @notice Add auth to an account + * @param account Account to add auth to + */ + function addAuthorization(address account) external isAuthorized { + authorizedAccounts[account] = 1; + emit AddAuthorization(account); + } + /** + * @notice Remove auth from an account + * @param account Account to remove auth from + */ + function removeAuthorization(address account) external isAuthorized { + authorizedAccounts[account] = 0; + emit RemoveAuthorization(account); + } + /** + * @notice Checks whether msg.sender can call an authed function + **/ + modifier isAuthorized { + require(authorizedAccounts[msg.sender] == 1, "NativeUnderlyingUniswapV2SafeSaviour/account-not-authorized"); + _; + } + + mapping (address => uint256) public allowedUsers; + /** + * @notice Allow a user to deposit assets + * @param usr User to whitelist + */ + function allowUser(address usr) external isAuthorized { + allowedUsers[usr] = 1; + emit AllowUser(usr); + } + /** + * @notice Disallow a user from depositing assets + * @param usr User to disallow + */ + function disallowUser(address usr) external isAuthorized { + allowedUsers[usr] = 0; + emit DisallowUser(usr); + } + /** + * @notice Checks whether an address is an allowed user + **/ + modifier isAllowed { + require( + either(restrictUsage == 0, both(restrictUsage == 1, allowedUsers[msg.sender] == 1)), + "NativeUnderlyingUniswapV2SafeSaviour/account-not-allowed" + ); + _; + } + + // --- Structs --- + struct Reserves { + uint256 systemCoins; + uint256 collateralCoins; + } + + // --- Variables --- + // Flag that tells whether usage of the contract is restricted to allowed users + uint256 public restrictUsage; + + // Whether the system coin is token0 in the Uniswap pool or not + bool public isSystemCoinToken0; + // Amount of LP tokens currently protecting each position + mapping(address => uint256) public lpTokenCover; + // Amount of system coin/collateral tokens that Safe owners can get back + mapping(address => Reserves) public underlyingReserves; + // Liquidity manager contract for Uniswap v2/v3 + UniswapLiquidityManagerLike public liquidityManager; + // The ERC20 system coin + ERC20Like public systemCoin; + // The system coin join contract + CoinJoinLike public coinJoin; + // The collateral join contract for adding collateral in the system + CollateralJoinLike public collateralJoin; + // The LP token + ERC20Like public lpToken; + // The collateral token + ERC20Like public collateralToken; + // Oracle providing the system coin price feed + PriceFeedLike public systemCoinOrcl; + // Contract that defines desired CRatios for each Safe after it is saved + SaviourCRatioSetterLike public cRatioSetter; + + // --- Events --- + event AddAuthorization(address account); + event RemoveAuthorization(address account); + event AllowUser(address usr); + event DisallowUser(address usr); + event ModifyParameters(bytes32 indexed parameter, uint256 val); + event ModifyParameters(bytes32 indexed parameter, address data); + event Deposit( + address indexed caller, + address indexed safeHandler, + uint256 lpTokenAmount + ); + event Withdraw( + address indexed caller, + address indexed safeHandler, + address dst, + uint256 lpTokenAmount + ); + event GetReserves( + address indexed caller, + address indexed safeHandler, + uint256 systemCoinAmount, + uint256 collateralAmount, + address dst + ); + + constructor( + bool isSystemCoinToken0_, + address coinJoin_, + address collateralJoin_, + address cRatioSetter_, + address systemCoinOrcl_, + address liquidationEngine_, + address taxCollector_, + address oracleRelayer_, + address safeManager_, + address saviourRegistry_, + address liquidityManager_, + address lpToken_, + uint256 minKeeperPayoutValue_ + ) public { + require(coinJoin_ != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-coin-join"); + require(collateralJoin_ != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-collateral-join"); + require(cRatioSetter_ != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-cratio-setter"); + require(systemCoinOrcl_ != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-system-coin-oracle"); + require(oracleRelayer_ != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-oracle-relayer"); + require(liquidationEngine_ != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-liquidation-engine"); + require(taxCollector_ != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-tax-collector"); + require(safeManager_ != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-safe-manager"); + require(saviourRegistry_ != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-saviour-registry"); + require(liquidityManager_ != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-liq-manager"); + require(lpToken_ != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-lp-token"); + require(minKeeperPayoutValue_ > 0, "NativeUnderlyingUniswapV2SafeSaviour/invalid-min-payout-value"); + + authorizedAccounts[msg.sender] = 1; + + isSystemCoinToken0 = isSystemCoinToken0_; + minKeeperPayoutValue = minKeeperPayoutValue_; + + coinJoin = CoinJoinLike(coinJoin_); + collateralJoin = CollateralJoinLike(collateralJoin_); + cRatioSetter = SaviourCRatioSetterLike(cRatioSetter_); + liquidationEngine = LiquidationEngineLike(liquidationEngine_); + taxCollector = TaxCollectorLike(taxCollector_); + oracleRelayer = OracleRelayerLike(oracleRelayer_); + systemCoinOrcl = PriceFeedLike(systemCoinOrcl_); + systemCoin = ERC20Like(coinJoin.systemCoin()); + safeEngine = SAFEEngineLike(coinJoin.safeEngine()); + safeManager = GebSafeManagerLike(safeManager_); + saviourRegistry = SAFESaviourRegistryLike(saviourRegistry_); + liquidityManager = UniswapLiquidityManagerLike(liquidityManager_); + lpToken = ERC20Like(lpToken_); + collateralToken = ERC20Like(collateralJoin.collateral()); + + systemCoinOrcl.getResultWithValidity(); + oracleRelayer.redemptionPrice(); + + require(collateralJoin.contractEnabled() == 1, "NativeUnderlyingUniswapV2SafeSaviour/join-disabled"); + require(address(collateralToken) != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-col-token"); + require(address(safeEngine) != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-safe-engine"); + require(address(systemCoin) != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-sys-coin"); + + emit AddAuthorization(msg.sender); + emit ModifyParameters("minKeeperPayoutValue", minKeeperPayoutValue); + emit ModifyParameters("oracleRelayer", oracleRelayer_); + emit ModifyParameters("taxCollector", taxCollector_); + emit ModifyParameters("systemCoinOrcl", systemCoinOrcl_); + emit ModifyParameters("liquidationEngine", liquidationEngine_); + emit ModifyParameters("liquidityManager", liquidityManager_); + } + + // --- Administration --- + /** + * @notice Modify an uint256 param + * @param parameter The name of the parameter + * @param val New value for the parameter + */ + function modifyParameters(bytes32 parameter, uint256 val) external isAuthorized { + if (parameter == "minKeeperPayoutValue") { + require(val > 0, "NativeUnderlyingUniswapV2SafeSaviour/null-min-payout"); + minKeeperPayoutValue = val; + } + else if (parameter == "restrictUsage") { + require(val <= 1, "NativeUnderlyingUniswapV2SafeSaviour/invalid-restriction"); + restrictUsage = val; + } + else revert("NativeUnderlyingUniswapV2SafeSaviour/modify-unrecognized-param"); + emit ModifyParameters(parameter, val); + } + /** + * @notice Modify an address param + * @param parameter The name of the parameter + * @param data New address for the parameter + */ + function modifyParameters(bytes32 parameter, address data) external isAuthorized { + require(data != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-data"); + + if (parameter == "systemCoinOrcl") { + systemCoinOrcl = PriceFeedLike(data); + systemCoinOrcl.getResultWithValidity(); + } + else if (parameter == "oracleRelayer") { + oracleRelayer = OracleRelayerLike(data); + oracleRelayer.redemptionPrice(); + } + else if (parameter == "liquidityManager") { + liquidityManager = UniswapLiquidityManagerLike(data); + } + else if (parameter == "liquidationEngine") { + liquidationEngine = LiquidationEngineLike(data); + } + else if (parameter == "taxCollector") { + taxCollector = TaxCollectorLike(data); + } + else revert("NativeUnderlyingUniswapV2SafeSaviour/modify-unrecognized-param"); + emit ModifyParameters(parameter, data); + } + + // --- Transferring Reserves --- + /* + * @notify Get back system coins or collateral tokens that were withdrawn from Uniswap and not used to save a specific SAFE + * @param safeID The ID of the safe that was previously saved and has leftover funds that can be withdrawn + * @param dst The address that will receive + */ + function getReserves(uint256 safeID, address dst) external controlsSAFE(msg.sender, safeID) nonReentrant { + address safeHandler = safeManager.safes(safeID); + (uint256 systemCoins, uint256 collateralCoins) = + (underlyingReserves[safeHandler].systemCoins, underlyingReserves[safeHandler].collateralCoins); + + require(either(systemCoins > 0, collateralCoins > 0), "NativeUnderlyingUniswapV2SafeSaviour/no-reserves"); + delete(underlyingReserves[safeManager.safes(safeID)]); + + if (systemCoins > 0) { + systemCoin.transfer(dst, systemCoins); + } + + if (collateralCoins > 0) { + collateralToken.transfer(dst, collateralCoins); + } + + emit GetReserves(msg.sender, safeHandler, systemCoins, collateralCoins, dst); + } + + // --- Adding/Withdrawing Cover --- + /* + * @notice Deposit lpToken in the contract in order to provide cover for a specific SAFE managed by the SAFE Manager + * @param safeID The ID of the SAFE to protect. This ID should be registered inside GebSafeManager + * @param lpTokenAmount The amount of collateralToken to deposit + */ + function deposit(uint256 safeID, uint256 lpTokenAmount) external isAllowed() liquidationEngineApproved(address(this)) nonReentrant { + require(lpTokenAmount > 0, "NativeUnderlyingUniswapV2SafeSaviour/null-lp-amount"); + + // Check that the SAFE exists inside GebSafeManager + address safeHandler = safeManager.safes(safeID); + require(safeHandler != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-handler"); + + // Check that the SAFE has debt + (, uint256 safeDebt) = + SAFEEngineLike(collateralJoin.safeEngine()).safes(collateralJoin.collateralType(), safeHandler); + require(safeDebt > 0, "NativeUnderlyingUniswapV2SafeSaviour/safe-does-not-have-debt"); + + // Update the lpToken balance used to cover the SAFE and transfer tokens to this contract + lpTokenCover[safeHandler] = add(lpTokenCover[safeHandler], lpTokenAmount); + require(lpToken.transferFrom(msg.sender, address(this), lpTokenAmount), "NativeUnderlyingUniswapV2SafeSaviour/could-not-transfer-lp"); + + emit Deposit(msg.sender, safeHandler, lpTokenAmount); + } + /* + * @notice Withdraw lpToken from the contract and provide less cover for a SAFE + * @dev Only an address that controls the SAFE inside the SAFE Manager can call this + * @param safeID The ID of the SAFE to remove cover from. This ID should be registered inside the SAFE Manager + * @param lpTokenAmount The amount of lpToken to withdraw + * @param dst The address that will receive the LP tokens + */ + function withdraw(uint256 safeID, uint256 lpTokenAmount, address dst) external controlsSAFE(msg.sender, safeID) nonReentrant { + require(lpTokenAmount > 0, "NativeUnderlyingUniswapV2SafeSaviour/null-lp-amount"); + + // Fetch the handler from the SAFE manager + address safeHandler = safeManager.safes(safeID); + require(lpTokenCover[safeHandler] >= lpTokenAmount, "NativeUnderlyingUniswapV2SafeSaviour/not-enough-to-withdraw"); + + // Withdraw cover and transfer collateralToken to the caller + lpTokenCover[safeHandler] = sub(lpTokenCover[safeHandler], lpTokenAmount); + lpToken.transfer(dst, lpTokenAmount); + + emit Withdraw(msg.sender, safeHandler, dst, lpTokenAmount); + } + + // --- Saving Logic --- + /* + * @notice Saves a SAFE by withdrawing liquidity and repaying debt and/or adding more collateral + * @dev Only the LiquidationEngine can call this + * @param keeper The keeper that called LiquidationEngine.liquidateSAFE and that should be rewarded for spending gas to save a SAFE + * @param collateralType The collateral type backing the SAFE that's being liquidated + * @param safeHandler The handler of the SAFE that's being liquidated + * @return Whether the SAFE has been saved, the amount of LP tokens that were used to withdraw liquidity as well as the amount of + * system coins sent to the keeper as their payment (this implementation always returns 0) + */ + function saveSAFE(address keeper, bytes32 collateralType, address safeHandler) override external returns (bool, uint256, uint256) { + require(address(liquidationEngine) == msg.sender, "NativeUnderlyingUniswapV2SafeSaviour/caller-not-liquidation-engine"); + require(keeper != address(0), "NativeUnderlyingUniswapV2SafeSaviour/null-keeper-address"); + + if (both(both(collateralType == "", safeHandler == address(0)), keeper == address(liquidationEngine))) { + return (true, uint(-1), uint(-1)); + } + + // Check that this is handling the correct collateral + require(collateralType == collateralJoin.collateralType(), "NativeUnderlyingUniswapV2SafeSaviour/invalid-collateral-type"); + + // Check that the SAFE has a non null amount of LP tokens covering it + require(lpTokenCover[safeHandler] > 0, "NativeUnderlyingUniswapV2SafeSaviour/null-cover"); + + // Tax the collateral + taxCollector.taxSingle(collateralType); + + // Get the amount of tokens used to top up the SAFE + (uint256 safeDebtRepaid, uint256 safeCollateralAdded) = + getTokensForSaving(safeHandler, oracleRelayer.redemptionPrice()); + + // There must be tokens used to save the SAVE + require(either(safeDebtRepaid > 0, safeCollateralAdded > 0), "NativeUnderlyingUniswapV2SafeSaviour/cannot-save-safe"); + + // Get the amounts of tokens sent to the keeper as payment + (uint256 keeperSysCoins, uint256 keeperCollateralCoins) = + getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), safeDebtRepaid, safeCollateralAdded); + + // There must be tokens that go to the keeper + require(either(keeperSysCoins > 0, keeperCollateralCoins > 0), "NativeUnderlyingUniswapV2SafeSaviour/cannot-pay-keeper"); + + // Store cover amount in local var + uint256 totalCover = lpTokenCover[safeHandler]; + delete(lpTokenCover[safeHandler]); + + // Mark the SAFE in the registry as just having been saved + saviourRegistry.markSave(collateralType, safeHandler); + + // Withdraw all liquidity + uint256 sysCoinBalance = systemCoin.balanceOf(address(this)); + uint256 collateralCoinBalance = collateralToken.balanceOf(address(this)); + + lpToken.approve(address(liquidityManager), totalCover); + liquidityManager.removeLiquidity(totalCover, 0, 0, address(this)); + + // Checks after removing liquidity + require( + either(systemCoin.balanceOf(address(this)) > sysCoinBalance, collateralToken.balanceOf(address(this)) > collateralCoinBalance), + "NativeUnderlyingUniswapV2SafeSaviour/faulty-remove-liquidity" + ); + + // Compute remaining balances of tokens that will go into reserves + sysCoinBalance = sub(sub(systemCoin.balanceOf(address(this)), sysCoinBalance), add(safeDebtRepaid, keeperSysCoins)); + collateralCoinBalance = sub( + sub(collateralToken.balanceOf(address(this)), collateralCoinBalance), add(safeCollateralAdded, keeperCollateralCoins) + ); + + // Update reserves + if (sysCoinBalance > 0) { + underlyingReserves[safeHandler].systemCoins = add( + underlyingReserves[safeHandler].systemCoins, sysCoinBalance + ); + } + if (collateralCoinBalance > 0) { + underlyingReserves[safeHandler].collateralCoins = add( + underlyingReserves[safeHandler].collateralCoins, collateralCoinBalance + ); + } + + // Save the SAFE + if (safeDebtRepaid > 0) { + // Approve the coin join contract to take system coins and repay debt + systemCoin.approve(address(coinJoin), safeDebtRepaid); + // Calculate the non adjusted system coin amount + uint256 nonAdjustedSystemCoinsToRepay = div(mul(safeDebtRepaid, RAY), getAccumulatedRate(collateralType)); + + // Join system coins in the system and repay the SAFE's debt + coinJoin.join(address(this), safeDebtRepaid); + safeEngine.modifySAFECollateralization( + collateralType, + safeHandler, + address(0), + address(this), + int256(0), + -int256(nonAdjustedSystemCoinsToRepay) + ); + } + + if (safeCollateralAdded > 0) { + // Approve collateralToken to the collateral join contract + collateralToken.approve(address(collateralJoin), safeCollateralAdded); + + // Join collateralToken in the system and add it in the saved SAFE + collateralJoin.join(address(this), safeCollateralAdded); + safeEngine.modifySAFECollateralization( + collateralType, + safeHandler, + address(this), + address(0), + int256(safeCollateralAdded), + int256(0) + ); + } + + // Pay keeper + if (keeperSysCoins > 0) { + systemCoin.transfer(keeper, keeperSysCoins); + } + + if (keeperCollateralCoins > 0) { + collateralToken.transfer(keeper, keeperCollateralCoins); + } + + // Emit an event + emit SaveSAFE(keeper, collateralType, safeHandler, totalCover); + + return (true, totalCover, 0); + } + + // --- Getters --- + /* + * @notify Must be implemented according to the interface although it always returns 0 + */ + function getKeeperPayoutValue() override public returns (uint256) { + return 0; + } + /* + * @notify Must be implemented according to the interface although it always returns false + */ + function keeperPayoutExceedsMinValue() override public returns (bool) { + return false; + } + /* + * @notice Determine whether a SAFE can be saved with the current amount of lpTokenCover deposited as cover for it + * @param safeHandler The handler of the SAFE which the function takes into account + * @return Whether the SAFE can be saved or not + */ + function canSave(bytes32, address safeHandler) override external returns (bool) { + // Fetch the redemption price first + uint256 redemptionPrice = oracleRelayer.redemptionPrice(); + + // Fetch the amount of tokens used to save the SAFE + (uint256 safeDebtRepaid, uint256 safeCollateralAdded) = + getTokensForSaving(safeHandler, redemptionPrice); + + // Fetch the amount of tokens sent to the keeper + (uint256 keeperSysCoins, uint256 keeperCollateralCoins) = + getKeeperPayoutTokens(safeHandler, redemptionPrice, safeDebtRepaid, safeCollateralAdded); + + // If there are some tokens used to repay the keeper, return true + if (both( + either(safeDebtRepaid > 0, safeCollateralAdded > 0), + either(keeperSysCoins > 0, keeperCollateralCoins > 0) + )) { + return true; + } + + return false; + } + /* + * @notice Return the total amount of LP tokens covering a specific SAFE + * @param collateralType The SAFE collateral type (ignored in this implementation) + * @param safeHandler The handler of the SAFE which the function takes into account + * @return The total LP token cover for a specific SAFE + */ + function tokenAmountUsedToSave(bytes32, address safeHandler) override public returns (uint256) { + return lpTokenCover[safeHandler]; + } + /* + * @notify Fetch the collateral's price + */ + function getCollateralPrice() public view returns (uint256) { + (address ethFSM,,) = oracleRelayer.collateralTypes(collateralJoin.collateralType()); + if (ethFSM == address(0)) return 0; + + (uint256 priceFeedValue, bool hasValidValue) = PriceFeedLike(ethFSM).getResultWithValidity(); + if (!hasValidValue) return 0; + + return priceFeedValue; + } + /* + * @notify Fetch the system coin's market price + */ + function getSystemCoinMarketPrice() public view returns (uint256) { + (uint256 priceFeedValue, bool hasValidValue) = systemCoinOrcl.getResultWithValidity(); + if (!hasValidValue) return 0; + + return priceFeedValue; + } + /* + * @notify Get the target collateralization ratio that a SAFE should have after it's saved + * @param safeHandler The handler/address of the SAFE whose target collateralization ratio is retrieved + */ + function getTargetCRatio(address safeHandler) public view returns (uint256) { + bytes32 collateralType = collateralJoin.collateralType(); + uint256 defaultCRatio = cRatioSetter.defaultDesiredCollateralizationRatios(collateralType); + uint256 targetCRatio = (cRatioSetter.desiredCollateralizationRatios(collateralType, safeHandler) == 0) ? + defaultCRatio : cRatioSetter.desiredCollateralizationRatios(collateralType, safeHandler); + return targetCRatio; + } + /* + * @notify Return the amount of system coins and collateral tokens retrieved from the LP position covering a specific SAFE + * @param safeHandler The handler/address of the targeted SAFE + */ + function getLPUnderlying(address safeHandler) public view returns (uint256, uint256) { + uint256 coverAmount = lpTokenCover[safeHandler]; + + if (coverAmount == 0) return (0, 0); + + (uint256 sysCoinsFromLP, uint256 collateralFromLP) = (isSystemCoinToken0) ? + (liquidityManager.getToken0FromLiquidity(coverAmount), liquidityManager.getToken1FromLiquidity(coverAmount)) : + (liquidityManager.getToken1FromLiquidity(coverAmount), liquidityManager.getToken0FromLiquidity(coverAmount)); + + return (sysCoinsFromLP, collateralFromLP); + } + /* + * @notice Return the amount of system coins and/or collateral tokens used to save a SAFE + * @param safeHandler The handler/address of the targeted SAFE + * @param redemptionPrice The system coin redemption price used in calculations + */ + function getTokensForSaving(address safeHandler, uint256 redemptionPrice) + public view returns (uint256, uint256) { + if (either(lpTokenCover[safeHandler] == 0, redemptionPrice == 0)) { + return (0, 0); + } + + // Get the default CRatio for the SAFE + (uint256 depositedCollateralToken, uint256 safeDebt) = + SAFEEngineLike(collateralJoin.safeEngine()).safes(collateralJoin.collateralType(), safeHandler); + uint256 targetCRatio = getTargetCRatio(safeHandler); + if (either(safeDebt == 0, targetCRatio == 0)) { + return (0, 0); + } + + // Get the collateral market price + uint256 collateralPrice = getCollateralPrice(); + if (collateralPrice == 0) { + return (0, 0); + } + + // Calculate how much debt would need to be repaid + uint256 debtToRepay = mul( + mul(HUNDRED, mul(depositedCollateralToken, collateralPrice) / WAD) / targetCRatio, RAY + ) / redemptionPrice; + + if (either(debtToRepay >= safeDebt, debtBelowFloor(collateralJoin.collateralType(), debtToRepay))) { + return (0, 0); + } + safeDebt = mul(safeDebt, getAccumulatedRate(collateralJoin.collateralType())) / RAY; + debtToRepay = sub(safeDebt, debtToRepay); + + // Calculate underlying amounts received from LP withdrawal + (uint256 sysCoinsFromLP, uint256 collateralFromLP) = getLPUnderlying(safeHandler); + + // Determine total debt to repay; return if the SAFE can be saved solely by repaying debt, continue calculations otherwise + if (sysCoinsFromLP >= debtToRepay) { + return (debtToRepay, 0); + } else { + // Calculate the amount of collateral that would need to be added to the SAFE + uint256 scaledDownDebtValue = mul(add(mul(redemptionPrice, sub(safeDebt, sysCoinsFromLP)) / RAY, ONE), targetCRatio) / HUNDRED; + + uint256 collateralTokenNeeded = div(mul(scaledDownDebtValue, WAD), collateralPrice); + collateralTokenNeeded = (depositedCollateralToken < collateralTokenNeeded) ? + sub(collateralTokenNeeded, depositedCollateralToken) : MAX_UINT; + + // See if there's enough collateral to add to the SAFE in order to save it + if (collateralTokenNeeded <= collateralFromLP) { + return (sysCoinsFromLP, collateralTokenNeeded); + } else { + return (0, 0); + } + } + } + /* + * @notice Return the amount of system coins and/or collateral tokens used to pay a keeper + * @param safeHandler The handler/address of the targeted SAFE + * @param redemptionPrice The system coin redemption price used in calculations + * @param safeDebtRepaid The amount of system coins that are already used to save the targeted SAFE + * @param safeCollateralAdded The amount of collateral tokens that are already used to save the targeted SAFE + */ + function getKeeperPayoutTokens(address safeHandler, uint256 redemptionPrice, uint256 safeDebtRepaid, uint256 safeCollateralAdded) + public view returns (uint256, uint256) { + // Get the system coin and collateral market prices + uint256 collateralPrice = getCollateralPrice(); + uint256 sysCoinMarketPrice = getSystemCoinMarketPrice(); + if (either(collateralPrice == 0, sysCoinMarketPrice == 0)) { + return (0, 0); + } + + // Calculate underlying amounts received from LP withdrawal + (uint256 sysCoinsFromLP, uint256 collateralFromLP) = getLPUnderlying(safeHandler); + + // Check if the keeper can get system coins and if yes, compute how many + uint256 keeperSysCoins; + if (sysCoinsFromLP > safeDebtRepaid) { + uint256 remainingSystemCoins = sub(sysCoinsFromLP, safeDebtRepaid); + uint256 payoutInSystemCoins = div(mul(minKeeperPayoutValue, WAD), sysCoinMarketPrice); + + if (payoutInSystemCoins <= remainingSystemCoins) { + return (payoutInSystemCoins, 0); + } else { + keeperSysCoins = remainingSystemCoins; + } + } + + // Calculate how much collateral the keeper will get + if (collateralFromLP <= safeCollateralAdded) return (0, 0); + + uint256 remainingCollateral = sub(collateralFromLP, safeCollateralAdded); + uint256 remainingKeeperPayoutValue = sub(minKeeperPayoutValue, mul(keeperSysCoins, sysCoinMarketPrice) / WAD); + uint256 collateralTokenNeeded = div(mul(remainingKeeperPayoutValue, WAD), collateralPrice); + + // If there are enough collateral tokens retreived from LP in order to pay the keeper, return the token amounts + if (collateralTokenNeeded <= remainingCollateral) { + return (keeperSysCoins, collateralTokenNeeded); + } else { + // Otherwise, return zeroes + return (0, 0); + } + } + /* + * @notify Returns whether a target debt amount is below the debt floor of a specific collateral type + * @param collateralType The collateral type whose floor we compare against + * @param targetDebtAmount The target debt amount for a SAFE that has collateralType collateral in it + */ + function debtBelowFloor(bytes32 collateralType, uint256 targetDebtAmount) public view returns (bool) { + (, , , , uint256 debtFloor, ) = safeEngine.collateralTypes(collateralType); + return (mul(targetDebtAmount, RAY) < debtFloor); + } + /* + * @notify Get the accumulated interest rate for a specific collateral type + * @param The collateral type for which to retrieve the rate + */ + function getAccumulatedRate(bytes32 collateralType) + public view returns (uint256 accumulatedRate) { + (, accumulatedRate, , , , ) = safeEngine.collateralTypes(collateralType); + } +} diff --git a/src/saviours/NativeUnderlyingUniswapV3SafeSaviour.sol b/src/saviours/NativeUnderlyingUniswapV3SafeSaviour.sol index 8b57bce..46dfebb 100644 --- a/src/saviours/NativeUnderlyingUniswapV3SafeSaviour.sol +++ b/src/saviours/NativeUnderlyingUniswapV3SafeSaviour.sol @@ -1,656 +1,656 @@ -// Copyright (C) 2021 Reflexer Labs, INC - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -pragma solidity 0.6.7; - -import "../interfaces/UniswapLiquidityManagerLike.sol"; -import "../interfaces/SaviourCRatioSetterLike.sol"; -import "../interfaces/SafeSaviourLike.sol"; -import "../math/SafeMath.sol"; - -contract NativeUnderlyingUniswapV3SafeSaviour is SafeMath, SafeSaviourLike { - // --- Auth --- - mapping (address => uint256) public authorizedAccounts; - /** - * @notice Add auth to an account - * @param account Account to add auth to - */ - function addAuthorization(address account) external isAuthorized { - authorizedAccounts[account] = 1; - emit AddAuthorization(account); - } - /** - * @notice Remove auth from an account - * @param account Account to remove auth from - */ - function removeAuthorization(address account) external isAuthorized { - authorizedAccounts[account] = 0; - emit RemoveAuthorization(account); - } - /** - * @notice Checks whether msg.sender can call an authed function - **/ - modifier isAuthorized { - require(authorizedAccounts[msg.sender] == 1, "NativeUnderlyingUniswapV3SafeSaviour/account-not-authorized"); - _; - } - - mapping (address => uint256) public allowedUsers; - /** - * @notice Allow a user to deposit assets - * @param usr User to whitelist - */ - function allowUser(address usr) external isAuthorized { - allowedUsers[usr] = 1; - emit AllowUser(usr); - } - /** - * @notice Disallow a user from depositing assets - * @param usr User to disallow - */ - function disallowUser(address usr) external isAuthorized { - allowedUsers[usr] = 0; - emit DisallowUser(usr); - } - /** - * @notice Checks whether an address is an allowed user - **/ - modifier isAllowed { - require( - either(restrictUsage == 0, both(restrictUsage == 1, allowedUsers[msg.sender] == 1)), - "NativeUnderlyingUniswapV3SafeSaviour/account-not-allowed" - ); - _; - } - - // --- Structs --- - struct Reserves { - uint256 systemCoins; - uint256 collateralCoins; - } - - // --- Variables --- - // Flag that tells whether usage of the contract is restricted to allowed users - uint256 public restrictUsage; - - // Whether the system coin is token0 in the Uniswap pool or not - bool public isSystemCoinToken0; - // Amount of LP tokens currently protecting each position - mapping(address => uint256) public lpTokenCover; - // Amount of system coin/collateral tokens that Safe owners can get back - mapping(address => Reserves) public underlyingReserves; - // Liquidity manager contract for Uniswap v2/v3 - UniswapLiquidityManagerLike public liquidityManager; - // The ERC20 system coin - ERC20Like public systemCoin; - // The system coin join contract - CoinJoinLike public coinJoin; - // The collateral join contract for adding collateral in the system - CollateralJoinLike public collateralJoin; - // The LP token - ERC20Like public lpToken; - // The collateral token - ERC20Like public collateralToken; - // Oracle providing the system coin price feed - PriceFeedLike public systemCoinOrcl; - // Contract that defines desired CRatios for each Safe after it is saved - SaviourCRatioSetterLike public cRatioSetter; - - // --- Events --- - event AddAuthorization(address account); - event RemoveAuthorization(address account); - event AllowUser(address usr); - event DisallowUser(address usr); - event ModifyParameters(bytes32 indexed parameter, uint256 val); - event ModifyParameters(bytes32 indexed parameter, address data); - event Deposit( - address indexed caller, - address indexed safeHandler, - uint256 lpTokenAmount - ); - event Withdraw( - address indexed caller, - address indexed safeHandler, - address dst, - uint256 lpTokenAmount - ); - event GetReserves( - address indexed caller, - address indexed safeHandler, - uint256 systemCoinAmount, - uint256 collateralAmount, - address dst - ); - - constructor( - bool isSystemCoinToken0_, - address coinJoin_, - address collateralJoin_, - address cRatioSetter_, - address systemCoinOrcl_, - address liquidationEngine_, - address taxCollector_, - address oracleRelayer_, - address safeManager_, - address saviourRegistry_, - address liquidityManager_, - address lpToken_, - uint256 minKeeperPayoutValue_ - ) public { - require(coinJoin_ != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-coin-join"); - require(collateralJoin_ != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-collateral-join"); - require(cRatioSetter_ != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-cratio-setter"); - require(systemCoinOrcl_ != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-system-coin-oracle"); - require(oracleRelayer_ != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-oracle-relayer"); - require(liquidationEngine_ != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-liquidation-engine"); - require(taxCollector_ != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-tax-collector"); - require(safeManager_ != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-safe-manager"); - require(saviourRegistry_ != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-saviour-registry"); - require(liquidityManager_ != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-liq-manager"); - require(lpToken_ != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-lp-token"); - require(minKeeperPayoutValue_ > 0, "NativeUnderlyingUniswapV3SafeSaviour/invalid-min-payout-value"); - - authorizedAccounts[msg.sender] = 1; - - isSystemCoinToken0 = isSystemCoinToken0_; - minKeeperPayoutValue = minKeeperPayoutValue_; - - coinJoin = CoinJoinLike(coinJoin_); - collateralJoin = CollateralJoinLike(collateralJoin_); - cRatioSetter = SaviourCRatioSetterLike(cRatioSetter_); - liquidationEngine = LiquidationEngineLike(liquidationEngine_); - taxCollector = TaxCollectorLike(taxCollector_); - oracleRelayer = OracleRelayerLike(oracleRelayer_); - systemCoinOrcl = PriceFeedLike(systemCoinOrcl_); - systemCoin = ERC20Like(coinJoin.systemCoin()); - safeEngine = SAFEEngineLike(coinJoin.safeEngine()); - safeManager = GebSafeManagerLike(safeManager_); - saviourRegistry = SAFESaviourRegistryLike(saviourRegistry_); - liquidityManager = UniswapLiquidityManagerLike(liquidityManager_); - lpToken = ERC20Like(lpToken_); - collateralToken = ERC20Like(collateralJoin.collateral()); - - systemCoinOrcl.getResultWithValidity(); - oracleRelayer.redemptionPrice(); - - require(collateralJoin.contractEnabled() == 1, "NativeUnderlyingUniswapV3SafeSaviour/join-disabled"); - require(address(collateralToken) != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-col-token"); - require(address(safeEngine) != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-safe-engine"); - require(address(systemCoin) != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-sys-coin"); - - emit AddAuthorization(msg.sender); - emit ModifyParameters("minKeeperPayoutValue", minKeeperPayoutValue); - emit ModifyParameters("oracleRelayer", oracleRelayer_); - emit ModifyParameters("taxCollector", taxCollector_); - emit ModifyParameters("systemCoinOrcl", systemCoinOrcl_); - emit ModifyParameters("liquidationEngine", liquidationEngine_); - emit ModifyParameters("liquidityManager", liquidityManager_); - } - - // --- Administration --- - /** - * @notice Modify an uint256 param - * @param parameter The name of the parameter - * @param val New value for the parameter - */ - function modifyParameters(bytes32 parameter, uint256 val) external isAuthorized { - if (parameter == "minKeeperPayoutValue") { - require(val > 0, "NativeUnderlyingUniswapV3SafeSaviour/null-min-payout"); - minKeeperPayoutValue = val; - } - else if (parameter == "restrictUsage") { - require(val <= 1, "NativeUnderlyingUniswapV3SafeSaviour/invalid-restriction"); - restrictUsage = val; - } - else revert("NativeUnderlyingUniswapV3SafeSaviour/modify-unrecognized-param"); - emit ModifyParameters(parameter, val); - } - /** - * @notice Modify an address param - * @param parameter The name of the parameter - * @param data New address for the parameter - */ - function modifyParameters(bytes32 parameter, address data) external isAuthorized { - require(data != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-data"); - - if (parameter == "systemCoinOrcl") { - systemCoinOrcl = PriceFeedLike(data); - systemCoinOrcl.getResultWithValidity(); - } - else if (parameter == "oracleRelayer") { - oracleRelayer = OracleRelayerLike(data); - oracleRelayer.redemptionPrice(); - } - else if (parameter == "liquidityManager") { - liquidityManager = UniswapLiquidityManagerLike(data); - } - else if (parameter == "liquidationEngine") { - liquidationEngine = LiquidationEngineLike(data); - } - else if (parameter == "taxCollector") { - taxCollector = TaxCollectorLike(data); - } - else revert("NativeUnderlyingUniswapV3SafeSaviour/modify-unrecognized-param"); - emit ModifyParameters(parameter, data); - } - - // --- Transferring Reserves --- - /* - * @notify Get back system coins or collateral tokens that were withdrawn from Uniswap and not used to save a specific SAFE - * @param safeID The ID of the safe that was previously saved and has leftover funds that can be withdrawn - * @param dst The address that will receive - */ - function getReserves(uint256 safeID, address dst) external controlsSAFE(msg.sender, safeID) nonReentrant { - address safeHandler = safeManager.safes(safeID); - (uint256 systemCoins, uint256 collateralCoins) = - (underlyingReserves[safeHandler].systemCoins, underlyingReserves[safeHandler].collateralCoins); - - require(either(systemCoins > 0, collateralCoins > 0), "NativeUnderlyingUniswapV3SafeSaviour/no-reserves"); - delete(underlyingReserves[safeManager.safes(safeID)]); - - if (systemCoins > 0) { - systemCoin.transfer(dst, systemCoins); - } - - if (collateralCoins > 0) { - collateralToken.transfer(dst, collateralCoins); - } - - emit GetReserves(msg.sender, safeHandler, systemCoins, collateralCoins, dst); - } - - // --- Adding/Withdrawing Cover --- - /* - * @notice Deposit lpToken in the contract in order to provide cover for a specific SAFE managed by the SAFE Manager - * @param safeID The ID of the SAFE to protect. This ID should be registered inside GebSafeManager - * @param lpTokenAmount The amount of collateralToken to deposit - */ - function deposit(uint256 safeID, uint256 lpTokenAmount) external isAllowed() liquidationEngineApproved(address(this)) nonReentrant { - require(lpTokenAmount > 0, "NativeUnderlyingUniswapV3SafeSaviour/null-lp-amount"); - - // Check that the SAFE exists inside GebSafeManager - address safeHandler = safeManager.safes(safeID); - require(safeHandler != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-handler"); - - // Check that the SAFE has debt - (, uint256 safeDebt) = - SAFEEngineLike(collateralJoin.safeEngine()).safes(collateralJoin.collateralType(), safeHandler); - require(safeDebt > 0, "NativeUnderlyingUniswapV3SafeSaviour/safe-does-not-have-debt"); - - // Update the lpToken balance used to cover the SAFE and transfer tokens to this contract - lpTokenCover[safeHandler] = add(lpTokenCover[safeHandler], lpTokenAmount); - require(lpToken.transferFrom(msg.sender, address(this), lpTokenAmount), "NativeUnderlyingUniswapV3SafeSaviour/could-not-transfer-lp"); - - emit Deposit(msg.sender, safeHandler, lpTokenAmount); - } - /* - * @notice Withdraw lpToken from the contract and provide less cover for a SAFE - * @dev Only an address that controls the SAFE inside the SAFE Manager can call this - * @param safeID The ID of the SAFE to remove cover from. This ID should be registered inside the SAFE Manager - * @param lpTokenAmount The amount of lpToken to withdraw - * @param dst The address that will receive the LP tokens - */ - function withdraw(uint256 safeID, uint256 lpTokenAmount, address dst) external controlsSAFE(msg.sender, safeID) nonReentrant { - require(lpTokenAmount > 0, "NativeUnderlyingUniswapV3SafeSaviour/null-lp-amount"); - - // Fetch the handler from the SAFE manager - address safeHandler = safeManager.safes(safeID); - require(lpTokenCover[safeHandler] >= lpTokenAmount, "NativeUnderlyingUniswapV3SafeSaviour/not-enough-to-withdraw"); - - // Withdraw cover and transfer collateralToken to the caller - lpTokenCover[safeHandler] = sub(lpTokenCover[safeHandler], lpTokenAmount); - lpToken.transfer(dst, lpTokenAmount); - - emit Withdraw(msg.sender, safeHandler, dst, lpTokenAmount); - } - - // --- Saving Logic --- - /* - * @notice Saves a SAFE by withdrawing liquidity and repaying debt and/or adding more collateral - * @dev Only the LiquidationEngine can call this - * @param keeper The keeper that called LiquidationEngine.liquidateSAFE and that should be rewarded for spending gas to save a SAFE - * @param collateralType The collateral type backing the SAFE that's being liquidated - * @param safeHandler The handler of the SAFE that's being liquidated - * @return Whether the SAFE has been saved, the amount of LP tokens that were used to withdraw liquidity as well as the amount of - * system coins sent to the keeper as their payment (this implementation always returns 0) - */ - function saveSAFE(address keeper, bytes32 collateralType, address safeHandler) override external returns (bool, uint256, uint256) { - require(address(liquidationEngine) == msg.sender, "NativeUnderlyingUniswapV3SafeSaviour/caller-not-liquidation-engine"); - require(keeper != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-keeper-address"); - - if (both(both(collateralType == "", safeHandler == address(0)), keeper == address(liquidationEngine))) { - return (true, uint(-1), uint(-1)); - } - - require(collateralType == collateralJoin.collateralType(), "NativeUnderlyingUniswapV3SafeSaviour/invalid-collateral-type"); - - // Check that the SAFE has a non null amount of LP tokens covering it - require(lpTokenCover[safeHandler] > 0, "NativeUnderlyingUniswapV3SafeSaviour/null-cover"); - - // Get current balances - uint256 sysCoinBalance = systemCoin.balanceOf(address(this)); - uint256 collateralCoinBalance = collateralToken.balanceOf(address(this)); - - // Mark the SAFE in the registry as just having been saved - saviourRegistry.markSave(collateralType, safeHandler); - - // Store cover amount in local var - uint256 totalCover = lpTokenCover[safeHandler]; - delete(lpTokenCover[safeHandler]); - - // Withdraw all liquidity - lpToken.approve(address(liquidityManager), totalCover); - liquidityManager.removeLiquidity(totalCover, 0, 0, address(this)); - - // Checks after removing liquidity - require( - either(systemCoin.balanceOf(address(this)) > sysCoinBalance, collateralToken.balanceOf(address(this)) > collateralCoinBalance), - "NativeUnderlyingUniswapV3SafeSaviour/faulty-remove-liquidity" - ); - - // Get amounts withdrawn - sysCoinBalance = sub(systemCoin.balanceOf(address(this)), sysCoinBalance); - collateralCoinBalance = sub(collateralToken.balanceOf(address(this)), collateralCoinBalance); - - // Get the amount of tokens used to top up the SAFE - (uint256 safeDebtRepaid, uint256 safeCollateralAdded) = - getTokensForSaving( - safeHandler, - oracleRelayer.redemptionPrice(), - sysCoinBalance, - collateralCoinBalance - ); - - // There must be tokens used to save the SAVE - require(either(safeDebtRepaid > 0, safeCollateralAdded > 0), "NativeUnderlyingUniswapV3SafeSaviour/cannot-save-safe"); - - // Get the amounts of tokens sent to the keeper as payment - (uint256 keeperSysCoins, uint256 keeperCollateralCoins) = - getKeeperPayoutTokens( - safeHandler, - oracleRelayer.redemptionPrice(), - safeDebtRepaid, - safeCollateralAdded, - sysCoinBalance, - collateralCoinBalance - ); - - // There must be tokens that go to the keeper - require(either(keeperSysCoins > 0, keeperCollateralCoins > 0), "NativeUnderlyingUniswapV3SafeSaviour/cannot-pay-keeper"); - - // Compute remaining balances of tokens that will go into reserves - sysCoinBalance = sub(sysCoinBalance, add(safeDebtRepaid, keeperSysCoins)); - collateralCoinBalance = sub( - collateralCoinBalance, add(safeCollateralAdded, keeperCollateralCoins) - ); - - // Update reserves - if (sysCoinBalance > 0) { - underlyingReserves[safeHandler].systemCoins = add( - underlyingReserves[safeHandler].systemCoins, sysCoinBalance - ); - } - if (collateralCoinBalance > 0) { - underlyingReserves[safeHandler].collateralCoins = add( - underlyingReserves[safeHandler].collateralCoins, collateralCoinBalance - ); - } - - // Save the SAFE - if (safeDebtRepaid > 0) { - // Approve the coin join contract to take system coins and repay debt - systemCoin.approve(address(coinJoin), safeDebtRepaid); - // Calculate the non adjusted system coin amount - uint256 nonAdjustedSystemCoinsToRepay = div(mul(safeDebtRepaid, RAY), getAccumulatedRate(collateralType)); - - // Join system coins in the system and repay the SAFE's debt - coinJoin.join(address(this), safeDebtRepaid); - safeEngine.modifySAFECollateralization( - collateralType, - safeHandler, - address(0), - address(this), - int256(0), - -int256(nonAdjustedSystemCoinsToRepay) - ); - } - - if (safeCollateralAdded > 0) { - // Approve collateralToken to the collateral join contract - collateralToken.approve(address(collateralJoin), safeCollateralAdded); - - // Join collateralToken in the system and add it in the saved SAFE - collateralJoin.join(address(this), safeCollateralAdded); - safeEngine.modifySAFECollateralization( - collateralType, - safeHandler, - address(this), - address(0), - int256(safeCollateralAdded), - int256(0) - ); - } - - // Pay keeper - if (keeperSysCoins > 0) { - systemCoin.transfer(keeper, keeperSysCoins); - } - - if (keeperCollateralCoins > 0) { - collateralToken.transfer(keeper, keeperCollateralCoins); - } - - // Emit an event - emit SaveSAFE(keeper, collateralType, safeHandler, totalCover); - - return (true, totalCover, 0); - } - - // --- Getters --- - /* - * @notify Must be implemented according to the interface although it always returns 0 - */ - function getKeeperPayoutValue() override public returns (uint256) { - return 0; - } - /* - * @notify Must be implemented according to the interface although it always returns false - */ - function keeperPayoutExceedsMinValue() override public returns (bool) { - return false; - } - /* - * @notify Must be implemented according to the interface although it always returns false - */ - function canSave(bytes32, address safeHandler) override external returns (bool) { - return false; - } - /* - * @notice Return the total amount of LP tokens covering a specific SAFE - * @param collateralType The SAFE collateral type (ignored in this implementation) - * @param safeHandler The handler of the SAFE which the function takes into account - * @return The total LP token cover for a specific SAFE - */ - function tokenAmountUsedToSave(bytes32, address safeHandler) override public returns (uint256) { - return lpTokenCover[safeHandler]; - } - /* - * @notify Fetch the collateral's price - */ - function getCollateralPrice() public view returns (uint256) { - (address ethFSM,,) = oracleRelayer.collateralTypes(collateralJoin.collateralType()); - if (ethFSM == address(0)) return 0; - - (uint256 priceFeedValue, bool hasValidValue) = PriceFeedLike(ethFSM).getResultWithValidity(); - if (!hasValidValue) return 0; - - return priceFeedValue; - } - /* - * @notify Fetch the system coin's market price - */ - function getSystemCoinMarketPrice() public view returns (uint256) { - (uint256 priceFeedValue, bool hasValidValue) = systemCoinOrcl.getResultWithValidity(); - if (!hasValidValue) return 0; - - return priceFeedValue; - } - /* - * @notify Get the target collateralization ratio that a SAFE should have after it's saved - * @param safeHandler The handler/address of the SAFE whose target collateralization ratio is retrieved - */ - function getTargetCRatio(address safeHandler) public view returns (uint256) { - bytes32 collateralType = collateralJoin.collateralType(); - uint256 defaultCRatio = cRatioSetter.defaultDesiredCollateralizationRatios(collateralType); - uint256 targetCRatio = (cRatioSetter.desiredCollateralizationRatios(collateralType, safeHandler) == 0) ? - defaultCRatio : cRatioSetter.desiredCollateralizationRatios(collateralType, safeHandler); - return targetCRatio; - } - /* - * @notice Return the amount of system coins and/or collateral tokens used to save a SAFE - * @param safeHandler The handler/address of the targeted SAFE - * @param redemptionPrice The system coin redemption price used in calculations - * @param sysCoinsFromLP System coins withdrawn from Uniswap - * @param collateralFromLP Collateral tokens withdrawn from Uniswap - */ - function getTokensForSaving( - address safeHandler, - uint256 redemptionPrice, - uint256 sysCoinsFromLP, - uint256 collateralFromLP - ) public view returns (uint256, uint256) { - if (either(redemptionPrice == 0, both(sysCoinsFromLP == 0, collateralFromLP == 0))) { - return (0, 0); - } - - // Get the default CRatio for the SAFE - (uint256 depositedCollateralToken, uint256 safeDebt) = - SAFEEngineLike(collateralJoin.safeEngine()).safes(collateralJoin.collateralType(), safeHandler); - uint256 targetCRatio = getTargetCRatio(safeHandler); - if (either(safeDebt == 0, targetCRatio == 0)) { - return (0, 0); - } - - // Get the collateral market price - uint256 collateralPrice = getCollateralPrice(); - if (collateralPrice == 0) { - return (0, 0); - } - - // Calculate how much debt would need to be repaid - { - uint256 debtToRepay = mul( - mul(HUNDRED, mul(depositedCollateralToken, collateralPrice) / WAD) / targetCRatio, RAY - ) / redemptionPrice; - - if (either(debtToRepay >= safeDebt, debtBelowFloor(collateralJoin.collateralType(), debtToRepay))) { - return (0, 0); - } - safeDebt = mul(safeDebt, getAccumulatedRate(collateralJoin.collateralType())) / RAY; - debtToRepay = sub(safeDebt, debtToRepay); - - // Determine total debt to repay; return if the SAFE can be saved solely by repaying debt, continue calculations otherwise - if (sysCoinsFromLP >= debtToRepay) { - return (debtToRepay, 0); - } - } - - // Calculate the amount of collateral that would need to be added to the SAFE - uint256 debtGap = sub(safeDebt, sysCoinsFromLP); - uint256 scaledDownDebtValue = mul(add(mul(redemptionPrice, debtGap) / RAY, ONE), targetCRatio) / HUNDRED; - - uint256 collateralTokenNeeded = div(mul(scaledDownDebtValue, WAD), collateralPrice); - collateralTokenNeeded = (either(depositedCollateralToken < collateralTokenNeeded, collateralTokenNeeded == 0)) ? - sub(collateralTokenNeeded, depositedCollateralToken) : MAX_UINT; - - // See if there's enough collateral to add to the SAFE in order to save it - if (collateralTokenNeeded <= collateralFromLP) { - return (sysCoinsFromLP, collateralTokenNeeded); - } else { - return (0, 0); - } - } - /* - * @notice Return the amount of system coins and/or collateral tokens used to pay a keeper - * @param safeHandler The handler/address of the targeted SAFE - * @param redemptionPrice The system coin redemption price used in calculations - * @param safeDebtRepaid The amount of system coins that are already used to save the targeted SAFE - * @param safeCollateralAdded The amount of collateral tokens that are already used to save the targeted SAFE - * @param sysCoinsFromLP System coins withdrawn from Uniswap - * @param collateralFromLP Collateral tokens withdrawn from Uniswap - */ - function getKeeperPayoutTokens( - address safeHandler, - uint256 redemptionPrice, - uint256 safeDebtRepaid, - uint256 safeCollateralAdded, - uint256 sysCoinsFromLP, - uint256 collateralFromLP - ) public view returns (uint256, uint256) { - if (both(sysCoinsFromLP == 0, collateralFromLP == 0)) return (0, 0); - - // Get the system coin and collateral market prices - uint256 collateralPrice = getCollateralPrice(); - uint256 sysCoinMarketPrice = getSystemCoinMarketPrice(); - if (either(collateralPrice == 0, sysCoinMarketPrice == 0)) { - return (0, 0); - } - - // Check if the keeper can get system coins and if yes, compute how many - uint256 keeperSysCoins; - if (sysCoinsFromLP > safeDebtRepaid) { - uint256 remainingSystemCoins = sub(sysCoinsFromLP, safeDebtRepaid); - uint256 payoutInSystemCoins = div(mul(minKeeperPayoutValue, WAD), sysCoinMarketPrice); - - if (payoutInSystemCoins <= remainingSystemCoins) { - return (payoutInSystemCoins, 0); - } else { - keeperSysCoins = remainingSystemCoins; - } - } - - // Calculate how much collateral the keeper will get - if (collateralFromLP <= safeCollateralAdded) return (0, 0); - - uint256 remainingCollateral = sub(collateralFromLP, safeCollateralAdded); - uint256 remainingKeeperPayoutValue = sub(minKeeperPayoutValue, mul(keeperSysCoins, sysCoinMarketPrice) / WAD); - uint256 collateralTokenNeeded = div(mul(remainingKeeperPayoutValue, WAD), collateralPrice); - - // If there are enough collateral tokens retreived from LP in order to pay the keeper, return the token amounts - if (collateralTokenNeeded <= remainingCollateral) { - return (keeperSysCoins, collateralTokenNeeded); - } else { - // Otherwise, return zeroes - return (0, 0); - } - } - /* - * @notify Returns whether a target debt amount is below the debt floor of a specific collateral type - * @param collateralType The collateral type whose floor we compare against - * @param targetDebtAmount The target debt amount for a SAFE that has collateralType collateral in it - */ - function debtBelowFloor(bytes32 collateralType, uint256 targetDebtAmount) public view returns (bool) { - (, , , , uint256 debtFloor, ) = safeEngine.collateralTypes(collateralType); - return (mul(targetDebtAmount, RAY) < debtFloor); - } - /* - * @notify Get the accumulated interest rate for a specific collateral type - * @param The collateral type for which to retrieve the rate - */ - function getAccumulatedRate(bytes32 collateralType) - public view returns (uint256 accumulatedRate) { - (, accumulatedRate, , , , ) = safeEngine.collateralTypes(collateralType); - } -} +// Copyright (C) 2021 Reflexer Labs, INC + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity 0.6.7; + +import "../interfaces/UniswapLiquidityManagerLike.sol"; +import "../interfaces/SaviourCRatioSetterLike.sol"; +import "../interfaces/SafeSaviourLike.sol"; +import "../math/SafeMath.sol"; + +contract NativeUnderlyingUniswapV3SafeSaviour is SafeMath, SafeSaviourLike { + // --- Auth --- + mapping (address => uint256) public authorizedAccounts; + /** + * @notice Add auth to an account + * @param account Account to add auth to + */ + function addAuthorization(address account) external isAuthorized { + authorizedAccounts[account] = 1; + emit AddAuthorization(account); + } + /** + * @notice Remove auth from an account + * @param account Account to remove auth from + */ + function removeAuthorization(address account) external isAuthorized { + authorizedAccounts[account] = 0; + emit RemoveAuthorization(account); + } + /** + * @notice Checks whether msg.sender can call an authed function + **/ + modifier isAuthorized { + require(authorizedAccounts[msg.sender] == 1, "NativeUnderlyingUniswapV3SafeSaviour/account-not-authorized"); + _; + } + + mapping (address => uint256) public allowedUsers; + /** + * @notice Allow a user to deposit assets + * @param usr User to whitelist + */ + function allowUser(address usr) external isAuthorized { + allowedUsers[usr] = 1; + emit AllowUser(usr); + } + /** + * @notice Disallow a user from depositing assets + * @param usr User to disallow + */ + function disallowUser(address usr) external isAuthorized { + allowedUsers[usr] = 0; + emit DisallowUser(usr); + } + /** + * @notice Checks whether an address is an allowed user + **/ + modifier isAllowed { + require( + either(restrictUsage == 0, both(restrictUsage == 1, allowedUsers[msg.sender] == 1)), + "NativeUnderlyingUniswapV3SafeSaviour/account-not-allowed" + ); + _; + } + + // --- Structs --- + struct Reserves { + uint256 systemCoins; + uint256 collateralCoins; + } + + // --- Variables --- + // Flag that tells whether usage of the contract is restricted to allowed users + uint256 public restrictUsage; + + // Whether the system coin is token0 in the Uniswap pool or not + bool public isSystemCoinToken0; + // Amount of LP tokens currently protecting each position + mapping(address => uint256) public lpTokenCover; + // Amount of system coin/collateral tokens that Safe owners can get back + mapping(address => Reserves) public underlyingReserves; + // Liquidity manager contract for Uniswap v2/v3 + UniswapLiquidityManagerLike public liquidityManager; + // The ERC20 system coin + ERC20Like public systemCoin; + // The system coin join contract + CoinJoinLike public coinJoin; + // The collateral join contract for adding collateral in the system + CollateralJoinLike public collateralJoin; + // The LP token + ERC20Like public lpToken; + // The collateral token + ERC20Like public collateralToken; + // Oracle providing the system coin price feed + PriceFeedLike public systemCoinOrcl; + // Contract that defines desired CRatios for each Safe after it is saved + SaviourCRatioSetterLike public cRatioSetter; + + // --- Events --- + event AddAuthorization(address account); + event RemoveAuthorization(address account); + event AllowUser(address usr); + event DisallowUser(address usr); + event ModifyParameters(bytes32 indexed parameter, uint256 val); + event ModifyParameters(bytes32 indexed parameter, address data); + event Deposit( + address indexed caller, + address indexed safeHandler, + uint256 lpTokenAmount + ); + event Withdraw( + address indexed caller, + address indexed safeHandler, + address dst, + uint256 lpTokenAmount + ); + event GetReserves( + address indexed caller, + address indexed safeHandler, + uint256 systemCoinAmount, + uint256 collateralAmount, + address dst + ); + + constructor( + bool isSystemCoinToken0_, + address coinJoin_, + address collateralJoin_, + address cRatioSetter_, + address systemCoinOrcl_, + address liquidationEngine_, + address taxCollector_, + address oracleRelayer_, + address safeManager_, + address saviourRegistry_, + address liquidityManager_, + address lpToken_, + uint256 minKeeperPayoutValue_ + ) public { + require(coinJoin_ != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-coin-join"); + require(collateralJoin_ != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-collateral-join"); + require(cRatioSetter_ != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-cratio-setter"); + require(systemCoinOrcl_ != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-system-coin-oracle"); + require(oracleRelayer_ != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-oracle-relayer"); + require(liquidationEngine_ != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-liquidation-engine"); + require(taxCollector_ != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-tax-collector"); + require(safeManager_ != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-safe-manager"); + require(saviourRegistry_ != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-saviour-registry"); + require(liquidityManager_ != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-liq-manager"); + require(lpToken_ != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-lp-token"); + require(minKeeperPayoutValue_ > 0, "NativeUnderlyingUniswapV3SafeSaviour/invalid-min-payout-value"); + + authorizedAccounts[msg.sender] = 1; + + isSystemCoinToken0 = isSystemCoinToken0_; + minKeeperPayoutValue = minKeeperPayoutValue_; + + coinJoin = CoinJoinLike(coinJoin_); + collateralJoin = CollateralJoinLike(collateralJoin_); + cRatioSetter = SaviourCRatioSetterLike(cRatioSetter_); + liquidationEngine = LiquidationEngineLike(liquidationEngine_); + taxCollector = TaxCollectorLike(taxCollector_); + oracleRelayer = OracleRelayerLike(oracleRelayer_); + systemCoinOrcl = PriceFeedLike(systemCoinOrcl_); + systemCoin = ERC20Like(coinJoin.systemCoin()); + safeEngine = SAFEEngineLike(coinJoin.safeEngine()); + safeManager = GebSafeManagerLike(safeManager_); + saviourRegistry = SAFESaviourRegistryLike(saviourRegistry_); + liquidityManager = UniswapLiquidityManagerLike(liquidityManager_); + lpToken = ERC20Like(lpToken_); + collateralToken = ERC20Like(collateralJoin.collateral()); + + systemCoinOrcl.getResultWithValidity(); + oracleRelayer.redemptionPrice(); + + require(collateralJoin.contractEnabled() == 1, "NativeUnderlyingUniswapV3SafeSaviour/join-disabled"); + require(address(collateralToken) != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-col-token"); + require(address(safeEngine) != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-safe-engine"); + require(address(systemCoin) != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-sys-coin"); + + emit AddAuthorization(msg.sender); + emit ModifyParameters("minKeeperPayoutValue", minKeeperPayoutValue); + emit ModifyParameters("oracleRelayer", oracleRelayer_); + emit ModifyParameters("taxCollector", taxCollector_); + emit ModifyParameters("systemCoinOrcl", systemCoinOrcl_); + emit ModifyParameters("liquidationEngine", liquidationEngine_); + emit ModifyParameters("liquidityManager", liquidityManager_); + } + + // --- Administration --- + /** + * @notice Modify an uint256 param + * @param parameter The name of the parameter + * @param val New value for the parameter + */ + function modifyParameters(bytes32 parameter, uint256 val) external isAuthorized { + if (parameter == "minKeeperPayoutValue") { + require(val > 0, "NativeUnderlyingUniswapV3SafeSaviour/null-min-payout"); + minKeeperPayoutValue = val; + } + else if (parameter == "restrictUsage") { + require(val <= 1, "NativeUnderlyingUniswapV3SafeSaviour/invalid-restriction"); + restrictUsage = val; + } + else revert("NativeUnderlyingUniswapV3SafeSaviour/modify-unrecognized-param"); + emit ModifyParameters(parameter, val); + } + /** + * @notice Modify an address param + * @param parameter The name of the parameter + * @param data New address for the parameter + */ + function modifyParameters(bytes32 parameter, address data) external isAuthorized { + require(data != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-data"); + + if (parameter == "systemCoinOrcl") { + systemCoinOrcl = PriceFeedLike(data); + systemCoinOrcl.getResultWithValidity(); + } + else if (parameter == "oracleRelayer") { + oracleRelayer = OracleRelayerLike(data); + oracleRelayer.redemptionPrice(); + } + else if (parameter == "liquidityManager") { + liquidityManager = UniswapLiquidityManagerLike(data); + } + else if (parameter == "liquidationEngine") { + liquidationEngine = LiquidationEngineLike(data); + } + else if (parameter == "taxCollector") { + taxCollector = TaxCollectorLike(data); + } + else revert("NativeUnderlyingUniswapV3SafeSaviour/modify-unrecognized-param"); + emit ModifyParameters(parameter, data); + } + + // --- Transferring Reserves --- + /* + * @notify Get back system coins or collateral tokens that were withdrawn from Uniswap and not used to save a specific SAFE + * @param safeID The ID of the safe that was previously saved and has leftover funds that can be withdrawn + * @param dst The address that will receive + */ + function getReserves(uint256 safeID, address dst) external controlsSAFE(msg.sender, safeID) nonReentrant { + address safeHandler = safeManager.safes(safeID); + (uint256 systemCoins, uint256 collateralCoins) = + (underlyingReserves[safeHandler].systemCoins, underlyingReserves[safeHandler].collateralCoins); + + require(either(systemCoins > 0, collateralCoins > 0), "NativeUnderlyingUniswapV3SafeSaviour/no-reserves"); + delete(underlyingReserves[safeManager.safes(safeID)]); + + if (systemCoins > 0) { + systemCoin.transfer(dst, systemCoins); + } + + if (collateralCoins > 0) { + collateralToken.transfer(dst, collateralCoins); + } + + emit GetReserves(msg.sender, safeHandler, systemCoins, collateralCoins, dst); + } + + // --- Adding/Withdrawing Cover --- + /* + * @notice Deposit lpToken in the contract in order to provide cover for a specific SAFE managed by the SAFE Manager + * @param safeID The ID of the SAFE to protect. This ID should be registered inside GebSafeManager + * @param lpTokenAmount The amount of collateralToken to deposit + */ + function deposit(uint256 safeID, uint256 lpTokenAmount) external isAllowed() liquidationEngineApproved(address(this)) nonReentrant { + require(lpTokenAmount > 0, "NativeUnderlyingUniswapV3SafeSaviour/null-lp-amount"); + + // Check that the SAFE exists inside GebSafeManager + address safeHandler = safeManager.safes(safeID); + require(safeHandler != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-handler"); + + // Check that the SAFE has debt + (, uint256 safeDebt) = + SAFEEngineLike(collateralJoin.safeEngine()).safes(collateralJoin.collateralType(), safeHandler); + require(safeDebt > 0, "NativeUnderlyingUniswapV3SafeSaviour/safe-does-not-have-debt"); + + // Update the lpToken balance used to cover the SAFE and transfer tokens to this contract + lpTokenCover[safeHandler] = add(lpTokenCover[safeHandler], lpTokenAmount); + require(lpToken.transferFrom(msg.sender, address(this), lpTokenAmount), "NativeUnderlyingUniswapV3SafeSaviour/could-not-transfer-lp"); + + emit Deposit(msg.sender, safeHandler, lpTokenAmount); + } + /* + * @notice Withdraw lpToken from the contract and provide less cover for a SAFE + * @dev Only an address that controls the SAFE inside the SAFE Manager can call this + * @param safeID The ID of the SAFE to remove cover from. This ID should be registered inside the SAFE Manager + * @param lpTokenAmount The amount of lpToken to withdraw + * @param dst The address that will receive the LP tokens + */ + function withdraw(uint256 safeID, uint256 lpTokenAmount, address dst) external controlsSAFE(msg.sender, safeID) nonReentrant { + require(lpTokenAmount > 0, "NativeUnderlyingUniswapV3SafeSaviour/null-lp-amount"); + + // Fetch the handler from the SAFE manager + address safeHandler = safeManager.safes(safeID); + require(lpTokenCover[safeHandler] >= lpTokenAmount, "NativeUnderlyingUniswapV3SafeSaviour/not-enough-to-withdraw"); + + // Withdraw cover and transfer collateralToken to the caller + lpTokenCover[safeHandler] = sub(lpTokenCover[safeHandler], lpTokenAmount); + lpToken.transfer(dst, lpTokenAmount); + + emit Withdraw(msg.sender, safeHandler, dst, lpTokenAmount); + } + + // --- Saving Logic --- + /* + * @notice Saves a SAFE by withdrawing liquidity and repaying debt and/or adding more collateral + * @dev Only the LiquidationEngine can call this + * @param keeper The keeper that called LiquidationEngine.liquidateSAFE and that should be rewarded for spending gas to save a SAFE + * @param collateralType The collateral type backing the SAFE that's being liquidated + * @param safeHandler The handler of the SAFE that's being liquidated + * @return Whether the SAFE has been saved, the amount of LP tokens that were used to withdraw liquidity as well as the amount of + * system coins sent to the keeper as their payment (this implementation always returns 0) + */ + function saveSAFE(address keeper, bytes32 collateralType, address safeHandler) override external returns (bool, uint256, uint256) { + require(address(liquidationEngine) == msg.sender, "NativeUnderlyingUniswapV3SafeSaviour/caller-not-liquidation-engine"); + require(keeper != address(0), "NativeUnderlyingUniswapV3SafeSaviour/null-keeper-address"); + + if (both(both(collateralType == "", safeHandler == address(0)), keeper == address(liquidationEngine))) { + return (true, uint(-1), uint(-1)); + } + + require(collateralType == collateralJoin.collateralType(), "NativeUnderlyingUniswapV3SafeSaviour/invalid-collateral-type"); + + // Check that the SAFE has a non null amount of LP tokens covering it + require(lpTokenCover[safeHandler] > 0, "NativeUnderlyingUniswapV3SafeSaviour/null-cover"); + + // Get current balances + uint256 sysCoinBalance = systemCoin.balanceOf(address(this)); + uint256 collateralCoinBalance = collateralToken.balanceOf(address(this)); + + // Mark the SAFE in the registry as just having been saved + saviourRegistry.markSave(collateralType, safeHandler); + + // Store cover amount in local var + uint256 totalCover = lpTokenCover[safeHandler]; + delete(lpTokenCover[safeHandler]); + + // Withdraw all liquidity + lpToken.approve(address(liquidityManager), totalCover); + liquidityManager.removeLiquidity(totalCover, 0, 0, address(this)); + + // Checks after removing liquidity + require( + either(systemCoin.balanceOf(address(this)) > sysCoinBalance, collateralToken.balanceOf(address(this)) > collateralCoinBalance), + "NativeUnderlyingUniswapV3SafeSaviour/faulty-remove-liquidity" + ); + + // Get amounts withdrawn + sysCoinBalance = sub(systemCoin.balanceOf(address(this)), sysCoinBalance); + collateralCoinBalance = sub(collateralToken.balanceOf(address(this)), collateralCoinBalance); + + // Get the amount of tokens used to top up the SAFE + (uint256 safeDebtRepaid, uint256 safeCollateralAdded) = + getTokensForSaving( + safeHandler, + oracleRelayer.redemptionPrice(), + sysCoinBalance, + collateralCoinBalance + ); + + // There must be tokens used to save the SAVE + require(either(safeDebtRepaid > 0, safeCollateralAdded > 0), "NativeUnderlyingUniswapV3SafeSaviour/cannot-save-safe"); + + // Get the amounts of tokens sent to the keeper as payment + (uint256 keeperSysCoins, uint256 keeperCollateralCoins) = + getKeeperPayoutTokens( + safeHandler, + oracleRelayer.redemptionPrice(), + safeDebtRepaid, + safeCollateralAdded, + sysCoinBalance, + collateralCoinBalance + ); + + // There must be tokens that go to the keeper + require(either(keeperSysCoins > 0, keeperCollateralCoins > 0), "NativeUnderlyingUniswapV3SafeSaviour/cannot-pay-keeper"); + + // Compute remaining balances of tokens that will go into reserves + sysCoinBalance = sub(sysCoinBalance, add(safeDebtRepaid, keeperSysCoins)); + collateralCoinBalance = sub( + collateralCoinBalance, add(safeCollateralAdded, keeperCollateralCoins) + ); + + // Update reserves + if (sysCoinBalance > 0) { + underlyingReserves[safeHandler].systemCoins = add( + underlyingReserves[safeHandler].systemCoins, sysCoinBalance + ); + } + if (collateralCoinBalance > 0) { + underlyingReserves[safeHandler].collateralCoins = add( + underlyingReserves[safeHandler].collateralCoins, collateralCoinBalance + ); + } + + // Save the SAFE + if (safeDebtRepaid > 0) { + // Approve the coin join contract to take system coins and repay debt + systemCoin.approve(address(coinJoin), safeDebtRepaid); + // Calculate the non adjusted system coin amount + uint256 nonAdjustedSystemCoinsToRepay = div(mul(safeDebtRepaid, RAY), getAccumulatedRate(collateralType)); + + // Join system coins in the system and repay the SAFE's debt + coinJoin.join(address(this), safeDebtRepaid); + safeEngine.modifySAFECollateralization( + collateralType, + safeHandler, + address(0), + address(this), + int256(0), + -int256(nonAdjustedSystemCoinsToRepay) + ); + } + + if (safeCollateralAdded > 0) { + // Approve collateralToken to the collateral join contract + collateralToken.approve(address(collateralJoin), safeCollateralAdded); + + // Join collateralToken in the system and add it in the saved SAFE + collateralJoin.join(address(this), safeCollateralAdded); + safeEngine.modifySAFECollateralization( + collateralType, + safeHandler, + address(this), + address(0), + int256(safeCollateralAdded), + int256(0) + ); + } + + // Pay keeper + if (keeperSysCoins > 0) { + systemCoin.transfer(keeper, keeperSysCoins); + } + + if (keeperCollateralCoins > 0) { + collateralToken.transfer(keeper, keeperCollateralCoins); + } + + // Emit an event + emit SaveSAFE(keeper, collateralType, safeHandler, totalCover); + + return (true, totalCover, 0); + } + + // --- Getters --- + /* + * @notify Must be implemented according to the interface although it always returns 0 + */ + function getKeeperPayoutValue() override public returns (uint256) { + return 0; + } + /* + * @notify Must be implemented according to the interface although it always returns false + */ + function keeperPayoutExceedsMinValue() override public returns (bool) { + return false; + } + /* + * @notify Must be implemented according to the interface although it always returns false + */ + function canSave(bytes32, address safeHandler) override external returns (bool) { + return false; + } + /* + * @notice Return the total amount of LP tokens covering a specific SAFE + * @param collateralType The SAFE collateral type (ignored in this implementation) + * @param safeHandler The handler of the SAFE which the function takes into account + * @return The total LP token cover for a specific SAFE + */ + function tokenAmountUsedToSave(bytes32, address safeHandler) override public returns (uint256) { + return lpTokenCover[safeHandler]; + } + /* + * @notify Fetch the collateral's price + */ + function getCollateralPrice() public view returns (uint256) { + (address ethFSM,,) = oracleRelayer.collateralTypes(collateralJoin.collateralType()); + if (ethFSM == address(0)) return 0; + + (uint256 priceFeedValue, bool hasValidValue) = PriceFeedLike(ethFSM).getResultWithValidity(); + if (!hasValidValue) return 0; + + return priceFeedValue; + } + /* + * @notify Fetch the system coin's market price + */ + function getSystemCoinMarketPrice() public view returns (uint256) { + (uint256 priceFeedValue, bool hasValidValue) = systemCoinOrcl.getResultWithValidity(); + if (!hasValidValue) return 0; + + return priceFeedValue; + } + /* + * @notify Get the target collateralization ratio that a SAFE should have after it's saved + * @param safeHandler The handler/address of the SAFE whose target collateralization ratio is retrieved + */ + function getTargetCRatio(address safeHandler) public view returns (uint256) { + bytes32 collateralType = collateralJoin.collateralType(); + uint256 defaultCRatio = cRatioSetter.defaultDesiredCollateralizationRatios(collateralType); + uint256 targetCRatio = (cRatioSetter.desiredCollateralizationRatios(collateralType, safeHandler) == 0) ? + defaultCRatio : cRatioSetter.desiredCollateralizationRatios(collateralType, safeHandler); + return targetCRatio; + } + /* + * @notice Return the amount of system coins and/or collateral tokens used to save a SAFE + * @param safeHandler The handler/address of the targeted SAFE + * @param redemptionPrice The system coin redemption price used in calculations + * @param sysCoinsFromLP System coins withdrawn from Uniswap + * @param collateralFromLP Collateral tokens withdrawn from Uniswap + */ + function getTokensForSaving( + address safeHandler, + uint256 redemptionPrice, + uint256 sysCoinsFromLP, + uint256 collateralFromLP + ) public view returns (uint256, uint256) { + if (either(redemptionPrice == 0, both(sysCoinsFromLP == 0, collateralFromLP == 0))) { + return (0, 0); + } + + // Get the default CRatio for the SAFE + (uint256 depositedCollateralToken, uint256 safeDebt) = + SAFEEngineLike(collateralJoin.safeEngine()).safes(collateralJoin.collateralType(), safeHandler); + uint256 targetCRatio = getTargetCRatio(safeHandler); + if (either(safeDebt == 0, targetCRatio == 0)) { + return (0, 0); + } + + // Get the collateral market price + uint256 collateralPrice = getCollateralPrice(); + if (collateralPrice == 0) { + return (0, 0); + } + + // Calculate how much debt would need to be repaid + { + uint256 debtToRepay = mul( + mul(HUNDRED, mul(depositedCollateralToken, collateralPrice) / WAD) / targetCRatio, RAY + ) / redemptionPrice; + + if (either(debtToRepay >= safeDebt, debtBelowFloor(collateralJoin.collateralType(), debtToRepay))) { + return (0, 0); + } + safeDebt = mul(safeDebt, getAccumulatedRate(collateralJoin.collateralType())) / RAY; + debtToRepay = sub(safeDebt, debtToRepay); + + // Determine total debt to repay; return if the SAFE can be saved solely by repaying debt, continue calculations otherwise + if (sysCoinsFromLP >= debtToRepay) { + return (debtToRepay, 0); + } + } + + // Calculate the amount of collateral that would need to be added to the SAFE + uint256 debtGap = sub(safeDebt, sysCoinsFromLP); + uint256 scaledDownDebtValue = mul(add(mul(redemptionPrice, debtGap) / RAY, ONE), targetCRatio) / HUNDRED; + + uint256 collateralTokenNeeded = div(mul(scaledDownDebtValue, WAD), collateralPrice); + collateralTokenNeeded = (either(depositedCollateralToken < collateralTokenNeeded, collateralTokenNeeded == 0)) ? + sub(collateralTokenNeeded, depositedCollateralToken) : MAX_UINT; + + // See if there's enough collateral to add to the SAFE in order to save it + if (collateralTokenNeeded <= collateralFromLP) { + return (sysCoinsFromLP, collateralTokenNeeded); + } else { + return (0, 0); + } + } + /* + * @notice Return the amount of system coins and/or collateral tokens used to pay a keeper + * @param safeHandler The handler/address of the targeted SAFE + * @param redemptionPrice The system coin redemption price used in calculations + * @param safeDebtRepaid The amount of system coins that are already used to save the targeted SAFE + * @param safeCollateralAdded The amount of collateral tokens that are already used to save the targeted SAFE + * @param sysCoinsFromLP System coins withdrawn from Uniswap + * @param collateralFromLP Collateral tokens withdrawn from Uniswap + */ + function getKeeperPayoutTokens( + address safeHandler, + uint256 redemptionPrice, + uint256 safeDebtRepaid, + uint256 safeCollateralAdded, + uint256 sysCoinsFromLP, + uint256 collateralFromLP + ) public view returns (uint256, uint256) { + if (both(sysCoinsFromLP == 0, collateralFromLP == 0)) return (0, 0); + + // Get the system coin and collateral market prices + uint256 collateralPrice = getCollateralPrice(); + uint256 sysCoinMarketPrice = getSystemCoinMarketPrice(); + if (either(collateralPrice == 0, sysCoinMarketPrice == 0)) { + return (0, 0); + } + + // Check if the keeper can get system coins and if yes, compute how many + uint256 keeperSysCoins; + if (sysCoinsFromLP > safeDebtRepaid) { + uint256 remainingSystemCoins = sub(sysCoinsFromLP, safeDebtRepaid); + uint256 payoutInSystemCoins = div(mul(minKeeperPayoutValue, WAD), sysCoinMarketPrice); + + if (payoutInSystemCoins <= remainingSystemCoins) { + return (payoutInSystemCoins, 0); + } else { + keeperSysCoins = remainingSystemCoins; + } + } + + // Calculate how much collateral the keeper will get + if (collateralFromLP <= safeCollateralAdded) return (0, 0); + + uint256 remainingCollateral = sub(collateralFromLP, safeCollateralAdded); + uint256 remainingKeeperPayoutValue = sub(minKeeperPayoutValue, mul(keeperSysCoins, sysCoinMarketPrice) / WAD); + uint256 collateralTokenNeeded = div(mul(remainingKeeperPayoutValue, WAD), collateralPrice); + + // If there are enough collateral tokens retreived from LP in order to pay the keeper, return the token amounts + if (collateralTokenNeeded <= remainingCollateral) { + return (keeperSysCoins, collateralTokenNeeded); + } else { + // Otherwise, return zeroes + return (0, 0); + } + } + /* + * @notify Returns whether a target debt amount is below the debt floor of a specific collateral type + * @param collateralType The collateral type whose floor we compare against + * @param targetDebtAmount The target debt amount for a SAFE that has collateralType collateral in it + */ + function debtBelowFloor(bytes32 collateralType, uint256 targetDebtAmount) public view returns (bool) { + (, , , , uint256 debtFloor, ) = safeEngine.collateralTypes(collateralType); + return (mul(targetDebtAmount, RAY) < debtFloor); + } + /* + * @notify Get the accumulated interest rate for a specific collateral type + * @param The collateral type for which to retrieve the rate + */ + function getAccumulatedRate(bytes32 collateralType) + public view returns (uint256 accumulatedRate) { + (, accumulatedRate, , , , ) = safeEngine.collateralTypes(collateralType); + } +} diff --git a/src/saviours/SystemCoinUniswapV2SafeSaviour.sol b/src/saviours/SystemCoinUniswapV2SafeSaviour.sol index 7f738b5..dd1aebd 100644 --- a/src/saviours/SystemCoinUniswapV2SafeSaviour.sol +++ b/src/saviours/SystemCoinUniswapV2SafeSaviour.sol @@ -1,687 +1,687 @@ -// Copyright (C) 2021 Reflexer Labs, INC - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -pragma solidity 0.6.7; - -import "../interfaces/SwapManagerLike.sol"; -import "../interfaces/UniswapLiquidityManagerLike.sol"; -import "../interfaces/SaviourCRatioSetterLike.sol"; -import "../interfaces/SafeSaviourLike.sol"; -import "../math/SafeMath.sol"; - -contract SystemCoinUniswapV2SafeSaviour is SafeMath, SafeSaviourLike { - // --- Auth --- - mapping (address => uint256) public authorizedAccounts; - /** - * @notice Add auth to an account - * @param account Account to add auth to - */ - function addAuthorization(address account) external isAuthorized { - authorizedAccounts[account] = 1; - emit AddAuthorization(account); - } - /** - * @notice Remove auth from an account - * @param account Account to remove auth from - */ - function removeAuthorization(address account) external isAuthorized { - authorizedAccounts[account] = 0; - emit RemoveAuthorization(account); - } - /** - * @notice Checks whether msg.sender can call an authed function - **/ - modifier isAuthorized { - require(authorizedAccounts[msg.sender] == 1, "SystemCoinUniswapV2SafeSaviour/account-not-authorized"); - _; - } - - mapping (address => uint256) public allowedUsers; - /** - * @notice Allow a user to deposit assets - * @param usr User to whitelist - */ - function allowUser(address usr) external isAuthorized { - allowedUsers[usr] = 1; - emit AllowUser(usr); - } - /** - * @notice Disallow a user from depositing assets - * @param usr User to disallow - */ - function disallowUser(address usr) external isAuthorized { - allowedUsers[usr] = 0; - emit DisallowUser(usr); - } - /** - * @notice Checks whether an address is an allowed user - **/ - modifier isAllowed { - require( - either(restrictUsage == 0, both(restrictUsage == 1, allowedUsers[msg.sender] == 1)), - "SystemCoinUniswapV2SafeSaviour/account-not-allowed" - ); - _; - } - - // --- Structs --- - struct Reserves { - uint256 systemCoins; - uint256 collateralCoins; - } - - // --- Variables --- - // Flag that tells whether usage of the contract is restricted to allowed users - uint256 public restrictUsage; - - // Whether the system coin is token0 in the Uniswap pool or not - bool public isSystemCoinToken0; - // Amount of LP tokens currently protecting each position - mapping(address => uint256) public lpTokenCover; - // Amount of system coin/collateral tokens that Safe owners can get back - mapping(address => Reserves) public underlyingReserves; - // Liquidity manager contract for Uniswap v2/v3 - UniswapLiquidityManagerLike public liquidityManager; - // The contract that can swap tokens - SwapManagerLike public swapManager; - // The ERC20 system coin - ERC20Like public systemCoin; - // The system coin join contract - CoinJoinLike public coinJoin; - // The collateral join contract for adding collateral in the system - CollateralJoinLike public collateralJoin; - // The LP token - ERC20Like public lpToken; - // The collateral token - ERC20Like public collateralToken; - // The second token in the Uniswap pair (besides the system coin) - ERC20Like public pairToken; - // Oracle providing the system coin price feed - PriceFeedLike public systemCoinOrcl; - // Contract that defines desired CRatios for each Safe after it is saved - SaviourCRatioSetterLike public cRatioSetter; - - // --- Events --- - event AddAuthorization(address account); - event RemoveAuthorization(address account); - event AllowUser(address usr); - event DisallowUser(address usr); - event ModifyParameters(bytes32 indexed parameter, uint256 val); - event ModifyParameters(bytes32 indexed parameter, address data); - event Deposit( - address indexed caller, - address indexed safeHandler, - uint256 lpTokenAmount - ); - event Withdraw( - address indexed caller, - address indexed safeHandler, - address dst, - uint256 lpTokenAmount - ); - event GetReserves( - address indexed caller, - address indexed safeHandler, - uint256 systemCoinAmount, - uint256 collateralAmount, - address dst - ); - - constructor( - bool isSystemCoinToken0_, - address coinJoin_, - address collateralJoin_, - address cRatioSetter_, - address systemCoinOrcl_, - address liquidationEngine_, - address oracleRelayer_, - address safeManager_, - address saviourRegistry_, - address liquidityManager_, - address swapManager_, - address pairToken_, - address lpToken_, - uint256 minKeeperPayoutValue_ - ) public { - require(coinJoin_ != address(0), "SystemCoinUniswapV2SafeSaviour/null-coin-join"); - require(collateralJoin_ != address(0), "SystemCoinUniswapV2SafeSaviour/null-collateral-join"); - require(cRatioSetter_ != address(0), "SystemCoinUniswapV2SafeSaviour/null-cratio-setter"); - require(systemCoinOrcl_ != address(0), "SystemCoinUniswapV2SafeSaviour/null-system-coin-oracle"); - require(oracleRelayer_ != address(0), "SystemCoinUniswapV2SafeSaviour/null-oracle-relayer"); - require(liquidationEngine_ != address(0), "SystemCoinUniswapV2SafeSaviour/null-liquidation-engine"); - require(safeManager_ != address(0), "SystemCoinUniswapV2SafeSaviour/null-safe-manager"); - require(saviourRegistry_ != address(0), "SystemCoinUniswapV2SafeSaviour/null-saviour-registry"); - require(liquidityManager_ != address(0), "SystemCoinUniswapV2SafeSaviour/null-liq-manager"); - require(swapManager_ != address(0), "SystemCoinUniswapV2SafeSaviour/null-swap-manager"); - require(lpToken_ != address(0), "SystemCoinUniswapV2SafeSaviour/null-lp-token"); - require(pairToken_ != address(0), "SystemCoinUniswapV2SafeSaviour/null-pair-token"); - require(minKeeperPayoutValue_ > 0, "SystemCoinUniswapV2SafeSaviour/invalid-min-payout-value"); - - authorizedAccounts[msg.sender] = 1; - - isSystemCoinToken0 = isSystemCoinToken0_; - minKeeperPayoutValue = minKeeperPayoutValue_; - - coinJoin = CoinJoinLike(coinJoin_); - collateralJoin = CollateralJoinLike(collateralJoin_); - cRatioSetter = SaviourCRatioSetterLike(cRatioSetter_); - liquidationEngine = LiquidationEngineLike(liquidationEngine_); - oracleRelayer = OracleRelayerLike(oracleRelayer_); - systemCoinOrcl = PriceFeedLike(systemCoinOrcl_); - systemCoin = ERC20Like(coinJoin.systemCoin()); - safeEngine = SAFEEngineLike(coinJoin.safeEngine()); - safeManager = GebSafeManagerLike(safeManager_); - saviourRegistry = SAFESaviourRegistryLike(saviourRegistry_); - liquidityManager = UniswapLiquidityManagerLike(liquidityManager_); - swapManager = SwapManagerLike(swapManager_); - pairToken = ERC20Like(pairToken_); - lpToken = ERC20Like(lpToken_); - collateralToken = ERC20Like(collateralJoin.collateral()); - - systemCoinOrcl.getResultWithValidity(); - oracleRelayer.redemptionPrice(); - - require(collateralJoin.contractEnabled() == 1, "SystemCoinUniswapV2SafeSaviour/join-disabled"); - require(address(collateralToken) != address(0), "SystemCoinUniswapV2SafeSaviour/null-col-token"); - require(address(safeEngine) != address(0), "SystemCoinUniswapV2SafeSaviour/null-safe-engine"); - require(address(systemCoin) != address(0), "SystemCoinUniswapV2SafeSaviour/null-sys-coin"); - - emit AddAuthorization(msg.sender); - emit ModifyParameters("minKeeperPayoutValue", minKeeperPayoutValue); - emit ModifyParameters("oracleRelayer", oracleRelayer_); - emit ModifyParameters("systemCoinOrcl", systemCoinOrcl_); - emit ModifyParameters("liquidationEngine", liquidationEngine_); - emit ModifyParameters("liquidityManager", liquidityManager_); - emit ModifyParameters("swapManager", swapManager_); - } - - // --- Administration --- - /** - * @notice Modify an uint256 param - * @param parameter The name of the parameter - * @param val New value for the parameter - */ - function modifyParameters(bytes32 parameter, uint256 val) external isAuthorized { - if (parameter == "minKeeperPayoutValue") { - require(val > 0, "SystemCoinUniswapV2SafeSaviour/null-min-payout"); - minKeeperPayoutValue = val; - } - else if (parameter == "restrictUsage") { - require(val <= 1, "SystemCoinUniswapV2SafeSaviour/invalid-restriction"); - restrictUsage = val; - } - else revert("SystemCoinUniswapV2SafeSaviour/modify-unrecognized-param"); - emit ModifyParameters(parameter, val); - } - /** - * @notice Modify an address param - * @param parameter The name of the parameter - * @param data New address for the parameter - */ - function modifyParameters(bytes32 parameter, address data) external isAuthorized { - require(data != address(0), "SystemCoinUniswapV2SafeSaviour/null-data"); - - if (parameter == "systemCoinOrcl") { - systemCoinOrcl = PriceFeedLike(data); - systemCoinOrcl.getResultWithValidity(); - } - else if (parameter == "oracleRelayer") { - oracleRelayer = OracleRelayerLike(data); - oracleRelayer.redemptionPrice(); - } - else if (parameter == "liquidityManager") { - liquidityManager = UniswapLiquidityManagerLike(data); - } - else if (parameter == "liquidationEngine") { - liquidationEngine = LiquidationEngineLike(data); - } - else if (parameter == "taxCollector") { - taxCollector = TaxCollectorLike(data); - } - else if (parameter == "swapManager") { - swapManager = SwapManagerLike(data); - } - else revert("SystemCoinUniswapV2SafeSaviour/modify-unrecognized-param"); - emit ModifyParameters(parameter, data); - } - - // --- Adding/Withdrawing Cover --- - /* - * @notice Deposit lpToken in the contract in order to provide cover for a specific SAFE managed by the SAFE Manager - * @param safeID The ID of the SAFE to protect. This ID should be registered inside GebSafeManager - * @param lpTokenAmount The amount of collateralToken to deposit - */ - function deposit(uint256 safeID, uint256 lpTokenAmount) external isAllowed() liquidationEngineApproved(address(this)) nonReentrant { - require(lpTokenAmount > 0, "SystemCoinUniswapV2SafeSaviour/null-lp-amount"); - - // Check that the SAFE exists inside GebSafeManager - address safeHandler = safeManager.safes(safeID); - require(safeHandler != address(0), "SystemCoinUniswapV2SafeSaviour/null-handler"); - - // Check that the SAFE has debt - (, uint256 safeDebt) = - SAFEEngineLike(collateralJoin.safeEngine()).safes(collateralJoin.collateralType(), safeHandler); - require(safeDebt > 0, "SystemCoinUniswapV2SafeSaviour/safe-does-not-have-debt"); - - // Update the lpToken balance used to cover the SAFE and transfer tokens to this contract - lpTokenCover[safeHandler] = add(lpTokenCover[safeHandler], lpTokenAmount); - require(lpToken.transferFrom(msg.sender, address(this), lpTokenAmount), "SystemCoinUniswapV2SafeSaviour/could-not-transfer-lp"); - - emit Deposit(msg.sender, safeHandler, lpTokenAmount); - } - /* - * @notice Withdraw lpToken from the contract and provide less cover for a SAFE - * @dev Only an address that controls the SAFE inside the SAFE Manager can call this - * @param safeID The ID of the SAFE to remove cover from. This ID should be registered inside the SAFE Manager - * @param lpTokenAmount The amount of lpToken to withdraw - * @param dst The address that will receive the LP tokens - */ - function withdraw(uint256 safeID, uint256 lpTokenAmount, address dst) external controlsSAFE(msg.sender, safeID) nonReentrant { - require(lpTokenAmount > 0, "SystemCoinUniswapV2SafeSaviour/null-lp-amount"); - - // Fetch the handler from the SAFE manager - address safeHandler = safeManager.safes(safeID); - require(lpTokenCover[safeHandler] >= lpTokenAmount, "SystemCoinUniswapV2SafeSaviour/not-enough-to-withdraw"); - - // Withdraw cover and transfer collateralToken to the caller - lpTokenCover[safeHandler] = sub(lpTokenCover[safeHandler], lpTokenAmount); - lpToken.transfer(dst, lpTokenAmount); - - emit Withdraw(msg.sender, safeHandler, dst, lpTokenAmount); - } - - // --- Transferring Reserves --- - /* - * @notify Get back system coins or collateral tokens that were not used to save a specific SAFE - * @param safeID The ID of the safe that was previously saved and has leftover funds that can be withdrawn - * @param dst The address that will receive - */ - function getReserves(uint256 safeID, address dst) external controlsSAFE(msg.sender, safeID) nonReentrant { - address safeHandler = safeManager.safes(safeID); - (uint256 systemCoins, uint256 collateralCoins) = - (underlyingReserves[safeHandler].systemCoins, underlyingReserves[safeHandler].collateralCoins); - - require(either(systemCoins > 0, collateralCoins > 0), "SystemCoinUniswapV2SafeSaviour/no-reserves"); - delete(underlyingReserves[safeManager.safes(safeID)]); - - if (systemCoins > 0) { - systemCoin.transfer(dst, systemCoins); - } - - if (collateralCoins > 0) { - collateralToken.transfer(dst, collateralCoins); - } - - emit GetReserves(msg.sender, safeHandler, systemCoins, collateralCoins, dst); - } - - // --- Saving Logic --- - /* - * @notice Saves a SAFE by withdrawing liquidity and repaying debt and/or adding more collateral - * @dev Only the LiquidationEngine can call this - * @param keeper The keeper that called LiquidationEngine.liquidateSAFE and that should be rewarded for spending gas to save a SAFE - * @param collateralType The collateral type backing the SAFE that's being liquidated - * @param safeHandler The handler of the SAFE that's being liquidated - * @return Whether the SAFE has been saved, the amount of LP tokens that were used to withdraw liquidity as well as the amount of - * system coins sent to the keeper as their payment (this implementation always returns 0) - */ - function saveSAFE(address keeper, bytes32 collateralType, address safeHandler) override external returns (bool, uint256, uint256) { - require(address(liquidationEngine) == msg.sender, "SystemCoinUniswapV2SafeSaviour/caller-not-liquidation-engine"); - require(keeper != address(0), "SystemCoinUniswapV2SafeSaviour/null-keeper-address"); - - if (both(both(collateralType == "", safeHandler == address(0)), keeper == address(liquidationEngine))) { - return (true, uint(-1), uint(-1)); - } - - // Check that we're handling the correct collateral - require(collateralType == collateralJoin.collateralType(), "SystemCoinUniswapV2SafeSaviour/invalid-collateral-type"); - - // Check that the SAFE has a non null amount of LP tokens covering it - require(lpTokenCover[safeHandler] > 0, "SystemCoinUniswapV2SafeSaviour/null-cover"); - - // Tax the collateral - taxCollector.taxSingle(collateralType); - - // Get the amount of tokens used to top up the SAFE - (uint256 safeDebtRepaid, uint256 safeCollateralAdded) = - getTokensForSaving(safeHandler, oracleRelayer.redemptionPrice()); - - // There must be tokens used to save the SAVE - require(either(safeDebtRepaid > 0, safeCollateralAdded > 0), "SystemCoinUniswapV2SafeSaviour/cannot-save-safe"); - - // Get the amounts of tokens sent to the keeper as payment - (uint256 keeperSysCoins, uint256 keeperCollateralCoins) = - getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), safeDebtRepaid, safeCollateralAdded); - - // There must be tokens that go to the keeper - require(either(keeperSysCoins > 0, keeperCollateralCoins > 0), "SystemCoinUniswapV2SafeSaviour/cannot-pay-keeper"); - - // Mark the SAFE in the registry as just having been saved - saviourRegistry.markSave(collateralType, safeHandler); - - // Withdraw all liquidity - uint256 sysCoinBalance = systemCoin.balanceOf(address(this)); - uint256 collateralCoinBalance = collateralToken.balanceOf(address(this)); - - lpToken.approve(address(liquidityManager), lpTokenCover[safeHandler]); - liquidityManager.removeLiquidity(lpTokenCover[safeHandler], 0, 0, address(this)); - - // Swap pair tokens to collateral tokens - { - pairToken.approve(address(swapManager), pairToken.balanceOf(address(this))); - swapManager.swap(address(pairToken), address(collateralToken), pairToken.balanceOf(address(this)), 1, address(this)); - } - - // Checks after removing liquidity and swapping pair tokens - require( - either(systemCoin.balanceOf(address(this)) > sysCoinBalance, collateralToken.balanceOf(address(this)) > collateralCoinBalance), - "SystemCoinUniswapV2SafeSaviour/faulty-remove-liquidity" - ); - - // Compute remaining balances of tokens that will go into reserves - sysCoinBalance = sub(sub(systemCoin.balanceOf(address(this)), sysCoinBalance), add(safeDebtRepaid, keeperSysCoins)); - collateralCoinBalance = sub( - sub(collateralToken.balanceOf(address(this)), collateralCoinBalance), add(safeCollateralAdded, keeperCollateralCoins) - ); - - // Update reserves - if (sysCoinBalance > 0) { - underlyingReserves[safeHandler].systemCoins = add( - underlyingReserves[safeHandler].systemCoins, sysCoinBalance - ); - } - if (collateralCoinBalance > 0) { - underlyingReserves[safeHandler].collateralCoins = add( - underlyingReserves[safeHandler].collateralCoins, collateralCoinBalance - ); - } - - // Save the SAFE - if (safeDebtRepaid > 0) { - // Approve the coin join contract to take system coins and repay debt - systemCoin.approve(address(coinJoin), safeDebtRepaid); - // Calculate the non adjusted system coin amount - uint256 nonAdjustedSystemCoinsToRepay = div(mul(safeDebtRepaid, RAY), getAccumulatedRate(collateralType)); - - // Join system coins in the system and repay the SAFE's debt - coinJoin.join(address(this), safeDebtRepaid); - safeEngine.modifySAFECollateralization( - collateralType, - safeHandler, - address(0), - address(this), - int256(0), - -int256(nonAdjustedSystemCoinsToRepay) - ); - } - - if (safeCollateralAdded > 0) { - // Approve collateralToken to the collateral join contract - collateralToken.approve(address(collateralJoin), safeCollateralAdded); - - // Join collateralToken in the system and add it in the saved SAFE - collateralJoin.join(address(this), safeCollateralAdded); - safeEngine.modifySAFECollateralization( - collateralType, - safeHandler, - address(this), - address(0), - int256(safeCollateralAdded), - int256(0) - ); - } - - // Pay keeper - if (keeperSysCoins > 0) { - systemCoin.transfer(keeper, keeperSysCoins); - } - - if (keeperCollateralCoins > 0) { - collateralToken.transfer(keeper, keeperCollateralCoins); - } - - // Emit an event - uint256 totalCover = lpTokenCover[safeHandler]; - delete(lpTokenCover[safeHandler]); - - emit SaveSAFE(keeper, collateralType, safeHandler, totalCover); - - return (true, totalCover, 0); - } - - // --- Getters --- - /* - * @notify Must be implemented according to the interface although it always returns 0 - */ - function getKeeperPayoutValue() override public returns (uint256) { - return 0; - } - /* - * @notify Must be implemented according to the interface although it always returns false - */ - function keeperPayoutExceedsMinValue() override public returns (bool) { - return false; - } - /* - * @notice Determine whether a SAFE can be saved with the current amount of lpTokenCover deposited as cover for it - * @param safeHandler The handler of the SAFE which the function takes into account - * @return Whether the SAFE can be saved or not - */ - function canSave(bytes32, address safeHandler) override external returns (bool) { - // Fetch the redemption price first - uint256 redemptionPrice = oracleRelayer.redemptionPrice(); - - // Fetch the amount of tokens used to save the SAFE - (uint256 safeDebtRepaid, uint256 safeCollateralAdded) = - getTokensForSaving(safeHandler, redemptionPrice); - - // Fetch the amount of tokens sent to the keeper - (uint256 keeperSysCoins, uint256 keeperCollateralCoins) = - getKeeperPayoutTokens(safeHandler, redemptionPrice, safeDebtRepaid, safeCollateralAdded); - - // If there are some tokens used to save the SAFE and some tokens used to repay the keeper, return true - if (both( - either(safeDebtRepaid > 0, safeCollateralAdded > 0), - either(keeperSysCoins > 0, keeperCollateralCoins > 0) - )) { - return true; - } - - return false; - } - /* - * @notice Return the total amount of LP tokens covering a specific SAFE - * @param collateralType The SAFE collateral type (ignored in this implementation) - * @param safeHandler The handler of the SAFE which the function takes into account - * @return The total LP token cover for a specific SAFE - */ - function tokenAmountUsedToSave(bytes32, address safeHandler) override public returns (uint256) { - return lpTokenCover[safeHandler]; - } - /* - * @notify Fetch the collateral's price - */ - function getCollateralPrice() public view returns (uint256) { - (address ethFSM,,) = oracleRelayer.collateralTypes(collateralJoin.collateralType()); - if (ethFSM == address(0)) return 0; - - (uint256 priceFeedValue, bool hasValidValue) = PriceFeedLike(ethFSM).getResultWithValidity(); - if (!hasValidValue) return 0; - - return priceFeedValue; - } - /* - * @notify Fetch the system coin's market price - */ - function getSystemCoinMarketPrice() public view returns (uint256) { - (uint256 priceFeedValue, bool hasValidValue) = systemCoinOrcl.getResultWithValidity(); - if (!hasValidValue) return 0; - - return priceFeedValue; - } - /* - * @notify Get the target collateralization ratio that a SAFE should have after it's saved - * @param safeHandler The handler/address of the SAFE whose target collateralization ratio is retrieved - */ - function getTargetCRatio(address safeHandler) public view returns (uint256) { - bytes32 collateralType = collateralJoin.collateralType(); - uint256 defaultCRatio = cRatioSetter.defaultDesiredCollateralizationRatios(collateralType); - uint256 targetCRatio = (cRatioSetter.desiredCollateralizationRatios(collateralType, safeHandler) == 0) ? - defaultCRatio : cRatioSetter.desiredCollateralizationRatios(collateralType, safeHandler); - return targetCRatio; - } - /* - * @notify Return the amount of system coins and collateral tokens retrieved from the - * LP position covering a specific SAFE + converting the pair token to collateral tokens - * @param safeHandler The handler/address of the targeted SAFE - */ - function getLPUnderlying(address safeHandler) public view returns (uint256, uint256) { - uint256 coverAmount = lpTokenCover[safeHandler]; - - if (coverAmount == 0) return (0, 0); - - (uint256 sysCoinsFromLP, uint256 pairTokenFromLP) = (isSystemCoinToken0) ? - (liquidityManager.getToken0FromLiquidity(coverAmount), liquidityManager.getToken1FromLiquidity(coverAmount)) : - (liquidityManager.getToken1FromLiquidity(coverAmount), liquidityManager.getToken0FromLiquidity(coverAmount)); - - uint256 collateralFromLP = swapManager.getAmountOut(address(pairToken), address(collateralToken), pairTokenFromLP); - - return (sysCoinsFromLP, collateralFromLP); - } - /* - * @notice Return the amount of system coins and/or collateral tokens used to save a SAFE - * @param safeHandler The handler/address of the targeted SAFE - * @param redemptionPrice The system coin redemption price used in calculations - */ - function getTokensForSaving(address safeHandler, uint256 redemptionPrice) - public view returns (uint256, uint256) { - if (either(lpTokenCover[safeHandler] == 0, redemptionPrice == 0)) { - return (0, 0); - } - - // Get the default CRatio for the SAFE - (uint256 depositedCollateralToken, uint256 safeDebt) = - SAFEEngineLike(collateralJoin.safeEngine()).safes(collateralJoin.collateralType(), safeHandler); - uint256 targetCRatio = getTargetCRatio(safeHandler); - if (either(safeDebt == 0, targetCRatio == 0)) { - return (0, 0); - } - - // Get the collateral market price - uint256 collateralPrice = getCollateralPrice(); - if (collateralPrice == 0) { - return (0, 0); - } - - // Calculate underlying amounts received from LP withdrawal - (uint256 sysCoinsFromLP, uint256 collateralFromLP) = getLPUnderlying(safeHandler); - - // Calculate how much debt would need to be repaid - { - uint256 debtToRepay = mul( - mul(HUNDRED, mul(depositedCollateralToken, collateralPrice) / WAD) / targetCRatio, RAY - ) / redemptionPrice; - - if (either(debtToRepay >= safeDebt, debtBelowFloor(collateralJoin.collateralType(), debtToRepay))) { - return (0, 0); - } - safeDebt = mul(safeDebt, getAccumulatedRate(collateralJoin.collateralType())) / RAY; - debtToRepay = sub(safeDebt, debtToRepay); - - // Determine total debt to repay; return if the SAFE can be saved solely by repaying debt, continue calculations otherwise - if (sysCoinsFromLP >= debtToRepay) { - return (debtToRepay, 0); - } - } - - // Calculate the amount of collateral that would need to be added to the SAFE - uint256 debtGap = sub(safeDebt, sysCoinsFromLP); - uint256 scaledDownDebtValue = mul(add(mul(redemptionPrice, debtGap) / RAY, ONE), targetCRatio) / HUNDRED; - uint256 collateralTokenNeeded = div(mul(scaledDownDebtValue, WAD), collateralPrice); - collateralTokenNeeded = (depositedCollateralToken < collateralTokenNeeded) ? - sub(collateralTokenNeeded, depositedCollateralToken) : MAX_UINT; - - // See if there's enough collateral to add to the SAFE in order to save it - if (collateralTokenNeeded <= collateralFromLP) { - return (sysCoinsFromLP, collateralTokenNeeded); - } else { - return (0, 0); - } - } - /* - * @notice Return the amount of system coins and/or collateral tokens used to pay a keeper - * @param safeHandler The handler/address of the targeted SAFE - * @param redemptionPrice The system coin redemption price used in calculations - * @param safeDebtRepaid The amount of system coins that are already used to save the targeted SAFE - * @param safeCollateralAdded The amount of collateral tokens that are already used to save the targeted SAFE - */ - function getKeeperPayoutTokens(address safeHandler, uint256 redemptionPrice, uint256 safeDebtRepaid, uint256 safeCollateralAdded) - public view returns (uint256, uint256) { - // Get the system coin and collateral market prices - uint256 collateralPrice = getCollateralPrice(); - uint256 sysCoinMarketPrice = getSystemCoinMarketPrice(); - if (either(collateralPrice == 0, sysCoinMarketPrice == 0)) { - return (0, 0); - } - - // Calculate underlying amounts received from LP withdrawal - (uint256 sysCoinsFromLP, uint256 collateralFromLP) = getLPUnderlying(safeHandler); - - // Check if the keeper can get system coins and if yes, compute how many - uint256 keeperSysCoins; - if (sysCoinsFromLP > safeDebtRepaid) { - uint256 remainingSystemCoins = sub(sysCoinsFromLP, safeDebtRepaid); - uint256 payoutInSystemCoins = div(mul(minKeeperPayoutValue, WAD), sysCoinMarketPrice); - - if (payoutInSystemCoins <= remainingSystemCoins) { - return (payoutInSystemCoins, 0); - } else { - keeperSysCoins = remainingSystemCoins; - } - } - - // Calculate how much collateral the keeper will get - if (collateralFromLP <= safeCollateralAdded) return (0, 0); - - uint256 remainingCollateral = sub(collateralFromLP, safeCollateralAdded); - uint256 remainingKeeperPayoutValue = sub(minKeeperPayoutValue, mul(keeperSysCoins, sysCoinMarketPrice) / WAD); - uint256 collateralTokenNeeded = div(mul(remainingKeeperPayoutValue, WAD), collateralPrice); - - // If there are enough collateral tokens retreived from LP in order to pay the keeper, return the token amounts - if (collateralTokenNeeded <= remainingCollateral) { - return (keeperSysCoins, collateralTokenNeeded); - } else { - // Otherwise, return zeroes - return (0, 0); - } - } - /* - * @notify Returns whether a target debt amount is below the debt floor of a specific collateral type - * @param collateralType The collateral type whose floor we compare against - * @param targetDebtAmount The target debt amount for a SAFE that has collateralType collateral in it - */ - function debtBelowFloor(bytes32 collateralType, uint256 targetDebtAmount) public view returns (bool) { - (, , , , uint256 debtFloor, ) = safeEngine.collateralTypes(collateralType); - return (mul(targetDebtAmount, RAY) < debtFloor); - } - /* - * @notify Get the accumulated interest rate for a specific collateral type - * @param The collateral type for which to retrieve the rate - */ - function getAccumulatedRate(bytes32 collateralType) - public view returns (uint256 accumulatedRate) { - (, accumulatedRate, , , , ) = safeEngine.collateralTypes(collateralType); - } -} +// Copyright (C) 2021 Reflexer Labs, INC + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity 0.6.7; + +import "../interfaces/SwapManagerLike.sol"; +import "../interfaces/UniswapLiquidityManagerLike.sol"; +import "../interfaces/SaviourCRatioSetterLike.sol"; +import "../interfaces/SafeSaviourLike.sol"; +import "../math/SafeMath.sol"; + +contract SystemCoinUniswapV2SafeSaviour is SafeMath, SafeSaviourLike { + // --- Auth --- + mapping (address => uint256) public authorizedAccounts; + /** + * @notice Add auth to an account + * @param account Account to add auth to + */ + function addAuthorization(address account) external isAuthorized { + authorizedAccounts[account] = 1; + emit AddAuthorization(account); + } + /** + * @notice Remove auth from an account + * @param account Account to remove auth from + */ + function removeAuthorization(address account) external isAuthorized { + authorizedAccounts[account] = 0; + emit RemoveAuthorization(account); + } + /** + * @notice Checks whether msg.sender can call an authed function + **/ + modifier isAuthorized { + require(authorizedAccounts[msg.sender] == 1, "SystemCoinUniswapV2SafeSaviour/account-not-authorized"); + _; + } + + mapping (address => uint256) public allowedUsers; + /** + * @notice Allow a user to deposit assets + * @param usr User to whitelist + */ + function allowUser(address usr) external isAuthorized { + allowedUsers[usr] = 1; + emit AllowUser(usr); + } + /** + * @notice Disallow a user from depositing assets + * @param usr User to disallow + */ + function disallowUser(address usr) external isAuthorized { + allowedUsers[usr] = 0; + emit DisallowUser(usr); + } + /** + * @notice Checks whether an address is an allowed user + **/ + modifier isAllowed { + require( + either(restrictUsage == 0, both(restrictUsage == 1, allowedUsers[msg.sender] == 1)), + "SystemCoinUniswapV2SafeSaviour/account-not-allowed" + ); + _; + } + + // --- Structs --- + struct Reserves { + uint256 systemCoins; + uint256 collateralCoins; + } + + // --- Variables --- + // Flag that tells whether usage of the contract is restricted to allowed users + uint256 public restrictUsage; + + // Whether the system coin is token0 in the Uniswap pool or not + bool public isSystemCoinToken0; + // Amount of LP tokens currently protecting each position + mapping(address => uint256) public lpTokenCover; + // Amount of system coin/collateral tokens that Safe owners can get back + mapping(address => Reserves) public underlyingReserves; + // Liquidity manager contract for Uniswap v2/v3 + UniswapLiquidityManagerLike public liquidityManager; + // The contract that can swap tokens + SwapManagerLike public swapManager; + // The ERC20 system coin + ERC20Like public systemCoin; + // The system coin join contract + CoinJoinLike public coinJoin; + // The collateral join contract for adding collateral in the system + CollateralJoinLike public collateralJoin; + // The LP token + ERC20Like public lpToken; + // The collateral token + ERC20Like public collateralToken; + // The second token in the Uniswap pair (besides the system coin) + ERC20Like public pairToken; + // Oracle providing the system coin price feed + PriceFeedLike public systemCoinOrcl; + // Contract that defines desired CRatios for each Safe after it is saved + SaviourCRatioSetterLike public cRatioSetter; + + // --- Events --- + event AddAuthorization(address account); + event RemoveAuthorization(address account); + event AllowUser(address usr); + event DisallowUser(address usr); + event ModifyParameters(bytes32 indexed parameter, uint256 val); + event ModifyParameters(bytes32 indexed parameter, address data); + event Deposit( + address indexed caller, + address indexed safeHandler, + uint256 lpTokenAmount + ); + event Withdraw( + address indexed caller, + address indexed safeHandler, + address dst, + uint256 lpTokenAmount + ); + event GetReserves( + address indexed caller, + address indexed safeHandler, + uint256 systemCoinAmount, + uint256 collateralAmount, + address dst + ); + + constructor( + bool isSystemCoinToken0_, + address coinJoin_, + address collateralJoin_, + address cRatioSetter_, + address systemCoinOrcl_, + address liquidationEngine_, + address oracleRelayer_, + address safeManager_, + address saviourRegistry_, + address liquidityManager_, + address swapManager_, + address pairToken_, + address lpToken_, + uint256 minKeeperPayoutValue_ + ) public { + require(coinJoin_ != address(0), "SystemCoinUniswapV2SafeSaviour/null-coin-join"); + require(collateralJoin_ != address(0), "SystemCoinUniswapV2SafeSaviour/null-collateral-join"); + require(cRatioSetter_ != address(0), "SystemCoinUniswapV2SafeSaviour/null-cratio-setter"); + require(systemCoinOrcl_ != address(0), "SystemCoinUniswapV2SafeSaviour/null-system-coin-oracle"); + require(oracleRelayer_ != address(0), "SystemCoinUniswapV2SafeSaviour/null-oracle-relayer"); + require(liquidationEngine_ != address(0), "SystemCoinUniswapV2SafeSaviour/null-liquidation-engine"); + require(safeManager_ != address(0), "SystemCoinUniswapV2SafeSaviour/null-safe-manager"); + require(saviourRegistry_ != address(0), "SystemCoinUniswapV2SafeSaviour/null-saviour-registry"); + require(liquidityManager_ != address(0), "SystemCoinUniswapV2SafeSaviour/null-liq-manager"); + require(swapManager_ != address(0), "SystemCoinUniswapV2SafeSaviour/null-swap-manager"); + require(lpToken_ != address(0), "SystemCoinUniswapV2SafeSaviour/null-lp-token"); + require(pairToken_ != address(0), "SystemCoinUniswapV2SafeSaviour/null-pair-token"); + require(minKeeperPayoutValue_ > 0, "SystemCoinUniswapV2SafeSaviour/invalid-min-payout-value"); + + authorizedAccounts[msg.sender] = 1; + + isSystemCoinToken0 = isSystemCoinToken0_; + minKeeperPayoutValue = minKeeperPayoutValue_; + + coinJoin = CoinJoinLike(coinJoin_); + collateralJoin = CollateralJoinLike(collateralJoin_); + cRatioSetter = SaviourCRatioSetterLike(cRatioSetter_); + liquidationEngine = LiquidationEngineLike(liquidationEngine_); + oracleRelayer = OracleRelayerLike(oracleRelayer_); + systemCoinOrcl = PriceFeedLike(systemCoinOrcl_); + systemCoin = ERC20Like(coinJoin.systemCoin()); + safeEngine = SAFEEngineLike(coinJoin.safeEngine()); + safeManager = GebSafeManagerLike(safeManager_); + saviourRegistry = SAFESaviourRegistryLike(saviourRegistry_); + liquidityManager = UniswapLiquidityManagerLike(liquidityManager_); + swapManager = SwapManagerLike(swapManager_); + pairToken = ERC20Like(pairToken_); + lpToken = ERC20Like(lpToken_); + collateralToken = ERC20Like(collateralJoin.collateral()); + + systemCoinOrcl.getResultWithValidity(); + oracleRelayer.redemptionPrice(); + + require(collateralJoin.contractEnabled() == 1, "SystemCoinUniswapV2SafeSaviour/join-disabled"); + require(address(collateralToken) != address(0), "SystemCoinUniswapV2SafeSaviour/null-col-token"); + require(address(safeEngine) != address(0), "SystemCoinUniswapV2SafeSaviour/null-safe-engine"); + require(address(systemCoin) != address(0), "SystemCoinUniswapV2SafeSaviour/null-sys-coin"); + + emit AddAuthorization(msg.sender); + emit ModifyParameters("minKeeperPayoutValue", minKeeperPayoutValue); + emit ModifyParameters("oracleRelayer", oracleRelayer_); + emit ModifyParameters("systemCoinOrcl", systemCoinOrcl_); + emit ModifyParameters("liquidationEngine", liquidationEngine_); + emit ModifyParameters("liquidityManager", liquidityManager_); + emit ModifyParameters("swapManager", swapManager_); + } + + // --- Administration --- + /** + * @notice Modify an uint256 param + * @param parameter The name of the parameter + * @param val New value for the parameter + */ + function modifyParameters(bytes32 parameter, uint256 val) external isAuthorized { + if (parameter == "minKeeperPayoutValue") { + require(val > 0, "SystemCoinUniswapV2SafeSaviour/null-min-payout"); + minKeeperPayoutValue = val; + } + else if (parameter == "restrictUsage") { + require(val <= 1, "SystemCoinUniswapV2SafeSaviour/invalid-restriction"); + restrictUsage = val; + } + else revert("SystemCoinUniswapV2SafeSaviour/modify-unrecognized-param"); + emit ModifyParameters(parameter, val); + } + /** + * @notice Modify an address param + * @param parameter The name of the parameter + * @param data New address for the parameter + */ + function modifyParameters(bytes32 parameter, address data) external isAuthorized { + require(data != address(0), "SystemCoinUniswapV2SafeSaviour/null-data"); + + if (parameter == "systemCoinOrcl") { + systemCoinOrcl = PriceFeedLike(data); + systemCoinOrcl.getResultWithValidity(); + } + else if (parameter == "oracleRelayer") { + oracleRelayer = OracleRelayerLike(data); + oracleRelayer.redemptionPrice(); + } + else if (parameter == "liquidityManager") { + liquidityManager = UniswapLiquidityManagerLike(data); + } + else if (parameter == "liquidationEngine") { + liquidationEngine = LiquidationEngineLike(data); + } + else if (parameter == "taxCollector") { + taxCollector = TaxCollectorLike(data); + } + else if (parameter == "swapManager") { + swapManager = SwapManagerLike(data); + } + else revert("SystemCoinUniswapV2SafeSaviour/modify-unrecognized-param"); + emit ModifyParameters(parameter, data); + } + + // --- Adding/Withdrawing Cover --- + /* + * @notice Deposit lpToken in the contract in order to provide cover for a specific SAFE managed by the SAFE Manager + * @param safeID The ID of the SAFE to protect. This ID should be registered inside GebSafeManager + * @param lpTokenAmount The amount of collateralToken to deposit + */ + function deposit(uint256 safeID, uint256 lpTokenAmount) external isAllowed() liquidationEngineApproved(address(this)) nonReentrant { + require(lpTokenAmount > 0, "SystemCoinUniswapV2SafeSaviour/null-lp-amount"); + + // Check that the SAFE exists inside GebSafeManager + address safeHandler = safeManager.safes(safeID); + require(safeHandler != address(0), "SystemCoinUniswapV2SafeSaviour/null-handler"); + + // Check that the SAFE has debt + (, uint256 safeDebt) = + SAFEEngineLike(collateralJoin.safeEngine()).safes(collateralJoin.collateralType(), safeHandler); + require(safeDebt > 0, "SystemCoinUniswapV2SafeSaviour/safe-does-not-have-debt"); + + // Update the lpToken balance used to cover the SAFE and transfer tokens to this contract + lpTokenCover[safeHandler] = add(lpTokenCover[safeHandler], lpTokenAmount); + require(lpToken.transferFrom(msg.sender, address(this), lpTokenAmount), "SystemCoinUniswapV2SafeSaviour/could-not-transfer-lp"); + + emit Deposit(msg.sender, safeHandler, lpTokenAmount); + } + /* + * @notice Withdraw lpToken from the contract and provide less cover for a SAFE + * @dev Only an address that controls the SAFE inside the SAFE Manager can call this + * @param safeID The ID of the SAFE to remove cover from. This ID should be registered inside the SAFE Manager + * @param lpTokenAmount The amount of lpToken to withdraw + * @param dst The address that will receive the LP tokens + */ + function withdraw(uint256 safeID, uint256 lpTokenAmount, address dst) external controlsSAFE(msg.sender, safeID) nonReentrant { + require(lpTokenAmount > 0, "SystemCoinUniswapV2SafeSaviour/null-lp-amount"); + + // Fetch the handler from the SAFE manager + address safeHandler = safeManager.safes(safeID); + require(lpTokenCover[safeHandler] >= lpTokenAmount, "SystemCoinUniswapV2SafeSaviour/not-enough-to-withdraw"); + + // Withdraw cover and transfer collateralToken to the caller + lpTokenCover[safeHandler] = sub(lpTokenCover[safeHandler], lpTokenAmount); + lpToken.transfer(dst, lpTokenAmount); + + emit Withdraw(msg.sender, safeHandler, dst, lpTokenAmount); + } + + // --- Transferring Reserves --- + /* + * @notify Get back system coins or collateral tokens that were not used to save a specific SAFE + * @param safeID The ID of the safe that was previously saved and has leftover funds that can be withdrawn + * @param dst The address that will receive + */ + function getReserves(uint256 safeID, address dst) external controlsSAFE(msg.sender, safeID) nonReentrant { + address safeHandler = safeManager.safes(safeID); + (uint256 systemCoins, uint256 collateralCoins) = + (underlyingReserves[safeHandler].systemCoins, underlyingReserves[safeHandler].collateralCoins); + + require(either(systemCoins > 0, collateralCoins > 0), "SystemCoinUniswapV2SafeSaviour/no-reserves"); + delete(underlyingReserves[safeManager.safes(safeID)]); + + if (systemCoins > 0) { + systemCoin.transfer(dst, systemCoins); + } + + if (collateralCoins > 0) { + collateralToken.transfer(dst, collateralCoins); + } + + emit GetReserves(msg.sender, safeHandler, systemCoins, collateralCoins, dst); + } + + // --- Saving Logic --- + /* + * @notice Saves a SAFE by withdrawing liquidity and repaying debt and/or adding more collateral + * @dev Only the LiquidationEngine can call this + * @param keeper The keeper that called LiquidationEngine.liquidateSAFE and that should be rewarded for spending gas to save a SAFE + * @param collateralType The collateral type backing the SAFE that's being liquidated + * @param safeHandler The handler of the SAFE that's being liquidated + * @return Whether the SAFE has been saved, the amount of LP tokens that were used to withdraw liquidity as well as the amount of + * system coins sent to the keeper as their payment (this implementation always returns 0) + */ + function saveSAFE(address keeper, bytes32 collateralType, address safeHandler) override external returns (bool, uint256, uint256) { + require(address(liquidationEngine) == msg.sender, "SystemCoinUniswapV2SafeSaviour/caller-not-liquidation-engine"); + require(keeper != address(0), "SystemCoinUniswapV2SafeSaviour/null-keeper-address"); + + if (both(both(collateralType == "", safeHandler == address(0)), keeper == address(liquidationEngine))) { + return (true, uint(-1), uint(-1)); + } + + // Check that we're handling the correct collateral + require(collateralType == collateralJoin.collateralType(), "SystemCoinUniswapV2SafeSaviour/invalid-collateral-type"); + + // Check that the SAFE has a non null amount of LP tokens covering it + require(lpTokenCover[safeHandler] > 0, "SystemCoinUniswapV2SafeSaviour/null-cover"); + + // Tax the collateral + taxCollector.taxSingle(collateralType); + + // Get the amount of tokens used to top up the SAFE + (uint256 safeDebtRepaid, uint256 safeCollateralAdded) = + getTokensForSaving(safeHandler, oracleRelayer.redemptionPrice()); + + // There must be tokens used to save the SAVE + require(either(safeDebtRepaid > 0, safeCollateralAdded > 0), "SystemCoinUniswapV2SafeSaviour/cannot-save-safe"); + + // Get the amounts of tokens sent to the keeper as payment + (uint256 keeperSysCoins, uint256 keeperCollateralCoins) = + getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), safeDebtRepaid, safeCollateralAdded); + + // There must be tokens that go to the keeper + require(either(keeperSysCoins > 0, keeperCollateralCoins > 0), "SystemCoinUniswapV2SafeSaviour/cannot-pay-keeper"); + + // Mark the SAFE in the registry as just having been saved + saviourRegistry.markSave(collateralType, safeHandler); + + // Withdraw all liquidity + uint256 sysCoinBalance = systemCoin.balanceOf(address(this)); + uint256 collateralCoinBalance = collateralToken.balanceOf(address(this)); + + lpToken.approve(address(liquidityManager), lpTokenCover[safeHandler]); + liquidityManager.removeLiquidity(lpTokenCover[safeHandler], 0, 0, address(this)); + + // Swap pair tokens to collateral tokens + { + pairToken.approve(address(swapManager), pairToken.balanceOf(address(this))); + swapManager.swap(address(pairToken), address(collateralToken), pairToken.balanceOf(address(this)), 1, address(this)); + } + + // Checks after removing liquidity and swapping pair tokens + require( + either(systemCoin.balanceOf(address(this)) > sysCoinBalance, collateralToken.balanceOf(address(this)) > collateralCoinBalance), + "SystemCoinUniswapV2SafeSaviour/faulty-remove-liquidity" + ); + + // Compute remaining balances of tokens that will go into reserves + sysCoinBalance = sub(sub(systemCoin.balanceOf(address(this)), sysCoinBalance), add(safeDebtRepaid, keeperSysCoins)); + collateralCoinBalance = sub( + sub(collateralToken.balanceOf(address(this)), collateralCoinBalance), add(safeCollateralAdded, keeperCollateralCoins) + ); + + // Update reserves + if (sysCoinBalance > 0) { + underlyingReserves[safeHandler].systemCoins = add( + underlyingReserves[safeHandler].systemCoins, sysCoinBalance + ); + } + if (collateralCoinBalance > 0) { + underlyingReserves[safeHandler].collateralCoins = add( + underlyingReserves[safeHandler].collateralCoins, collateralCoinBalance + ); + } + + // Save the SAFE + if (safeDebtRepaid > 0) { + // Approve the coin join contract to take system coins and repay debt + systemCoin.approve(address(coinJoin), safeDebtRepaid); + // Calculate the non adjusted system coin amount + uint256 nonAdjustedSystemCoinsToRepay = div(mul(safeDebtRepaid, RAY), getAccumulatedRate(collateralType)); + + // Join system coins in the system and repay the SAFE's debt + coinJoin.join(address(this), safeDebtRepaid); + safeEngine.modifySAFECollateralization( + collateralType, + safeHandler, + address(0), + address(this), + int256(0), + -int256(nonAdjustedSystemCoinsToRepay) + ); + } + + if (safeCollateralAdded > 0) { + // Approve collateralToken to the collateral join contract + collateralToken.approve(address(collateralJoin), safeCollateralAdded); + + // Join collateralToken in the system and add it in the saved SAFE + collateralJoin.join(address(this), safeCollateralAdded); + safeEngine.modifySAFECollateralization( + collateralType, + safeHandler, + address(this), + address(0), + int256(safeCollateralAdded), + int256(0) + ); + } + + // Pay keeper + if (keeperSysCoins > 0) { + systemCoin.transfer(keeper, keeperSysCoins); + } + + if (keeperCollateralCoins > 0) { + collateralToken.transfer(keeper, keeperCollateralCoins); + } + + // Emit an event + uint256 totalCover = lpTokenCover[safeHandler]; + delete(lpTokenCover[safeHandler]); + + emit SaveSAFE(keeper, collateralType, safeHandler, totalCover); + + return (true, totalCover, 0); + } + + // --- Getters --- + /* + * @notify Must be implemented according to the interface although it always returns 0 + */ + function getKeeperPayoutValue() override public returns (uint256) { + return 0; + } + /* + * @notify Must be implemented according to the interface although it always returns false + */ + function keeperPayoutExceedsMinValue() override public returns (bool) { + return false; + } + /* + * @notice Determine whether a SAFE can be saved with the current amount of lpTokenCover deposited as cover for it + * @param safeHandler The handler of the SAFE which the function takes into account + * @return Whether the SAFE can be saved or not + */ + function canSave(bytes32, address safeHandler) override external returns (bool) { + // Fetch the redemption price first + uint256 redemptionPrice = oracleRelayer.redemptionPrice(); + + // Fetch the amount of tokens used to save the SAFE + (uint256 safeDebtRepaid, uint256 safeCollateralAdded) = + getTokensForSaving(safeHandler, redemptionPrice); + + // Fetch the amount of tokens sent to the keeper + (uint256 keeperSysCoins, uint256 keeperCollateralCoins) = + getKeeperPayoutTokens(safeHandler, redemptionPrice, safeDebtRepaid, safeCollateralAdded); + + // If there are some tokens used to save the SAFE and some tokens used to repay the keeper, return true + if (both( + either(safeDebtRepaid > 0, safeCollateralAdded > 0), + either(keeperSysCoins > 0, keeperCollateralCoins > 0) + )) { + return true; + } + + return false; + } + /* + * @notice Return the total amount of LP tokens covering a specific SAFE + * @param collateralType The SAFE collateral type (ignored in this implementation) + * @param safeHandler The handler of the SAFE which the function takes into account + * @return The total LP token cover for a specific SAFE + */ + function tokenAmountUsedToSave(bytes32, address safeHandler) override public returns (uint256) { + return lpTokenCover[safeHandler]; + } + /* + * @notify Fetch the collateral's price + */ + function getCollateralPrice() public view returns (uint256) { + (address ethFSM,,) = oracleRelayer.collateralTypes(collateralJoin.collateralType()); + if (ethFSM == address(0)) return 0; + + (uint256 priceFeedValue, bool hasValidValue) = PriceFeedLike(ethFSM).getResultWithValidity(); + if (!hasValidValue) return 0; + + return priceFeedValue; + } + /* + * @notify Fetch the system coin's market price + */ + function getSystemCoinMarketPrice() public view returns (uint256) { + (uint256 priceFeedValue, bool hasValidValue) = systemCoinOrcl.getResultWithValidity(); + if (!hasValidValue) return 0; + + return priceFeedValue; + } + /* + * @notify Get the target collateralization ratio that a SAFE should have after it's saved + * @param safeHandler The handler/address of the SAFE whose target collateralization ratio is retrieved + */ + function getTargetCRatio(address safeHandler) public view returns (uint256) { + bytes32 collateralType = collateralJoin.collateralType(); + uint256 defaultCRatio = cRatioSetter.defaultDesiredCollateralizationRatios(collateralType); + uint256 targetCRatio = (cRatioSetter.desiredCollateralizationRatios(collateralType, safeHandler) == 0) ? + defaultCRatio : cRatioSetter.desiredCollateralizationRatios(collateralType, safeHandler); + return targetCRatio; + } + /* + * @notify Return the amount of system coins and collateral tokens retrieved from the + * LP position covering a specific SAFE + converting the pair token to collateral tokens + * @param safeHandler The handler/address of the targeted SAFE + */ + function getLPUnderlying(address safeHandler) public view returns (uint256, uint256) { + uint256 coverAmount = lpTokenCover[safeHandler]; + + if (coverAmount == 0) return (0, 0); + + (uint256 sysCoinsFromLP, uint256 pairTokenFromLP) = (isSystemCoinToken0) ? + (liquidityManager.getToken0FromLiquidity(coverAmount), liquidityManager.getToken1FromLiquidity(coverAmount)) : + (liquidityManager.getToken1FromLiquidity(coverAmount), liquidityManager.getToken0FromLiquidity(coverAmount)); + + uint256 collateralFromLP = swapManager.getAmountOut(address(pairToken), address(collateralToken), pairTokenFromLP); + + return (sysCoinsFromLP, collateralFromLP); + } + /* + * @notice Return the amount of system coins and/or collateral tokens used to save a SAFE + * @param safeHandler The handler/address of the targeted SAFE + * @param redemptionPrice The system coin redemption price used in calculations + */ + function getTokensForSaving(address safeHandler, uint256 redemptionPrice) + public view returns (uint256, uint256) { + if (either(lpTokenCover[safeHandler] == 0, redemptionPrice == 0)) { + return (0, 0); + } + + // Get the default CRatio for the SAFE + (uint256 depositedCollateralToken, uint256 safeDebt) = + SAFEEngineLike(collateralJoin.safeEngine()).safes(collateralJoin.collateralType(), safeHandler); + uint256 targetCRatio = getTargetCRatio(safeHandler); + if (either(safeDebt == 0, targetCRatio == 0)) { + return (0, 0); + } + + // Get the collateral market price + uint256 collateralPrice = getCollateralPrice(); + if (collateralPrice == 0) { + return (0, 0); + } + + // Calculate underlying amounts received from LP withdrawal + (uint256 sysCoinsFromLP, uint256 collateralFromLP) = getLPUnderlying(safeHandler); + + // Calculate how much debt would need to be repaid + { + uint256 debtToRepay = mul( + mul(HUNDRED, mul(depositedCollateralToken, collateralPrice) / WAD) / targetCRatio, RAY + ) / redemptionPrice; + + if (either(debtToRepay >= safeDebt, debtBelowFloor(collateralJoin.collateralType(), debtToRepay))) { + return (0, 0); + } + safeDebt = mul(safeDebt, getAccumulatedRate(collateralJoin.collateralType())) / RAY; + debtToRepay = sub(safeDebt, debtToRepay); + + // Determine total debt to repay; return if the SAFE can be saved solely by repaying debt, continue calculations otherwise + if (sysCoinsFromLP >= debtToRepay) { + return (debtToRepay, 0); + } + } + + // Calculate the amount of collateral that would need to be added to the SAFE + uint256 debtGap = sub(safeDebt, sysCoinsFromLP); + uint256 scaledDownDebtValue = mul(add(mul(redemptionPrice, debtGap) / RAY, ONE), targetCRatio) / HUNDRED; + uint256 collateralTokenNeeded = div(mul(scaledDownDebtValue, WAD), collateralPrice); + collateralTokenNeeded = (depositedCollateralToken < collateralTokenNeeded) ? + sub(collateralTokenNeeded, depositedCollateralToken) : MAX_UINT; + + // See if there's enough collateral to add to the SAFE in order to save it + if (collateralTokenNeeded <= collateralFromLP) { + return (sysCoinsFromLP, collateralTokenNeeded); + } else { + return (0, 0); + } + } + /* + * @notice Return the amount of system coins and/or collateral tokens used to pay a keeper + * @param safeHandler The handler/address of the targeted SAFE + * @param redemptionPrice The system coin redemption price used in calculations + * @param safeDebtRepaid The amount of system coins that are already used to save the targeted SAFE + * @param safeCollateralAdded The amount of collateral tokens that are already used to save the targeted SAFE + */ + function getKeeperPayoutTokens(address safeHandler, uint256 redemptionPrice, uint256 safeDebtRepaid, uint256 safeCollateralAdded) + public view returns (uint256, uint256) { + // Get the system coin and collateral market prices + uint256 collateralPrice = getCollateralPrice(); + uint256 sysCoinMarketPrice = getSystemCoinMarketPrice(); + if (either(collateralPrice == 0, sysCoinMarketPrice == 0)) { + return (0, 0); + } + + // Calculate underlying amounts received from LP withdrawal + (uint256 sysCoinsFromLP, uint256 collateralFromLP) = getLPUnderlying(safeHandler); + + // Check if the keeper can get system coins and if yes, compute how many + uint256 keeperSysCoins; + if (sysCoinsFromLP > safeDebtRepaid) { + uint256 remainingSystemCoins = sub(sysCoinsFromLP, safeDebtRepaid); + uint256 payoutInSystemCoins = div(mul(minKeeperPayoutValue, WAD), sysCoinMarketPrice); + + if (payoutInSystemCoins <= remainingSystemCoins) { + return (payoutInSystemCoins, 0); + } else { + keeperSysCoins = remainingSystemCoins; + } + } + + // Calculate how much collateral the keeper will get + if (collateralFromLP <= safeCollateralAdded) return (0, 0); + + uint256 remainingCollateral = sub(collateralFromLP, safeCollateralAdded); + uint256 remainingKeeperPayoutValue = sub(minKeeperPayoutValue, mul(keeperSysCoins, sysCoinMarketPrice) / WAD); + uint256 collateralTokenNeeded = div(mul(remainingKeeperPayoutValue, WAD), collateralPrice); + + // If there are enough collateral tokens retreived from LP in order to pay the keeper, return the token amounts + if (collateralTokenNeeded <= remainingCollateral) { + return (keeperSysCoins, collateralTokenNeeded); + } else { + // Otherwise, return zeroes + return (0, 0); + } + } + /* + * @notify Returns whether a target debt amount is below the debt floor of a specific collateral type + * @param collateralType The collateral type whose floor we compare against + * @param targetDebtAmount The target debt amount for a SAFE that has collateralType collateral in it + */ + function debtBelowFloor(bytes32 collateralType, uint256 targetDebtAmount) public view returns (bool) { + (, , , , uint256 debtFloor, ) = safeEngine.collateralTypes(collateralType); + return (mul(targetDebtAmount, RAY) < debtFloor); + } + /* + * @notify Get the accumulated interest rate for a specific collateral type + * @param The collateral type for which to retrieve the rate + */ + function getAccumulatedRate(bytes32 collateralType) + public view returns (uint256 accumulatedRate) { + (, accumulatedRate, , , , ) = safeEngine.collateralTypes(collateralType); + } +} diff --git a/src/saviours/SystemCoinUniswapV3SafeSaviour.sol b/src/saviours/SystemCoinUniswapV3SafeSaviour.sol index 1107b4e..e817b13 100644 --- a/src/saviours/SystemCoinUniswapV3SafeSaviour.sol +++ b/src/saviours/SystemCoinUniswapV3SafeSaviour.sol @@ -1,654 +1,654 @@ -// Copyright (C) 2021 Reflexer Labs, INC - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -pragma solidity 0.6.7; - -import "../interfaces/SwapManagerLike.sol"; -import "../interfaces/UniswapLiquidityManagerLike.sol"; -import "../interfaces/SaviourCRatioSetterLike.sol"; -import "../interfaces/SafeSaviourLike.sol"; -import "../math/SafeMath.sol"; - -contract SystemCoinUniswapV3SafeSaviour is SafeMath, SafeSaviourLike { - // --- Auth --- - mapping (address => uint256) public authorizedAccounts; - /** - * @notice Add auth to an account - * @param account Account to add auth to - */ - function addAuthorization(address account) external isAuthorized { - authorizedAccounts[account] = 1; - emit AddAuthorization(account); - } - /** - * @notice Remove auth from an account - * @param account Account to remove auth from - */ - function removeAuthorization(address account) external isAuthorized { - authorizedAccounts[account] = 0; - emit RemoveAuthorization(account); - } - /** - * @notice Checks whether msg.sender can call an authed function - **/ - modifier isAuthorized { - require(authorizedAccounts[msg.sender] == 1, "SystemCoinUniswapV3SafeSaviour/account-not-authorized"); - _; - } - - mapping (address => uint256) public allowedUsers; - /** - * @notice Allow a user to deposit assets - * @param usr User to whitelist - */ - function allowUser(address usr) external isAuthorized { - allowedUsers[usr] = 1; - emit AllowUser(usr); - } - /** - * @notice Disallow a user from depositing assets - * @param usr User to disallow - */ - function disallowUser(address usr) external isAuthorized { - allowedUsers[usr] = 0; - emit DisallowUser(usr); - } - /** - * @notice Checks whether an address is an allowed user - **/ - modifier isAllowed { - require( - either(restrictUsage == 0, both(restrictUsage == 1, allowedUsers[msg.sender] == 1)), - "SystemCoinUniswapV3SafeSaviour/account-not-allowed" - ); - _; - } - - // --- Structs --- - struct Reserves { - uint256 systemCoins; - uint256 collateralCoins; - } - - // --- Variables --- - // Flag that tells whether usage of the contract is restricted to allowed users - uint256 public restrictUsage; - - // Whether the system coin is token0 in the Uniswap pool or not - bool public isSystemCoinToken0; - // Amount of LP tokens currently protecting each position - mapping(address => uint256) public lpTokenCover; - // Amount of system coin/collateral tokens that Safe owners can get back - mapping(address => Reserves) public underlyingReserves; - // Liquidity manager contract for Uniswap v2/v3 - UniswapLiquidityManagerLike public liquidityManager; - // The contract that can swap tokens - SwapManagerLike public swapManager; - // The ERC20 system coin - ERC20Like public systemCoin; - // The system coin join contract - CoinJoinLike public coinJoin; - // The collateral join contract for adding collateral in the system - CollateralJoinLike public collateralJoin; - // The LP token - ERC20Like public lpToken; - // The collateral token - ERC20Like public collateralToken; - // The second token in the Uniswap pair (besides the system coin) - ERC20Like public pairToken; - // Oracle providing the system coin price feed - PriceFeedLike public systemCoinOrcl; - // Contract that defines desired CRatios for each Safe after it is saved - SaviourCRatioSetterLike public cRatioSetter; - - // --- Events --- - event AddAuthorization(address account); - event RemoveAuthorization(address account); - event AllowUser(address usr); - event DisallowUser(address usr); - event ModifyParameters(bytes32 indexed parameter, uint256 val); - event ModifyParameters(bytes32 indexed parameter, address data); - event Deposit( - address indexed caller, - address indexed safeHandler, - uint256 lpTokenAmount - ); - event Withdraw( - address indexed caller, - address indexed safeHandler, - address dst, - uint256 lpTokenAmount - ); - event GetReserves( - address indexed caller, - address indexed safeHandler, - uint256 systemCoinAmount, - uint256 collateralAmount, - address dst - ); - - constructor( - bool isSystemCoinToken0_, - address coinJoin_, - address collateralJoin_, - address cRatioSetter_, - address systemCoinOrcl_, - address liquidationEngine_, - address oracleRelayer_, - address safeManager_, - address saviourRegistry_, - address liquidityManager_, - address swapManager_, - address pairToken_, - address lpToken_, - uint256 minKeeperPayoutValue_ - ) public { - require(coinJoin_ != address(0), "SystemCoinUniswapV3SafeSaviour/null-coin-join"); - require(collateralJoin_ != address(0), "SystemCoinUniswapV3SafeSaviour/null-collateral-join"); - require(cRatioSetter_ != address(0), "SystemCoinUniswapV3SafeSaviour/null-cratio-setter"); - require(systemCoinOrcl_ != address(0), "SystemCoinUniswapV3SafeSaviour/null-system-coin-oracle"); - require(oracleRelayer_ != address(0), "SystemCoinUniswapV3SafeSaviour/null-oracle-relayer"); - require(liquidationEngine_ != address(0), "SystemCoinUniswapV3SafeSaviour/null-liquidation-engine"); - require(safeManager_ != address(0), "SystemCoinUniswapV3SafeSaviour/null-safe-manager"); - require(saviourRegistry_ != address(0), "SystemCoinUniswapV3SafeSaviour/null-saviour-registry"); - require(liquidityManager_ != address(0), "SystemCoinUniswapV3SafeSaviour/null-liq-manager"); - require(swapManager_ != address(0), "SystemCoinUniswapV3SafeSaviour/null-swap-manager"); - require(lpToken_ != address(0), "SystemCoinUniswapV3SafeSaviour/null-lp-token"); - require(pairToken_ != address(0), "SystemCoinUniswapV3SafeSaviour/null-pair-token"); - require(minKeeperPayoutValue_ > 0, "SystemCoinUniswapV3SafeSaviour/invalid-min-payout-value"); - - authorizedAccounts[msg.sender] = 1; - - isSystemCoinToken0 = isSystemCoinToken0_; - minKeeperPayoutValue = minKeeperPayoutValue_; - - coinJoin = CoinJoinLike(coinJoin_); - collateralJoin = CollateralJoinLike(collateralJoin_); - cRatioSetter = SaviourCRatioSetterLike(cRatioSetter_); - liquidationEngine = LiquidationEngineLike(liquidationEngine_); - oracleRelayer = OracleRelayerLike(oracleRelayer_); - systemCoinOrcl = PriceFeedLike(systemCoinOrcl_); - systemCoin = ERC20Like(coinJoin.systemCoin()); - safeEngine = SAFEEngineLike(coinJoin.safeEngine()); - safeManager = GebSafeManagerLike(safeManager_); - saviourRegistry = SAFESaviourRegistryLike(saviourRegistry_); - liquidityManager = UniswapLiquidityManagerLike(liquidityManager_); - swapManager = SwapManagerLike(swapManager_); - pairToken = ERC20Like(pairToken_); - lpToken = ERC20Like(lpToken_); - collateralToken = ERC20Like(collateralJoin.collateral()); - - systemCoinOrcl.getResultWithValidity(); - oracleRelayer.redemptionPrice(); - - require(collateralJoin.contractEnabled() == 1, "SystemCoinUniswapV3SafeSaviour/join-disabled"); - require(address(collateralToken) != address(0), "SystemCoinUniswapV3SafeSaviour/null-col-token"); - require(address(safeEngine) != address(0), "SystemCoinUniswapV3SafeSaviour/null-safe-engine"); - require(address(systemCoin) != address(0), "SystemCoinUniswapV3SafeSaviour/null-sys-coin"); - - emit AddAuthorization(msg.sender); - emit ModifyParameters("minKeeperPayoutValue", minKeeperPayoutValue); - emit ModifyParameters("oracleRelayer", oracleRelayer_); - emit ModifyParameters("systemCoinOrcl", systemCoinOrcl_); - emit ModifyParameters("liquidationEngine", liquidationEngine_); - emit ModifyParameters("liquidityManager", liquidityManager_); - emit ModifyParameters("swapManager", swapManager_); - } - - // --- Administration --- - /** - * @notice Modify an uint256 param - * @param parameter The name of the parameter - * @param val New value for the parameter - */ - function modifyParameters(bytes32 parameter, uint256 val) external isAuthorized { - if (parameter == "minKeeperPayoutValue") { - require(val > 0, "SystemCoinUniswapV3SafeSaviour/null-min-payout"); - minKeeperPayoutValue = val; - } - else if (parameter == "restrictUsage") { - require(val <= 1, "SystemCoinUniswapV3SafeSaviour/invalid-restriction"); - restrictUsage = val; - } - else revert("SystemCoinUniswapV3SafeSaviour/modify-unrecognized-param"); - emit ModifyParameters(parameter, val); - } - /** - * @notice Modify an address param - * @param parameter The name of the parameter - * @param data New address for the parameter - */ - function modifyParameters(bytes32 parameter, address data) external isAuthorized { - require(data != address(0), "SystemCoinUniswapV3SafeSaviour/null-data"); - - if (parameter == "systemCoinOrcl") { - systemCoinOrcl = PriceFeedLike(data); - systemCoinOrcl.getResultWithValidity(); - } - else if (parameter == "oracleRelayer") { - oracleRelayer = OracleRelayerLike(data); - oracleRelayer.redemptionPrice(); - } - else if (parameter == "liquidityManager") { - liquidityManager = UniswapLiquidityManagerLike(data); - } - else if (parameter == "liquidationEngine") { - liquidationEngine = LiquidationEngineLike(data); - } - else if (parameter == "swapManager") { - swapManager = SwapManagerLike(data); - } - else revert("SystemCoinUniswapV3SafeSaviour/modify-unrecognized-param"); - emit ModifyParameters(parameter, data); - } - - // --- Adding/Withdrawing Cover --- - /* - * @notice Deposit lpToken in the contract in order to provide cover for a specific SAFE managed by the SAFE Manager - * @param safeID The ID of the SAFE to protect. This ID should be registered inside GebSafeManager - * @param lpTokenAmount The amount of collateralToken to deposit - */ - function deposit(uint256 safeID, uint256 lpTokenAmount) external isAllowed() liquidationEngineApproved(address(this)) nonReentrant { - require(lpTokenAmount > 0, "SystemCoinUniswapV3SafeSaviour/null-lp-amount"); - - // Check that the SAFE exists inside GebSafeManager - address safeHandler = safeManager.safes(safeID); - require(safeHandler != address(0), "SystemCoinUniswapV3SafeSaviour/null-handler"); - - // Check that the SAFE has debt - (, uint256 safeDebt) = - SAFEEngineLike(collateralJoin.safeEngine()).safes(collateralJoin.collateralType(), safeHandler); - require(safeDebt > 0, "SystemCoinUniswapV3SafeSaviour/safe-does-not-have-debt"); - - // Update the lpToken balance used to cover the SAFE and transfer tokens to this contract - lpTokenCover[safeHandler] = add(lpTokenCover[safeHandler], lpTokenAmount); - require(lpToken.transferFrom(msg.sender, address(this), lpTokenAmount), "SystemCoinUniswapV3SafeSaviour/could-not-transfer-lp"); - - emit Deposit(msg.sender, safeHandler, lpTokenAmount); - } - /* - * @notice Withdraw lpToken from the contract and provide less cover for a SAFE - * @dev Only an address that controls the SAFE inside the SAFE Manager can call this - * @param safeID The ID of the SAFE to remove cover from. This ID should be registered inside the SAFE Manager - * @param lpTokenAmount The amount of lpToken to withdraw - * @param dst The address that will receive the LP tokens - */ - function withdraw(uint256 safeID, uint256 lpTokenAmount, address dst) external controlsSAFE(msg.sender, safeID) nonReentrant { - require(lpTokenAmount > 0, "SystemCoinUniswapV3SafeSaviour/null-lp-amount"); - - // Fetch the handler from the SAFE manager - address safeHandler = safeManager.safes(safeID); - require(lpTokenCover[safeHandler] >= lpTokenAmount, "SystemCoinUniswapV3SafeSaviour/not-enough-to-withdraw"); - - // Withdraw cover and transfer collateralToken to the caller - lpTokenCover[safeHandler] = sub(lpTokenCover[safeHandler], lpTokenAmount); - lpToken.transfer(dst, lpTokenAmount); - - emit Withdraw(msg.sender, safeHandler, dst, lpTokenAmount); - } - - // --- Transferring Reserves --- - /* - * @notify Get back system coins or collateral tokens that were not used to save a specific SAFE - * @param safeID The ID of the safe that was previously saved and has leftover funds that can be withdrawn - * @param dst The address that will receive - */ - function getReserves(uint256 safeID, address dst) external controlsSAFE(msg.sender, safeID) nonReentrant { - address safeHandler = safeManager.safes(safeID); - (uint256 systemCoins, uint256 collateralCoins) = - (underlyingReserves[safeHandler].systemCoins, underlyingReserves[safeHandler].collateralCoins); - - require(either(systemCoins > 0, collateralCoins > 0), "SystemCoinUniswapV3SafeSaviour/no-reserves"); - delete(underlyingReserves[safeManager.safes(safeID)]); - - if (systemCoins > 0) { - systemCoin.transfer(dst, systemCoins); - } - - if (collateralCoins > 0) { - collateralToken.transfer(dst, collateralCoins); - } - - emit GetReserves(msg.sender, safeHandler, systemCoins, collateralCoins, dst); - } - - // --- Saving Logic --- - /* - * @notice Saves a SAFE by withdrawing liquidity and repaying debt and/or adding more collateral - * @dev Only the LiquidationEngine can call this - * @param keeper The keeper that called LiquidationEngine.liquidateSAFE and that should be rewarded for spending gas to save a SAFE - * @param collateralType The collateral type backing the SAFE that's being liquidated - * @param safeHandler The handler of the SAFE that's being liquidated - * @return Whether the SAFE has been saved, the amount of LP tokens that were used to withdraw liquidity as well as the amount of - * system coins sent to the keeper as their payment (this implementation always returns 0) - */ - function saveSAFE(address keeper, bytes32 collateralType, address safeHandler) override external returns (bool, uint256, uint256) { - require(address(liquidationEngine) == msg.sender, "SystemCoinUniswapV3SafeSaviour/caller-not-liquidation-engine"); - require(keeper != address(0), "SystemCoinUniswapV3SafeSaviour/null-keeper-address"); - - if (both(both(collateralType == "", safeHandler == address(0)), keeper == address(liquidationEngine))) { - return (true, uint(-1), uint(-1)); - } - - require(collateralType == collateralJoin.collateralType(), "SystemCoinUniswapV3SafeSaviour/invalid-collateral-type"); - - // Check that the SAFE has a non null amount of LP tokens covering it - require(lpTokenCover[safeHandler] > 0, "SystemCoinUniswapV3SafeSaviour/null-cover"); - - // Store cover amount in local var - uint256 totalCover = lpTokenCover[safeHandler]; - delete(lpTokenCover[safeHandler]); - - // Mark the SAFE in the registry as just having been saved - saviourRegistry.markSave(collateralType, safeHandler); - - // Withdraw all liquidity - uint256 sysCoinBalance = systemCoin.balanceOf(address(this)); - uint256 pairTokenBalance = pairToken.balanceOf(address(this)); - uint256 collateralCoinBalance = collateralToken.balanceOf(address(this)); - - lpToken.approve(address(liquidityManager), totalCover); - liquidityManager.removeLiquidity(totalCover, 0, 0, address(this)); - - // Swap pair tokens to collateral tokens - { - uint256 pairTokensReceived = sub(pairToken.balanceOf(address(this)), pairTokenBalance); - pairToken.approve(address(swapManager), pairTokensReceived); - swapManager.swap(address(pairToken), address(collateralToken), pairTokensReceived, 1, address(this)); - } - - // Checks after removing liquidity and swapping pair tokens - require( - either(systemCoin.balanceOf(address(this)) > sysCoinBalance, collateralToken.balanceOf(address(this)) > collateralCoinBalance), - "SystemCoinUniswapV3SafeSaviour/faulty-remove-liquidity" - ); - - // Get amounts withdrawn - sysCoinBalance = sub(systemCoin.balanceOf(address(this)), sysCoinBalance); - collateralCoinBalance = sub(collateralToken.balanceOf(address(this)), collateralCoinBalance); - - // Get the amount of tokens used to top up the SAFE - (uint256 safeDebtRepaid, uint256 safeCollateralAdded) = - getTokensForSaving(safeHandler, oracleRelayer.redemptionPrice(), sysCoinBalance, collateralCoinBalance); - - // There must be tokens used to save the SAVE - require(either(safeDebtRepaid > 0, safeCollateralAdded > 0), "SystemCoinUniswapV3SafeSaviour/cannot-save-safe"); - - // Get the amounts of tokens sent to the keeper as payment - (uint256 keeperSysCoins, uint256 keeperCollateralCoins) = - getKeeperPayoutTokens( - safeHandler, - oracleRelayer.redemptionPrice(), - safeDebtRepaid, - safeCollateralAdded, - sysCoinBalance, - collateralCoinBalance - ); - - // There must be tokens that go to the keeper - require(either(keeperSysCoins > 0, keeperCollateralCoins > 0), "SystemCoinUniswapV3SafeSaviour/cannot-pay-keeper"); - - // Compute remaining balances of tokens that will go into reserves - sysCoinBalance = sub(sysCoinBalance, add(safeDebtRepaid, keeperSysCoins)); - collateralCoinBalance = sub( - collateralCoinBalance, add(safeCollateralAdded, keeperCollateralCoins) - ); - - // Update reserves - if (sysCoinBalance > 0) { - underlyingReserves[safeHandler].systemCoins = add( - underlyingReserves[safeHandler].systemCoins, sysCoinBalance - ); - } - if (collateralCoinBalance > 0) { - underlyingReserves[safeHandler].collateralCoins = add( - underlyingReserves[safeHandler].collateralCoins, collateralCoinBalance - ); - } - - // Save the SAFE - if (safeDebtRepaid > 0) { - // Approve the coin join contract to take system coins and repay debt - systemCoin.approve(address(coinJoin), safeDebtRepaid); - - // Join system coins in the system and repay the SAFE's debt - coinJoin.join(address(this), safeDebtRepaid); - safeEngine.modifySAFECollateralization( - collateralType, - safeHandler, - address(0), - address(this), - int256(0), - -int256(safeDebtRepaid) - ); - } - - if (safeCollateralAdded > 0) { - // Approve collateralToken to the collateral join contract - collateralToken.approve(address(collateralJoin), safeCollateralAdded); - - // Join collateralToken in the system and add it in the saved SAFE - collateralJoin.join(address(this), safeCollateralAdded); - safeEngine.modifySAFECollateralization( - collateralType, - safeHandler, - address(this), - address(0), - int256(safeCollateralAdded), - int256(0) - ); - } - - // Pay keeper - if (keeperSysCoins > 0) { - systemCoin.transfer(keeper, keeperSysCoins); - } - - if (keeperCollateralCoins > 0) { - collateralToken.transfer(keeper, keeperCollateralCoins); - } - - // Emit an event - emit SaveSAFE(keeper, collateralType, safeHandler, totalCover); - - return (true, totalCover, 0); - } - - // --- Getters --- - /* - * @notify Must be implemented according to the interface although it always returns 0 - */ - function getKeeperPayoutValue() override public returns (uint256) { - return 0; - } - /* - * @notify Must be implemented according to the interface although it always returns false - */ - function keeperPayoutExceedsMinValue() override public returns (bool) { - return false; - } - /* - * @notify Must be implemented according to the interface although it always returns false - */ - function canSave(bytes32, address safeHandler) override external returns (bool) { - return false; - } - /* - * @notice Return the total amount of LP tokens covering a specific SAFE - * @param collateralType The SAFE collateral type (ignored in this implementation) - * @param safeHandler The handler of the SAFE which the function takes into account - * @return The total LP token cover for a specific SAFE - */ - function tokenAmountUsedToSave(bytes32, address safeHandler) override public returns (uint256) { - return lpTokenCover[safeHandler]; - } - /* - * @notify Fetch the collateral's price - */ - function getCollateralPrice() public view returns (uint256) { - (address ethFSM,,) = oracleRelayer.collateralTypes(collateralJoin.collateralType()); - if (ethFSM == address(0)) return 0; - - (uint256 priceFeedValue, bool hasValidValue) = PriceFeedLike(ethFSM).getResultWithValidity(); - if (!hasValidValue) return 0; - - return priceFeedValue; - } - /* - * @notify Fetch the system coin's market price - */ - function getSystemCoinMarketPrice() public view returns (uint256) { - (uint256 priceFeedValue, bool hasValidValue) = systemCoinOrcl.getResultWithValidity(); - if (!hasValidValue) return 0; - - return priceFeedValue; - } - /* - * @notify Get the target collateralization ratio that a SAFE should have after it's saved - * @param safeHandler The handler/address of the SAFE whose target collateralization ratio is retrieved - */ - function getTargetCRatio(address safeHandler) public view returns (uint256) { - bytes32 collateralType = collateralJoin.collateralType(); - uint256 defaultCRatio = cRatioSetter.defaultDesiredCollateralizationRatios(collateralType); - uint256 targetCRatio = (cRatioSetter.desiredCollateralizationRatios(collateralType, safeHandler) == 0) ? - defaultCRatio : cRatioSetter.desiredCollateralizationRatios(collateralType, safeHandler); - return targetCRatio; - } - /* - * @notice Return the amount of system coins and/or collateral tokens used to save a SAFE - * @param safeHandler The handler/address of the targeted SAFE - * @param redemptionPrice The system coin redemption price used in calculations - * @param sysCoinsFromLP System coins withdrawn from Uniswap - * @param collateralFromLP Collateral tokens withdrawn from Uniswap - */ - function getTokensForSaving(address safeHandler, uint256 redemptionPrice, uint256 sysCoinsFromLP, uint256 collateralFromLP) - public view returns (uint256, uint256) { - if (either(redemptionPrice == 0, both(sysCoinsFromLP == 0, collateralFromLP == 0))) { - return (0, 0); - } - - // Get the default CRatio for the SAFE - (uint256 depositedCollateralToken, uint256 safeDebt) = - SAFEEngineLike(collateralJoin.safeEngine()).safes(collateralJoin.collateralType(), safeHandler); - uint256 targetCRatio = getTargetCRatio(safeHandler); - if (either(safeDebt == 0, targetCRatio == 0)) { - return (0, 0); - } - - // Get the collateral market price - uint256 collateralPrice = getCollateralPrice(); - if (collateralPrice == 0) { - return (0, 0); - } - - // Calculate how much debt would need to be repaid - { - uint256 debtToRepay = mul( - mul(HUNDRED, mul(depositedCollateralToken, collateralPrice) / WAD) / targetCRatio, RAY - ) / redemptionPrice; - debtToRepay = div(mul(debtToRepay, RAY), getAccumulatedRate(collateralJoin.collateralType())); - - if (debtToRepay >= safeDebt) { - return (0, 0); - } - debtToRepay = sub(safeDebt, debtToRepay); - - // Determine total debt to repay; return if the SAFE can be saved solely by repaying debt, continue calculations otherwise - if (sysCoinsFromLP >= debtToRepay) { - return (debtToRepay, 0); - } - } - - // Calculate the amount of collateral that would need to be added to the SAFE - uint256 debtGap = sub(safeDebt, sysCoinsFromLP); - uint256 scaledDownDebtValue = mul( - mul(redemptionPrice, debtGap) / RAY, getAccumulatedRate(collateralJoin.collateralType()) - ) / RAY; - scaledDownDebtValue = mul( - add(scaledDownDebtValue, ONE), targetCRatio - ) / HUNDRED; - - uint256 collateralTokenNeeded = div(mul(scaledDownDebtValue, WAD), collateralPrice); - collateralTokenNeeded = (either(depositedCollateralToken < collateralTokenNeeded, collateralTokenNeeded == 0)) ? - sub(collateralTokenNeeded, depositedCollateralToken) : MAX_UINT; - - // See if there's enough collateral to add to the SAFE in order to save it - if (collateralTokenNeeded <= collateralFromLP) { - return (sysCoinsFromLP, collateralTokenNeeded); - } else { - return (0, 0); - } - } - /* - * @notice Return the amount of system coins and/or collateral tokens used to pay a keeper - * @param safeHandler The handler/address of the targeted SAFE - * @param redemptionPrice The system coin redemption price used in calculations - * @param safeDebtRepaid The amount of system coins that are already used to save the targeted SAFE - * @param safeCollateralAdded The amount of collateral tokens that are already used to save the targeted SAFE - * @param sysCoinsFromLP System coins withdrawn from Uniswap - * @param collateralFromLP Collateral tokens withdrawn from Uniswap - */ - function getKeeperPayoutTokens( - address safeHandler, - uint256 redemptionPrice, - uint256 safeDebtRepaid, - uint256 safeCollateralAdded, - uint256 sysCoinsFromLP, - uint256 collateralFromLP - ) public view returns (uint256, uint256) { - // Get the system coin and collateral market prices - uint256 collateralPrice = getCollateralPrice(); - uint256 sysCoinMarketPrice = getSystemCoinMarketPrice(); - if (either(collateralPrice == 0, sysCoinMarketPrice == 0)) { - return (0, 0); - } - - // Check if the keeper can get system coins and if yes, compute how many - uint256 keeperSysCoins; - if (sysCoinsFromLP > safeDebtRepaid) { - uint256 remainingSystemCoins = sub(sysCoinsFromLP, safeDebtRepaid); - uint256 payoutInSystemCoins = div(mul(minKeeperPayoutValue, WAD), sysCoinMarketPrice); - - if (payoutInSystemCoins <= remainingSystemCoins) { - return (payoutInSystemCoins, 0); - } else { - keeperSysCoins = remainingSystemCoins; - } - } - - // Calculate how much collateral the keeper will get - if (collateralFromLP <= safeCollateralAdded) return (0, 0); - - uint256 remainingCollateral = sub(collateralFromLP, safeCollateralAdded); - uint256 remainingKeeperPayoutValue = sub(minKeeperPayoutValue, mul(keeperSysCoins, sysCoinMarketPrice) / WAD); - uint256 collateralTokenNeeded = div(mul(remainingKeeperPayoutValue, WAD), collateralPrice); - - // If there are enough collateral tokens retreived from LP in order to pay the keeper, return the token amounts - if (collateralTokenNeeded <= remainingCollateral) { - return (keeperSysCoins, collateralTokenNeeded); - } else { - // Otherwise, return zeroes - return (0, 0); - } - } - /* - * @notify Get the accumulated interest rate for a specific collateral type - * @param The collateral type for which to retrieve the rate - */ - function getAccumulatedRate(bytes32 collateralType) - public view returns (uint256 accumulatedRate) { - (, accumulatedRate, , , , ) = safeEngine.collateralTypes(collateralType); - } -} +// Copyright (C) 2021 Reflexer Labs, INC + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity 0.6.7; + +import "../interfaces/SwapManagerLike.sol"; +import "../interfaces/UniswapLiquidityManagerLike.sol"; +import "../interfaces/SaviourCRatioSetterLike.sol"; +import "../interfaces/SafeSaviourLike.sol"; +import "../math/SafeMath.sol"; + +contract SystemCoinUniswapV3SafeSaviour is SafeMath, SafeSaviourLike { + // --- Auth --- + mapping (address => uint256) public authorizedAccounts; + /** + * @notice Add auth to an account + * @param account Account to add auth to + */ + function addAuthorization(address account) external isAuthorized { + authorizedAccounts[account] = 1; + emit AddAuthorization(account); + } + /** + * @notice Remove auth from an account + * @param account Account to remove auth from + */ + function removeAuthorization(address account) external isAuthorized { + authorizedAccounts[account] = 0; + emit RemoveAuthorization(account); + } + /** + * @notice Checks whether msg.sender can call an authed function + **/ + modifier isAuthorized { + require(authorizedAccounts[msg.sender] == 1, "SystemCoinUniswapV3SafeSaviour/account-not-authorized"); + _; + } + + mapping (address => uint256) public allowedUsers; + /** + * @notice Allow a user to deposit assets + * @param usr User to whitelist + */ + function allowUser(address usr) external isAuthorized { + allowedUsers[usr] = 1; + emit AllowUser(usr); + } + /** + * @notice Disallow a user from depositing assets + * @param usr User to disallow + */ + function disallowUser(address usr) external isAuthorized { + allowedUsers[usr] = 0; + emit DisallowUser(usr); + } + /** + * @notice Checks whether an address is an allowed user + **/ + modifier isAllowed { + require( + either(restrictUsage == 0, both(restrictUsage == 1, allowedUsers[msg.sender] == 1)), + "SystemCoinUniswapV3SafeSaviour/account-not-allowed" + ); + _; + } + + // --- Structs --- + struct Reserves { + uint256 systemCoins; + uint256 collateralCoins; + } + + // --- Variables --- + // Flag that tells whether usage of the contract is restricted to allowed users + uint256 public restrictUsage; + + // Whether the system coin is token0 in the Uniswap pool or not + bool public isSystemCoinToken0; + // Amount of LP tokens currently protecting each position + mapping(address => uint256) public lpTokenCover; + // Amount of system coin/collateral tokens that Safe owners can get back + mapping(address => Reserves) public underlyingReserves; + // Liquidity manager contract for Uniswap v2/v3 + UniswapLiquidityManagerLike public liquidityManager; + // The contract that can swap tokens + SwapManagerLike public swapManager; + // The ERC20 system coin + ERC20Like public systemCoin; + // The system coin join contract + CoinJoinLike public coinJoin; + // The collateral join contract for adding collateral in the system + CollateralJoinLike public collateralJoin; + // The LP token + ERC20Like public lpToken; + // The collateral token + ERC20Like public collateralToken; + // The second token in the Uniswap pair (besides the system coin) + ERC20Like public pairToken; + // Oracle providing the system coin price feed + PriceFeedLike public systemCoinOrcl; + // Contract that defines desired CRatios for each Safe after it is saved + SaviourCRatioSetterLike public cRatioSetter; + + // --- Events --- + event AddAuthorization(address account); + event RemoveAuthorization(address account); + event AllowUser(address usr); + event DisallowUser(address usr); + event ModifyParameters(bytes32 indexed parameter, uint256 val); + event ModifyParameters(bytes32 indexed parameter, address data); + event Deposit( + address indexed caller, + address indexed safeHandler, + uint256 lpTokenAmount + ); + event Withdraw( + address indexed caller, + address indexed safeHandler, + address dst, + uint256 lpTokenAmount + ); + event GetReserves( + address indexed caller, + address indexed safeHandler, + uint256 systemCoinAmount, + uint256 collateralAmount, + address dst + ); + + constructor( + bool isSystemCoinToken0_, + address coinJoin_, + address collateralJoin_, + address cRatioSetter_, + address systemCoinOrcl_, + address liquidationEngine_, + address oracleRelayer_, + address safeManager_, + address saviourRegistry_, + address liquidityManager_, + address swapManager_, + address pairToken_, + address lpToken_, + uint256 minKeeperPayoutValue_ + ) public { + require(coinJoin_ != address(0), "SystemCoinUniswapV3SafeSaviour/null-coin-join"); + require(collateralJoin_ != address(0), "SystemCoinUniswapV3SafeSaviour/null-collateral-join"); + require(cRatioSetter_ != address(0), "SystemCoinUniswapV3SafeSaviour/null-cratio-setter"); + require(systemCoinOrcl_ != address(0), "SystemCoinUniswapV3SafeSaviour/null-system-coin-oracle"); + require(oracleRelayer_ != address(0), "SystemCoinUniswapV3SafeSaviour/null-oracle-relayer"); + require(liquidationEngine_ != address(0), "SystemCoinUniswapV3SafeSaviour/null-liquidation-engine"); + require(safeManager_ != address(0), "SystemCoinUniswapV3SafeSaviour/null-safe-manager"); + require(saviourRegistry_ != address(0), "SystemCoinUniswapV3SafeSaviour/null-saviour-registry"); + require(liquidityManager_ != address(0), "SystemCoinUniswapV3SafeSaviour/null-liq-manager"); + require(swapManager_ != address(0), "SystemCoinUniswapV3SafeSaviour/null-swap-manager"); + require(lpToken_ != address(0), "SystemCoinUniswapV3SafeSaviour/null-lp-token"); + require(pairToken_ != address(0), "SystemCoinUniswapV3SafeSaviour/null-pair-token"); + require(minKeeperPayoutValue_ > 0, "SystemCoinUniswapV3SafeSaviour/invalid-min-payout-value"); + + authorizedAccounts[msg.sender] = 1; + + isSystemCoinToken0 = isSystemCoinToken0_; + minKeeperPayoutValue = minKeeperPayoutValue_; + + coinJoin = CoinJoinLike(coinJoin_); + collateralJoin = CollateralJoinLike(collateralJoin_); + cRatioSetter = SaviourCRatioSetterLike(cRatioSetter_); + liquidationEngine = LiquidationEngineLike(liquidationEngine_); + oracleRelayer = OracleRelayerLike(oracleRelayer_); + systemCoinOrcl = PriceFeedLike(systemCoinOrcl_); + systemCoin = ERC20Like(coinJoin.systemCoin()); + safeEngine = SAFEEngineLike(coinJoin.safeEngine()); + safeManager = GebSafeManagerLike(safeManager_); + saviourRegistry = SAFESaviourRegistryLike(saviourRegistry_); + liquidityManager = UniswapLiquidityManagerLike(liquidityManager_); + swapManager = SwapManagerLike(swapManager_); + pairToken = ERC20Like(pairToken_); + lpToken = ERC20Like(lpToken_); + collateralToken = ERC20Like(collateralJoin.collateral()); + + systemCoinOrcl.getResultWithValidity(); + oracleRelayer.redemptionPrice(); + + require(collateralJoin.contractEnabled() == 1, "SystemCoinUniswapV3SafeSaviour/join-disabled"); + require(address(collateralToken) != address(0), "SystemCoinUniswapV3SafeSaviour/null-col-token"); + require(address(safeEngine) != address(0), "SystemCoinUniswapV3SafeSaviour/null-safe-engine"); + require(address(systemCoin) != address(0), "SystemCoinUniswapV3SafeSaviour/null-sys-coin"); + + emit AddAuthorization(msg.sender); + emit ModifyParameters("minKeeperPayoutValue", minKeeperPayoutValue); + emit ModifyParameters("oracleRelayer", oracleRelayer_); + emit ModifyParameters("systemCoinOrcl", systemCoinOrcl_); + emit ModifyParameters("liquidationEngine", liquidationEngine_); + emit ModifyParameters("liquidityManager", liquidityManager_); + emit ModifyParameters("swapManager", swapManager_); + } + + // --- Administration --- + /** + * @notice Modify an uint256 param + * @param parameter The name of the parameter + * @param val New value for the parameter + */ + function modifyParameters(bytes32 parameter, uint256 val) external isAuthorized { + if (parameter == "minKeeperPayoutValue") { + require(val > 0, "SystemCoinUniswapV3SafeSaviour/null-min-payout"); + minKeeperPayoutValue = val; + } + else if (parameter == "restrictUsage") { + require(val <= 1, "SystemCoinUniswapV3SafeSaviour/invalid-restriction"); + restrictUsage = val; + } + else revert("SystemCoinUniswapV3SafeSaviour/modify-unrecognized-param"); + emit ModifyParameters(parameter, val); + } + /** + * @notice Modify an address param + * @param parameter The name of the parameter + * @param data New address for the parameter + */ + function modifyParameters(bytes32 parameter, address data) external isAuthorized { + require(data != address(0), "SystemCoinUniswapV3SafeSaviour/null-data"); + + if (parameter == "systemCoinOrcl") { + systemCoinOrcl = PriceFeedLike(data); + systemCoinOrcl.getResultWithValidity(); + } + else if (parameter == "oracleRelayer") { + oracleRelayer = OracleRelayerLike(data); + oracleRelayer.redemptionPrice(); + } + else if (parameter == "liquidityManager") { + liquidityManager = UniswapLiquidityManagerLike(data); + } + else if (parameter == "liquidationEngine") { + liquidationEngine = LiquidationEngineLike(data); + } + else if (parameter == "swapManager") { + swapManager = SwapManagerLike(data); + } + else revert("SystemCoinUniswapV3SafeSaviour/modify-unrecognized-param"); + emit ModifyParameters(parameter, data); + } + + // --- Adding/Withdrawing Cover --- + /* + * @notice Deposit lpToken in the contract in order to provide cover for a specific SAFE managed by the SAFE Manager + * @param safeID The ID of the SAFE to protect. This ID should be registered inside GebSafeManager + * @param lpTokenAmount The amount of collateralToken to deposit + */ + function deposit(uint256 safeID, uint256 lpTokenAmount) external isAllowed() liquidationEngineApproved(address(this)) nonReentrant { + require(lpTokenAmount > 0, "SystemCoinUniswapV3SafeSaviour/null-lp-amount"); + + // Check that the SAFE exists inside GebSafeManager + address safeHandler = safeManager.safes(safeID); + require(safeHandler != address(0), "SystemCoinUniswapV3SafeSaviour/null-handler"); + + // Check that the SAFE has debt + (, uint256 safeDebt) = + SAFEEngineLike(collateralJoin.safeEngine()).safes(collateralJoin.collateralType(), safeHandler); + require(safeDebt > 0, "SystemCoinUniswapV3SafeSaviour/safe-does-not-have-debt"); + + // Update the lpToken balance used to cover the SAFE and transfer tokens to this contract + lpTokenCover[safeHandler] = add(lpTokenCover[safeHandler], lpTokenAmount); + require(lpToken.transferFrom(msg.sender, address(this), lpTokenAmount), "SystemCoinUniswapV3SafeSaviour/could-not-transfer-lp"); + + emit Deposit(msg.sender, safeHandler, lpTokenAmount); + } + /* + * @notice Withdraw lpToken from the contract and provide less cover for a SAFE + * @dev Only an address that controls the SAFE inside the SAFE Manager can call this + * @param safeID The ID of the SAFE to remove cover from. This ID should be registered inside the SAFE Manager + * @param lpTokenAmount The amount of lpToken to withdraw + * @param dst The address that will receive the LP tokens + */ + function withdraw(uint256 safeID, uint256 lpTokenAmount, address dst) external controlsSAFE(msg.sender, safeID) nonReentrant { + require(lpTokenAmount > 0, "SystemCoinUniswapV3SafeSaviour/null-lp-amount"); + + // Fetch the handler from the SAFE manager + address safeHandler = safeManager.safes(safeID); + require(lpTokenCover[safeHandler] >= lpTokenAmount, "SystemCoinUniswapV3SafeSaviour/not-enough-to-withdraw"); + + // Withdraw cover and transfer collateralToken to the caller + lpTokenCover[safeHandler] = sub(lpTokenCover[safeHandler], lpTokenAmount); + lpToken.transfer(dst, lpTokenAmount); + + emit Withdraw(msg.sender, safeHandler, dst, lpTokenAmount); + } + + // --- Transferring Reserves --- + /* + * @notify Get back system coins or collateral tokens that were not used to save a specific SAFE + * @param safeID The ID of the safe that was previously saved and has leftover funds that can be withdrawn + * @param dst The address that will receive + */ + function getReserves(uint256 safeID, address dst) external controlsSAFE(msg.sender, safeID) nonReentrant { + address safeHandler = safeManager.safes(safeID); + (uint256 systemCoins, uint256 collateralCoins) = + (underlyingReserves[safeHandler].systemCoins, underlyingReserves[safeHandler].collateralCoins); + + require(either(systemCoins > 0, collateralCoins > 0), "SystemCoinUniswapV3SafeSaviour/no-reserves"); + delete(underlyingReserves[safeManager.safes(safeID)]); + + if (systemCoins > 0) { + systemCoin.transfer(dst, systemCoins); + } + + if (collateralCoins > 0) { + collateralToken.transfer(dst, collateralCoins); + } + + emit GetReserves(msg.sender, safeHandler, systemCoins, collateralCoins, dst); + } + + // --- Saving Logic --- + /* + * @notice Saves a SAFE by withdrawing liquidity and repaying debt and/or adding more collateral + * @dev Only the LiquidationEngine can call this + * @param keeper The keeper that called LiquidationEngine.liquidateSAFE and that should be rewarded for spending gas to save a SAFE + * @param collateralType The collateral type backing the SAFE that's being liquidated + * @param safeHandler The handler of the SAFE that's being liquidated + * @return Whether the SAFE has been saved, the amount of LP tokens that were used to withdraw liquidity as well as the amount of + * system coins sent to the keeper as their payment (this implementation always returns 0) + */ + function saveSAFE(address keeper, bytes32 collateralType, address safeHandler) override external returns (bool, uint256, uint256) { + require(address(liquidationEngine) == msg.sender, "SystemCoinUniswapV3SafeSaviour/caller-not-liquidation-engine"); + require(keeper != address(0), "SystemCoinUniswapV3SafeSaviour/null-keeper-address"); + + if (both(both(collateralType == "", safeHandler == address(0)), keeper == address(liquidationEngine))) { + return (true, uint(-1), uint(-1)); + } + + require(collateralType == collateralJoin.collateralType(), "SystemCoinUniswapV3SafeSaviour/invalid-collateral-type"); + + // Check that the SAFE has a non null amount of LP tokens covering it + require(lpTokenCover[safeHandler] > 0, "SystemCoinUniswapV3SafeSaviour/null-cover"); + + // Store cover amount in local var + uint256 totalCover = lpTokenCover[safeHandler]; + delete(lpTokenCover[safeHandler]); + + // Mark the SAFE in the registry as just having been saved + saviourRegistry.markSave(collateralType, safeHandler); + + // Withdraw all liquidity + uint256 sysCoinBalance = systemCoin.balanceOf(address(this)); + uint256 pairTokenBalance = pairToken.balanceOf(address(this)); + uint256 collateralCoinBalance = collateralToken.balanceOf(address(this)); + + lpToken.approve(address(liquidityManager), totalCover); + liquidityManager.removeLiquidity(totalCover, 0, 0, address(this)); + + // Swap pair tokens to collateral tokens + { + uint256 pairTokensReceived = sub(pairToken.balanceOf(address(this)), pairTokenBalance); + pairToken.approve(address(swapManager), pairTokensReceived); + swapManager.swap(address(pairToken), address(collateralToken), pairTokensReceived, 1, address(this)); + } + + // Checks after removing liquidity and swapping pair tokens + require( + either(systemCoin.balanceOf(address(this)) > sysCoinBalance, collateralToken.balanceOf(address(this)) > collateralCoinBalance), + "SystemCoinUniswapV3SafeSaviour/faulty-remove-liquidity" + ); + + // Get amounts withdrawn + sysCoinBalance = sub(systemCoin.balanceOf(address(this)), sysCoinBalance); + collateralCoinBalance = sub(collateralToken.balanceOf(address(this)), collateralCoinBalance); + + // Get the amount of tokens used to top up the SAFE + (uint256 safeDebtRepaid, uint256 safeCollateralAdded) = + getTokensForSaving(safeHandler, oracleRelayer.redemptionPrice(), sysCoinBalance, collateralCoinBalance); + + // There must be tokens used to save the SAVE + require(either(safeDebtRepaid > 0, safeCollateralAdded > 0), "SystemCoinUniswapV3SafeSaviour/cannot-save-safe"); + + // Get the amounts of tokens sent to the keeper as payment + (uint256 keeperSysCoins, uint256 keeperCollateralCoins) = + getKeeperPayoutTokens( + safeHandler, + oracleRelayer.redemptionPrice(), + safeDebtRepaid, + safeCollateralAdded, + sysCoinBalance, + collateralCoinBalance + ); + + // There must be tokens that go to the keeper + require(either(keeperSysCoins > 0, keeperCollateralCoins > 0), "SystemCoinUniswapV3SafeSaviour/cannot-pay-keeper"); + + // Compute remaining balances of tokens that will go into reserves + sysCoinBalance = sub(sysCoinBalance, add(safeDebtRepaid, keeperSysCoins)); + collateralCoinBalance = sub( + collateralCoinBalance, add(safeCollateralAdded, keeperCollateralCoins) + ); + + // Update reserves + if (sysCoinBalance > 0) { + underlyingReserves[safeHandler].systemCoins = add( + underlyingReserves[safeHandler].systemCoins, sysCoinBalance + ); + } + if (collateralCoinBalance > 0) { + underlyingReserves[safeHandler].collateralCoins = add( + underlyingReserves[safeHandler].collateralCoins, collateralCoinBalance + ); + } + + // Save the SAFE + if (safeDebtRepaid > 0) { + // Approve the coin join contract to take system coins and repay debt + systemCoin.approve(address(coinJoin), safeDebtRepaid); + + // Join system coins in the system and repay the SAFE's debt + coinJoin.join(address(this), safeDebtRepaid); + safeEngine.modifySAFECollateralization( + collateralType, + safeHandler, + address(0), + address(this), + int256(0), + -int256(safeDebtRepaid) + ); + } + + if (safeCollateralAdded > 0) { + // Approve collateralToken to the collateral join contract + collateralToken.approve(address(collateralJoin), safeCollateralAdded); + + // Join collateralToken in the system and add it in the saved SAFE + collateralJoin.join(address(this), safeCollateralAdded); + safeEngine.modifySAFECollateralization( + collateralType, + safeHandler, + address(this), + address(0), + int256(safeCollateralAdded), + int256(0) + ); + } + + // Pay keeper + if (keeperSysCoins > 0) { + systemCoin.transfer(keeper, keeperSysCoins); + } + + if (keeperCollateralCoins > 0) { + collateralToken.transfer(keeper, keeperCollateralCoins); + } + + // Emit an event + emit SaveSAFE(keeper, collateralType, safeHandler, totalCover); + + return (true, totalCover, 0); + } + + // --- Getters --- + /* + * @notify Must be implemented according to the interface although it always returns 0 + */ + function getKeeperPayoutValue() override public returns (uint256) { + return 0; + } + /* + * @notify Must be implemented according to the interface although it always returns false + */ + function keeperPayoutExceedsMinValue() override public returns (bool) { + return false; + } + /* + * @notify Must be implemented according to the interface although it always returns false + */ + function canSave(bytes32, address safeHandler) override external returns (bool) { + return false; + } + /* + * @notice Return the total amount of LP tokens covering a specific SAFE + * @param collateralType The SAFE collateral type (ignored in this implementation) + * @param safeHandler The handler of the SAFE which the function takes into account + * @return The total LP token cover for a specific SAFE + */ + function tokenAmountUsedToSave(bytes32, address safeHandler) override public returns (uint256) { + return lpTokenCover[safeHandler]; + } + /* + * @notify Fetch the collateral's price + */ + function getCollateralPrice() public view returns (uint256) { + (address ethFSM,,) = oracleRelayer.collateralTypes(collateralJoin.collateralType()); + if (ethFSM == address(0)) return 0; + + (uint256 priceFeedValue, bool hasValidValue) = PriceFeedLike(ethFSM).getResultWithValidity(); + if (!hasValidValue) return 0; + + return priceFeedValue; + } + /* + * @notify Fetch the system coin's market price + */ + function getSystemCoinMarketPrice() public view returns (uint256) { + (uint256 priceFeedValue, bool hasValidValue) = systemCoinOrcl.getResultWithValidity(); + if (!hasValidValue) return 0; + + return priceFeedValue; + } + /* + * @notify Get the target collateralization ratio that a SAFE should have after it's saved + * @param safeHandler The handler/address of the SAFE whose target collateralization ratio is retrieved + */ + function getTargetCRatio(address safeHandler) public view returns (uint256) { + bytes32 collateralType = collateralJoin.collateralType(); + uint256 defaultCRatio = cRatioSetter.defaultDesiredCollateralizationRatios(collateralType); + uint256 targetCRatio = (cRatioSetter.desiredCollateralizationRatios(collateralType, safeHandler) == 0) ? + defaultCRatio : cRatioSetter.desiredCollateralizationRatios(collateralType, safeHandler); + return targetCRatio; + } + /* + * @notice Return the amount of system coins and/or collateral tokens used to save a SAFE + * @param safeHandler The handler/address of the targeted SAFE + * @param redemptionPrice The system coin redemption price used in calculations + * @param sysCoinsFromLP System coins withdrawn from Uniswap + * @param collateralFromLP Collateral tokens withdrawn from Uniswap + */ + function getTokensForSaving(address safeHandler, uint256 redemptionPrice, uint256 sysCoinsFromLP, uint256 collateralFromLP) + public view returns (uint256, uint256) { + if (either(redemptionPrice == 0, both(sysCoinsFromLP == 0, collateralFromLP == 0))) { + return (0, 0); + } + + // Get the default CRatio for the SAFE + (uint256 depositedCollateralToken, uint256 safeDebt) = + SAFEEngineLike(collateralJoin.safeEngine()).safes(collateralJoin.collateralType(), safeHandler); + uint256 targetCRatio = getTargetCRatio(safeHandler); + if (either(safeDebt == 0, targetCRatio == 0)) { + return (0, 0); + } + + // Get the collateral market price + uint256 collateralPrice = getCollateralPrice(); + if (collateralPrice == 0) { + return (0, 0); + } + + // Calculate how much debt would need to be repaid + { + uint256 debtToRepay = mul( + mul(HUNDRED, mul(depositedCollateralToken, collateralPrice) / WAD) / targetCRatio, RAY + ) / redemptionPrice; + debtToRepay = div(mul(debtToRepay, RAY), getAccumulatedRate(collateralJoin.collateralType())); + + if (debtToRepay >= safeDebt) { + return (0, 0); + } + debtToRepay = sub(safeDebt, debtToRepay); + + // Determine total debt to repay; return if the SAFE can be saved solely by repaying debt, continue calculations otherwise + if (sysCoinsFromLP >= debtToRepay) { + return (debtToRepay, 0); + } + } + + // Calculate the amount of collateral that would need to be added to the SAFE + uint256 debtGap = sub(safeDebt, sysCoinsFromLP); + uint256 scaledDownDebtValue = mul( + mul(redemptionPrice, debtGap) / RAY, getAccumulatedRate(collateralJoin.collateralType()) + ) / RAY; + scaledDownDebtValue = mul( + add(scaledDownDebtValue, ONE), targetCRatio + ) / HUNDRED; + + uint256 collateralTokenNeeded = div(mul(scaledDownDebtValue, WAD), collateralPrice); + collateralTokenNeeded = (either(depositedCollateralToken < collateralTokenNeeded, collateralTokenNeeded == 0)) ? + sub(collateralTokenNeeded, depositedCollateralToken) : MAX_UINT; + + // See if there's enough collateral to add to the SAFE in order to save it + if (collateralTokenNeeded <= collateralFromLP) { + return (sysCoinsFromLP, collateralTokenNeeded); + } else { + return (0, 0); + } + } + /* + * @notice Return the amount of system coins and/or collateral tokens used to pay a keeper + * @param safeHandler The handler/address of the targeted SAFE + * @param redemptionPrice The system coin redemption price used in calculations + * @param safeDebtRepaid The amount of system coins that are already used to save the targeted SAFE + * @param safeCollateralAdded The amount of collateral tokens that are already used to save the targeted SAFE + * @param sysCoinsFromLP System coins withdrawn from Uniswap + * @param collateralFromLP Collateral tokens withdrawn from Uniswap + */ + function getKeeperPayoutTokens( + address safeHandler, + uint256 redemptionPrice, + uint256 safeDebtRepaid, + uint256 safeCollateralAdded, + uint256 sysCoinsFromLP, + uint256 collateralFromLP + ) public view returns (uint256, uint256) { + // Get the system coin and collateral market prices + uint256 collateralPrice = getCollateralPrice(); + uint256 sysCoinMarketPrice = getSystemCoinMarketPrice(); + if (either(collateralPrice == 0, sysCoinMarketPrice == 0)) { + return (0, 0); + } + + // Check if the keeper can get system coins and if yes, compute how many + uint256 keeperSysCoins; + if (sysCoinsFromLP > safeDebtRepaid) { + uint256 remainingSystemCoins = sub(sysCoinsFromLP, safeDebtRepaid); + uint256 payoutInSystemCoins = div(mul(minKeeperPayoutValue, WAD), sysCoinMarketPrice); + + if (payoutInSystemCoins <= remainingSystemCoins) { + return (payoutInSystemCoins, 0); + } else { + keeperSysCoins = remainingSystemCoins; + } + } + + // Calculate how much collateral the keeper will get + if (collateralFromLP <= safeCollateralAdded) return (0, 0); + + uint256 remainingCollateral = sub(collateralFromLP, safeCollateralAdded); + uint256 remainingKeeperPayoutValue = sub(minKeeperPayoutValue, mul(keeperSysCoins, sysCoinMarketPrice) / WAD); + uint256 collateralTokenNeeded = div(mul(remainingKeeperPayoutValue, WAD), collateralPrice); + + // If there are enough collateral tokens retreived from LP in order to pay the keeper, return the token amounts + if (collateralTokenNeeded <= remainingCollateral) { + return (keeperSysCoins, collateralTokenNeeded); + } else { + // Otherwise, return zeroes + return (0, 0); + } + } + /* + * @notify Get the accumulated interest rate for a specific collateral type + * @param The collateral type for which to retrieve the rate + */ + function getAccumulatedRate(bytes32 collateralType) + public view returns (uint256 accumulatedRate) { + (, accumulatedRate, , , , ) = safeEngine.collateralTypes(collateralType); + } +} diff --git a/src/saviours/YearnSystemCoinSafeSaviour.sol b/src/saviours/YearnSystemCoinSafeSaviour.sol index a8c679f..468e26b 100644 --- a/src/saviours/YearnSystemCoinSafeSaviour.sol +++ b/src/saviours/YearnSystemCoinSafeSaviour.sol @@ -1,459 +1,459 @@ -// Copyright (C) 2021 James Connolly, Reflexer Labs, INC - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -pragma solidity 0.6.7; - -import "../interfaces/YVaultLike.sol"; -import "../interfaces/SaviourCRatioSetterLike.sol"; -import "../interfaces/SafeSaviourLike.sol"; -import "../math/SafeMath.sol"; - -contract YearnSystemCoinSafeSaviour is SafeMath, SafeSaviourLike { - // --- Auth --- - mapping (address => uint256) public authorizedAccounts; - /** - * @notice Add auth to an account - * @param account Account to add auth to - */ - function addAuthorization(address account) external isAuthorized { - authorizedAccounts[account] = 1; - emit AddAuthorization(account); - } - /** - * @notice Remove auth from an account - * @param account Account to remove auth from - */ - function removeAuthorization(address account) external isAuthorized { - authorizedAccounts[account] = 0; - emit RemoveAuthorization(account); - } - /** - * @notice Checks whether msg.sender can call an authed function - **/ - modifier isAuthorized { - require(authorizedAccounts[msg.sender] == 1, "YearnSystemCoinSafeSaviour/account-not-authorized"); - _; - } - - mapping (address => uint256) public allowedUsers; - /** - * @notice Allow a user to deposit assets - * @param usr User to whitelist - */ - function allowUser(address usr) external isAuthorized { - allowedUsers[usr] = 1; - emit AllowUser(usr); - } - /** - * @notice Disallow a user from depositing assets - * @param usr User to disallow - */ - function disallowUser(address usr) external isAuthorized { - allowedUsers[usr] = 0; - emit DisallowUser(usr); - } - /** - * @notice Checks whether an address is an allowed user - **/ - modifier isAllowed { - require( - either(restrictUsage == 0, both(restrictUsage == 1, allowedUsers[msg.sender] == 1)), - "YearnSystemCoinSafeSaviour/account-not-allowed" - ); - _; - } - - // --- Variables --- - // Flag that tells whether usage of the contract is restricted to allowed users - uint256 public restrictUsage; - - // Amount of collateral deposited to cover each SAFE - mapping(bytes32 => mapping(address => uint256)) public yvTokenCover; - // The yVault address - YVaultLike public yVault; - // The ERC20 system coin - ERC20Like public systemCoin; - // The system coin join contract - CoinJoinLike public coinJoin; - // Oracle providing the system coin price feed - PriceFeedLike public systemCoinOrcl; - // Contract that defines desired CRatios for each Safe after it is saved - SaviourCRatioSetterLike public cRatioSetter; - - // --- Events --- - event AddAuthorization(address account); - event RemoveAuthorization(address account); - event AllowUser(address usr); - event DisallowUser(address usr); - event ModifyParameters(bytes32 indexed parameter, uint256 val); - event ModifyParameters(bytes32 indexed parameter, address data); - event Deposit( - address indexed caller, - bytes32 collateralType, - address indexed safeHandler, - uint256 systemCoinAmount, - uint256 yvTokenAmount - ); - event Withdraw( - address indexed caller, - bytes32 collateralType, - address indexed safeHandler, - address dst, - uint256 systemCoinAmount, - uint256 yvTokenAmount - ); - - constructor( - address coinJoin_, - address cRatioSetter_, - address systemCoinOrcl_, - address liquidationEngine_, - address taxCollector_, - address oracleRelayer_, - address safeManager_, - address saviourRegistry_, - address yVault_, - uint256 keeperPayout_, - uint256 minKeeperPayoutValue_ - ) public { - require(coinJoin_ != address(0), "YearnSystemCoinSafeSaviour/null-coin-join"); - require(cRatioSetter_ != address(0), "YearnSystemCoinSafeSaviour/null-cratio-setter"); - require(systemCoinOrcl_ != address(0), "YearnSystemCoinSafeSaviour/null-system-coin-oracle"); - require(oracleRelayer_ != address(0), "YearnSystemCoinSafeSaviour/null-oracle-relayer"); - require(liquidationEngine_ != address(0), "YearnSystemCoinSafeSaviour/null-liquidation-engine"); - require(taxCollector_ != address(0), "YearnSystemCoinSafeSaviour/null-tax-collector"); - require(safeManager_ != address(0), "YearnSystemCoinSafeSaviour/null-safe-manager"); - require(saviourRegistry_ != address(0), "YearnSystemCoinSafeSaviour/null-saviour-registry"); - require(yVault_ != address(0), "YearnSystemCoinSafeSaviour/null-y-vault"); - require(keeperPayout_ > 0, "YearnSystemCoinSafeSaviour/invalid-keeper-payout"); - require(minKeeperPayoutValue_ > 0, "YearnSystemCoinSafeSaviour/invalid-min-payout-value"); - - authorizedAccounts[msg.sender] = 1; - - keeperPayout = keeperPayout_; - - minKeeperPayoutValue = minKeeperPayoutValue_; - coinJoin = CoinJoinLike(coinJoin_); - cRatioSetter = SaviourCRatioSetterLike(cRatioSetter_); - liquidationEngine = LiquidationEngineLike(liquidationEngine_); - taxCollector = TaxCollectorLike(taxCollector_); - - oracleRelayer = OracleRelayerLike(oracleRelayer_); - systemCoinOrcl = PriceFeedLike(systemCoinOrcl_); - systemCoin = ERC20Like(coinJoin.systemCoin()); - safeEngine = SAFEEngineLike(coinJoin.safeEngine()); - safeManager = GebSafeManagerLike(safeManager_); - saviourRegistry = SAFESaviourRegistryLike(saviourRegistry_); - yVault = YVaultLike(yVault_); - - systemCoinOrcl.read(); - systemCoinOrcl.getResultWithValidity(); - oracleRelayer.redemptionPrice(); - - require(address(safeEngine) != address(0), "YearnSystemCoinSafeSaviour/null-safe-engine"); - require(address(systemCoin) != address(0), "YearnSystemCoinSafeSaviour/null-sys-coin"); - - emit AddAuthorization(msg.sender); - emit ModifyParameters("keeperPayout", keeperPayout); - emit ModifyParameters("minKeeperPayoutValue", minKeeperPayoutValue); - emit ModifyParameters("liquidationEngine", liquidationEngine_); - emit ModifyParameters("taxCollector", taxCollector_); - emit ModifyParameters("oracleRelayer", oracleRelayer_); - emit ModifyParameters("systemCoinOrcl", systemCoinOrcl_); - } - - // --- Administration --- - /** - * @notice Modify an uint256 param - * @param parameter The name of the parameter - * @param val New value for the parameter - */ - function modifyParameters(bytes32 parameter, uint256 val) external isAuthorized { - if (parameter == "keeperPayout") { - require(val > 0, "YearnSystemCoinSafeSaviour/null-payout"); - keeperPayout = val; - } - else if (parameter == "minKeeperPayoutValue") { - require(val > 0, "YearnSystemCoinSafeSaviour/null-min-payout"); - minKeeperPayoutValue = val; - } - else if (parameter == "restrictUsage") { - require(val <= 1, "YearnSystemCoinSafeSaviour/invalid-restriction"); - restrictUsage = val; - } - else revert("YearnSystemCoinSafeSaviour/modify-unrecognized-param"); - emit ModifyParameters(parameter, val); - } - /** - * @notice Modify an address param - * @param parameter The name of the parameter - * @param data New address for the parameter - */ - function modifyParameters(bytes32 parameter, address data) external isAuthorized { - require(data != address(0), "YearnSystemCoinSafeSaviour/null-data"); - - if (parameter == "systemCoinOrcl") { - systemCoinOrcl = PriceFeedLike(data); - systemCoinOrcl.read(); - systemCoinOrcl.getResultWithValidity(); - } - else if (parameter == "oracleRelayer") { - oracleRelayer = OracleRelayerLike(data); - oracleRelayer.redemptionPrice(); - } - else if (parameter == "liquidationEngine") { - liquidationEngine = LiquidationEngineLike(data); - } - else if (parameter == "taxCollector") { - taxCollector = TaxCollectorLike(data); - } - else revert("YearnSystemCoinSafeSaviour/modify-unrecognized-param"); - emit ModifyParameters(parameter, data); - } - - // --- Adding/Withdrawing Cover --- - /* - * @notice Deposit systemCoin in the contract and lend in the Yearn vault in order to provide cover for a specific SAFE controlled by the SAFE Manager - * @param collateralType The collateral type used in the SAFE - * @param safeID The ID of the SAFE to protect. This ID should be registered inside GebSafeManager - * @param systemCoinAmount The amount of systemCoin to deposit - */ - function deposit(bytes32 collateralType, uint256 safeID, uint256 systemCoinAmount) - external isAllowed() liquidationEngineApproved(address(this)) nonReentrant { - uint256 defaultCRatio = cRatioSetter.defaultDesiredCollateralizationRatios(collateralType); - require(systemCoinAmount > 0, "YearnSystemCoinSafeSaviour/null-system-coin-amount"); - require(defaultCRatio > 0, "YearnSystemCoinSafeSaviour/collateral-not-set"); - - // Check that the SAFE exists inside GebSafeManager - address safeHandler = safeManager.safes(safeID); - require(safeHandler != address(0), "YearnSystemCoinSafeSaviour/null-handler"); - - // Check that the SAFE has debt - (, uint256 safeDebt) = safeEngine.safes(collateralType, safeHandler); - require(safeDebt > 0, "YearnSystemCoinSafeSaviour/safe-does-not-have-debt"); - - // Deposit into Yearn - systemCoin.transferFrom(msg.sender, address(this), systemCoinAmount); - systemCoin.approve(address(yVault), systemCoinAmount); - uint256 yvTokens = yVault.deposit(systemCoinAmount); // use return value to save on math operations - require(yvTokens > 0, "YearnSystemCoinSafeSaviour/no-vault-tokens-returned"); - - // Update the yvToken balance used to cover the SAFE - yvTokenCover[collateralType][safeHandler] = add(yvTokenCover[collateralType][safeHandler], yvTokens); - - emit Deposit(msg.sender, collateralType, safeHandler, systemCoinAmount, yvTokens); - } - /* - * @notice Withdraw collateralToken from the contract and provide less cover for a SAFE - * @dev Only an address that controls the SAFE inside GebSafeManager can call this - * @param safeID The ID of the SAFE to remove cover from. This ID should be registered inside GebSafeManager - * @param yvTokenAmount The amount of yvTokens to burn - * @param dst The address that will receive the withdrawn system coins - */ - function withdraw(bytes32 collateralType, uint256 safeID, uint256 yvTokenAmount, address dst) - external controlsSAFE(msg.sender, safeID) nonReentrant { - require(yvTokenAmount > 0, "YearnSystemCoinSafeSaviour/null-yvToken-amount"); - - // Fetch the handler from the SAFE manager - address safeHandler = safeManager.safes(safeID); - require(yvTokenCover[collateralType][safeHandler] >= yvTokenAmount, "YearnSystemCoinSafeSaviour/withdraw-request-higher-than-balance"); - - // Redeem system coins from Yearn and transfer them to the caller - yvTokenCover[collateralType][safeHandler] = sub(yvTokenCover[collateralType][safeHandler], yvTokenAmount); - - uint256 withdrawnSysCoinAmount = yVault.withdraw(yvTokenAmount); // use return value to save on math operations - require(withdrawnSysCoinAmount > 0, "YearnSystemCoinSafeSaviour/no-coins-withdrawn"); - systemCoin.transfer(dst, withdrawnSysCoinAmount); - - emit Withdraw( - msg.sender, - collateralType, - safeHandler, - dst, - withdrawnSysCoinAmount, - yvTokenAmount - ); - } - - // --- Saving Logic --- - /* - * @notice Saves a SAFE by repaying some of its debt - * @dev Only the LiquidationEngine can call this - * @param keeper The keeper that called LiquidationEngine.liquidateSAFE and that should be rewarded for spending gas to save a SAFE - * @param collateralType The collateral type backing the SAFE that's being liquidated - * @param safeHandler The handler of the SAFE that's being liquidated - * @return Whether the SAFE has been saved, the amount of system coin debt repaid as well as the amount of - * system coins sent to the keeper as their payment - */ - function saveSAFE(address keeper, bytes32 collateralType, address safeHandler) override external returns (bool, uint256, uint256) { - require(address(liquidationEngine) == msg.sender, "YearnSystemCoinSafeSaviour/caller-not-liquidation-engine"); - require(keeper != address(0), "YearnSystemCoinSafeSaviour/null-keeper-address"); - - if (both(both(collateralType == "", safeHandler == address(0)), keeper == address(liquidationEngine))) { - return (true, uint(-1), uint(-1)); - } - - // Check that the fiat value of the keeper payout is high enough - require(keeperPayoutExceedsMinValue(), "YearnSystemCoinSafeSaviour/small-keeper-payout-value"); - - // Tax the collateral - taxCollector.taxSingle(collateralType); - - // Compute and check the validity of the amount of yvTokens used to save the SAFE - uint256 tokenAmountUsed = tokenAmountUsedToSave(collateralType, safeHandler); - require(both(tokenAmountUsed != MAX_UINT, tokenAmountUsed != 0), "YearnSystemCoinSafeSaviour/invalid-tokens-used-to-save"); - - // Check that there are enough yvTokens to cover both the keeper's payout and the amount used to save the SAFE - uint256 keeperYTokenPayout = div(mul(keeperPayout, WAD), yVault.pricePerShare()); - uint256 amountToWithdraw = add(keeperYTokenPayout, tokenAmountUsed); - require(yvTokenCover[collateralType][safeHandler] >= amountToWithdraw, "YearnSystemCoinSafeSaviour/not-enough-cover-deposited"); - - // Update the remaining cover - yvTokenCover[collateralType][safeHandler] = sub(yvTokenCover[collateralType][safeHandler], amountToWithdraw); - - // Mark the SAFE in the registry as just having been saved - saviourRegistry.markSave(collateralType, safeHandler); - - // Get system coins back from the Yearn vault - uint256 withdrawnAmount = yVault.withdraw(amountToWithdraw); - require(withdrawnAmount > 0, "YearnSystemCoinSafeSaviour/null-sys-coin-withdrawn"); - uint256 systemCoinsToRepay = sub(withdrawnAmount, keeperPayout); - - // Approve the coin join contract to take system coins and repay debt - systemCoin.approve(address(coinJoin), 0); - systemCoin.approve(address(coinJoin), systemCoinsToRepay); - - // Join system coins in the system and repay the SAFE's debt - { - coinJoin.join(address(this), systemCoinsToRepay); - uint256 nonAdjustedSystemCoinsToRepay = div(mul(systemCoinsToRepay, RAY), getAccumulatedRate(collateralType)); - - safeEngine.modifySAFECollateralization( - collateralType, - safeHandler, - address(0), - address(this), - int256(0), - -int256(nonAdjustedSystemCoinsToRepay) - ); - } - - // Send the fee to the keeper - systemCoin.transfer(keeper, keeperPayout); - - // Emit an event - emit SaveSAFE(keeper, collateralType, safeHandler, tokenAmountUsed); - - return (true, tokenAmountUsed, keeperPayout); - } - - // --- Getters --- - /* - * @notice Compute whether the value of keeperPayout system coins is higher than or equal to minKeeperPayoutValue - * @dev Used to determine whether it's worth it for the keeper to save the SAFE in exchange for keeperPayout system coins - * @return A bool representing whether the value of keeperPayout system coins is >= minKeeperPayoutValue - */ - function keeperPayoutExceedsMinValue() override public returns (bool) { - (uint256 priceFeedValue, bool hasValidValue) = systemCoinOrcl.getResultWithValidity(); - - if (either(!hasValidValue, priceFeedValue == 0)) { - return false; - } - - return (minKeeperPayoutValue <= mul(keeperPayout, priceFeedValue) / WAD); - } - /* - * @notice Return the current value of the keeper payout - */ - function getKeeperPayoutValue() override public returns (uint256) { - (uint256 priceFeedValue, bool hasValidValue) = systemCoinOrcl.getResultWithValidity(); - - if (either(!hasValidValue, priceFeedValue == 0)) { - return 0; - } - - return mul(keeperPayout, priceFeedValue) / WAD; - } - /* - * @notice Determine whether a SAFE can be saved with the current amount of yvToken deposited as cover for it - * @param collateralType The SAFE collateral type (ignored in this implementation) - * @param safeHandler The handler of the SAFE which the function takes into account - * @return Whether the SAFE can be saved or not - */ - function canSave(bytes32 collateralType, address safeHandler) override external returns (bool) { - uint256 tokenAmountUsed = tokenAmountUsedToSave(collateralType, safeHandler); - - if (either(tokenAmountUsed == MAX_UINT, tokenAmountUsed == 0)) { - return false; - } - - uint256 keeperYTokenPayout = div(mul(keeperPayout, WAD), yVault.pricePerShare()); - return (yvTokenCover[collateralType][safeHandler] >= add(tokenAmountUsed, keeperYTokenPayout)); - } - /* - * @notice Calculate the amount of collateralToken used to save a SAFE and bring its CRatio to the desired level - * @param collateralType The SAFE collateral type (ignored in this implementation) - * @param safeHandler The handler of the SAFE which the function takes into account - * @return The amount of collateralToken used to save the SAFE and bring its CRatio to the desired level - */ - function tokenAmountUsedToSave(bytes32 collateralType, address safeHandler) override public returns (uint256) { - if (yvTokenCover[collateralType][safeHandler] == 0) return 0; - - (uint256 depositedCollateralToken, uint256 safeDebt) = safeEngine.safes(collateralType, safeHandler); - (address ethFSM,,) = oracleRelayer.collateralTypes(collateralType); - if (ethFSM == address(0)) return MAX_UINT; - - (uint256 priceFeedValue, bool hasValidValue) = PriceFeedLike(ethFSM).getResultWithValidity(); - - // If the SAFE doesn't have debt, if the price feed is faulty or if the default desired CRatio is null, abort - uint256 defaultCRatio = cRatioSetter.defaultDesiredCollateralizationRatios(collateralType); - if (either(either(safeDebt == 0, either(priceFeedValue == 0, !hasValidValue)), defaultCRatio == 0)) { - return MAX_UINT; - } - - // Calculate the amount of debt that needs to be repaid so the SAFE gets to the target CRatio - uint256 targetCRatio = (cRatioSetter.desiredCollateralizationRatios(collateralType, safeHandler) == 0) ? - defaultCRatio : cRatioSetter.desiredCollateralizationRatios(collateralType, safeHandler); - - uint256 targetDebtAmount = mul( - mul(HUNDRED, mul(depositedCollateralToken, priceFeedValue) / WAD) / targetCRatio, RAY - ) / oracleRelayer.redemptionPrice(); - - // If you need to repay more than the amount of debt in the SAFE (or all the debt), return 0 - if (either(targetDebtAmount >= safeDebt, debtBelowFloor(collateralType, targetDebtAmount))) { - return 0; - } else { - safeDebt = mul(safeDebt, getAccumulatedRate(collateralType)) / RAY; - return div(mul(sub(safeDebt, targetDebtAmount), WAD), yVault.pricePerShare()); - } - } - /* - * @notify Returns whether a target debt amount is below the debt floor of a specific collateral type - * @param collateralType The collateral type whose floor we compare against - * @param targetDebtAmount The target debt amount for a SAFE that has collateralType collateral in it - */ - function debtBelowFloor(bytes32 collateralType, uint256 targetDebtAmount) public view returns (bool) { - (, , , , uint256 debtFloor, ) = safeEngine.collateralTypes(collateralType); - return (mul(targetDebtAmount, RAY) < debtFloor); - } - /* - * @notify Get the accumulated interest rate for a specific collateral type - * @param The collateral type for which to retrieve the rate - */ - function getAccumulatedRate(bytes32 collateralType) - public view returns (uint256 accumulatedRate) { - (, accumulatedRate, , , , ) = safeEngine.collateralTypes(collateralType); - } -} +// Copyright (C) 2021 James Connolly, Reflexer Labs, INC + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity 0.6.7; + +import "../interfaces/YVaultLike.sol"; +import "../interfaces/SaviourCRatioSetterLike.sol"; +import "../interfaces/SafeSaviourLike.sol"; +import "../math/SafeMath.sol"; + +contract YearnSystemCoinSafeSaviour is SafeMath, SafeSaviourLike { + // --- Auth --- + mapping (address => uint256) public authorizedAccounts; + /** + * @notice Add auth to an account + * @param account Account to add auth to + */ + function addAuthorization(address account) external isAuthorized { + authorizedAccounts[account] = 1; + emit AddAuthorization(account); + } + /** + * @notice Remove auth from an account + * @param account Account to remove auth from + */ + function removeAuthorization(address account) external isAuthorized { + authorizedAccounts[account] = 0; + emit RemoveAuthorization(account); + } + /** + * @notice Checks whether msg.sender can call an authed function + **/ + modifier isAuthorized { + require(authorizedAccounts[msg.sender] == 1, "YearnSystemCoinSafeSaviour/account-not-authorized"); + _; + } + + mapping (address => uint256) public allowedUsers; + /** + * @notice Allow a user to deposit assets + * @param usr User to whitelist + */ + function allowUser(address usr) external isAuthorized { + allowedUsers[usr] = 1; + emit AllowUser(usr); + } + /** + * @notice Disallow a user from depositing assets + * @param usr User to disallow + */ + function disallowUser(address usr) external isAuthorized { + allowedUsers[usr] = 0; + emit DisallowUser(usr); + } + /** + * @notice Checks whether an address is an allowed user + **/ + modifier isAllowed { + require( + either(restrictUsage == 0, both(restrictUsage == 1, allowedUsers[msg.sender] == 1)), + "YearnSystemCoinSafeSaviour/account-not-allowed" + ); + _; + } + + // --- Variables --- + // Flag that tells whether usage of the contract is restricted to allowed users + uint256 public restrictUsage; + + // Amount of collateral deposited to cover each SAFE + mapping(bytes32 => mapping(address => uint256)) public yvTokenCover; + // The yVault address + YVaultLike public yVault; + // The ERC20 system coin + ERC20Like public systemCoin; + // The system coin join contract + CoinJoinLike public coinJoin; + // Oracle providing the system coin price feed + PriceFeedLike public systemCoinOrcl; + // Contract that defines desired CRatios for each Safe after it is saved + SaviourCRatioSetterLike public cRatioSetter; + + // --- Events --- + event AddAuthorization(address account); + event RemoveAuthorization(address account); + event AllowUser(address usr); + event DisallowUser(address usr); + event ModifyParameters(bytes32 indexed parameter, uint256 val); + event ModifyParameters(bytes32 indexed parameter, address data); + event Deposit( + address indexed caller, + bytes32 collateralType, + address indexed safeHandler, + uint256 systemCoinAmount, + uint256 yvTokenAmount + ); + event Withdraw( + address indexed caller, + bytes32 collateralType, + address indexed safeHandler, + address dst, + uint256 systemCoinAmount, + uint256 yvTokenAmount + ); + + constructor( + address coinJoin_, + address cRatioSetter_, + address systemCoinOrcl_, + address liquidationEngine_, + address taxCollector_, + address oracleRelayer_, + address safeManager_, + address saviourRegistry_, + address yVault_, + uint256 keeperPayout_, + uint256 minKeeperPayoutValue_ + ) public { + require(coinJoin_ != address(0), "YearnSystemCoinSafeSaviour/null-coin-join"); + require(cRatioSetter_ != address(0), "YearnSystemCoinSafeSaviour/null-cratio-setter"); + require(systemCoinOrcl_ != address(0), "YearnSystemCoinSafeSaviour/null-system-coin-oracle"); + require(oracleRelayer_ != address(0), "YearnSystemCoinSafeSaviour/null-oracle-relayer"); + require(liquidationEngine_ != address(0), "YearnSystemCoinSafeSaviour/null-liquidation-engine"); + require(taxCollector_ != address(0), "YearnSystemCoinSafeSaviour/null-tax-collector"); + require(safeManager_ != address(0), "YearnSystemCoinSafeSaviour/null-safe-manager"); + require(saviourRegistry_ != address(0), "YearnSystemCoinSafeSaviour/null-saviour-registry"); + require(yVault_ != address(0), "YearnSystemCoinSafeSaviour/null-y-vault"); + require(keeperPayout_ > 0, "YearnSystemCoinSafeSaviour/invalid-keeper-payout"); + require(minKeeperPayoutValue_ > 0, "YearnSystemCoinSafeSaviour/invalid-min-payout-value"); + + authorizedAccounts[msg.sender] = 1; + + keeperPayout = keeperPayout_; + + minKeeperPayoutValue = minKeeperPayoutValue_; + coinJoin = CoinJoinLike(coinJoin_); + cRatioSetter = SaviourCRatioSetterLike(cRatioSetter_); + liquidationEngine = LiquidationEngineLike(liquidationEngine_); + taxCollector = TaxCollectorLike(taxCollector_); + + oracleRelayer = OracleRelayerLike(oracleRelayer_); + systemCoinOrcl = PriceFeedLike(systemCoinOrcl_); + systemCoin = ERC20Like(coinJoin.systemCoin()); + safeEngine = SAFEEngineLike(coinJoin.safeEngine()); + safeManager = GebSafeManagerLike(safeManager_); + saviourRegistry = SAFESaviourRegistryLike(saviourRegistry_); + yVault = YVaultLike(yVault_); + + systemCoinOrcl.read(); + systemCoinOrcl.getResultWithValidity(); + oracleRelayer.redemptionPrice(); + + require(address(safeEngine) != address(0), "YearnSystemCoinSafeSaviour/null-safe-engine"); + require(address(systemCoin) != address(0), "YearnSystemCoinSafeSaviour/null-sys-coin"); + + emit AddAuthorization(msg.sender); + emit ModifyParameters("keeperPayout", keeperPayout); + emit ModifyParameters("minKeeperPayoutValue", minKeeperPayoutValue); + emit ModifyParameters("liquidationEngine", liquidationEngine_); + emit ModifyParameters("taxCollector", taxCollector_); + emit ModifyParameters("oracleRelayer", oracleRelayer_); + emit ModifyParameters("systemCoinOrcl", systemCoinOrcl_); + } + + // --- Administration --- + /** + * @notice Modify an uint256 param + * @param parameter The name of the parameter + * @param val New value for the parameter + */ + function modifyParameters(bytes32 parameter, uint256 val) external isAuthorized { + if (parameter == "keeperPayout") { + require(val > 0, "YearnSystemCoinSafeSaviour/null-payout"); + keeperPayout = val; + } + else if (parameter == "minKeeperPayoutValue") { + require(val > 0, "YearnSystemCoinSafeSaviour/null-min-payout"); + minKeeperPayoutValue = val; + } + else if (parameter == "restrictUsage") { + require(val <= 1, "YearnSystemCoinSafeSaviour/invalid-restriction"); + restrictUsage = val; + } + else revert("YearnSystemCoinSafeSaviour/modify-unrecognized-param"); + emit ModifyParameters(parameter, val); + } + /** + * @notice Modify an address param + * @param parameter The name of the parameter + * @param data New address for the parameter + */ + function modifyParameters(bytes32 parameter, address data) external isAuthorized { + require(data != address(0), "YearnSystemCoinSafeSaviour/null-data"); + + if (parameter == "systemCoinOrcl") { + systemCoinOrcl = PriceFeedLike(data); + systemCoinOrcl.read(); + systemCoinOrcl.getResultWithValidity(); + } + else if (parameter == "oracleRelayer") { + oracleRelayer = OracleRelayerLike(data); + oracleRelayer.redemptionPrice(); + } + else if (parameter == "liquidationEngine") { + liquidationEngine = LiquidationEngineLike(data); + } + else if (parameter == "taxCollector") { + taxCollector = TaxCollectorLike(data); + } + else revert("YearnSystemCoinSafeSaviour/modify-unrecognized-param"); + emit ModifyParameters(parameter, data); + } + + // --- Adding/Withdrawing Cover --- + /* + * @notice Deposit systemCoin in the contract and lend in the Yearn vault in order to provide cover for a specific SAFE controlled by the SAFE Manager + * @param collateralType The collateral type used in the SAFE + * @param safeID The ID of the SAFE to protect. This ID should be registered inside GebSafeManager + * @param systemCoinAmount The amount of systemCoin to deposit + */ + function deposit(bytes32 collateralType, uint256 safeID, uint256 systemCoinAmount) + external isAllowed() liquidationEngineApproved(address(this)) nonReentrant { + uint256 defaultCRatio = cRatioSetter.defaultDesiredCollateralizationRatios(collateralType); + require(systemCoinAmount > 0, "YearnSystemCoinSafeSaviour/null-system-coin-amount"); + require(defaultCRatio > 0, "YearnSystemCoinSafeSaviour/collateral-not-set"); + + // Check that the SAFE exists inside GebSafeManager + address safeHandler = safeManager.safes(safeID); + require(safeHandler != address(0), "YearnSystemCoinSafeSaviour/null-handler"); + + // Check that the SAFE has debt + (, uint256 safeDebt) = safeEngine.safes(collateralType, safeHandler); + require(safeDebt > 0, "YearnSystemCoinSafeSaviour/safe-does-not-have-debt"); + + // Deposit into Yearn + systemCoin.transferFrom(msg.sender, address(this), systemCoinAmount); + systemCoin.approve(address(yVault), systemCoinAmount); + uint256 yvTokens = yVault.deposit(systemCoinAmount); // use return value to save on math operations + require(yvTokens > 0, "YearnSystemCoinSafeSaviour/no-vault-tokens-returned"); + + // Update the yvToken balance used to cover the SAFE + yvTokenCover[collateralType][safeHandler] = add(yvTokenCover[collateralType][safeHandler], yvTokens); + + emit Deposit(msg.sender, collateralType, safeHandler, systemCoinAmount, yvTokens); + } + /* + * @notice Withdraw collateralToken from the contract and provide less cover for a SAFE + * @dev Only an address that controls the SAFE inside GebSafeManager can call this + * @param safeID The ID of the SAFE to remove cover from. This ID should be registered inside GebSafeManager + * @param yvTokenAmount The amount of yvTokens to burn + * @param dst The address that will receive the withdrawn system coins + */ + function withdraw(bytes32 collateralType, uint256 safeID, uint256 yvTokenAmount, address dst) + external controlsSAFE(msg.sender, safeID) nonReentrant { + require(yvTokenAmount > 0, "YearnSystemCoinSafeSaviour/null-yvToken-amount"); + + // Fetch the handler from the SAFE manager + address safeHandler = safeManager.safes(safeID); + require(yvTokenCover[collateralType][safeHandler] >= yvTokenAmount, "YearnSystemCoinSafeSaviour/withdraw-request-higher-than-balance"); + + // Redeem system coins from Yearn and transfer them to the caller + yvTokenCover[collateralType][safeHandler] = sub(yvTokenCover[collateralType][safeHandler], yvTokenAmount); + + uint256 withdrawnSysCoinAmount = yVault.withdraw(yvTokenAmount); // use return value to save on math operations + require(withdrawnSysCoinAmount > 0, "YearnSystemCoinSafeSaviour/no-coins-withdrawn"); + systemCoin.transfer(dst, withdrawnSysCoinAmount); + + emit Withdraw( + msg.sender, + collateralType, + safeHandler, + dst, + withdrawnSysCoinAmount, + yvTokenAmount + ); + } + + // --- Saving Logic --- + /* + * @notice Saves a SAFE by repaying some of its debt + * @dev Only the LiquidationEngine can call this + * @param keeper The keeper that called LiquidationEngine.liquidateSAFE and that should be rewarded for spending gas to save a SAFE + * @param collateralType The collateral type backing the SAFE that's being liquidated + * @param safeHandler The handler of the SAFE that's being liquidated + * @return Whether the SAFE has been saved, the amount of system coin debt repaid as well as the amount of + * system coins sent to the keeper as their payment + */ + function saveSAFE(address keeper, bytes32 collateralType, address safeHandler) override external returns (bool, uint256, uint256) { + require(address(liquidationEngine) == msg.sender, "YearnSystemCoinSafeSaviour/caller-not-liquidation-engine"); + require(keeper != address(0), "YearnSystemCoinSafeSaviour/null-keeper-address"); + + if (both(both(collateralType == "", safeHandler == address(0)), keeper == address(liquidationEngine))) { + return (true, uint(-1), uint(-1)); + } + + // Check that the fiat value of the keeper payout is high enough + require(keeperPayoutExceedsMinValue(), "YearnSystemCoinSafeSaviour/small-keeper-payout-value"); + + // Tax the collateral + taxCollector.taxSingle(collateralType); + + // Compute and check the validity of the amount of yvTokens used to save the SAFE + uint256 tokenAmountUsed = tokenAmountUsedToSave(collateralType, safeHandler); + require(both(tokenAmountUsed != MAX_UINT, tokenAmountUsed != 0), "YearnSystemCoinSafeSaviour/invalid-tokens-used-to-save"); + + // Check that there are enough yvTokens to cover both the keeper's payout and the amount used to save the SAFE + uint256 keeperYTokenPayout = div(mul(keeperPayout, WAD), yVault.pricePerShare()); + uint256 amountToWithdraw = add(keeperYTokenPayout, tokenAmountUsed); + require(yvTokenCover[collateralType][safeHandler] >= amountToWithdraw, "YearnSystemCoinSafeSaviour/not-enough-cover-deposited"); + + // Update the remaining cover + yvTokenCover[collateralType][safeHandler] = sub(yvTokenCover[collateralType][safeHandler], amountToWithdraw); + + // Mark the SAFE in the registry as just having been saved + saviourRegistry.markSave(collateralType, safeHandler); + + // Get system coins back from the Yearn vault + uint256 withdrawnAmount = yVault.withdraw(amountToWithdraw); + require(withdrawnAmount > 0, "YearnSystemCoinSafeSaviour/null-sys-coin-withdrawn"); + uint256 systemCoinsToRepay = sub(withdrawnAmount, keeperPayout); + + // Approve the coin join contract to take system coins and repay debt + systemCoin.approve(address(coinJoin), 0); + systemCoin.approve(address(coinJoin), systemCoinsToRepay); + + // Join system coins in the system and repay the SAFE's debt + { + coinJoin.join(address(this), systemCoinsToRepay); + uint256 nonAdjustedSystemCoinsToRepay = div(mul(systemCoinsToRepay, RAY), getAccumulatedRate(collateralType)); + + safeEngine.modifySAFECollateralization( + collateralType, + safeHandler, + address(0), + address(this), + int256(0), + -int256(nonAdjustedSystemCoinsToRepay) + ); + } + + // Send the fee to the keeper + systemCoin.transfer(keeper, keeperPayout); + + // Emit an event + emit SaveSAFE(keeper, collateralType, safeHandler, tokenAmountUsed); + + return (true, tokenAmountUsed, keeperPayout); + } + + // --- Getters --- + /* + * @notice Compute whether the value of keeperPayout system coins is higher than or equal to minKeeperPayoutValue + * @dev Used to determine whether it's worth it for the keeper to save the SAFE in exchange for keeperPayout system coins + * @return A bool representing whether the value of keeperPayout system coins is >= minKeeperPayoutValue + */ + function keeperPayoutExceedsMinValue() override public returns (bool) { + (uint256 priceFeedValue, bool hasValidValue) = systemCoinOrcl.getResultWithValidity(); + + if (either(!hasValidValue, priceFeedValue == 0)) { + return false; + } + + return (minKeeperPayoutValue <= mul(keeperPayout, priceFeedValue) / WAD); + } + /* + * @notice Return the current value of the keeper payout + */ + function getKeeperPayoutValue() override public returns (uint256) { + (uint256 priceFeedValue, bool hasValidValue) = systemCoinOrcl.getResultWithValidity(); + + if (either(!hasValidValue, priceFeedValue == 0)) { + return 0; + } + + return mul(keeperPayout, priceFeedValue) / WAD; + } + /* + * @notice Determine whether a SAFE can be saved with the current amount of yvToken deposited as cover for it + * @param collateralType The SAFE collateral type (ignored in this implementation) + * @param safeHandler The handler of the SAFE which the function takes into account + * @return Whether the SAFE can be saved or not + */ + function canSave(bytes32 collateralType, address safeHandler) override external returns (bool) { + uint256 tokenAmountUsed = tokenAmountUsedToSave(collateralType, safeHandler); + + if (either(tokenAmountUsed == MAX_UINT, tokenAmountUsed == 0)) { + return false; + } + + uint256 keeperYTokenPayout = div(mul(keeperPayout, WAD), yVault.pricePerShare()); + return (yvTokenCover[collateralType][safeHandler] >= add(tokenAmountUsed, keeperYTokenPayout)); + } + /* + * @notice Calculate the amount of collateralToken used to save a SAFE and bring its CRatio to the desired level + * @param collateralType The SAFE collateral type (ignored in this implementation) + * @param safeHandler The handler of the SAFE which the function takes into account + * @return The amount of collateralToken used to save the SAFE and bring its CRatio to the desired level + */ + function tokenAmountUsedToSave(bytes32 collateralType, address safeHandler) override public returns (uint256) { + if (yvTokenCover[collateralType][safeHandler] == 0) return 0; + + (uint256 depositedCollateralToken, uint256 safeDebt) = safeEngine.safes(collateralType, safeHandler); + (address ethFSM,,) = oracleRelayer.collateralTypes(collateralType); + if (ethFSM == address(0)) return MAX_UINT; + + (uint256 priceFeedValue, bool hasValidValue) = PriceFeedLike(ethFSM).getResultWithValidity(); + + // If the SAFE doesn't have debt, if the price feed is faulty or if the default desired CRatio is null, abort + uint256 defaultCRatio = cRatioSetter.defaultDesiredCollateralizationRatios(collateralType); + if (either(either(safeDebt == 0, either(priceFeedValue == 0, !hasValidValue)), defaultCRatio == 0)) { + return MAX_UINT; + } + + // Calculate the amount of debt that needs to be repaid so the SAFE gets to the target CRatio + uint256 targetCRatio = (cRatioSetter.desiredCollateralizationRatios(collateralType, safeHandler) == 0) ? + defaultCRatio : cRatioSetter.desiredCollateralizationRatios(collateralType, safeHandler); + + uint256 targetDebtAmount = mul( + mul(HUNDRED, mul(depositedCollateralToken, priceFeedValue) / WAD) / targetCRatio, RAY + ) / oracleRelayer.redemptionPrice(); + + // If you need to repay more than the amount of debt in the SAFE (or all the debt), return 0 + if (either(targetDebtAmount >= safeDebt, debtBelowFloor(collateralType, targetDebtAmount))) { + return 0; + } else { + safeDebt = mul(safeDebt, getAccumulatedRate(collateralType)) / RAY; + return div(mul(sub(safeDebt, targetDebtAmount), WAD), yVault.pricePerShare()); + } + } + /* + * @notify Returns whether a target debt amount is below the debt floor of a specific collateral type + * @param collateralType The collateral type whose floor we compare against + * @param targetDebtAmount The target debt amount for a SAFE that has collateralType collateral in it + */ + function debtBelowFloor(bytes32 collateralType, uint256 targetDebtAmount) public view returns (bool) { + (, , , , uint256 debtFloor, ) = safeEngine.collateralTypes(collateralType); + return (mul(targetDebtAmount, RAY) < debtFloor); + } + /* + * @notify Get the accumulated interest rate for a specific collateral type + * @param The collateral type for which to retrieve the rate + */ + function getAccumulatedRate(bytes32 collateralType) + public view returns (uint256 accumulatedRate) { + (, accumulatedRate, , , , ) = safeEngine.collateralTypes(collateralType); + } +} diff --git a/src/test/CompoundSystemCoinSafeSaviour.t.sol b/src/test/CompoundSystemCoinSafeSaviour.t.sol index 013ead7..ac83e43 100644 --- a/src/test/CompoundSystemCoinSafeSaviour.t.sol +++ b/src/test/CompoundSystemCoinSafeSaviour.t.sol @@ -1,1071 +1,1071 @@ -pragma solidity 0.6.7; - -import "ds-test/test.sol"; -import "ds-token/token.sol"; - -import {SAFEEngine} from 'geb/SAFEEngine.sol'; -import {Coin} from 'geb/Coin.sol'; -import {LiquidationEngine} from 'geb/LiquidationEngine.sol'; -import {AccountingEngine} from 'geb/AccountingEngine.sol'; -import {TaxCollector} from 'geb/TaxCollector.sol'; -import {BasicCollateralJoin, CoinJoin} from 'geb/BasicTokenAdapters.sol'; -import {OracleRelayer} from 'geb/OracleRelayer.sol'; -import {EnglishCollateralAuctionHouse} from 'geb/CollateralAuctionHouse.sol'; -import {GebSafeManager} from "geb-safe-manager/GebSafeManager.sol"; - -import {CErc20, CToken} from "../integrations/compound/CErc20.sol"; -import {ComptrollerG2} from "../integrations/compound/ComptrollerG2.sol"; -import {Unitroller} from "../integrations/compound/Unitroller.sol"; -import {WhitePaperInterestRateModel} from "../integrations/compound/WhitePaperInterestRateModel.sol"; -import {PriceOracle} from "../integrations/compound/PriceOracle.sol"; - -import {SaviourCRatioSetter} from "../SaviourCRatioSetter.sol"; -import {SAFESaviourRegistry} from "../SAFESaviourRegistry.sol"; - -import {CompoundSystemCoinSafeSaviour} from "../saviours/CompoundSystemCoinSafeSaviour.sol"; - -abstract contract Hevm { - function warp(uint256) virtual public; -} -contract CompoundPriceOracle is PriceOracle { - uint256 price; - - function setPrice(uint256 newPrice) public { - price = newPrice; - } - - function getUnderlyingPrice(CToken cToken) override external view returns (uint) { - return price; - } -} -contract Feed { - uint256 public price; - bool public validPrice; - uint public lastUpdateTime; - address public priceSource; - - constructor(uint256 price_, bool validPrice_) public { - price = price_; - validPrice = validPrice_; - lastUpdateTime = now; - } - function updatePriceSource(address priceSource_) external { - priceSource = priceSource_; - } - function changeValidity() external { - validPrice = !validPrice; - } - function updateCollateralPrice(uint256 price_) external { - price = price_; - lastUpdateTime = now; - } - function read() external view returns (uint256) { - return price; - } - function getResultWithValidity() external view returns (uint256, bool) { - return (price, validPrice); - } -} -contract TestSAFEEngine is SAFEEngine { - uint256 constant RAY = 10 ** 27; - - constructor() public {} - - function mint(address usr, uint wad) public { - coinBalance[usr] += wad * RAY; - globalDebt += wad * RAY; - } - function balanceOf(address usr) public view returns (uint) { - return uint(coinBalance[usr] / RAY); - } - function setAccumulatedRate(bytes32 collateralType, uint256 rate) public { - collateralTypes[collateralType].accumulatedRate = rate; - } -} -contract TestAccountingEngine is AccountingEngine { - constructor(address safeEngine, address surplusAuctionHouse, address debtAuctionHouse) - public AccountingEngine(safeEngine, surplusAuctionHouse, debtAuctionHouse) {} - - function totalDeficit() public view returns (uint) { - return safeEngine.debtBalance(address(this)); - } - function totalSurplus() public view returns (uint) { - return safeEngine.coinBalance(address(this)); - } - function preAuctionDebt() public view returns (uint) { - return subtract(subtract(totalDeficit(), totalQueuedDebt), totalOnAuctionDebt); - } -} -contract FakeUser { - function doModifyParameters( - CompoundSystemCoinSafeSaviour saviour, - bytes32 parameter, - uint256 data - ) public { - saviour.modifyParameters(parameter, data); - } - - function doModifyParameters( - CompoundSystemCoinSafeSaviour saviour, - bytes32 parameter, - address data - ) public { - saviour.modifyParameters(parameter, data); - } - - function doOpenSafe( - GebSafeManager manager, - bytes32 collateralType, - address usr - ) public returns (uint256) { - return manager.openSAFE(collateralType, usr); - } - - function doSafeAllow( - GebSafeManager manager, - uint safe, - address usr, - uint ok - ) public { - manager.allowSAFE(safe, usr, ok); - } - - function doHandlerAllow( - GebSafeManager manager, - address usr, - uint ok - ) public { - manager.allowHandler(usr, ok); - } - - function doTransferSAFEOwnership( - GebSafeManager manager, - uint safe, - address dst - ) public { - manager.transferSAFEOwnership(safe, dst); - } - - function doModifySAFECollateralization( - GebSafeManager manager, - uint safe, - int deltaCollateral, - int deltaDebt - ) public { - manager.modifySAFECollateralization(safe, deltaCollateral, deltaDebt); - } - - function doApproveSAFEModification( - SAFEEngine safeEngine, - address usr - ) public { - safeEngine.approveSAFEModification(usr); - } - - function doSAFEEngineModifySAFECollateralization( - SAFEEngine safeEngine, - bytes32 collateralType, - address safe, - address collateralSource, - address debtDst, - int deltaCollateral, - int deltaDebt - ) public { - safeEngine.modifySAFECollateralization(collateralType, safe, collateralSource, debtDst, deltaCollateral, deltaDebt); - } - - function doProtectSAFE( - GebSafeManager manager, - uint safe, - address liquidationEngine, - address saviour - ) public { - manager.protectSAFE(safe, liquidationEngine, saviour); - } - - function doDeposit( - CompoundSystemCoinSafeSaviour saviour, - Coin systemCoin, - bytes32 collateralType, - uint256 safeID, - uint256 systemCoinAmount - ) public { - systemCoin.approve(address(saviour), systemCoinAmount); - saviour.deposit(collateralType, safeID, systemCoinAmount); - } - - function doWithdraw( - CompoundSystemCoinSafeSaviour saviour, - bytes32 collateralType, - uint256 safeID, - uint256 cTokenAmount, - address dst - ) public { - saviour.withdraw(collateralType, safeID, cTokenAmount, dst); - } - - function doTransferInternalCoins( - GebSafeManager manager, - uint256 safe, - address dst, - uint256 amt - ) public { - manager.transferInternalCoins(safe, dst, amt); - } - - function doSetDesiredCollateralizationRatio( - SaviourCRatioSetter cRatioSetter, - bytes32 collateralType, - uint safe, - uint cRatio - ) public { - cRatioSetter.setDesiredCollateralizationRatio(collateralType, safe, cRatio); - } -} - -contract CompoundSystemCoinSafeSaviourTest is DSTest { - Hevm hevm; - - TestSAFEEngine safeEngine; - TestAccountingEngine accountingEngine; - LiquidationEngine liquidationEngine; - OracleRelayer oracleRelayer; - TaxCollector taxCollector; - - BasicCollateralJoin collateralJoin; - CoinJoin coinJoin; - - CoinJoin systemCoinJoin; - EnglishCollateralAuctionHouse collateralAuctionHouse; - - GebSafeManager safeManager; - - Feed systemCoinOracle; - CompoundPriceOracle compoundSysCoinOracle; - - Coin systemCoin; - - CompoundSystemCoinSafeSaviour saviour; - SaviourCRatioSetter cRatioSetter; - SAFESaviourRegistry saviourRegistry; - - CErc20 cRAI; - ComptrollerG2 comptroller; - Unitroller unitroller; - WhitePaperInterestRateModel interestRateModel; - - FakeUser alice; - - Feed goldFSM; - Feed goldMedian; - - DSToken gold; - - address me; - - // Compound Params - uint256 systemCoinsToMint = 100000 * 10**18; - uint256 systemCoinPrice = 1 ether; - - uint256 baseRatePerYear = 10**17; - uint256 multiplierPerYear = 45 * 10**17; - uint256 liquidationIncentive = 1 ether; - uint256 closeFactor = 0.051 ether; - uint256 maxAssets = 10; - uint256 exchangeRate = 1 ether; - - uint8 cTokenDecimals = 8; - - string cTokenSymbol = "cRAI"; - string cTokenName = "cRAI"; - - // Saviour params - uint256 saveCooldown = 1 days; - uint256 keeperPayout = 0.5 ether; - uint256 minKeeperPayoutValue = 0.01 ether; - uint256 payoutToSAFESize = 40; - uint256 defaultDesiredCollateralizationRatio = 200; - uint256 minDesiredCollateralizationRatio = 155; - - // Core system params - uint256 goldPrice = 3.75 ether; - uint256 minCRatio = 1.5 ether; - uint256 goldToMint = 5000 ether; - uint256 goldCeiling = 1000 ether; - uint256 goldFloor = 10 ether; - uint256 goldSafetyPrice = 1 ether; - uint256 goldLiquidationPenalty = 1 ether; - - uint256 defaultCollateralAmount = 40 ether; - uint256 defaultTokenAmount = 100 ether; - - function setUp() public { - hevm = Hevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); - hevm.warp(604411200); - - // System coin - systemCoin = new Coin("RAI", "RAI", 1); - systemCoin.mint(address(this), systemCoinsToMint); - systemCoinOracle = new Feed(systemCoinPrice, true); - - // Compound setup - compoundSysCoinOracle = new CompoundPriceOracle(); - compoundSysCoinOracle.setPrice(systemCoinPrice); - - interestRateModel = new WhitePaperInterestRateModel(baseRatePerYear, multiplierPerYear); - unitroller = new Unitroller(); - comptroller = new ComptrollerG2(); - - unitroller._setPendingImplementation(address(comptroller)); - comptroller._become(unitroller); - - comptroller._setLiquidationIncentive(liquidationIncentive); - comptroller._setCloseFactor(closeFactor); - comptroller._setMaxAssets(maxAssets); - comptroller._setPriceOracle(compoundSysCoinOracle); - - cRAI = new CErc20(); - cRAI.initialize( - address(systemCoin), - comptroller, - interestRateModel, - exchangeRate, - cTokenName, - cTokenSymbol, - cTokenDecimals - ); - comptroller._supportMarket(cRAI); - - // Core system - safeEngine = new TestSAFEEngine(); - - goldFSM = new Feed(goldPrice, true); - goldMedian = new Feed(goldPrice, true); - goldFSM.updatePriceSource(address(goldMedian)); - - oracleRelayer = new OracleRelayer(address(safeEngine)); - oracleRelayer.modifyParameters("redemptionPrice", ray(systemCoinPrice)); - oracleRelayer.modifyParameters("gold", "orcl", address(goldFSM)); - oracleRelayer.modifyParameters("gold", "safetyCRatio", ray(minCRatio)); - oracleRelayer.modifyParameters("gold", "liquidationCRatio", ray(minCRatio)); - safeEngine.addAuthorization(address(oracleRelayer)); - - accountingEngine = new TestAccountingEngine( - address(safeEngine), address(0x1), address(0x2) - ); - safeEngine.addAuthorization(address(accountingEngine)); - - taxCollector = new TaxCollector(address(safeEngine)); - taxCollector.initializeCollateralType("gold"); - taxCollector.modifyParameters("primaryTaxReceiver", address(accountingEngine)); - taxCollector.modifyParameters("gold", "stabilityFee", 1000000564701133626865910626); // 5% / day - safeEngine.addAuthorization(address(taxCollector)); - - liquidationEngine = new LiquidationEngine(address(safeEngine)); - liquidationEngine.modifyParameters("accountingEngine", address(accountingEngine)); - - safeEngine.addAuthorization(address(liquidationEngine)); - accountingEngine.addAuthorization(address(liquidationEngine)); - - gold = new DSToken("GOLD", 'GOLD'); - gold.mint(goldToMint); - - safeEngine.initializeCollateralType("gold"); - - collateralJoin = new BasicCollateralJoin(address(safeEngine), "gold", address(gold)); - - coinJoin = new CoinJoin(address(safeEngine), address(systemCoin)); - systemCoin.addAuthorization(address(coinJoin)); - - safeEngine.addAuthorization(address(collateralJoin)); - - safeEngine.modifyParameters("gold", "safetyPrice", ray(goldSafetyPrice)); - safeEngine.modifyParameters("gold", "debtCeiling", rad(goldCeiling)); - safeEngine.modifyParameters("globalDebtCeiling", rad(goldCeiling)); - safeEngine.modifyParameters("gold", "debtFloor", rad(goldFloor)); - - collateralAuctionHouse = new EnglishCollateralAuctionHouse(address(safeEngine), address(liquidationEngine), "gold"); - collateralAuctionHouse.addAuthorization(address(liquidationEngine)); - - liquidationEngine.addAuthorization(address(collateralAuctionHouse)); - liquidationEngine.modifyParameters("gold", "collateralAuctionHouse", address(collateralAuctionHouse)); - liquidationEngine.modifyParameters("gold", "liquidationPenalty", goldLiquidationPenalty); - - safeEngine.addAuthorization(address(collateralAuctionHouse)); - safeEngine.approveSAFEModification(address(collateralAuctionHouse)); - - safeManager = new GebSafeManager(address(safeEngine)); - oracleRelayer.updateCollateralPrice("gold"); - - // Saviour infra - saviourRegistry = new SAFESaviourRegistry(saveCooldown); - cRatioSetter = new SaviourCRatioSetter(address(oracleRelayer), address(safeManager)); - cRatioSetter.setDefaultCRatio("gold", defaultDesiredCollateralizationRatio); - - saviour = new CompoundSystemCoinSafeSaviour( - address(coinJoin), - address(cRatioSetter), - address(systemCoinOracle), - address(liquidationEngine), - address(taxCollector), - address(oracleRelayer), - address(safeManager), - address(saviourRegistry), - address(cRAI), - keeperPayout, - minKeeperPayoutValue - ); - saviourRegistry.toggleSaviour(address(saviour)); - liquidationEngine.connectSAFESaviour(address(saviour)); - - me = address(this); - alice = new FakeUser(); - } - - // --- Math --- - function ray(uint wad) internal pure returns (uint) { - return wad * 10 ** 9; - } - function rad(uint wad) internal pure returns (uint) { - return wad * 10 ** 27; - } - - // --- Default actions/scenarios --- - function default_create_liquidatable_position(uint256 desiredCRatio, uint256 liquidatableCollateralPrice) internal returns (address) { - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "gold", safe, desiredCRatio); - assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); - - goldMedian.updateCollateralPrice(liquidatableCollateralPrice); - goldFSM.updateCollateralPrice(liquidatableCollateralPrice); - oracleRelayer.updateCollateralPrice("gold"); - - return safeHandler; - } - function default_save(uint256 safe, address safeHandler, uint desiredCRatio) internal { - default_modify_collateralization(safe, safeHandler); - - alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "gold", safe, desiredCRatio); - assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); - - goldMedian.updateCollateralPrice(3 ether); - goldFSM.updateCollateralPrice(3 ether); - oracleRelayer.updateCollateralPrice("gold"); - - safeEngine.mint(safeHandler, rad(defaultTokenAmount)); - systemCoin.mint(address(alice), defaultTokenAmount); - alice.doDeposit(saviour, systemCoin, "gold", safe, defaultTokenAmount); - - uint256 oldCTokenSupply = cRAI.totalSupply(); - - assertTrue(saviour.keeperPayoutExceedsMinValue()); - assertTrue(saviour.canSave("gold", safeHandler)); - - liquidationEngine.modifyParameters("gold", "liquidationQuantity", rad(111 ether)); - liquidationEngine.modifyParameters("gold", "liquidationPenalty", 1.1 ether); - - uint256 preSaveKeeperBalance = systemCoin.balanceOf(address(this)); - uint auction = liquidationEngine.liquidateSAFE("gold", safeHandler); - - assertEq(auction, 0); - assertEq(systemCoin.balanceOf(address(this)) - preSaveKeeperBalance, saviour.keeperPayout()); - assertTrue(oldCTokenSupply - cRAI.totalSupply() > 0); - assertTrue(cRAI.totalSupply() > 0); - - (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("gold", safeHandler); - (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("gold"); - uint256 computedCRatio = lockedCollateral * 3E27 * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10 ** 27); - assertTrue(computedCRatio == desiredCRatio || computedCRatio == desiredCRatio - 1); - } - function default_second_save(uint256 safe, address safeHandler, uint desiredCRatio) internal { - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "gold", safe, desiredCRatio); - - goldMedian.updateCollateralPrice(2.5 ether); - goldFSM.updateCollateralPrice(2.5 ether); - oracleRelayer.updateCollateralPrice("gold"); - - uint256 oldCTokenSupply = cRAI.totalSupply(); - - assertTrue(saviour.keeperPayoutExceedsMinValue()); - assertTrue(saviour.canSave("gold", safeHandler)); - - liquidationEngine.modifyParameters("gold", "liquidationQuantity", rad(111 ether)); - liquidationEngine.modifyParameters("gold", "liquidationPenalty", 1.1 ether); - - uint256 preSaveKeeperBalance = systemCoin.balanceOf(address(this)); - uint auction = liquidationEngine.liquidateSAFE("gold", safeHandler); - - assertEq(auction, 0); - assertEq(systemCoin.balanceOf(address(this)) - preSaveKeeperBalance, saviour.keeperPayout()); - assertTrue(oldCTokenSupply - cRAI.totalSupply() > 0); - assertTrue(cRAI.totalSupply() > 0); - - (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("gold", safeHandler); - (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("gold"); - uint256 computedCRatio = lockedCollateral * 2.5E27 * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10 ** 27); - assertTrue(computedCRatio == desiredCRatio || computedCRatio == desiredCRatio - 1); - } - function default_liquidate_safe(address safeHandler) internal { - goldMedian.updateCollateralPrice(3 ether); - goldFSM.updateCollateralPrice(3 ether); - oracleRelayer.updateCollateralPrice("gold"); - - liquidationEngine.modifyParameters("gold", "liquidationQuantity", rad(111 ether)); - liquidationEngine.modifyParameters("gold", "liquidationPenalty", 1.1 ether); - - uint auction = liquidationEngine.liquidateSAFE("gold", safeHandler); - // the full SAFE is liquidated - (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("gold", me); - assertEq(lockedCollateral, 0); - assertEq(generatedDebt, 0); - // all debt goes to the accounting engine - assertEq(accountingEngine.totalQueuedDebt(), rad(defaultTokenAmount)); - // auction is for all collateral - (,uint amountToSell,,,,,, uint256 amountToRaise) = collateralAuctionHouse.bids(auction); - assertEq(amountToSell, defaultCollateralAmount); - assertEq(amountToRaise, rad(110 ether)); - } - function default_create_liquidatable_position_deposit_cover(uint256 desiredCRatio, uint256 liquidatableCollateralPrice) internal returns (address) { - // Create position - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "gold", safe, desiredCRatio); - assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); - - goldMedian.updateCollateralPrice(liquidatableCollateralPrice); - goldFSM.updateCollateralPrice(liquidatableCollateralPrice); - oracleRelayer.updateCollateralPrice("gold"); - - // Deposit cover - safeEngine.mint(safeHandler, rad(defaultTokenAmount)); - systemCoin.mint(address(alice), defaultTokenAmount); - - alice.doDeposit(saviour, systemCoin, "gold", safe, defaultTokenAmount); - - uint256 totalSupply = cRAI.totalSupply(); - assertTrue(totalSupply > 0); - assertEq(cRAI.balanceOf(address(saviour)), totalSupply); - assertEq(systemCoin.balanceOf(address(cRAI)), defaultTokenAmount); - assertEq(saviour.cTokenCover("gold", safeHandler), totalSupply); - - return safeHandler; - } - function default_create_position_deposit_cover() internal returns (uint, address, uint) { - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - safeEngine.mint(safeHandler, rad(defaultTokenAmount)); - systemCoin.mint(address(alice), defaultTokenAmount); - - alice.doDeposit(saviour, systemCoin, "gold", safe, defaultTokenAmount); - - uint256 totalSupply = cRAI.totalSupply(); - assertTrue(totalSupply > 0); - assertEq(cRAI.balanceOf(address(saviour)), totalSupply); - assertEq(systemCoin.balanceOf(address(cRAI)), defaultTokenAmount); - assertEq(saviour.cTokenCover("gold", safeHandler), totalSupply); - - return (safe, safeHandler, totalSupply); - } - function default_modify_collateralization(uint256 safe, address safeHandler) internal { - gold.approve(address(collateralJoin)); - collateralJoin.join(address(safeHandler), defaultTokenAmount); - alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount)); - } - - // --- Tests --- - function test_setup() public { - assertEq(saviour.authorizedAccounts(address(this)), 1); - assertEq(saviour.keeperPayout(), keeperPayout); - assertEq(saviour.minKeeperPayoutValue(), minKeeperPayoutValue); - - assertEq(address(saviour.coinJoin()), address(coinJoin)); - assertEq(address(saviour.cRatioSetter()), address(cRatioSetter)); - assertEq(address(saviour.liquidationEngine()), address(liquidationEngine)); - assertEq(address(saviour.oracleRelayer()), address(oracleRelayer)); - assertEq(address(saviour.systemCoinOrcl()), address(systemCoinOracle)); - assertEq(address(saviour.taxCollector()), address(taxCollector)); - assertEq(address(saviour.systemCoin()), address(systemCoin)); - assertEq(address(saviour.safeEngine()), address(safeEngine)); - assertEq(address(saviour.safeManager()), address(safeManager)); - assertEq(address(saviour.saviourRegistry()), address(saviourRegistry)); - assertEq(address(saviour.cToken()), address(cRAI)); - } - function testFail_modifyParameters_uint_unauthorized() public { - alice.doModifyParameters(saviour, "keeperPayout", 5); - } - function test_modifyParameters_uint() public { - saviour.modifyParameters("keeperPayout", 5); - assertEq(saviour.keeperPayout(), 5); - } - function testFail_modifyParameters_address_unauthorized() public { - systemCoinOracle = new Feed(systemCoinPrice, true); - alice.doModifyParameters(saviour, "systemCoinOrcl", address(systemCoinOracle)); - } - function test_modifyParameters_address() public { - systemCoinOracle = new Feed(systemCoinPrice, true); - saviour.modifyParameters("systemCoinOrcl", address(systemCoinOracle)); - assertEq(address(saviour.systemCoinOrcl()), address(systemCoinOracle)); - } - function testFail_deposit_liq_engine_not_approved() public { - liquidationEngine.disconnectSAFESaviour(address(saviour)); - - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - safeEngine.mint(safeHandler, rad(defaultTokenAmount)); - systemCoin.mint(address(alice), defaultTokenAmount); - - alice.doDeposit(saviour, systemCoin, "gold", 1, defaultTokenAmount); - } - function testFail_deposit_null_sys_coin_amount() public { - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - safeEngine.mint(safeHandler, rad(defaultTokenAmount)); - systemCoin.mint(address(alice), defaultTokenAmount); - - alice.doDeposit(saviour, systemCoin, "gold", safe, 0); - } - function testFail_deposit_inexistent_safe() public { - systemCoin.mint(address(alice), defaultTokenAmount); - - alice.doDeposit(saviour, systemCoin, "gold", 1, defaultTokenAmount); - } - function test_deposit_no_prior_compound_liquidity() public { - default_create_position_deposit_cover(); - } - function test_deposit_twice() public { - (uint safe, address safeHandler, ) = default_create_position_deposit_cover(); - - // Second deposit - safeEngine.mint(safeHandler, rad(defaultTokenAmount)); - systemCoin.mint(address(alice), defaultTokenAmount); - - alice.doDeposit(saviour, systemCoin, "gold", safe, defaultTokenAmount); - - uint256 totalSupply = cRAI.totalSupply(); - assertTrue(totalSupply > 0); - assertEq(cRAI.balanceOf(address(saviour)), totalSupply); - assertEq(systemCoin.balanceOf(address(cRAI)), defaultTokenAmount * 2); - assertEq(saviour.cTokenCover("gold", safeHandler), totalSupply); - } - function test_deposit_after_everything_withdrawn() public { - (uint safe, address safeHandler, uint totalSupply) = default_create_position_deposit_cover(); - - // Withdraw - alice.doWithdraw(saviour, "gold", safe, totalSupply, address(alice)); - - totalSupply = cRAI.totalSupply(); - assertTrue(totalSupply == 0); - assertEq(cRAI.balanceOf(address(saviour)), 0); - assertEq(systemCoin.balanceOf(address(cRAI)), 0); - assertEq(saviour.cTokenCover("gold", safeHandler), 0); - - // Second deposit - alice.doDeposit(saviour, systemCoin, "gold", safe, defaultTokenAmount); - - totalSupply = cRAI.totalSupply(); - assertTrue(totalSupply > 0); - assertEq(cRAI.balanceOf(address(saviour)), totalSupply); - assertEq(systemCoin.balanceOf(address(cRAI)), defaultTokenAmount); - assertEq(saviour.cTokenCover("gold", safeHandler), totalSupply); - } - function testFail_withdraw_unauthorized() public { - (uint safe, , ) = default_create_position_deposit_cover(); - - // Withdraw by unauthed - FakeUser bob = new FakeUser(); - bob.doWithdraw(saviour, "gold", safe, defaultTokenAmount, address(this)); - } - function testFail_withdraw_more_than_deposited() public { - (uint safe, , uint totalSupply) = default_create_position_deposit_cover(); - - // Withdraw - alice.doWithdraw(saviour, "gold", safe, totalSupply + 1, address(this)); - } - function testFail_withdraw_null() public { - (uint safe, , ) = default_create_position_deposit_cover(); - - // Withdraw - alice.doWithdraw(saviour, "gold", safe, 0, address(this)); - } - function test_withdraw() public { - (uint safe, address safeHandler, uint totalSupply) = default_create_position_deposit_cover(); - - // Withdraw - alice.doWithdraw(saviour, "gold", safe, totalSupply, address(this)); - - totalSupply = cRAI.totalSupply(); - assertTrue(totalSupply == 0); - assertEq(cRAI.balanceOf(address(saviour)), 0); - assertEq(systemCoin.balanceOf(address(cRAI)), 0); - assertEq(saviour.cTokenCover("gold", safeHandler), 0); - } - function test_withdraw_twice() public { - (uint safe, address safeHandler, uint totalSupply) = default_create_position_deposit_cover(); - - // Withdraw first time - alice.doWithdraw(saviour, "gold", safe, totalSupply / 2, address(this)); - - assertTrue(totalSupply > 0); - assertEq(cRAI.balanceOf(address(saviour)), totalSupply / 2); - assertEq(systemCoin.balanceOf(address(cRAI)), defaultTokenAmount / 2); - assertEq(saviour.cTokenCover("gold", safeHandler), totalSupply / 2); - - // Withdraw second time - alice.doWithdraw(saviour, "gold", safe, totalSupply / 2, address(this)); - - totalSupply = cRAI.totalSupply(); - assertTrue(totalSupply == 0); - assertEq(cRAI.balanceOf(address(saviour)), 0); - assertEq(systemCoin.balanceOf(address(cRAI)), 0); - assertEq(saviour.cTokenCover("gold", safeHandler), 0); - } - function test_keeperPayoutExceedsMinValue_valid_orcl_result_true() public { - assertTrue(saviour.keeperPayoutExceedsMinValue()); - } - function test_keeperPayoutExceedsMinValue_valid_orcl_result_false() public { - saviour.modifyParameters("minKeeperPayoutValue", minKeeperPayoutValue * 10000); - assertTrue(!saviour.keeperPayoutExceedsMinValue()); - } - function test_keeperPayoutExceedsMinValue_invalid_orcl_result() public { - systemCoinOracle.changeValidity(); - assertTrue(!saviour.keeperPayoutExceedsMinValue()); - } - function test_keeperPayoutExceedsMinValue_null_orcl_result() public { - systemCoinOracle.updateCollateralPrice(0); - assertTrue(!saviour.keeperPayoutExceedsMinValue()); - } - function test_getKeeperPayoutValue_valid_orcl_result_true() public { - assertEq(saviour.getKeeperPayoutValue(), 0.5 ether); - } - function test_getKeeperPayoutValue_invalid_orcl_result() public { - systemCoinOracle.changeValidity(); - assertEq(saviour.getKeeperPayoutValue(), 0); - } - function test_getKeeperPayoutValue_null_orcl_result() public { - systemCoinOracle.updateCollateralPrice(0); - assertEq(saviour.getKeeperPayoutValue(), 0); - } - function test_tokenAmountUsedToSave_col_invalid_price() public { - address safeHandler = default_create_liquidatable_position(250, 3 ether); - - safeEngine.mint(safeHandler, rad(defaultTokenAmount)); - systemCoin.mint(address(alice), defaultTokenAmount); - alice.doDeposit(saviour, systemCoin, "gold", 1, defaultTokenAmount); - - goldFSM.changeValidity(); - assertEq(saviour.tokenAmountUsedToSave("gold", safeHandler), uint(-1)); - } - function test_tokenAmountUsedToSave_null_price() public { - address safeHandler = default_create_liquidatable_position(200, 1 ether); - - safeEngine.mint(safeHandler, rad(defaultTokenAmount)); - systemCoin.mint(address(alice), defaultTokenAmount); - alice.doDeposit(saviour, systemCoin, "gold", 1, defaultTokenAmount); - - goldFSM.updateCollateralPrice(0); - assertEq(saviour.tokenAmountUsedToSave("gold", safeHandler), uint(-1)); - } - function test_tokenAmountUsedToSave() public { - address safeHandler = default_create_liquidatable_position(400, 1 ether); - - safeEngine.mint(safeHandler, rad(defaultTokenAmount)); - systemCoin.mint(address(alice), defaultTokenAmount); - alice.doDeposit(saviour, systemCoin, "gold", 1, defaultTokenAmount); - - assertEq(saviour.tokenAmountUsedToSave("gold", safeHandler), 90 ether); - } - function test_canSave_invalid_collateral_price() public { - address safeHandler = default_create_liquidatable_position_deposit_cover(250, 1 ether); - goldFSM.changeValidity(); - assertTrue(!saviour.canSave("gold", safeHandler)); - } - function test_canSave_null_collateral_price() public { - address safeHandler = default_create_liquidatable_position_deposit_cover(250, 1 ether); - goldFSM.updateCollateralPrice(0); - assertTrue(!saviour.canSave("gold", safeHandler)); - } - function test_canSave_null_safe_debt() public { - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - assertTrue(!saviour.canSave("gold", safeHandler)); - } - function test_canSave_insufficient_ctoken_cover() public { - saviour.modifyParameters("keeperPayout", 5000 ether); - address safeHandler = default_create_liquidatable_position_deposit_cover(250, 1 ether); - assertTrue(!saviour.canSave("gold", safeHandler)); - } - function test_canSave() public { - address safeHandler = default_create_liquidatable_position_deposit_cover(250, 1 ether); - assertTrue(saviour.canSave("gold", safeHandler)); - } - function test_canSave_charged_interest() public { - address safeHandler = default_create_liquidatable_position_deposit_cover(250, 1 ether); - - hevm.warp(now + 1 days); - taxCollector.taxSingle("gold"); - - assertTrue(saviour.canSave("gold", safeHandler)); - } - function testFail_saveSAFE_debt_below_floor() public { - hevm.warp(now + 1); - - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "gold", safe, 200); - - goldMedian.updateCollateralPrice(3 ether); - goldFSM.updateCollateralPrice(3 ether); - oracleRelayer.updateCollateralPrice("gold"); - - safeEngine.mint(safeHandler, rad(defaultTokenAmount)); - systemCoin.mint(address(alice), defaultTokenAmount); - alice.doDeposit(saviour, systemCoin, "gold", safe, defaultTokenAmount); - - liquidationEngine.modifyParameters("gold", "liquidationQuantity", rad(111 ether)); - liquidationEngine.modifyParameters("gold", "liquidationPenalty", 1.1 ether); - safeEngine.modifyParameters("gold", "debtFloor", defaultTokenAmount - 1); - saviour.saveSAFE(address(this), "gold", safeHandler); - } - function testFail_saveSAFE_invalid_caller() public { - hevm.warp(now + 1); - - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "gold", safe, 200); - - goldMedian.updateCollateralPrice(3 ether); - goldFSM.updateCollateralPrice(3 ether); - oracleRelayer.updateCollateralPrice("gold"); - - safeEngine.mint(safeHandler, rad(defaultTokenAmount)); - systemCoin.mint(address(alice), defaultTokenAmount); - alice.doDeposit(saviour, systemCoin, "gold", safe, defaultTokenAmount); - - liquidationEngine.modifyParameters("gold", "liquidationQuantity", rad(111 ether)); - liquidationEngine.modifyParameters("gold", "liquidationPenalty", 1.1 ether); - - saviour.saveSAFE(address(this), "gold", safeHandler); - } - function test_saveSAFE_small_payout() public { - saviour.modifyParameters("keeperPayout", 5000 ether); - - hevm.warp(now + 1); - - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "gold", safe, 155); - assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); - - safeEngine.mint(safeHandler, rad(defaultTokenAmount)); - systemCoin.mint(address(alice), defaultTokenAmount); - alice.doDeposit(saviour, systemCoin, "gold", safe, defaultTokenAmount); - - default_liquidate_safe(safeHandler); - assertEq(saviourRegistry.lastSaveTime("gold", safeHandler), 0); - } - function test_saveSAFE_insufficient_ctoken_coverage() public { - hevm.warp(now + 1); - - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "gold", safe, 900); - assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); - - safeEngine.mint(safeHandler, rad(defaultTokenAmount)); - systemCoin.mint(address(alice), defaultTokenAmount); - alice.doDeposit(saviour, systemCoin, "gold", safe, defaultTokenAmount / 10); - - default_liquidate_safe(safeHandler); - assertEq(saviourRegistry.lastSaveTime("gold", safeHandler), 0); - } - function test_saveSAFE_high_cratio() public { - hevm.warp(now + 1); - - // Initial debt - gold.approve(address(collateralJoin)); - collateralJoin.join(address(this), defaultTokenAmount); - safeEngine.modifySAFECollateralization("gold", me, me, me, int(defaultCollateralAmount), int(defaultTokenAmount)); - safeEngine.transferInternalCoins(me, address(coinJoin), safeEngine.coinBalance(me)); - - // Target SAFE - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - - // Save - hevm.warp(now + 1 days); - - gold.mint(address(this), defaultTokenAmount); - gold.approve(address(collateralJoin)); - collateralJoin.join(address(safeHandler), defaultTokenAmount); - alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount)); - - alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "gold", safe, 950); - assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); - - goldMedian.updateCollateralPrice(3 ether); - goldFSM.updateCollateralPrice(3 ether); - oracleRelayer.updateCollateralPrice("gold"); - - safeEngine.mint(safeHandler, rad(defaultTokenAmount)); - systemCoin.mint(address(alice), defaultTokenAmount); - alice.doDeposit(saviour, systemCoin, "gold", safe, defaultTokenAmount); - - uint256 oldCTokenSupply = cRAI.totalSupply(); - - assertTrue(saviour.keeperPayoutExceedsMinValue()); - assertTrue(saviour.canSave("gold", safeHandler)); - - liquidationEngine.modifyParameters("gold", "liquidationQuantity", rad(111 ether)); - liquidationEngine.modifyParameters("gold", "liquidationPenalty", 1.1 ether); - - assertEq(safeEngine.coinBalance(me), 0); - - uint256 preSaveKeeperBalance = systemCoin.balanceOf(address(this)); - uint auction = liquidationEngine.liquidateSAFE("gold", safeHandler); - - // Checks - assertEq(auction, 0); - assertEq(safeEngine.coinBalance(me), 0); - assertEq(systemCoin.balanceOf(address(this)) - preSaveKeeperBalance, saviour.keeperPayout()); - assertTrue(oldCTokenSupply - cRAI.totalSupply() > 0); - assertTrue(cRAI.totalSupply() > 0); - - (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("gold", safeHandler); - (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("gold"); - assertEq(lockedCollateral * 3E27 * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10 ** 27), 949); - - assertEq(saviourRegistry.lastSaveTime("gold", safeHandler), now); - } - function test_saveSAFE_accumulate_rate() public { - // Initial debt - gold.approve(address(collateralJoin)); - collateralJoin.join(address(this), defaultTokenAmount); - safeEngine.modifySAFECollateralization("gold", me, me, me, int(defaultCollateralAmount), int(defaultTokenAmount)); - safeEngine.transferInternalCoins(me, address(coinJoin), safeEngine.coinBalance(me)); - - // Target safe - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - - // Warp and tax - hevm.warp(now + 3 days); - taxCollector.taxSingle("gold"); - - // Save - gold.mint(address(this), defaultTokenAmount); - gold.approve(address(collateralJoin)); - collateralJoin.join(address(safeHandler), defaultTokenAmount); - alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount / 2)); - - alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "gold", safe, 200); - assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); - - goldMedian.updateCollateralPrice(2 ether); - goldFSM.updateCollateralPrice(2 ether); - oracleRelayer.updateCollateralPrice("gold"); - - safeEngine.mint(safeHandler, rad(defaultTokenAmount)); - systemCoin.mint(address(alice), defaultTokenAmount); - alice.doDeposit(saviour, systemCoin, "gold", safe, defaultTokenAmount); - - uint256 oldCTokenSupply = cRAI.totalSupply(); - - assertTrue(saviour.keeperPayoutExceedsMinValue()); - assertTrue(saviour.canSave("gold", safeHandler)); - - liquidationEngine.modifyParameters("gold", "liquidationQuantity", rad(111 ether)); - liquidationEngine.modifyParameters("gold", "liquidationPenalty", 1.1 ether); - - assertEq(safeEngine.coinBalance(me), 0); - - uint256 preSaveKeeperBalance = systemCoin.balanceOf(address(this)); - uint auction = liquidationEngine.liquidateSAFE("gold", safeHandler); - - assertEq(auction, 0); - assertEq(safeEngine.coinBalance(me), 0); - assertEq(systemCoin.balanceOf(address(this)) - preSaveKeeperBalance, saviour.keeperPayout()); - assertTrue(oldCTokenSupply - cRAI.totalSupply() > 0); - assertTrue(cRAI.totalSupply() > 0); - - (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("gold", safeHandler); - (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("gold"); - assertEq(lockedCollateral * 2E27 * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10 ** 27), 199); - - assertEq(saviourRegistry.lastSaveTime("gold", safeHandler), now); - } - function test_saveSAFE_withdraw() public { - hevm.warp(now + 1); - - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - default_save(safe, safeHandler, 200); - - assertEq(saviourRegistry.lastSaveTime("gold", safeHandler), now); - - // Withdraw - alice.doWithdraw(saviour, "gold", safe, saviour.cTokenCover("gold", safeHandler), address(this)); - - uint256 totalSupply = cRAI.totalSupply(); - assertTrue(totalSupply == 0); - assertEq(cRAI.balanceOf(address(saviour)), 0); - assertEq(systemCoin.balanceOf(address(cRAI)), 0); - assertEq(saviour.cTokenCover("gold", safeHandler), 0); - } - function testFail_saveSAFE_twice_in_row_same_keeper() public { - hevm.warp(now + 1); - - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - default_save(safe, safeHandler, 200); - - hevm.warp(now + 1); - default_save(safe, safeHandler, 200); - } - function test_saveSAFE_twice_large_delay() public { - hevm.warp(now + 1); - - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - default_save(safe, safeHandler, 155); - - hevm.warp(now + saviourRegistry.saveCooldown() + 1); - default_second_save(safe, safeHandler, 200); - - assertEq(saviourRegistry.lastSaveTime("gold", safeHandler), now); - } -} +pragma solidity 0.6.7; + +import "ds-test/test.sol"; +import "ds-token/token.sol"; + +import {SAFEEngine} from 'geb/SAFEEngine.sol'; +import {Coin} from 'geb/Coin.sol'; +import {LiquidationEngine} from 'geb/LiquidationEngine.sol'; +import {AccountingEngine} from 'geb/AccountingEngine.sol'; +import {TaxCollector} from 'geb/TaxCollector.sol'; +import {BasicCollateralJoin, CoinJoin} from 'geb/BasicTokenAdapters.sol'; +import {OracleRelayer} from 'geb/OracleRelayer.sol'; +import {EnglishCollateralAuctionHouse} from 'geb/CollateralAuctionHouse.sol'; +import {GebSafeManager} from "geb-safe-manager/GebSafeManager.sol"; + +import {CErc20, CToken} from "../integrations/compound/CErc20.sol"; +import {ComptrollerG2} from "../integrations/compound/ComptrollerG2.sol"; +import {Unitroller} from "../integrations/compound/Unitroller.sol"; +import {WhitePaperInterestRateModel} from "../integrations/compound/WhitePaperInterestRateModel.sol"; +import {PriceOracle} from "../integrations/compound/PriceOracle.sol"; + +import {SaviourCRatioSetter} from "../SaviourCRatioSetter.sol"; +import {SAFESaviourRegistry} from "../SAFESaviourRegistry.sol"; + +import {CompoundSystemCoinSafeSaviour} from "../saviours/CompoundSystemCoinSafeSaviour.sol"; + +abstract contract Hevm { + function warp(uint256) virtual public; +} +contract CompoundPriceOracle is PriceOracle { + uint256 price; + + function setPrice(uint256 newPrice) public { + price = newPrice; + } + + function getUnderlyingPrice(CToken cToken) override external view returns (uint) { + return price; + } +} +contract Feed { + uint256 public price; + bool public validPrice; + uint public lastUpdateTime; + address public priceSource; + + constructor(uint256 price_, bool validPrice_) public { + price = price_; + validPrice = validPrice_; + lastUpdateTime = now; + } + function updatePriceSource(address priceSource_) external { + priceSource = priceSource_; + } + function changeValidity() external { + validPrice = !validPrice; + } + function updateCollateralPrice(uint256 price_) external { + price = price_; + lastUpdateTime = now; + } + function read() external view returns (uint256) { + return price; + } + function getResultWithValidity() external view returns (uint256, bool) { + return (price, validPrice); + } +} +contract TestSAFEEngine is SAFEEngine { + uint256 constant RAY = 10 ** 27; + + constructor() public {} + + function mint(address usr, uint wad) public { + coinBalance[usr] += wad * RAY; + globalDebt += wad * RAY; + } + function balanceOf(address usr) public view returns (uint) { + return uint(coinBalance[usr] / RAY); + } + function setAccumulatedRate(bytes32 collateralType, uint256 rate) public { + collateralTypes[collateralType].accumulatedRate = rate; + } +} +contract TestAccountingEngine is AccountingEngine { + constructor(address safeEngine, address surplusAuctionHouse, address debtAuctionHouse) + public AccountingEngine(safeEngine, surplusAuctionHouse, debtAuctionHouse) {} + + function totalDeficit() public view returns (uint) { + return safeEngine.debtBalance(address(this)); + } + function totalSurplus() public view returns (uint) { + return safeEngine.coinBalance(address(this)); + } + function preAuctionDebt() public view returns (uint) { + return subtract(subtract(totalDeficit(), totalQueuedDebt), totalOnAuctionDebt); + } +} +contract FakeUser { + function doModifyParameters( + CompoundSystemCoinSafeSaviour saviour, + bytes32 parameter, + uint256 data + ) public { + saviour.modifyParameters(parameter, data); + } + + function doModifyParameters( + CompoundSystemCoinSafeSaviour saviour, + bytes32 parameter, + address data + ) public { + saviour.modifyParameters(parameter, data); + } + + function doOpenSafe( + GebSafeManager manager, + bytes32 collateralType, + address usr + ) public returns (uint256) { + return manager.openSAFE(collateralType, usr); + } + + function doSafeAllow( + GebSafeManager manager, + uint safe, + address usr, + uint ok + ) public { + manager.allowSAFE(safe, usr, ok); + } + + function doHandlerAllow( + GebSafeManager manager, + address usr, + uint ok + ) public { + manager.allowHandler(usr, ok); + } + + function doTransferSAFEOwnership( + GebSafeManager manager, + uint safe, + address dst + ) public { + manager.transferSAFEOwnership(safe, dst); + } + + function doModifySAFECollateralization( + GebSafeManager manager, + uint safe, + int deltaCollateral, + int deltaDebt + ) public { + manager.modifySAFECollateralization(safe, deltaCollateral, deltaDebt); + } + + function doApproveSAFEModification( + SAFEEngine safeEngine, + address usr + ) public { + safeEngine.approveSAFEModification(usr); + } + + function doSAFEEngineModifySAFECollateralization( + SAFEEngine safeEngine, + bytes32 collateralType, + address safe, + address collateralSource, + address debtDst, + int deltaCollateral, + int deltaDebt + ) public { + safeEngine.modifySAFECollateralization(collateralType, safe, collateralSource, debtDst, deltaCollateral, deltaDebt); + } + + function doProtectSAFE( + GebSafeManager manager, + uint safe, + address liquidationEngine, + address saviour + ) public { + manager.protectSAFE(safe, liquidationEngine, saviour); + } + + function doDeposit( + CompoundSystemCoinSafeSaviour saviour, + Coin systemCoin, + bytes32 collateralType, + uint256 safeID, + uint256 systemCoinAmount + ) public { + systemCoin.approve(address(saviour), systemCoinAmount); + saviour.deposit(collateralType, safeID, systemCoinAmount); + } + + function doWithdraw( + CompoundSystemCoinSafeSaviour saviour, + bytes32 collateralType, + uint256 safeID, + uint256 cTokenAmount, + address dst + ) public { + saviour.withdraw(collateralType, safeID, cTokenAmount, dst); + } + + function doTransferInternalCoins( + GebSafeManager manager, + uint256 safe, + address dst, + uint256 amt + ) public { + manager.transferInternalCoins(safe, dst, amt); + } + + function doSetDesiredCollateralizationRatio( + SaviourCRatioSetter cRatioSetter, + bytes32 collateralType, + uint safe, + uint cRatio + ) public { + cRatioSetter.setDesiredCollateralizationRatio(collateralType, safe, cRatio); + } +} + +contract CompoundSystemCoinSafeSaviourTest is DSTest { + Hevm hevm; + + TestSAFEEngine safeEngine; + TestAccountingEngine accountingEngine; + LiquidationEngine liquidationEngine; + OracleRelayer oracleRelayer; + TaxCollector taxCollector; + + BasicCollateralJoin collateralJoin; + CoinJoin coinJoin; + + CoinJoin systemCoinJoin; + EnglishCollateralAuctionHouse collateralAuctionHouse; + + GebSafeManager safeManager; + + Feed systemCoinOracle; + CompoundPriceOracle compoundSysCoinOracle; + + Coin systemCoin; + + CompoundSystemCoinSafeSaviour saviour; + SaviourCRatioSetter cRatioSetter; + SAFESaviourRegistry saviourRegistry; + + CErc20 cRAI; + ComptrollerG2 comptroller; + Unitroller unitroller; + WhitePaperInterestRateModel interestRateModel; + + FakeUser alice; + + Feed goldFSM; + Feed goldMedian; + + DSToken gold; + + address me; + + // Compound Params + uint256 systemCoinsToMint = 100000 * 10**18; + uint256 systemCoinPrice = 1 ether; + + uint256 baseRatePerYear = 10**17; + uint256 multiplierPerYear = 45 * 10**17; + uint256 liquidationIncentive = 1 ether; + uint256 closeFactor = 0.051 ether; + uint256 maxAssets = 10; + uint256 exchangeRate = 1 ether; + + uint8 cTokenDecimals = 8; + + string cTokenSymbol = "cRAI"; + string cTokenName = "cRAI"; + + // Saviour params + uint256 saveCooldown = 1 days; + uint256 keeperPayout = 0.5 ether; + uint256 minKeeperPayoutValue = 0.01 ether; + uint256 payoutToSAFESize = 40; + uint256 defaultDesiredCollateralizationRatio = 200; + uint256 minDesiredCollateralizationRatio = 155; + + // Core system params + uint256 goldPrice = 3.75 ether; + uint256 minCRatio = 1.5 ether; + uint256 goldToMint = 5000 ether; + uint256 goldCeiling = 1000 ether; + uint256 goldFloor = 10 ether; + uint256 goldSafetyPrice = 1 ether; + uint256 goldLiquidationPenalty = 1 ether; + + uint256 defaultCollateralAmount = 40 ether; + uint256 defaultTokenAmount = 100 ether; + + function setUp() public { + hevm = Hevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + hevm.warp(604411200); + + // System coin + systemCoin = new Coin("RAI", "RAI", 1); + systemCoin.mint(address(this), systemCoinsToMint); + systemCoinOracle = new Feed(systemCoinPrice, true); + + // Compound setup + compoundSysCoinOracle = new CompoundPriceOracle(); + compoundSysCoinOracle.setPrice(systemCoinPrice); + + interestRateModel = new WhitePaperInterestRateModel(baseRatePerYear, multiplierPerYear); + unitroller = new Unitroller(); + comptroller = new ComptrollerG2(); + + unitroller._setPendingImplementation(address(comptroller)); + comptroller._become(unitroller); + + comptroller._setLiquidationIncentive(liquidationIncentive); + comptroller._setCloseFactor(closeFactor); + comptroller._setMaxAssets(maxAssets); + comptroller._setPriceOracle(compoundSysCoinOracle); + + cRAI = new CErc20(); + cRAI.initialize( + address(systemCoin), + comptroller, + interestRateModel, + exchangeRate, + cTokenName, + cTokenSymbol, + cTokenDecimals + ); + comptroller._supportMarket(cRAI); + + // Core system + safeEngine = new TestSAFEEngine(); + + goldFSM = new Feed(goldPrice, true); + goldMedian = new Feed(goldPrice, true); + goldFSM.updatePriceSource(address(goldMedian)); + + oracleRelayer = new OracleRelayer(address(safeEngine)); + oracleRelayer.modifyParameters("redemptionPrice", ray(systemCoinPrice)); + oracleRelayer.modifyParameters("gold", "orcl", address(goldFSM)); + oracleRelayer.modifyParameters("gold", "safetyCRatio", ray(minCRatio)); + oracleRelayer.modifyParameters("gold", "liquidationCRatio", ray(minCRatio)); + safeEngine.addAuthorization(address(oracleRelayer)); + + accountingEngine = new TestAccountingEngine( + address(safeEngine), address(0x1), address(0x2) + ); + safeEngine.addAuthorization(address(accountingEngine)); + + taxCollector = new TaxCollector(address(safeEngine)); + taxCollector.initializeCollateralType("gold"); + taxCollector.modifyParameters("primaryTaxReceiver", address(accountingEngine)); + taxCollector.modifyParameters("gold", "stabilityFee", 1000000564701133626865910626); // 5% / day + safeEngine.addAuthorization(address(taxCollector)); + + liquidationEngine = new LiquidationEngine(address(safeEngine)); + liquidationEngine.modifyParameters("accountingEngine", address(accountingEngine)); + + safeEngine.addAuthorization(address(liquidationEngine)); + accountingEngine.addAuthorization(address(liquidationEngine)); + + gold = new DSToken("GOLD", 'GOLD'); + gold.mint(goldToMint); + + safeEngine.initializeCollateralType("gold"); + + collateralJoin = new BasicCollateralJoin(address(safeEngine), "gold", address(gold)); + + coinJoin = new CoinJoin(address(safeEngine), address(systemCoin)); + systemCoin.addAuthorization(address(coinJoin)); + + safeEngine.addAuthorization(address(collateralJoin)); + + safeEngine.modifyParameters("gold", "safetyPrice", ray(goldSafetyPrice)); + safeEngine.modifyParameters("gold", "debtCeiling", rad(goldCeiling)); + safeEngine.modifyParameters("globalDebtCeiling", rad(goldCeiling)); + safeEngine.modifyParameters("gold", "debtFloor", rad(goldFloor)); + + collateralAuctionHouse = new EnglishCollateralAuctionHouse(address(safeEngine), address(liquidationEngine), "gold"); + collateralAuctionHouse.addAuthorization(address(liquidationEngine)); + + liquidationEngine.addAuthorization(address(collateralAuctionHouse)); + liquidationEngine.modifyParameters("gold", "collateralAuctionHouse", address(collateralAuctionHouse)); + liquidationEngine.modifyParameters("gold", "liquidationPenalty", goldLiquidationPenalty); + + safeEngine.addAuthorization(address(collateralAuctionHouse)); + safeEngine.approveSAFEModification(address(collateralAuctionHouse)); + + safeManager = new GebSafeManager(address(safeEngine)); + oracleRelayer.updateCollateralPrice("gold"); + + // Saviour infra + saviourRegistry = new SAFESaviourRegistry(saveCooldown); + cRatioSetter = new SaviourCRatioSetter(address(oracleRelayer), address(safeManager)); + cRatioSetter.setDefaultCRatio("gold", defaultDesiredCollateralizationRatio); + + saviour = new CompoundSystemCoinSafeSaviour( + address(coinJoin), + address(cRatioSetter), + address(systemCoinOracle), + address(liquidationEngine), + address(taxCollector), + address(oracleRelayer), + address(safeManager), + address(saviourRegistry), + address(cRAI), + keeperPayout, + minKeeperPayoutValue + ); + saviourRegistry.toggleSaviour(address(saviour)); + liquidationEngine.connectSAFESaviour(address(saviour)); + + me = address(this); + alice = new FakeUser(); + } + + // --- Math --- + function ray(uint wad) internal pure returns (uint) { + return wad * 10 ** 9; + } + function rad(uint wad) internal pure returns (uint) { + return wad * 10 ** 27; + } + + // --- Default actions/scenarios --- + function default_create_liquidatable_position(uint256 desiredCRatio, uint256 liquidatableCollateralPrice) internal returns (address) { + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "gold", safe, desiredCRatio); + assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); + + goldMedian.updateCollateralPrice(liquidatableCollateralPrice); + goldFSM.updateCollateralPrice(liquidatableCollateralPrice); + oracleRelayer.updateCollateralPrice("gold"); + + return safeHandler; + } + function default_save(uint256 safe, address safeHandler, uint desiredCRatio) internal { + default_modify_collateralization(safe, safeHandler); + + alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "gold", safe, desiredCRatio); + assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); + + goldMedian.updateCollateralPrice(3 ether); + goldFSM.updateCollateralPrice(3 ether); + oracleRelayer.updateCollateralPrice("gold"); + + safeEngine.mint(safeHandler, rad(defaultTokenAmount)); + systemCoin.mint(address(alice), defaultTokenAmount); + alice.doDeposit(saviour, systemCoin, "gold", safe, defaultTokenAmount); + + uint256 oldCTokenSupply = cRAI.totalSupply(); + + assertTrue(saviour.keeperPayoutExceedsMinValue()); + assertTrue(saviour.canSave("gold", safeHandler)); + + liquidationEngine.modifyParameters("gold", "liquidationQuantity", rad(111 ether)); + liquidationEngine.modifyParameters("gold", "liquidationPenalty", 1.1 ether); + + uint256 preSaveKeeperBalance = systemCoin.balanceOf(address(this)); + uint auction = liquidationEngine.liquidateSAFE("gold", safeHandler); + + assertEq(auction, 0); + assertEq(systemCoin.balanceOf(address(this)) - preSaveKeeperBalance, saviour.keeperPayout()); + assertTrue(oldCTokenSupply - cRAI.totalSupply() > 0); + assertTrue(cRAI.totalSupply() > 0); + + (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("gold", safeHandler); + (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("gold"); + uint256 computedCRatio = lockedCollateral * 3E27 * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10 ** 27); + assertTrue(computedCRatio == desiredCRatio || computedCRatio == desiredCRatio - 1); + } + function default_second_save(uint256 safe, address safeHandler, uint desiredCRatio) internal { + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "gold", safe, desiredCRatio); + + goldMedian.updateCollateralPrice(2.5 ether); + goldFSM.updateCollateralPrice(2.5 ether); + oracleRelayer.updateCollateralPrice("gold"); + + uint256 oldCTokenSupply = cRAI.totalSupply(); + + assertTrue(saviour.keeperPayoutExceedsMinValue()); + assertTrue(saviour.canSave("gold", safeHandler)); + + liquidationEngine.modifyParameters("gold", "liquidationQuantity", rad(111 ether)); + liquidationEngine.modifyParameters("gold", "liquidationPenalty", 1.1 ether); + + uint256 preSaveKeeperBalance = systemCoin.balanceOf(address(this)); + uint auction = liquidationEngine.liquidateSAFE("gold", safeHandler); + + assertEq(auction, 0); + assertEq(systemCoin.balanceOf(address(this)) - preSaveKeeperBalance, saviour.keeperPayout()); + assertTrue(oldCTokenSupply - cRAI.totalSupply() > 0); + assertTrue(cRAI.totalSupply() > 0); + + (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("gold", safeHandler); + (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("gold"); + uint256 computedCRatio = lockedCollateral * 2.5E27 * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10 ** 27); + assertTrue(computedCRatio == desiredCRatio || computedCRatio == desiredCRatio - 1); + } + function default_liquidate_safe(address safeHandler) internal { + goldMedian.updateCollateralPrice(3 ether); + goldFSM.updateCollateralPrice(3 ether); + oracleRelayer.updateCollateralPrice("gold"); + + liquidationEngine.modifyParameters("gold", "liquidationQuantity", rad(111 ether)); + liquidationEngine.modifyParameters("gold", "liquidationPenalty", 1.1 ether); + + uint auction = liquidationEngine.liquidateSAFE("gold", safeHandler); + // the full SAFE is liquidated + (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("gold", me); + assertEq(lockedCollateral, 0); + assertEq(generatedDebt, 0); + // all debt goes to the accounting engine + assertEq(accountingEngine.totalQueuedDebt(), rad(defaultTokenAmount)); + // auction is for all collateral + (,uint amountToSell,,,,,, uint256 amountToRaise) = collateralAuctionHouse.bids(auction); + assertEq(amountToSell, defaultCollateralAmount); + assertEq(amountToRaise, rad(110 ether)); + } + function default_create_liquidatable_position_deposit_cover(uint256 desiredCRatio, uint256 liquidatableCollateralPrice) internal returns (address) { + // Create position + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "gold", safe, desiredCRatio); + assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); + + goldMedian.updateCollateralPrice(liquidatableCollateralPrice); + goldFSM.updateCollateralPrice(liquidatableCollateralPrice); + oracleRelayer.updateCollateralPrice("gold"); + + // Deposit cover + safeEngine.mint(safeHandler, rad(defaultTokenAmount)); + systemCoin.mint(address(alice), defaultTokenAmount); + + alice.doDeposit(saviour, systemCoin, "gold", safe, defaultTokenAmount); + + uint256 totalSupply = cRAI.totalSupply(); + assertTrue(totalSupply > 0); + assertEq(cRAI.balanceOf(address(saviour)), totalSupply); + assertEq(systemCoin.balanceOf(address(cRAI)), defaultTokenAmount); + assertEq(saviour.cTokenCover("gold", safeHandler), totalSupply); + + return safeHandler; + } + function default_create_position_deposit_cover() internal returns (uint, address, uint) { + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + safeEngine.mint(safeHandler, rad(defaultTokenAmount)); + systemCoin.mint(address(alice), defaultTokenAmount); + + alice.doDeposit(saviour, systemCoin, "gold", safe, defaultTokenAmount); + + uint256 totalSupply = cRAI.totalSupply(); + assertTrue(totalSupply > 0); + assertEq(cRAI.balanceOf(address(saviour)), totalSupply); + assertEq(systemCoin.balanceOf(address(cRAI)), defaultTokenAmount); + assertEq(saviour.cTokenCover("gold", safeHandler), totalSupply); + + return (safe, safeHandler, totalSupply); + } + function default_modify_collateralization(uint256 safe, address safeHandler) internal { + gold.approve(address(collateralJoin)); + collateralJoin.join(address(safeHandler), defaultTokenAmount); + alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount)); + } + + // --- Tests --- + function test_setup() public { + assertEq(saviour.authorizedAccounts(address(this)), 1); + assertEq(saviour.keeperPayout(), keeperPayout); + assertEq(saviour.minKeeperPayoutValue(), minKeeperPayoutValue); + + assertEq(address(saviour.coinJoin()), address(coinJoin)); + assertEq(address(saviour.cRatioSetter()), address(cRatioSetter)); + assertEq(address(saviour.liquidationEngine()), address(liquidationEngine)); + assertEq(address(saviour.oracleRelayer()), address(oracleRelayer)); + assertEq(address(saviour.systemCoinOrcl()), address(systemCoinOracle)); + assertEq(address(saviour.taxCollector()), address(taxCollector)); + assertEq(address(saviour.systemCoin()), address(systemCoin)); + assertEq(address(saviour.safeEngine()), address(safeEngine)); + assertEq(address(saviour.safeManager()), address(safeManager)); + assertEq(address(saviour.saviourRegistry()), address(saviourRegistry)); + assertEq(address(saviour.cToken()), address(cRAI)); + } + function testFail_modifyParameters_uint_unauthorized() public { + alice.doModifyParameters(saviour, "keeperPayout", 5); + } + function test_modifyParameters_uint() public { + saviour.modifyParameters("keeperPayout", 5); + assertEq(saviour.keeperPayout(), 5); + } + function testFail_modifyParameters_address_unauthorized() public { + systemCoinOracle = new Feed(systemCoinPrice, true); + alice.doModifyParameters(saviour, "systemCoinOrcl", address(systemCoinOracle)); + } + function test_modifyParameters_address() public { + systemCoinOracle = new Feed(systemCoinPrice, true); + saviour.modifyParameters("systemCoinOrcl", address(systemCoinOracle)); + assertEq(address(saviour.systemCoinOrcl()), address(systemCoinOracle)); + } + function testFail_deposit_liq_engine_not_approved() public { + liquidationEngine.disconnectSAFESaviour(address(saviour)); + + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + safeEngine.mint(safeHandler, rad(defaultTokenAmount)); + systemCoin.mint(address(alice), defaultTokenAmount); + + alice.doDeposit(saviour, systemCoin, "gold", 1, defaultTokenAmount); + } + function testFail_deposit_null_sys_coin_amount() public { + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + safeEngine.mint(safeHandler, rad(defaultTokenAmount)); + systemCoin.mint(address(alice), defaultTokenAmount); + + alice.doDeposit(saviour, systemCoin, "gold", safe, 0); + } + function testFail_deposit_inexistent_safe() public { + systemCoin.mint(address(alice), defaultTokenAmount); + + alice.doDeposit(saviour, systemCoin, "gold", 1, defaultTokenAmount); + } + function test_deposit_no_prior_compound_liquidity() public { + default_create_position_deposit_cover(); + } + function test_deposit_twice() public { + (uint safe, address safeHandler, ) = default_create_position_deposit_cover(); + + // Second deposit + safeEngine.mint(safeHandler, rad(defaultTokenAmount)); + systemCoin.mint(address(alice), defaultTokenAmount); + + alice.doDeposit(saviour, systemCoin, "gold", safe, defaultTokenAmount); + + uint256 totalSupply = cRAI.totalSupply(); + assertTrue(totalSupply > 0); + assertEq(cRAI.balanceOf(address(saviour)), totalSupply); + assertEq(systemCoin.balanceOf(address(cRAI)), defaultTokenAmount * 2); + assertEq(saviour.cTokenCover("gold", safeHandler), totalSupply); + } + function test_deposit_after_everything_withdrawn() public { + (uint safe, address safeHandler, uint totalSupply) = default_create_position_deposit_cover(); + + // Withdraw + alice.doWithdraw(saviour, "gold", safe, totalSupply, address(alice)); + + totalSupply = cRAI.totalSupply(); + assertTrue(totalSupply == 0); + assertEq(cRAI.balanceOf(address(saviour)), 0); + assertEq(systemCoin.balanceOf(address(cRAI)), 0); + assertEq(saviour.cTokenCover("gold", safeHandler), 0); + + // Second deposit + alice.doDeposit(saviour, systemCoin, "gold", safe, defaultTokenAmount); + + totalSupply = cRAI.totalSupply(); + assertTrue(totalSupply > 0); + assertEq(cRAI.balanceOf(address(saviour)), totalSupply); + assertEq(systemCoin.balanceOf(address(cRAI)), defaultTokenAmount); + assertEq(saviour.cTokenCover("gold", safeHandler), totalSupply); + } + function testFail_withdraw_unauthorized() public { + (uint safe, , ) = default_create_position_deposit_cover(); + + // Withdraw by unauthed + FakeUser bob = new FakeUser(); + bob.doWithdraw(saviour, "gold", safe, defaultTokenAmount, address(this)); + } + function testFail_withdraw_more_than_deposited() public { + (uint safe, , uint totalSupply) = default_create_position_deposit_cover(); + + // Withdraw + alice.doWithdraw(saviour, "gold", safe, totalSupply + 1, address(this)); + } + function testFail_withdraw_null() public { + (uint safe, , ) = default_create_position_deposit_cover(); + + // Withdraw + alice.doWithdraw(saviour, "gold", safe, 0, address(this)); + } + function test_withdraw() public { + (uint safe, address safeHandler, uint totalSupply) = default_create_position_deposit_cover(); + + // Withdraw + alice.doWithdraw(saviour, "gold", safe, totalSupply, address(this)); + + totalSupply = cRAI.totalSupply(); + assertTrue(totalSupply == 0); + assertEq(cRAI.balanceOf(address(saviour)), 0); + assertEq(systemCoin.balanceOf(address(cRAI)), 0); + assertEq(saviour.cTokenCover("gold", safeHandler), 0); + } + function test_withdraw_twice() public { + (uint safe, address safeHandler, uint totalSupply) = default_create_position_deposit_cover(); + + // Withdraw first time + alice.doWithdraw(saviour, "gold", safe, totalSupply / 2, address(this)); + + assertTrue(totalSupply > 0); + assertEq(cRAI.balanceOf(address(saviour)), totalSupply / 2); + assertEq(systemCoin.balanceOf(address(cRAI)), defaultTokenAmount / 2); + assertEq(saviour.cTokenCover("gold", safeHandler), totalSupply / 2); + + // Withdraw second time + alice.doWithdraw(saviour, "gold", safe, totalSupply / 2, address(this)); + + totalSupply = cRAI.totalSupply(); + assertTrue(totalSupply == 0); + assertEq(cRAI.balanceOf(address(saviour)), 0); + assertEq(systemCoin.balanceOf(address(cRAI)), 0); + assertEq(saviour.cTokenCover("gold", safeHandler), 0); + } + function test_keeperPayoutExceedsMinValue_valid_orcl_result_true() public { + assertTrue(saviour.keeperPayoutExceedsMinValue()); + } + function test_keeperPayoutExceedsMinValue_valid_orcl_result_false() public { + saviour.modifyParameters("minKeeperPayoutValue", minKeeperPayoutValue * 10000); + assertTrue(!saviour.keeperPayoutExceedsMinValue()); + } + function test_keeperPayoutExceedsMinValue_invalid_orcl_result() public { + systemCoinOracle.changeValidity(); + assertTrue(!saviour.keeperPayoutExceedsMinValue()); + } + function test_keeperPayoutExceedsMinValue_null_orcl_result() public { + systemCoinOracle.updateCollateralPrice(0); + assertTrue(!saviour.keeperPayoutExceedsMinValue()); + } + function test_getKeeperPayoutValue_valid_orcl_result_true() public { + assertEq(saviour.getKeeperPayoutValue(), 0.5 ether); + } + function test_getKeeperPayoutValue_invalid_orcl_result() public { + systemCoinOracle.changeValidity(); + assertEq(saviour.getKeeperPayoutValue(), 0); + } + function test_getKeeperPayoutValue_null_orcl_result() public { + systemCoinOracle.updateCollateralPrice(0); + assertEq(saviour.getKeeperPayoutValue(), 0); + } + function test_tokenAmountUsedToSave_col_invalid_price() public { + address safeHandler = default_create_liquidatable_position(250, 3 ether); + + safeEngine.mint(safeHandler, rad(defaultTokenAmount)); + systemCoin.mint(address(alice), defaultTokenAmount); + alice.doDeposit(saviour, systemCoin, "gold", 1, defaultTokenAmount); + + goldFSM.changeValidity(); + assertEq(saviour.tokenAmountUsedToSave("gold", safeHandler), uint(-1)); + } + function test_tokenAmountUsedToSave_null_price() public { + address safeHandler = default_create_liquidatable_position(200, 1 ether); + + safeEngine.mint(safeHandler, rad(defaultTokenAmount)); + systemCoin.mint(address(alice), defaultTokenAmount); + alice.doDeposit(saviour, systemCoin, "gold", 1, defaultTokenAmount); + + goldFSM.updateCollateralPrice(0); + assertEq(saviour.tokenAmountUsedToSave("gold", safeHandler), uint(-1)); + } + function test_tokenAmountUsedToSave() public { + address safeHandler = default_create_liquidatable_position(400, 1 ether); + + safeEngine.mint(safeHandler, rad(defaultTokenAmount)); + systemCoin.mint(address(alice), defaultTokenAmount); + alice.doDeposit(saviour, systemCoin, "gold", 1, defaultTokenAmount); + + assertEq(saviour.tokenAmountUsedToSave("gold", safeHandler), 90 ether); + } + function test_canSave_invalid_collateral_price() public { + address safeHandler = default_create_liquidatable_position_deposit_cover(250, 1 ether); + goldFSM.changeValidity(); + assertTrue(!saviour.canSave("gold", safeHandler)); + } + function test_canSave_null_collateral_price() public { + address safeHandler = default_create_liquidatable_position_deposit_cover(250, 1 ether); + goldFSM.updateCollateralPrice(0); + assertTrue(!saviour.canSave("gold", safeHandler)); + } + function test_canSave_null_safe_debt() public { + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + assertTrue(!saviour.canSave("gold", safeHandler)); + } + function test_canSave_insufficient_ctoken_cover() public { + saviour.modifyParameters("keeperPayout", 5000 ether); + address safeHandler = default_create_liquidatable_position_deposit_cover(250, 1 ether); + assertTrue(!saviour.canSave("gold", safeHandler)); + } + function test_canSave() public { + address safeHandler = default_create_liquidatable_position_deposit_cover(250, 1 ether); + assertTrue(saviour.canSave("gold", safeHandler)); + } + function test_canSave_charged_interest() public { + address safeHandler = default_create_liquidatable_position_deposit_cover(250, 1 ether); + + hevm.warp(now + 1 days); + taxCollector.taxSingle("gold"); + + assertTrue(saviour.canSave("gold", safeHandler)); + } + function testFail_saveSAFE_debt_below_floor() public { + hevm.warp(now + 1); + + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "gold", safe, 200); + + goldMedian.updateCollateralPrice(3 ether); + goldFSM.updateCollateralPrice(3 ether); + oracleRelayer.updateCollateralPrice("gold"); + + safeEngine.mint(safeHandler, rad(defaultTokenAmount)); + systemCoin.mint(address(alice), defaultTokenAmount); + alice.doDeposit(saviour, systemCoin, "gold", safe, defaultTokenAmount); + + liquidationEngine.modifyParameters("gold", "liquidationQuantity", rad(111 ether)); + liquidationEngine.modifyParameters("gold", "liquidationPenalty", 1.1 ether); + safeEngine.modifyParameters("gold", "debtFloor", defaultTokenAmount - 1); + saviour.saveSAFE(address(this), "gold", safeHandler); + } + function testFail_saveSAFE_invalid_caller() public { + hevm.warp(now + 1); + + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "gold", safe, 200); + + goldMedian.updateCollateralPrice(3 ether); + goldFSM.updateCollateralPrice(3 ether); + oracleRelayer.updateCollateralPrice("gold"); + + safeEngine.mint(safeHandler, rad(defaultTokenAmount)); + systemCoin.mint(address(alice), defaultTokenAmount); + alice.doDeposit(saviour, systemCoin, "gold", safe, defaultTokenAmount); + + liquidationEngine.modifyParameters("gold", "liquidationQuantity", rad(111 ether)); + liquidationEngine.modifyParameters("gold", "liquidationPenalty", 1.1 ether); + + saviour.saveSAFE(address(this), "gold", safeHandler); + } + function test_saveSAFE_small_payout() public { + saviour.modifyParameters("keeperPayout", 5000 ether); + + hevm.warp(now + 1); + + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "gold", safe, 155); + assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); + + safeEngine.mint(safeHandler, rad(defaultTokenAmount)); + systemCoin.mint(address(alice), defaultTokenAmount); + alice.doDeposit(saviour, systemCoin, "gold", safe, defaultTokenAmount); + + default_liquidate_safe(safeHandler); + assertEq(saviourRegistry.lastSaveTime("gold", safeHandler), 0); + } + function test_saveSAFE_insufficient_ctoken_coverage() public { + hevm.warp(now + 1); + + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "gold", safe, 900); + assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); + + safeEngine.mint(safeHandler, rad(defaultTokenAmount)); + systemCoin.mint(address(alice), defaultTokenAmount); + alice.doDeposit(saviour, systemCoin, "gold", safe, defaultTokenAmount / 10); + + default_liquidate_safe(safeHandler); + assertEq(saviourRegistry.lastSaveTime("gold", safeHandler), 0); + } + function test_saveSAFE_high_cratio() public { + hevm.warp(now + 1); + + // Initial debt + gold.approve(address(collateralJoin)); + collateralJoin.join(address(this), defaultTokenAmount); + safeEngine.modifySAFECollateralization("gold", me, me, me, int(defaultCollateralAmount), int(defaultTokenAmount)); + safeEngine.transferInternalCoins(me, address(coinJoin), safeEngine.coinBalance(me)); + + // Target SAFE + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + + // Save + hevm.warp(now + 1 days); + + gold.mint(address(this), defaultTokenAmount); + gold.approve(address(collateralJoin)); + collateralJoin.join(address(safeHandler), defaultTokenAmount); + alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount)); + + alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "gold", safe, 950); + assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); + + goldMedian.updateCollateralPrice(3 ether); + goldFSM.updateCollateralPrice(3 ether); + oracleRelayer.updateCollateralPrice("gold"); + + safeEngine.mint(safeHandler, rad(defaultTokenAmount)); + systemCoin.mint(address(alice), defaultTokenAmount); + alice.doDeposit(saviour, systemCoin, "gold", safe, defaultTokenAmount); + + uint256 oldCTokenSupply = cRAI.totalSupply(); + + assertTrue(saviour.keeperPayoutExceedsMinValue()); + assertTrue(saviour.canSave("gold", safeHandler)); + + liquidationEngine.modifyParameters("gold", "liquidationQuantity", rad(111 ether)); + liquidationEngine.modifyParameters("gold", "liquidationPenalty", 1.1 ether); + + assertEq(safeEngine.coinBalance(me), 0); + + uint256 preSaveKeeperBalance = systemCoin.balanceOf(address(this)); + uint auction = liquidationEngine.liquidateSAFE("gold", safeHandler); + + // Checks + assertEq(auction, 0); + assertEq(safeEngine.coinBalance(me), 0); + assertEq(systemCoin.balanceOf(address(this)) - preSaveKeeperBalance, saviour.keeperPayout()); + assertTrue(oldCTokenSupply - cRAI.totalSupply() > 0); + assertTrue(cRAI.totalSupply() > 0); + + (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("gold", safeHandler); + (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("gold"); + assertEq(lockedCollateral * 3E27 * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10 ** 27), 949); + + assertEq(saviourRegistry.lastSaveTime("gold", safeHandler), now); + } + function test_saveSAFE_accumulate_rate() public { + // Initial debt + gold.approve(address(collateralJoin)); + collateralJoin.join(address(this), defaultTokenAmount); + safeEngine.modifySAFECollateralization("gold", me, me, me, int(defaultCollateralAmount), int(defaultTokenAmount)); + safeEngine.transferInternalCoins(me, address(coinJoin), safeEngine.coinBalance(me)); + + // Target safe + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + + // Warp and tax + hevm.warp(now + 3 days); + taxCollector.taxSingle("gold"); + + // Save + gold.mint(address(this), defaultTokenAmount); + gold.approve(address(collateralJoin)); + collateralJoin.join(address(safeHandler), defaultTokenAmount); + alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount / 2)); + + alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "gold", safe, 200); + assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); + + goldMedian.updateCollateralPrice(2 ether); + goldFSM.updateCollateralPrice(2 ether); + oracleRelayer.updateCollateralPrice("gold"); + + safeEngine.mint(safeHandler, rad(defaultTokenAmount)); + systemCoin.mint(address(alice), defaultTokenAmount); + alice.doDeposit(saviour, systemCoin, "gold", safe, defaultTokenAmount); + + uint256 oldCTokenSupply = cRAI.totalSupply(); + + assertTrue(saviour.keeperPayoutExceedsMinValue()); + assertTrue(saviour.canSave("gold", safeHandler)); + + liquidationEngine.modifyParameters("gold", "liquidationQuantity", rad(111 ether)); + liquidationEngine.modifyParameters("gold", "liquidationPenalty", 1.1 ether); + + assertEq(safeEngine.coinBalance(me), 0); + + uint256 preSaveKeeperBalance = systemCoin.balanceOf(address(this)); + uint auction = liquidationEngine.liquidateSAFE("gold", safeHandler); + + assertEq(auction, 0); + assertEq(safeEngine.coinBalance(me), 0); + assertEq(systemCoin.balanceOf(address(this)) - preSaveKeeperBalance, saviour.keeperPayout()); + assertTrue(oldCTokenSupply - cRAI.totalSupply() > 0); + assertTrue(cRAI.totalSupply() > 0); + + (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("gold", safeHandler); + (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("gold"); + assertEq(lockedCollateral * 2E27 * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10 ** 27), 199); + + assertEq(saviourRegistry.lastSaveTime("gold", safeHandler), now); + } + function test_saveSAFE_withdraw() public { + hevm.warp(now + 1); + + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + default_save(safe, safeHandler, 200); + + assertEq(saviourRegistry.lastSaveTime("gold", safeHandler), now); + + // Withdraw + alice.doWithdraw(saviour, "gold", safe, saviour.cTokenCover("gold", safeHandler), address(this)); + + uint256 totalSupply = cRAI.totalSupply(); + assertTrue(totalSupply == 0); + assertEq(cRAI.balanceOf(address(saviour)), 0); + assertEq(systemCoin.balanceOf(address(cRAI)), 0); + assertEq(saviour.cTokenCover("gold", safeHandler), 0); + } + function testFail_saveSAFE_twice_in_row_same_keeper() public { + hevm.warp(now + 1); + + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + default_save(safe, safeHandler, 200); + + hevm.warp(now + 1); + default_save(safe, safeHandler, 200); + } + function test_saveSAFE_twice_large_delay() public { + hevm.warp(now + 1); + + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + default_save(safe, safeHandler, 155); + + hevm.warp(now + saviourRegistry.saveCooldown() + 1); + default_second_save(safe, safeHandler, 200); + + assertEq(saviourRegistry.lastSaveTime("gold", safeHandler), now); + } +} diff --git a/src/test/GeneralTokenReserveSafeSaviourTest.t.sol b/src/test/GeneralTokenReserveSafeSaviourTest.t.sol index 7b64c9a..9ff2237 100644 --- a/src/test/GeneralTokenReserveSafeSaviourTest.t.sol +++ b/src/test/GeneralTokenReserveSafeSaviourTest.t.sol @@ -1,730 +1,730 @@ -pragma solidity ^0.6.7; -pragma experimental ABIEncoderV2; - -import "ds-test/test.sol"; -import "ds-token/token.sol"; - -import {SAFEEngine} from 'geb/SAFEEngine.sol'; -import {LiquidationEngine} from 'geb/LiquidationEngine.sol'; -import {AccountingEngine} from 'geb/AccountingEngine.sol'; -import {TaxCollector} from 'geb/TaxCollector.sol'; -import 'geb/BasicTokenAdapters.sol'; -import {OracleRelayer} from 'geb/OracleRelayer.sol'; -import {EnglishCollateralAuctionHouse} from 'geb/CollateralAuctionHouse.sol'; -import {GebSafeManager} from "geb-safe-manager/GebSafeManager.sol"; - -import {SaviourCRatioSetter} from "../SaviourCRatioSetter.sol"; -import {SAFESaviourRegistry} from "../SAFESaviourRegistry.sol"; -import {GeneralTokenReserveSafeSaviour} from "../saviours/GeneralTokenReserveSafeSaviour.sol"; - -abstract contract Hevm { - function warp(uint256) virtual public; -} -contract Feed { - bytes32 public price; - bool public validPrice; - uint public lastUpdateTime; - address public priceSource; - - constructor(uint256 price_, bool validPrice_) public { - price = bytes32(price_); - validPrice = validPrice_; - lastUpdateTime = now; - } - function updatePriceSource(address priceSource_) external { - priceSource = priceSource_; - } - function updateCollateralPrice(uint256 price_) external { - price = bytes32(price_); - lastUpdateTime = now; - } - function getResultWithValidity() external view returns (bytes32, bool) { - return (price, validPrice); - } -} -contract TestSAFEEngine is SAFEEngine { - uint256 constant RAY = 10 ** 27; - - constructor() public {} - - function mint(address usr, uint wad) public { - coinBalance[usr] += wad * RAY; - globalDebt += wad * RAY; - } - function balanceOf(address usr) public view returns (uint) { - return uint(coinBalance[usr] / RAY); - } -} -contract TestAccountingEngine is AccountingEngine { - constructor(address safeEngine, address surplusAuctionHouse, address debtAuctionHouse) - public AccountingEngine(safeEngine, surplusAuctionHouse, debtAuctionHouse) {} - - function totalDeficit() public view returns (uint) { - return safeEngine.debtBalance(address(this)); - } - function totalSurplus() public view returns (uint) { - return safeEngine.coinBalance(address(this)); - } - function preAuctionDebt() public view returns (uint) { - return subtract(subtract(totalDeficit(), totalQueuedDebt), totalOnAuctionDebt); - } -} -contract FakeUser { - function doOpenSafe( - GebSafeManager manager, - bytes32 collateralType, - address usr - ) public returns (uint256) { - return manager.openSAFE(collateralType, usr); - } - - function doSafeAllow( - GebSafeManager manager, - uint safe, - address usr, - uint ok - ) public { - manager.allowSAFE(safe, usr, ok); - } - - function doHandlerAllow( - GebSafeManager manager, - address usr, - uint ok - ) public { - manager.allowHandler(usr, ok); - } - - function doTransferSAFEOwnership( - GebSafeManager manager, - uint safe, - address dst - ) public { - manager.transferSAFEOwnership(safe, dst); - } - - function doModifySAFECollateralization( - GebSafeManager manager, - uint safe, - int deltaCollateral, - int deltaDebt - ) public { - manager.modifySAFECollateralization(safe, deltaCollateral, deltaDebt); - } - - function doApproveSAFEModification( - SAFEEngine safeEngine, - address usr - ) public { - safeEngine.approveSAFEModification(usr); - } - - function doSAFEEngineModifySAFECollateralization( - SAFEEngine safeEngine, - bytes32 collateralType, - address safe, - address collateralSource, - address debtDst, - int deltaCollateral, - int deltaDebt - ) public { - safeEngine.modifySAFECollateralization(collateralType, safe, collateralSource, debtDst, deltaCollateral, deltaDebt); - } - - function doProtectSAFE( - GebSafeManager manager, - uint safe, - address liquidationEngine, - address saviour - ) public { - manager.protectSAFE(safe, liquidationEngine, saviour); - } - - function doDeposit( - GeneralTokenReserveSafeSaviour saviour, - DSToken collateral, - uint safe, - uint amount - ) public { - collateral.approve(address(saviour), amount); - saviour.deposit(safe, amount); - } - - function doWithdraw( - GeneralTokenReserveSafeSaviour saviour, - uint safe, - uint amount, - address dst - ) public { - saviour.withdraw(safe, amount, dst); - } - - function doSetDesiredCollateralizationRatio( - GeneralTokenReserveSafeSaviour saviour, - SaviourCRatioSetter cRatioSetter, - bytes32 collateralType, - uint safe, - uint cRatio - ) public { - cRatioSetter.setDesiredCollateralizationRatio(collateralType, safe, cRatio); - } -} - -contract GeneralTokenReserveSafeSaviourTest is DSTest { - Hevm hevm; - - TestSAFEEngine safeEngine; - TestAccountingEngine accountingEngine; - LiquidationEngine liquidationEngine; - OracleRelayer oracleRelayer; - TaxCollector taxCollector; - - BasicCollateralJoin collateralA; - EnglishCollateralAuctionHouse collateralAuctionHouse; - - GebSafeManager safeManager; - - Feed goldFSM; - Feed goldMedian; - - DSToken gold; - - GeneralTokenReserveSafeSaviour saviour; - SaviourCRatioSetter cRatioSetter; - SAFESaviourRegistry saviourRegistry; - - FakeUser alice; - address me; - - // Saviour parameters - uint256 saveCooldown = 1 days; - uint256 keeperPayout = 0.5 ether; - uint256 minKeeperPayoutValue = 0.01 ether; - uint256 payoutToSAFESize = 40; - uint256 defaultDesiredCollateralizationRatio = 300; - - function ray(uint wad) internal pure returns (uint) { - return wad * 10 ** 9; - } - function rad(uint wad) internal pure returns (uint) { - return wad * 10 ** 27; - } - - // Default actions/scenarios - function default_modify_collateralization(uint256 safe, address safeHandler) internal { - gold.approve(address(collateralA)); - collateralA.join(address(safeHandler), 100 ether); - alice.doModifySAFECollateralization(safeManager, safe, 40 ether, 100 ether); - } - function default_liquidate_safe(address safeHandler) internal { - goldMedian.updateCollateralPrice(3 ether); - goldFSM.updateCollateralPrice(3 ether); - oracleRelayer.updateCollateralPrice("gold"); - - liquidationEngine.modifyParameters("gold", "liquidationQuantity", rad(111 ether)); - liquidationEngine.modifyParameters("gold", "liquidationPenalty", 1.1 ether); - - uint auction = liquidationEngine.liquidateSAFE("gold", safeHandler); - // the full SAFE is liquidated - (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("gold", me); - assertEq(lockedCollateral, 0); - assertEq(generatedDebt, 0); - // all debt goes to the accounting engine - assertEq(accountingEngine.totalQueuedDebt(), rad(100 ether)); - // auction is for all collateral - (,uint amountToSell,,,,,, uint256 amountToRaise) = collateralAuctionHouse.bids(auction); - assertEq(amountToSell, 40 ether); - assertEq(amountToRaise, rad(110 ether)); - } - function default_repay_all_debt(uint256 safe, address safeHandler) internal { - alice.doModifySAFECollateralization(safeManager, safe, 0, -100 ether); - } - function default_save(uint256 safe, address safeHandler, uint desiredCRatio) internal { - default_modify_collateralization(safe, safeHandler); - - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(saviour, cRatioSetter, "gold", safe, desiredCRatio); - assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); - - goldMedian.updateCollateralPrice(3 ether); - goldFSM.updateCollateralPrice(3 ether); - oracleRelayer.updateCollateralPrice("gold"); - - gold.mint(address(alice), saviour.tokenAmountUsedToSave("gold", safeHandler) + saviour.keeperPayout()); - alice.doDeposit(saviour, gold, safe, saviour.tokenAmountUsedToSave("gold", safeHandler) + saviour.keeperPayout()); - - assertTrue(saviour.keeperPayoutExceedsMinValue()); - assertTrue(saviour.canSave("gold", safeHandler)); - - liquidationEngine.modifyParameters("gold", "liquidationQuantity", rad(111 ether)); - liquidationEngine.modifyParameters("gold", "liquidationPenalty", 1.1 ether); - - uint256 preSaveKeeperBalance = gold.balanceOf(address(this)); - uint auction = liquidationEngine.liquidateSAFE("gold", safeHandler); - assertEq(auction, 0); - assertEq(gold.balanceOf(address(this)) - preSaveKeeperBalance, saviour.keeperPayout()); - - (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("gold", safeHandler); - assertEq(lockedCollateral * 3E27 * 100 / (generatedDebt * oracleRelayer.redemptionPrice()), desiredCRatio); - } - function default_save(uint256 safe, address safeHandler) internal { - default_modify_collateralization(safe, safeHandler); - - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); - - goldMedian.updateCollateralPrice(3 ether); - goldFSM.updateCollateralPrice(3 ether); - oracleRelayer.updateCollateralPrice("gold"); - - gold.mint(address(alice), saviour.tokenAmountUsedToSave("gold", safeHandler) + saviour.keeperPayout()); - alice.doDeposit(saviour, gold, safe, saviour.tokenAmountUsedToSave("gold", safeHandler) + saviour.keeperPayout()); - - assertTrue(saviour.keeperPayoutExceedsMinValue()); - assertTrue(saviour.canSave("gold", safeHandler)); - - liquidationEngine.modifyParameters("gold", "liquidationQuantity", rad(111 ether)); - liquidationEngine.modifyParameters("gold", "liquidationPenalty", 1.1 ether); - - uint auction = liquidationEngine.liquidateSAFE("gold", safeHandler); - assertEq(auction, 0); - - (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("gold", safeHandler); - assertEq(lockedCollateral * 3E27 * 100 / (generatedDebt * oracleRelayer.redemptionPrice()), cRatioSetter.defaultDesiredCollateralizationRatios("gold")); - } - - function setUp() public { - hevm = Hevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); - hevm.warp(604411200); - - safeEngine = new TestSAFEEngine(); - - goldFSM = new Feed(3.75 ether, true); - goldMedian = new Feed(3.75 ether, true); - goldFSM.updatePriceSource(address(goldMedian)); - - oracleRelayer = new OracleRelayer(address(safeEngine)); - oracleRelayer.modifyParameters("gold", "orcl", address(goldFSM)); - oracleRelayer.modifyParameters("gold", "safetyCRatio", ray(1.5 ether)); - oracleRelayer.modifyParameters("gold", "liquidationCRatio", ray(1.5 ether)); - safeEngine.addAuthorization(address(oracleRelayer)); - - accountingEngine = new TestAccountingEngine( - address(safeEngine), address(0x1), address(0x2) - ); - safeEngine.addAuthorization(address(accountingEngine)); - - taxCollector = new TaxCollector(address(safeEngine)); - taxCollector.initializeCollateralType("gold"); - taxCollector.modifyParameters("primaryTaxReceiver", address(accountingEngine)); - taxCollector.modifyParameters("gold", "stabilityFee", 1000000564701133626865910626); // 5% / day - safeEngine.addAuthorization(address(taxCollector)); - - liquidationEngine = new LiquidationEngine(address(safeEngine)); - liquidationEngine.modifyParameters("accountingEngine", address(accountingEngine)); - - safeEngine.addAuthorization(address(liquidationEngine)); - accountingEngine.addAuthorization(address(liquidationEngine)); - - gold = new DSToken("GEM", "GEM"); - gold.mint(1000 ether); - - safeEngine.initializeCollateralType("gold"); - collateralA = new BasicCollateralJoin(address(safeEngine), "gold", address(gold)); - safeEngine.addAuthorization(address(collateralA)); - - safeEngine.modifyParameters("gold", "safetyPrice", ray(1 ether)); - safeEngine.modifyParameters("gold", "debtCeiling", rad(1000 ether)); - safeEngine.modifyParameters("globalDebtCeiling", rad(1000 ether)); - - collateralAuctionHouse = new EnglishCollateralAuctionHouse(address(safeEngine), address(liquidationEngine), "gold"); - collateralAuctionHouse.addAuthorization(address(liquidationEngine)); - - liquidationEngine.addAuthorization(address(collateralAuctionHouse)); - liquidationEngine.modifyParameters("gold", "collateralAuctionHouse", address(collateralAuctionHouse)); - liquidationEngine.modifyParameters("gold", "liquidationPenalty", 1 ether); - - safeEngine.addAuthorization(address(collateralAuctionHouse)); - safeEngine.approveSAFEModification(address(collateralAuctionHouse)); - - safeManager = new GebSafeManager(address(safeEngine)); - oracleRelayer.updateCollateralPrice("gold"); - - saviourRegistry = new SAFESaviourRegistry(saveCooldown); - cRatioSetter = new SaviourCRatioSetter(address(oracleRelayer), address(safeManager)); - cRatioSetter.setDefaultCRatio("gold", defaultDesiredCollateralizationRatio); - - saviour = new GeneralTokenReserveSafeSaviour( - address(cRatioSetter), - address(collateralA), - address(liquidationEngine), - address(taxCollector), - address(oracleRelayer), - address(safeManager), - address(saviourRegistry), - keeperPayout, - minKeeperPayoutValue, - payoutToSAFESize - ); - saviourRegistry.toggleSaviour(address(saviour)); - liquidationEngine.connectSAFESaviour(address(saviour)); - - me = address(this); - alice = new FakeUser(); - } - - function test_deposit_as_owner() public { - assertEq(liquidationEngine.safeSaviours(address(saviour)), 1); - - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - gold.transfer(address(alice), 200 ether); - alice.doDeposit(saviour, gold, safe, 200 ether); - - assertEq(gold.balanceOf(address(saviour)), 200 ether); - assertEq(saviour.collateralTokenCover(safeHandler), 200 ether); - } - function test_deposit_as_random() public { - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - gold.approve(address(saviour), 500 ether); - saviour.deposit(safe, 500 ether); - - assertEq(gold.balanceOf(address(saviour)), 500 ether); - assertEq(saviour.collateralTokenCover(safeHandler), 500 ether); - } - function testFail_deposit_after_repaying_debt() public { - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - gold.approve(address(saviour), 500 ether); - saviour.deposit(safe, 250 ether); - - default_repay_all_debt(safe, safeHandler); - saviour.deposit(safe, 250 ether); - } - function testFail_deposit_when_no_debt() public { - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - - gold.approve(address(saviour), 500 ether); - saviour.deposit(safe, 500 ether); - } - function testFail_deposit_when_not_engine_approved() public { - liquidationEngine.disconnectSAFESaviour(address(saviour)); - - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - gold.approve(address(saviour), 500 ether); - saviour.deposit(safe, 250 ether); - } - function test_deposit_then_withdraw_as_owner() public { - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - gold.transfer(address(alice), 500 ether); - alice.doDeposit(saviour, gold, safe, 500 ether); - - alice.doWithdraw(saviour, safe, 100 ether, address(this)); - assertEq(gold.balanceOf(address(saviour)), 400 ether); - assertEq(saviour.collateralTokenCover(safeHandler), 400 ether); - - alice.doWithdraw(saviour, safe, 400 ether, address(this)); - assertEq(gold.balanceOf(address(saviour)), 0); - assertEq(saviour.collateralTokenCover(safeHandler), 0); - } - function test_withdraw_when_safe_has_no_debt() public { - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - gold.transfer(address(alice), 500 ether); - alice.doDeposit(saviour, gold, safe, 500 ether); - - default_repay_all_debt(safe, safeHandler); - alice.doWithdraw(saviour, safe, 500 ether, address(this)); - assertEq(gold.balanceOf(address(saviour)), 0); - assertEq(saviour.collateralTokenCover(safeHandler), 0); - } - function test_deposit_then_withdraw_as_approved() public { - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - gold.transfer(address(alice), 500 ether); - alice.doDeposit(saviour, gold, safe, 500 ether); - - assertEq(gold.balanceOf(address(this)), 400 ether); - - alice.doSafeAllow(safeManager, safe, address(this), 1); - saviour.withdraw(safe, 250 ether, address(this)); - - assertEq(gold.balanceOf(address(this)), 650 ether); - assertEq(gold.balanceOf(address(saviour)), 250 ether); - assertEq(saviour.collateralTokenCover(safeHandler), 250 ether); - } - function testFail_deposit_then_withdraw_as_non_approved() public { - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - gold.transfer(address(alice), 500 ether); - alice.doDeposit(saviour, gold, safe, 500 ether); - - assertEq(gold.balanceOf(address(this)), 400 ether); - saviour.withdraw(safe, 250 ether, address(this)); - } - function test_set_desired_cratio_by_owner() public { - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - - alice.doSetDesiredCollateralizationRatio(saviour, cRatioSetter, "gold", safe, 151); - assertEq(cRatioSetter.desiredCollateralizationRatios("gold", safeHandler), 151); - } - function test_set_desired_cratio_by_approved() public { - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - alice.doSafeAllow(safeManager, safe, address(this), 1); - cRatioSetter.setDesiredCollateralizationRatio("gold", safe, 151); - assertEq(cRatioSetter.desiredCollateralizationRatios("gold", safeHandler), 151); - } - function testFail_set_desired_cratio_by_unauthed() public { - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - cRatioSetter.setDesiredCollateralizationRatio("gold", safe, 151); - } - function testFail_set_desired_cratio_above_max_limit() public { - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - alice.doSafeAllow(safeManager, safe, address(this), 1); - cRatioSetter.setDesiredCollateralizationRatio("gold", safe, cRatioSetter.MAX_CRATIO() + 1); - } - function test_liquidate_no_saviour_set() public { - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - - default_modify_collateralization(safe, safeHandler); - default_liquidate_safe(safeHandler); - } - function test_add_remove_saviour_from_manager() public { - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - - default_modify_collateralization(safe, safeHandler); - - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(0)); - assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(0)); - } - function test_liquidate_add_saviour_with_no_cover() public { - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - - default_modify_collateralization(safe, safeHandler); - - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(saviour, cRatioSetter, "gold", safe, 200); - - assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); - assertTrue(!saviour.canSave("gold", safeHandler)); - assertTrue(saviour.keeperPayoutExceedsMinValue()); - assertEq(saviour.tokenAmountUsedToSave("gold", safeHandler), 13333333333333333333); - - default_liquidate_safe(safeHandler); - } - function test_liquidate_cover_only_for_keeper_payout() public { - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - - default_modify_collateralization(safe, safeHandler); - - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(saviour, cRatioSetter, "gold", safe, 200); - - assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); - assertTrue(saviour.keeperPayoutExceedsMinValue()); - assertEq(saviour.getKeeperPayoutValue(), 1.875 ether); - - gold.transfer(address(alice), saviour.keeperPayout()); - alice.doDeposit(saviour, gold, safe, saviour.keeperPayout()); - - assertTrue(!saviour.canSave("gold", safeHandler)); - default_liquidate_safe(safeHandler); - } - function test_liquidate_cover_only_no_keeper_payout() public { - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - - default_modify_collateralization(safe, safeHandler); - - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(saviour, cRatioSetter, "gold", safe, 200); - assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); - - gold.transfer(address(alice), saviour.tokenAmountUsedToSave("gold", safeHandler)); - alice.doDeposit(saviour, gold, safe, saviour.tokenAmountUsedToSave("gold", safeHandler)); - - assertTrue(!saviour.canSave("gold", safeHandler)); - default_liquidate_safe(safeHandler); - } - function test_liquidate_keeper_payout_value_small() public { - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - - default_modify_collateralization(safe, safeHandler); - - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(saviour, cRatioSetter, "gold", safe, 200); - assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); - - goldMedian.updateCollateralPrice(0.02 ether - 1); - goldFSM.updateCollateralPrice(0.02 ether - 1); - - assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); - assertEq(saviour.getKeeperPayoutValue(), 9999999999999999); - - gold.mint(address(alice), saviour.tokenAmountUsedToSave("gold", safeHandler) + saviour.keeperPayout()); - alice.doDeposit(saviour, gold, safe, saviour.tokenAmountUsedToSave("gold", safeHandler) + saviour.keeperPayout()); - - assertTrue(!saviour.keeperPayoutExceedsMinValue()); - assertTrue(saviour.canSave("gold", safeHandler)); - - // Liquidate with the current 0.02 ether - 1 price - oracleRelayer.updateCollateralPrice("gold"); - - liquidationEngine.modifyParameters("gold", "liquidationQuantity", rad(111 ether)); - liquidationEngine.modifyParameters("gold", "liquidationPenalty", 1.1 ether); - - uint auction = liquidationEngine.liquidateSAFE("gold", safeHandler); - // the full SAFE is liquidated - (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("gold", me); - assertEq(lockedCollateral, 0); - assertEq(generatedDebt, 0); - // all debt goes to the accounting engine - assertEq(accountingEngine.totalQueuedDebt(), rad(100 ether)); - // auction is for all collateral - (,uint amountToSell,,,,,, uint256 amountToRaise) = collateralAuctionHouse.bids(auction); - assertEq(amountToSell, 40 ether); - assertEq(amountToRaise, rad(110 ether)); - } - function test_successfully_save_small_cratio() public { - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - default_save(safe, safeHandler, 200); - } - function test_saveSAFE_accumulated_rate() public { - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - - // Warp and save - hevm.warp(now + 5 days); - taxCollector.taxSingle("gold"); - - gold.approve(address(collateralA)); - collateralA.join(address(safeHandler), 100 ether); - alice.doModifySAFECollateralization(safeManager, safe, 80 ether, 60 ether); - - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(saviour, cRatioSetter, "gold", safe, 200); - assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); - - goldMedian.updateCollateralPrice(1 ether); - goldFSM.updateCollateralPrice(1 ether); - oracleRelayer.updateCollateralPrice("gold"); - - gold.mint(address(alice), 1000000 ether); - alice.doDeposit(saviour, gold, safe, 1000000 ether); - - assertTrue(saviour.keeperPayoutExceedsMinValue()); - assertTrue(saviour.canSave("gold", safeHandler)); - - liquidationEngine.modifyParameters("gold", "liquidationQuantity", rad(111 ether)); - liquidationEngine.modifyParameters("gold", "liquidationPenalty", 1.1 ether); - - uint256 preSaveKeeperBalance = gold.balanceOf(address(this)); - uint auction = liquidationEngine.liquidateSAFE("gold", safeHandler); - assertEq(auction, 0); - assertEq(gold.balanceOf(address(this)) - preSaveKeeperBalance, saviour.keeperPayout()); - - (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("gold", safeHandler); - (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("gold"); - assertEq(lockedCollateral * 1E27 * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10**27), 200); - } - function test_successfully_save_max_cratio() public { - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - default_save(safe, safeHandler, cRatioSetter.MAX_CRATIO()); - } - function test_successfully_save_default_cratio() public { - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - default_save(safe, safeHandler); - } - function test_liquidate_twice_in_row_same_saviour() public { - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - default_save(safe, safeHandler, 155); - - // Add collateral and try to save again - goldMedian.updateCollateralPrice(2 ether); - goldFSM.updateCollateralPrice(2 ether); - oracleRelayer.updateCollateralPrice("gold"); - - gold.mint(address(alice), saviour.tokenAmountUsedToSave("gold", safeHandler) + saviour.keeperPayout()); - alice.doDeposit(saviour, gold, safe, saviour.tokenAmountUsedToSave("gold", safeHandler) + saviour.keeperPayout()); - - assertTrue(saviour.keeperPayoutExceedsMinValue()); - assertTrue(saviour.canSave("gold", safeHandler)); - - // Can't save because the SAFE saviour registry break time hasn't elapsed - uint auction = liquidationEngine.liquidateSAFE("gold", safeHandler); - assertEq(auction, 1); - } - function test_liquidate_twice_in_row_different_saviours() public { - // Create a new saviour and set it up - GeneralTokenReserveSafeSaviour secondSaviour = new GeneralTokenReserveSafeSaviour( - address(cRatioSetter), - address(collateralA), - address(liquidationEngine), - address(taxCollector), - address(oracleRelayer), - address(safeManager), - address(saviourRegistry), - keeperPayout, - minKeeperPayoutValue, - payoutToSAFESize - ); - saviourRegistry.toggleSaviour(address(secondSaviour)); - liquidationEngine.connectSAFESaviour(address(secondSaviour)); - - // Save the safe with the original saviour first - uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); - address safeHandler = safeManager.safes(safe); - default_save(safe, safeHandler, 155); - - // Try to save with the second saviour afterwards - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(secondSaviour)); - assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(secondSaviour)); - - goldMedian.updateCollateralPrice(1.5 ether); - goldFSM.updateCollateralPrice(1.5 ether); - oracleRelayer.updateCollateralPrice("gold"); - - gold.mint(address(alice), secondSaviour.tokenAmountUsedToSave("gold", safeHandler) + secondSaviour.keeperPayout()); - alice.doDeposit(secondSaviour, gold, safe, secondSaviour.tokenAmountUsedToSave("gold", safeHandler) + secondSaviour.keeperPayout()); - - assertTrue(secondSaviour.keeperPayoutExceedsMinValue()); - assertTrue(secondSaviour.canSave("gold", safeHandler)); - - // Can't save because the SAFE saviour registry break time hasn't elapsed - uint auction = liquidationEngine.liquidateSAFE("gold", safeHandler); - assertEq(auction, 1); - } -} +pragma solidity ^0.6.7; +pragma experimental ABIEncoderV2; + +import "ds-test/test.sol"; +import "ds-token/token.sol"; + +import {SAFEEngine} from 'geb/SAFEEngine.sol'; +import {LiquidationEngine} from 'geb/LiquidationEngine.sol'; +import {AccountingEngine} from 'geb/AccountingEngine.sol'; +import {TaxCollector} from 'geb/TaxCollector.sol'; +import 'geb/BasicTokenAdapters.sol'; +import {OracleRelayer} from 'geb/OracleRelayer.sol'; +import {EnglishCollateralAuctionHouse} from 'geb/CollateralAuctionHouse.sol'; +import {GebSafeManager} from "geb-safe-manager/GebSafeManager.sol"; + +import {SaviourCRatioSetter} from "../SaviourCRatioSetter.sol"; +import {SAFESaviourRegistry} from "../SAFESaviourRegistry.sol"; +import {GeneralTokenReserveSafeSaviour} from "../saviours/GeneralTokenReserveSafeSaviour.sol"; + +abstract contract Hevm { + function warp(uint256) virtual public; +} +contract Feed { + bytes32 public price; + bool public validPrice; + uint public lastUpdateTime; + address public priceSource; + + constructor(uint256 price_, bool validPrice_) public { + price = bytes32(price_); + validPrice = validPrice_; + lastUpdateTime = now; + } + function updatePriceSource(address priceSource_) external { + priceSource = priceSource_; + } + function updateCollateralPrice(uint256 price_) external { + price = bytes32(price_); + lastUpdateTime = now; + } + function getResultWithValidity() external view returns (bytes32, bool) { + return (price, validPrice); + } +} +contract TestSAFEEngine is SAFEEngine { + uint256 constant RAY = 10 ** 27; + + constructor() public {} + + function mint(address usr, uint wad) public { + coinBalance[usr] += wad * RAY; + globalDebt += wad * RAY; + } + function balanceOf(address usr) public view returns (uint) { + return uint(coinBalance[usr] / RAY); + } +} +contract TestAccountingEngine is AccountingEngine { + constructor(address safeEngine, address surplusAuctionHouse, address debtAuctionHouse) + public AccountingEngine(safeEngine, surplusAuctionHouse, debtAuctionHouse) {} + + function totalDeficit() public view returns (uint) { + return safeEngine.debtBalance(address(this)); + } + function totalSurplus() public view returns (uint) { + return safeEngine.coinBalance(address(this)); + } + function preAuctionDebt() public view returns (uint) { + return subtract(subtract(totalDeficit(), totalQueuedDebt), totalOnAuctionDebt); + } +} +contract FakeUser { + function doOpenSafe( + GebSafeManager manager, + bytes32 collateralType, + address usr + ) public returns (uint256) { + return manager.openSAFE(collateralType, usr); + } + + function doSafeAllow( + GebSafeManager manager, + uint safe, + address usr, + uint ok + ) public { + manager.allowSAFE(safe, usr, ok); + } + + function doHandlerAllow( + GebSafeManager manager, + address usr, + uint ok + ) public { + manager.allowHandler(usr, ok); + } + + function doTransferSAFEOwnership( + GebSafeManager manager, + uint safe, + address dst + ) public { + manager.transferSAFEOwnership(safe, dst); + } + + function doModifySAFECollateralization( + GebSafeManager manager, + uint safe, + int deltaCollateral, + int deltaDebt + ) public { + manager.modifySAFECollateralization(safe, deltaCollateral, deltaDebt); + } + + function doApproveSAFEModification( + SAFEEngine safeEngine, + address usr + ) public { + safeEngine.approveSAFEModification(usr); + } + + function doSAFEEngineModifySAFECollateralization( + SAFEEngine safeEngine, + bytes32 collateralType, + address safe, + address collateralSource, + address debtDst, + int deltaCollateral, + int deltaDebt + ) public { + safeEngine.modifySAFECollateralization(collateralType, safe, collateralSource, debtDst, deltaCollateral, deltaDebt); + } + + function doProtectSAFE( + GebSafeManager manager, + uint safe, + address liquidationEngine, + address saviour + ) public { + manager.protectSAFE(safe, liquidationEngine, saviour); + } + + function doDeposit( + GeneralTokenReserveSafeSaviour saviour, + DSToken collateral, + uint safe, + uint amount + ) public { + collateral.approve(address(saviour), amount); + saviour.deposit(safe, amount); + } + + function doWithdraw( + GeneralTokenReserveSafeSaviour saviour, + uint safe, + uint amount, + address dst + ) public { + saviour.withdraw(safe, amount, dst); + } + + function doSetDesiredCollateralizationRatio( + GeneralTokenReserveSafeSaviour saviour, + SaviourCRatioSetter cRatioSetter, + bytes32 collateralType, + uint safe, + uint cRatio + ) public { + cRatioSetter.setDesiredCollateralizationRatio(collateralType, safe, cRatio); + } +} + +contract GeneralTokenReserveSafeSaviourTest is DSTest { + Hevm hevm; + + TestSAFEEngine safeEngine; + TestAccountingEngine accountingEngine; + LiquidationEngine liquidationEngine; + OracleRelayer oracleRelayer; + TaxCollector taxCollector; + + BasicCollateralJoin collateralA; + EnglishCollateralAuctionHouse collateralAuctionHouse; + + GebSafeManager safeManager; + + Feed goldFSM; + Feed goldMedian; + + DSToken gold; + + GeneralTokenReserveSafeSaviour saviour; + SaviourCRatioSetter cRatioSetter; + SAFESaviourRegistry saviourRegistry; + + FakeUser alice; + address me; + + // Saviour parameters + uint256 saveCooldown = 1 days; + uint256 keeperPayout = 0.5 ether; + uint256 minKeeperPayoutValue = 0.01 ether; + uint256 payoutToSAFESize = 40; + uint256 defaultDesiredCollateralizationRatio = 300; + + function ray(uint wad) internal pure returns (uint) { + return wad * 10 ** 9; + } + function rad(uint wad) internal pure returns (uint) { + return wad * 10 ** 27; + } + + // Default actions/scenarios + function default_modify_collateralization(uint256 safe, address safeHandler) internal { + gold.approve(address(collateralA)); + collateralA.join(address(safeHandler), 100 ether); + alice.doModifySAFECollateralization(safeManager, safe, 40 ether, 100 ether); + } + function default_liquidate_safe(address safeHandler) internal { + goldMedian.updateCollateralPrice(3 ether); + goldFSM.updateCollateralPrice(3 ether); + oracleRelayer.updateCollateralPrice("gold"); + + liquidationEngine.modifyParameters("gold", "liquidationQuantity", rad(111 ether)); + liquidationEngine.modifyParameters("gold", "liquidationPenalty", 1.1 ether); + + uint auction = liquidationEngine.liquidateSAFE("gold", safeHandler); + // the full SAFE is liquidated + (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("gold", me); + assertEq(lockedCollateral, 0); + assertEq(generatedDebt, 0); + // all debt goes to the accounting engine + assertEq(accountingEngine.totalQueuedDebt(), rad(100 ether)); + // auction is for all collateral + (,uint amountToSell,,,,,, uint256 amountToRaise) = collateralAuctionHouse.bids(auction); + assertEq(amountToSell, 40 ether); + assertEq(amountToRaise, rad(110 ether)); + } + function default_repay_all_debt(uint256 safe, address safeHandler) internal { + alice.doModifySAFECollateralization(safeManager, safe, 0, -100 ether); + } + function default_save(uint256 safe, address safeHandler, uint desiredCRatio) internal { + default_modify_collateralization(safe, safeHandler); + + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(saviour, cRatioSetter, "gold", safe, desiredCRatio); + assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); + + goldMedian.updateCollateralPrice(3 ether); + goldFSM.updateCollateralPrice(3 ether); + oracleRelayer.updateCollateralPrice("gold"); + + gold.mint(address(alice), saviour.tokenAmountUsedToSave("gold", safeHandler) + saviour.keeperPayout()); + alice.doDeposit(saviour, gold, safe, saviour.tokenAmountUsedToSave("gold", safeHandler) + saviour.keeperPayout()); + + assertTrue(saviour.keeperPayoutExceedsMinValue()); + assertTrue(saviour.canSave("gold", safeHandler)); + + liquidationEngine.modifyParameters("gold", "liquidationQuantity", rad(111 ether)); + liquidationEngine.modifyParameters("gold", "liquidationPenalty", 1.1 ether); + + uint256 preSaveKeeperBalance = gold.balanceOf(address(this)); + uint auction = liquidationEngine.liquidateSAFE("gold", safeHandler); + assertEq(auction, 0); + assertEq(gold.balanceOf(address(this)) - preSaveKeeperBalance, saviour.keeperPayout()); + + (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("gold", safeHandler); + assertEq(lockedCollateral * 3E27 * 100 / (generatedDebt * oracleRelayer.redemptionPrice()), desiredCRatio); + } + function default_save(uint256 safe, address safeHandler) internal { + default_modify_collateralization(safe, safeHandler); + + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); + + goldMedian.updateCollateralPrice(3 ether); + goldFSM.updateCollateralPrice(3 ether); + oracleRelayer.updateCollateralPrice("gold"); + + gold.mint(address(alice), saviour.tokenAmountUsedToSave("gold", safeHandler) + saviour.keeperPayout()); + alice.doDeposit(saviour, gold, safe, saviour.tokenAmountUsedToSave("gold", safeHandler) + saviour.keeperPayout()); + + assertTrue(saviour.keeperPayoutExceedsMinValue()); + assertTrue(saviour.canSave("gold", safeHandler)); + + liquidationEngine.modifyParameters("gold", "liquidationQuantity", rad(111 ether)); + liquidationEngine.modifyParameters("gold", "liquidationPenalty", 1.1 ether); + + uint auction = liquidationEngine.liquidateSAFE("gold", safeHandler); + assertEq(auction, 0); + + (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("gold", safeHandler); + assertEq(lockedCollateral * 3E27 * 100 / (generatedDebt * oracleRelayer.redemptionPrice()), cRatioSetter.defaultDesiredCollateralizationRatios("gold")); + } + + function setUp() public { + hevm = Hevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + hevm.warp(604411200); + + safeEngine = new TestSAFEEngine(); + + goldFSM = new Feed(3.75 ether, true); + goldMedian = new Feed(3.75 ether, true); + goldFSM.updatePriceSource(address(goldMedian)); + + oracleRelayer = new OracleRelayer(address(safeEngine)); + oracleRelayer.modifyParameters("gold", "orcl", address(goldFSM)); + oracleRelayer.modifyParameters("gold", "safetyCRatio", ray(1.5 ether)); + oracleRelayer.modifyParameters("gold", "liquidationCRatio", ray(1.5 ether)); + safeEngine.addAuthorization(address(oracleRelayer)); + + accountingEngine = new TestAccountingEngine( + address(safeEngine), address(0x1), address(0x2) + ); + safeEngine.addAuthorization(address(accountingEngine)); + + taxCollector = new TaxCollector(address(safeEngine)); + taxCollector.initializeCollateralType("gold"); + taxCollector.modifyParameters("primaryTaxReceiver", address(accountingEngine)); + taxCollector.modifyParameters("gold", "stabilityFee", 1000000564701133626865910626); // 5% / day + safeEngine.addAuthorization(address(taxCollector)); + + liquidationEngine = new LiquidationEngine(address(safeEngine)); + liquidationEngine.modifyParameters("accountingEngine", address(accountingEngine)); + + safeEngine.addAuthorization(address(liquidationEngine)); + accountingEngine.addAuthorization(address(liquidationEngine)); + + gold = new DSToken("GEM", "GEM"); + gold.mint(1000 ether); + + safeEngine.initializeCollateralType("gold"); + collateralA = new BasicCollateralJoin(address(safeEngine), "gold", address(gold)); + safeEngine.addAuthorization(address(collateralA)); + + safeEngine.modifyParameters("gold", "safetyPrice", ray(1 ether)); + safeEngine.modifyParameters("gold", "debtCeiling", rad(1000 ether)); + safeEngine.modifyParameters("globalDebtCeiling", rad(1000 ether)); + + collateralAuctionHouse = new EnglishCollateralAuctionHouse(address(safeEngine), address(liquidationEngine), "gold"); + collateralAuctionHouse.addAuthorization(address(liquidationEngine)); + + liquidationEngine.addAuthorization(address(collateralAuctionHouse)); + liquidationEngine.modifyParameters("gold", "collateralAuctionHouse", address(collateralAuctionHouse)); + liquidationEngine.modifyParameters("gold", "liquidationPenalty", 1 ether); + + safeEngine.addAuthorization(address(collateralAuctionHouse)); + safeEngine.approveSAFEModification(address(collateralAuctionHouse)); + + safeManager = new GebSafeManager(address(safeEngine)); + oracleRelayer.updateCollateralPrice("gold"); + + saviourRegistry = new SAFESaviourRegistry(saveCooldown); + cRatioSetter = new SaviourCRatioSetter(address(oracleRelayer), address(safeManager)); + cRatioSetter.setDefaultCRatio("gold", defaultDesiredCollateralizationRatio); + + saviour = new GeneralTokenReserveSafeSaviour( + address(cRatioSetter), + address(collateralA), + address(liquidationEngine), + address(taxCollector), + address(oracleRelayer), + address(safeManager), + address(saviourRegistry), + keeperPayout, + minKeeperPayoutValue, + payoutToSAFESize + ); + saviourRegistry.toggleSaviour(address(saviour)); + liquidationEngine.connectSAFESaviour(address(saviour)); + + me = address(this); + alice = new FakeUser(); + } + + function test_deposit_as_owner() public { + assertEq(liquidationEngine.safeSaviours(address(saviour)), 1); + + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + gold.transfer(address(alice), 200 ether); + alice.doDeposit(saviour, gold, safe, 200 ether); + + assertEq(gold.balanceOf(address(saviour)), 200 ether); + assertEq(saviour.collateralTokenCover(safeHandler), 200 ether); + } + function test_deposit_as_random() public { + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + gold.approve(address(saviour), 500 ether); + saviour.deposit(safe, 500 ether); + + assertEq(gold.balanceOf(address(saviour)), 500 ether); + assertEq(saviour.collateralTokenCover(safeHandler), 500 ether); + } + function testFail_deposit_after_repaying_debt() public { + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + gold.approve(address(saviour), 500 ether); + saviour.deposit(safe, 250 ether); + + default_repay_all_debt(safe, safeHandler); + saviour.deposit(safe, 250 ether); + } + function testFail_deposit_when_no_debt() public { + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + + gold.approve(address(saviour), 500 ether); + saviour.deposit(safe, 500 ether); + } + function testFail_deposit_when_not_engine_approved() public { + liquidationEngine.disconnectSAFESaviour(address(saviour)); + + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + gold.approve(address(saviour), 500 ether); + saviour.deposit(safe, 250 ether); + } + function test_deposit_then_withdraw_as_owner() public { + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + gold.transfer(address(alice), 500 ether); + alice.doDeposit(saviour, gold, safe, 500 ether); + + alice.doWithdraw(saviour, safe, 100 ether, address(this)); + assertEq(gold.balanceOf(address(saviour)), 400 ether); + assertEq(saviour.collateralTokenCover(safeHandler), 400 ether); + + alice.doWithdraw(saviour, safe, 400 ether, address(this)); + assertEq(gold.balanceOf(address(saviour)), 0); + assertEq(saviour.collateralTokenCover(safeHandler), 0); + } + function test_withdraw_when_safe_has_no_debt() public { + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + gold.transfer(address(alice), 500 ether); + alice.doDeposit(saviour, gold, safe, 500 ether); + + default_repay_all_debt(safe, safeHandler); + alice.doWithdraw(saviour, safe, 500 ether, address(this)); + assertEq(gold.balanceOf(address(saviour)), 0); + assertEq(saviour.collateralTokenCover(safeHandler), 0); + } + function test_deposit_then_withdraw_as_approved() public { + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + gold.transfer(address(alice), 500 ether); + alice.doDeposit(saviour, gold, safe, 500 ether); + + assertEq(gold.balanceOf(address(this)), 400 ether); + + alice.doSafeAllow(safeManager, safe, address(this), 1); + saviour.withdraw(safe, 250 ether, address(this)); + + assertEq(gold.balanceOf(address(this)), 650 ether); + assertEq(gold.balanceOf(address(saviour)), 250 ether); + assertEq(saviour.collateralTokenCover(safeHandler), 250 ether); + } + function testFail_deposit_then_withdraw_as_non_approved() public { + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + gold.transfer(address(alice), 500 ether); + alice.doDeposit(saviour, gold, safe, 500 ether); + + assertEq(gold.balanceOf(address(this)), 400 ether); + saviour.withdraw(safe, 250 ether, address(this)); + } + function test_set_desired_cratio_by_owner() public { + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + + alice.doSetDesiredCollateralizationRatio(saviour, cRatioSetter, "gold", safe, 151); + assertEq(cRatioSetter.desiredCollateralizationRatios("gold", safeHandler), 151); + } + function test_set_desired_cratio_by_approved() public { + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + alice.doSafeAllow(safeManager, safe, address(this), 1); + cRatioSetter.setDesiredCollateralizationRatio("gold", safe, 151); + assertEq(cRatioSetter.desiredCollateralizationRatios("gold", safeHandler), 151); + } + function testFail_set_desired_cratio_by_unauthed() public { + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + cRatioSetter.setDesiredCollateralizationRatio("gold", safe, 151); + } + function testFail_set_desired_cratio_above_max_limit() public { + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + alice.doSafeAllow(safeManager, safe, address(this), 1); + cRatioSetter.setDesiredCollateralizationRatio("gold", safe, cRatioSetter.MAX_CRATIO() + 1); + } + function test_liquidate_no_saviour_set() public { + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + + default_modify_collateralization(safe, safeHandler); + default_liquidate_safe(safeHandler); + } + function test_add_remove_saviour_from_manager() public { + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + + default_modify_collateralization(safe, safeHandler); + + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(0)); + assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(0)); + } + function test_liquidate_add_saviour_with_no_cover() public { + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + + default_modify_collateralization(safe, safeHandler); + + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(saviour, cRatioSetter, "gold", safe, 200); + + assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); + assertTrue(!saviour.canSave("gold", safeHandler)); + assertTrue(saviour.keeperPayoutExceedsMinValue()); + assertEq(saviour.tokenAmountUsedToSave("gold", safeHandler), 13333333333333333333); + + default_liquidate_safe(safeHandler); + } + function test_liquidate_cover_only_for_keeper_payout() public { + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + + default_modify_collateralization(safe, safeHandler); + + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(saviour, cRatioSetter, "gold", safe, 200); + + assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); + assertTrue(saviour.keeperPayoutExceedsMinValue()); + assertEq(saviour.getKeeperPayoutValue(), 1.875 ether); + + gold.transfer(address(alice), saviour.keeperPayout()); + alice.doDeposit(saviour, gold, safe, saviour.keeperPayout()); + + assertTrue(!saviour.canSave("gold", safeHandler)); + default_liquidate_safe(safeHandler); + } + function test_liquidate_cover_only_no_keeper_payout() public { + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + + default_modify_collateralization(safe, safeHandler); + + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(saviour, cRatioSetter, "gold", safe, 200); + assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); + + gold.transfer(address(alice), saviour.tokenAmountUsedToSave("gold", safeHandler)); + alice.doDeposit(saviour, gold, safe, saviour.tokenAmountUsedToSave("gold", safeHandler)); + + assertTrue(!saviour.canSave("gold", safeHandler)); + default_liquidate_safe(safeHandler); + } + function test_liquidate_keeper_payout_value_small() public { + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + + default_modify_collateralization(safe, safeHandler); + + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(saviour, cRatioSetter, "gold", safe, 200); + assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); + + goldMedian.updateCollateralPrice(0.02 ether - 1); + goldFSM.updateCollateralPrice(0.02 ether - 1); + + assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); + assertEq(saviour.getKeeperPayoutValue(), 9999999999999999); + + gold.mint(address(alice), saviour.tokenAmountUsedToSave("gold", safeHandler) + saviour.keeperPayout()); + alice.doDeposit(saviour, gold, safe, saviour.tokenAmountUsedToSave("gold", safeHandler) + saviour.keeperPayout()); + + assertTrue(!saviour.keeperPayoutExceedsMinValue()); + assertTrue(saviour.canSave("gold", safeHandler)); + + // Liquidate with the current 0.02 ether - 1 price + oracleRelayer.updateCollateralPrice("gold"); + + liquidationEngine.modifyParameters("gold", "liquidationQuantity", rad(111 ether)); + liquidationEngine.modifyParameters("gold", "liquidationPenalty", 1.1 ether); + + uint auction = liquidationEngine.liquidateSAFE("gold", safeHandler); + // the full SAFE is liquidated + (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("gold", me); + assertEq(lockedCollateral, 0); + assertEq(generatedDebt, 0); + // all debt goes to the accounting engine + assertEq(accountingEngine.totalQueuedDebt(), rad(100 ether)); + // auction is for all collateral + (,uint amountToSell,,,,,, uint256 amountToRaise) = collateralAuctionHouse.bids(auction); + assertEq(amountToSell, 40 ether); + assertEq(amountToRaise, rad(110 ether)); + } + function test_successfully_save_small_cratio() public { + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + default_save(safe, safeHandler, 200); + } + function test_saveSAFE_accumulated_rate() public { + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + + // Warp and save + hevm.warp(now + 5 days); + taxCollector.taxSingle("gold"); + + gold.approve(address(collateralA)); + collateralA.join(address(safeHandler), 100 ether); + alice.doModifySAFECollateralization(safeManager, safe, 80 ether, 60 ether); + + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(saviour, cRatioSetter, "gold", safe, 200); + assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(saviour)); + + goldMedian.updateCollateralPrice(1 ether); + goldFSM.updateCollateralPrice(1 ether); + oracleRelayer.updateCollateralPrice("gold"); + + gold.mint(address(alice), 1000000 ether); + alice.doDeposit(saviour, gold, safe, 1000000 ether); + + assertTrue(saviour.keeperPayoutExceedsMinValue()); + assertTrue(saviour.canSave("gold", safeHandler)); + + liquidationEngine.modifyParameters("gold", "liquidationQuantity", rad(111 ether)); + liquidationEngine.modifyParameters("gold", "liquidationPenalty", 1.1 ether); + + uint256 preSaveKeeperBalance = gold.balanceOf(address(this)); + uint auction = liquidationEngine.liquidateSAFE("gold", safeHandler); + assertEq(auction, 0); + assertEq(gold.balanceOf(address(this)) - preSaveKeeperBalance, saviour.keeperPayout()); + + (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("gold", safeHandler); + (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("gold"); + assertEq(lockedCollateral * 1E27 * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10**27), 200); + } + function test_successfully_save_max_cratio() public { + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + default_save(safe, safeHandler, cRatioSetter.MAX_CRATIO()); + } + function test_successfully_save_default_cratio() public { + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + default_save(safe, safeHandler); + } + function test_liquidate_twice_in_row_same_saviour() public { + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + default_save(safe, safeHandler, 155); + + // Add collateral and try to save again + goldMedian.updateCollateralPrice(2 ether); + goldFSM.updateCollateralPrice(2 ether); + oracleRelayer.updateCollateralPrice("gold"); + + gold.mint(address(alice), saviour.tokenAmountUsedToSave("gold", safeHandler) + saviour.keeperPayout()); + alice.doDeposit(saviour, gold, safe, saviour.tokenAmountUsedToSave("gold", safeHandler) + saviour.keeperPayout()); + + assertTrue(saviour.keeperPayoutExceedsMinValue()); + assertTrue(saviour.canSave("gold", safeHandler)); + + // Can't save because the SAFE saviour registry break time hasn't elapsed + uint auction = liquidationEngine.liquidateSAFE("gold", safeHandler); + assertEq(auction, 1); + } + function test_liquidate_twice_in_row_different_saviours() public { + // Create a new saviour and set it up + GeneralTokenReserveSafeSaviour secondSaviour = new GeneralTokenReserveSafeSaviour( + address(cRatioSetter), + address(collateralA), + address(liquidationEngine), + address(taxCollector), + address(oracleRelayer), + address(safeManager), + address(saviourRegistry), + keeperPayout, + minKeeperPayoutValue, + payoutToSAFESize + ); + saviourRegistry.toggleSaviour(address(secondSaviour)); + liquidationEngine.connectSAFESaviour(address(secondSaviour)); + + // Save the safe with the original saviour first + uint safe = alice.doOpenSafe(safeManager, "gold", address(alice)); + address safeHandler = safeManager.safes(safe); + default_save(safe, safeHandler, 155); + + // Try to save with the second saviour afterwards + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(secondSaviour)); + assertEq(liquidationEngine.chosenSAFESaviour("gold", safeHandler), address(secondSaviour)); + + goldMedian.updateCollateralPrice(1.5 ether); + goldFSM.updateCollateralPrice(1.5 ether); + oracleRelayer.updateCollateralPrice("gold"); + + gold.mint(address(alice), secondSaviour.tokenAmountUsedToSave("gold", safeHandler) + secondSaviour.keeperPayout()); + alice.doDeposit(secondSaviour, gold, safe, secondSaviour.tokenAmountUsedToSave("gold", safeHandler) + secondSaviour.keeperPayout()); + + assertTrue(secondSaviour.keeperPayoutExceedsMinValue()); + assertTrue(secondSaviour.canSave("gold", safeHandler)); + + // Can't save because the SAFE saviour registry break time hasn't elapsed + uint auction = liquidationEngine.liquidateSAFE("gold", safeHandler); + assertEq(auction, 1); + } +} diff --git a/src/test/NativeUnderlyingUniswapV2CustomCRatioSafeSaviour.t.sol b/src/test/NativeUnderlyingUniswapV2CustomCRatioSafeSaviour.t.sol index cba6b89..58d7ae6 100644 --- a/src/test/NativeUnderlyingUniswapV2CustomCRatioSafeSaviour.t.sol +++ b/src/test/NativeUnderlyingUniswapV2CustomCRatioSafeSaviour.t.sol @@ -1,1222 +1,1222 @@ -pragma solidity 0.6.7; - -import "ds-test/test.sol"; -import "ds-weth/weth9.sol"; -import "ds-token/token.sol"; - -import {SAFEEngine} from 'geb/SAFEEngine.sol'; -import {Coin} from 'geb/Coin.sol'; -import {LiquidationEngine} from 'geb/LiquidationEngine.sol'; -import {AccountingEngine} from 'geb/AccountingEngine.sol'; -import {TaxCollector} from 'geb/TaxCollector.sol'; -import {BasicCollateralJoin, CoinJoin} from 'geb/BasicTokenAdapters.sol'; -import {OracleRelayer} from 'geb/OracleRelayer.sol'; -import {EnglishCollateralAuctionHouse} from 'geb/CollateralAuctionHouse.sol'; -import {GebSafeManager} from "geb-safe-manager/GebSafeManager.sol"; -import {SAFESaviourRegistry} from "../SAFESaviourRegistry.sol"; - -import "../integrations/uniswap/uni-v2/UniswapV2Factory.sol"; -import "../integrations/uniswap/uni-v2/UniswapV2Pair.sol"; -import "../integrations/uniswap/uni-v2/UniswapV2Router02.sol"; - -import "../integrations/uniswap/liquidity-managers/UniswapV2LiquidityManager.sol"; - -import "../saviours/NativeUnderlyingUniswapV2CustomCRatioSafeSaviour.sol"; - -abstract contract Hevm { - function warp(uint256) virtual public; -} - -contract TestSAFEEngine is SAFEEngine { - uint256 constant RAY = 10 ** 27; - - constructor() public {} - - function mint(address usr, uint wad) public { - coinBalance[usr] += wad * RAY; - globalDebt += wad * RAY; - } - function balanceOf(address usr) public view returns (uint) { - return uint(coinBalance[usr] / RAY); - } -} - -// --- Median Contracts --- -contract MockMedianizer { - uint256 public price; - bool public validPrice; - uint public lastUpdateTime; - address public priceSource; - - constructor(uint256 price_, bool validPrice_) public { - price = price_; - validPrice = validPrice_; - lastUpdateTime = now; - } - function updatePriceSource(address priceSource_) external { - priceSource = priceSource_; - } - function changeValidity() external { - validPrice = !validPrice; - } - function updateCollateralPrice(uint256 price_) external { - price = price_; - lastUpdateTime = now; - } - function read() external view returns (uint256) { - return price; - } - function getResultWithValidity() external view returns (uint256, bool) { - return (price, validPrice); - } -} - -// Users -contract FakeUser { - function doModifyParameters( - NativeUnderlyingUniswapV2CustomCRatioSafeSaviour saviour, - bytes32 parameter, - uint256 data - ) public { - saviour.modifyParameters(parameter, data); - } - - function doModifyParameters( - NativeUnderlyingUniswapV2CustomCRatioSafeSaviour saviour, - bytes32 parameter, - address data - ) public { - saviour.modifyParameters(parameter, data); - } - - function doOpenSafe( - GebSafeManager manager, - bytes32 collateralType, - address usr - ) public returns (uint256) { - return manager.openSAFE(collateralType, usr); - } - - function doSafeAllow( - GebSafeManager manager, - uint safe, - address usr, - uint ok - ) public { - manager.allowSAFE(safe, usr, ok); - } - - function doHandlerAllow( - GebSafeManager manager, - address usr, - uint ok - ) public { - manager.allowHandler(usr, ok); - } - - function doTransferSAFEOwnership( - GebSafeManager manager, - uint safe, - address dst - ) public { - manager.transferSAFEOwnership(safe, dst); - } - - function doModifySAFECollateralization( - GebSafeManager manager, - uint safe, - int deltaCollateral, - int deltaDebt - ) public { - manager.modifySAFECollateralization(safe, deltaCollateral, deltaDebt); - } - - function doApproveSAFEModification( - SAFEEngine safeEngine, - address usr - ) public { - safeEngine.approveSAFEModification(usr); - } - - function doSAFEEngineModifySAFECollateralization( - SAFEEngine safeEngine, - bytes32 collateralType, - address safe, - address collateralSource, - address debtDst, - int deltaCollateral, - int deltaDebt - ) public { - safeEngine.modifySAFECollateralization(collateralType, safe, collateralSource, debtDst, deltaCollateral, deltaDebt); - } - - function doDeposit( - NativeUnderlyingUniswapV2CustomCRatioSafeSaviour saviour, - DSToken lpToken, - uint256 safeID, - uint256 tokenAmount - ) public { - lpToken.approve(address(saviour), tokenAmount); - saviour.deposit(safeID, tokenAmount); - } - - function doWithdraw( - NativeUnderlyingUniswapV2CustomCRatioSafeSaviour saviour, - uint256 safeID, - uint256 lpTokenAmount, - address dst - ) public { - saviour.withdraw(safeID, lpTokenAmount, dst); - } - - function doTransferInternalCoins( - GebSafeManager manager, - uint256 safe, - address dst, - uint256 amt - ) public { - manager.transferInternalCoins(safe, dst, amt); - } - - function doGetReserves( - NativeUnderlyingUniswapV2CustomCRatioSafeSaviour saviour, - uint256 safeID, - address dst - ) public { - saviour.getReserves(safeID, dst); - } - - function doSetCRatioThreshold( - NativeUnderlyingUniswapV2CustomCRatioSafeSaviour saviour, - uint safeID, - uint cRatio - ) public { - saviour.setCRatioThreshold(safeID, cRatio); - } -} - -contract NativeUnderlyingUniswapV2CustomCRatioSafeSaviourTest is DSTest { - Hevm hevm; - - UniswapV2Factory uniswapFactory; - UniswapV2Router02 uniswapRouter; - UniswapV2LiquidityManager liquidityManager; - - UniswapV2Pair raiWETHPair; - - Coin systemCoin; - WETH9_ weth; - - TestSAFEEngine safeEngine; - AccountingEngine accountingEngine; - LiquidationEngine liquidationEngine; - OracleRelayer oracleRelayer; - TaxCollector taxCollector; - - BasicCollateralJoin collateralJoin; - - CoinJoin coinJoin; - CoinJoin systemCoinJoin; - - EnglishCollateralAuctionHouse collateralAuctionHouse; - - GebSafeManager safeManager; - - NativeUnderlyingUniswapV2CustomCRatioSafeSaviour saviour; - - MockMedianizer systemCoinOracle; - MockMedianizer ethFSM; - MockMedianizer ethMedian; - - FakeUser alice; - - address me; - - // Params - uint256 initTokenAmount = 100000 ether; - uint256 initETHUSDPrice = 250 * 10 ** 18; - uint256 initRAIUSDPrice = 4.242 * 10 ** 18; - - uint256 initETHRAIPairLiquidity = 5 ether; // 1250 USD - uint256 initRAIETHPairLiquidity = 294.672324375E18; // 1 RAI = 4.242 USD - - // Saviour params - bool isSystemCoinToken0; - uint256 saveCooldown = 1 days; - uint256 minKeeperPayoutValue = 1000 ether; - - // Core system params - uint256 minCRatio = 1.5 ether; - uint256 ethToMint = 5000 ether; - uint256 ethCeiling = uint(-1); - uint256 ethFloor = 10 ether; - uint256 ethLiquidationPenalty = 1 ether; - - uint256 defaultLiquidityMultiplier = 50; - uint256 defaultCollateralAmount = 40 ether; - uint256 defaultTokenAmount = 100 ether; - - function setUp() public { - hevm = Hevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); - hevm.warp(604411200); - - // System coin - systemCoin = new Coin("RAI", "RAI", 1); - systemCoin.mint(address(this), initTokenAmount); - systemCoinOracle = new MockMedianizer(initRAIUSDPrice, true); - - // Core system - safeEngine = new TestSAFEEngine(); - safeEngine.initializeCollateralType("eth"); - safeEngine.mint(address(this), rad(initTokenAmount)); - - ethFSM = new MockMedianizer(initETHUSDPrice, true); - ethMedian = new MockMedianizer(initETHUSDPrice, true); - ethFSM.updatePriceSource(address(ethMedian)); - - oracleRelayer = new OracleRelayer(address(safeEngine)); - oracleRelayer.modifyParameters("redemptionPrice", ray(initRAIUSDPrice)); - oracleRelayer.modifyParameters("eth", "orcl", address(ethFSM)); - oracleRelayer.modifyParameters("eth", "safetyCRatio", ray(minCRatio)); - oracleRelayer.modifyParameters("eth", "liquidationCRatio", ray(minCRatio)); - - safeEngine.addAuthorization(address(oracleRelayer)); - oracleRelayer.updateCollateralPrice("eth"); - - accountingEngine = new AccountingEngine( - address(safeEngine), address(0x1), address(0x2) - ); - safeEngine.addAuthorization(address(accountingEngine)); - - taxCollector = new TaxCollector(address(safeEngine)); - taxCollector.initializeCollateralType("eth"); - taxCollector.modifyParameters("primaryTaxReceiver", address(accountingEngine)); - taxCollector.modifyParameters("eth", "stabilityFee", 1000000564701133626865910626); // 5% / day - safeEngine.addAuthorization(address(taxCollector)); - - liquidationEngine = new LiquidationEngine(address(safeEngine)); - liquidationEngine.modifyParameters("accountingEngine", address(accountingEngine)); - - safeEngine.addAuthorization(address(liquidationEngine)); - accountingEngine.addAuthorization(address(liquidationEngine)); - - weth = new WETH9_(); - weth.deposit{value: initTokenAmount}(); - - collateralJoin = new BasicCollateralJoin(address(safeEngine), "eth", address(weth)); - - coinJoin = new CoinJoin(address(safeEngine), address(systemCoin)); - systemCoin.addAuthorization(address(coinJoin)); - safeEngine.transferInternalCoins(address(this), address(coinJoin), safeEngine.coinBalance(address(this))); - - safeEngine.addAuthorization(address(collateralJoin)); - - safeEngine.modifyParameters("eth", "debtCeiling", rad(ethCeiling)); - safeEngine.modifyParameters("globalDebtCeiling", rad(ethCeiling)); - safeEngine.modifyParameters("eth", "debtFloor", rad(ethFloor)); - - collateralAuctionHouse = new EnglishCollateralAuctionHouse(address(safeEngine), address(liquidationEngine), "eth"); - collateralAuctionHouse.addAuthorization(address(liquidationEngine)); - - liquidationEngine.addAuthorization(address(collateralAuctionHouse)); - liquidationEngine.modifyParameters("eth", "collateralAuctionHouse", address(collateralAuctionHouse)); - liquidationEngine.modifyParameters("eth", "liquidationPenalty", ethLiquidationPenalty); - - safeEngine.addAuthorization(address(collateralAuctionHouse)); - safeEngine.approveSAFEModification(address(collateralAuctionHouse)); - - safeManager = new GebSafeManager(address(safeEngine)); - oracleRelayer.updateCollateralPrice("eth"); - - // Uniswap setup - uniswapFactory = new UniswapV2Factory(address(this)); - createUniswapPair(); - uniswapRouter = new UniswapV2Router02(address(uniswapFactory), address(weth)); - addPairLiquidityRouter(address(systemCoin), address(weth), initRAIETHPairLiquidity, initETHRAIPairLiquidity); - - // Liquidity manager - liquidityManager = new UniswapV2LiquidityManager(address(raiWETHPair), address(uniswapRouter)); - - // Saviour infra - saviour = new NativeUnderlyingUniswapV2CustomCRatioSafeSaviour( - isSystemCoinToken0, - address(coinJoin), - address(collateralJoin), - address(systemCoinOracle), - address(liquidationEngine), - address(taxCollector), - address(oracleRelayer), - address(safeManager), - address(liquidityManager), - address(raiWETHPair), - minKeeperPayoutValue - ); - - me = address(this); - alice = new FakeUser(); - } - - // --- Math --- - function ray(uint wad) internal pure returns (uint) { - return wad * 10 ** 9; - } - function rad(uint wad) internal pure returns (uint) { - return wad * 10 ** 27; - } - function sub(uint256 x, uint256 y) internal pure returns (uint256 z) { - require((z = x - y) <= x); - } - - // --- Uniswap utils --- - function createUniswapPair() internal { - // Setup WETH/RAI pair - uniswapFactory.createPair(address(weth), address(systemCoin)); - raiWETHPair = UniswapV2Pair(uniswapFactory.getPair(address(weth), address(systemCoin))); - - if (address(raiWETHPair.token0()) == address(systemCoin)) isSystemCoinToken0 = true; - } - function addPairLiquidityRouter(address token1, address token2, uint256 amount1, uint256 amount2) internal { - DSToken(token1).approve(address(uniswapRouter), uint(-1)); - DSToken(token2).approve(address(uniswapRouter), uint(-1)); - uniswapRouter.addLiquidity(token1, token2, amount1, amount2, amount1, amount2, address(this), now); - UniswapV2Pair updatedPair = UniswapV2Pair(uniswapFactory.getPair(token1, token2)); - updatedPair.sync(); - } - function addPairLiquidityTransfer(UniswapV2Pair pair, address token1, address token2, uint256 amount1, uint256 amount2) internal { - DSToken(token1).transfer(address(pair), amount1); - DSToken(token2).transfer(address(pair), amount2); - pair.sync(); - } - - // --- Default actions/scenarios --- - function default_create_liquidatable_position(uint256 desiredCRatio, uint256 liquidatableCollateralPrice) internal returns (address) { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - ethMedian.updateCollateralPrice(liquidatableCollateralPrice); - ethFSM.updateCollateralPrice(liquidatableCollateralPrice); - oracleRelayer.updateCollateralPrice("eth"); - - return safeHandler; - } - function default_save(uint256 safe, address safeHandler, uint desiredCRatio) internal { - default_modify_collateralization(safe, safeHandler); - - alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); - alice.doSetCRatioThreshold(saviour, safe, desiredCRatio); - - ethMedian.updateCollateralPrice(initETHUSDPrice / 30); - ethFSM.updateCollateralPrice(initETHUSDPrice / 30); - oracleRelayer.updateCollateralPrice("eth"); - - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); - assertEq(raiWETHPair.balanceOf(address(saviour)), lpTokenAmount); - assertTrue(saviour.canSave("eth", safeHandler)); - - uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); - uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); - - (uint sysCoinsFromLP, ) = saviour.getLPUnderlying(safeHandler); - - saviour.saveSAFE(address(this), "eth", safeHandler); - assertTrue(saviour.underlyingReserves(safeHandler) > 0); - - - assertTrue( - systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance > 0 || - weth.balanceOf(address(this)) - preSaveWETHKeeperBalance > 0 - ); - assertTrue(raiWETHPair.balanceOf(address(saviour)) < lpTokenAmount); - assertEq(raiWETHPair.balanceOf(address(liquidityManager)), 0); - assertEq(saviour.lpTokenCover(safeHandler), 0); - } - function default_second_save(uint256 safe, address safeHandler, uint desiredCRatio) internal { - alice.doModifySAFECollateralization(safeManager, safe, int(0), int(defaultTokenAmount * 2)); - alice.doSetCRatioThreshold(saviour, safe, desiredCRatio); - - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); - assertEq(raiWETHPair.balanceOf(address(saviour)), lpTokenAmount); - assertTrue(saviour.canSave("eth", safeHandler)); - - uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); - uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); - - (uint sysCoinsFromLP, ) = saviour.getLPUnderlying(safeHandler); - saviour.saveSAFE(address(this), "eth", safeHandler); - assertTrue(saviour.underlyingReserves(safeHandler) > 0); - } - function default_liquidate_safe(address safeHandler) internal { - liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(100000 ether)); - liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); - - uint auction = liquidationEngine.liquidateSAFE("eth", safeHandler); - // the full SAFE is liquidated - (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", me); - assertEq(lockedCollateral, 0); - assertEq(generatedDebt, 0); - // all debt goes to the accounting engine - assertTrue(accountingEngine.totalQueuedDebt() > 0); - // auction is for all collateral - (,uint amountToSell,,,,,, uint256 amountToRaise) = collateralAuctionHouse.bids(auction); - assertEq(amountToSell, defaultCollateralAmount); - assertEq(amountToRaise, rad(1100 ether)); - } - function default_create_liquidatable_position_deposit_cover(uint256 desiredCRatio, uint256 liquidatableCollateralPrice) - internal returns (address) { - // Create position - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - // Change oracle price - ethMedian.updateCollateralPrice(liquidatableCollateralPrice); - ethFSM.updateCollateralPrice(liquidatableCollateralPrice); - oracleRelayer.updateCollateralPrice("eth"); - - // Deposit cover - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); - assertEq(raiWETHPair.balanceOf(address(saviour)), lpTokenAmount); - assertEq(saviour.lpTokenCover(safeHandler), lpTokenAmount); - - return safeHandler; - } - function default_create_position_deposit_cover() internal returns (uint, address) { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - // Deposit cover - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); - assertEq(raiWETHPair.balanceOf(address(saviour)), lpTokenAmount); - assertEq(saviour.lpTokenCover(safeHandler), lpTokenAmount); - - return (safe, safeHandler); - } - function default_modify_collateralization(uint256 safe, address safeHandler) internal { - weth.approve(address(collateralJoin), uint(-1)); - collateralJoin.join(address(safeHandler), defaultTokenAmount); - alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount) * 10); - } - - // --- Tests --- - function test_setup() public { - assertEq(saviour.authorizedAccounts(address(this)), 1); - assertTrue(saviour.isSystemCoinToken0() == isSystemCoinToken0); - assertEq(saviour.minKeeperPayoutValue(), minKeeperPayoutValue); - assertEq(saviour.restrictUsage(), 0); - - assertEq(address(saviour.coinJoin()), address(coinJoin)); - assertEq(address(saviour.collateralJoin()), address(collateralJoin)); - assertEq(address(saviour.liquidationEngine()), address(liquidationEngine)); - assertEq(address(saviour.oracleRelayer()), address(oracleRelayer)); - assertEq(address(saviour.systemCoinOrcl()), address(systemCoinOracle)); - assertEq(address(saviour.systemCoin()), address(systemCoin)); - assertEq(address(saviour.safeEngine()), address(safeEngine)); - assertEq(address(saviour.safeManager()), address(safeManager)); - assertEq(address(saviour.liquidityManager()), address(liquidityManager)); - assertEq(address(saviour.lpToken()), address(raiWETHPair)); - assertEq(address(saviour.collateralToken()), address(weth)); - } - function test_modify_uints() public { - saviour.modifyParameters("minKeeperPayoutValue", 5); - saviour.modifyParameters("restrictUsage", 1); - - assertEq(saviour.minKeeperPayoutValue(), 5); - assertEq(saviour.restrictUsage(), 1); - } - function testFail_modify_uint_unauthed() public { - alice.doModifyParameters(saviour, "minKeeperPayoutValue", 5); - } - function test_modify_addresses() public { - oracleRelayer = new OracleRelayer(address(safeEngine)); - systemCoinOracle = new MockMedianizer(initRAIUSDPrice, true); - saviour.modifyParameters("systemCoinOrcl", address(systemCoinOracle)); - saviour.modifyParameters("oracleRelayer", address(oracleRelayer)); - saviour.modifyParameters("liquidityManager", address(0xa)); - saviour.modifyParameters("liquidationEngine", address(0xb)); - saviour.modifyParameters("taxCollector", address(0xc)); - - assertEq(address(saviour.liquidityManager()), address(0xa)); - assertEq(address(saviour.oracleRelayer()), address(oracleRelayer)); - assertEq(address(saviour.systemCoinOrcl()), address(systemCoinOracle)); - assertEq(address(saviour.liquidationEngine()), address(0xb)); - assertEq(address(saviour.taxCollector()), address(0xc)); - } - function testFail_modify_address_unauthed() public { - alice.doModifyParameters(saviour, "systemCoinOrcl", address(systemCoinOracle)); - } - function testFail_deposit_null_lp_token_amount() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), 1, 0); - } - function testFail_deposit_inexistent_safe() public { - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), 1, lpTokenAmount); - } - function test_deposit_twice() public { - uint256 initialLPSupply = raiWETHPair.totalSupply(); - - (uint safe, address safeHandler) = default_create_position_deposit_cover(); - - // Second deposit - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); - - // Checks - assertTrue(raiWETHPair.balanceOf(address(saviour)) > 0 && saviour.lpTokenCover(safeHandler) > 0); - assertEq(saviour.lpTokenCover(safeHandler), raiWETHPair.totalSupply() - initialLPSupply); - assertEq(raiWETHPair.balanceOf(address(saviour)), raiWETHPair.totalSupply() - initialLPSupply); - } - function test_deposit_after_everything_withdrawn() public { - (uint safe, address safeHandler) = default_create_position_deposit_cover(); - - // Withdraw - uint256 currentLPBalanceAlice = raiWETHPair.balanceOf(address(alice)); - uint256 currentLPBalanceSaviour = raiWETHPair.balanceOf(address(saviour)); - alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler), address(alice)); - - // Checks - assertEq(raiWETHPair.balanceOf(address(alice)), currentLPBalanceAlice + currentLPBalanceSaviour); - assertTrue(raiWETHPair.balanceOf(address(saviour)) == 0 && saviour.lpTokenCover(safeHandler) == 0); - - // Deposit again - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, currentLPBalanceSaviour); - - // Checks - assertTrue(raiWETHPair.balanceOf(address(saviour)) > 0 && saviour.lpTokenCover(safeHandler) > 0); - assertEq(saviour.lpTokenCover(safeHandler), currentLPBalanceSaviour); - assertEq(raiWETHPair.balanceOf(address(saviour)), currentLPBalanceSaviour); - assertEq(raiWETHPair.balanceOf(address(alice)), currentLPBalanceAlice); - } - function testFail_withdraw_unauthorized() public { - (uint safe, ) = default_create_position_deposit_cover(); - - // Withdraw by unauthed - FakeUser bob = new FakeUser(); - bob.doWithdraw(saviour, safe, raiWETHPair.balanceOf(address(saviour)), address(bob)); - } - function testFail_withdraw_more_than_deposited() public { - (uint safe, address safeHandler) = default_create_position_deposit_cover(); - uint256 currentLPBalance = raiWETHPair.balanceOf(address(this)); - alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler) + 1, address(this)); - } - function testFail_withdraw_null() public { - (uint safe, address safeHandler) = default_create_position_deposit_cover(); - alice.doWithdraw(saviour, safe, 0, address(this)); - } - function test_withdraw() public { - (uint safe, address safeHandler) = default_create_position_deposit_cover(); - - // Withdraw - uint256 currentLPBalanceAlice = raiWETHPair.balanceOf(address(alice)); - uint256 currentLPBalanceSaviour = raiWETHPair.balanceOf(address(saviour)); - alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler), address(alice)); - - // Checks - assertEq(raiWETHPair.balanceOf(address(alice)), currentLPBalanceAlice + currentLPBalanceSaviour); - assertTrue(raiWETHPair.balanceOf(address(saviour)) == 0 && saviour.lpTokenCover(safeHandler) == 0); - } - function test_withdraw_twice() public { - (uint safe, address safeHandler) = default_create_position_deposit_cover(); - - // Withdraw once - uint256 currentLPBalanceAlice = raiWETHPair.balanceOf(address(alice)); - uint256 currentLPBalanceSaviour = raiWETHPair.balanceOf(address(saviour)); - alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler) / 2, address(alice)); - - // Checks - assertEq(raiWETHPair.balanceOf(address(alice)), currentLPBalanceAlice + currentLPBalanceSaviour / 2); - assertTrue(raiWETHPair.balanceOf(address(saviour)) == currentLPBalanceSaviour / 2 && saviour.lpTokenCover(safeHandler) == currentLPBalanceSaviour / 2); - - // Withdraw again - alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler), address(alice)); - - // Checks - assertEq(raiWETHPair.balanceOf(address(alice)), currentLPBalanceAlice + currentLPBalanceSaviour); - assertTrue(raiWETHPair.balanceOf(address(saviour)) == 0 && saviour.lpTokenCover(safeHandler) == 0); - } - function test_withdraw_custom_dst() public { - (uint safe, address safeHandler) = default_create_position_deposit_cover(); - - // Withdraw - uint256 currentLPBalanceAlice = raiWETHPair.balanceOf(address(0xb1)); - uint256 currentLPBalanceSaviour = raiWETHPair.balanceOf(address(saviour)); - alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler), address(0xb1)); - - // Checks - assertEq(raiWETHPair.balanceOf(address(0xb1)), currentLPBalanceSaviour); - assertEq(raiWETHPair.balanceOf(address(alice)), currentLPBalanceAlice); - assertTrue(raiWETHPair.balanceOf(address(saviour)) == 0 && saviour.lpTokenCover(safeHandler) == 0); - } - function test_tokenAmountUsedToSave() public { - (uint safe, address safeHandler) = default_create_position_deposit_cover(); - - assertEq(saviour.lpTokenCover(safeHandler), saviour.tokenAmountUsedToSave("eth", safeHandler)); - } - function test_getCollateralPrice_zero_price() public { - ethFSM.updateCollateralPrice(0); - assertEq(saviour.getCollateralPrice(), 0); - } - function test_getCollateralPrice_invalid() public { - ethFSM.changeValidity(); - assertEq(saviour.getCollateralPrice(), 0); - } - function test_getCollateralPrice_null_fsm() public { - oracleRelayer.modifyParameters("eth", "orcl", address(0)); - assertEq(saviour.getCollateralPrice(), 0); - } - function test_getCollateralPrice() public { - assertEq(saviour.getCollateralPrice(), initETHUSDPrice); - } - function test_getSystemCoinMarketPrice_invalid() public { - systemCoinOracle.changeValidity(); - assertEq(saviour.getSystemCoinMarketPrice(), 0); - } - function test_getSystemCoinMarketPrice_null_price() public { - systemCoinOracle.updateCollateralPrice(0); - assertEq(saviour.getSystemCoinMarketPrice(), 0); - } - function test_getSystemCoinMarketPrice() public { - assertEq(saviour.getSystemCoinMarketPrice(), initRAIUSDPrice); - } - function test_getLPUnderlying_inexistent_handler() public { - (uint sysCoins, uint collateral) = saviour.getLPUnderlying(address(0x1)); - assertEq(sysCoins, collateral); - assertEq(sysCoins, 0); - } - function test_getLPUnderlying() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); - - (uint sysCoins, uint collateral) = saviour.getLPUnderlying(safeHandler); - assertEq(sysCoins, initRAIETHPairLiquidity * defaultLiquidityMultiplier); - assertEq(collateral, initETHRAIPairLiquidity * defaultLiquidityMultiplier); - } - function test_getKeeperPayoutTokens_null_collateral_price() public { - address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); - ethFSM.updateCollateralPrice(0); - - (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), 0, 0); - - assertEq(sysCoins, 0); - assertEq(collateral, 0); - } - function test_getKeeperPayoutTokens_null_sys_coin_price() public { - address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); - systemCoinOracle.updateCollateralPrice(0); - - (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), 0, 0); - - assertEq(sysCoins, 0); - assertEq(collateral, 0); - } - function test_getKeeperPayoutTokens_only_sys_coins_used() public { - address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); - (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), uint(-1), 0); - - assertEq(sysCoins, minKeeperPayoutValue * 10 ** 18 / systemCoinOracle.read()); - assertEq(collateral, 0); - } - function test_getKeeperPayoutTokens_only_collateral_used() public { - (, address safeHandler) = default_create_position_deposit_cover(); - (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), 0, uint(-1)); - - assertEq(sysCoins, 0); - assertEq(collateral, minKeeperPayoutValue * 10 ** 18 / ethFSM.read()); - } - function test_getKeeperPayoutTokens_both_tokens_used() public { - (, address safeHandler) = default_create_position_deposit_cover(); - (uint underlyingSysCoins, ) = saviour.getLPUnderlying(safeHandler); - - (uint sysCoins, uint collateral) = - saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), (minKeeperPayoutValue * 10 ** 18 / (systemCoinOracle.read() * 2)), uint(-1)); - - assertEq(sysCoins, (minKeeperPayoutValue * 10 ** 18 / (systemCoinOracle.read() * 2))); - assertEq(collateral, 2 ether); - } - function test_getKeeperPayoutTokens_not_enough_tokens_to_pay() public { - saviour.modifyParameters("minKeeperPayoutValue", 10000000 ether); - - (, address safeHandler) = default_create_position_deposit_cover(); - (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), 0, 0); - - assertEq(sysCoins, 0); - assertEq(collateral, 0); - } - function test_canSave_cannot_save_safe() public { - // Create position - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - - weth.approve(address(collateralJoin), uint(-1)); - collateralJoin.join(address(safeHandler), defaultTokenAmount); - alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 10)); - - // Change oracle price - ethMedian.updateCollateralPrice(initETHUSDPrice / 30); - ethFSM.updateCollateralPrice(initETHUSDPrice / 30); - oracleRelayer.updateCollateralPrice("eth"); - - // Deposit cover - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity / 5, initETHRAIPairLiquidity / 5 - ); - - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount / 10); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount / 10); - assertTrue(!saviour.canSave("eth", safeHandler)); - } - function test_canSave_cannot_pay_keeper() public { - saviour.modifyParameters("minKeeperPayoutValue", 10000000 ether); - address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); - assertTrue(!saviour.canSave("eth", safeHandler)); - } - function test_canSave_both_tokens_used() public { - // Create position - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - // Change oracle price - ethMedian.updateCollateralPrice(initETHUSDPrice / 3); - ethFSM.updateCollateralPrice(initETHUSDPrice / 3); - oracleRelayer.updateCollateralPrice("eth"); - - // Deposit cover - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity * 2, initETHRAIPairLiquidity * 2 - ); - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); - saviour.modifyParameters("minKeeperPayoutValue", 50 ether); - - assertTrue(saviour.canSave("eth", safeHandler)); - } - function test_canSave() public { - address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); - assertTrue(saviour.canSave("eth", safeHandler)); - } - function test_saveSAFE_no_cover() public { - address safeHandler = default_create_liquidatable_position(200, initETHUSDPrice / 30); - default_liquidate_safe(safeHandler); - } - function test_saveSAFE_cannot_save_safe() public { - // Create position - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - - weth.approve(address(collateralJoin), uint(-1)); - collateralJoin.join(address(safeHandler), defaultTokenAmount); - alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 10)); - - // Change oracle price - ethMedian.updateCollateralPrice(initETHUSDPrice / 30); - ethFSM.updateCollateralPrice(initETHUSDPrice / 30); - oracleRelayer.updateCollateralPrice("eth"); - - // Deposit cover - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity / 5, initETHRAIPairLiquidity / 5 - ); - - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount / 10); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount / 10); - - default_liquidate_safe(safeHandler); - } - function test_saveSAFE_cannot_pay_keeper() public { - address safeHandler = default_create_liquidatable_position(200, initETHUSDPrice / 30); - saviour.modifyParameters("minKeeperPayoutValue", 10000000 ether); - default_liquidate_safe(safeHandler); - } - function test_saveSAFE_below_desired_threshold() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - - default_save(safe, safeHandler, 200); - } - function testFail_saveSAFE_above_threshold() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - alice.doSetCRatioThreshold(saviour, safe, 150); - - alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); - - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); - - liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(100000 ether)); - liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); - - saviour.saveSAFE(address(this), "eth", safeHandler); - } - function test_saveSAFE_accumulate_rate() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - - // Warp and save - hevm.warp(now + 2 days); - taxCollector.taxSingle("eth"); - - weth.approve(address(collateralJoin), uint(-1)); - collateralJoin.join(address(safeHandler), defaultTokenAmount); - alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 5)); - - alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); - alice.doSetCRatioThreshold(saviour, safe, 150); - - ethMedian.updateCollateralPrice(initETHUSDPrice / 10); - ethFSM.updateCollateralPrice(initETHUSDPrice / 10); - oracleRelayer.updateCollateralPrice("eth"); - - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); - assertEq(raiWETHPair.balanceOf(address(saviour)), lpTokenAmount); - assertTrue(saviour.canSave("eth", safeHandler)); - - liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(100000 ether)); - liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); - - uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); - uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); - - saviour.saveSAFE(address(this), "eth", safeHandler); - - assertTrue(saviour.underlyingReserves(safeHandler) > 0); - assertTrue( - systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance > 0 || - weth.balanceOf(address(this)) - preSaveWETHKeeperBalance > 0 - ); - assertTrue(raiWETHPair.balanceOf(address(saviour)) < lpTokenAmount); - assertEq(raiWETHPair.balanceOf(address(liquidityManager)), 0); - assertEq(saviour.lpTokenCover(safeHandler), 0); - } - function test_saveSAFE_both_tokens() public { - // Create position - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - alice.doSetCRatioThreshold(saviour, safe, 150); - - // Change oracle price - ethMedian.updateCollateralPrice(initETHUSDPrice / 3); - ethFSM.updateCollateralPrice(initETHUSDPrice / 3); - oracleRelayer.updateCollateralPrice("eth"); - - // Deposit cover - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity * 2, initETHRAIPairLiquidity * 2 - ); - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); - saviour.modifyParameters("minKeeperPayoutValue", 50 ether); - - assertTrue(saviour.canSave("eth", safeHandler)); - - liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(100000 ether)); - liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); - - uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); - uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); - - saviour.saveSAFE(address(this), "eth", safeHandler); - - assertEq(saviour.underlyingReserves(safeHandler), 0); - assertTrue( - systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance > 0 || - weth.balanceOf(address(this)) - preSaveWETHKeeperBalance > 0 - ); - assertTrue(raiWETHPair.balanceOf(address(saviour)) < lpTokenAmount); - assertEq(raiWETHPair.balanceOf(address(liquidityManager)), 0); - assertEq(saviour.lpTokenCover(safeHandler), 0); - } - function test_saveSAFE_both_tokens_accumulate_rate() public { - // Create position - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - - // Warp, mint and save - hevm.warp(now + 1 days); - taxCollector.taxSingle("eth"); - - weth.approve(address(collateralJoin), uint(-1)); - collateralJoin.join(address(safeHandler), defaultTokenAmount); - alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 8)); - - alice.doSetCRatioThreshold(saviour, safe, 150); - - // Change oracle price - ethMedian.updateCollateralPrice(initETHUSDPrice / 4); - ethFSM.updateCollateralPrice(initETHUSDPrice / 4); - oracleRelayer.updateCollateralPrice("eth"); - - // Deposit cover - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity * 2, initETHRAIPairLiquidity * 2 - ); - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); - saviour.modifyParameters("minKeeperPayoutValue", 50 ether); - - assertTrue(saviour.canSave("eth", safeHandler)); - - liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(100000 ether)); - liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); - - uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); - uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); - - saviour.saveSAFE(address(this), "eth", safeHandler); - - assertTrue(saviour.underlyingReserves(safeHandler) == 0); - assertTrue( - systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance > 0 || - weth.balanceOf(address(this)) - preSaveWETHKeeperBalance > 0 - ); - assertTrue(raiWETHPair.balanceOf(address(saviour)) < lpTokenAmount); - assertEq(raiWETHPair.balanceOf(address(liquidityManager)), 0); - assertEq(saviour.lpTokenCover(safeHandler), 0); - } - function test_saveSAFE_twice() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_save(safe, safeHandler, 150); - - default_second_save(safe, safeHandler, 570); - } - function testFail_saveSAFE_withdraw_cover() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_save(safe, safeHandler, 155); - - alice.doWithdraw(saviour, safe, 1, address(this)); - } - function assertAlmostEq(uint a, uint b, uint p) internal { - a = a / p; - b = b / p; - assertTrue(a >= b-1 && a <= b+1); - } - function test_get_cRatio(uint seed) public { - uint cRatio = (seed % 5 * 10**27) + 1 * 10**22; // from .001% to 500.001% - - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - weth.approve(address(collateralJoin), uint(-1)); - - collateralJoin.join(address(safeHandler), defaultCollateralAmount); - alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 10)); - - uint collateralPrice = defaultTokenAmount * 10 * initRAIUSDPrice / 10**27 * cRatio / defaultCollateralAmount; - ethFSM.updateCollateralPrice(collateralPrice); - oracleRelayer.updateCollateralPrice("eth"); - - assertAlmostEq(saviour.getSafeCRatio(safeHandler), cRatio, 10**8); // Allowing for some precision loss, cRatio is RAY - (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", safeHandler); - (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("eth"); - uint256 safeCRatio = lockedCollateral * ray(ethFSM.read()) * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10 ** 27); - assertTrue(safeCRatio == cRatio / 10**25 || safeCRatio == (cRatio / 10**25) - 1); - } - function helper_ratio_bounds(uint value) internal returns (uint) { - return (value % 4 * 10**27) + 1 * 10**27; // from 100 to 500 (cRatioSetter will not allow setting a cRatio lower than the collateral cRatio) - } - function test_saveSAFE_fuzz(uint safeCRatio, uint thresholdCRatio) public { - safeCRatio = helper_ratio_bounds(safeCRatio); - thresholdCRatio = helper_ratio_bounds(thresholdCRatio) / 10**25; - - // creating a safe - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - weth.approve(address(collateralJoin), uint(-1)); - - collateralJoin.join(address(safeHandler), defaultCollateralAmount); - alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 10)); - - // pushing it to the set cRatio - uint collateralPrice = defaultTokenAmount * 10 * initRAIUSDPrice / 10**27 * safeCRatio / defaultCollateralAmount; - ethFSM.updateCollateralPrice(collateralPrice); - oracleRelayer.updateCollateralPrice("eth"); - - // adding cover - alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); - - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier - ); - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - raiWETHPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); - - // setting threshold and desired cRatios - alice.doSetCRatioThreshold(saviour, safe, thresholdCRatio); - - uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); - uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); - - // save SAFE - try saviour.saveSAFE(address(this), "eth", safeHandler) { - // success saving safe - assertTrue(safeCRatio / 10**25 <= thresholdCRatio); - - assertTrue( - systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance > 0 || - weth.balanceOf(address(this)) - preSaveWETHKeeperBalance > 0 - ); - assertTrue(raiWETHPair.balanceOf(address(saviour)) < lpTokenAmount); - assertEq(raiWETHPair.balanceOf(address(liquidityManager)), 0); - assertEq(saviour.lpTokenCover(safeHandler), 0); - - (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", safeHandler); - if (generatedDebt == 0) return; - (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("eth"); - uint256 cRatio = lockedCollateral * ray(ethFSM.read()) * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10 ** 27); - assertTrue(cRatio > (safeCRatio / 10**25) - 1); - } catch { - // failed saving safe - assertTrue(safeCRatio / 10**25 >= thresholdCRatio); - - assertTrue( - systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance == 0 && - weth.balanceOf(address(this)) - preSaveWETHKeeperBalance == 0 - ); - assertEq(raiWETHPair.balanceOf(address(saviour)), lpTokenAmount); - assertEq(raiWETHPair.balanceOf(address(liquidityManager)), 0); - assertEq(saviour.lpTokenCover(safeHandler), lpTokenAmount); - - (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", safeHandler); - (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("eth"); - uint256 cRatio = lockedCollateral * ray(ethFSM.read()) * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10 ** 27); - assertTrue(cRatio == (safeCRatio / 10**25) || cRatio == (safeCRatio / 10**25) - 1); - } - } - - function test_saveSAFE_get_reserves() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_save(safe, safeHandler, 155); - - uint256 oldSysCoinBalance = systemCoin.balanceOf(address(alice)); - - uint sysCoinReserve = saviour.underlyingReserves(safeHandler); - - alice.doGetReserves(saviour, safe, address(alice)); - assertTrue(systemCoin.balanceOf(address(alice)) - sysCoinReserve == oldSysCoinBalance); - - assertEq(systemCoin.balanceOf(address(saviour)), 0); - } - function test_saveSAFE_get_reserves_twice() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_save(safe, safeHandler, 155); - alice.doGetReserves(saviour, safe, address(alice)); - - default_second_save(safe, safeHandler, 570); - - uint256 oldSysCoinBalance = systemCoin.balanceOf(address(0x1)); - uint256 oldCollateralBalance = weth.balanceOf(address(0x1)); - - uint sysCoinReserve = saviour.underlyingReserves(safeHandler); - - alice.doGetReserves(saviour, safe, address(0x1)); - assertEq(systemCoin.balanceOf(address(0x1)) - sysCoinReserve, oldSysCoinBalance); - assertEq(systemCoin.balanceOf(address(saviour)), 0); - } - function testFail_getReserves_invalid_caller() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_save(safe, safeHandler, 155); - - saviour.getReserves(safe, address(alice)); - } -} +pragma solidity 0.6.7; + +import "ds-test/test.sol"; +import "ds-weth/weth9.sol"; +import "ds-token/token.sol"; + +import {SAFEEngine} from 'geb/SAFEEngine.sol'; +import {Coin} from 'geb/Coin.sol'; +import {LiquidationEngine} from 'geb/LiquidationEngine.sol'; +import {AccountingEngine} from 'geb/AccountingEngine.sol'; +import {TaxCollector} from 'geb/TaxCollector.sol'; +import {BasicCollateralJoin, CoinJoin} from 'geb/BasicTokenAdapters.sol'; +import {OracleRelayer} from 'geb/OracleRelayer.sol'; +import {EnglishCollateralAuctionHouse} from 'geb/CollateralAuctionHouse.sol'; +import {GebSafeManager} from "geb-safe-manager/GebSafeManager.sol"; +import {SAFESaviourRegistry} from "../SAFESaviourRegistry.sol"; + +import "../integrations/uniswap/uni-v2/UniswapV2Factory.sol"; +import "../integrations/uniswap/uni-v2/UniswapV2Pair.sol"; +import "../integrations/uniswap/uni-v2/UniswapV2Router02.sol"; + +import "../integrations/uniswap/liquidity-managers/UniswapV2LiquidityManager.sol"; + +import "../saviours/NativeUnderlyingUniswapV2CustomCRatioSafeSaviour.sol"; + +abstract contract Hevm { + function warp(uint256) virtual public; +} + +contract TestSAFEEngine is SAFEEngine { + uint256 constant RAY = 10 ** 27; + + constructor() public {} + + function mint(address usr, uint wad) public { + coinBalance[usr] += wad * RAY; + globalDebt += wad * RAY; + } + function balanceOf(address usr) public view returns (uint) { + return uint(coinBalance[usr] / RAY); + } +} + +// --- Median Contracts --- +contract MockMedianizer { + uint256 public price; + bool public validPrice; + uint public lastUpdateTime; + address public priceSource; + + constructor(uint256 price_, bool validPrice_) public { + price = price_; + validPrice = validPrice_; + lastUpdateTime = now; + } + function updatePriceSource(address priceSource_) external { + priceSource = priceSource_; + } + function changeValidity() external { + validPrice = !validPrice; + } + function updateCollateralPrice(uint256 price_) external { + price = price_; + lastUpdateTime = now; + } + function read() external view returns (uint256) { + return price; + } + function getResultWithValidity() external view returns (uint256, bool) { + return (price, validPrice); + } +} + +// Users +contract FakeUser { + function doModifyParameters( + NativeUnderlyingUniswapV2CustomCRatioSafeSaviour saviour, + bytes32 parameter, + uint256 data + ) public { + saviour.modifyParameters(parameter, data); + } + + function doModifyParameters( + NativeUnderlyingUniswapV2CustomCRatioSafeSaviour saviour, + bytes32 parameter, + address data + ) public { + saviour.modifyParameters(parameter, data); + } + + function doOpenSafe( + GebSafeManager manager, + bytes32 collateralType, + address usr + ) public returns (uint256) { + return manager.openSAFE(collateralType, usr); + } + + function doSafeAllow( + GebSafeManager manager, + uint safe, + address usr, + uint ok + ) public { + manager.allowSAFE(safe, usr, ok); + } + + function doHandlerAllow( + GebSafeManager manager, + address usr, + uint ok + ) public { + manager.allowHandler(usr, ok); + } + + function doTransferSAFEOwnership( + GebSafeManager manager, + uint safe, + address dst + ) public { + manager.transferSAFEOwnership(safe, dst); + } + + function doModifySAFECollateralization( + GebSafeManager manager, + uint safe, + int deltaCollateral, + int deltaDebt + ) public { + manager.modifySAFECollateralization(safe, deltaCollateral, deltaDebt); + } + + function doApproveSAFEModification( + SAFEEngine safeEngine, + address usr + ) public { + safeEngine.approveSAFEModification(usr); + } + + function doSAFEEngineModifySAFECollateralization( + SAFEEngine safeEngine, + bytes32 collateralType, + address safe, + address collateralSource, + address debtDst, + int deltaCollateral, + int deltaDebt + ) public { + safeEngine.modifySAFECollateralization(collateralType, safe, collateralSource, debtDst, deltaCollateral, deltaDebt); + } + + function doDeposit( + NativeUnderlyingUniswapV2CustomCRatioSafeSaviour saviour, + DSToken lpToken, + uint256 safeID, + uint256 tokenAmount + ) public { + lpToken.approve(address(saviour), tokenAmount); + saviour.deposit(safeID, tokenAmount); + } + + function doWithdraw( + NativeUnderlyingUniswapV2CustomCRatioSafeSaviour saviour, + uint256 safeID, + uint256 lpTokenAmount, + address dst + ) public { + saviour.withdraw(safeID, lpTokenAmount, dst); + } + + function doTransferInternalCoins( + GebSafeManager manager, + uint256 safe, + address dst, + uint256 amt + ) public { + manager.transferInternalCoins(safe, dst, amt); + } + + function doGetReserves( + NativeUnderlyingUniswapV2CustomCRatioSafeSaviour saviour, + uint256 safeID, + address dst + ) public { + saviour.getReserves(safeID, dst); + } + + function doSetCRatioThreshold( + NativeUnderlyingUniswapV2CustomCRatioSafeSaviour saviour, + uint safeID, + uint cRatio + ) public { + saviour.setCRatioThreshold(safeID, cRatio); + } +} + +contract NativeUnderlyingUniswapV2CustomCRatioSafeSaviourTest is DSTest { + Hevm hevm; + + UniswapV2Factory uniswapFactory; + UniswapV2Router02 uniswapRouter; + UniswapV2LiquidityManager liquidityManager; + + UniswapV2Pair raiWETHPair; + + Coin systemCoin; + WETH9_ weth; + + TestSAFEEngine safeEngine; + AccountingEngine accountingEngine; + LiquidationEngine liquidationEngine; + OracleRelayer oracleRelayer; + TaxCollector taxCollector; + + BasicCollateralJoin collateralJoin; + + CoinJoin coinJoin; + CoinJoin systemCoinJoin; + + EnglishCollateralAuctionHouse collateralAuctionHouse; + + GebSafeManager safeManager; + + NativeUnderlyingUniswapV2CustomCRatioSafeSaviour saviour; + + MockMedianizer systemCoinOracle; + MockMedianizer ethFSM; + MockMedianizer ethMedian; + + FakeUser alice; + + address me; + + // Params + uint256 initTokenAmount = 100000 ether; + uint256 initETHUSDPrice = 250 * 10 ** 18; + uint256 initRAIUSDPrice = 4.242 * 10 ** 18; + + uint256 initETHRAIPairLiquidity = 5 ether; // 1250 USD + uint256 initRAIETHPairLiquidity = 294.672324375E18; // 1 RAI = 4.242 USD + + // Saviour params + bool isSystemCoinToken0; + uint256 saveCooldown = 1 days; + uint256 minKeeperPayoutValue = 1000 ether; + + // Core system params + uint256 minCRatio = 1.5 ether; + uint256 ethToMint = 5000 ether; + uint256 ethCeiling = uint(-1); + uint256 ethFloor = 10 ether; + uint256 ethLiquidationPenalty = 1 ether; + + uint256 defaultLiquidityMultiplier = 50; + uint256 defaultCollateralAmount = 40 ether; + uint256 defaultTokenAmount = 100 ether; + + function setUp() public { + hevm = Hevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + hevm.warp(604411200); + + // System coin + systemCoin = new Coin("RAI", "RAI", 1); + systemCoin.mint(address(this), initTokenAmount); + systemCoinOracle = new MockMedianizer(initRAIUSDPrice, true); + + // Core system + safeEngine = new TestSAFEEngine(); + safeEngine.initializeCollateralType("eth"); + safeEngine.mint(address(this), rad(initTokenAmount)); + + ethFSM = new MockMedianizer(initETHUSDPrice, true); + ethMedian = new MockMedianizer(initETHUSDPrice, true); + ethFSM.updatePriceSource(address(ethMedian)); + + oracleRelayer = new OracleRelayer(address(safeEngine)); + oracleRelayer.modifyParameters("redemptionPrice", ray(initRAIUSDPrice)); + oracleRelayer.modifyParameters("eth", "orcl", address(ethFSM)); + oracleRelayer.modifyParameters("eth", "safetyCRatio", ray(minCRatio)); + oracleRelayer.modifyParameters("eth", "liquidationCRatio", ray(minCRatio)); + + safeEngine.addAuthorization(address(oracleRelayer)); + oracleRelayer.updateCollateralPrice("eth"); + + accountingEngine = new AccountingEngine( + address(safeEngine), address(0x1), address(0x2) + ); + safeEngine.addAuthorization(address(accountingEngine)); + + taxCollector = new TaxCollector(address(safeEngine)); + taxCollector.initializeCollateralType("eth"); + taxCollector.modifyParameters("primaryTaxReceiver", address(accountingEngine)); + taxCollector.modifyParameters("eth", "stabilityFee", 1000000564701133626865910626); // 5% / day + safeEngine.addAuthorization(address(taxCollector)); + + liquidationEngine = new LiquidationEngine(address(safeEngine)); + liquidationEngine.modifyParameters("accountingEngine", address(accountingEngine)); + + safeEngine.addAuthorization(address(liquidationEngine)); + accountingEngine.addAuthorization(address(liquidationEngine)); + + weth = new WETH9_(); + weth.deposit{value: initTokenAmount}(); + + collateralJoin = new BasicCollateralJoin(address(safeEngine), "eth", address(weth)); + + coinJoin = new CoinJoin(address(safeEngine), address(systemCoin)); + systemCoin.addAuthorization(address(coinJoin)); + safeEngine.transferInternalCoins(address(this), address(coinJoin), safeEngine.coinBalance(address(this))); + + safeEngine.addAuthorization(address(collateralJoin)); + + safeEngine.modifyParameters("eth", "debtCeiling", rad(ethCeiling)); + safeEngine.modifyParameters("globalDebtCeiling", rad(ethCeiling)); + safeEngine.modifyParameters("eth", "debtFloor", rad(ethFloor)); + + collateralAuctionHouse = new EnglishCollateralAuctionHouse(address(safeEngine), address(liquidationEngine), "eth"); + collateralAuctionHouse.addAuthorization(address(liquidationEngine)); + + liquidationEngine.addAuthorization(address(collateralAuctionHouse)); + liquidationEngine.modifyParameters("eth", "collateralAuctionHouse", address(collateralAuctionHouse)); + liquidationEngine.modifyParameters("eth", "liquidationPenalty", ethLiquidationPenalty); + + safeEngine.addAuthorization(address(collateralAuctionHouse)); + safeEngine.approveSAFEModification(address(collateralAuctionHouse)); + + safeManager = new GebSafeManager(address(safeEngine)); + oracleRelayer.updateCollateralPrice("eth"); + + // Uniswap setup + uniswapFactory = new UniswapV2Factory(address(this)); + createUniswapPair(); + uniswapRouter = new UniswapV2Router02(address(uniswapFactory), address(weth)); + addPairLiquidityRouter(address(systemCoin), address(weth), initRAIETHPairLiquidity, initETHRAIPairLiquidity); + + // Liquidity manager + liquidityManager = new UniswapV2LiquidityManager(address(raiWETHPair), address(uniswapRouter)); + + // Saviour infra + saviour = new NativeUnderlyingUniswapV2CustomCRatioSafeSaviour( + isSystemCoinToken0, + address(coinJoin), + address(collateralJoin), + address(systemCoinOracle), + address(liquidationEngine), + address(taxCollector), + address(oracleRelayer), + address(safeManager), + address(liquidityManager), + address(raiWETHPair), + minKeeperPayoutValue + ); + + me = address(this); + alice = new FakeUser(); + } + + // --- Math --- + function ray(uint wad) internal pure returns (uint) { + return wad * 10 ** 9; + } + function rad(uint wad) internal pure returns (uint) { + return wad * 10 ** 27; + } + function sub(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x - y) <= x); + } + + // --- Uniswap utils --- + function createUniswapPair() internal { + // Setup WETH/RAI pair + uniswapFactory.createPair(address(weth), address(systemCoin)); + raiWETHPair = UniswapV2Pair(uniswapFactory.getPair(address(weth), address(systemCoin))); + + if (address(raiWETHPair.token0()) == address(systemCoin)) isSystemCoinToken0 = true; + } + function addPairLiquidityRouter(address token1, address token2, uint256 amount1, uint256 amount2) internal { + DSToken(token1).approve(address(uniswapRouter), uint(-1)); + DSToken(token2).approve(address(uniswapRouter), uint(-1)); + uniswapRouter.addLiquidity(token1, token2, amount1, amount2, amount1, amount2, address(this), now); + UniswapV2Pair updatedPair = UniswapV2Pair(uniswapFactory.getPair(token1, token2)); + updatedPair.sync(); + } + function addPairLiquidityTransfer(UniswapV2Pair pair, address token1, address token2, uint256 amount1, uint256 amount2) internal { + DSToken(token1).transfer(address(pair), amount1); + DSToken(token2).transfer(address(pair), amount2); + pair.sync(); + } + + // --- Default actions/scenarios --- + function default_create_liquidatable_position(uint256 desiredCRatio, uint256 liquidatableCollateralPrice) internal returns (address) { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + ethMedian.updateCollateralPrice(liquidatableCollateralPrice); + ethFSM.updateCollateralPrice(liquidatableCollateralPrice); + oracleRelayer.updateCollateralPrice("eth"); + + return safeHandler; + } + function default_save(uint256 safe, address safeHandler, uint desiredCRatio) internal { + default_modify_collateralization(safe, safeHandler); + + alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); + alice.doSetCRatioThreshold(saviour, safe, desiredCRatio); + + ethMedian.updateCollateralPrice(initETHUSDPrice / 30); + ethFSM.updateCollateralPrice(initETHUSDPrice / 30); + oracleRelayer.updateCollateralPrice("eth"); + + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); + assertEq(raiWETHPair.balanceOf(address(saviour)), lpTokenAmount); + assertTrue(saviour.canSave("eth", safeHandler)); + + uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); + uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); + + (uint sysCoinsFromLP, ) = saviour.getLPUnderlying(safeHandler); + + saviour.saveSAFE(address(this), "eth", safeHandler); + assertTrue(saviour.underlyingReserves(safeHandler) > 0); + + + assertTrue( + systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance > 0 || + weth.balanceOf(address(this)) - preSaveWETHKeeperBalance > 0 + ); + assertTrue(raiWETHPair.balanceOf(address(saviour)) < lpTokenAmount); + assertEq(raiWETHPair.balanceOf(address(liquidityManager)), 0); + assertEq(saviour.lpTokenCover(safeHandler), 0); + } + function default_second_save(uint256 safe, address safeHandler, uint desiredCRatio) internal { + alice.doModifySAFECollateralization(safeManager, safe, int(0), int(defaultTokenAmount * 2)); + alice.doSetCRatioThreshold(saviour, safe, desiredCRatio); + + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); + assertEq(raiWETHPair.balanceOf(address(saviour)), lpTokenAmount); + assertTrue(saviour.canSave("eth", safeHandler)); + + uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); + uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); + + (uint sysCoinsFromLP, ) = saviour.getLPUnderlying(safeHandler); + saviour.saveSAFE(address(this), "eth", safeHandler); + assertTrue(saviour.underlyingReserves(safeHandler) > 0); + } + function default_liquidate_safe(address safeHandler) internal { + liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(100000 ether)); + liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); + + uint auction = liquidationEngine.liquidateSAFE("eth", safeHandler); + // the full SAFE is liquidated + (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", me); + assertEq(lockedCollateral, 0); + assertEq(generatedDebt, 0); + // all debt goes to the accounting engine + assertTrue(accountingEngine.totalQueuedDebt() > 0); + // auction is for all collateral + (,uint amountToSell,,,,,, uint256 amountToRaise) = collateralAuctionHouse.bids(auction); + assertEq(amountToSell, defaultCollateralAmount); + assertEq(amountToRaise, rad(1100 ether)); + } + function default_create_liquidatable_position_deposit_cover(uint256 desiredCRatio, uint256 liquidatableCollateralPrice) + internal returns (address) { + // Create position + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + // Change oracle price + ethMedian.updateCollateralPrice(liquidatableCollateralPrice); + ethFSM.updateCollateralPrice(liquidatableCollateralPrice); + oracleRelayer.updateCollateralPrice("eth"); + + // Deposit cover + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); + assertEq(raiWETHPair.balanceOf(address(saviour)), lpTokenAmount); + assertEq(saviour.lpTokenCover(safeHandler), lpTokenAmount); + + return safeHandler; + } + function default_create_position_deposit_cover() internal returns (uint, address) { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + // Deposit cover + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); + assertEq(raiWETHPair.balanceOf(address(saviour)), lpTokenAmount); + assertEq(saviour.lpTokenCover(safeHandler), lpTokenAmount); + + return (safe, safeHandler); + } + function default_modify_collateralization(uint256 safe, address safeHandler) internal { + weth.approve(address(collateralJoin), uint(-1)); + collateralJoin.join(address(safeHandler), defaultTokenAmount); + alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount) * 10); + } + + // --- Tests --- + function test_setup() public { + assertEq(saviour.authorizedAccounts(address(this)), 1); + assertTrue(saviour.isSystemCoinToken0() == isSystemCoinToken0); + assertEq(saviour.minKeeperPayoutValue(), minKeeperPayoutValue); + assertEq(saviour.restrictUsage(), 0); + + assertEq(address(saviour.coinJoin()), address(coinJoin)); + assertEq(address(saviour.collateralJoin()), address(collateralJoin)); + assertEq(address(saviour.liquidationEngine()), address(liquidationEngine)); + assertEq(address(saviour.oracleRelayer()), address(oracleRelayer)); + assertEq(address(saviour.systemCoinOrcl()), address(systemCoinOracle)); + assertEq(address(saviour.systemCoin()), address(systemCoin)); + assertEq(address(saviour.safeEngine()), address(safeEngine)); + assertEq(address(saviour.safeManager()), address(safeManager)); + assertEq(address(saviour.liquidityManager()), address(liquidityManager)); + assertEq(address(saviour.lpToken()), address(raiWETHPair)); + assertEq(address(saviour.collateralToken()), address(weth)); + } + function test_modify_uints() public { + saviour.modifyParameters("minKeeperPayoutValue", 5); + saviour.modifyParameters("restrictUsage", 1); + + assertEq(saviour.minKeeperPayoutValue(), 5); + assertEq(saviour.restrictUsage(), 1); + } + function testFail_modify_uint_unauthed() public { + alice.doModifyParameters(saviour, "minKeeperPayoutValue", 5); + } + function test_modify_addresses() public { + oracleRelayer = new OracleRelayer(address(safeEngine)); + systemCoinOracle = new MockMedianizer(initRAIUSDPrice, true); + saviour.modifyParameters("systemCoinOrcl", address(systemCoinOracle)); + saviour.modifyParameters("oracleRelayer", address(oracleRelayer)); + saviour.modifyParameters("liquidityManager", address(0xa)); + saviour.modifyParameters("liquidationEngine", address(0xb)); + saviour.modifyParameters("taxCollector", address(0xc)); + + assertEq(address(saviour.liquidityManager()), address(0xa)); + assertEq(address(saviour.oracleRelayer()), address(oracleRelayer)); + assertEq(address(saviour.systemCoinOrcl()), address(systemCoinOracle)); + assertEq(address(saviour.liquidationEngine()), address(0xb)); + assertEq(address(saviour.taxCollector()), address(0xc)); + } + function testFail_modify_address_unauthed() public { + alice.doModifyParameters(saviour, "systemCoinOrcl", address(systemCoinOracle)); + } + function testFail_deposit_null_lp_token_amount() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), 1, 0); + } + function testFail_deposit_inexistent_safe() public { + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), 1, lpTokenAmount); + } + function test_deposit_twice() public { + uint256 initialLPSupply = raiWETHPair.totalSupply(); + + (uint safe, address safeHandler) = default_create_position_deposit_cover(); + + // Second deposit + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); + + // Checks + assertTrue(raiWETHPair.balanceOf(address(saviour)) > 0 && saviour.lpTokenCover(safeHandler) > 0); + assertEq(saviour.lpTokenCover(safeHandler), raiWETHPair.totalSupply() - initialLPSupply); + assertEq(raiWETHPair.balanceOf(address(saviour)), raiWETHPair.totalSupply() - initialLPSupply); + } + function test_deposit_after_everything_withdrawn() public { + (uint safe, address safeHandler) = default_create_position_deposit_cover(); + + // Withdraw + uint256 currentLPBalanceAlice = raiWETHPair.balanceOf(address(alice)); + uint256 currentLPBalanceSaviour = raiWETHPair.balanceOf(address(saviour)); + alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler), address(alice)); + + // Checks + assertEq(raiWETHPair.balanceOf(address(alice)), currentLPBalanceAlice + currentLPBalanceSaviour); + assertTrue(raiWETHPair.balanceOf(address(saviour)) == 0 && saviour.lpTokenCover(safeHandler) == 0); + + // Deposit again + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, currentLPBalanceSaviour); + + // Checks + assertTrue(raiWETHPair.balanceOf(address(saviour)) > 0 && saviour.lpTokenCover(safeHandler) > 0); + assertEq(saviour.lpTokenCover(safeHandler), currentLPBalanceSaviour); + assertEq(raiWETHPair.balanceOf(address(saviour)), currentLPBalanceSaviour); + assertEq(raiWETHPair.balanceOf(address(alice)), currentLPBalanceAlice); + } + function testFail_withdraw_unauthorized() public { + (uint safe, ) = default_create_position_deposit_cover(); + + // Withdraw by unauthed + FakeUser bob = new FakeUser(); + bob.doWithdraw(saviour, safe, raiWETHPair.balanceOf(address(saviour)), address(bob)); + } + function testFail_withdraw_more_than_deposited() public { + (uint safe, address safeHandler) = default_create_position_deposit_cover(); + uint256 currentLPBalance = raiWETHPair.balanceOf(address(this)); + alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler) + 1, address(this)); + } + function testFail_withdraw_null() public { + (uint safe, address safeHandler) = default_create_position_deposit_cover(); + alice.doWithdraw(saviour, safe, 0, address(this)); + } + function test_withdraw() public { + (uint safe, address safeHandler) = default_create_position_deposit_cover(); + + // Withdraw + uint256 currentLPBalanceAlice = raiWETHPair.balanceOf(address(alice)); + uint256 currentLPBalanceSaviour = raiWETHPair.balanceOf(address(saviour)); + alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler), address(alice)); + + // Checks + assertEq(raiWETHPair.balanceOf(address(alice)), currentLPBalanceAlice + currentLPBalanceSaviour); + assertTrue(raiWETHPair.balanceOf(address(saviour)) == 0 && saviour.lpTokenCover(safeHandler) == 0); + } + function test_withdraw_twice() public { + (uint safe, address safeHandler) = default_create_position_deposit_cover(); + + // Withdraw once + uint256 currentLPBalanceAlice = raiWETHPair.balanceOf(address(alice)); + uint256 currentLPBalanceSaviour = raiWETHPair.balanceOf(address(saviour)); + alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler) / 2, address(alice)); + + // Checks + assertEq(raiWETHPair.balanceOf(address(alice)), currentLPBalanceAlice + currentLPBalanceSaviour / 2); + assertTrue(raiWETHPair.balanceOf(address(saviour)) == currentLPBalanceSaviour / 2 && saviour.lpTokenCover(safeHandler) == currentLPBalanceSaviour / 2); + + // Withdraw again + alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler), address(alice)); + + // Checks + assertEq(raiWETHPair.balanceOf(address(alice)), currentLPBalanceAlice + currentLPBalanceSaviour); + assertTrue(raiWETHPair.balanceOf(address(saviour)) == 0 && saviour.lpTokenCover(safeHandler) == 0); + } + function test_withdraw_custom_dst() public { + (uint safe, address safeHandler) = default_create_position_deposit_cover(); + + // Withdraw + uint256 currentLPBalanceAlice = raiWETHPair.balanceOf(address(0xb1)); + uint256 currentLPBalanceSaviour = raiWETHPair.balanceOf(address(saviour)); + alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler), address(0xb1)); + + // Checks + assertEq(raiWETHPair.balanceOf(address(0xb1)), currentLPBalanceSaviour); + assertEq(raiWETHPair.balanceOf(address(alice)), currentLPBalanceAlice); + assertTrue(raiWETHPair.balanceOf(address(saviour)) == 0 && saviour.lpTokenCover(safeHandler) == 0); + } + function test_tokenAmountUsedToSave() public { + (uint safe, address safeHandler) = default_create_position_deposit_cover(); + + assertEq(saviour.lpTokenCover(safeHandler), saviour.tokenAmountUsedToSave("eth", safeHandler)); + } + function test_getCollateralPrice_zero_price() public { + ethFSM.updateCollateralPrice(0); + assertEq(saviour.getCollateralPrice(), 0); + } + function test_getCollateralPrice_invalid() public { + ethFSM.changeValidity(); + assertEq(saviour.getCollateralPrice(), 0); + } + function test_getCollateralPrice_null_fsm() public { + oracleRelayer.modifyParameters("eth", "orcl", address(0)); + assertEq(saviour.getCollateralPrice(), 0); + } + function test_getCollateralPrice() public { + assertEq(saviour.getCollateralPrice(), initETHUSDPrice); + } + function test_getSystemCoinMarketPrice_invalid() public { + systemCoinOracle.changeValidity(); + assertEq(saviour.getSystemCoinMarketPrice(), 0); + } + function test_getSystemCoinMarketPrice_null_price() public { + systemCoinOracle.updateCollateralPrice(0); + assertEq(saviour.getSystemCoinMarketPrice(), 0); + } + function test_getSystemCoinMarketPrice() public { + assertEq(saviour.getSystemCoinMarketPrice(), initRAIUSDPrice); + } + function test_getLPUnderlying_inexistent_handler() public { + (uint sysCoins, uint collateral) = saviour.getLPUnderlying(address(0x1)); + assertEq(sysCoins, collateral); + assertEq(sysCoins, 0); + } + function test_getLPUnderlying() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); + + (uint sysCoins, uint collateral) = saviour.getLPUnderlying(safeHandler); + assertEq(sysCoins, initRAIETHPairLiquidity * defaultLiquidityMultiplier); + assertEq(collateral, initETHRAIPairLiquidity * defaultLiquidityMultiplier); + } + function test_getKeeperPayoutTokens_null_collateral_price() public { + address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); + ethFSM.updateCollateralPrice(0); + + (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), 0, 0); + + assertEq(sysCoins, 0); + assertEq(collateral, 0); + } + function test_getKeeperPayoutTokens_null_sys_coin_price() public { + address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); + systemCoinOracle.updateCollateralPrice(0); + + (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), 0, 0); + + assertEq(sysCoins, 0); + assertEq(collateral, 0); + } + function test_getKeeperPayoutTokens_only_sys_coins_used() public { + address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); + (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), uint(-1), 0); + + assertEq(sysCoins, minKeeperPayoutValue * 10 ** 18 / systemCoinOracle.read()); + assertEq(collateral, 0); + } + function test_getKeeperPayoutTokens_only_collateral_used() public { + (, address safeHandler) = default_create_position_deposit_cover(); + (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), 0, uint(-1)); + + assertEq(sysCoins, 0); + assertEq(collateral, minKeeperPayoutValue * 10 ** 18 / ethFSM.read()); + } + function test_getKeeperPayoutTokens_both_tokens_used() public { + (, address safeHandler) = default_create_position_deposit_cover(); + (uint underlyingSysCoins, ) = saviour.getLPUnderlying(safeHandler); + + (uint sysCoins, uint collateral) = + saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), (minKeeperPayoutValue * 10 ** 18 / (systemCoinOracle.read() * 2)), uint(-1)); + + assertEq(sysCoins, (minKeeperPayoutValue * 10 ** 18 / (systemCoinOracle.read() * 2))); + assertEq(collateral, 2 ether); + } + function test_getKeeperPayoutTokens_not_enough_tokens_to_pay() public { + saviour.modifyParameters("minKeeperPayoutValue", 10000000 ether); + + (, address safeHandler) = default_create_position_deposit_cover(); + (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), 0, 0); + + assertEq(sysCoins, 0); + assertEq(collateral, 0); + } + function test_canSave_cannot_save_safe() public { + // Create position + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + + weth.approve(address(collateralJoin), uint(-1)); + collateralJoin.join(address(safeHandler), defaultTokenAmount); + alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 10)); + + // Change oracle price + ethMedian.updateCollateralPrice(initETHUSDPrice / 30); + ethFSM.updateCollateralPrice(initETHUSDPrice / 30); + oracleRelayer.updateCollateralPrice("eth"); + + // Deposit cover + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity / 5, initETHRAIPairLiquidity / 5 + ); + + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount / 10); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount / 10); + assertTrue(!saviour.canSave("eth", safeHandler)); + } + function test_canSave_cannot_pay_keeper() public { + saviour.modifyParameters("minKeeperPayoutValue", 10000000 ether); + address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); + assertTrue(!saviour.canSave("eth", safeHandler)); + } + function test_canSave_both_tokens_used() public { + // Create position + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + // Change oracle price + ethMedian.updateCollateralPrice(initETHUSDPrice / 3); + ethFSM.updateCollateralPrice(initETHUSDPrice / 3); + oracleRelayer.updateCollateralPrice("eth"); + + // Deposit cover + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity * 2, initETHRAIPairLiquidity * 2 + ); + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); + saviour.modifyParameters("minKeeperPayoutValue", 50 ether); + + assertTrue(saviour.canSave("eth", safeHandler)); + } + function test_canSave() public { + address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); + assertTrue(saviour.canSave("eth", safeHandler)); + } + function test_saveSAFE_no_cover() public { + address safeHandler = default_create_liquidatable_position(200, initETHUSDPrice / 30); + default_liquidate_safe(safeHandler); + } + function test_saveSAFE_cannot_save_safe() public { + // Create position + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + + weth.approve(address(collateralJoin), uint(-1)); + collateralJoin.join(address(safeHandler), defaultTokenAmount); + alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 10)); + + // Change oracle price + ethMedian.updateCollateralPrice(initETHUSDPrice / 30); + ethFSM.updateCollateralPrice(initETHUSDPrice / 30); + oracleRelayer.updateCollateralPrice("eth"); + + // Deposit cover + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity / 5, initETHRAIPairLiquidity / 5 + ); + + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount / 10); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount / 10); + + default_liquidate_safe(safeHandler); + } + function test_saveSAFE_cannot_pay_keeper() public { + address safeHandler = default_create_liquidatable_position(200, initETHUSDPrice / 30); + saviour.modifyParameters("minKeeperPayoutValue", 10000000 ether); + default_liquidate_safe(safeHandler); + } + function test_saveSAFE_below_desired_threshold() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + + default_save(safe, safeHandler, 200); + } + function testFail_saveSAFE_above_threshold() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + alice.doSetCRatioThreshold(saviour, safe, 150); + + alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); + + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); + + liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(100000 ether)); + liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); + + saviour.saveSAFE(address(this), "eth", safeHandler); + } + function test_saveSAFE_accumulate_rate() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + + // Warp and save + hevm.warp(now + 2 days); + taxCollector.taxSingle("eth"); + + weth.approve(address(collateralJoin), uint(-1)); + collateralJoin.join(address(safeHandler), defaultTokenAmount); + alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 5)); + + alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); + alice.doSetCRatioThreshold(saviour, safe, 150); + + ethMedian.updateCollateralPrice(initETHUSDPrice / 10); + ethFSM.updateCollateralPrice(initETHUSDPrice / 10); + oracleRelayer.updateCollateralPrice("eth"); + + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); + assertEq(raiWETHPair.balanceOf(address(saviour)), lpTokenAmount); + assertTrue(saviour.canSave("eth", safeHandler)); + + liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(100000 ether)); + liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); + + uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); + uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); + + saviour.saveSAFE(address(this), "eth", safeHandler); + + assertTrue(saviour.underlyingReserves(safeHandler) > 0); + assertTrue( + systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance > 0 || + weth.balanceOf(address(this)) - preSaveWETHKeeperBalance > 0 + ); + assertTrue(raiWETHPair.balanceOf(address(saviour)) < lpTokenAmount); + assertEq(raiWETHPair.balanceOf(address(liquidityManager)), 0); + assertEq(saviour.lpTokenCover(safeHandler), 0); + } + function test_saveSAFE_both_tokens() public { + // Create position + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + alice.doSetCRatioThreshold(saviour, safe, 150); + + // Change oracle price + ethMedian.updateCollateralPrice(initETHUSDPrice / 3); + ethFSM.updateCollateralPrice(initETHUSDPrice / 3); + oracleRelayer.updateCollateralPrice("eth"); + + // Deposit cover + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity * 2, initETHRAIPairLiquidity * 2 + ); + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); + saviour.modifyParameters("minKeeperPayoutValue", 50 ether); + + assertTrue(saviour.canSave("eth", safeHandler)); + + liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(100000 ether)); + liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); + + uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); + uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); + + saviour.saveSAFE(address(this), "eth", safeHandler); + + assertEq(saviour.underlyingReserves(safeHandler), 0); + assertTrue( + systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance > 0 || + weth.balanceOf(address(this)) - preSaveWETHKeeperBalance > 0 + ); + assertTrue(raiWETHPair.balanceOf(address(saviour)) < lpTokenAmount); + assertEq(raiWETHPair.balanceOf(address(liquidityManager)), 0); + assertEq(saviour.lpTokenCover(safeHandler), 0); + } + function test_saveSAFE_both_tokens_accumulate_rate() public { + // Create position + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + + // Warp, mint and save + hevm.warp(now + 1 days); + taxCollector.taxSingle("eth"); + + weth.approve(address(collateralJoin), uint(-1)); + collateralJoin.join(address(safeHandler), defaultTokenAmount); + alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 8)); + + alice.doSetCRatioThreshold(saviour, safe, 150); + + // Change oracle price + ethMedian.updateCollateralPrice(initETHUSDPrice / 4); + ethFSM.updateCollateralPrice(initETHUSDPrice / 4); + oracleRelayer.updateCollateralPrice("eth"); + + // Deposit cover + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity * 2, initETHRAIPairLiquidity * 2 + ); + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); + saviour.modifyParameters("minKeeperPayoutValue", 50 ether); + + assertTrue(saviour.canSave("eth", safeHandler)); + + liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(100000 ether)); + liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); + + uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); + uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); + + saviour.saveSAFE(address(this), "eth", safeHandler); + + assertTrue(saviour.underlyingReserves(safeHandler) == 0); + assertTrue( + systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance > 0 || + weth.balanceOf(address(this)) - preSaveWETHKeeperBalance > 0 + ); + assertTrue(raiWETHPair.balanceOf(address(saviour)) < lpTokenAmount); + assertEq(raiWETHPair.balanceOf(address(liquidityManager)), 0); + assertEq(saviour.lpTokenCover(safeHandler), 0); + } + function test_saveSAFE_twice() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_save(safe, safeHandler, 150); + + default_second_save(safe, safeHandler, 570); + } + function testFail_saveSAFE_withdraw_cover() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_save(safe, safeHandler, 155); + + alice.doWithdraw(saviour, safe, 1, address(this)); + } + function assertAlmostEq(uint a, uint b, uint p) internal { + a = a / p; + b = b / p; + assertTrue(a >= b-1 && a <= b+1); + } + function test_get_cRatio(uint seed) public { + uint cRatio = (seed % 5 * 10**27) + 1 * 10**22; // from .001% to 500.001% + + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + weth.approve(address(collateralJoin), uint(-1)); + + collateralJoin.join(address(safeHandler), defaultCollateralAmount); + alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 10)); + + uint collateralPrice = defaultTokenAmount * 10 * initRAIUSDPrice / 10**27 * cRatio / defaultCollateralAmount; + ethFSM.updateCollateralPrice(collateralPrice); + oracleRelayer.updateCollateralPrice("eth"); + + assertAlmostEq(saviour.getSafeCRatio(safeHandler), cRatio, 10**8); // Allowing for some precision loss, cRatio is RAY + (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", safeHandler); + (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("eth"); + uint256 safeCRatio = lockedCollateral * ray(ethFSM.read()) * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10 ** 27); + assertTrue(safeCRatio == cRatio / 10**25 || safeCRatio == (cRatio / 10**25) - 1); + } + function helper_ratio_bounds(uint value) internal returns (uint) { + return (value % 4 * 10**27) + 1 * 10**27; // from 100 to 500 (cRatioSetter will not allow setting a cRatio lower than the collateral cRatio) + } + function test_saveSAFE_fuzz(uint safeCRatio, uint thresholdCRatio) public { + safeCRatio = helper_ratio_bounds(safeCRatio); + thresholdCRatio = helper_ratio_bounds(thresholdCRatio) / 10**25; + + // creating a safe + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + weth.approve(address(collateralJoin), uint(-1)); + + collateralJoin.join(address(safeHandler), defaultCollateralAmount); + alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 10)); + + // pushing it to the set cRatio + uint collateralPrice = defaultTokenAmount * 10 * initRAIUSDPrice / 10**27 * safeCRatio / defaultCollateralAmount; + ethFSM.updateCollateralPrice(collateralPrice); + oracleRelayer.updateCollateralPrice("eth"); + + // adding cover + alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); + + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier + ); + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + raiWETHPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); + + // setting threshold and desired cRatios + alice.doSetCRatioThreshold(saviour, safe, thresholdCRatio); + + uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); + uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); + + // save SAFE + try saviour.saveSAFE(address(this), "eth", safeHandler) { + // success saving safe + assertTrue(safeCRatio / 10**25 <= thresholdCRatio); + + assertTrue( + systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance > 0 || + weth.balanceOf(address(this)) - preSaveWETHKeeperBalance > 0 + ); + assertTrue(raiWETHPair.balanceOf(address(saviour)) < lpTokenAmount); + assertEq(raiWETHPair.balanceOf(address(liquidityManager)), 0); + assertEq(saviour.lpTokenCover(safeHandler), 0); + + (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", safeHandler); + if (generatedDebt == 0) return; + (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("eth"); + uint256 cRatio = lockedCollateral * ray(ethFSM.read()) * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10 ** 27); + assertTrue(cRatio > (safeCRatio / 10**25) - 1); + } catch { + // failed saving safe + assertTrue(safeCRatio / 10**25 >= thresholdCRatio); + + assertTrue( + systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance == 0 && + weth.balanceOf(address(this)) - preSaveWETHKeeperBalance == 0 + ); + assertEq(raiWETHPair.balanceOf(address(saviour)), lpTokenAmount); + assertEq(raiWETHPair.balanceOf(address(liquidityManager)), 0); + assertEq(saviour.lpTokenCover(safeHandler), lpTokenAmount); + + (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", safeHandler); + (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("eth"); + uint256 cRatio = lockedCollateral * ray(ethFSM.read()) * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10 ** 27); + assertTrue(cRatio == (safeCRatio / 10**25) || cRatio == (safeCRatio / 10**25) - 1); + } + } + + function test_saveSAFE_get_reserves() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_save(safe, safeHandler, 155); + + uint256 oldSysCoinBalance = systemCoin.balanceOf(address(alice)); + + uint sysCoinReserve = saviour.underlyingReserves(safeHandler); + + alice.doGetReserves(saviour, safe, address(alice)); + assertTrue(systemCoin.balanceOf(address(alice)) - sysCoinReserve == oldSysCoinBalance); + + assertEq(systemCoin.balanceOf(address(saviour)), 0); + } + function test_saveSAFE_get_reserves_twice() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_save(safe, safeHandler, 155); + alice.doGetReserves(saviour, safe, address(alice)); + + default_second_save(safe, safeHandler, 570); + + uint256 oldSysCoinBalance = systemCoin.balanceOf(address(0x1)); + uint256 oldCollateralBalance = weth.balanceOf(address(0x1)); + + uint sysCoinReserve = saviour.underlyingReserves(safeHandler); + + alice.doGetReserves(saviour, safe, address(0x1)); + assertEq(systemCoin.balanceOf(address(0x1)) - sysCoinReserve, oldSysCoinBalance); + assertEq(systemCoin.balanceOf(address(saviour)), 0); + } + function testFail_getReserves_invalid_caller() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_save(safe, safeHandler, 155); + + saviour.getReserves(safe, address(alice)); + } +} diff --git a/src/test/NativeUnderlyingUniswapV2SafeSaviour.t.sol b/src/test/NativeUnderlyingUniswapV2SafeSaviour.t.sol index a1f0257..d870318 100644 --- a/src/test/NativeUnderlyingUniswapV2SafeSaviour.t.sol +++ b/src/test/NativeUnderlyingUniswapV2SafeSaviour.t.sol @@ -1,1348 +1,1348 @@ -pragma solidity 0.6.7; - -import "ds-test/test.sol"; -import "ds-weth/weth9.sol"; -import "ds-token/token.sol"; - -import {SAFEEngine} from 'geb/SAFEEngine.sol'; -import {Coin} from 'geb/Coin.sol'; -import {LiquidationEngine} from 'geb/LiquidationEngine.sol'; -import {AccountingEngine} from 'geb/AccountingEngine.sol'; -import {TaxCollector} from 'geb/TaxCollector.sol'; -import {BasicCollateralJoin, CoinJoin} from 'geb/BasicTokenAdapters.sol'; -import {OracleRelayer} from 'geb/OracleRelayer.sol'; -import {EnglishCollateralAuctionHouse} from 'geb/CollateralAuctionHouse.sol'; -import {GebSafeManager} from "geb-safe-manager/GebSafeManager.sol"; - -import {SaviourCRatioSetter} from "../SaviourCRatioSetter.sol"; -import {SAFESaviourRegistry} from "../SAFESaviourRegistry.sol"; - -import "../integrations/uniswap/uni-v2/UniswapV2Factory.sol"; -import "../integrations/uniswap/uni-v2/UniswapV2Pair.sol"; -import "../integrations/uniswap/uni-v2/UniswapV2Router02.sol"; - -import "../integrations/uniswap/liquidity-managers/UniswapV2LiquidityManager.sol"; - -import "../saviours/NativeUnderlyingUniswapV2SafeSaviour.sol"; - -abstract contract Hevm { - function warp(uint256) virtual public; -} - -contract TestSAFEEngine is SAFEEngine { - uint256 constant RAY = 10 ** 27; - - constructor() public {} - - function mint(address usr, uint wad) public { - coinBalance[usr] += wad * RAY; - globalDebt += wad * RAY; - } - function balanceOf(address usr) public view returns (uint) { - return uint(coinBalance[usr] / RAY); - } -} - -// --- Median Contracts --- -contract MockMedianizer { - uint256 public price; - bool public validPrice; - uint public lastUpdateTime; - address public priceSource; - - constructor(uint256 price_, bool validPrice_) public { - price = price_; - validPrice = validPrice_; - lastUpdateTime = now; - } - function updatePriceSource(address priceSource_) external { - priceSource = priceSource_; - } - function changeValidity() external { - validPrice = !validPrice; - } - function updateCollateralPrice(uint256 price_) external { - price = price_; - lastUpdateTime = now; - } - function read() external view returns (uint256) { - return price; - } - function getResultWithValidity() external view returns (uint256, bool) { - return (price, validPrice); - } -} - -// Users -contract FakeUser { - function doModifyParameters( - NativeUnderlyingUniswapV2SafeSaviour saviour, - bytes32 parameter, - uint256 data - ) public { - saviour.modifyParameters(parameter, data); - } - - function doModifyParameters( - NativeUnderlyingUniswapV2SafeSaviour saviour, - bytes32 parameter, - address data - ) public { - saviour.modifyParameters(parameter, data); - } - - function doOpenSafe( - GebSafeManager manager, - bytes32 collateralType, - address usr - ) public returns (uint256) { - return manager.openSAFE(collateralType, usr); - } - - function doSafeAllow( - GebSafeManager manager, - uint safe, - address usr, - uint ok - ) public { - manager.allowSAFE(safe, usr, ok); - } - - function doHandlerAllow( - GebSafeManager manager, - address usr, - uint ok - ) public { - manager.allowHandler(usr, ok); - } - - function doTransferSAFEOwnership( - GebSafeManager manager, - uint safe, - address dst - ) public { - manager.transferSAFEOwnership(safe, dst); - } - - function doModifySAFECollateralization( - GebSafeManager manager, - uint safe, - int deltaCollateral, - int deltaDebt - ) public { - manager.modifySAFECollateralization(safe, deltaCollateral, deltaDebt); - } - - function doApproveSAFEModification( - SAFEEngine safeEngine, - address usr - ) public { - safeEngine.approveSAFEModification(usr); - } - - function doSAFEEngineModifySAFECollateralization( - SAFEEngine safeEngine, - bytes32 collateralType, - address safe, - address collateralSource, - address debtDst, - int deltaCollateral, - int deltaDebt - ) public { - safeEngine.modifySAFECollateralization(collateralType, safe, collateralSource, debtDst, deltaCollateral, deltaDebt); - } - - function doProtectSAFE( - GebSafeManager manager, - uint safe, - address liquidationEngine, - address saviour - ) public { - manager.protectSAFE(safe, liquidationEngine, saviour); - } - - function doDeposit( - NativeUnderlyingUniswapV2SafeSaviour saviour, - DSToken lpToken, - uint256 safeID, - uint256 tokenAmount - ) public { - lpToken.approve(address(saviour), tokenAmount); - saviour.deposit(safeID, tokenAmount); - } - - function doWithdraw( - NativeUnderlyingUniswapV2SafeSaviour saviour, - uint256 safeID, - uint256 lpTokenAmount, - address dst - ) public { - saviour.withdraw(safeID, lpTokenAmount, dst); - } - - function doGetReserves( - NativeUnderlyingUniswapV2SafeSaviour saviour, - uint256 safeID, - address dst - ) public { - saviour.getReserves(safeID, dst); - } - - function doTransferInternalCoins( - GebSafeManager manager, - uint256 safe, - address dst, - uint256 amt - ) public { - manager.transferInternalCoins(safe, dst, amt); - } - - function doSetDesiredCollateralizationRatio( - SaviourCRatioSetter cRatioSetter, - bytes32 collateralType, - uint safe, - uint cRatio - ) public { - cRatioSetter.setDesiredCollateralizationRatio(collateralType, safe, cRatio); - } -} - -contract NativeUnderlyingUniswapV2SafeSaviourTest is DSTest { - Hevm hevm; - - UniswapV2Factory uniswapFactory; - UniswapV2Router02 uniswapRouter; - UniswapV2LiquidityManager liquidityManager; - - UniswapV2Pair raiWETHPair; - - Coin systemCoin; - WETH9_ weth; - - TestSAFEEngine safeEngine; - AccountingEngine accountingEngine; - LiquidationEngine liquidationEngine; - OracleRelayer oracleRelayer; - TaxCollector taxCollector; - - BasicCollateralJoin collateralJoin; - - CoinJoin coinJoin; - CoinJoin systemCoinJoin; - - EnglishCollateralAuctionHouse collateralAuctionHouse; - - GebSafeManager safeManager; - - NativeUnderlyingUniswapV2SafeSaviour saviour; - SaviourCRatioSetter cRatioSetter; - SAFESaviourRegistry saviourRegistry; - - MockMedianizer systemCoinOracle; - MockMedianizer ethFSM; - MockMedianizer ethMedian; - - FakeUser alice; - - address me; - - // Params - uint256 initTokenAmount = 100000 ether; - uint256 initETHUSDPrice = 250 * 10 ** 18; - uint256 initRAIUSDPrice = 4.242 * 10 ** 18; - - uint256 initETHRAIPairLiquidity = 5 ether; // 1250 USD - uint256 initRAIETHPairLiquidity = 294.672324375E18; // 1 RAI = 4.242 USD - - // Saviour params - bool isSystemCoinToken0; - uint256 saveCooldown = 1 days; - uint256 minKeeperPayoutValue = 1000 ether; - uint256 defaultDesiredCollateralizationRatio = 200; - uint256 minDesiredCollateralizationRatio = 155; - - // Core system params - uint256 minCRatio = 1.5 ether; - uint256 ethToMint = 5000 ether; - uint256 ethCeiling = uint(-1); - uint256 ethFloor = 10 ether; - uint256 ethLiquidationPenalty = 1 ether; - - uint256 defaultLiquidityMultiplier = 50; - uint256 defaultCollateralAmount = 40 ether; - uint256 defaultTokenAmount = 100 ether; - - function setUp() public { - hevm = Hevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); - hevm.warp(604411200); - - // System coin - systemCoin = new Coin("RAI", "RAI", 1); - systemCoin.mint(address(this), initTokenAmount); - systemCoinOracle = new MockMedianizer(initRAIUSDPrice, true); - - // Core system - safeEngine = new TestSAFEEngine(); - safeEngine.initializeCollateralType("eth"); - safeEngine.mint(address(this), rad(initTokenAmount)); - - ethFSM = new MockMedianizer(initETHUSDPrice, true); - ethMedian = new MockMedianizer(initETHUSDPrice, true); - ethFSM.updatePriceSource(address(ethMedian)); - - oracleRelayer = new OracleRelayer(address(safeEngine)); - oracleRelayer.modifyParameters("redemptionPrice", ray(initRAIUSDPrice)); - oracleRelayer.modifyParameters("eth", "orcl", address(ethFSM)); - oracleRelayer.modifyParameters("eth", "safetyCRatio", ray(minCRatio)); - oracleRelayer.modifyParameters("eth", "liquidationCRatio", ray(minCRatio)); - - safeEngine.addAuthorization(address(oracleRelayer)); - oracleRelayer.updateCollateralPrice("eth"); - - accountingEngine = new AccountingEngine( - address(safeEngine), address(0x1), address(0x2) - ); - safeEngine.addAuthorization(address(accountingEngine)); - - taxCollector = new TaxCollector(address(safeEngine)); - taxCollector.initializeCollateralType("eth"); - taxCollector.modifyParameters("primaryTaxReceiver", address(accountingEngine)); - taxCollector.modifyParameters("eth", "stabilityFee", 1000000564701133626865910626); // 5% / day - safeEngine.addAuthorization(address(taxCollector)); - - liquidationEngine = new LiquidationEngine(address(safeEngine)); - liquidationEngine.modifyParameters("accountingEngine", address(accountingEngine)); - - safeEngine.addAuthorization(address(liquidationEngine)); - accountingEngine.addAuthorization(address(liquidationEngine)); - - weth = new WETH9_(); - weth.deposit{value: initTokenAmount}(); - - collateralJoin = new BasicCollateralJoin(address(safeEngine), "eth", address(weth)); - - coinJoin = new CoinJoin(address(safeEngine), address(systemCoin)); - systemCoin.addAuthorization(address(coinJoin)); - safeEngine.transferInternalCoins(address(this), address(coinJoin), safeEngine.coinBalance(address(this))); - - safeEngine.addAuthorization(address(collateralJoin)); - - safeEngine.modifyParameters("eth", "debtCeiling", rad(ethCeiling)); - safeEngine.modifyParameters("globalDebtCeiling", rad(ethCeiling)); - safeEngine.modifyParameters("eth", "debtFloor", rad(ethFloor)); - - collateralAuctionHouse = new EnglishCollateralAuctionHouse(address(safeEngine), address(liquidationEngine), "eth"); - collateralAuctionHouse.addAuthorization(address(liquidationEngine)); - - liquidationEngine.addAuthorization(address(collateralAuctionHouse)); - liquidationEngine.modifyParameters("eth", "collateralAuctionHouse", address(collateralAuctionHouse)); - liquidationEngine.modifyParameters("eth", "liquidationPenalty", ethLiquidationPenalty); - - safeEngine.addAuthorization(address(collateralAuctionHouse)); - safeEngine.approveSAFEModification(address(collateralAuctionHouse)); - - safeManager = new GebSafeManager(address(safeEngine)); - oracleRelayer.updateCollateralPrice("eth"); - - // Uniswap setup - uniswapFactory = new UniswapV2Factory(address(this)); - createUniswapPair(); - uniswapRouter = new UniswapV2Router02(address(uniswapFactory), address(weth)); - addPairLiquidityRouter(address(systemCoin), address(weth), initRAIETHPairLiquidity, initETHRAIPairLiquidity); - - // Liquidity manager - liquidityManager = new UniswapV2LiquidityManager(address(raiWETHPair), address(uniswapRouter)); - - // Saviour infra - saviourRegistry = new SAFESaviourRegistry(saveCooldown); - cRatioSetter = new SaviourCRatioSetter(address(oracleRelayer), address(safeManager)); - cRatioSetter.setDefaultCRatio("eth", defaultDesiredCollateralizationRatio); - - saviour = new NativeUnderlyingUniswapV2SafeSaviour( - isSystemCoinToken0, - address(coinJoin), - address(collateralJoin), - address(cRatioSetter), - address(systemCoinOracle), - address(liquidationEngine), - address(taxCollector), - address(oracleRelayer), - address(safeManager), - address(saviourRegistry), - address(liquidityManager), - address(raiWETHPair), - minKeeperPayoutValue - ); - saviourRegistry.toggleSaviour(address(saviour)); - liquidationEngine.connectSAFESaviour(address(saviour)); - - me = address(this); - alice = new FakeUser(); - } - - // --- Math --- - function ray(uint wad) internal pure returns (uint) { - return wad * 10 ** 9; - } - function rad(uint wad) internal pure returns (uint) { - return wad * 10 ** 27; - } - function sub(uint256 x, uint256 y) internal pure returns (uint256 z) { - require((z = x - y) <= x); - } - - // --- Uniswap utils --- - function createUniswapPair() internal { - // Setup WETH/RAI pair - uniswapFactory.createPair(address(weth), address(systemCoin)); - raiWETHPair = UniswapV2Pair(uniswapFactory.getPair(address(weth), address(systemCoin))); - - if (address(raiWETHPair.token0()) == address(systemCoin)) isSystemCoinToken0 = true; - } - function addPairLiquidityRouter(address token1, address token2, uint256 amount1, uint256 amount2) internal { - DSToken(token1).approve(address(uniswapRouter), uint(-1)); - DSToken(token2).approve(address(uniswapRouter), uint(-1)); - uniswapRouter.addLiquidity(token1, token2, amount1, amount2, amount1, amount2, address(this), now); - UniswapV2Pair updatedPair = UniswapV2Pair(uniswapFactory.getPair(token1, token2)); - updatedPair.sync(); - } - function addPairLiquidityTransfer(UniswapV2Pair pair, address token1, address token2, uint256 amount1, uint256 amount2) internal { - DSToken(token1).transfer(address(pair), amount1); - DSToken(token2).transfer(address(pair), amount2); - pair.sync(); - } - - // --- Default actions/scenarios --- - function default_create_liquidatable_position(uint256 desiredCRatio, uint256 liquidatableCollateralPrice) internal returns (address) { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, desiredCRatio); - assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); - - ethMedian.updateCollateralPrice(liquidatableCollateralPrice); - ethFSM.updateCollateralPrice(liquidatableCollateralPrice); - oracleRelayer.updateCollateralPrice("eth"); - - return safeHandler; - } - function default_save(uint256 safe, address safeHandler, uint desiredCRatio) internal { - default_modify_collateralization(safe, safeHandler); - - alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, desiredCRatio); - assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); - - ethMedian.updateCollateralPrice(initETHUSDPrice / 30); - ethFSM.updateCollateralPrice(initETHUSDPrice / 30); - oracleRelayer.updateCollateralPrice("eth"); - - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); - assertEq(raiWETHPair.balanceOf(address(saviour)), lpTokenAmount); - assertTrue(saviour.canSave("eth", safeHandler)); - - liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(100000 ether)); - liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); - - uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); - uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); - - uint auction = liquidationEngine.liquidateSAFE("eth", safeHandler); - (uint256 sysCoinReserve, uint256 collateralReserve) = saviour.underlyingReserves(safeHandler); - - assertEq(auction, 0); - assertTrue( - sysCoinReserve > 0 || - collateralReserve > 0 - ); - assertTrue( - systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance > 0 || - weth.balanceOf(address(this)) - preSaveWETHKeeperBalance > 0 - ); - assertTrue(raiWETHPair.balanceOf(address(saviour)) < lpTokenAmount); - assertEq(raiWETHPair.balanceOf(address(liquidityManager)), 0); - assertEq(saviour.lpTokenCover(safeHandler), 0); - - (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", safeHandler); - (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("eth"); - uint256 cRatio = lockedCollateral * ray(ethFSM.read()) * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10 ** 27); - assertTrue(cRatio == desiredCRatio || cRatio == desiredCRatio - 1); - } - function default_second_save(uint256 safe, address safeHandler, uint desiredCRatio) internal { - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, desiredCRatio); - - ethMedian.updateCollateralPrice(initETHUSDPrice / 40); - ethFSM.updateCollateralPrice(initETHUSDPrice / 40); - oracleRelayer.updateCollateralPrice("eth"); - - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); - assertEq(raiWETHPair.balanceOf(address(saviour)), lpTokenAmount); - assertTrue(saviour.canSave("eth", safeHandler)); - - liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(111 ether)); - liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); - - uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); - uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); - (uint256 oldSysCoinReserve, uint256 oldCollateralReserve) = saviour.underlyingReserves(safeHandler); - uint auction = liquidationEngine.liquidateSAFE("eth", safeHandler); - (uint256 sysCoinReserve, uint256 collateralReserve) = saviour.underlyingReserves(safeHandler); - - assertEq(auction, 0); - assertTrue( - sysCoinReserve > oldSysCoinReserve || - collateralReserve > oldCollateralReserve - ); - assertTrue( - systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance > 0 || - weth.balanceOf(address(this)) - preSaveWETHKeeperBalance > 0 - ); - assertTrue(raiWETHPair.balanceOf(address(saviour)) < lpTokenAmount); - assertEq(raiWETHPair.balanceOf(address(liquidityManager)), 0); - assertEq(saviour.lpTokenCover(safeHandler), 0); - - (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", safeHandler); - (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("eth"); - uint256 cRatio = lockedCollateral * ray(ethFSM.read()) * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10 ** 27); - assertTrue(cRatio == desiredCRatio || cRatio == desiredCRatio - 1); - } - function default_liquidate_safe(address safeHandler) internal { - liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(100000 ether)); - liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); - - uint auction = liquidationEngine.liquidateSAFE("eth", safeHandler); - // the full SAFE is liquidated - (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", me); - assertEq(lockedCollateral, 0); - assertEq(generatedDebt, 0); - // all debt goes to the accounting engine - assertTrue(accountingEngine.totalQueuedDebt() > 0); - // auction is for all collateral - (,uint amountToSell,,,,,, uint256 amountToRaise) = collateralAuctionHouse.bids(auction); - assertEq(amountToSell, defaultCollateralAmount); - assertEq(amountToRaise, rad(1100 ether)); - } - function default_create_liquidatable_position_deposit_cover(uint256 desiredCRatio, uint256 liquidatableCollateralPrice) - internal returns (address) { - // Create position - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, desiredCRatio); - assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); - - // Change oracle price - ethMedian.updateCollateralPrice(liquidatableCollateralPrice); - ethFSM.updateCollateralPrice(liquidatableCollateralPrice); - oracleRelayer.updateCollateralPrice("eth"); - - // Deposit cover - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); - assertEq(raiWETHPair.balanceOf(address(saviour)), lpTokenAmount); - assertEq(saviour.lpTokenCover(safeHandler), lpTokenAmount); - - return safeHandler; - } - function default_create_position_deposit_cover() internal returns (uint, address) { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - // Deposit cover - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); - assertEq(raiWETHPair.balanceOf(address(saviour)), lpTokenAmount); - assertEq(saviour.lpTokenCover(safeHandler), lpTokenAmount); - - return (safe, safeHandler); - } - function default_modify_collateralization(uint256 safe, address safeHandler) internal { - weth.approve(address(collateralJoin), uint(-1)); - collateralJoin.join(address(safeHandler), defaultTokenAmount); - alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 10)); - } - - // --- Tests --- - function test_setup() public { - assertEq(saviour.authorizedAccounts(address(this)), 1); - assertTrue(saviour.isSystemCoinToken0() == isSystemCoinToken0); - assertEq(saviour.minKeeperPayoutValue(), minKeeperPayoutValue); - assertEq(saviour.restrictUsage(), 0); - - assertEq(address(saviour.coinJoin()), address(coinJoin)); - assertEq(address(saviour.collateralJoin()), address(collateralJoin)); - assertEq(address(saviour.cRatioSetter()), address(cRatioSetter)); - assertEq(address(saviour.liquidationEngine()), address(liquidationEngine)); - assertEq(address(saviour.oracleRelayer()), address(oracleRelayer)); - assertEq(address(saviour.systemCoinOrcl()), address(systemCoinOracle)); - assertEq(address(saviour.systemCoin()), address(systemCoin)); - assertEq(address(saviour.safeEngine()), address(safeEngine)); - assertEq(address(saviour.safeManager()), address(safeManager)); - assertEq(address(saviour.saviourRegistry()), address(saviourRegistry)); - assertEq(address(saviour.liquidityManager()), address(liquidityManager)); - assertEq(address(saviour.lpToken()), address(raiWETHPair)); - assertEq(address(saviour.collateralToken()), address(weth)); - } - function test_modify_uints() public { - saviour.modifyParameters("minKeeperPayoutValue", 5); - saviour.modifyParameters("restrictUsage", 1); - - assertEq(saviour.minKeeperPayoutValue(), 5); - assertEq(saviour.restrictUsage(), 1); - } - function testFail_modify_uint_unauthed() public { - alice.doModifyParameters(saviour, "minKeeperPayoutValue", 5); - } - function test_modify_addresses() public { - saviour.modifyParameters("systemCoinOrcl", address(systemCoinOracle)); - saviour.modifyParameters("oracleRelayer", address(oracleRelayer)); - saviour.modifyParameters("liquidityManager", address(liquidityManager)); - - assertEq(address(saviour.liquidityManager()), address(liquidityManager)); - assertEq(address(saviour.oracleRelayer()), address(oracleRelayer)); - assertEq(address(saviour.systemCoinOrcl()), address(systemCoinOracle)); - } - function testFail_modify_address_unauthed() public { - alice.doModifyParameters(saviour, "systemCoinOrcl", address(systemCoinOracle)); - } - function testFail_deposit_liq_engine_not_approved() public { - liquidationEngine.disconnectSAFESaviour(address(saviour)); - - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), 1, lpTokenAmount); - } - function testFail_deposit_null_lp_token_amount() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), 1, 0); - } - function testFail_deposit_inexistent_safe() public { - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), 1, lpTokenAmount); - } - function test_deposit_twice() public { - uint256 initialLPSupply = raiWETHPair.totalSupply(); - - (uint safe, address safeHandler) = default_create_position_deposit_cover(); - - // Second deposit - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); - - // Checks - assertTrue(raiWETHPair.balanceOf(address(saviour)) > 0 && saviour.lpTokenCover(safeHandler) > 0); - assertEq(saviour.lpTokenCover(safeHandler), raiWETHPair.totalSupply() - initialLPSupply); - assertEq(raiWETHPair.balanceOf(address(saviour)), raiWETHPair.totalSupply() - initialLPSupply); - } - function test_deposit_after_everything_withdrawn() public { - (uint safe, address safeHandler) = default_create_position_deposit_cover(); - - // Withdraw - uint256 currentLPBalanceAlice = raiWETHPair.balanceOf(address(alice)); - uint256 currentLPBalanceSaviour = raiWETHPair.balanceOf(address(saviour)); - alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler), address(alice)); - - // Checks - assertEq(raiWETHPair.balanceOf(address(alice)), currentLPBalanceAlice + currentLPBalanceSaviour); - assertTrue(raiWETHPair.balanceOf(address(saviour)) == 0 && saviour.lpTokenCover(safeHandler) == 0); - - // Deposit again - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, currentLPBalanceSaviour); - - // Checks - assertTrue(raiWETHPair.balanceOf(address(saviour)) > 0 && saviour.lpTokenCover(safeHandler) > 0); - assertEq(saviour.lpTokenCover(safeHandler), currentLPBalanceSaviour); - assertEq(raiWETHPair.balanceOf(address(saviour)), currentLPBalanceSaviour); - assertEq(raiWETHPair.balanceOf(address(alice)), currentLPBalanceAlice); - } - function testFail_withdraw_unauthorized() public { - (uint safe, ) = default_create_position_deposit_cover(); - - // Withdraw by unauthed - FakeUser bob = new FakeUser(); - bob.doWithdraw(saviour, safe, raiWETHPair.balanceOf(address(saviour)), address(bob)); - } - function testFail_withdraw_more_than_deposited() public { - (uint safe, address safeHandler) = default_create_position_deposit_cover(); - uint256 currentLPBalance = raiWETHPair.balanceOf(address(this)); - alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler) + 1, address(this)); - } - function testFail_withdraw_null() public { - (uint safe, address safeHandler) = default_create_position_deposit_cover(); - alice.doWithdraw(saviour, safe, 0, address(this)); - } - function test_withdraw() public { - (uint safe, address safeHandler) = default_create_position_deposit_cover(); - - // Withdraw - uint256 currentLPBalanceAlice = raiWETHPair.balanceOf(address(alice)); - uint256 currentLPBalanceSaviour = raiWETHPair.balanceOf(address(saviour)); - alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler), address(alice)); - - // Checks - assertEq(raiWETHPair.balanceOf(address(alice)), currentLPBalanceAlice + currentLPBalanceSaviour); - assertTrue(raiWETHPair.balanceOf(address(saviour)) == 0 && saviour.lpTokenCover(safeHandler) == 0); - } - function test_withdraw_twice() public { - (uint safe, address safeHandler) = default_create_position_deposit_cover(); - - // Withdraw once - uint256 currentLPBalanceAlice = raiWETHPair.balanceOf(address(alice)); - uint256 currentLPBalanceSaviour = raiWETHPair.balanceOf(address(saviour)); - alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler) / 2, address(alice)); - - // Checks - assertEq(raiWETHPair.balanceOf(address(alice)), currentLPBalanceAlice + currentLPBalanceSaviour / 2); - assertTrue(raiWETHPair.balanceOf(address(saviour)) == currentLPBalanceSaviour / 2 && saviour.lpTokenCover(safeHandler) == currentLPBalanceSaviour / 2); - - // Withdraw again - alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler), address(alice)); - - // Checks - assertEq(raiWETHPair.balanceOf(address(alice)), currentLPBalanceAlice + currentLPBalanceSaviour); - assertTrue(raiWETHPair.balanceOf(address(saviour)) == 0 && saviour.lpTokenCover(safeHandler) == 0); - } - function test_withdraw_custom_dst() public { - (uint safe, address safeHandler) = default_create_position_deposit_cover(); - - // Withdraw - uint256 currentLPBalanceAlice = raiWETHPair.balanceOf(address(0xb1)); - uint256 currentLPBalanceSaviour = raiWETHPair.balanceOf(address(saviour)); - alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler), address(0xb1)); - - // Checks - assertEq(raiWETHPair.balanceOf(address(0xb1)), currentLPBalanceSaviour); - assertEq(raiWETHPair.balanceOf(address(alice)), currentLPBalanceAlice); - assertTrue(raiWETHPair.balanceOf(address(saviour)) == 0 && saviour.lpTokenCover(safeHandler) == 0); - } - function test_tokenAmountUsedToSave() public { - (uint safe, address safeHandler) = default_create_position_deposit_cover(); - - assertEq(saviour.lpTokenCover(safeHandler), saviour.tokenAmountUsedToSave("eth", safeHandler)); - } - function test_getCollateralPrice_zero_price() public { - ethFSM.updateCollateralPrice(0); - assertEq(saviour.getCollateralPrice(), 0); - } - function test_getCollateralPrice_invalid() public { - ethFSM.changeValidity(); - assertEq(saviour.getCollateralPrice(), 0); - } - function test_getCollateralPrice_null_fsm() public { - oracleRelayer.modifyParameters("eth", "orcl", address(0)); - assertEq(saviour.getCollateralPrice(), 0); - } - function test_getCollateralPrice() public { - assertEq(saviour.getCollateralPrice(), initETHUSDPrice); - } - function test_getSystemCoinMarketPrice_invalid() public { - systemCoinOracle.changeValidity(); - assertEq(saviour.getSystemCoinMarketPrice(), 0); - } - function test_getSystemCoinMarketPrice_null_price() public { - systemCoinOracle.updateCollateralPrice(0); - assertEq(saviour.getSystemCoinMarketPrice(), 0); - } - function test_getSystemCoinMarketPrice() public { - assertEq(saviour.getSystemCoinMarketPrice(), initRAIUSDPrice); - } - function test_getTargetCRatio_inexistent_handler() public { - assertEq(saviour.getTargetCRatio(address(0x1)), defaultDesiredCollateralizationRatio); - } - function test_getTargetCRatio_no_custom_desired_ratio() public { - (uint safe, address safeHandler) = default_create_position_deposit_cover(); - assertEq(saviour.getTargetCRatio(safeHandler), defaultDesiredCollateralizationRatio); - } - function test_getTargetCRatio_custom_desired_ratio() public { - (uint safe, address safeHandler) = default_create_position_deposit_cover(); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, defaultDesiredCollateralizationRatio * 2); - assertEq(saviour.getTargetCRatio(safeHandler), defaultDesiredCollateralizationRatio * 2); - } - function test_getLPUnderlying_inexistent_handler() public { - (uint sysCoins, uint collateral) = saviour.getLPUnderlying(address(0x1)); - assertEq(sysCoins, collateral); - assertEq(sysCoins, 0); - } - function test_getLPUnderlying() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); - - (uint sysCoins, uint collateral) = saviour.getLPUnderlying(safeHandler); - assertEq(sysCoins, initRAIETHPairLiquidity * defaultLiquidityMultiplier); - assertEq(collateral, initETHRAIPairLiquidity * defaultLiquidityMultiplier); - } - function test_getTokensForSaving_no_cover() public { - (uint sysCoins, uint collateral) = saviour.getTokensForSaving(address(0x1), oracleRelayer.redemptionPrice()); - - assertEq(sysCoins, collateral); - assertEq(sysCoins, 0); - } - function test_getTokensForSaving_null_redemption() public { - address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); - (uint sysCoins, uint collateral) = saviour.getTokensForSaving(safeHandler, 0); - - assertEq(sysCoins, collateral); - assertEq(sysCoins, 0); - } - function test_getTokensForSaving_null_collateral_price() public { - address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); - ethFSM.updateCollateralPrice(0); - (uint sysCoins, uint collateral) = saviour.getTokensForSaving(safeHandler, oracleRelayer.redemptionPrice()); - - assertEq(sysCoins, collateral); - assertEq(sysCoins, 0); - } - function test_getTokensForSaving_save_only_with_sys_coins() public { - address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); - (uint sysCoins, uint collateral) = saviour.getTokensForSaving(safeHandler, oracleRelayer.redemptionPrice()); - - assertTrue(sysCoins > 0); - assertEq(collateral, 0); - } - function test_getTokensForSaving_both_tokens_used() public { - // Create position - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 200); - assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); - - // Change oracle price - ethMedian.updateCollateralPrice(initETHUSDPrice / 3); - ethFSM.updateCollateralPrice(initETHUSDPrice / 3); - oracleRelayer.updateCollateralPrice("eth"); - - // Deposit cover - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity * 2, initETHRAIPairLiquidity * 2 - ); - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); - - (uint sysCoins, uint collateral) = saviour.getTokensForSaving(safeHandler, oracleRelayer.redemptionPrice()); - - assertTrue(sysCoins > 0); - assertTrue(collateral > 0); - } - function test_getTokensForSaving_not_enough_lp_collateral() public { - // Create position - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - - weth.approve(address(collateralJoin), uint(-1)); - collateralJoin.join(address(safeHandler), defaultTokenAmount); - alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 10)); - - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 155); - assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); - - // Change oracle price - ethMedian.updateCollateralPrice(initETHUSDPrice / 30); - ethFSM.updateCollateralPrice(initETHUSDPrice / 30); - oracleRelayer.updateCollateralPrice("eth"); - - // Deposit cover - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity / 5, initETHRAIPairLiquidity / 5 - ); - - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount / 10); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount / 10); - - (uint sysCoins, uint collateral) = saviour.getTokensForSaving(safeHandler, oracleRelayer.redemptionPrice()); - - assertEq(sysCoins, 0); - assertEq(collateral, 0); - } - function test_getKeeperPayoutTokens_null_collateral_price() public { - address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); - ethFSM.updateCollateralPrice(0); - - (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), 0, 0); - - assertEq(sysCoins, 0); - assertEq(collateral, 0); - } - function test_getKeeperPayoutTokens_null_sys_coin_price() public { - address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); - systemCoinOracle.updateCollateralPrice(0); - - (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), 0, 0); - - assertEq(sysCoins, 0); - assertEq(collateral, 0); - } - function test_getKeeperPayoutTokens_only_sys_coins_used() public { - address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); - (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), 0, 0); - - assertEq(sysCoins, minKeeperPayoutValue * 10 ** 18 / systemCoinOracle.read()); - assertEq(collateral, 0); - } - function test_getKeeperPayoutTokens_only_collateral_used() public { - (, address safeHandler) = default_create_position_deposit_cover(); - (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), uint(-1), 0); - - assertEq(sysCoins, 0); - assertEq(collateral, minKeeperPayoutValue * 10 ** 18 / ethFSM.read()); - } - function test_getKeeperPayoutTokens_both_tokens_used() public { - (, address safeHandler) = default_create_position_deposit_cover(); - (uint underlyingSysCoins, ) = saviour.getLPUnderlying(safeHandler); - - (uint sysCoins, uint collateral) = - saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), underlyingSysCoins - (minKeeperPayoutValue * 10 ** 18 / (systemCoinOracle.read() * 2)), 0); - - assertEq(sysCoins, (minKeeperPayoutValue * 10 ** 18 / (systemCoinOracle.read() * 2))); - assertEq(collateral, 2 ether); - } - function test_getKeeperPayoutTokens_not_enough_tokens_to_pay() public { - saviour.modifyParameters("minKeeperPayoutValue", 10000000 ether); - - (, address safeHandler) = default_create_position_deposit_cover(); - (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), 0, 0); - - assertEq(sysCoins, 0); - assertEq(collateral, 0); - } - function test_canSave_cannot_save_safe() public { - // Create position - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - - weth.approve(address(collateralJoin), uint(-1)); - collateralJoin.join(address(safeHandler), defaultTokenAmount); - alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 10)); - - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 155); - assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); - - // Change oracle price - ethMedian.updateCollateralPrice(initETHUSDPrice / 30); - ethFSM.updateCollateralPrice(initETHUSDPrice / 30); - oracleRelayer.updateCollateralPrice("eth"); - - // Deposit cover - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity / 5, initETHRAIPairLiquidity / 5 - ); - - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount / 10); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount / 10); - assertTrue(!saviour.canSave("eth", safeHandler)); - } - function test_canSave_cannot_pay_keeper() public { - saviour.modifyParameters("minKeeperPayoutValue", 10000000 ether); - address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); - assertTrue(!saviour.canSave("eth", safeHandler)); - } - function test_canSave_both_tokens_used() public { - // Create position - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 200); - assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); - - // Change oracle price - ethMedian.updateCollateralPrice(initETHUSDPrice / 3); - ethFSM.updateCollateralPrice(initETHUSDPrice / 3); - oracleRelayer.updateCollateralPrice("eth"); - - // Deposit cover - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity * 2, initETHRAIPairLiquidity * 2 - ); - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); - saviour.modifyParameters("minKeeperPayoutValue", 50 ether); - - assertTrue(saviour.canSave("eth", safeHandler)); - } - function test_canSave() public { - address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); - assertTrue(saviour.canSave("eth", safeHandler)); - } - function testFail_saveSAFE_invalid_caller() public { - address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); - saviour.saveSAFE(address(this), "eth", safeHandler); - } - function test_saveSAFE_no_cover() public { - address safeHandler = default_create_liquidatable_position(200, initETHUSDPrice / 30); - default_liquidate_safe(safeHandler); - } - function test_saveSAFE_cannot_save_safe() public { - // Create position - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - - weth.approve(address(collateralJoin), uint(-1)); - collateralJoin.join(address(safeHandler), defaultTokenAmount); - alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 10)); - - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 155); - assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); - - // Change oracle price - ethMedian.updateCollateralPrice(initETHUSDPrice / 30); - ethFSM.updateCollateralPrice(initETHUSDPrice / 30); - oracleRelayer.updateCollateralPrice("eth"); - - // Deposit cover - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity / 5, initETHRAIPairLiquidity / 5 - ); - - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount / 10); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount / 10); - - default_liquidate_safe(safeHandler); - } - function test_saveSAFE_cannot_pay_keeper() public { - address safeHandler = default_create_liquidatable_position(200, initETHUSDPrice / 30); - saviour.modifyParameters("minKeeperPayoutValue", 10000000 ether); - default_liquidate_safe(safeHandler); - } - function test_saveSAFE() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_save(safe, safeHandler, 200); - } - function test_saveSAFE_accumulate_rate() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - - // Warp and save - hevm.warp(now + 2 days); - taxCollector.taxSingle("eth"); - - weth.approve(address(collateralJoin), uint(-1)); - collateralJoin.join(address(safeHandler), defaultTokenAmount); - alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 5)); - - alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 200); - assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); - - ethMedian.updateCollateralPrice(initETHUSDPrice / 10); - ethFSM.updateCollateralPrice(initETHUSDPrice / 10); - oracleRelayer.updateCollateralPrice("eth"); - - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); - assertEq(raiWETHPair.balanceOf(address(saviour)), lpTokenAmount); - assertTrue(saviour.canSave("eth", safeHandler)); - - liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(100000 ether)); - liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); - - uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); - uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); - - uint auction = liquidationEngine.liquidateSAFE("eth", safeHandler); - (uint256 sysCoinReserve, uint256 collateralReserve) = saviour.underlyingReserves(safeHandler); - - assertEq(auction, 0); - assertTrue( - sysCoinReserve > 0 || - collateralReserve > 0 - ); - assertTrue( - systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance > 0 || - weth.balanceOf(address(this)) - preSaveWETHKeeperBalance > 0 - ); - assertTrue(raiWETHPair.balanceOf(address(saviour)) < lpTokenAmount); - assertEq(raiWETHPair.balanceOf(address(liquidityManager)), 0); - assertEq(saviour.lpTokenCover(safeHandler), 0); - - (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", safeHandler); - (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("eth"); - assertEq(lockedCollateral * ray(ethFSM.read()) * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10 ** 27), 200); - } - function test_saveSAFE_both_tokens() public { - // Create position - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 200); - assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); - - // Change oracle price - ethMedian.updateCollateralPrice(initETHUSDPrice / 3); - ethFSM.updateCollateralPrice(initETHUSDPrice / 3); - oracleRelayer.updateCollateralPrice("eth"); - - // Deposit cover - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity * 2, initETHRAIPairLiquidity * 2 - ); - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); - saviour.modifyParameters("minKeeperPayoutValue", 50 ether); - - assertTrue(saviour.canSave("eth", safeHandler)); - - liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(100000 ether)); - liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); - - uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); - uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); - - uint auction = liquidationEngine.liquidateSAFE("eth", safeHandler); - (uint256 sysCoinReserve, uint256 collateralReserve) = saviour.underlyingReserves(safeHandler); - - assertEq(auction, 0); - assertTrue( - sysCoinReserve > 0 || - collateralReserve > 0 - ); - assertTrue( - systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance > 0 || - weth.balanceOf(address(this)) - preSaveWETHKeeperBalance > 0 - ); - assertTrue(raiWETHPair.balanceOf(address(saviour)) < lpTokenAmount); - assertEq(raiWETHPair.balanceOf(address(liquidityManager)), 0); - assertEq(saviour.lpTokenCover(safeHandler), 0); - - (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", safeHandler); - assertEq(lockedCollateral * ray(ethFSM.read()) * 100 / (generatedDebt * oracleRelayer.redemptionPrice()), 199); - } - function test_saveSAFE_both_tokens_accumulate_rate() public { - // Create position - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - - // Warp, mint and save - hevm.warp(now + 1 days); - taxCollector.taxSingle("eth"); - - weth.approve(address(collateralJoin), uint(-1)); - collateralJoin.join(address(safeHandler), defaultTokenAmount); - alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 8)); - - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 250); - assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); - - // Change oracle price - ethMedian.updateCollateralPrice(initETHUSDPrice / 4); - ethFSM.updateCollateralPrice(initETHUSDPrice / 4); - oracleRelayer.updateCollateralPrice("eth"); - - // Deposit cover - uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(weth), initRAIETHPairLiquidity * 2, initETHRAIPairLiquidity * 2 - ); - lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); - raiWETHPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); - saviour.modifyParameters("minKeeperPayoutValue", 50 ether); - - assertTrue(saviour.canSave("eth", safeHandler)); - - liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(100000 ether)); - liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); - - uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); - uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); - - (uint sysCoinsForSaving, uint collateralForSaving) = saviour.getTokensForSaving(safeHandler, oracleRelayer.redemptionPrice()); - assertTrue(collateralForSaving > 0); - assertTrue(sysCoinsForSaving > 0); - - uint auction = liquidationEngine.liquidateSAFE("eth", safeHandler); - (uint256 sysCoinReserve, uint256 collateralReserve) = saviour.underlyingReserves(safeHandler); - - assertEq(auction, 0); - assertTrue( - sysCoinReserve > 0 || - collateralReserve > 0 - ); - assertTrue( - systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance > 0 || - weth.balanceOf(address(this)) - preSaveWETHKeeperBalance > 0 - ); - assertTrue(raiWETHPair.balanceOf(address(saviour)) < lpTokenAmount); - assertEq(raiWETHPair.balanceOf(address(liquidityManager)), 0); - assertEq(saviour.lpTokenCover(safeHandler), 0); - - (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", safeHandler); - (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("eth"); - assertEq(lockedCollateral * ray(ethFSM.read()) * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10 ** 27), 249); - } - function test_saveSAFE_twice() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_save(safe, safeHandler, 155); - - hevm.warp(now + saviourRegistry.saveCooldown() + 1); - default_second_save(safe, safeHandler, 200); - } - function testFail_saveSAFE_withdraw_cover() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_save(safe, safeHandler, 155); - - alice.doWithdraw(saviour, safe, 1, address(this)); - } - function test_saveSAFE_get_reserves() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_save(safe, safeHandler, 155); - - uint256 oldSysCoinBalance = systemCoin.balanceOf(address(alice)); - uint256 oldCollateralBalance = weth.balanceOf(address(alice)); - - (uint sysCoinReserve, uint collateralReserve) = saviour.underlyingReserves(safeHandler); - - alice.doGetReserves(saviour, safe, address(alice)); - assertTrue(systemCoin.balanceOf(address(alice)) - sysCoinReserve == oldSysCoinBalance); - assertTrue(weth.balanceOf(address(alice)) - collateralReserve == oldCollateralBalance); - - assertEq(systemCoin.balanceOf(address(saviour)), 0); - assertEq(weth.balanceOf(address(saviour)), 0); - } - function testFail_save_twice_without_waiting() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_save(safe, safeHandler, 155); - default_second_save(safe, safeHandler, 200); - } - function test_saveSAFE_get_reserves_twice() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_save(safe, safeHandler, 155); - alice.doGetReserves(saviour, safe, address(alice)); - - hevm.warp(now + saviourRegistry.saveCooldown() + 1); - default_second_save(safe, safeHandler, 200); - - uint256 oldSysCoinBalance = systemCoin.balanceOf(address(0x1)); - uint256 oldCollateralBalance = weth.balanceOf(address(0x1)); - - (uint sysCoinReserve, uint collateralReserve) = saviour.underlyingReserves(safeHandler); - - alice.doGetReserves(saviour, safe, address(0x1)); - assertTrue(systemCoin.balanceOf(address(0x1)) - sysCoinReserve == oldSysCoinBalance); - assertTrue(weth.balanceOf(address(0x1)) - collateralReserve == oldCollateralBalance); - - assertEq(systemCoin.balanceOf(address(saviour)), 0); - assertEq(weth.balanceOf(address(saviour)), 0); - } - function testFail_getReserves_invalid_caller() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_save(safe, safeHandler, 155); - - saviour.getReserves(safe, address(alice)); - } -} +pragma solidity 0.6.7; + +import "ds-test/test.sol"; +import "ds-weth/weth9.sol"; +import "ds-token/token.sol"; + +import {SAFEEngine} from 'geb/SAFEEngine.sol'; +import {Coin} from 'geb/Coin.sol'; +import {LiquidationEngine} from 'geb/LiquidationEngine.sol'; +import {AccountingEngine} from 'geb/AccountingEngine.sol'; +import {TaxCollector} from 'geb/TaxCollector.sol'; +import {BasicCollateralJoin, CoinJoin} from 'geb/BasicTokenAdapters.sol'; +import {OracleRelayer} from 'geb/OracleRelayer.sol'; +import {EnglishCollateralAuctionHouse} from 'geb/CollateralAuctionHouse.sol'; +import {GebSafeManager} from "geb-safe-manager/GebSafeManager.sol"; + +import {SaviourCRatioSetter} from "../SaviourCRatioSetter.sol"; +import {SAFESaviourRegistry} from "../SAFESaviourRegistry.sol"; + +import "../integrations/uniswap/uni-v2/UniswapV2Factory.sol"; +import "../integrations/uniswap/uni-v2/UniswapV2Pair.sol"; +import "../integrations/uniswap/uni-v2/UniswapV2Router02.sol"; + +import "../integrations/uniswap/liquidity-managers/UniswapV2LiquidityManager.sol"; + +import "../saviours/NativeUnderlyingUniswapV2SafeSaviour.sol"; + +abstract contract Hevm { + function warp(uint256) virtual public; +} + +contract TestSAFEEngine is SAFEEngine { + uint256 constant RAY = 10 ** 27; + + constructor() public {} + + function mint(address usr, uint wad) public { + coinBalance[usr] += wad * RAY; + globalDebt += wad * RAY; + } + function balanceOf(address usr) public view returns (uint) { + return uint(coinBalance[usr] / RAY); + } +} + +// --- Median Contracts --- +contract MockMedianizer { + uint256 public price; + bool public validPrice; + uint public lastUpdateTime; + address public priceSource; + + constructor(uint256 price_, bool validPrice_) public { + price = price_; + validPrice = validPrice_; + lastUpdateTime = now; + } + function updatePriceSource(address priceSource_) external { + priceSource = priceSource_; + } + function changeValidity() external { + validPrice = !validPrice; + } + function updateCollateralPrice(uint256 price_) external { + price = price_; + lastUpdateTime = now; + } + function read() external view returns (uint256) { + return price; + } + function getResultWithValidity() external view returns (uint256, bool) { + return (price, validPrice); + } +} + +// Users +contract FakeUser { + function doModifyParameters( + NativeUnderlyingUniswapV2SafeSaviour saviour, + bytes32 parameter, + uint256 data + ) public { + saviour.modifyParameters(parameter, data); + } + + function doModifyParameters( + NativeUnderlyingUniswapV2SafeSaviour saviour, + bytes32 parameter, + address data + ) public { + saviour.modifyParameters(parameter, data); + } + + function doOpenSafe( + GebSafeManager manager, + bytes32 collateralType, + address usr + ) public returns (uint256) { + return manager.openSAFE(collateralType, usr); + } + + function doSafeAllow( + GebSafeManager manager, + uint safe, + address usr, + uint ok + ) public { + manager.allowSAFE(safe, usr, ok); + } + + function doHandlerAllow( + GebSafeManager manager, + address usr, + uint ok + ) public { + manager.allowHandler(usr, ok); + } + + function doTransferSAFEOwnership( + GebSafeManager manager, + uint safe, + address dst + ) public { + manager.transferSAFEOwnership(safe, dst); + } + + function doModifySAFECollateralization( + GebSafeManager manager, + uint safe, + int deltaCollateral, + int deltaDebt + ) public { + manager.modifySAFECollateralization(safe, deltaCollateral, deltaDebt); + } + + function doApproveSAFEModification( + SAFEEngine safeEngine, + address usr + ) public { + safeEngine.approveSAFEModification(usr); + } + + function doSAFEEngineModifySAFECollateralization( + SAFEEngine safeEngine, + bytes32 collateralType, + address safe, + address collateralSource, + address debtDst, + int deltaCollateral, + int deltaDebt + ) public { + safeEngine.modifySAFECollateralization(collateralType, safe, collateralSource, debtDst, deltaCollateral, deltaDebt); + } + + function doProtectSAFE( + GebSafeManager manager, + uint safe, + address liquidationEngine, + address saviour + ) public { + manager.protectSAFE(safe, liquidationEngine, saviour); + } + + function doDeposit( + NativeUnderlyingUniswapV2SafeSaviour saviour, + DSToken lpToken, + uint256 safeID, + uint256 tokenAmount + ) public { + lpToken.approve(address(saviour), tokenAmount); + saviour.deposit(safeID, tokenAmount); + } + + function doWithdraw( + NativeUnderlyingUniswapV2SafeSaviour saviour, + uint256 safeID, + uint256 lpTokenAmount, + address dst + ) public { + saviour.withdraw(safeID, lpTokenAmount, dst); + } + + function doGetReserves( + NativeUnderlyingUniswapV2SafeSaviour saviour, + uint256 safeID, + address dst + ) public { + saviour.getReserves(safeID, dst); + } + + function doTransferInternalCoins( + GebSafeManager manager, + uint256 safe, + address dst, + uint256 amt + ) public { + manager.transferInternalCoins(safe, dst, amt); + } + + function doSetDesiredCollateralizationRatio( + SaviourCRatioSetter cRatioSetter, + bytes32 collateralType, + uint safe, + uint cRatio + ) public { + cRatioSetter.setDesiredCollateralizationRatio(collateralType, safe, cRatio); + } +} + +contract NativeUnderlyingUniswapV2SafeSaviourTest is DSTest { + Hevm hevm; + + UniswapV2Factory uniswapFactory; + UniswapV2Router02 uniswapRouter; + UniswapV2LiquidityManager liquidityManager; + + UniswapV2Pair raiWETHPair; + + Coin systemCoin; + WETH9_ weth; + + TestSAFEEngine safeEngine; + AccountingEngine accountingEngine; + LiquidationEngine liquidationEngine; + OracleRelayer oracleRelayer; + TaxCollector taxCollector; + + BasicCollateralJoin collateralJoin; + + CoinJoin coinJoin; + CoinJoin systemCoinJoin; + + EnglishCollateralAuctionHouse collateralAuctionHouse; + + GebSafeManager safeManager; + + NativeUnderlyingUniswapV2SafeSaviour saviour; + SaviourCRatioSetter cRatioSetter; + SAFESaviourRegistry saviourRegistry; + + MockMedianizer systemCoinOracle; + MockMedianizer ethFSM; + MockMedianizer ethMedian; + + FakeUser alice; + + address me; + + // Params + uint256 initTokenAmount = 100000 ether; + uint256 initETHUSDPrice = 250 * 10 ** 18; + uint256 initRAIUSDPrice = 4.242 * 10 ** 18; + + uint256 initETHRAIPairLiquidity = 5 ether; // 1250 USD + uint256 initRAIETHPairLiquidity = 294.672324375E18; // 1 RAI = 4.242 USD + + // Saviour params + bool isSystemCoinToken0; + uint256 saveCooldown = 1 days; + uint256 minKeeperPayoutValue = 1000 ether; + uint256 defaultDesiredCollateralizationRatio = 200; + uint256 minDesiredCollateralizationRatio = 155; + + // Core system params + uint256 minCRatio = 1.5 ether; + uint256 ethToMint = 5000 ether; + uint256 ethCeiling = uint(-1); + uint256 ethFloor = 10 ether; + uint256 ethLiquidationPenalty = 1 ether; + + uint256 defaultLiquidityMultiplier = 50; + uint256 defaultCollateralAmount = 40 ether; + uint256 defaultTokenAmount = 100 ether; + + function setUp() public { + hevm = Hevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + hevm.warp(604411200); + + // System coin + systemCoin = new Coin("RAI", "RAI", 1); + systemCoin.mint(address(this), initTokenAmount); + systemCoinOracle = new MockMedianizer(initRAIUSDPrice, true); + + // Core system + safeEngine = new TestSAFEEngine(); + safeEngine.initializeCollateralType("eth"); + safeEngine.mint(address(this), rad(initTokenAmount)); + + ethFSM = new MockMedianizer(initETHUSDPrice, true); + ethMedian = new MockMedianizer(initETHUSDPrice, true); + ethFSM.updatePriceSource(address(ethMedian)); + + oracleRelayer = new OracleRelayer(address(safeEngine)); + oracleRelayer.modifyParameters("redemptionPrice", ray(initRAIUSDPrice)); + oracleRelayer.modifyParameters("eth", "orcl", address(ethFSM)); + oracleRelayer.modifyParameters("eth", "safetyCRatio", ray(minCRatio)); + oracleRelayer.modifyParameters("eth", "liquidationCRatio", ray(minCRatio)); + + safeEngine.addAuthorization(address(oracleRelayer)); + oracleRelayer.updateCollateralPrice("eth"); + + accountingEngine = new AccountingEngine( + address(safeEngine), address(0x1), address(0x2) + ); + safeEngine.addAuthorization(address(accountingEngine)); + + taxCollector = new TaxCollector(address(safeEngine)); + taxCollector.initializeCollateralType("eth"); + taxCollector.modifyParameters("primaryTaxReceiver", address(accountingEngine)); + taxCollector.modifyParameters("eth", "stabilityFee", 1000000564701133626865910626); // 5% / day + safeEngine.addAuthorization(address(taxCollector)); + + liquidationEngine = new LiquidationEngine(address(safeEngine)); + liquidationEngine.modifyParameters("accountingEngine", address(accountingEngine)); + + safeEngine.addAuthorization(address(liquidationEngine)); + accountingEngine.addAuthorization(address(liquidationEngine)); + + weth = new WETH9_(); + weth.deposit{value: initTokenAmount}(); + + collateralJoin = new BasicCollateralJoin(address(safeEngine), "eth", address(weth)); + + coinJoin = new CoinJoin(address(safeEngine), address(systemCoin)); + systemCoin.addAuthorization(address(coinJoin)); + safeEngine.transferInternalCoins(address(this), address(coinJoin), safeEngine.coinBalance(address(this))); + + safeEngine.addAuthorization(address(collateralJoin)); + + safeEngine.modifyParameters("eth", "debtCeiling", rad(ethCeiling)); + safeEngine.modifyParameters("globalDebtCeiling", rad(ethCeiling)); + safeEngine.modifyParameters("eth", "debtFloor", rad(ethFloor)); + + collateralAuctionHouse = new EnglishCollateralAuctionHouse(address(safeEngine), address(liquidationEngine), "eth"); + collateralAuctionHouse.addAuthorization(address(liquidationEngine)); + + liquidationEngine.addAuthorization(address(collateralAuctionHouse)); + liquidationEngine.modifyParameters("eth", "collateralAuctionHouse", address(collateralAuctionHouse)); + liquidationEngine.modifyParameters("eth", "liquidationPenalty", ethLiquidationPenalty); + + safeEngine.addAuthorization(address(collateralAuctionHouse)); + safeEngine.approveSAFEModification(address(collateralAuctionHouse)); + + safeManager = new GebSafeManager(address(safeEngine)); + oracleRelayer.updateCollateralPrice("eth"); + + // Uniswap setup + uniswapFactory = new UniswapV2Factory(address(this)); + createUniswapPair(); + uniswapRouter = new UniswapV2Router02(address(uniswapFactory), address(weth)); + addPairLiquidityRouter(address(systemCoin), address(weth), initRAIETHPairLiquidity, initETHRAIPairLiquidity); + + // Liquidity manager + liquidityManager = new UniswapV2LiquidityManager(address(raiWETHPair), address(uniswapRouter)); + + // Saviour infra + saviourRegistry = new SAFESaviourRegistry(saveCooldown); + cRatioSetter = new SaviourCRatioSetter(address(oracleRelayer), address(safeManager)); + cRatioSetter.setDefaultCRatio("eth", defaultDesiredCollateralizationRatio); + + saviour = new NativeUnderlyingUniswapV2SafeSaviour( + isSystemCoinToken0, + address(coinJoin), + address(collateralJoin), + address(cRatioSetter), + address(systemCoinOracle), + address(liquidationEngine), + address(taxCollector), + address(oracleRelayer), + address(safeManager), + address(saviourRegistry), + address(liquidityManager), + address(raiWETHPair), + minKeeperPayoutValue + ); + saviourRegistry.toggleSaviour(address(saviour)); + liquidationEngine.connectSAFESaviour(address(saviour)); + + me = address(this); + alice = new FakeUser(); + } + + // --- Math --- + function ray(uint wad) internal pure returns (uint) { + return wad * 10 ** 9; + } + function rad(uint wad) internal pure returns (uint) { + return wad * 10 ** 27; + } + function sub(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x - y) <= x); + } + + // --- Uniswap utils --- + function createUniswapPair() internal { + // Setup WETH/RAI pair + uniswapFactory.createPair(address(weth), address(systemCoin)); + raiWETHPair = UniswapV2Pair(uniswapFactory.getPair(address(weth), address(systemCoin))); + + if (address(raiWETHPair.token0()) == address(systemCoin)) isSystemCoinToken0 = true; + } + function addPairLiquidityRouter(address token1, address token2, uint256 amount1, uint256 amount2) internal { + DSToken(token1).approve(address(uniswapRouter), uint(-1)); + DSToken(token2).approve(address(uniswapRouter), uint(-1)); + uniswapRouter.addLiquidity(token1, token2, amount1, amount2, amount1, amount2, address(this), now); + UniswapV2Pair updatedPair = UniswapV2Pair(uniswapFactory.getPair(token1, token2)); + updatedPair.sync(); + } + function addPairLiquidityTransfer(UniswapV2Pair pair, address token1, address token2, uint256 amount1, uint256 amount2) internal { + DSToken(token1).transfer(address(pair), amount1); + DSToken(token2).transfer(address(pair), amount2); + pair.sync(); + } + + // --- Default actions/scenarios --- + function default_create_liquidatable_position(uint256 desiredCRatio, uint256 liquidatableCollateralPrice) internal returns (address) { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, desiredCRatio); + assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); + + ethMedian.updateCollateralPrice(liquidatableCollateralPrice); + ethFSM.updateCollateralPrice(liquidatableCollateralPrice); + oracleRelayer.updateCollateralPrice("eth"); + + return safeHandler; + } + function default_save(uint256 safe, address safeHandler, uint desiredCRatio) internal { + default_modify_collateralization(safe, safeHandler); + + alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, desiredCRatio); + assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); + + ethMedian.updateCollateralPrice(initETHUSDPrice / 30); + ethFSM.updateCollateralPrice(initETHUSDPrice / 30); + oracleRelayer.updateCollateralPrice("eth"); + + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); + assertEq(raiWETHPair.balanceOf(address(saviour)), lpTokenAmount); + assertTrue(saviour.canSave("eth", safeHandler)); + + liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(100000 ether)); + liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); + + uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); + uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); + + uint auction = liquidationEngine.liquidateSAFE("eth", safeHandler); + (uint256 sysCoinReserve, uint256 collateralReserve) = saviour.underlyingReserves(safeHandler); + + assertEq(auction, 0); + assertTrue( + sysCoinReserve > 0 || + collateralReserve > 0 + ); + assertTrue( + systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance > 0 || + weth.balanceOf(address(this)) - preSaveWETHKeeperBalance > 0 + ); + assertTrue(raiWETHPair.balanceOf(address(saviour)) < lpTokenAmount); + assertEq(raiWETHPair.balanceOf(address(liquidityManager)), 0); + assertEq(saviour.lpTokenCover(safeHandler), 0); + + (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", safeHandler); + (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("eth"); + uint256 cRatio = lockedCollateral * ray(ethFSM.read()) * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10 ** 27); + assertTrue(cRatio == desiredCRatio || cRatio == desiredCRatio - 1); + } + function default_second_save(uint256 safe, address safeHandler, uint desiredCRatio) internal { + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, desiredCRatio); + + ethMedian.updateCollateralPrice(initETHUSDPrice / 40); + ethFSM.updateCollateralPrice(initETHUSDPrice / 40); + oracleRelayer.updateCollateralPrice("eth"); + + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); + assertEq(raiWETHPair.balanceOf(address(saviour)), lpTokenAmount); + assertTrue(saviour.canSave("eth", safeHandler)); + + liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(111 ether)); + liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); + + uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); + uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); + (uint256 oldSysCoinReserve, uint256 oldCollateralReserve) = saviour.underlyingReserves(safeHandler); + uint auction = liquidationEngine.liquidateSAFE("eth", safeHandler); + (uint256 sysCoinReserve, uint256 collateralReserve) = saviour.underlyingReserves(safeHandler); + + assertEq(auction, 0); + assertTrue( + sysCoinReserve > oldSysCoinReserve || + collateralReserve > oldCollateralReserve + ); + assertTrue( + systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance > 0 || + weth.balanceOf(address(this)) - preSaveWETHKeeperBalance > 0 + ); + assertTrue(raiWETHPair.balanceOf(address(saviour)) < lpTokenAmount); + assertEq(raiWETHPair.balanceOf(address(liquidityManager)), 0); + assertEq(saviour.lpTokenCover(safeHandler), 0); + + (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", safeHandler); + (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("eth"); + uint256 cRatio = lockedCollateral * ray(ethFSM.read()) * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10 ** 27); + assertTrue(cRatio == desiredCRatio || cRatio == desiredCRatio - 1); + } + function default_liquidate_safe(address safeHandler) internal { + liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(100000 ether)); + liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); + + uint auction = liquidationEngine.liquidateSAFE("eth", safeHandler); + // the full SAFE is liquidated + (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", me); + assertEq(lockedCollateral, 0); + assertEq(generatedDebt, 0); + // all debt goes to the accounting engine + assertTrue(accountingEngine.totalQueuedDebt() > 0); + // auction is for all collateral + (,uint amountToSell,,,,,, uint256 amountToRaise) = collateralAuctionHouse.bids(auction); + assertEq(amountToSell, defaultCollateralAmount); + assertEq(amountToRaise, rad(1100 ether)); + } + function default_create_liquidatable_position_deposit_cover(uint256 desiredCRatio, uint256 liquidatableCollateralPrice) + internal returns (address) { + // Create position + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, desiredCRatio); + assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); + + // Change oracle price + ethMedian.updateCollateralPrice(liquidatableCollateralPrice); + ethFSM.updateCollateralPrice(liquidatableCollateralPrice); + oracleRelayer.updateCollateralPrice("eth"); + + // Deposit cover + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); + assertEq(raiWETHPair.balanceOf(address(saviour)), lpTokenAmount); + assertEq(saviour.lpTokenCover(safeHandler), lpTokenAmount); + + return safeHandler; + } + function default_create_position_deposit_cover() internal returns (uint, address) { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + // Deposit cover + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); + assertEq(raiWETHPair.balanceOf(address(saviour)), lpTokenAmount); + assertEq(saviour.lpTokenCover(safeHandler), lpTokenAmount); + + return (safe, safeHandler); + } + function default_modify_collateralization(uint256 safe, address safeHandler) internal { + weth.approve(address(collateralJoin), uint(-1)); + collateralJoin.join(address(safeHandler), defaultTokenAmount); + alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 10)); + } + + // --- Tests --- + function test_setup() public { + assertEq(saviour.authorizedAccounts(address(this)), 1); + assertTrue(saviour.isSystemCoinToken0() == isSystemCoinToken0); + assertEq(saviour.minKeeperPayoutValue(), minKeeperPayoutValue); + assertEq(saviour.restrictUsage(), 0); + + assertEq(address(saviour.coinJoin()), address(coinJoin)); + assertEq(address(saviour.collateralJoin()), address(collateralJoin)); + assertEq(address(saviour.cRatioSetter()), address(cRatioSetter)); + assertEq(address(saviour.liquidationEngine()), address(liquidationEngine)); + assertEq(address(saviour.oracleRelayer()), address(oracleRelayer)); + assertEq(address(saviour.systemCoinOrcl()), address(systemCoinOracle)); + assertEq(address(saviour.systemCoin()), address(systemCoin)); + assertEq(address(saviour.safeEngine()), address(safeEngine)); + assertEq(address(saviour.safeManager()), address(safeManager)); + assertEq(address(saviour.saviourRegistry()), address(saviourRegistry)); + assertEq(address(saviour.liquidityManager()), address(liquidityManager)); + assertEq(address(saviour.lpToken()), address(raiWETHPair)); + assertEq(address(saviour.collateralToken()), address(weth)); + } + function test_modify_uints() public { + saviour.modifyParameters("minKeeperPayoutValue", 5); + saviour.modifyParameters("restrictUsage", 1); + + assertEq(saviour.minKeeperPayoutValue(), 5); + assertEq(saviour.restrictUsage(), 1); + } + function testFail_modify_uint_unauthed() public { + alice.doModifyParameters(saviour, "minKeeperPayoutValue", 5); + } + function test_modify_addresses() public { + saviour.modifyParameters("systemCoinOrcl", address(systemCoinOracle)); + saviour.modifyParameters("oracleRelayer", address(oracleRelayer)); + saviour.modifyParameters("liquidityManager", address(liquidityManager)); + + assertEq(address(saviour.liquidityManager()), address(liquidityManager)); + assertEq(address(saviour.oracleRelayer()), address(oracleRelayer)); + assertEq(address(saviour.systemCoinOrcl()), address(systemCoinOracle)); + } + function testFail_modify_address_unauthed() public { + alice.doModifyParameters(saviour, "systemCoinOrcl", address(systemCoinOracle)); + } + function testFail_deposit_liq_engine_not_approved() public { + liquidationEngine.disconnectSAFESaviour(address(saviour)); + + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), 1, lpTokenAmount); + } + function testFail_deposit_null_lp_token_amount() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), 1, 0); + } + function testFail_deposit_inexistent_safe() public { + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), 1, lpTokenAmount); + } + function test_deposit_twice() public { + uint256 initialLPSupply = raiWETHPair.totalSupply(); + + (uint safe, address safeHandler) = default_create_position_deposit_cover(); + + // Second deposit + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); + + // Checks + assertTrue(raiWETHPair.balanceOf(address(saviour)) > 0 && saviour.lpTokenCover(safeHandler) > 0); + assertEq(saviour.lpTokenCover(safeHandler), raiWETHPair.totalSupply() - initialLPSupply); + assertEq(raiWETHPair.balanceOf(address(saviour)), raiWETHPair.totalSupply() - initialLPSupply); + } + function test_deposit_after_everything_withdrawn() public { + (uint safe, address safeHandler) = default_create_position_deposit_cover(); + + // Withdraw + uint256 currentLPBalanceAlice = raiWETHPair.balanceOf(address(alice)); + uint256 currentLPBalanceSaviour = raiWETHPair.balanceOf(address(saviour)); + alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler), address(alice)); + + // Checks + assertEq(raiWETHPair.balanceOf(address(alice)), currentLPBalanceAlice + currentLPBalanceSaviour); + assertTrue(raiWETHPair.balanceOf(address(saviour)) == 0 && saviour.lpTokenCover(safeHandler) == 0); + + // Deposit again + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, currentLPBalanceSaviour); + + // Checks + assertTrue(raiWETHPair.balanceOf(address(saviour)) > 0 && saviour.lpTokenCover(safeHandler) > 0); + assertEq(saviour.lpTokenCover(safeHandler), currentLPBalanceSaviour); + assertEq(raiWETHPair.balanceOf(address(saviour)), currentLPBalanceSaviour); + assertEq(raiWETHPair.balanceOf(address(alice)), currentLPBalanceAlice); + } + function testFail_withdraw_unauthorized() public { + (uint safe, ) = default_create_position_deposit_cover(); + + // Withdraw by unauthed + FakeUser bob = new FakeUser(); + bob.doWithdraw(saviour, safe, raiWETHPair.balanceOf(address(saviour)), address(bob)); + } + function testFail_withdraw_more_than_deposited() public { + (uint safe, address safeHandler) = default_create_position_deposit_cover(); + uint256 currentLPBalance = raiWETHPair.balanceOf(address(this)); + alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler) + 1, address(this)); + } + function testFail_withdraw_null() public { + (uint safe, address safeHandler) = default_create_position_deposit_cover(); + alice.doWithdraw(saviour, safe, 0, address(this)); + } + function test_withdraw() public { + (uint safe, address safeHandler) = default_create_position_deposit_cover(); + + // Withdraw + uint256 currentLPBalanceAlice = raiWETHPair.balanceOf(address(alice)); + uint256 currentLPBalanceSaviour = raiWETHPair.balanceOf(address(saviour)); + alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler), address(alice)); + + // Checks + assertEq(raiWETHPair.balanceOf(address(alice)), currentLPBalanceAlice + currentLPBalanceSaviour); + assertTrue(raiWETHPair.balanceOf(address(saviour)) == 0 && saviour.lpTokenCover(safeHandler) == 0); + } + function test_withdraw_twice() public { + (uint safe, address safeHandler) = default_create_position_deposit_cover(); + + // Withdraw once + uint256 currentLPBalanceAlice = raiWETHPair.balanceOf(address(alice)); + uint256 currentLPBalanceSaviour = raiWETHPair.balanceOf(address(saviour)); + alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler) / 2, address(alice)); + + // Checks + assertEq(raiWETHPair.balanceOf(address(alice)), currentLPBalanceAlice + currentLPBalanceSaviour / 2); + assertTrue(raiWETHPair.balanceOf(address(saviour)) == currentLPBalanceSaviour / 2 && saviour.lpTokenCover(safeHandler) == currentLPBalanceSaviour / 2); + + // Withdraw again + alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler), address(alice)); + + // Checks + assertEq(raiWETHPair.balanceOf(address(alice)), currentLPBalanceAlice + currentLPBalanceSaviour); + assertTrue(raiWETHPair.balanceOf(address(saviour)) == 0 && saviour.lpTokenCover(safeHandler) == 0); + } + function test_withdraw_custom_dst() public { + (uint safe, address safeHandler) = default_create_position_deposit_cover(); + + // Withdraw + uint256 currentLPBalanceAlice = raiWETHPair.balanceOf(address(0xb1)); + uint256 currentLPBalanceSaviour = raiWETHPair.balanceOf(address(saviour)); + alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler), address(0xb1)); + + // Checks + assertEq(raiWETHPair.balanceOf(address(0xb1)), currentLPBalanceSaviour); + assertEq(raiWETHPair.balanceOf(address(alice)), currentLPBalanceAlice); + assertTrue(raiWETHPair.balanceOf(address(saviour)) == 0 && saviour.lpTokenCover(safeHandler) == 0); + } + function test_tokenAmountUsedToSave() public { + (uint safe, address safeHandler) = default_create_position_deposit_cover(); + + assertEq(saviour.lpTokenCover(safeHandler), saviour.tokenAmountUsedToSave("eth", safeHandler)); + } + function test_getCollateralPrice_zero_price() public { + ethFSM.updateCollateralPrice(0); + assertEq(saviour.getCollateralPrice(), 0); + } + function test_getCollateralPrice_invalid() public { + ethFSM.changeValidity(); + assertEq(saviour.getCollateralPrice(), 0); + } + function test_getCollateralPrice_null_fsm() public { + oracleRelayer.modifyParameters("eth", "orcl", address(0)); + assertEq(saviour.getCollateralPrice(), 0); + } + function test_getCollateralPrice() public { + assertEq(saviour.getCollateralPrice(), initETHUSDPrice); + } + function test_getSystemCoinMarketPrice_invalid() public { + systemCoinOracle.changeValidity(); + assertEq(saviour.getSystemCoinMarketPrice(), 0); + } + function test_getSystemCoinMarketPrice_null_price() public { + systemCoinOracle.updateCollateralPrice(0); + assertEq(saviour.getSystemCoinMarketPrice(), 0); + } + function test_getSystemCoinMarketPrice() public { + assertEq(saviour.getSystemCoinMarketPrice(), initRAIUSDPrice); + } + function test_getTargetCRatio_inexistent_handler() public { + assertEq(saviour.getTargetCRatio(address(0x1)), defaultDesiredCollateralizationRatio); + } + function test_getTargetCRatio_no_custom_desired_ratio() public { + (uint safe, address safeHandler) = default_create_position_deposit_cover(); + assertEq(saviour.getTargetCRatio(safeHandler), defaultDesiredCollateralizationRatio); + } + function test_getTargetCRatio_custom_desired_ratio() public { + (uint safe, address safeHandler) = default_create_position_deposit_cover(); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, defaultDesiredCollateralizationRatio * 2); + assertEq(saviour.getTargetCRatio(safeHandler), defaultDesiredCollateralizationRatio * 2); + } + function test_getLPUnderlying_inexistent_handler() public { + (uint sysCoins, uint collateral) = saviour.getLPUnderlying(address(0x1)); + assertEq(sysCoins, collateral); + assertEq(sysCoins, 0); + } + function test_getLPUnderlying() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); + + (uint sysCoins, uint collateral) = saviour.getLPUnderlying(safeHandler); + assertEq(sysCoins, initRAIETHPairLiquidity * defaultLiquidityMultiplier); + assertEq(collateral, initETHRAIPairLiquidity * defaultLiquidityMultiplier); + } + function test_getTokensForSaving_no_cover() public { + (uint sysCoins, uint collateral) = saviour.getTokensForSaving(address(0x1), oracleRelayer.redemptionPrice()); + + assertEq(sysCoins, collateral); + assertEq(sysCoins, 0); + } + function test_getTokensForSaving_null_redemption() public { + address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); + (uint sysCoins, uint collateral) = saviour.getTokensForSaving(safeHandler, 0); + + assertEq(sysCoins, collateral); + assertEq(sysCoins, 0); + } + function test_getTokensForSaving_null_collateral_price() public { + address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); + ethFSM.updateCollateralPrice(0); + (uint sysCoins, uint collateral) = saviour.getTokensForSaving(safeHandler, oracleRelayer.redemptionPrice()); + + assertEq(sysCoins, collateral); + assertEq(sysCoins, 0); + } + function test_getTokensForSaving_save_only_with_sys_coins() public { + address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); + (uint sysCoins, uint collateral) = saviour.getTokensForSaving(safeHandler, oracleRelayer.redemptionPrice()); + + assertTrue(sysCoins > 0); + assertEq(collateral, 0); + } + function test_getTokensForSaving_both_tokens_used() public { + // Create position + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 200); + assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); + + // Change oracle price + ethMedian.updateCollateralPrice(initETHUSDPrice / 3); + ethFSM.updateCollateralPrice(initETHUSDPrice / 3); + oracleRelayer.updateCollateralPrice("eth"); + + // Deposit cover + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity * 2, initETHRAIPairLiquidity * 2 + ); + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); + + (uint sysCoins, uint collateral) = saviour.getTokensForSaving(safeHandler, oracleRelayer.redemptionPrice()); + + assertTrue(sysCoins > 0); + assertTrue(collateral > 0); + } + function test_getTokensForSaving_not_enough_lp_collateral() public { + // Create position + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + + weth.approve(address(collateralJoin), uint(-1)); + collateralJoin.join(address(safeHandler), defaultTokenAmount); + alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 10)); + + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 155); + assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); + + // Change oracle price + ethMedian.updateCollateralPrice(initETHUSDPrice / 30); + ethFSM.updateCollateralPrice(initETHUSDPrice / 30); + oracleRelayer.updateCollateralPrice("eth"); + + // Deposit cover + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity / 5, initETHRAIPairLiquidity / 5 + ); + + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount / 10); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount / 10); + + (uint sysCoins, uint collateral) = saviour.getTokensForSaving(safeHandler, oracleRelayer.redemptionPrice()); + + assertEq(sysCoins, 0); + assertEq(collateral, 0); + } + function test_getKeeperPayoutTokens_null_collateral_price() public { + address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); + ethFSM.updateCollateralPrice(0); + + (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), 0, 0); + + assertEq(sysCoins, 0); + assertEq(collateral, 0); + } + function test_getKeeperPayoutTokens_null_sys_coin_price() public { + address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); + systemCoinOracle.updateCollateralPrice(0); + + (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), 0, 0); + + assertEq(sysCoins, 0); + assertEq(collateral, 0); + } + function test_getKeeperPayoutTokens_only_sys_coins_used() public { + address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); + (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), 0, 0); + + assertEq(sysCoins, minKeeperPayoutValue * 10 ** 18 / systemCoinOracle.read()); + assertEq(collateral, 0); + } + function test_getKeeperPayoutTokens_only_collateral_used() public { + (, address safeHandler) = default_create_position_deposit_cover(); + (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), uint(-1), 0); + + assertEq(sysCoins, 0); + assertEq(collateral, minKeeperPayoutValue * 10 ** 18 / ethFSM.read()); + } + function test_getKeeperPayoutTokens_both_tokens_used() public { + (, address safeHandler) = default_create_position_deposit_cover(); + (uint underlyingSysCoins, ) = saviour.getLPUnderlying(safeHandler); + + (uint sysCoins, uint collateral) = + saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), underlyingSysCoins - (minKeeperPayoutValue * 10 ** 18 / (systemCoinOracle.read() * 2)), 0); + + assertEq(sysCoins, (minKeeperPayoutValue * 10 ** 18 / (systemCoinOracle.read() * 2))); + assertEq(collateral, 2 ether); + } + function test_getKeeperPayoutTokens_not_enough_tokens_to_pay() public { + saviour.modifyParameters("minKeeperPayoutValue", 10000000 ether); + + (, address safeHandler) = default_create_position_deposit_cover(); + (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), 0, 0); + + assertEq(sysCoins, 0); + assertEq(collateral, 0); + } + function test_canSave_cannot_save_safe() public { + // Create position + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + + weth.approve(address(collateralJoin), uint(-1)); + collateralJoin.join(address(safeHandler), defaultTokenAmount); + alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 10)); + + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 155); + assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); + + // Change oracle price + ethMedian.updateCollateralPrice(initETHUSDPrice / 30); + ethFSM.updateCollateralPrice(initETHUSDPrice / 30); + oracleRelayer.updateCollateralPrice("eth"); + + // Deposit cover + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity / 5, initETHRAIPairLiquidity / 5 + ); + + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount / 10); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount / 10); + assertTrue(!saviour.canSave("eth", safeHandler)); + } + function test_canSave_cannot_pay_keeper() public { + saviour.modifyParameters("minKeeperPayoutValue", 10000000 ether); + address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); + assertTrue(!saviour.canSave("eth", safeHandler)); + } + function test_canSave_both_tokens_used() public { + // Create position + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 200); + assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); + + // Change oracle price + ethMedian.updateCollateralPrice(initETHUSDPrice / 3); + ethFSM.updateCollateralPrice(initETHUSDPrice / 3); + oracleRelayer.updateCollateralPrice("eth"); + + // Deposit cover + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity * 2, initETHRAIPairLiquidity * 2 + ); + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); + saviour.modifyParameters("minKeeperPayoutValue", 50 ether); + + assertTrue(saviour.canSave("eth", safeHandler)); + } + function test_canSave() public { + address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); + assertTrue(saviour.canSave("eth", safeHandler)); + } + function testFail_saveSAFE_invalid_caller() public { + address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); + saviour.saveSAFE(address(this), "eth", safeHandler); + } + function test_saveSAFE_no_cover() public { + address safeHandler = default_create_liquidatable_position(200, initETHUSDPrice / 30); + default_liquidate_safe(safeHandler); + } + function test_saveSAFE_cannot_save_safe() public { + // Create position + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + + weth.approve(address(collateralJoin), uint(-1)); + collateralJoin.join(address(safeHandler), defaultTokenAmount); + alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 10)); + + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 155); + assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); + + // Change oracle price + ethMedian.updateCollateralPrice(initETHUSDPrice / 30); + ethFSM.updateCollateralPrice(initETHUSDPrice / 30); + oracleRelayer.updateCollateralPrice("eth"); + + // Deposit cover + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity / 5, initETHRAIPairLiquidity / 5 + ); + + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount / 10); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount / 10); + + default_liquidate_safe(safeHandler); + } + function test_saveSAFE_cannot_pay_keeper() public { + address safeHandler = default_create_liquidatable_position(200, initETHUSDPrice / 30); + saviour.modifyParameters("minKeeperPayoutValue", 10000000 ether); + default_liquidate_safe(safeHandler); + } + function test_saveSAFE() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_save(safe, safeHandler, 200); + } + function test_saveSAFE_accumulate_rate() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + + // Warp and save + hevm.warp(now + 2 days); + taxCollector.taxSingle("eth"); + + weth.approve(address(collateralJoin), uint(-1)); + collateralJoin.join(address(safeHandler), defaultTokenAmount); + alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 5)); + + alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 200); + assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); + + ethMedian.updateCollateralPrice(initETHUSDPrice / 10); + ethFSM.updateCollateralPrice(initETHUSDPrice / 10); + oracleRelayer.updateCollateralPrice("eth"); + + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity * defaultLiquidityMultiplier, initETHRAIPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); + assertEq(raiWETHPair.balanceOf(address(saviour)), lpTokenAmount); + assertTrue(saviour.canSave("eth", safeHandler)); + + liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(100000 ether)); + liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); + + uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); + uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); + + uint auction = liquidationEngine.liquidateSAFE("eth", safeHandler); + (uint256 sysCoinReserve, uint256 collateralReserve) = saviour.underlyingReserves(safeHandler); + + assertEq(auction, 0); + assertTrue( + sysCoinReserve > 0 || + collateralReserve > 0 + ); + assertTrue( + systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance > 0 || + weth.balanceOf(address(this)) - preSaveWETHKeeperBalance > 0 + ); + assertTrue(raiWETHPair.balanceOf(address(saviour)) < lpTokenAmount); + assertEq(raiWETHPair.balanceOf(address(liquidityManager)), 0); + assertEq(saviour.lpTokenCover(safeHandler), 0); + + (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", safeHandler); + (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("eth"); + assertEq(lockedCollateral * ray(ethFSM.read()) * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10 ** 27), 200); + } + function test_saveSAFE_both_tokens() public { + // Create position + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 200); + assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); + + // Change oracle price + ethMedian.updateCollateralPrice(initETHUSDPrice / 3); + ethFSM.updateCollateralPrice(initETHUSDPrice / 3); + oracleRelayer.updateCollateralPrice("eth"); + + // Deposit cover + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity * 2, initETHRAIPairLiquidity * 2 + ); + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); + saviour.modifyParameters("minKeeperPayoutValue", 50 ether); + + assertTrue(saviour.canSave("eth", safeHandler)); + + liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(100000 ether)); + liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); + + uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); + uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); + + uint auction = liquidationEngine.liquidateSAFE("eth", safeHandler); + (uint256 sysCoinReserve, uint256 collateralReserve) = saviour.underlyingReserves(safeHandler); + + assertEq(auction, 0); + assertTrue( + sysCoinReserve > 0 || + collateralReserve > 0 + ); + assertTrue( + systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance > 0 || + weth.balanceOf(address(this)) - preSaveWETHKeeperBalance > 0 + ); + assertTrue(raiWETHPair.balanceOf(address(saviour)) < lpTokenAmount); + assertEq(raiWETHPair.balanceOf(address(liquidityManager)), 0); + assertEq(saviour.lpTokenCover(safeHandler), 0); + + (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", safeHandler); + assertEq(lockedCollateral * ray(ethFSM.read()) * 100 / (generatedDebt * oracleRelayer.redemptionPrice()), 199); + } + function test_saveSAFE_both_tokens_accumulate_rate() public { + // Create position + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + + // Warp, mint and save + hevm.warp(now + 1 days); + taxCollector.taxSingle("eth"); + + weth.approve(address(collateralJoin), uint(-1)); + collateralJoin.join(address(safeHandler), defaultTokenAmount); + alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 8)); + + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 250); + assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); + + // Change oracle price + ethMedian.updateCollateralPrice(initETHUSDPrice / 4); + ethFSM.updateCollateralPrice(initETHUSDPrice / 4); + oracleRelayer.updateCollateralPrice("eth"); + + // Deposit cover + uint256 lpTokenAmount = raiWETHPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(weth), initRAIETHPairLiquidity * 2, initETHRAIPairLiquidity * 2 + ); + lpTokenAmount = sub(raiWETHPair.balanceOf(address(this)), lpTokenAmount); + raiWETHPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiWETHPair)), safe, lpTokenAmount); + saviour.modifyParameters("minKeeperPayoutValue", 50 ether); + + assertTrue(saviour.canSave("eth", safeHandler)); + + liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(100000 ether)); + liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); + + uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); + uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); + + (uint sysCoinsForSaving, uint collateralForSaving) = saviour.getTokensForSaving(safeHandler, oracleRelayer.redemptionPrice()); + assertTrue(collateralForSaving > 0); + assertTrue(sysCoinsForSaving > 0); + + uint auction = liquidationEngine.liquidateSAFE("eth", safeHandler); + (uint256 sysCoinReserve, uint256 collateralReserve) = saviour.underlyingReserves(safeHandler); + + assertEq(auction, 0); + assertTrue( + sysCoinReserve > 0 || + collateralReserve > 0 + ); + assertTrue( + systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance > 0 || + weth.balanceOf(address(this)) - preSaveWETHKeeperBalance > 0 + ); + assertTrue(raiWETHPair.balanceOf(address(saviour)) < lpTokenAmount); + assertEq(raiWETHPair.balanceOf(address(liquidityManager)), 0); + assertEq(saviour.lpTokenCover(safeHandler), 0); + + (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", safeHandler); + (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("eth"); + assertEq(lockedCollateral * ray(ethFSM.read()) * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10 ** 27), 249); + } + function test_saveSAFE_twice() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_save(safe, safeHandler, 155); + + hevm.warp(now + saviourRegistry.saveCooldown() + 1); + default_second_save(safe, safeHandler, 200); + } + function testFail_saveSAFE_withdraw_cover() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_save(safe, safeHandler, 155); + + alice.doWithdraw(saviour, safe, 1, address(this)); + } + function test_saveSAFE_get_reserves() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_save(safe, safeHandler, 155); + + uint256 oldSysCoinBalance = systemCoin.balanceOf(address(alice)); + uint256 oldCollateralBalance = weth.balanceOf(address(alice)); + + (uint sysCoinReserve, uint collateralReserve) = saviour.underlyingReserves(safeHandler); + + alice.doGetReserves(saviour, safe, address(alice)); + assertTrue(systemCoin.balanceOf(address(alice)) - sysCoinReserve == oldSysCoinBalance); + assertTrue(weth.balanceOf(address(alice)) - collateralReserve == oldCollateralBalance); + + assertEq(systemCoin.balanceOf(address(saviour)), 0); + assertEq(weth.balanceOf(address(saviour)), 0); + } + function testFail_save_twice_without_waiting() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_save(safe, safeHandler, 155); + default_second_save(safe, safeHandler, 200); + } + function test_saveSAFE_get_reserves_twice() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_save(safe, safeHandler, 155); + alice.doGetReserves(saviour, safe, address(alice)); + + hevm.warp(now + saviourRegistry.saveCooldown() + 1); + default_second_save(safe, safeHandler, 200); + + uint256 oldSysCoinBalance = systemCoin.balanceOf(address(0x1)); + uint256 oldCollateralBalance = weth.balanceOf(address(0x1)); + + (uint sysCoinReserve, uint collateralReserve) = saviour.underlyingReserves(safeHandler); + + alice.doGetReserves(saviour, safe, address(0x1)); + assertTrue(systemCoin.balanceOf(address(0x1)) - sysCoinReserve == oldSysCoinBalance); + assertTrue(weth.balanceOf(address(0x1)) - collateralReserve == oldCollateralBalance); + + assertEq(systemCoin.balanceOf(address(saviour)), 0); + assertEq(weth.balanceOf(address(saviour)), 0); + } + function testFail_getReserves_invalid_caller() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_save(safe, safeHandler, 155); + + saviour.getReserves(safe, address(alice)); + } +} diff --git a/src/test/SAFESaviourRegistry.t.sol b/src/test/SAFESaviourRegistry.t.sol index 6249380..429f405 100644 --- a/src/test/SAFESaviourRegistry.t.sol +++ b/src/test/SAFESaviourRegistry.t.sol @@ -1,93 +1,93 @@ -pragma solidity ^0.6.7; - -import "ds-test/test.sol"; - -import "../SAFESaviourRegistry.sol"; - -abstract contract Hevm { - function warp(uint256) virtual public; -} -contract Usr { - function toggleSaviour(address registry, address saviour) external { - SAFESaviourRegistry(registry).toggleSaviour(saviour); - } - function markSave(address registry, bytes32 collateralType, address safeHandler) external { - SAFESaviourRegistry(registry).markSave(collateralType, safeHandler); - } -} - -contract SAFESaviourRegistryTest is DSTest { - Hevm hevm; - - SAFESaviourRegistry registry; - Usr alice; - Usr bob; - - uint256 saveCooldown = 1 hours; - - uint constant HUNDRED = 10 ** 2; - uint constant RAY = 10 ** 27; - - function ray(uint wad) internal pure returns (uint) { - return wad * 10 ** 9; - } - function rad(uint wad) internal pure returns (uint) { - return wad * RAY; - } - - function setUp() public { - hevm = Hevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); - hevm.warp(604411200); - - alice = new Usr(); - bob = new Usr(); - - registry = new SAFESaviourRegistry(saveCooldown); - } - - function test_setup() public { - assertEq(registry.saveCooldown(), saveCooldown); - assertEq(registry.authorizedAccounts(address(this)), 1); - } - function test_modify_save_cooldown() public { - registry.modifyParameters("saveCooldown", 1 days); - assertEq(registry.saveCooldown(), 1 days); - } - function test_toggle_saviour() public { - assertEq(registry.saviours(address(alice)), 0); - registry.toggleSaviour(address(alice)); - assertEq(registry.saviours(address(alice)), 1); - registry.toggleSaviour(address(alice)); - assertEq(registry.saviours(address(alice)), 0); - } - function testFail_toggle_saviour_by_unauthed() public { - alice.toggleSaviour(address(registry), address(bob)); - } - function testFail_mark_save_not_saviour() public { - alice.markSave(address(registry), "ETH-A", address(0x1)); - } - function testFail_mark_save_as_saviour_without_waiting() public { - registry.toggleSaviour(address(alice)); - alice.markSave(address(registry), "ETH-A", address(0x1)); - alice.markSave(address(registry), "ETH-A", address(0x1)); - } - function test_mark_save_as_saviour() public { - registry.toggleSaviour(address(alice)); - alice.markSave(address(registry), "ETH-A", address(0x1)); - assertEq(registry.lastSaveTime("ETH-A", address(0x1)), now); - } - function test_mark_save_as_saviour_after_waiting() public { - registry.toggleSaviour(address(alice)); - alice.markSave(address(registry), "ETH-A", address(0x1)); - hevm.warp(now + registry.saveCooldown() + 1); - alice.markSave(address(registry), "ETH-A", address(0x1)); - assertEq(registry.lastSaveTime("ETH-A", address(0x1)), now); - } - function test_mark_save_as_saviour_same_handlers_different_collaterals() public { - registry.toggleSaviour(address(alice)); - alice.markSave(address(registry), "ETH-A", address(0x1)); - alice.markSave(address(registry), "ETH-B", address(0x1)); - assertEq(registry.lastSaveTime("ETH-B", address(0x1)), now); - assertEq(registry.lastSaveTime("ETH-A", address(0x1)), now); - } -} +pragma solidity ^0.6.7; + +import "ds-test/test.sol"; + +import "../SAFESaviourRegistry.sol"; + +abstract contract Hevm { + function warp(uint256) virtual public; +} +contract Usr { + function toggleSaviour(address registry, address saviour) external { + SAFESaviourRegistry(registry).toggleSaviour(saviour); + } + function markSave(address registry, bytes32 collateralType, address safeHandler) external { + SAFESaviourRegistry(registry).markSave(collateralType, safeHandler); + } +} + +contract SAFESaviourRegistryTest is DSTest { + Hevm hevm; + + SAFESaviourRegistry registry; + Usr alice; + Usr bob; + + uint256 saveCooldown = 1 hours; + + uint constant HUNDRED = 10 ** 2; + uint constant RAY = 10 ** 27; + + function ray(uint wad) internal pure returns (uint) { + return wad * 10 ** 9; + } + function rad(uint wad) internal pure returns (uint) { + return wad * RAY; + } + + function setUp() public { + hevm = Hevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + hevm.warp(604411200); + + alice = new Usr(); + bob = new Usr(); + + registry = new SAFESaviourRegistry(saveCooldown); + } + + function test_setup() public { + assertEq(registry.saveCooldown(), saveCooldown); + assertEq(registry.authorizedAccounts(address(this)), 1); + } + function test_modify_save_cooldown() public { + registry.modifyParameters("saveCooldown", 1 days); + assertEq(registry.saveCooldown(), 1 days); + } + function test_toggle_saviour() public { + assertEq(registry.saviours(address(alice)), 0); + registry.toggleSaviour(address(alice)); + assertEq(registry.saviours(address(alice)), 1); + registry.toggleSaviour(address(alice)); + assertEq(registry.saviours(address(alice)), 0); + } + function testFail_toggle_saviour_by_unauthed() public { + alice.toggleSaviour(address(registry), address(bob)); + } + function testFail_mark_save_not_saviour() public { + alice.markSave(address(registry), "ETH-A", address(0x1)); + } + function testFail_mark_save_as_saviour_without_waiting() public { + registry.toggleSaviour(address(alice)); + alice.markSave(address(registry), "ETH-A", address(0x1)); + alice.markSave(address(registry), "ETH-A", address(0x1)); + } + function test_mark_save_as_saviour() public { + registry.toggleSaviour(address(alice)); + alice.markSave(address(registry), "ETH-A", address(0x1)); + assertEq(registry.lastSaveTime("ETH-A", address(0x1)), now); + } + function test_mark_save_as_saviour_after_waiting() public { + registry.toggleSaviour(address(alice)); + alice.markSave(address(registry), "ETH-A", address(0x1)); + hevm.warp(now + registry.saveCooldown() + 1); + alice.markSave(address(registry), "ETH-A", address(0x1)); + assertEq(registry.lastSaveTime("ETH-A", address(0x1)), now); + } + function test_mark_save_as_saviour_same_handlers_different_collaterals() public { + registry.toggleSaviour(address(alice)); + alice.markSave(address(registry), "ETH-A", address(0x1)); + alice.markSave(address(registry), "ETH-B", address(0x1)); + assertEq(registry.lastSaveTime("ETH-B", address(0x1)), now); + assertEq(registry.lastSaveTime("ETH-A", address(0x1)), now); + } +} diff --git a/src/test/SaviourCRatioSetter.t.sol b/src/test/SaviourCRatioSetter.t.sol index afa7f33..b307814 100644 --- a/src/test/SaviourCRatioSetter.t.sol +++ b/src/test/SaviourCRatioSetter.t.sol @@ -1,120 +1,120 @@ -pragma solidity 0.6.7; - -import "ds-test/test.sol"; - -import {SAFEEngine} from 'geb/SAFEEngine.sol'; -import {OracleRelayer} from 'geb/OracleRelayer.sol'; -import {GebSafeManager} from "geb-safe-manager/GebSafeManager.sol"; - -import "../SaviourCRatioSetter.sol"; - -abstract contract Hevm { - function warp(uint256) virtual public; -} - -contract User { - function modifyParameters(SaviourCRatioSetter setter, bytes32 parameter, address data) external { - setter.modifyParameters(parameter, data); - } - function setDefaultCRatio(SaviourCRatioSetter setter, bytes32 collateralType, uint256 cRatio) external { - setter.setDefaultCRatio(collateralType, cRatio); - } - function setMinDesiredCollateralizationRatio(SaviourCRatioSetter setter, bytes32 collateralType, uint256 cRatio) external { - setter.setMinDesiredCollateralizationRatio(collateralType, cRatio); - } -} - -contract SaviourCRatioSetterTest is DSTest { - Hevm hevm; - - User user; - - SAFEEngine safeEngine; - OracleRelayer oracleRelayer; - GebSafeManager safeManager; - SaviourCRatioSetter setter; - - function setUp() public { - hevm = Hevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); - hevm.warp(604411200); - - user = new User(); - - safeEngine = new SAFEEngine(); - oracleRelayer = new OracleRelayer(address(safeEngine)); - safeManager = new GebSafeManager(address(safeEngine)); - - oracleRelayer.modifyParameters("gold", "safetyCRatio", ray(1.5 ether)); - oracleRelayer.modifyParameters("gold", "liquidationCRatio", ray(1.5 ether)); - - setter = new SaviourCRatioSetter(address(oracleRelayer), address(safeManager)); - } - - function ray(uint wad) internal pure returns (uint) { - return wad * 10 ** 9; - } - - function test_setup() public { - assertEq(setter.authorizedAccounts(address(this)), 1); - assertEq(address(setter.oracleRelayer()), address(oracleRelayer)); - assertEq(address(setter.safeManager()), address(safeManager)); - } - function test_modifyParameters() public { - oracleRelayer = new OracleRelayer(address(safeEngine)); - setter.modifyParameters("oracleRelayer", address(oracleRelayer)); - assertEq(address(setter.oracleRelayer()), address(oracleRelayer)); - } - function testFail_modifyParameters_unauthorized() public { - oracleRelayer = new OracleRelayer(address(safeEngine)); - user.modifyParameters(setter, "oracleRelayer", address(oracleRelayer)); - } - function testFail_setDefaultRatio_unauthorized() public { - user.setDefaultCRatio(setter, "gold", 200); - } - function testFail_setDefaultRatio_inexistent_collateral() public { - setter.setDefaultCRatio("silver", 200); - } - function testFail_setDefaultRatio_low_proposed_cratio() public { - setter.setDefaultCRatio("gold", 140); - } - function testFail_setDefaultRatio_high_proposed_ratio() public { - setter.setDefaultCRatio("gold", setter.MAX_CRATIO() + 1); - } - function test_setDefaultRatio() public { - setter.setDefaultCRatio("gold", 200); - assertEq(setter.defaultDesiredCollateralizationRatios("gold"), 200); - } - function testFail_setMinCRatio_unauthorized() public { - user.setMinDesiredCollateralizationRatio(setter, "gold", 200); - } - function testFail_setMinCRatio_above_max() public { - setter.setMinDesiredCollateralizationRatio("gold", setter.MAX_CRATIO() + 1); - } - function test_setMinCRatio() public { - setter.setMinDesiredCollateralizationRatio("gold", 200); - assertEq(setter.minDesiredCollateralizationRatios("gold"), 200); - } - function testFail_setDesiredCRatio_unauthorized() public { - setter.setDesiredCollateralizationRatio("gold", 1, 200); - } - function testFail_setDesiredCRatio_proposed_ratio_smaller_than_scaled() public { - safeManager.openSAFE("gold", address(this)); - setter.setDesiredCollateralizationRatio("gold", 1, 140); - } - function testFail_setDesiredCRatio_proposed_ratio_smaller_than_min() public { - safeManager.openSAFE("gold", address(this)); - setter.setMinDesiredCollateralizationRatio("gold", 200); - setter.setDesiredCollateralizationRatio("gold", 1, 199); - } - function testFail_setDesiredCRatio_proposed_ratio_higher_than_max() public { - safeManager.openSAFE("gold", address(this)); - setter.setMinDesiredCollateralizationRatio("gold", 200); - setter.setDesiredCollateralizationRatio("gold", 1, setter.MAX_CRATIO() + 1); - } - function test_setDesiredCRatio() public { - safeManager.openSAFE("gold", address(this)); - setter.setMinDesiredCollateralizationRatio("gold", 200); - setter.setDesiredCollateralizationRatio("gold", 1, 200); - assertEq(setter.desiredCollateralizationRatios("gold", safeManager.safes(1)), 200); - } -} +pragma solidity 0.6.7; + +import "ds-test/test.sol"; + +import {SAFEEngine} from 'geb/SAFEEngine.sol'; +import {OracleRelayer} from 'geb/OracleRelayer.sol'; +import {GebSafeManager} from "geb-safe-manager/GebSafeManager.sol"; + +import "../SaviourCRatioSetter.sol"; + +abstract contract Hevm { + function warp(uint256) virtual public; +} + +contract User { + function modifyParameters(SaviourCRatioSetter setter, bytes32 parameter, address data) external { + setter.modifyParameters(parameter, data); + } + function setDefaultCRatio(SaviourCRatioSetter setter, bytes32 collateralType, uint256 cRatio) external { + setter.setDefaultCRatio(collateralType, cRatio); + } + function setMinDesiredCollateralizationRatio(SaviourCRatioSetter setter, bytes32 collateralType, uint256 cRatio) external { + setter.setMinDesiredCollateralizationRatio(collateralType, cRatio); + } +} + +contract SaviourCRatioSetterTest is DSTest { + Hevm hevm; + + User user; + + SAFEEngine safeEngine; + OracleRelayer oracleRelayer; + GebSafeManager safeManager; + SaviourCRatioSetter setter; + + function setUp() public { + hevm = Hevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + hevm.warp(604411200); + + user = new User(); + + safeEngine = new SAFEEngine(); + oracleRelayer = new OracleRelayer(address(safeEngine)); + safeManager = new GebSafeManager(address(safeEngine)); + + oracleRelayer.modifyParameters("gold", "safetyCRatio", ray(1.5 ether)); + oracleRelayer.modifyParameters("gold", "liquidationCRatio", ray(1.5 ether)); + + setter = new SaviourCRatioSetter(address(oracleRelayer), address(safeManager)); + } + + function ray(uint wad) internal pure returns (uint) { + return wad * 10 ** 9; + } + + function test_setup() public { + assertEq(setter.authorizedAccounts(address(this)), 1); + assertEq(address(setter.oracleRelayer()), address(oracleRelayer)); + assertEq(address(setter.safeManager()), address(safeManager)); + } + function test_modifyParameters() public { + oracleRelayer = new OracleRelayer(address(safeEngine)); + setter.modifyParameters("oracleRelayer", address(oracleRelayer)); + assertEq(address(setter.oracleRelayer()), address(oracleRelayer)); + } + function testFail_modifyParameters_unauthorized() public { + oracleRelayer = new OracleRelayer(address(safeEngine)); + user.modifyParameters(setter, "oracleRelayer", address(oracleRelayer)); + } + function testFail_setDefaultRatio_unauthorized() public { + user.setDefaultCRatio(setter, "gold", 200); + } + function testFail_setDefaultRatio_inexistent_collateral() public { + setter.setDefaultCRatio("silver", 200); + } + function testFail_setDefaultRatio_low_proposed_cratio() public { + setter.setDefaultCRatio("gold", 140); + } + function testFail_setDefaultRatio_high_proposed_ratio() public { + setter.setDefaultCRatio("gold", setter.MAX_CRATIO() + 1); + } + function test_setDefaultRatio() public { + setter.setDefaultCRatio("gold", 200); + assertEq(setter.defaultDesiredCollateralizationRatios("gold"), 200); + } + function testFail_setMinCRatio_unauthorized() public { + user.setMinDesiredCollateralizationRatio(setter, "gold", 200); + } + function testFail_setMinCRatio_above_max() public { + setter.setMinDesiredCollateralizationRatio("gold", setter.MAX_CRATIO() + 1); + } + function test_setMinCRatio() public { + setter.setMinDesiredCollateralizationRatio("gold", 200); + assertEq(setter.minDesiredCollateralizationRatios("gold"), 200); + } + function testFail_setDesiredCRatio_unauthorized() public { + setter.setDesiredCollateralizationRatio("gold", 1, 200); + } + function testFail_setDesiredCRatio_proposed_ratio_smaller_than_scaled() public { + safeManager.openSAFE("gold", address(this)); + setter.setDesiredCollateralizationRatio("gold", 1, 140); + } + function testFail_setDesiredCRatio_proposed_ratio_smaller_than_min() public { + safeManager.openSAFE("gold", address(this)); + setter.setMinDesiredCollateralizationRatio("gold", 200); + setter.setDesiredCollateralizationRatio("gold", 1, 199); + } + function testFail_setDesiredCRatio_proposed_ratio_higher_than_max() public { + safeManager.openSAFE("gold", address(this)); + setter.setMinDesiredCollateralizationRatio("gold", 200); + setter.setDesiredCollateralizationRatio("gold", 1, setter.MAX_CRATIO() + 1); + } + function test_setDesiredCRatio() public { + safeManager.openSAFE("gold", address(this)); + setter.setMinDesiredCollateralizationRatio("gold", 200); + setter.setDesiredCollateralizationRatio("gold", 1, 200); + assertEq(setter.desiredCollateralizationRatios("gold", safeManager.safes(1)), 200); + } +} diff --git a/src/test/SystemCoinUniswapV2SafeSaviour.t.sol b/src/test/SystemCoinUniswapV2SafeSaviour.t.sol index 9aabaca..82ac240 100644 --- a/src/test/SystemCoinUniswapV2SafeSaviour.t.sol +++ b/src/test/SystemCoinUniswapV2SafeSaviour.t.sol @@ -1,1318 +1,1318 @@ -pragma solidity 0.6.7; - -import "ds-test/test.sol"; -import "ds-weth/weth9.sol"; -import "ds-token/token.sol"; - -import {SAFEEngine} from 'geb/SAFEEngine.sol'; -import {Coin} from 'geb/Coin.sol'; -import {LiquidationEngine} from 'geb/LiquidationEngine.sol'; -import {AccountingEngine} from 'geb/AccountingEngine.sol'; -import {TaxCollector} from 'geb/TaxCollector.sol'; -import {BasicCollateralJoin, CoinJoin} from 'geb/BasicTokenAdapters.sol'; -import {OracleRelayer} from 'geb/OracleRelayer.sol'; -import {EnglishCollateralAuctionHouse} from 'geb/CollateralAuctionHouse.sol'; -import {GebSafeManager} from "geb-safe-manager/GebSafeManager.sol"; - -import {SaviourCRatioSetter} from "../SaviourCRatioSetter.sol"; -import {SAFESaviourRegistry} from "../SAFESaviourRegistry.sol"; - -import "../integrations/uniswap/uni-v2/UniswapV2Factory.sol"; -import "../integrations/uniswap/uni-v2/UniswapV2Pair.sol"; -import "../integrations/uniswap/uni-v2/UniswapV2Router02.sol"; - -import "../integrations/uniswap/swappers/UniswapV2Swapper.sol"; - -import "../integrations/uniswap/liquidity-managers/UniswapV2LiquidityManager.sol"; -import "../integrations/uniswap/liquidity-managers/UniswapV3LiquidityManager.sol"; - -import "../saviours/SystemCoinUniswapV2SafeSaviour.sol"; - -abstract contract Hevm { - function warp(uint256) virtual public; -} - -contract TestSAFEEngine is SAFEEngine { - uint256 constant RAY = 10 ** 27; - - constructor() public {} - - function mint(address usr, uint wad) public { - coinBalance[usr] += wad * RAY; - globalDebt += wad * RAY; - } - function balanceOf(address usr) public view returns (uint) { - return uint(coinBalance[usr] / RAY); - } -} - -// --- Median Contracts --- -contract MockMedianizer { - uint256 public price; - bool public validPrice; - uint public lastUpdateTime; - address public priceSource; - - constructor(uint256 price_, bool validPrice_) public { - price = price_; - validPrice = validPrice_; - lastUpdateTime = now; - } - function updatePriceSource(address priceSource_) external { - priceSource = priceSource_; - } - function changeValidity() external { - validPrice = !validPrice; - } - function updateCollateralPrice(uint256 price_) external { - price = price_; - lastUpdateTime = now; - } - function read() external view returns (uint256) { - return price; - } - function getResultWithValidity() external view returns (uint256, bool) { - return (price, validPrice); - } -} - -// Users -contract FakeUser { - function doModifyParameters( - SystemCoinUniswapV2SafeSaviour saviour, - bytes32 parameter, - uint256 data - ) public { - saviour.modifyParameters(parameter, data); - } - - function doModifyParameters( - SystemCoinUniswapV2SafeSaviour saviour, - bytes32 parameter, - address data - ) public { - saviour.modifyParameters(parameter, data); - } - - function doOpenSafe( - GebSafeManager manager, - bytes32 collateralType, - address usr - ) public returns (uint256) { - return manager.openSAFE(collateralType, usr); - } - - function doSafeAllow( - GebSafeManager manager, - uint safe, - address usr, - uint ok - ) public { - manager.allowSAFE(safe, usr, ok); - } - - function doHandlerAllow( - GebSafeManager manager, - address usr, - uint ok - ) public { - manager.allowHandler(usr, ok); - } - - function doTransferSAFEOwnership( - GebSafeManager manager, - uint safe, - address dst - ) public { - manager.transferSAFEOwnership(safe, dst); - } - - function doModifySAFECollateralization( - GebSafeManager manager, - uint safe, - int deltaCollateral, - int deltaDebt - ) public { - manager.modifySAFECollateralization(safe, deltaCollateral, deltaDebt); - } - - function doApproveSAFEModification( - SAFEEngine safeEngine, - address usr - ) public { - safeEngine.approveSAFEModification(usr); - } - - function doSAFEEngineModifySAFECollateralization( - SAFEEngine safeEngine, - bytes32 collateralType, - address safe, - address collateralSource, - address debtDst, - int deltaCollateral, - int deltaDebt - ) public { - safeEngine.modifySAFECollateralization(collateralType, safe, collateralSource, debtDst, deltaCollateral, deltaDebt); - } - - function doProtectSAFE( - GebSafeManager manager, - uint safe, - address liquidationEngine, - address saviour - ) public { - manager.protectSAFE(safe, liquidationEngine, saviour); - } - - function doDeposit( - SystemCoinUniswapV2SafeSaviour saviour, - DSToken lpToken, - uint256 safeID, - uint256 tokenAmount - ) public { - lpToken.approve(address(saviour), tokenAmount); - saviour.deposit(safeID, tokenAmount); - } - - function doWithdraw( - SystemCoinUniswapV2SafeSaviour saviour, - uint256 safeID, - uint256 lpTokenAmount, - address dst - ) public { - saviour.withdraw(safeID, lpTokenAmount, dst); - } - - function doGetReserves( - SystemCoinUniswapV2SafeSaviour saviour, - uint256 safeID, - address dst - ) public { - saviour.getReserves(safeID, dst); - } - - function doTransferInternalCoins( - GebSafeManager manager, - uint256 safe, - address dst, - uint256 amt - ) public { - manager.transferInternalCoins(safe, dst, amt); - } - - function doSetDesiredCollateralizationRatio( - SaviourCRatioSetter cRatioSetter, - bytes32 collateralType, - uint safe, - uint cRatio - ) public { - cRatioSetter.setDesiredCollateralizationRatio(collateralType, safe, cRatio); - } -} - -contract SystemCoinUniswapV2SafeSaviourTest is DSTest { - Hevm hevm; - - UniswapV2Factory uniswapFactory; - UniswapV2Router02 uniswapRouter; - UniswapV2LiquidityManager liquidityManager; - UniswapV2Swapper swapManager; - - UniswapV2Pair stableWETHPair; - UniswapV2Pair raiSTABLEPair; - - Coin systemCoin; - WETH9_ weth; - DSToken stableToken; - - TestSAFEEngine safeEngine; - AccountingEngine accountingEngine; - LiquidationEngine liquidationEngine; - OracleRelayer oracleRelayer; - TaxCollector taxCollector; - - BasicCollateralJoin collateralJoin; - - CoinJoin coinJoin; - CoinJoin systemCoinJoin; - - EnglishCollateralAuctionHouse collateralAuctionHouse; - - GebSafeManager safeManager; - - SystemCoinUniswapV2SafeSaviour saviour; - SaviourCRatioSetter cRatioSetter; - SAFESaviourRegistry saviourRegistry; - - MockMedianizer systemCoinOracle; - MockMedianizer ethFSM; - MockMedianizer ethMedian; - - FakeUser alice; - - address me; - - // Params - uint256 initTokenAmount = 1000000 ether; - uint256 initETHUSDPrice = 250 * 10 ** 18; - uint256 initRAIUSDPrice = 4.242 * 10 ** 18; - uint256 initSTABLEUSDPrice = 10 ** 18; - - uint256 initETHPairLiquidity = 5 ether; // 1250 USD - uint256 initRAIPairLiquidity = 294.672324375E18; // 1 RAI = 4.242 USD - uint256 initSTABLEPairLiquidity = 1250 ether; // 1250 USD - - // Saviour params - bool isSystemCoinToken0; - uint256 saveCooldown = 1 days; - uint256 minKeeperPayoutValue = 1000 ether; - uint256 defaultDesiredCollateralizationRatio = 200; - uint256 minDesiredCollateralizationRatio = 155; - - // Core system params - uint256 minCRatio = 1.5 ether; - uint256 ethToMint = 5000 ether; - uint256 ethCeiling = uint(-1); - uint256 ethLiquidationPenalty = 1 ether; - - uint256 defaultLiquidityMultiplier = 100; - uint256 defaultCollateralAmount = 40 ether; - uint256 defaultTokenAmount = 100 ether; - - function setUp() public { - hevm = Hevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); - hevm.warp(604411200); - - // Stable token - stableToken = new DSToken("STABLE", "STABLE"); - stableToken.mint(address(this), initTokenAmount); - - // System coin - systemCoin = new Coin("RAI", "RAI", 1); - systemCoin.mint(address(this), initTokenAmount); - systemCoinOracle = new MockMedianizer(initRAIUSDPrice, true); - - // Core system - safeEngine = new TestSAFEEngine(); - safeEngine.initializeCollateralType("eth"); - safeEngine.mint(address(this), rad(initTokenAmount)); - - ethFSM = new MockMedianizer(initETHUSDPrice, true); - ethMedian = new MockMedianizer(initETHUSDPrice, true); - ethFSM.updatePriceSource(address(ethMedian)); - - oracleRelayer = new OracleRelayer(address(safeEngine)); - oracleRelayer.modifyParameters("redemptionPrice", ray(initRAIUSDPrice)); - oracleRelayer.modifyParameters("eth", "orcl", address(ethFSM)); - oracleRelayer.modifyParameters("eth", "safetyCRatio", ray(minCRatio)); - oracleRelayer.modifyParameters("eth", "liquidationCRatio", ray(minCRatio)); - - safeEngine.addAuthorization(address(oracleRelayer)); - oracleRelayer.updateCollateralPrice("eth"); - - accountingEngine = new AccountingEngine( - address(safeEngine), address(0x1), address(0x2) - ); - safeEngine.addAuthorization(address(accountingEngine)); - - taxCollector = new TaxCollector(address(safeEngine)); - taxCollector.initializeCollateralType("eth"); - taxCollector.modifyParameters("primaryTaxReceiver", address(accountingEngine)); - taxCollector.modifyParameters("eth", "stabilityFee", 1000000564701133626865910626); // 5% / day - safeEngine.addAuthorization(address(taxCollector)); - - liquidationEngine = new LiquidationEngine(address(safeEngine)); - liquidationEngine.modifyParameters("accountingEngine", address(accountingEngine)); - - safeEngine.addAuthorization(address(liquidationEngine)); - accountingEngine.addAuthorization(address(liquidationEngine)); - - weth = new WETH9_(); - weth.deposit{value: initTokenAmount}(); - - collateralJoin = new BasicCollateralJoin(address(safeEngine), "eth", address(weth)); - - coinJoin = new CoinJoin(address(safeEngine), address(systemCoin)); - systemCoin.addAuthorization(address(coinJoin)); - safeEngine.transferInternalCoins(address(this), address(coinJoin), safeEngine.coinBalance(address(this))); - - safeEngine.addAuthorization(address(collateralJoin)); - - safeEngine.modifyParameters("eth", "debtCeiling", rad(ethCeiling)); - safeEngine.modifyParameters("globalDebtCeiling", rad(ethCeiling)); - - collateralAuctionHouse = new EnglishCollateralAuctionHouse(address(safeEngine), address(liquidationEngine), "eth"); - collateralAuctionHouse.addAuthorization(address(liquidationEngine)); - - liquidationEngine.addAuthorization(address(collateralAuctionHouse)); - liquidationEngine.modifyParameters("eth", "collateralAuctionHouse", address(collateralAuctionHouse)); - liquidationEngine.modifyParameters("eth", "liquidationPenalty", ethLiquidationPenalty); - - safeEngine.addAuthorization(address(collateralAuctionHouse)); - safeEngine.approveSAFEModification(address(collateralAuctionHouse)); - - safeManager = new GebSafeManager(address(safeEngine)); - oracleRelayer.updateCollateralPrice("eth"); - - // Uniswap setup - uniswapFactory = new UniswapV2Factory(address(this)); - createUniswapPairs(); - uniswapRouter = new UniswapV2Router02(address(uniswapFactory), address(weth)); - addPairLiquidityRouter(address(stableToken), address(weth), initSTABLEPairLiquidity * 20, initETHPairLiquidity * 20); - addPairLiquidityRouter(address(systemCoin), address(stableToken), initRAIPairLiquidity * 20, initSTABLEPairLiquidity * 20); - - // Liquidity manager - liquidityManager = new UniswapV2LiquidityManager(address(raiSTABLEPair), address(uniswapRouter)); - - // Swap manager - swapManager = new UniswapV2Swapper(address(uniswapFactory), address(uniswapRouter)); - - // Saviour infra - saviourRegistry = new SAFESaviourRegistry(saveCooldown); - cRatioSetter = new SaviourCRatioSetter(address(oracleRelayer), address(safeManager)); - cRatioSetter.setDefaultCRatio("eth", defaultDesiredCollateralizationRatio); - - saviour = new SystemCoinUniswapV2SafeSaviour( - isSystemCoinToken0, - address(coinJoin), - address(collateralJoin), - address(cRatioSetter), - address(systemCoinOracle), - address(liquidationEngine), - address(oracleRelayer), - address(safeManager), - address(saviourRegistry), - address(liquidityManager), - address(swapManager), - address(stableToken), - address(raiSTABLEPair), - minKeeperPayoutValue - ); - saviour.modifyParameters("taxCollector", address(taxCollector)); - - saviourRegistry.toggleSaviour(address(saviour)); - liquidationEngine.connectSAFESaviour(address(saviour)); - - me = address(this); - alice = new FakeUser(); - } - - // --- Math --- - function ray(uint wad) internal pure returns (uint) { - return wad * 10 ** 9; - } - function rad(uint wad) internal pure returns (uint) { - return wad * 10 ** 27; - } - function sub(uint256 x, uint256 y) internal pure returns (uint256 z) { - require((z = x - y) <= x); - } - - // --- Uniswap utils --- - function createUniswapPairs() internal { - // Setup STABLE/RAI pair - uniswapFactory.createPair(address(stableToken), address(systemCoin)); - raiSTABLEPair = UniswapV2Pair(uniswapFactory.getPair(address(stableToken), address(systemCoin))); - - // Setup STABLE/WETH pair - uniswapFactory.createPair(address(weth), address(stableToken)); - stableWETHPair = UniswapV2Pair(uniswapFactory.getPair(address(weth), address(stableToken))); - - if (address(raiSTABLEPair.token0()) == address(systemCoin)) isSystemCoinToken0 = true; - } - function addPairLiquidityRouter(address token1, address token2, uint256 amount1, uint256 amount2) internal { - DSToken(token1).approve(address(uniswapRouter), uint(-1)); - DSToken(token2).approve(address(uniswapRouter), uint(-1)); - uniswapRouter.addLiquidity(token1, token2, amount1, amount2, amount1, amount2, address(this), now); - UniswapV2Pair updatedPair = UniswapV2Pair(uniswapFactory.getPair(token1, token2)); - updatedPair.sync(); - } - function addPairLiquidityTransfer(UniswapV2Pair pair, address token1, address token2, uint256 amount1, uint256 amount2) internal { - DSToken(token1).transfer(address(pair), amount1); - DSToken(token2).transfer(address(pair), amount2); - pair.sync(); - } - - // --- Default actions/scenarios --- - function default_create_liquidatable_position(uint256 desiredCRatio, uint256 liquidatableCollateralPrice) internal returns (address) { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, desiredCRatio); - assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); - - ethMedian.updateCollateralPrice(liquidatableCollateralPrice); - ethFSM.updateCollateralPrice(liquidatableCollateralPrice); - oracleRelayer.updateCollateralPrice("eth"); - - return safeHandler; - } - function default_save(uint256 safe, address safeHandler, uint desiredCRatio) internal { - default_modify_collateralization(safe, safeHandler); - - alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, desiredCRatio); - assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); - - ethMedian.updateCollateralPrice(initETHUSDPrice / 30); - ethFSM.updateCollateralPrice(initETHUSDPrice / 30); - oracleRelayer.updateCollateralPrice("eth"); - - uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(stableToken), initRAIPairLiquidity * defaultLiquidityMultiplier, initSTABLEPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); - raiSTABLEPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), safe, lpTokenAmount); - assertEq(raiSTABLEPair.balanceOf(address(saviour)), lpTokenAmount); - assertTrue(saviour.canSave("eth", safeHandler)); - - liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(100000 ether)); - liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); - - uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); - uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); - (uint256 oldSysCoinReserve, uint256 oldCollateralReserve) = saviour.underlyingReserves(safeHandler); - - uint auction = liquidationEngine.liquidateSAFE("eth", safeHandler); - (uint256 sysCoinReserve, uint256 collateralReserve) = saviour.underlyingReserves(safeHandler); - - assertEq(auction, 0); - assertEq(stableToken.balanceOf(address(saviour)), 0); - assertEq(stableToken.balanceOf(address(liquidityManager)), 0); - assertTrue( - sysCoinReserve > oldSysCoinReserve || - collateralReserve > oldCollateralReserve - ); - assertTrue( - systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance > 0 || - weth.balanceOf(address(this)) - preSaveWETHKeeperBalance > 0 - ); - assertEq(raiSTABLEPair.balanceOf(address(saviour)), 0); - assertEq(raiSTABLEPair.balanceOf(address(liquidityManager)), 0); - assertEq(saviour.lpTokenCover(safeHandler), 0); - - (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", safeHandler); - (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("eth"); - assertEq(lockedCollateral * ray(ethFSM.read()) * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10 ** 27), desiredCRatio); - } - function default_second_save(uint256 safe, address safeHandler, uint desiredCRatio) internal { - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, desiredCRatio); - - ethMedian.updateCollateralPrice(initETHUSDPrice / 40); - ethFSM.updateCollateralPrice(initETHUSDPrice / 40); - oracleRelayer.updateCollateralPrice("eth"); - - uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(stableToken), initRAIPairLiquidity * defaultLiquidityMultiplier, initSTABLEPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); - raiSTABLEPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), safe, lpTokenAmount); - assertEq(raiSTABLEPair.balanceOf(address(saviour)), lpTokenAmount); - assertTrue(saviour.canSave("eth", safeHandler)); - - liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(111 ether)); - liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); - - uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); - uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); - (uint256 oldSysCoinReserve, uint256 oldCollateralReserve) = saviour.underlyingReserves(safeHandler); - - uint auction = liquidationEngine.liquidateSAFE("eth", safeHandler); - (uint256 sysCoinReserve, uint256 collateralReserve) = saviour.underlyingReserves(safeHandler); - - assertEq(auction, 0); - assertEq(stableToken.balanceOf(address(saviour)), 0); - assertEq(stableToken.balanceOf(address(liquidityManager)), 0); - assertTrue( - sysCoinReserve > oldSysCoinReserve || - collateralReserve > oldCollateralReserve - ); - assertTrue( - systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance > 0 || - weth.balanceOf(address(this)) - preSaveWETHKeeperBalance > 0 - ); - assertEq(raiSTABLEPair.balanceOf(address(saviour)), 0); - assertEq(raiSTABLEPair.balanceOf(address(liquidityManager)), 0); - assertEq(saviour.lpTokenCover(safeHandler), 0); - - (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", safeHandler); - (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("eth"); - uint256 cRatio = lockedCollateral * ray(ethFSM.read()) * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10 ** 27); - assertTrue(cRatio == desiredCRatio || cRatio == desiredCRatio - 1); - } - function default_liquidate_safe(address safeHandler) internal { - liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(100000 ether)); - liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); - - uint auction = liquidationEngine.liquidateSAFE("eth", safeHandler); - // the full SAFE is liquidated - (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", me); - assertEq(lockedCollateral, 0); - assertEq(generatedDebt, 0); - // all debt goes to the accounting engine - assertTrue(accountingEngine.totalQueuedDebt() > 0); - // auction is for all collateral - (,uint amountToSell,,,,,, uint256 amountToRaise) = collateralAuctionHouse.bids(auction); - assertEq(amountToSell, defaultCollateralAmount); - assertEq(amountToRaise, rad(1100 ether)); - } - function default_create_liquidatable_position_deposit_cover(uint256 desiredCRatio, uint256 liquidatableCollateralPrice) - internal returns (address) { - // Create position - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, desiredCRatio); - assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); - - // Change oracle price - ethMedian.updateCollateralPrice(liquidatableCollateralPrice); - ethFSM.updateCollateralPrice(liquidatableCollateralPrice); - oracleRelayer.updateCollateralPrice("eth"); - - // Deposit cover - uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(stableToken), initRAIPairLiquidity * defaultLiquidityMultiplier, initSTABLEPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); - raiSTABLEPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), safe, lpTokenAmount); - assertEq(raiSTABLEPair.balanceOf(address(saviour)), lpTokenAmount); - assertEq(saviour.lpTokenCover(safeHandler), lpTokenAmount); - - return safeHandler; - } - function default_create_position_deposit_cover() internal returns (uint, address) { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - // Deposit cover - uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(stableToken), initRAIPairLiquidity * defaultLiquidityMultiplier, initSTABLEPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); - raiSTABLEPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), safe, lpTokenAmount); - assertEq(raiSTABLEPair.balanceOf(address(saviour)), lpTokenAmount); - assertEq(saviour.lpTokenCover(safeHandler), lpTokenAmount); - - return (safe, safeHandler); - } - function default_modify_collateralization(uint256 safe, address safeHandler) internal { - weth.approve(address(collateralJoin), uint(-1)); - collateralJoin.join(address(safeHandler), defaultTokenAmount); - alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 10)); - } - - // --- Tests --- - function test_setup() public { - assertEq(saviour.authorizedAccounts(address(this)), 1); - assertTrue(saviour.isSystemCoinToken0() == isSystemCoinToken0); - assertEq(saviour.minKeeperPayoutValue(), minKeeperPayoutValue); - assertEq(saviour.restrictUsage(), 0); - - assertEq(address(saviour.coinJoin()), address(coinJoin)); - assertEq(address(saviour.collateralJoin()), address(collateralJoin)); - assertEq(address(saviour.cRatioSetter()), address(cRatioSetter)); - assertEq(address(saviour.liquidationEngine()), address(liquidationEngine)); - assertEq(address(saviour.oracleRelayer()), address(oracleRelayer)); - assertEq(address(saviour.systemCoinOrcl()), address(systemCoinOracle)); - assertEq(address(saviour.systemCoin()), address(systemCoin)); - assertEq(address(saviour.safeEngine()), address(safeEngine)); - assertEq(address(saviour.safeManager()), address(safeManager)); - assertEq(address(saviour.saviourRegistry()), address(saviourRegistry)); - assertEq(address(saviour.liquidityManager()), address(liquidityManager)); - assertEq(address(saviour.swapManager()), address(swapManager)); - assertEq(address(saviour.lpToken()), address(raiSTABLEPair)); - assertEq(address(saviour.collateralToken()), address(weth)); - assertEq(address(saviour.pairToken()), address(stableToken)); - } - function test_modify_uints() public { - saviour.modifyParameters("minKeeperPayoutValue", 5); - saviour.modifyParameters("restrictUsage", 1); - - assertEq(saviour.minKeeperPayoutValue(), 5); - assertEq(saviour.restrictUsage(), 1); - } - function testFail_modify_uint_unauthed() public { - alice.doModifyParameters(saviour, "minKeeperPayoutValue", 5); - } - function test_modify_addresses() public { - saviour.modifyParameters("systemCoinOrcl", address(systemCoinOracle)); - saviour.modifyParameters("oracleRelayer", address(oracleRelayer)); - saviour.modifyParameters("liquidityManager", address(liquidityManager)); - saviour.modifyParameters("swapManager", address(swapManager)); - - assertEq(address(saviour.liquidityManager()), address(liquidityManager)); - assertEq(address(saviour.oracleRelayer()), address(oracleRelayer)); - assertEq(address(saviour.systemCoinOrcl()), address(systemCoinOracle)); - assertEq(address(saviour.swapManager()), address(swapManager)); - } - function testFail_modify_address_unauthed() public { - alice.doModifyParameters(saviour, "systemCoinOrcl", address(systemCoinOracle)); - } - function testFail_deposit_liq_engine_not_approved() public { - liquidationEngine.disconnectSAFESaviour(address(saviour)); - - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(stableToken), initRAIPairLiquidity * defaultLiquidityMultiplier, initSTABLEPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); - raiSTABLEPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), 1, lpTokenAmount); - } - function testFail_deposit_null_lp_token_amount() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(stableToken), initRAIPairLiquidity * defaultLiquidityMultiplier, initSTABLEPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); - raiSTABLEPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), 1, 0); - } - function testFail_deposit_inexistent_safe() public { - uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(stableToken), initRAIPairLiquidity * defaultLiquidityMultiplier, initSTABLEPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); - raiSTABLEPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), 1, lpTokenAmount); - } - function test_deposit_twice() public { - uint256 initialLPSupply = raiSTABLEPair.totalSupply(); - - (uint safe, address safeHandler) = default_create_position_deposit_cover(); - - // Second deposit - uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(stableToken), initRAIPairLiquidity * defaultLiquidityMultiplier, initSTABLEPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); - raiSTABLEPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), safe, lpTokenAmount); - - // Checks - assertTrue(raiSTABLEPair.balanceOf(address(saviour)) > 0 && saviour.lpTokenCover(safeHandler) > 0); - assertEq(saviour.lpTokenCover(safeHandler), raiSTABLEPair.totalSupply() - initialLPSupply); - assertEq(raiSTABLEPair.balanceOf(address(saviour)), raiSTABLEPair.totalSupply() - initialLPSupply); - } - function test_deposit_after_everything_withdrawn() public { - (uint safe, address safeHandler) = default_create_position_deposit_cover(); - - // Withdraw - uint256 currentLPBalanceAlice = raiSTABLEPair.balanceOf(address(alice)); - uint256 currentLPBalanceSaviour = raiSTABLEPair.balanceOf(address(saviour)); - alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler), address(alice)); - - // Checks - assertEq(raiSTABLEPair.balanceOf(address(alice)), currentLPBalanceAlice + currentLPBalanceSaviour); - assertTrue(raiSTABLEPair.balanceOf(address(saviour)) == 0 && saviour.lpTokenCover(safeHandler) == 0); - - // Deposit again - alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), safe, currentLPBalanceSaviour); - - // Checks - assertTrue(raiSTABLEPair.balanceOf(address(saviour)) > 0 && saviour.lpTokenCover(safeHandler) > 0); - assertEq(saviour.lpTokenCover(safeHandler), currentLPBalanceSaviour); - assertEq(raiSTABLEPair.balanceOf(address(saviour)), currentLPBalanceSaviour); - assertEq(raiSTABLEPair.balanceOf(address(alice)), currentLPBalanceAlice); - } - function testFail_withdraw_unauthorized() public { - (uint safe, ) = default_create_position_deposit_cover(); - - // Withdraw by unauthed - FakeUser bob = new FakeUser(); - bob.doWithdraw(saviour, safe, raiSTABLEPair.balanceOf(address(saviour)), address(bob)); - } - function testFail_withdraw_more_than_deposited() public { - (uint safe, address safeHandler) = default_create_position_deposit_cover(); - uint256 currentLPBalance = raiSTABLEPair.balanceOf(address(this)); - alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler) + 1, address(this)); - } - function testFail_withdraw_null() public { - (uint safe, address safeHandler) = default_create_position_deposit_cover(); - alice.doWithdraw(saviour, safe, 0, address(this)); - } - function test_withdraw() public { - (uint safe, address safeHandler) = default_create_position_deposit_cover(); - - // Withdraw - uint256 currentLPBalanceAlice = raiSTABLEPair.balanceOf(address(alice)); - uint256 currentLPBalanceSaviour = raiSTABLEPair.balanceOf(address(saviour)); - alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler), address(alice)); - - // Checks - assertEq(raiSTABLEPair.balanceOf(address(alice)), currentLPBalanceAlice + currentLPBalanceSaviour); - assertTrue(raiSTABLEPair.balanceOf(address(saviour)) == 0 && saviour.lpTokenCover(safeHandler) == 0); - } - function test_withdraw_twice() public { - (uint safe, address safeHandler) = default_create_position_deposit_cover(); - - // Withdraw once - uint256 currentLPBalanceAlice = raiSTABLEPair.balanceOf(address(alice)); - uint256 currentLPBalanceSaviour = raiSTABLEPair.balanceOf(address(saviour)); - alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler) / 2, address(alice)); - - // Checks - assertEq(raiSTABLEPair.balanceOf(address(alice)), currentLPBalanceAlice + currentLPBalanceSaviour / 2); - assertTrue(raiSTABLEPair.balanceOf(address(saviour)) == currentLPBalanceSaviour / 2 && saviour.lpTokenCover(safeHandler) == currentLPBalanceSaviour / 2); - - // Withdraw again - alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler), address(alice)); - - // Checks - assertEq(raiSTABLEPair.balanceOf(address(alice)), currentLPBalanceAlice + currentLPBalanceSaviour); - assertTrue(raiSTABLEPair.balanceOf(address(saviour)) == 0 && saviour.lpTokenCover(safeHandler) == 0); - } - function test_withdraw_custom_dst() public { - (uint safe, address safeHandler) = default_create_position_deposit_cover(); - - // Withdraw - uint256 currentLPBalanceAlice = raiSTABLEPair.balanceOf(address(0xb1)); - uint256 currentLPBalanceSaviour = raiSTABLEPair.balanceOf(address(saviour)); - alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler), address(0xb1)); - - // Checks - assertEq(raiSTABLEPair.balanceOf(address(0xb1)), currentLPBalanceSaviour); - assertEq(raiSTABLEPair.balanceOf(address(alice)), currentLPBalanceAlice); - assertTrue(raiSTABLEPair.balanceOf(address(saviour)) == 0 && saviour.lpTokenCover(safeHandler) == 0); - } - function test_tokenAmountUsedToSave() public { - (uint safe, address safeHandler) = default_create_position_deposit_cover(); - - assertEq(saviour.lpTokenCover(safeHandler), saviour.tokenAmountUsedToSave("eth", safeHandler)); - } - function test_getCollateralPrice_zero_price() public { - ethFSM.updateCollateralPrice(0); - assertEq(saviour.getCollateralPrice(), 0); - } - function test_getCollateralPrice_invalid() public { - ethFSM.changeValidity(); - assertEq(saviour.getCollateralPrice(), 0); - } - function test_getCollateralPrice_null_fsm() public { - oracleRelayer.modifyParameters("eth", "orcl", address(0)); - assertEq(saviour.getCollateralPrice(), 0); - } - function test_getCollateralPrice() public { - assertEq(saviour.getCollateralPrice(), initETHUSDPrice); - } - function test_getSystemCoinMarketPrice_invalid() public { - systemCoinOracle.changeValidity(); - assertEq(saviour.getSystemCoinMarketPrice(), 0); - } - function test_getSystemCoinMarketPrice_null_price() public { - systemCoinOracle.updateCollateralPrice(0); - assertEq(saviour.getSystemCoinMarketPrice(), 0); - } - function test_getSystemCoinMarketPrice() public { - assertEq(saviour.getSystemCoinMarketPrice(), initRAIUSDPrice); - } - function test_getTargetCRatio_inexistent_handler() public { - assertEq(saviour.getTargetCRatio(address(0x1)), defaultDesiredCollateralizationRatio); - } - function test_getTargetCRatio_no_custom_desired_ratio() public { - (uint safe, address safeHandler) = default_create_position_deposit_cover(); - assertEq(saviour.getTargetCRatio(safeHandler), defaultDesiredCollateralizationRatio); - } - function test_getTargetCRatio_custom_desired_ratio() public { - (uint safe, address safeHandler) = default_create_position_deposit_cover(); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, defaultDesiredCollateralizationRatio * 2); - assertEq(saviour.getTargetCRatio(safeHandler), defaultDesiredCollateralizationRatio * 2); - } - function test_getLPUnderlying_inexistent_handler() public { - (uint sysCoins, uint collateral) = saviour.getLPUnderlying(address(0x1)); - assertEq(sysCoins, collateral); - assertEq(sysCoins, 0); - } - function test_getLPUnderlying() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(stableToken), initRAIPairLiquidity * defaultLiquidityMultiplier, initSTABLEPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); - raiSTABLEPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), safe, lpTokenAmount); - - (uint sysCoins, uint collateral) = saviour.getLPUnderlying(safeHandler); - assertEq(sysCoins, initRAIPairLiquidity * defaultLiquidityMultiplier); - assertTrue(collateral > 0); - } - function test_getTokensForSaving_no_cover() public { - (uint sysCoins, uint collateral) = saviour.getTokensForSaving(address(0x1), oracleRelayer.redemptionPrice()); - - assertEq(sysCoins, collateral); - assertEq(sysCoins, 0); - } - function test_getTokensForSaving_null_redemption() public { - address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); - (uint sysCoins, uint collateral) = saviour.getTokensForSaving(safeHandler, 0); - - assertEq(sysCoins, collateral); - assertEq(sysCoins, 0); - } - function test_getTokensForSaving_null_collateral_price() public { - address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); - ethFSM.updateCollateralPrice(0); - (uint sysCoins, uint collateral) = saviour.getTokensForSaving(safeHandler, oracleRelayer.redemptionPrice()); - - assertEq(sysCoins, collateral); - assertEq(sysCoins, 0); - } - function test_getTokensForSaving_save_only_with_sys_coins() public { - address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); - (uint sysCoins, uint collateral) = saviour.getTokensForSaving(safeHandler, oracleRelayer.redemptionPrice()); - - assertTrue(sysCoins > 0); - assertEq(collateral, 0); - } - function test_getTokensForSaving_both_tokens_used() public { - // Create position - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 200); - assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); - - // Change oracle price - ethMedian.updateCollateralPrice(initETHUSDPrice / 3); - ethFSM.updateCollateralPrice(initETHUSDPrice / 3); - oracleRelayer.updateCollateralPrice("eth"); - - // Deposit cover - uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(stableToken), initRAIPairLiquidity * 2, initSTABLEPairLiquidity * 2 - ); - lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); - raiSTABLEPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), safe, lpTokenAmount); - - (uint sysCoins, uint collateral) = saviour.getTokensForSaving(safeHandler, oracleRelayer.redemptionPrice()); - - assertTrue(sysCoins > 0); - assertTrue(collateral > 0); - } - function test_getTokensForSaving_not_enough_lp_collateral() public { - // Create position - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - - weth.approve(address(collateralJoin), uint(-1)); - collateralJoin.join(address(safeHandler), defaultTokenAmount); - alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 10)); - - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 155); - assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); - - // Change oracle price - ethMedian.updateCollateralPrice(initETHUSDPrice / 30); - ethFSM.updateCollateralPrice(initETHUSDPrice / 30); - oracleRelayer.updateCollateralPrice("eth"); - - // Deposit cover - uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(stableToken), initRAIPairLiquidity / 5, initSTABLEPairLiquidity / 5 - ); - - lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); - raiSTABLEPair.transfer(address(alice), lpTokenAmount / 10); - - alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), safe, lpTokenAmount / 10); - - (uint sysCoins, uint collateral) = saviour.getTokensForSaving(safeHandler, oracleRelayer.redemptionPrice()); - - assertEq(sysCoins, 0); - assertEq(collateral, 0); - } - function test_getKeeperPayoutTokens_null_collateral_price() public { - address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); - ethFSM.updateCollateralPrice(0); - - (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), 0, 0); - - assertEq(sysCoins, 0); - assertEq(collateral, 0); - } - function test_getKeeperPayoutTokens_null_sys_coin_price() public { - address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); - systemCoinOracle.updateCollateralPrice(0); - - (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), 0, 0); - - assertEq(sysCoins, 0); - assertEq(collateral, 0); - } - function test_getKeeperPayoutTokens_only_sys_coins_used() public { - address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); - (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), 0, 0); - - assertEq(sysCoins, minKeeperPayoutValue * 10 ** 18 / systemCoinOracle.read()); - assertEq(collateral, 0); - } - function test_getKeeperPayoutTokens_only_collateral_used() public { - (, address safeHandler) = default_create_position_deposit_cover(); - (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), uint(-1), 0); - - assertEq(sysCoins, 0); - assertEq(collateral, minKeeperPayoutValue * 10 ** 18 / ethFSM.read()); - } - function test_getKeeperPayoutTokens_both_tokens_used() public { - (, address safeHandler) = default_create_position_deposit_cover(); - (uint underlyingSysCoins, ) = saviour.getLPUnderlying(safeHandler); - - (uint sysCoins, uint collateral) = - saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), underlyingSysCoins - (minKeeperPayoutValue * 10 ** 18 / (systemCoinOracle.read() * 2)), 0); - - assertEq(sysCoins, (minKeeperPayoutValue * 10 ** 18 / (systemCoinOracle.read() * 2))); - assertEq(collateral, 2 ether); - } - function test_getKeeperPayoutTokens_not_enough_tokens_to_pay() public { - saviour.modifyParameters("minKeeperPayoutValue", 10000000 ether); - - (, address safeHandler) = default_create_position_deposit_cover(); - (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), 0, 0); - - assertEq(sysCoins, 0); - assertEq(collateral, 0); - } - function test_canSave_cannot_save_safe() public { - // Create position - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - - weth.approve(address(collateralJoin), uint(-1)); - collateralJoin.join(address(safeHandler), defaultTokenAmount); - alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 10)); - - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 155); - assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); - - // Change oracle price - ethMedian.updateCollateralPrice(initETHUSDPrice / 30); - ethFSM.updateCollateralPrice(initETHUSDPrice / 30); - oracleRelayer.updateCollateralPrice("eth"); - - // Deposit cover - uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(stableToken), initRAIPairLiquidity / 5, initSTABLEPairLiquidity / 5 - ); - lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); - raiSTABLEPair.transfer(address(alice), lpTokenAmount / 10); - - alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), safe, lpTokenAmount / 10); - - assertTrue(!saviour.canSave("eth", safeHandler)); - } - function test_canSave_cannot_pay_keeper() public { - saviour.modifyParameters("minKeeperPayoutValue", 10000000 ether); - address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); - assertTrue(!saviour.canSave("eth", safeHandler)); - } - function test_canSave_both_tokens_used() public { - // Create position - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 200); - assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); - - // Change oracle price - ethMedian.updateCollateralPrice(initETHUSDPrice / 3); - ethFSM.updateCollateralPrice(initETHUSDPrice / 3); - oracleRelayer.updateCollateralPrice("eth"); - - // Deposit cover - uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(stableToken), initRAIPairLiquidity * 2, initSTABLEPairLiquidity * 2 - ); - lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); - raiSTABLEPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), safe, lpTokenAmount); - saviour.modifyParameters("minKeeperPayoutValue", 50 ether); - - assertTrue(saviour.canSave("eth", safeHandler)); - } - function test_canSave() public { - address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); - assertTrue(saviour.canSave("eth", safeHandler)); - } - function testFail_saveSAFE_invalid_caller() public { - address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); - saviour.saveSAFE(address(this), "eth", safeHandler); - } - function test_saveSAFE_no_cover() public { - address safeHandler = default_create_liquidatable_position(200, initETHUSDPrice / 30); - default_liquidate_safe(safeHandler); - } - function test_saveSAFE_cannot_save_safe() public { - // Create position - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - - weth.approve(address(collateralJoin), uint(-1)); - collateralJoin.join(address(safeHandler), defaultTokenAmount); - alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 10)); - - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 155); - assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); - - // Change oracle price - ethMedian.updateCollateralPrice(initETHUSDPrice / 30); - ethFSM.updateCollateralPrice(initETHUSDPrice / 30); - oracleRelayer.updateCollateralPrice("eth"); - - // Deposit cover - uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(stableToken), initRAIPairLiquidity / 5, initSTABLEPairLiquidity / 5 - ); - lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); - raiSTABLEPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), safe, lpTokenAmount / 10); - - default_liquidate_safe(safeHandler); - } - function test_saveSAFE_cannot_pay_keeper() public { - address safeHandler = default_create_liquidatable_position(200, initETHUSDPrice / 30); - saviour.modifyParameters("minKeeperPayoutValue", 10000000 ether); - default_liquidate_safe(safeHandler); - } - function test_saveSAFE() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_save(safe, safeHandler, 200); - } - function test_saveSAFE_accumulate_rate() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - - // Warp and save - hevm.warp(now + 2 days); - taxCollector.taxSingle("eth"); - - weth.approve(address(collateralJoin), uint(-1)); - collateralJoin.join(address(safeHandler), defaultTokenAmount); - alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 8)); - - alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 200); - assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); - - ethMedian.updateCollateralPrice(initETHUSDPrice / 30); - ethFSM.updateCollateralPrice(initETHUSDPrice / 30); - oracleRelayer.updateCollateralPrice("eth"); - - uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(stableToken), initRAIPairLiquidity * defaultLiquidityMultiplier, initSTABLEPairLiquidity * defaultLiquidityMultiplier - ); - lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); - raiSTABLEPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), safe, lpTokenAmount); - assertEq(raiSTABLEPair.balanceOf(address(saviour)), lpTokenAmount); - assertTrue(saviour.canSave("eth", safeHandler)); - - liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(100000 ether)); - liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); - - uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); - uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); - (uint256 oldSysCoinReserve, uint256 oldCollateralReserve) = saviour.underlyingReserves(safeHandler); - - uint auction = liquidationEngine.liquidateSAFE("eth", safeHandler); - (uint256 sysCoinReserve, uint256 collateralReserve) = saviour.underlyingReserves(safeHandler); - - assertEq(auction, 0); - assertEq(stableToken.balanceOf(address(saviour)), 0); - assertEq(stableToken.balanceOf(address(liquidityManager)), 0); - assertTrue( - sysCoinReserve > oldSysCoinReserve || - collateralReserve > oldCollateralReserve - ); - assertTrue( - systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance > 0 || - weth.balanceOf(address(this)) - preSaveWETHKeeperBalance > 0 - ); - assertEq(raiSTABLEPair.balanceOf(address(saviour)), 0); - assertEq(raiSTABLEPair.balanceOf(address(liquidityManager)), 0); - assertEq(saviour.lpTokenCover(safeHandler), 0); - - (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", safeHandler); - (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("eth"); - assertEq(lockedCollateral * ray(ethFSM.read()) * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10 ** 27), 200); - } - function test_saveSAFE_both_tokens() public { - // Create position - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_modify_collateralization(safe, safeHandler); - - alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); - alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 200); - assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); - - // Change oracle price - ethMedian.updateCollateralPrice(initETHUSDPrice / 3); - ethFSM.updateCollateralPrice(initETHUSDPrice / 3); - oracleRelayer.updateCollateralPrice("eth"); - - // Deposit cover - uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); - addPairLiquidityRouter( - address(systemCoin), address(stableToken), initRAIPairLiquidity * 2, initSTABLEPairLiquidity * 2 - ); - lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); - raiSTABLEPair.transfer(address(alice), lpTokenAmount); - - alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), safe, lpTokenAmount); - saviour.modifyParameters("minKeeperPayoutValue", 50 ether); - - assertTrue(saviour.canSave("eth", safeHandler)); - - liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(100000 ether)); - liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); - - uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); - uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); - (uint256 oldSysCoinReserve, uint256 oldCollateralReserve) = saviour.underlyingReserves(safeHandler); - - uint auction = liquidationEngine.liquidateSAFE("eth", safeHandler); - (uint256 sysCoinReserve, uint256 collateralReserve) = saviour.underlyingReserves(safeHandler); - - assertEq(auction, 0); - assertEq(stableToken.balanceOf(address(saviour)), 0); - assertEq(stableToken.balanceOf(address(liquidityManager)), 0); - assertTrue( - sysCoinReserve > oldSysCoinReserve || - collateralReserve > oldCollateralReserve - ); - assertTrue( - systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance > 0 || - weth.balanceOf(address(this)) - preSaveWETHKeeperBalance > 0 - ); - assertEq(raiSTABLEPair.balanceOf(address(saviour)), 0); - assertEq(raiSTABLEPair.balanceOf(address(liquidityManager)), 0); - assertEq(saviour.lpTokenCover(safeHandler), 0); - - (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", safeHandler); - assertEq(lockedCollateral * ray(ethFSM.read()) * 100 / (generatedDebt * oracleRelayer.redemptionPrice()), 199); - } - function test_saveSAFE_twice() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_save(safe, safeHandler, 155); - - hevm.warp(now + saviourRegistry.saveCooldown() + 1); - default_second_save(safe, safeHandler, 200); - } - function testFail_saveSAFE_withdraw_cover() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_save(safe, safeHandler, 155); - - alice.doWithdraw(saviour, safe, 1, address(this)); - } - function test_saveSAFE_get_reserves() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_save(safe, safeHandler, 155); - - uint256 oldSysCoinBalance = systemCoin.balanceOf(address(alice)); - uint256 oldCollateralBalance = weth.balanceOf(address(alice)); - - (uint sysCoinReserve, uint collateralReserve) = saviour.underlyingReserves(safeHandler); - - alice.doGetReserves(saviour, safe, address(alice)); - assertTrue(systemCoin.balanceOf(address(alice)) - sysCoinReserve == oldSysCoinBalance); - assertTrue(weth.balanceOf(address(alice)) - collateralReserve == oldCollateralBalance); - - assertEq(systemCoin.balanceOf(address(saviour)), 0); - assertEq(weth.balanceOf(address(saviour)), 0); - } - function testFail_save_twice_without_waiting() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_save(safe, safeHandler, 155); - default_second_save(safe, safeHandler, 200); - } - function test_saveSAFE_get_reserves_twice() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_save(safe, safeHandler, 155); - alice.doGetReserves(saviour, safe, address(alice)); - - hevm.warp(now + saviourRegistry.saveCooldown() + 1); - default_second_save(safe, safeHandler, 200); - - uint256 oldSysCoinBalance = systemCoin.balanceOf(address(0x1)); - uint256 oldCollateralBalance = weth.balanceOf(address(0x1)); - - (uint sysCoinReserve, uint collateralReserve) = saviour.underlyingReserves(safeHandler); - - alice.doGetReserves(saviour, safe, address(0x1)); - assertTrue(systemCoin.balanceOf(address(0x1)) - sysCoinReserve == oldSysCoinBalance); - assertTrue(weth.balanceOf(address(0x1)) - collateralReserve == oldCollateralBalance); - - assertEq(systemCoin.balanceOf(address(saviour)), 0); - assertEq(weth.balanceOf(address(saviour)), 0); - } - function testFail_getReserves_invalid_caller() public { - uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); - address safeHandler = safeManager.safes(safe); - default_save(safe, safeHandler, 155); - - saviour.getReserves(safe, address(alice)); - } -} +pragma solidity 0.6.7; + +import "ds-test/test.sol"; +import "ds-weth/weth9.sol"; +import "ds-token/token.sol"; + +import {SAFEEngine} from 'geb/SAFEEngine.sol'; +import {Coin} from 'geb/Coin.sol'; +import {LiquidationEngine} from 'geb/LiquidationEngine.sol'; +import {AccountingEngine} from 'geb/AccountingEngine.sol'; +import {TaxCollector} from 'geb/TaxCollector.sol'; +import {BasicCollateralJoin, CoinJoin} from 'geb/BasicTokenAdapters.sol'; +import {OracleRelayer} from 'geb/OracleRelayer.sol'; +import {EnglishCollateralAuctionHouse} from 'geb/CollateralAuctionHouse.sol'; +import {GebSafeManager} from "geb-safe-manager/GebSafeManager.sol"; + +import {SaviourCRatioSetter} from "../SaviourCRatioSetter.sol"; +import {SAFESaviourRegistry} from "../SAFESaviourRegistry.sol"; + +import "../integrations/uniswap/uni-v2/UniswapV2Factory.sol"; +import "../integrations/uniswap/uni-v2/UniswapV2Pair.sol"; +import "../integrations/uniswap/uni-v2/UniswapV2Router02.sol"; + +import "../integrations/uniswap/swappers/UniswapV2Swapper.sol"; + +import "../integrations/uniswap/liquidity-managers/UniswapV2LiquidityManager.sol"; +import "../integrations/uniswap/liquidity-managers/UniswapV3LiquidityManager.sol"; + +import "../saviours/SystemCoinUniswapV2SafeSaviour.sol"; + +abstract contract Hevm { + function warp(uint256) virtual public; +} + +contract TestSAFEEngine is SAFEEngine { + uint256 constant RAY = 10 ** 27; + + constructor() public {} + + function mint(address usr, uint wad) public { + coinBalance[usr] += wad * RAY; + globalDebt += wad * RAY; + } + function balanceOf(address usr) public view returns (uint) { + return uint(coinBalance[usr] / RAY); + } +} + +// --- Median Contracts --- +contract MockMedianizer { + uint256 public price; + bool public validPrice; + uint public lastUpdateTime; + address public priceSource; + + constructor(uint256 price_, bool validPrice_) public { + price = price_; + validPrice = validPrice_; + lastUpdateTime = now; + } + function updatePriceSource(address priceSource_) external { + priceSource = priceSource_; + } + function changeValidity() external { + validPrice = !validPrice; + } + function updateCollateralPrice(uint256 price_) external { + price = price_; + lastUpdateTime = now; + } + function read() external view returns (uint256) { + return price; + } + function getResultWithValidity() external view returns (uint256, bool) { + return (price, validPrice); + } +} + +// Users +contract FakeUser { + function doModifyParameters( + SystemCoinUniswapV2SafeSaviour saviour, + bytes32 parameter, + uint256 data + ) public { + saviour.modifyParameters(parameter, data); + } + + function doModifyParameters( + SystemCoinUniswapV2SafeSaviour saviour, + bytes32 parameter, + address data + ) public { + saviour.modifyParameters(parameter, data); + } + + function doOpenSafe( + GebSafeManager manager, + bytes32 collateralType, + address usr + ) public returns (uint256) { + return manager.openSAFE(collateralType, usr); + } + + function doSafeAllow( + GebSafeManager manager, + uint safe, + address usr, + uint ok + ) public { + manager.allowSAFE(safe, usr, ok); + } + + function doHandlerAllow( + GebSafeManager manager, + address usr, + uint ok + ) public { + manager.allowHandler(usr, ok); + } + + function doTransferSAFEOwnership( + GebSafeManager manager, + uint safe, + address dst + ) public { + manager.transferSAFEOwnership(safe, dst); + } + + function doModifySAFECollateralization( + GebSafeManager manager, + uint safe, + int deltaCollateral, + int deltaDebt + ) public { + manager.modifySAFECollateralization(safe, deltaCollateral, deltaDebt); + } + + function doApproveSAFEModification( + SAFEEngine safeEngine, + address usr + ) public { + safeEngine.approveSAFEModification(usr); + } + + function doSAFEEngineModifySAFECollateralization( + SAFEEngine safeEngine, + bytes32 collateralType, + address safe, + address collateralSource, + address debtDst, + int deltaCollateral, + int deltaDebt + ) public { + safeEngine.modifySAFECollateralization(collateralType, safe, collateralSource, debtDst, deltaCollateral, deltaDebt); + } + + function doProtectSAFE( + GebSafeManager manager, + uint safe, + address liquidationEngine, + address saviour + ) public { + manager.protectSAFE(safe, liquidationEngine, saviour); + } + + function doDeposit( + SystemCoinUniswapV2SafeSaviour saviour, + DSToken lpToken, + uint256 safeID, + uint256 tokenAmount + ) public { + lpToken.approve(address(saviour), tokenAmount); + saviour.deposit(safeID, tokenAmount); + } + + function doWithdraw( + SystemCoinUniswapV2SafeSaviour saviour, + uint256 safeID, + uint256 lpTokenAmount, + address dst + ) public { + saviour.withdraw(safeID, lpTokenAmount, dst); + } + + function doGetReserves( + SystemCoinUniswapV2SafeSaviour saviour, + uint256 safeID, + address dst + ) public { + saviour.getReserves(safeID, dst); + } + + function doTransferInternalCoins( + GebSafeManager manager, + uint256 safe, + address dst, + uint256 amt + ) public { + manager.transferInternalCoins(safe, dst, amt); + } + + function doSetDesiredCollateralizationRatio( + SaviourCRatioSetter cRatioSetter, + bytes32 collateralType, + uint safe, + uint cRatio + ) public { + cRatioSetter.setDesiredCollateralizationRatio(collateralType, safe, cRatio); + } +} + +contract SystemCoinUniswapV2SafeSaviourTest is DSTest { + Hevm hevm; + + UniswapV2Factory uniswapFactory; + UniswapV2Router02 uniswapRouter; + UniswapV2LiquidityManager liquidityManager; + UniswapV2Swapper swapManager; + + UniswapV2Pair stableWETHPair; + UniswapV2Pair raiSTABLEPair; + + Coin systemCoin; + WETH9_ weth; + DSToken stableToken; + + TestSAFEEngine safeEngine; + AccountingEngine accountingEngine; + LiquidationEngine liquidationEngine; + OracleRelayer oracleRelayer; + TaxCollector taxCollector; + + BasicCollateralJoin collateralJoin; + + CoinJoin coinJoin; + CoinJoin systemCoinJoin; + + EnglishCollateralAuctionHouse collateralAuctionHouse; + + GebSafeManager safeManager; + + SystemCoinUniswapV2SafeSaviour saviour; + SaviourCRatioSetter cRatioSetter; + SAFESaviourRegistry saviourRegistry; + + MockMedianizer systemCoinOracle; + MockMedianizer ethFSM; + MockMedianizer ethMedian; + + FakeUser alice; + + address me; + + // Params + uint256 initTokenAmount = 1000000 ether; + uint256 initETHUSDPrice = 250 * 10 ** 18; + uint256 initRAIUSDPrice = 4.242 * 10 ** 18; + uint256 initSTABLEUSDPrice = 10 ** 18; + + uint256 initETHPairLiquidity = 5 ether; // 1250 USD + uint256 initRAIPairLiquidity = 294.672324375E18; // 1 RAI = 4.242 USD + uint256 initSTABLEPairLiquidity = 1250 ether; // 1250 USD + + // Saviour params + bool isSystemCoinToken0; + uint256 saveCooldown = 1 days; + uint256 minKeeperPayoutValue = 1000 ether; + uint256 defaultDesiredCollateralizationRatio = 200; + uint256 minDesiredCollateralizationRatio = 155; + + // Core system params + uint256 minCRatio = 1.5 ether; + uint256 ethToMint = 5000 ether; + uint256 ethCeiling = uint(-1); + uint256 ethLiquidationPenalty = 1 ether; + + uint256 defaultLiquidityMultiplier = 100; + uint256 defaultCollateralAmount = 40 ether; + uint256 defaultTokenAmount = 100 ether; + + function setUp() public { + hevm = Hevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + hevm.warp(604411200); + + // Stable token + stableToken = new DSToken("STABLE", "STABLE"); + stableToken.mint(address(this), initTokenAmount); + + // System coin + systemCoin = new Coin("RAI", "RAI", 1); + systemCoin.mint(address(this), initTokenAmount); + systemCoinOracle = new MockMedianizer(initRAIUSDPrice, true); + + // Core system + safeEngine = new TestSAFEEngine(); + safeEngine.initializeCollateralType("eth"); + safeEngine.mint(address(this), rad(initTokenAmount)); + + ethFSM = new MockMedianizer(initETHUSDPrice, true); + ethMedian = new MockMedianizer(initETHUSDPrice, true); + ethFSM.updatePriceSource(address(ethMedian)); + + oracleRelayer = new OracleRelayer(address(safeEngine)); + oracleRelayer.modifyParameters("redemptionPrice", ray(initRAIUSDPrice)); + oracleRelayer.modifyParameters("eth", "orcl", address(ethFSM)); + oracleRelayer.modifyParameters("eth", "safetyCRatio", ray(minCRatio)); + oracleRelayer.modifyParameters("eth", "liquidationCRatio", ray(minCRatio)); + + safeEngine.addAuthorization(address(oracleRelayer)); + oracleRelayer.updateCollateralPrice("eth"); + + accountingEngine = new AccountingEngine( + address(safeEngine), address(0x1), address(0x2) + ); + safeEngine.addAuthorization(address(accountingEngine)); + + taxCollector = new TaxCollector(address(safeEngine)); + taxCollector.initializeCollateralType("eth"); + taxCollector.modifyParameters("primaryTaxReceiver", address(accountingEngine)); + taxCollector.modifyParameters("eth", "stabilityFee", 1000000564701133626865910626); // 5% / day + safeEngine.addAuthorization(address(taxCollector)); + + liquidationEngine = new LiquidationEngine(address(safeEngine)); + liquidationEngine.modifyParameters("accountingEngine", address(accountingEngine)); + + safeEngine.addAuthorization(address(liquidationEngine)); + accountingEngine.addAuthorization(address(liquidationEngine)); + + weth = new WETH9_(); + weth.deposit{value: initTokenAmount}(); + + collateralJoin = new BasicCollateralJoin(address(safeEngine), "eth", address(weth)); + + coinJoin = new CoinJoin(address(safeEngine), address(systemCoin)); + systemCoin.addAuthorization(address(coinJoin)); + safeEngine.transferInternalCoins(address(this), address(coinJoin), safeEngine.coinBalance(address(this))); + + safeEngine.addAuthorization(address(collateralJoin)); + + safeEngine.modifyParameters("eth", "debtCeiling", rad(ethCeiling)); + safeEngine.modifyParameters("globalDebtCeiling", rad(ethCeiling)); + + collateralAuctionHouse = new EnglishCollateralAuctionHouse(address(safeEngine), address(liquidationEngine), "eth"); + collateralAuctionHouse.addAuthorization(address(liquidationEngine)); + + liquidationEngine.addAuthorization(address(collateralAuctionHouse)); + liquidationEngine.modifyParameters("eth", "collateralAuctionHouse", address(collateralAuctionHouse)); + liquidationEngine.modifyParameters("eth", "liquidationPenalty", ethLiquidationPenalty); + + safeEngine.addAuthorization(address(collateralAuctionHouse)); + safeEngine.approveSAFEModification(address(collateralAuctionHouse)); + + safeManager = new GebSafeManager(address(safeEngine)); + oracleRelayer.updateCollateralPrice("eth"); + + // Uniswap setup + uniswapFactory = new UniswapV2Factory(address(this)); + createUniswapPairs(); + uniswapRouter = new UniswapV2Router02(address(uniswapFactory), address(weth)); + addPairLiquidityRouter(address(stableToken), address(weth), initSTABLEPairLiquidity * 20, initETHPairLiquidity * 20); + addPairLiquidityRouter(address(systemCoin), address(stableToken), initRAIPairLiquidity * 20, initSTABLEPairLiquidity * 20); + + // Liquidity manager + liquidityManager = new UniswapV2LiquidityManager(address(raiSTABLEPair), address(uniswapRouter)); + + // Swap manager + swapManager = new UniswapV2Swapper(address(uniswapFactory), address(uniswapRouter)); + + // Saviour infra + saviourRegistry = new SAFESaviourRegistry(saveCooldown); + cRatioSetter = new SaviourCRatioSetter(address(oracleRelayer), address(safeManager)); + cRatioSetter.setDefaultCRatio("eth", defaultDesiredCollateralizationRatio); + + saviour = new SystemCoinUniswapV2SafeSaviour( + isSystemCoinToken0, + address(coinJoin), + address(collateralJoin), + address(cRatioSetter), + address(systemCoinOracle), + address(liquidationEngine), + address(oracleRelayer), + address(safeManager), + address(saviourRegistry), + address(liquidityManager), + address(swapManager), + address(stableToken), + address(raiSTABLEPair), + minKeeperPayoutValue + ); + saviour.modifyParameters("taxCollector", address(taxCollector)); + + saviourRegistry.toggleSaviour(address(saviour)); + liquidationEngine.connectSAFESaviour(address(saviour)); + + me = address(this); + alice = new FakeUser(); + } + + // --- Math --- + function ray(uint wad) internal pure returns (uint) { + return wad * 10 ** 9; + } + function rad(uint wad) internal pure returns (uint) { + return wad * 10 ** 27; + } + function sub(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x - y) <= x); + } + + // --- Uniswap utils --- + function createUniswapPairs() internal { + // Setup STABLE/RAI pair + uniswapFactory.createPair(address(stableToken), address(systemCoin)); + raiSTABLEPair = UniswapV2Pair(uniswapFactory.getPair(address(stableToken), address(systemCoin))); + + // Setup STABLE/WETH pair + uniswapFactory.createPair(address(weth), address(stableToken)); + stableWETHPair = UniswapV2Pair(uniswapFactory.getPair(address(weth), address(stableToken))); + + if (address(raiSTABLEPair.token0()) == address(systemCoin)) isSystemCoinToken0 = true; + } + function addPairLiquidityRouter(address token1, address token2, uint256 amount1, uint256 amount2) internal { + DSToken(token1).approve(address(uniswapRouter), uint(-1)); + DSToken(token2).approve(address(uniswapRouter), uint(-1)); + uniswapRouter.addLiquidity(token1, token2, amount1, amount2, amount1, amount2, address(this), now); + UniswapV2Pair updatedPair = UniswapV2Pair(uniswapFactory.getPair(token1, token2)); + updatedPair.sync(); + } + function addPairLiquidityTransfer(UniswapV2Pair pair, address token1, address token2, uint256 amount1, uint256 amount2) internal { + DSToken(token1).transfer(address(pair), amount1); + DSToken(token2).transfer(address(pair), amount2); + pair.sync(); + } + + // --- Default actions/scenarios --- + function default_create_liquidatable_position(uint256 desiredCRatio, uint256 liquidatableCollateralPrice) internal returns (address) { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, desiredCRatio); + assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); + + ethMedian.updateCollateralPrice(liquidatableCollateralPrice); + ethFSM.updateCollateralPrice(liquidatableCollateralPrice); + oracleRelayer.updateCollateralPrice("eth"); + + return safeHandler; + } + function default_save(uint256 safe, address safeHandler, uint desiredCRatio) internal { + default_modify_collateralization(safe, safeHandler); + + alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, desiredCRatio); + assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); + + ethMedian.updateCollateralPrice(initETHUSDPrice / 30); + ethFSM.updateCollateralPrice(initETHUSDPrice / 30); + oracleRelayer.updateCollateralPrice("eth"); + + uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(stableToken), initRAIPairLiquidity * defaultLiquidityMultiplier, initSTABLEPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); + raiSTABLEPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), safe, lpTokenAmount); + assertEq(raiSTABLEPair.balanceOf(address(saviour)), lpTokenAmount); + assertTrue(saviour.canSave("eth", safeHandler)); + + liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(100000 ether)); + liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); + + uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); + uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); + (uint256 oldSysCoinReserve, uint256 oldCollateralReserve) = saviour.underlyingReserves(safeHandler); + + uint auction = liquidationEngine.liquidateSAFE("eth", safeHandler); + (uint256 sysCoinReserve, uint256 collateralReserve) = saviour.underlyingReserves(safeHandler); + + assertEq(auction, 0); + assertEq(stableToken.balanceOf(address(saviour)), 0); + assertEq(stableToken.balanceOf(address(liquidityManager)), 0); + assertTrue( + sysCoinReserve > oldSysCoinReserve || + collateralReserve > oldCollateralReserve + ); + assertTrue( + systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance > 0 || + weth.balanceOf(address(this)) - preSaveWETHKeeperBalance > 0 + ); + assertEq(raiSTABLEPair.balanceOf(address(saviour)), 0); + assertEq(raiSTABLEPair.balanceOf(address(liquidityManager)), 0); + assertEq(saviour.lpTokenCover(safeHandler), 0); + + (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", safeHandler); + (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("eth"); + assertEq(lockedCollateral * ray(ethFSM.read()) * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10 ** 27), desiredCRatio); + } + function default_second_save(uint256 safe, address safeHandler, uint desiredCRatio) internal { + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, desiredCRatio); + + ethMedian.updateCollateralPrice(initETHUSDPrice / 40); + ethFSM.updateCollateralPrice(initETHUSDPrice / 40); + oracleRelayer.updateCollateralPrice("eth"); + + uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(stableToken), initRAIPairLiquidity * defaultLiquidityMultiplier, initSTABLEPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); + raiSTABLEPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), safe, lpTokenAmount); + assertEq(raiSTABLEPair.balanceOf(address(saviour)), lpTokenAmount); + assertTrue(saviour.canSave("eth", safeHandler)); + + liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(111 ether)); + liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); + + uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); + uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); + (uint256 oldSysCoinReserve, uint256 oldCollateralReserve) = saviour.underlyingReserves(safeHandler); + + uint auction = liquidationEngine.liquidateSAFE("eth", safeHandler); + (uint256 sysCoinReserve, uint256 collateralReserve) = saviour.underlyingReserves(safeHandler); + + assertEq(auction, 0); + assertEq(stableToken.balanceOf(address(saviour)), 0); + assertEq(stableToken.balanceOf(address(liquidityManager)), 0); + assertTrue( + sysCoinReserve > oldSysCoinReserve || + collateralReserve > oldCollateralReserve + ); + assertTrue( + systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance > 0 || + weth.balanceOf(address(this)) - preSaveWETHKeeperBalance > 0 + ); + assertEq(raiSTABLEPair.balanceOf(address(saviour)), 0); + assertEq(raiSTABLEPair.balanceOf(address(liquidityManager)), 0); + assertEq(saviour.lpTokenCover(safeHandler), 0); + + (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", safeHandler); + (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("eth"); + uint256 cRatio = lockedCollateral * ray(ethFSM.read()) * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10 ** 27); + assertTrue(cRatio == desiredCRatio || cRatio == desiredCRatio - 1); + } + function default_liquidate_safe(address safeHandler) internal { + liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(100000 ether)); + liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); + + uint auction = liquidationEngine.liquidateSAFE("eth", safeHandler); + // the full SAFE is liquidated + (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", me); + assertEq(lockedCollateral, 0); + assertEq(generatedDebt, 0); + // all debt goes to the accounting engine + assertTrue(accountingEngine.totalQueuedDebt() > 0); + // auction is for all collateral + (,uint amountToSell,,,,,, uint256 amountToRaise) = collateralAuctionHouse.bids(auction); + assertEq(amountToSell, defaultCollateralAmount); + assertEq(amountToRaise, rad(1100 ether)); + } + function default_create_liquidatable_position_deposit_cover(uint256 desiredCRatio, uint256 liquidatableCollateralPrice) + internal returns (address) { + // Create position + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, desiredCRatio); + assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); + + // Change oracle price + ethMedian.updateCollateralPrice(liquidatableCollateralPrice); + ethFSM.updateCollateralPrice(liquidatableCollateralPrice); + oracleRelayer.updateCollateralPrice("eth"); + + // Deposit cover + uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(stableToken), initRAIPairLiquidity * defaultLiquidityMultiplier, initSTABLEPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); + raiSTABLEPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), safe, lpTokenAmount); + assertEq(raiSTABLEPair.balanceOf(address(saviour)), lpTokenAmount); + assertEq(saviour.lpTokenCover(safeHandler), lpTokenAmount); + + return safeHandler; + } + function default_create_position_deposit_cover() internal returns (uint, address) { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + // Deposit cover + uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(stableToken), initRAIPairLiquidity * defaultLiquidityMultiplier, initSTABLEPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); + raiSTABLEPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), safe, lpTokenAmount); + assertEq(raiSTABLEPair.balanceOf(address(saviour)), lpTokenAmount); + assertEq(saviour.lpTokenCover(safeHandler), lpTokenAmount); + + return (safe, safeHandler); + } + function default_modify_collateralization(uint256 safe, address safeHandler) internal { + weth.approve(address(collateralJoin), uint(-1)); + collateralJoin.join(address(safeHandler), defaultTokenAmount); + alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 10)); + } + + // --- Tests --- + function test_setup() public { + assertEq(saviour.authorizedAccounts(address(this)), 1); + assertTrue(saviour.isSystemCoinToken0() == isSystemCoinToken0); + assertEq(saviour.minKeeperPayoutValue(), minKeeperPayoutValue); + assertEq(saviour.restrictUsage(), 0); + + assertEq(address(saviour.coinJoin()), address(coinJoin)); + assertEq(address(saviour.collateralJoin()), address(collateralJoin)); + assertEq(address(saviour.cRatioSetter()), address(cRatioSetter)); + assertEq(address(saviour.liquidationEngine()), address(liquidationEngine)); + assertEq(address(saviour.oracleRelayer()), address(oracleRelayer)); + assertEq(address(saviour.systemCoinOrcl()), address(systemCoinOracle)); + assertEq(address(saviour.systemCoin()), address(systemCoin)); + assertEq(address(saviour.safeEngine()), address(safeEngine)); + assertEq(address(saviour.safeManager()), address(safeManager)); + assertEq(address(saviour.saviourRegistry()), address(saviourRegistry)); + assertEq(address(saviour.liquidityManager()), address(liquidityManager)); + assertEq(address(saviour.swapManager()), address(swapManager)); + assertEq(address(saviour.lpToken()), address(raiSTABLEPair)); + assertEq(address(saviour.collateralToken()), address(weth)); + assertEq(address(saviour.pairToken()), address(stableToken)); + } + function test_modify_uints() public { + saviour.modifyParameters("minKeeperPayoutValue", 5); + saviour.modifyParameters("restrictUsage", 1); + + assertEq(saviour.minKeeperPayoutValue(), 5); + assertEq(saviour.restrictUsage(), 1); + } + function testFail_modify_uint_unauthed() public { + alice.doModifyParameters(saviour, "minKeeperPayoutValue", 5); + } + function test_modify_addresses() public { + saviour.modifyParameters("systemCoinOrcl", address(systemCoinOracle)); + saviour.modifyParameters("oracleRelayer", address(oracleRelayer)); + saviour.modifyParameters("liquidityManager", address(liquidityManager)); + saviour.modifyParameters("swapManager", address(swapManager)); + + assertEq(address(saviour.liquidityManager()), address(liquidityManager)); + assertEq(address(saviour.oracleRelayer()), address(oracleRelayer)); + assertEq(address(saviour.systemCoinOrcl()), address(systemCoinOracle)); + assertEq(address(saviour.swapManager()), address(swapManager)); + } + function testFail_modify_address_unauthed() public { + alice.doModifyParameters(saviour, "systemCoinOrcl", address(systemCoinOracle)); + } + function testFail_deposit_liq_engine_not_approved() public { + liquidationEngine.disconnectSAFESaviour(address(saviour)); + + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(stableToken), initRAIPairLiquidity * defaultLiquidityMultiplier, initSTABLEPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); + raiSTABLEPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), 1, lpTokenAmount); + } + function testFail_deposit_null_lp_token_amount() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(stableToken), initRAIPairLiquidity * defaultLiquidityMultiplier, initSTABLEPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); + raiSTABLEPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), 1, 0); + } + function testFail_deposit_inexistent_safe() public { + uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(stableToken), initRAIPairLiquidity * defaultLiquidityMultiplier, initSTABLEPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); + raiSTABLEPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), 1, lpTokenAmount); + } + function test_deposit_twice() public { + uint256 initialLPSupply = raiSTABLEPair.totalSupply(); + + (uint safe, address safeHandler) = default_create_position_deposit_cover(); + + // Second deposit + uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(stableToken), initRAIPairLiquidity * defaultLiquidityMultiplier, initSTABLEPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); + raiSTABLEPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), safe, lpTokenAmount); + + // Checks + assertTrue(raiSTABLEPair.balanceOf(address(saviour)) > 0 && saviour.lpTokenCover(safeHandler) > 0); + assertEq(saviour.lpTokenCover(safeHandler), raiSTABLEPair.totalSupply() - initialLPSupply); + assertEq(raiSTABLEPair.balanceOf(address(saviour)), raiSTABLEPair.totalSupply() - initialLPSupply); + } + function test_deposit_after_everything_withdrawn() public { + (uint safe, address safeHandler) = default_create_position_deposit_cover(); + + // Withdraw + uint256 currentLPBalanceAlice = raiSTABLEPair.balanceOf(address(alice)); + uint256 currentLPBalanceSaviour = raiSTABLEPair.balanceOf(address(saviour)); + alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler), address(alice)); + + // Checks + assertEq(raiSTABLEPair.balanceOf(address(alice)), currentLPBalanceAlice + currentLPBalanceSaviour); + assertTrue(raiSTABLEPair.balanceOf(address(saviour)) == 0 && saviour.lpTokenCover(safeHandler) == 0); + + // Deposit again + alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), safe, currentLPBalanceSaviour); + + // Checks + assertTrue(raiSTABLEPair.balanceOf(address(saviour)) > 0 && saviour.lpTokenCover(safeHandler) > 0); + assertEq(saviour.lpTokenCover(safeHandler), currentLPBalanceSaviour); + assertEq(raiSTABLEPair.balanceOf(address(saviour)), currentLPBalanceSaviour); + assertEq(raiSTABLEPair.balanceOf(address(alice)), currentLPBalanceAlice); + } + function testFail_withdraw_unauthorized() public { + (uint safe, ) = default_create_position_deposit_cover(); + + // Withdraw by unauthed + FakeUser bob = new FakeUser(); + bob.doWithdraw(saviour, safe, raiSTABLEPair.balanceOf(address(saviour)), address(bob)); + } + function testFail_withdraw_more_than_deposited() public { + (uint safe, address safeHandler) = default_create_position_deposit_cover(); + uint256 currentLPBalance = raiSTABLEPair.balanceOf(address(this)); + alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler) + 1, address(this)); + } + function testFail_withdraw_null() public { + (uint safe, address safeHandler) = default_create_position_deposit_cover(); + alice.doWithdraw(saviour, safe, 0, address(this)); + } + function test_withdraw() public { + (uint safe, address safeHandler) = default_create_position_deposit_cover(); + + // Withdraw + uint256 currentLPBalanceAlice = raiSTABLEPair.balanceOf(address(alice)); + uint256 currentLPBalanceSaviour = raiSTABLEPair.balanceOf(address(saviour)); + alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler), address(alice)); + + // Checks + assertEq(raiSTABLEPair.balanceOf(address(alice)), currentLPBalanceAlice + currentLPBalanceSaviour); + assertTrue(raiSTABLEPair.balanceOf(address(saviour)) == 0 && saviour.lpTokenCover(safeHandler) == 0); + } + function test_withdraw_twice() public { + (uint safe, address safeHandler) = default_create_position_deposit_cover(); + + // Withdraw once + uint256 currentLPBalanceAlice = raiSTABLEPair.balanceOf(address(alice)); + uint256 currentLPBalanceSaviour = raiSTABLEPair.balanceOf(address(saviour)); + alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler) / 2, address(alice)); + + // Checks + assertEq(raiSTABLEPair.balanceOf(address(alice)), currentLPBalanceAlice + currentLPBalanceSaviour / 2); + assertTrue(raiSTABLEPair.balanceOf(address(saviour)) == currentLPBalanceSaviour / 2 && saviour.lpTokenCover(safeHandler) == currentLPBalanceSaviour / 2); + + // Withdraw again + alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler), address(alice)); + + // Checks + assertEq(raiSTABLEPair.balanceOf(address(alice)), currentLPBalanceAlice + currentLPBalanceSaviour); + assertTrue(raiSTABLEPair.balanceOf(address(saviour)) == 0 && saviour.lpTokenCover(safeHandler) == 0); + } + function test_withdraw_custom_dst() public { + (uint safe, address safeHandler) = default_create_position_deposit_cover(); + + // Withdraw + uint256 currentLPBalanceAlice = raiSTABLEPair.balanceOf(address(0xb1)); + uint256 currentLPBalanceSaviour = raiSTABLEPair.balanceOf(address(saviour)); + alice.doWithdraw(saviour, safe, saviour.lpTokenCover(safeHandler), address(0xb1)); + + // Checks + assertEq(raiSTABLEPair.balanceOf(address(0xb1)), currentLPBalanceSaviour); + assertEq(raiSTABLEPair.balanceOf(address(alice)), currentLPBalanceAlice); + assertTrue(raiSTABLEPair.balanceOf(address(saviour)) == 0 && saviour.lpTokenCover(safeHandler) == 0); + } + function test_tokenAmountUsedToSave() public { + (uint safe, address safeHandler) = default_create_position_deposit_cover(); + + assertEq(saviour.lpTokenCover(safeHandler), saviour.tokenAmountUsedToSave("eth", safeHandler)); + } + function test_getCollateralPrice_zero_price() public { + ethFSM.updateCollateralPrice(0); + assertEq(saviour.getCollateralPrice(), 0); + } + function test_getCollateralPrice_invalid() public { + ethFSM.changeValidity(); + assertEq(saviour.getCollateralPrice(), 0); + } + function test_getCollateralPrice_null_fsm() public { + oracleRelayer.modifyParameters("eth", "orcl", address(0)); + assertEq(saviour.getCollateralPrice(), 0); + } + function test_getCollateralPrice() public { + assertEq(saviour.getCollateralPrice(), initETHUSDPrice); + } + function test_getSystemCoinMarketPrice_invalid() public { + systemCoinOracle.changeValidity(); + assertEq(saviour.getSystemCoinMarketPrice(), 0); + } + function test_getSystemCoinMarketPrice_null_price() public { + systemCoinOracle.updateCollateralPrice(0); + assertEq(saviour.getSystemCoinMarketPrice(), 0); + } + function test_getSystemCoinMarketPrice() public { + assertEq(saviour.getSystemCoinMarketPrice(), initRAIUSDPrice); + } + function test_getTargetCRatio_inexistent_handler() public { + assertEq(saviour.getTargetCRatio(address(0x1)), defaultDesiredCollateralizationRatio); + } + function test_getTargetCRatio_no_custom_desired_ratio() public { + (uint safe, address safeHandler) = default_create_position_deposit_cover(); + assertEq(saviour.getTargetCRatio(safeHandler), defaultDesiredCollateralizationRatio); + } + function test_getTargetCRatio_custom_desired_ratio() public { + (uint safe, address safeHandler) = default_create_position_deposit_cover(); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, defaultDesiredCollateralizationRatio * 2); + assertEq(saviour.getTargetCRatio(safeHandler), defaultDesiredCollateralizationRatio * 2); + } + function test_getLPUnderlying_inexistent_handler() public { + (uint sysCoins, uint collateral) = saviour.getLPUnderlying(address(0x1)); + assertEq(sysCoins, collateral); + assertEq(sysCoins, 0); + } + function test_getLPUnderlying() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(stableToken), initRAIPairLiquidity * defaultLiquidityMultiplier, initSTABLEPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); + raiSTABLEPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), safe, lpTokenAmount); + + (uint sysCoins, uint collateral) = saviour.getLPUnderlying(safeHandler); + assertEq(sysCoins, initRAIPairLiquidity * defaultLiquidityMultiplier); + assertTrue(collateral > 0); + } + function test_getTokensForSaving_no_cover() public { + (uint sysCoins, uint collateral) = saviour.getTokensForSaving(address(0x1), oracleRelayer.redemptionPrice()); + + assertEq(sysCoins, collateral); + assertEq(sysCoins, 0); + } + function test_getTokensForSaving_null_redemption() public { + address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); + (uint sysCoins, uint collateral) = saviour.getTokensForSaving(safeHandler, 0); + + assertEq(sysCoins, collateral); + assertEq(sysCoins, 0); + } + function test_getTokensForSaving_null_collateral_price() public { + address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); + ethFSM.updateCollateralPrice(0); + (uint sysCoins, uint collateral) = saviour.getTokensForSaving(safeHandler, oracleRelayer.redemptionPrice()); + + assertEq(sysCoins, collateral); + assertEq(sysCoins, 0); + } + function test_getTokensForSaving_save_only_with_sys_coins() public { + address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); + (uint sysCoins, uint collateral) = saviour.getTokensForSaving(safeHandler, oracleRelayer.redemptionPrice()); + + assertTrue(sysCoins > 0); + assertEq(collateral, 0); + } + function test_getTokensForSaving_both_tokens_used() public { + // Create position + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 200); + assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); + + // Change oracle price + ethMedian.updateCollateralPrice(initETHUSDPrice / 3); + ethFSM.updateCollateralPrice(initETHUSDPrice / 3); + oracleRelayer.updateCollateralPrice("eth"); + + // Deposit cover + uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(stableToken), initRAIPairLiquidity * 2, initSTABLEPairLiquidity * 2 + ); + lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); + raiSTABLEPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), safe, lpTokenAmount); + + (uint sysCoins, uint collateral) = saviour.getTokensForSaving(safeHandler, oracleRelayer.redemptionPrice()); + + assertTrue(sysCoins > 0); + assertTrue(collateral > 0); + } + function test_getTokensForSaving_not_enough_lp_collateral() public { + // Create position + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + + weth.approve(address(collateralJoin), uint(-1)); + collateralJoin.join(address(safeHandler), defaultTokenAmount); + alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 10)); + + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 155); + assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); + + // Change oracle price + ethMedian.updateCollateralPrice(initETHUSDPrice / 30); + ethFSM.updateCollateralPrice(initETHUSDPrice / 30); + oracleRelayer.updateCollateralPrice("eth"); + + // Deposit cover + uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(stableToken), initRAIPairLiquidity / 5, initSTABLEPairLiquidity / 5 + ); + + lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); + raiSTABLEPair.transfer(address(alice), lpTokenAmount / 10); + + alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), safe, lpTokenAmount / 10); + + (uint sysCoins, uint collateral) = saviour.getTokensForSaving(safeHandler, oracleRelayer.redemptionPrice()); + + assertEq(sysCoins, 0); + assertEq(collateral, 0); + } + function test_getKeeperPayoutTokens_null_collateral_price() public { + address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); + ethFSM.updateCollateralPrice(0); + + (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), 0, 0); + + assertEq(sysCoins, 0); + assertEq(collateral, 0); + } + function test_getKeeperPayoutTokens_null_sys_coin_price() public { + address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); + systemCoinOracle.updateCollateralPrice(0); + + (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), 0, 0); + + assertEq(sysCoins, 0); + assertEq(collateral, 0); + } + function test_getKeeperPayoutTokens_only_sys_coins_used() public { + address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); + (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), 0, 0); + + assertEq(sysCoins, minKeeperPayoutValue * 10 ** 18 / systemCoinOracle.read()); + assertEq(collateral, 0); + } + function test_getKeeperPayoutTokens_only_collateral_used() public { + (, address safeHandler) = default_create_position_deposit_cover(); + (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), uint(-1), 0); + + assertEq(sysCoins, 0); + assertEq(collateral, minKeeperPayoutValue * 10 ** 18 / ethFSM.read()); + } + function test_getKeeperPayoutTokens_both_tokens_used() public { + (, address safeHandler) = default_create_position_deposit_cover(); + (uint underlyingSysCoins, ) = saviour.getLPUnderlying(safeHandler); + + (uint sysCoins, uint collateral) = + saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), underlyingSysCoins - (minKeeperPayoutValue * 10 ** 18 / (systemCoinOracle.read() * 2)), 0); + + assertEq(sysCoins, (minKeeperPayoutValue * 10 ** 18 / (systemCoinOracle.read() * 2))); + assertEq(collateral, 2 ether); + } + function test_getKeeperPayoutTokens_not_enough_tokens_to_pay() public { + saviour.modifyParameters("minKeeperPayoutValue", 10000000 ether); + + (, address safeHandler) = default_create_position_deposit_cover(); + (uint sysCoins, uint collateral) = saviour.getKeeperPayoutTokens(safeHandler, oracleRelayer.redemptionPrice(), 0, 0); + + assertEq(sysCoins, 0); + assertEq(collateral, 0); + } + function test_canSave_cannot_save_safe() public { + // Create position + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + + weth.approve(address(collateralJoin), uint(-1)); + collateralJoin.join(address(safeHandler), defaultTokenAmount); + alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 10)); + + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 155); + assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); + + // Change oracle price + ethMedian.updateCollateralPrice(initETHUSDPrice / 30); + ethFSM.updateCollateralPrice(initETHUSDPrice / 30); + oracleRelayer.updateCollateralPrice("eth"); + + // Deposit cover + uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(stableToken), initRAIPairLiquidity / 5, initSTABLEPairLiquidity / 5 + ); + lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); + raiSTABLEPair.transfer(address(alice), lpTokenAmount / 10); + + alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), safe, lpTokenAmount / 10); + + assertTrue(!saviour.canSave("eth", safeHandler)); + } + function test_canSave_cannot_pay_keeper() public { + saviour.modifyParameters("minKeeperPayoutValue", 10000000 ether); + address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); + assertTrue(!saviour.canSave("eth", safeHandler)); + } + function test_canSave_both_tokens_used() public { + // Create position + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 200); + assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); + + // Change oracle price + ethMedian.updateCollateralPrice(initETHUSDPrice / 3); + ethFSM.updateCollateralPrice(initETHUSDPrice / 3); + oracleRelayer.updateCollateralPrice("eth"); + + // Deposit cover + uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(stableToken), initRAIPairLiquidity * 2, initSTABLEPairLiquidity * 2 + ); + lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); + raiSTABLEPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), safe, lpTokenAmount); + saviour.modifyParameters("minKeeperPayoutValue", 50 ether); + + assertTrue(saviour.canSave("eth", safeHandler)); + } + function test_canSave() public { + address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); + assertTrue(saviour.canSave("eth", safeHandler)); + } + function testFail_saveSAFE_invalid_caller() public { + address safeHandler = default_create_liquidatable_position_deposit_cover(200, initETHUSDPrice / 30); + saviour.saveSAFE(address(this), "eth", safeHandler); + } + function test_saveSAFE_no_cover() public { + address safeHandler = default_create_liquidatable_position(200, initETHUSDPrice / 30); + default_liquidate_safe(safeHandler); + } + function test_saveSAFE_cannot_save_safe() public { + // Create position + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + + weth.approve(address(collateralJoin), uint(-1)); + collateralJoin.join(address(safeHandler), defaultTokenAmount); + alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 10)); + + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 155); + assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); + + // Change oracle price + ethMedian.updateCollateralPrice(initETHUSDPrice / 30); + ethFSM.updateCollateralPrice(initETHUSDPrice / 30); + oracleRelayer.updateCollateralPrice("eth"); + + // Deposit cover + uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(stableToken), initRAIPairLiquidity / 5, initSTABLEPairLiquidity / 5 + ); + lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); + raiSTABLEPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), safe, lpTokenAmount / 10); + + default_liquidate_safe(safeHandler); + } + function test_saveSAFE_cannot_pay_keeper() public { + address safeHandler = default_create_liquidatable_position(200, initETHUSDPrice / 30); + saviour.modifyParameters("minKeeperPayoutValue", 10000000 ether); + default_liquidate_safe(safeHandler); + } + function test_saveSAFE() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_save(safe, safeHandler, 200); + } + function test_saveSAFE_accumulate_rate() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + + // Warp and save + hevm.warp(now + 2 days); + taxCollector.taxSingle("eth"); + + weth.approve(address(collateralJoin), uint(-1)); + collateralJoin.join(address(safeHandler), defaultTokenAmount); + alice.doModifySAFECollateralization(safeManager, safe, int(defaultCollateralAmount), int(defaultTokenAmount * 8)); + + alice.doTransferInternalCoins(safeManager, safe, address(coinJoin), safeEngine.coinBalance(safeHandler)); + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 200); + assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); + + ethMedian.updateCollateralPrice(initETHUSDPrice / 30); + ethFSM.updateCollateralPrice(initETHUSDPrice / 30); + oracleRelayer.updateCollateralPrice("eth"); + + uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(stableToken), initRAIPairLiquidity * defaultLiquidityMultiplier, initSTABLEPairLiquidity * defaultLiquidityMultiplier + ); + lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); + raiSTABLEPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), safe, lpTokenAmount); + assertEq(raiSTABLEPair.balanceOf(address(saviour)), lpTokenAmount); + assertTrue(saviour.canSave("eth", safeHandler)); + + liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(100000 ether)); + liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); + + uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); + uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); + (uint256 oldSysCoinReserve, uint256 oldCollateralReserve) = saviour.underlyingReserves(safeHandler); + + uint auction = liquidationEngine.liquidateSAFE("eth", safeHandler); + (uint256 sysCoinReserve, uint256 collateralReserve) = saviour.underlyingReserves(safeHandler); + + assertEq(auction, 0); + assertEq(stableToken.balanceOf(address(saviour)), 0); + assertEq(stableToken.balanceOf(address(liquidityManager)), 0); + assertTrue( + sysCoinReserve > oldSysCoinReserve || + collateralReserve > oldCollateralReserve + ); + assertTrue( + systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance > 0 || + weth.balanceOf(address(this)) - preSaveWETHKeeperBalance > 0 + ); + assertEq(raiSTABLEPair.balanceOf(address(saviour)), 0); + assertEq(raiSTABLEPair.balanceOf(address(liquidityManager)), 0); + assertEq(saviour.lpTokenCover(safeHandler), 0); + + (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", safeHandler); + (, uint accumulatedRate, , , , ) = safeEngine.collateralTypes("eth"); + assertEq(lockedCollateral * ray(ethFSM.read()) * 100 / (generatedDebt * oracleRelayer.redemptionPrice() * accumulatedRate / 10 ** 27), 200); + } + function test_saveSAFE_both_tokens() public { + // Create position + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_modify_collateralization(safe, safeHandler); + + alice.doProtectSAFE(safeManager, safe, address(liquidationEngine), address(saviour)); + alice.doSetDesiredCollateralizationRatio(cRatioSetter, "eth", safe, 200); + assertEq(liquidationEngine.chosenSAFESaviour("eth", safeHandler), address(saviour)); + + // Change oracle price + ethMedian.updateCollateralPrice(initETHUSDPrice / 3); + ethFSM.updateCollateralPrice(initETHUSDPrice / 3); + oracleRelayer.updateCollateralPrice("eth"); + + // Deposit cover + uint256 lpTokenAmount = raiSTABLEPair.balanceOf(address(this)); + addPairLiquidityRouter( + address(systemCoin), address(stableToken), initRAIPairLiquidity * 2, initSTABLEPairLiquidity * 2 + ); + lpTokenAmount = sub(raiSTABLEPair.balanceOf(address(this)), lpTokenAmount); + raiSTABLEPair.transfer(address(alice), lpTokenAmount); + + alice.doDeposit(saviour, DSToken(address(raiSTABLEPair)), safe, lpTokenAmount); + saviour.modifyParameters("minKeeperPayoutValue", 50 ether); + + assertTrue(saviour.canSave("eth", safeHandler)); + + liquidationEngine.modifyParameters("eth", "liquidationQuantity", rad(100000 ether)); + liquidationEngine.modifyParameters("eth", "liquidationPenalty", 1.1 ether); + + uint256 preSaveSysCoinKeeperBalance = systemCoin.balanceOf(address(this)); + uint256 preSaveWETHKeeperBalance = weth.balanceOf(address(this)); + (uint256 oldSysCoinReserve, uint256 oldCollateralReserve) = saviour.underlyingReserves(safeHandler); + + uint auction = liquidationEngine.liquidateSAFE("eth", safeHandler); + (uint256 sysCoinReserve, uint256 collateralReserve) = saviour.underlyingReserves(safeHandler); + + assertEq(auction, 0); + assertEq(stableToken.balanceOf(address(saviour)), 0); + assertEq(stableToken.balanceOf(address(liquidityManager)), 0); + assertTrue( + sysCoinReserve > oldSysCoinReserve || + collateralReserve > oldCollateralReserve + ); + assertTrue( + systemCoin.balanceOf(address(this)) - preSaveSysCoinKeeperBalance > 0 || + weth.balanceOf(address(this)) - preSaveWETHKeeperBalance > 0 + ); + assertEq(raiSTABLEPair.balanceOf(address(saviour)), 0); + assertEq(raiSTABLEPair.balanceOf(address(liquidityManager)), 0); + assertEq(saviour.lpTokenCover(safeHandler), 0); + + (uint lockedCollateral, uint generatedDebt) = safeEngine.safes("eth", safeHandler); + assertEq(lockedCollateral * ray(ethFSM.read()) * 100 / (generatedDebt * oracleRelayer.redemptionPrice()), 199); + } + function test_saveSAFE_twice() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_save(safe, safeHandler, 155); + + hevm.warp(now + saviourRegistry.saveCooldown() + 1); + default_second_save(safe, safeHandler, 200); + } + function testFail_saveSAFE_withdraw_cover() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_save(safe, safeHandler, 155); + + alice.doWithdraw(saviour, safe, 1, address(this)); + } + function test_saveSAFE_get_reserves() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_save(safe, safeHandler, 155); + + uint256 oldSysCoinBalance = systemCoin.balanceOf(address(alice)); + uint256 oldCollateralBalance = weth.balanceOf(address(alice)); + + (uint sysCoinReserve, uint collateralReserve) = saviour.underlyingReserves(safeHandler); + + alice.doGetReserves(saviour, safe, address(alice)); + assertTrue(systemCoin.balanceOf(address(alice)) - sysCoinReserve == oldSysCoinBalance); + assertTrue(weth.balanceOf(address(alice)) - collateralReserve == oldCollateralBalance); + + assertEq(systemCoin.balanceOf(address(saviour)), 0); + assertEq(weth.balanceOf(address(saviour)), 0); + } + function testFail_save_twice_without_waiting() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_save(safe, safeHandler, 155); + default_second_save(safe, safeHandler, 200); + } + function test_saveSAFE_get_reserves_twice() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_save(safe, safeHandler, 155); + alice.doGetReserves(saviour, safe, address(alice)); + + hevm.warp(now + saviourRegistry.saveCooldown() + 1); + default_second_save(safe, safeHandler, 200); + + uint256 oldSysCoinBalance = systemCoin.balanceOf(address(0x1)); + uint256 oldCollateralBalance = weth.balanceOf(address(0x1)); + + (uint sysCoinReserve, uint collateralReserve) = saviour.underlyingReserves(safeHandler); + + alice.doGetReserves(saviour, safe, address(0x1)); + assertTrue(systemCoin.balanceOf(address(0x1)) - sysCoinReserve == oldSysCoinBalance); + assertTrue(weth.balanceOf(address(0x1)) - collateralReserve == oldCollateralBalance); + + assertEq(systemCoin.balanceOf(address(saviour)), 0); + assertEq(weth.balanceOf(address(saviour)), 0); + } + function testFail_getReserves_invalid_caller() public { + uint safe = alice.doOpenSafe(safeManager, "eth", address(alice)); + address safeHandler = safeManager.safes(safe); + default_save(safe, safeHandler, 155); + + saviour.getReserves(safe, address(alice)); + } +} diff --git a/src/test/UniswapV2LiquidityManager.t.sol b/src/test/UniswapV2LiquidityManager.t.sol index 4567c3d..981dbc1 100644 --- a/src/test/UniswapV2LiquidityManager.t.sol +++ b/src/test/UniswapV2LiquidityManager.t.sol @@ -1,200 +1,200 @@ -pragma solidity 0.6.7; - -import "ds-test/test.sol"; -import "ds-weth/weth9.sol"; -import "ds-token/token.sol"; - -import "../integrations/uniswap/uni-v2/UniswapV2Factory.sol"; -import "../integrations/uniswap/uni-v2/UniswapV2Pair.sol"; -import "../integrations/uniswap/uni-v2/UniswapV2Router02.sol"; - -import "../integrations/uniswap/liquidity-managers/UniswapV2LiquidityManager.sol"; - -abstract contract Hevm { - function warp(uint256) virtual public; -} - -contract UniswapV2LiquidityManagerTest is DSTest { - Hevm hevm; - - UniswapV2Factory uniswapFactory; - UniswapV2Router02 uniswapRouter; - UniswapV2LiquidityManager liquidityManager; - - UniswapV2Pair raiWETHPair; - - DSToken systemCoin; - WETH9_ weth; - - // Params - bool isSystemCoinToken0; - uint256 initTokenAmount = 100000 ether; - uint256 initETHUSDPrice = 250 * 10 ** 18; - uint256 initRAIUSDPrice = 4.242 * 10 ** 18; - - uint256 initETHRAIPairLiquidity = 5 ether; // 1250 USD - uint256 initRAIETHPairLiquidity = 294.672324375E18; // 1 RAI = 4.242 USD - - uint256 ethRAISimulationExtraRAI = 100 ether; - uint256 ethRAISimulationExtraETH = 0.5 ether; - - uint256 initialLPTokens; - - function setUp() public { - hevm = Hevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); - hevm.warp(604411200); - - // Tokens - systemCoin = new DSToken("RAI", "RAI"); - systemCoin.mint(address(this), initTokenAmount); - - weth = new WETH9_(); - weth.deposit{value: initTokenAmount}(); - - // Uniswap setup - uniswapFactory = new UniswapV2Factory(address(this)); - createUniswapPair(); - uniswapRouter = new UniswapV2Router02(address(uniswapFactory), address(weth)); - addPairLiquidityRouter(address(systemCoin), address(weth), initRAIETHPairLiquidity, initETHRAIPairLiquidity); - initialLPTokens = raiWETHPair.balanceOf(address(this)); - - // Liquidity manager - liquidityManager = new UniswapV2LiquidityManager(address(raiWETHPair), address(uniswapRouter)); - } - - // --- Uniswap utils --- - function createUniswapPair() internal { - // Setup WETH/RAI pair - uniswapFactory.createPair(address(weth), address(systemCoin)); - raiWETHPair = UniswapV2Pair(uniswapFactory.getPair(address(weth), address(systemCoin))); - - if (address(raiWETHPair.token0()) == address(systemCoin)) isSystemCoinToken0 = true; - } - function addPairLiquidityRouter(address token1, address token2, uint256 amount1, uint256 amount2) internal { - DSToken(token1).approve(address(uniswapRouter), uint(-1)); - DSToken(token2).approve(address(uniswapRouter), uint(-1)); - uniswapRouter.addLiquidity(token1, token2, amount1, amount2, amount1, amount2, address(this), now); - UniswapV2Pair updatedPair = UniswapV2Pair(uniswapFactory.getPair(token1, token2)); - updatedPair.sync(); - } - function addPairLiquidityRouterNoSync(address token1, address token2, uint256 amount1, uint256 amount2) internal { - DSToken(token1).approve(address(uniswapRouter), uint(-1)); - DSToken(token2).approve(address(uniswapRouter), uint(-1)); - uniswapRouter.addLiquidity(token1, token2, amount1, amount2, amount1, amount2, address(this), now); - UniswapV2Pair updatedPair = UniswapV2Pair(uniswapFactory.getPair(token1, token2)); - } - function addPairLiquidityTransfer(UniswapV2Pair pair, address token1, address token2, uint256 amount1, uint256 amount2) internal { - DSToken(token1).transfer(address(pair), amount1); - DSToken(token2).transfer(address(pair), amount2); - pair.sync(); - } - function addPairLiquidityTransferNoSync(UniswapV2Pair pair, address token1, address token2, uint256 amount1, uint256 amount2) internal { - DSToken(token1).transfer(address(pair), amount1); - DSToken(token2).transfer(address(pair), amount2); - } - - // --- Tests --- - function test_getToken0FromLiquidity_zero() public { - assertEq(liquidityManager.getToken0FromLiquidity(0), 0); - } - function test_getToken0FromLiquidity() public { - uint256 tokenAmount = (isSystemCoinToken0) ? initRAIETHPairLiquidity : initETHRAIPairLiquidity; - assertTrue(liquidityManager.getToken0FromLiquidity(initialLPTokens) >= tokenAmount - 10 ** 10); - assertTrue(liquidityManager.getToken0FromLiquidity(initialLPTokens / 2) >= tokenAmount / 2 - 10 ** 10); - } - function test_getToken0FromLiquidity_lp_amount_larger_than_supply() public { - uint256 tokenAmount = (isSystemCoinToken0) ? initRAIETHPairLiquidity : initETHRAIPairLiquidity; - assertEq(liquidityManager.getToken0FromLiquidity(initialLPTokens * 2), 0); - } - - function test_getToken1FromLiquidity_zero() public { - assertEq(liquidityManager.getToken1FromLiquidity(0), 0); - } - function test_getToken1FromLiquidity() public { - uint256 tokenAmount = (isSystemCoinToken0) ? initETHRAIPairLiquidity : initRAIETHPairLiquidity; - assertTrue(liquidityManager.getToken1FromLiquidity(initialLPTokens) >= tokenAmount - 10 ** 10); - assertTrue(liquidityManager.getToken1FromLiquidity(initialLPTokens / 2) >= tokenAmount / 2 - 10 ** 10); - } - function test_getToken1FromLiquidity_lp_amount_larger_than_supply() public { - uint256 tokenAmount = (isSystemCoinToken0) ? initETHRAIPairLiquidity : initRAIETHPairLiquidity; - assertEq(liquidityManager.getToken1FromLiquidity(initialLPTokens * 2), 0); - } - - function test_getLiquidityFromToken0_zero() public { - assertEq(liquidityManager.getLiquidityFromToken0(0), 0); - } - function test_getLiquidityFromToken0() public { - uint256 tokenAmount = (isSystemCoinToken0) ? initRAIETHPairLiquidity : initETHRAIPairLiquidity; - DSToken token0 = DSToken((isSystemCoinToken0) ? address(systemCoin) : address(weth)); - uint256 currentTokenSupply = token0.balanceOf(address(this)); - uint256 lpTokens = liquidityManager.getLiquidityFromToken0(tokenAmount / 2); - - raiWETHPair.approve(address(liquidityManager), uint(-1)); - liquidityManager.removeLiquidity(lpTokens, 1, 1, address(this)); - - assertEq(token0.balanceOf(address(this)) - currentTokenSupply, tokenAmount / 2); - } - function test_getLiquidityFromToken0_token_amount_larger_than_pool_supply() public { - assertEq(liquidityManager.getLiquidityFromToken0(uint(-1)), 0); - } - - function test_getLiquidityFromToken1_zero() public { - assertEq(liquidityManager.getLiquidityFromToken1(0), 0); - } - function test_getLiquidityFromToken1() public { - uint256 tokenAmount = (isSystemCoinToken0) ? initETHRAIPairLiquidity : initRAIETHPairLiquidity; - DSToken token1 = DSToken((isSystemCoinToken0) ? address(weth) : address(systemCoin)); - uint256 currentTokenSupply = token1.balanceOf(address(this)); - uint256 lpTokens = liquidityManager.getLiquidityFromToken1(tokenAmount / 2); - - raiWETHPair.approve(address(liquidityManager), uint(-1)); - liquidityManager.removeLiquidity(lpTokens, 1, 1, address(this)); - - assertEq(token1.balanceOf(address(this)) - currentTokenSupply, tokenAmount / 2); - } - function test_getLiquidityFromToken1_token_amount_larger_than_pool_supply() public { - assertEq(liquidityManager.getLiquidityFromToken1(uint(-1)), 0); - } - - function test_remove_some_liquidity() public { - uint256 currentWETHSupply = weth.balanceOf(address(this)); - uint256 currentSysCoinSupply = systemCoin.balanceOf(address(this)); - - raiWETHPair.approve(address(liquidityManager), uint(-1)); - liquidityManager.removeLiquidity(initialLPTokens / 2, 1, 1, address(this)); - - assertEq(systemCoin.balanceOf(address(liquidityManager)), 0); - assertEq(weth.balanceOf(address(liquidityManager)), 0); - assertEq(raiWETHPair.balanceOf(address(liquidityManager)), 0); - - uint256 wethWithdrawn = weth.balanceOf(address(this)) - currentWETHSupply; - uint256 sysCoinWithdrawn = systemCoin.balanceOf(address(this)) - currentSysCoinSupply; - - assertTrue(wethWithdrawn > 0 && sysCoinWithdrawn > 0); - } - function test_remove_all_liquidity() public { - uint256 currentWETHSupply = weth.balanceOf(address(this)); - uint256 currentSysCoinSupply = systemCoin.balanceOf(address(this)); - - raiWETHPair.approve(address(liquidityManager), uint(-1)); - liquidityManager.removeLiquidity(initialLPTokens, 1, 1, address(this)); - - assertEq(systemCoin.balanceOf(address(liquidityManager)), 0); - assertEq(weth.balanceOf(address(liquidityManager)), 0); - assertEq(raiWETHPair.balanceOf(address(liquidityManager)), 0); - - uint256 wethWithdrawn = weth.balanceOf(address(this)) - currentWETHSupply; - uint256 sysCoinWithdrawn = systemCoin.balanceOf(address(this)) - currentSysCoinSupply; - - assertTrue(wethWithdrawn > 0 && sysCoinWithdrawn > 0); - assertEq(raiWETHPair.totalSupply(), 1000); - } - function testFail_remove_liquidity_token0_min_too_big() public { - raiWETHPair.approve(address(liquidityManager), uint(-1)); - liquidityManager.removeLiquidity(initialLPTokens, uint128(-1), 1, address(this)); - } - function testFail_remove_liquidity_token1_min_too_big() public { - raiWETHPair.approve(address(liquidityManager), uint(-1)); - liquidityManager.removeLiquidity(initialLPTokens, 1, uint128(-1), address(this)); - } -} +pragma solidity 0.6.7; + +import "ds-test/test.sol"; +import "ds-weth/weth9.sol"; +import "ds-token/token.sol"; + +import "../integrations/uniswap/uni-v2/UniswapV2Factory.sol"; +import "../integrations/uniswap/uni-v2/UniswapV2Pair.sol"; +import "../integrations/uniswap/uni-v2/UniswapV2Router02.sol"; + +import "../integrations/uniswap/liquidity-managers/UniswapV2LiquidityManager.sol"; + +abstract contract Hevm { + function warp(uint256) virtual public; +} + +contract UniswapV2LiquidityManagerTest is DSTest { + Hevm hevm; + + UniswapV2Factory uniswapFactory; + UniswapV2Router02 uniswapRouter; + UniswapV2LiquidityManager liquidityManager; + + UniswapV2Pair raiWETHPair; + + DSToken systemCoin; + WETH9_ weth; + + // Params + bool isSystemCoinToken0; + uint256 initTokenAmount = 100000 ether; + uint256 initETHUSDPrice = 250 * 10 ** 18; + uint256 initRAIUSDPrice = 4.242 * 10 ** 18; + + uint256 initETHRAIPairLiquidity = 5 ether; // 1250 USD + uint256 initRAIETHPairLiquidity = 294.672324375E18; // 1 RAI = 4.242 USD + + uint256 ethRAISimulationExtraRAI = 100 ether; + uint256 ethRAISimulationExtraETH = 0.5 ether; + + uint256 initialLPTokens; + + function setUp() public { + hevm = Hevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + hevm.warp(604411200); + + // Tokens + systemCoin = new DSToken("RAI", "RAI"); + systemCoin.mint(address(this), initTokenAmount); + + weth = new WETH9_(); + weth.deposit{value: initTokenAmount}(); + + // Uniswap setup + uniswapFactory = new UniswapV2Factory(address(this)); + createUniswapPair(); + uniswapRouter = new UniswapV2Router02(address(uniswapFactory), address(weth)); + addPairLiquidityRouter(address(systemCoin), address(weth), initRAIETHPairLiquidity, initETHRAIPairLiquidity); + initialLPTokens = raiWETHPair.balanceOf(address(this)); + + // Liquidity manager + liquidityManager = new UniswapV2LiquidityManager(address(raiWETHPair), address(uniswapRouter)); + } + + // --- Uniswap utils --- + function createUniswapPair() internal { + // Setup WETH/RAI pair + uniswapFactory.createPair(address(weth), address(systemCoin)); + raiWETHPair = UniswapV2Pair(uniswapFactory.getPair(address(weth), address(systemCoin))); + + if (address(raiWETHPair.token0()) == address(systemCoin)) isSystemCoinToken0 = true; + } + function addPairLiquidityRouter(address token1, address token2, uint256 amount1, uint256 amount2) internal { + DSToken(token1).approve(address(uniswapRouter), uint(-1)); + DSToken(token2).approve(address(uniswapRouter), uint(-1)); + uniswapRouter.addLiquidity(token1, token2, amount1, amount2, amount1, amount2, address(this), now); + UniswapV2Pair updatedPair = UniswapV2Pair(uniswapFactory.getPair(token1, token2)); + updatedPair.sync(); + } + function addPairLiquidityRouterNoSync(address token1, address token2, uint256 amount1, uint256 amount2) internal { + DSToken(token1).approve(address(uniswapRouter), uint(-1)); + DSToken(token2).approve(address(uniswapRouter), uint(-1)); + uniswapRouter.addLiquidity(token1, token2, amount1, amount2, amount1, amount2, address(this), now); + UniswapV2Pair updatedPair = UniswapV2Pair(uniswapFactory.getPair(token1, token2)); + } + function addPairLiquidityTransfer(UniswapV2Pair pair, address token1, address token2, uint256 amount1, uint256 amount2) internal { + DSToken(token1).transfer(address(pair), amount1); + DSToken(token2).transfer(address(pair), amount2); + pair.sync(); + } + function addPairLiquidityTransferNoSync(UniswapV2Pair pair, address token1, address token2, uint256 amount1, uint256 amount2) internal { + DSToken(token1).transfer(address(pair), amount1); + DSToken(token2).transfer(address(pair), amount2); + } + + // --- Tests --- + function test_getToken0FromLiquidity_zero() public { + assertEq(liquidityManager.getToken0FromLiquidity(0), 0); + } + function test_getToken0FromLiquidity() public { + uint256 tokenAmount = (isSystemCoinToken0) ? initRAIETHPairLiquidity : initETHRAIPairLiquidity; + assertTrue(liquidityManager.getToken0FromLiquidity(initialLPTokens) >= tokenAmount - 10 ** 10); + assertTrue(liquidityManager.getToken0FromLiquidity(initialLPTokens / 2) >= tokenAmount / 2 - 10 ** 10); + } + function test_getToken0FromLiquidity_lp_amount_larger_than_supply() public { + uint256 tokenAmount = (isSystemCoinToken0) ? initRAIETHPairLiquidity : initETHRAIPairLiquidity; + assertEq(liquidityManager.getToken0FromLiquidity(initialLPTokens * 2), 0); + } + + function test_getToken1FromLiquidity_zero() public { + assertEq(liquidityManager.getToken1FromLiquidity(0), 0); + } + function test_getToken1FromLiquidity() public { + uint256 tokenAmount = (isSystemCoinToken0) ? initETHRAIPairLiquidity : initRAIETHPairLiquidity; + assertTrue(liquidityManager.getToken1FromLiquidity(initialLPTokens) >= tokenAmount - 10 ** 10); + assertTrue(liquidityManager.getToken1FromLiquidity(initialLPTokens / 2) >= tokenAmount / 2 - 10 ** 10); + } + function test_getToken1FromLiquidity_lp_amount_larger_than_supply() public { + uint256 tokenAmount = (isSystemCoinToken0) ? initETHRAIPairLiquidity : initRAIETHPairLiquidity; + assertEq(liquidityManager.getToken1FromLiquidity(initialLPTokens * 2), 0); + } + + function test_getLiquidityFromToken0_zero() public { + assertEq(liquidityManager.getLiquidityFromToken0(0), 0); + } + function test_getLiquidityFromToken0() public { + uint256 tokenAmount = (isSystemCoinToken0) ? initRAIETHPairLiquidity : initETHRAIPairLiquidity; + DSToken token0 = DSToken((isSystemCoinToken0) ? address(systemCoin) : address(weth)); + uint256 currentTokenSupply = token0.balanceOf(address(this)); + uint256 lpTokens = liquidityManager.getLiquidityFromToken0(tokenAmount / 2); + + raiWETHPair.approve(address(liquidityManager), uint(-1)); + liquidityManager.removeLiquidity(lpTokens, 1, 1, address(this)); + + assertEq(token0.balanceOf(address(this)) - currentTokenSupply, tokenAmount / 2); + } + function test_getLiquidityFromToken0_token_amount_larger_than_pool_supply() public { + assertEq(liquidityManager.getLiquidityFromToken0(uint(-1)), 0); + } + + function test_getLiquidityFromToken1_zero() public { + assertEq(liquidityManager.getLiquidityFromToken1(0), 0); + } + function test_getLiquidityFromToken1() public { + uint256 tokenAmount = (isSystemCoinToken0) ? initETHRAIPairLiquidity : initRAIETHPairLiquidity; + DSToken token1 = DSToken((isSystemCoinToken0) ? address(weth) : address(systemCoin)); + uint256 currentTokenSupply = token1.balanceOf(address(this)); + uint256 lpTokens = liquidityManager.getLiquidityFromToken1(tokenAmount / 2); + + raiWETHPair.approve(address(liquidityManager), uint(-1)); + liquidityManager.removeLiquidity(lpTokens, 1, 1, address(this)); + + assertEq(token1.balanceOf(address(this)) - currentTokenSupply, tokenAmount / 2); + } + function test_getLiquidityFromToken1_token_amount_larger_than_pool_supply() public { + assertEq(liquidityManager.getLiquidityFromToken1(uint(-1)), 0); + } + + function test_remove_some_liquidity() public { + uint256 currentWETHSupply = weth.balanceOf(address(this)); + uint256 currentSysCoinSupply = systemCoin.balanceOf(address(this)); + + raiWETHPair.approve(address(liquidityManager), uint(-1)); + liquidityManager.removeLiquidity(initialLPTokens / 2, 1, 1, address(this)); + + assertEq(systemCoin.balanceOf(address(liquidityManager)), 0); + assertEq(weth.balanceOf(address(liquidityManager)), 0); + assertEq(raiWETHPair.balanceOf(address(liquidityManager)), 0); + + uint256 wethWithdrawn = weth.balanceOf(address(this)) - currentWETHSupply; + uint256 sysCoinWithdrawn = systemCoin.balanceOf(address(this)) - currentSysCoinSupply; + + assertTrue(wethWithdrawn > 0 && sysCoinWithdrawn > 0); + } + function test_remove_all_liquidity() public { + uint256 currentWETHSupply = weth.balanceOf(address(this)); + uint256 currentSysCoinSupply = systemCoin.balanceOf(address(this)); + + raiWETHPair.approve(address(liquidityManager), uint(-1)); + liquidityManager.removeLiquidity(initialLPTokens, 1, 1, address(this)); + + assertEq(systemCoin.balanceOf(address(liquidityManager)), 0); + assertEq(weth.balanceOf(address(liquidityManager)), 0); + assertEq(raiWETHPair.balanceOf(address(liquidityManager)), 0); + + uint256 wethWithdrawn = weth.balanceOf(address(this)) - currentWETHSupply; + uint256 sysCoinWithdrawn = systemCoin.balanceOf(address(this)) - currentSysCoinSupply; + + assertTrue(wethWithdrawn > 0 && sysCoinWithdrawn > 0); + assertEq(raiWETHPair.totalSupply(), 1000); + } + function testFail_remove_liquidity_token0_min_too_big() public { + raiWETHPair.approve(address(liquidityManager), uint(-1)); + liquidityManager.removeLiquidity(initialLPTokens, uint128(-1), 1, address(this)); + } + function testFail_remove_liquidity_token1_min_too_big() public { + raiWETHPair.approve(address(liquidityManager), uint(-1)); + liquidityManager.removeLiquidity(initialLPTokens, 1, uint128(-1), address(this)); + } +} diff --git a/src/test/UniswapV2Swapper.t.sol b/src/test/UniswapV2Swapper.t.sol index f92fecc..98b0efc 100644 --- a/src/test/UniswapV2Swapper.t.sol +++ b/src/test/UniswapV2Swapper.t.sol @@ -1,198 +1,198 @@ -pragma solidity 0.6.7; - -import "ds-test/test.sol"; -import "ds-weth/weth9.sol"; -import "ds-token/token.sol"; - -import "../integrations/uniswap/uni-v2/UniswapV2Factory.sol"; -import "../integrations/uniswap/uni-v2/UniswapV2Pair.sol"; -import "../integrations/uniswap/uni-v2/UniswapV2Router02.sol"; - -import "../integrations/uniswap/swappers/UniswapV2Swapper.sol"; - -abstract contract Hevm { - function warp(uint256) virtual public; -} - -contract UniswapV2SwapManagerTest is DSTest { - Hevm hevm; - - UniswapV2Factory uniswapFactory; - UniswapV2Router02 uniswapRouter; - UniswapV2Swapper swapManager; - - UniswapV2Pair raiWETHPair; - - DSToken systemCoin; - WETH9_ weth; - - // Params - bool isSystemCoinToken0; - uint256 initTokenAmount = 100000 ether; - uint256 initETHUSDPrice = 250 * 10 ** 18; - uint256 initRAIUSDPrice = 4.242 * 10 ** 18; - - uint256 initETHRAIPairLiquidity = 5 ether; // 1250 USD - uint256 initRAIETHPairLiquidity = 294.672324375E18; // 1 RAI = 4.242 USD - - uint256 ethRAISimulationExtraRAI = 100 ether; - uint256 ethRAISimulationExtraETH = 0.5 ether; - - uint256 initialLPTokens; - - function setUp() public { - hevm = Hevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); - hevm.warp(604411200); - - // Tokens - systemCoin = new DSToken("RAI", "RAI"); - systemCoin.mint(address(this), initTokenAmount); - - weth = new WETH9_(); - weth.deposit{value: initTokenAmount}(); - - // Uniswap setup - uniswapFactory = new UniswapV2Factory(address(this)); - createUniswapPair(); - uniswapRouter = new UniswapV2Router02(address(uniswapFactory), address(weth)); - addPairLiquidityRouter(address(systemCoin), address(weth), initRAIETHPairLiquidity, initETHRAIPairLiquidity); - initialLPTokens = raiWETHPair.balanceOf(address(this)); - - // Swap manager - swapManager = new UniswapV2Swapper(address(uniswapFactory), address(uniswapRouter)); - } - - // --- Uniswap utils --- - function createUniswapPair() internal { - // Setup WETH/RAI pair - uniswapFactory.createPair(address(weth), address(systemCoin)); - raiWETHPair = UniswapV2Pair(uniswapFactory.getPair(address(weth), address(systemCoin))); - - if (address(raiWETHPair.token0()) == address(systemCoin)) isSystemCoinToken0 = true; - } - function addPairLiquidityRouter(address token1, address token2, uint256 amount1, uint256 amount2) internal { - DSToken(token1).approve(address(uniswapRouter), uint(-1)); - DSToken(token2).approve(address(uniswapRouter), uint(-1)); - uniswapRouter.addLiquidity(token1, token2, amount1, amount2, amount1, amount2, address(this), now); - UniswapV2Pair updatedPair = UniswapV2Pair(uniswapFactory.getPair(token1, token2)); - updatedPair.sync(); - } - function addPairLiquidityRouterNoSync(address token1, address token2, uint256 amount1, uint256 amount2) internal { - DSToken(token1).approve(address(uniswapRouter), uint(-1)); - DSToken(token2).approve(address(uniswapRouter), uint(-1)); - uniswapRouter.addLiquidity(token1, token2, amount1, amount2, amount1, amount2, address(this), now); - UniswapV2Pair updatedPair = UniswapV2Pair(uniswapFactory.getPair(token1, token2)); - } - function addPairLiquidityTransfer(UniswapV2Pair pair, address token1, address token2, uint256 amount1, uint256 amount2) internal { - DSToken(token1).transfer(address(pair), amount1); - DSToken(token2).transfer(address(pair), amount2); - pair.sync(); - } - function addPairLiquidityTransferNoSync(UniswapV2Pair pair, address token1, address token2, uint256 amount1, uint256 amount2) internal { - DSToken(token1).transfer(address(pair), amount1); - DSToken(token2).transfer(address(pair), amount2); - } - - // --- Tests --- - function test_setup() public { - assertEq(address(swapManager.router()), address(uniswapRouter)); - assertEq(address(swapManager.factory()), address(uniswapFactory)); - } - function testFail_swap_zero_amount() public { - systemCoin.mint(address(this), 1 ether); - systemCoin.approve(address(swapManager), uint(-1)); - - swapManager.swap(address(systemCoin), address(weth), 0, 1, address(this)); - } - function testFail_swap_inexistent_pair() public { - systemCoin.mint(address(this), 1 ether); - systemCoin.approve(address(swapManager), uint(-1)); - - swapManager.swap(address(systemCoin), address(0x123), 1 ether, 1, address(this)); - } - function testFail_target_destination_null() public { - systemCoin.mint(address(this), 1 ether); - systemCoin.approve(address(swapManager), uint(-1)); - - swapManager.swap(address(systemCoin), address(weth), 1 ether, 1, address(0)); - } - function testFail_swapper_cannot_pull_tokens() public { - systemCoin.mint(address(this), 1 ether); - - swapManager.swap(address(systemCoin), address(weth), 1 ether, 1, address(this)); - } - function test_swap_when_swapper_contains_tokens() public { - weth.deposit{value: 1 ether}(); - weth.transfer(address(swapManager), 1 ether); - - systemCoin.mint(address(this), 2 ether); - systemCoin.transfer(address(swapManager), 1 ether); - systemCoin.approve(address(swapManager), uint(-1)); - - uint256 currentWethBalance = weth.balanceOf(address(this)); - swapManager.swap(address(systemCoin), address(weth), 1 ether, 1, address(this)); - assertTrue(weth.balanceOf(address(this)) > currentWethBalance); - - assertEq(swapManager.getTokenPathLength(), 0); - } - function test_swap_rai_weth() public { - systemCoin.mint(address(this), 1 ether); - systemCoin.approve(address(swapManager), uint(-1)); - - uint256 currentWethBalance = weth.balanceOf(address(this)); - swapManager.swap(address(systemCoin), address(weth), 1 ether, 1, address(this)); - assertTrue(weth.balanceOf(address(this)) > currentWethBalance); - - assertEq(swapManager.getTokenPathLength(), 0); - } - function test_swap_weth_rai() public { - weth.deposit{value: 1 ether}(); - weth.approve(address(swapManager), uint(-1)); - - uint256 currentSysCoinBalance = systemCoin.balanceOf(address(this)); - swapManager.swap(address(weth), address(systemCoin), 1 ether, 1, address(this)); - assertTrue(systemCoin.balanceOf(address(this)) > currentSysCoinBalance); - - assertEq(swapManager.getTokenPathLength(), 0); - } - function testFail_getAmountOut_null_amount_in() public { - swapManager.getAmountOut(address(systemCoin), address(weth), 0); - } - function testFail_getAmountOut_inexistent_pair() public { - swapManager.getAmountOut(address(systemCoin), address(0x123), 1 ether); - } - function test_getAmountOut_rai_weth() public { - uint256 amountOut = swapManager.getAmountOut(address(systemCoin), address(weth), 1 ether); - - systemCoin.mint(address(this), 1 ether); - systemCoin.approve(address(swapManager), uint(-1)); - - uint256 currentWethBalance = weth.balanceOf(address(this)); - swapManager.swap(address(systemCoin), address(weth), 1 ether, 1, address(this)); - assertEq(weth.balanceOf(address(this)) - currentWethBalance, amountOut); - - assertEq(swapManager.getTokenPathLength(), 0); - } - function test_getAmountOut_weth_rai() public { - uint256 amountOut = swapManager.getAmountOut(address(weth), address(systemCoin), 1 ether); - - weth.deposit{value: 1 ether}(); - weth.approve(address(swapManager), uint(-1)); - - uint256 currentSysCoinBalance = systemCoin.balanceOf(address(this)); - swapManager.swap(address(weth), address(systemCoin), 1 ether, 1, address(this)); - assertEq(systemCoin.balanceOf(address(this)) - currentSysCoinBalance, amountOut); - - assertEq(swapManager.getTokenPathLength(), 0); - } - function testFail_getAmountOut_amount_in_higher_than_pool_balance() public { - uint256 amountOut = swapManager.getAmountOut(address(weth), address(systemCoin), 1E45); - - weth.deposit{value: 1E45}(); - weth.approve(address(swapManager), uint(-1)); - - uint256 currentSysCoinBalance = systemCoin.balanceOf(address(this)); - swapManager.swap(address(weth), address(systemCoin), 1E45, 1, address(this)); - assertEq(systemCoin.balanceOf(address(this)) - currentSysCoinBalance, amountOut); - } -} +pragma solidity 0.6.7; + +import "ds-test/test.sol"; +import "ds-weth/weth9.sol"; +import "ds-token/token.sol"; + +import "../integrations/uniswap/uni-v2/UniswapV2Factory.sol"; +import "../integrations/uniswap/uni-v2/UniswapV2Pair.sol"; +import "../integrations/uniswap/uni-v2/UniswapV2Router02.sol"; + +import "../integrations/uniswap/swappers/UniswapV2Swapper.sol"; + +abstract contract Hevm { + function warp(uint256) virtual public; +} + +contract UniswapV2SwapManagerTest is DSTest { + Hevm hevm; + + UniswapV2Factory uniswapFactory; + UniswapV2Router02 uniswapRouter; + UniswapV2Swapper swapManager; + + UniswapV2Pair raiWETHPair; + + DSToken systemCoin; + WETH9_ weth; + + // Params + bool isSystemCoinToken0; + uint256 initTokenAmount = 100000 ether; + uint256 initETHUSDPrice = 250 * 10 ** 18; + uint256 initRAIUSDPrice = 4.242 * 10 ** 18; + + uint256 initETHRAIPairLiquidity = 5 ether; // 1250 USD + uint256 initRAIETHPairLiquidity = 294.672324375E18; // 1 RAI = 4.242 USD + + uint256 ethRAISimulationExtraRAI = 100 ether; + uint256 ethRAISimulationExtraETH = 0.5 ether; + + uint256 initialLPTokens; + + function setUp() public { + hevm = Hevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + hevm.warp(604411200); + + // Tokens + systemCoin = new DSToken("RAI", "RAI"); + systemCoin.mint(address(this), initTokenAmount); + + weth = new WETH9_(); + weth.deposit{value: initTokenAmount}(); + + // Uniswap setup + uniswapFactory = new UniswapV2Factory(address(this)); + createUniswapPair(); + uniswapRouter = new UniswapV2Router02(address(uniswapFactory), address(weth)); + addPairLiquidityRouter(address(systemCoin), address(weth), initRAIETHPairLiquidity, initETHRAIPairLiquidity); + initialLPTokens = raiWETHPair.balanceOf(address(this)); + + // Swap manager + swapManager = new UniswapV2Swapper(address(uniswapFactory), address(uniswapRouter)); + } + + // --- Uniswap utils --- + function createUniswapPair() internal { + // Setup WETH/RAI pair + uniswapFactory.createPair(address(weth), address(systemCoin)); + raiWETHPair = UniswapV2Pair(uniswapFactory.getPair(address(weth), address(systemCoin))); + + if (address(raiWETHPair.token0()) == address(systemCoin)) isSystemCoinToken0 = true; + } + function addPairLiquidityRouter(address token1, address token2, uint256 amount1, uint256 amount2) internal { + DSToken(token1).approve(address(uniswapRouter), uint(-1)); + DSToken(token2).approve(address(uniswapRouter), uint(-1)); + uniswapRouter.addLiquidity(token1, token2, amount1, amount2, amount1, amount2, address(this), now); + UniswapV2Pair updatedPair = UniswapV2Pair(uniswapFactory.getPair(token1, token2)); + updatedPair.sync(); + } + function addPairLiquidityRouterNoSync(address token1, address token2, uint256 amount1, uint256 amount2) internal { + DSToken(token1).approve(address(uniswapRouter), uint(-1)); + DSToken(token2).approve(address(uniswapRouter), uint(-1)); + uniswapRouter.addLiquidity(token1, token2, amount1, amount2, amount1, amount2, address(this), now); + UniswapV2Pair updatedPair = UniswapV2Pair(uniswapFactory.getPair(token1, token2)); + } + function addPairLiquidityTransfer(UniswapV2Pair pair, address token1, address token2, uint256 amount1, uint256 amount2) internal { + DSToken(token1).transfer(address(pair), amount1); + DSToken(token2).transfer(address(pair), amount2); + pair.sync(); + } + function addPairLiquidityTransferNoSync(UniswapV2Pair pair, address token1, address token2, uint256 amount1, uint256 amount2) internal { + DSToken(token1).transfer(address(pair), amount1); + DSToken(token2).transfer(address(pair), amount2); + } + + // --- Tests --- + function test_setup() public { + assertEq(address(swapManager.router()), address(uniswapRouter)); + assertEq(address(swapManager.factory()), address(uniswapFactory)); + } + function testFail_swap_zero_amount() public { + systemCoin.mint(address(this), 1 ether); + systemCoin.approve(address(swapManager), uint(-1)); + + swapManager.swap(address(systemCoin), address(weth), 0, 1, address(this)); + } + function testFail_swap_inexistent_pair() public { + systemCoin.mint(address(this), 1 ether); + systemCoin.approve(address(swapManager), uint(-1)); + + swapManager.swap(address(systemCoin), address(0x123), 1 ether, 1, address(this)); + } + function testFail_target_destination_null() public { + systemCoin.mint(address(this), 1 ether); + systemCoin.approve(address(swapManager), uint(-1)); + + swapManager.swap(address(systemCoin), address(weth), 1 ether, 1, address(0)); + } + function testFail_swapper_cannot_pull_tokens() public { + systemCoin.mint(address(this), 1 ether); + + swapManager.swap(address(systemCoin), address(weth), 1 ether, 1, address(this)); + } + function test_swap_when_swapper_contains_tokens() public { + weth.deposit{value: 1 ether}(); + weth.transfer(address(swapManager), 1 ether); + + systemCoin.mint(address(this), 2 ether); + systemCoin.transfer(address(swapManager), 1 ether); + systemCoin.approve(address(swapManager), uint(-1)); + + uint256 currentWethBalance = weth.balanceOf(address(this)); + swapManager.swap(address(systemCoin), address(weth), 1 ether, 1, address(this)); + assertTrue(weth.balanceOf(address(this)) > currentWethBalance); + + assertEq(swapManager.getTokenPathLength(), 0); + } + function test_swap_rai_weth() public { + systemCoin.mint(address(this), 1 ether); + systemCoin.approve(address(swapManager), uint(-1)); + + uint256 currentWethBalance = weth.balanceOf(address(this)); + swapManager.swap(address(systemCoin), address(weth), 1 ether, 1, address(this)); + assertTrue(weth.balanceOf(address(this)) > currentWethBalance); + + assertEq(swapManager.getTokenPathLength(), 0); + } + function test_swap_weth_rai() public { + weth.deposit{value: 1 ether}(); + weth.approve(address(swapManager), uint(-1)); + + uint256 currentSysCoinBalance = systemCoin.balanceOf(address(this)); + swapManager.swap(address(weth), address(systemCoin), 1 ether, 1, address(this)); + assertTrue(systemCoin.balanceOf(address(this)) > currentSysCoinBalance); + + assertEq(swapManager.getTokenPathLength(), 0); + } + function testFail_getAmountOut_null_amount_in() public { + swapManager.getAmountOut(address(systemCoin), address(weth), 0); + } + function testFail_getAmountOut_inexistent_pair() public { + swapManager.getAmountOut(address(systemCoin), address(0x123), 1 ether); + } + function test_getAmountOut_rai_weth() public { + uint256 amountOut = swapManager.getAmountOut(address(systemCoin), address(weth), 1 ether); + + systemCoin.mint(address(this), 1 ether); + systemCoin.approve(address(swapManager), uint(-1)); + + uint256 currentWethBalance = weth.balanceOf(address(this)); + swapManager.swap(address(systemCoin), address(weth), 1 ether, 1, address(this)); + assertEq(weth.balanceOf(address(this)) - currentWethBalance, amountOut); + + assertEq(swapManager.getTokenPathLength(), 0); + } + function test_getAmountOut_weth_rai() public { + uint256 amountOut = swapManager.getAmountOut(address(weth), address(systemCoin), 1 ether); + + weth.deposit{value: 1 ether}(); + weth.approve(address(swapManager), uint(-1)); + + uint256 currentSysCoinBalance = systemCoin.balanceOf(address(this)); + swapManager.swap(address(weth), address(systemCoin), 1 ether, 1, address(this)); + assertEq(systemCoin.balanceOf(address(this)) - currentSysCoinBalance, amountOut); + + assertEq(swapManager.getTokenPathLength(), 0); + } + function testFail_getAmountOut_amount_in_higher_than_pool_balance() public { + uint256 amountOut = swapManager.getAmountOut(address(weth), address(systemCoin), 1E45); + + weth.deposit{value: 1E45}(); + weth.approve(address(swapManager), uint(-1)); + + uint256 currentSysCoinBalance = systemCoin.balanceOf(address(this)); + swapManager.swap(address(weth), address(systemCoin), 1E45, 1, address(this)); + assertEq(systemCoin.balanceOf(address(this)) - currentSysCoinBalance, amountOut); + } +} diff --git a/src/utils/ReentrancyGuard.sol b/src/utils/ReentrancyGuard.sol index 24c90c3..4a0d60a 100644 --- a/src/utils/ReentrancyGuard.sol +++ b/src/utils/ReentrancyGuard.sol @@ -1,62 +1,62 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.6.0 <0.8.0; - -/** - * @dev Contract module that helps prevent reentrant calls to a function. - * - * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier - * available, which can be applied to functions to make sure there are no nested - * (reentrant) calls to them. - * - * Note that because there is a single `nonReentrant` guard, functions marked as - * `nonReentrant` may not call one another. This can be worked around by making - * those functions `private`, and then adding `external` `nonReentrant` entry - * points to them. - * - * TIP: If you would like to learn more about reentrancy and alternative ways - * to protect against it, check out our blog post - * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. - */ -abstract contract ReentrancyGuard { - // Booleans are more expensive than uint256 or any type that takes up a full - // word because each write operation emits an extra SLOAD to first read the - // slot's contents, replace the bits taken up by the boolean, and then write - // back. This is the compiler's defense against contract upgrades and - // pointer aliasing, and it cannot be disabled. - - // The values being non-zero value makes deployment a bit more expensive, - // but in exchange the refund on every call to nonReentrant will be lower in - // amount. Since refunds are capped to a percentage of the total - // transaction's gas, it is best to keep them low in cases like this one, to - // increase the likelihood of the full refund coming into effect. - uint256 private constant _NOT_ENTERED = 1; - uint256 private constant _ENTERED = 2; - - uint256 private _status; - - constructor () internal { - _status = _NOT_ENTERED; - } - - /** - * @dev Prevents a contract from calling itself, directly or indirectly. - * Calling a `nonReentrant` function from another `nonReentrant` - * function is not supported. It is possible to prevent this from happening - * by making the `nonReentrant` function external, and make it call a - * `private` function that does the actual work. - */ - modifier nonReentrant() { - // On the first call to nonReentrant, _notEntered will be true - require(_status != _ENTERED, "ReentrancyGuard: reentrant call"); - - // Any calls to nonReentrant after this point will fail - _status = _ENTERED; - - _; - - // By storing the original value once again, a refund is triggered (see - // https://eips.ethereum.org/EIPS/eip-2200) - _status = _NOT_ENTERED; - } -} +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.0 <0.8.0; + +/** + * @dev Contract module that helps prevent reentrant calls to a function. + * + * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier + * available, which can be applied to functions to make sure there are no nested + * (reentrant) calls to them. + * + * Note that because there is a single `nonReentrant` guard, functions marked as + * `nonReentrant` may not call one another. This can be worked around by making + * those functions `private`, and then adding `external` `nonReentrant` entry + * points to them. + * + * TIP: If you would like to learn more about reentrancy and alternative ways + * to protect against it, check out our blog post + * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. + */ +abstract contract ReentrancyGuard { + // Booleans are more expensive than uint256 or any type that takes up a full + // word because each write operation emits an extra SLOAD to first read the + // slot's contents, replace the bits taken up by the boolean, and then write + // back. This is the compiler's defense against contract upgrades and + // pointer aliasing, and it cannot be disabled. + + // The values being non-zero value makes deployment a bit more expensive, + // but in exchange the refund on every call to nonReentrant will be lower in + // amount. Since refunds are capped to a percentage of the total + // transaction's gas, it is best to keep them low in cases like this one, to + // increase the likelihood of the full refund coming into effect. + uint256 private constant _NOT_ENTERED = 1; + uint256 private constant _ENTERED = 2; + + uint256 private _status; + + constructor () internal { + _status = _NOT_ENTERED; + } + + /** + * @dev Prevents a contract from calling itself, directly or indirectly. + * Calling a `nonReentrant` function from another `nonReentrant` + * function is not supported. It is possible to prevent this from happening + * by making the `nonReentrant` function external, and make it call a + * `private` function that does the actual work. + */ + modifier nonReentrant() { + // On the first call to nonReentrant, _notEntered will be true + require(_status != _ENTERED, "ReentrancyGuard: reentrant call"); + + // Any calls to nonReentrant after this point will fail + _status = _ENTERED; + + _; + + // By storing the original value once again, a refund is triggered (see + // https://eips.ethereum.org/EIPS/eip-2200) + _status = _NOT_ENTERED; + } +}