├── .env.example ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── LICENSE ├── README.md ├── abis ├── Currency.json ├── Feed.json ├── SimpleInterestRate.json └── Vault.json ├── deployConfigs ├── goerli.base.json ├── localnet.json └── sepolia.base.json ├── foundry.toml ├── release.md ├── remappings.txt ├── script ├── base.s.sol └── deploy.s.sol ├── shell └── prepare-artifacts.sh ├── slither.config.json ├── src ├── currency.sol ├── helpers │ └── pausable.sol ├── interfaces │ ├── ICurrency.sol │ ├── IFeed.sol │ ├── IOSM.sol │ ├── IRate.sol │ └── IVault.sol ├── mocks │ └── ERC20Token.sol ├── modules │ ├── feed.sol │ └── rate.sol └── vault.sol └── test ├── base.t.sol ├── fuzz ├── currency │ └── currency.t.sol └── vault │ ├── burnCurrency │ ├── burnCurrency.t.sol │ └── burnCurrency.tree │ ├── depositCollateral │ ├── depositCollateral.t.sol │ └── depositCollateral.tree │ ├── liquidate │ ├── liquidate.t.sol │ └── liquidate.tree │ ├── mintCurrency │ ├── mintCurrency.t.sol │ └── mintCurrency.tree │ ├── otherActions │ ├── otherActions.t.sol │ └── roleBasedActions.t.sol │ └── withdrawCollateral │ ├── withdrawCollateral.t.sol │ └── withdrawCollateral.tree ├── helpers └── ErrorsAndEvents.sol └── invariant ├── baseInvariant.t.sol ├── collateralInvariant.t.sol ├── globalInvariant.t.sol ├── handlers ├── erc20Handler.sol ├── feedHandler.sol ├── medianHandler.sol ├── osmHandler.sol └── vaultHandler.sol ├── helpers ├── timeManager.sol └── vaultGetters.sol └── userVaultInvariant.t.sol /.env.example: -------------------------------------------------------------------------------- 1 | MNEMONIC="" 2 | WALLET_INDEX="" 3 | BASE_GOERLI_RPC_URL="" 4 | BASE_SEPOLIA_RPC_URL="" -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Bug report about the Descent Protocol smart contracts 4 | 5 | --- 6 | 7 | ## Prerequisites 8 | 9 | - First, many thanks for taking part in the community and helping us improve. We appreciate that a lot. 10 | - Support questions are better asked in our Telegram channel: https://t.me/descentdao 11 | - Please ensure the issue isn't already reported. 12 | 13 | *Please delete the above section and the instructions in the sections below before submitting* 14 | 15 | ## Description 16 | 17 | Please describe considely the bug you have found, and what you expect instead. 18 | 19 | ## Environment 20 | 21 | - Compiler version: 22 | - Compiler options (if applicable, e.g. optimizer enabled): 23 | - Framework/IDE (e.g. Truffle or Remix): 24 | - EVM execution environment / backend / blockchain client: 25 | - Operating system: 26 | 27 | ## Steps to reproduce 28 | 29 | If applicable, please provide a *minimal* source code example to trigger the bug you have found. 30 | Provide as much information as necessary to reproduce the bug. 31 | 32 | ## Additional context 33 | 34 | Add any other context about the problem here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea or feature request for the Descent protocol smart contracts project 4 | 5 | --- 6 | 7 | ## Prerequisites 8 | 9 | - First, many thanks for taking part in the community and helping us improve. We appreciate that a lot. 10 | - Support questions are better asked in our Discord: https://t.me/descentdao 11 | - Please ensure the issue isn't already reported. 12 | 13 | *Please delete the above section and the instructions in the sections below before submitting* 14 | 15 | ## Context / issue 16 | 17 | In case your feature request related to a problem, please add clear and concise description of what the issue is. 18 | 19 | ## Proposed solution 20 | 21 | Please add a clear and concise description of what you want to happen. 22 | 23 | ## Alternatives 24 | 25 | Please add a clear and concise description of any alternative solutions or features you've considered. 26 | 27 | ## Additional context 28 | 29 | Add any other context or screenshots about the feature request here. -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | 3 | concurrency: 4 | cancel-in-progress: true 5 | group: ${{github.workflow}}-${{github.ref}} 6 | 7 | on: 8 | workflow_dispatch: 9 | pull_request: 10 | push: 11 | branches: 12 | - "main" 13 | 14 | jobs: 15 | lint: 16 | uses: "Descent-Collective/reusable-workflows/.github/workflows/forge-lint.yml@main" 17 | 18 | build: 19 | uses: "Descent-Collective/reusable-workflows/.github/workflows/forge-build.yml@main" 20 | 21 | test-fuzz: 22 | needs: ["lint", "build"] 23 | uses: "Descent-Collective/reusable-workflows/.github/workflows/forge-test.yml@main" 24 | with: 25 | foundry-fuzz-runs: 5000 26 | foundry-profile: "test-optimized" 27 | match-path: "test/fuzz/**/*.sol" 28 | name: "Fuzz tests" 29 | 30 | test-invariant: 31 | needs: ["lint", "build"] 32 | uses: "Descent-Collective/reusable-workflows/.github/workflows/forge-test.yml@main" 33 | with: 34 | foundry-fuzz-runs: 5000 35 | foundry-profile: "test-optimized" 36 | match-path: "test/invariant/**/*.sol" 37 | name: "Invariant tests" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | artifacts/ 5 | coverage/ 6 | node_modules/ 7 | coverage.json 8 | typechain-types/ 9 | # Ignores development broadcast logs 10 | !/broadcast 11 | /broadcast/*/ 12 | 13 | # Docs 14 | docs/ 15 | 16 | # Dotenv file 17 | .env 18 | .DS_Store 19 | .vscode -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/oracle-module"] 5 | path = lib/oracle-module 6 | url = https://github.com/Descent-Collective/oracle-module 7 | [submodule "lib/solady"] 8 | path = lib/solady 9 | url = https://github.com/Vectorized/solady 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | # Version 0.1.0 4 | 5 | ## Compiler settings 6 | 7 | Solidity compiler: [0.8.21] 8 | 9 | ### contracts 10 | - Vault Contract: `0xc93d667F5381CF2E41722829DF14E016bBb33A6A` 11 | - Currency Contract(xNGN): `0xED68D8380ED16ad69b861aDFae3Bf8fF75Acc25f` 12 | - Feed Contract `0xEA263AD21E04d695a750D8Dc04d2b952dF7405aa` 13 | 14 | ## Changes 15 | - Add tests for liquidation 16 | - Add burnCurrency tests 17 | - Add default permit2 support for currencies 18 | - Allow deny to vault 19 | 20 | # Version 0.1.1 21 | 22 | ## Compiler settings 23 | 24 | Solidity compiler: [0.8.21] 25 | 26 | ### contracts 27 | - Vault Contract: `0xCaC650a8F8E71BDE3d60f0B020A4AA3874974705` 28 | - Currency Contract(xNGN): `0xC8A88052006142d7ae0B56452e1f153BF480E341` 29 | - Feed Contract `0xEdC725Db7e54C3C85EB551E859b90489d076a9Ca` 30 | 31 | ## Changes 32 | - Replace use of health factor with collateral ratio 33 | - Fix wrong emitted event data and add tests for it 34 | - Add more natspec and use GPL-3.0 license 35 | 36 | # Version 0.1.2 37 | 38 | ## Compiler settings 39 | 40 | Solidity compiler: [0.8.21] 41 | 42 | ### contracts 43 | - Vault Contract: `0xE2386C5eF4deC9d5815C60168e36c7153ba00D0C` 44 | - Currency Contract(xNGN): `0xee2bDAE7896910c49BeA25106B9f8e9f4B671c82` 45 | - Feed Contract `0x970066EE55DF2134D1b52451afb49034AE5Fa29a` 46 | 47 | ## Changes 48 | - Fix wrong calculation of withdrawable collateral 49 | - Fix typos 50 | 51 | 52 | # Sepolia Version 0.2.0 53 | 54 | ## Compiler settings 55 | 56 | Solidity compiler: [0.8.21] 57 | 58 | ### contracts 59 | - Vault Contract: `0x18196CCaA8C2844c82B40a8bDCa27349C7466280` 60 | - Currency Contract(xNGN): `0x5d0583Ef20884C0b175046d515Ec227200C12C89` 61 | - Feed Contract `0x970066EE55DF2134D1b52451afb49034AE5Fa29a` 62 | - Rate Contract `0x774843f6Baa4AAE62F026a8aF3c1C6FF3e55Ca39` 63 | 64 | ## Changes 65 | - Use 18 decimlas for rate and liquidation threshold 66 | - Abstract the rate calculation to a different contract to make it modular 67 | - Add global debt ceiling and add check for global and collateral debt ceiling when minting, also update deploy script and tests 68 | - Enable users to be able to repay and withdraw during paused 69 | - Added invariant tests and fix noticed bugs 70 | - Added fuzzed unit test for currency contract 71 | - Integrate the OSM, Median and Feed into the invariant tests 72 | - Replace open zeppelin with solady. 73 | - Use rounding down for liquidation reward calculation 74 | - Added invariant tests and fix noticed bugs 75 | 76 | # Sepolia Version 0.1.1 77 | 78 | ## Compiler settings 79 | 80 | Solidity compiler: [0.8.21] 81 | 82 | ### contracts 83 | - Vault Contract: `0x3d35807343CbF4fDb16E42297F2214f62848D032` 84 | - Currency Contract(xNGN): `0xB8747e5cce01AA5a51021989BA11aE33097db485` 85 | - Feed Contract `0xFBD26B871D55ba56B7a780eF1fF243Db7A3E81f4` 86 | - Rate Contract `0x00A0BcB0e2099f4a0564c26e24eBfA866D3235D6` 87 | 88 | ## Changes 89 | - Fix rate config bug -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Protocol Core 2 | 3 | ### Deployment address 4 | 5 | #### Base Georli 6 | 7 | | Contract Name | Addresses | 8 | | ------------------------ | ------------------------------------------ | 9 | | Vault Contract | 0xE2386C5eF4deC9d5815C60168e36c7153ba00D0C | 10 | | Currency Contract (xNGN) | 0xee2bDAE7896910c49BeA25106B9f8e9f4B671c82 | 11 | | Feed Contract | 0x970066EE55DF2134D1b52451afb49034AE5Fa29a | 12 | 13 | #### Base Sepolia 14 | 15 | | Contract Name | Addresses | 16 | | ------------------------ | ------------------------------------------ | 17 | | Vault Contract | 0x3d35807343CbF4fDb16E42297F2214f62848D032 | 18 | | Currency Contract (xNGN) | 0xB8747e5cce01AA5a51021989BA11aE33097db485 | 19 | | Feed Contract | 0xFBD26B871D55ba56B7a780eF1fF243Db7A3E81f4 | 20 | | Rate Contract | 0x00A0BcB0e2099f4a0564c26e24eBfA866D3235D6 | 21 | 22 | To install libraries needed, run: 23 | 24 | ```zsh 25 | forge install 26 | ``` 27 | 28 | To run tests, run: 29 | 30 | ```zsh 31 | forge test -vvv --gas-report 32 | ``` 33 | 34 | To run slither, run: 35 | 36 | ```zsh 37 | slither . 38 | ``` 39 | 40 | To start a local node, run: 41 | 42 | ```zsh 43 | anvil 44 | ``` 45 | 46 | To run deploy the deploy script, (be sure to have the parameters in `./deployConfigs/*.json/` needed for your script populated and also have an anvil instance running), run: 47 | 48 | ```zsh 49 | forge script script/deploy.s.sol:DeployScript --fork-url http://localhost:8545 --broadcast 50 | ``` 51 | 52 | ## Deploy Config 53 | 54 | Meaning of parameters of the deploy configs 55 | 56 | - baseRate: The base rate of the protocol, should be the per second rate, e.g 1.5% would be `((uint256(1.5e18) / uint256(100)) / uint256(365 days)`, i.e `475646879`. 57 | - collaterals: collateral types 58 | - collateralAddress: contract address of the given collateral on the given chain. 59 | - collateralRate: The collateral rate of the given collateral on the given chain, calculated same as baseRate. 60 | - liquidationThreshold: liquidation threshold of the given collateral, denominated in wad, where `1e18 == 100%` and `0.5e18 == 50%`. 61 | - liquidationBonus: liquidation bonus of the given collateral, denominated same as liquidationThreshold. 62 | - debtCeiling: debt ceiling of the currency for the given collateral. 63 | - collateralFloorPerPosition: minimum amount of collateral allowed to borrow against. 64 | -------------------------------------------------------------------------------- /deployConfigs/goerli.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Base Goerli Testnet Deployment Config", 3 | "baseRate": "31709791983", 4 | "debtCeiling": "115792089237316195423570985008687907853269984665640564039457584007913129639935", 5 | "collaterals": { 6 | "USDC": { 7 | "collateralAddress": "0xF175520C52418dfE19C8098071a252da48Cd1C19", 8 | "collateralRate": "47564687975", 9 | "liquidationThreshold": "75000000000000000000", 10 | "liquidationBonus": "10000000000000000000", 11 | "debtCeiling": "115792089237316195423570985008687907853269984665640564039457584007913129639935", 12 | "collateralFloorPerPosition": "0", 13 | "price": "1100" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /deployConfigs/localnet.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Local chain deployment config", 3 | "baseRate": "31709791983", 4 | "debtCeiling": "115792089237316195423570985008687907853269984665640564039457584007913129639935", 5 | "collaterals": { 6 | "USDC": { 7 | "collateralAddress": "", 8 | "collateralRate": "47564687975", 9 | "liquidationThreshold": "75000000000000000000", 10 | "liquidationBonus": "10000000000000000000", 11 | "debtCeiling": "115792089237316195423570985008687907853269984665640564039457584007913129639935", 12 | "collateralFloorPerPosition": "0", 13 | "price": "1100" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /deployConfigs/sepolia.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Base Sepolia Testnet Deployment Config", 3 | "baseRate": "31709791983", 4 | "debtCeiling": "115792089237316195423570985008687907853269984665640564039457584007913129639935", 5 | "collaterals": { 6 | "USDC": { 7 | "collateralAddress": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", 8 | "collateralRate": "47564687975", 9 | "liquidationThreshold": "75000000000000000000", 10 | "liquidationBonus": "10000000000000000000", 11 | "debtCeiling": "115792089237316195423570985008687907853269984665640564039457584007913129639935", 12 | "collateralFloorPerPosition": "0", 13 | "price": "1100000000" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | solc = "0.8.21" 3 | src = "src" 4 | test = "test" 5 | out = "out" 6 | libs = ["lib"] 7 | fs_permissions = [{ access = "read", path = "./deployConfigs/localnet.json" }, { access = "read", path = "./deployConfigs/goerli.base.json" }, { access = "read", path = "./deployConfigs/sepolia.base.json" }] 8 | bytecode_hash = "none" 9 | cbor_metadata = false 10 | evm_version = "paris" 11 | optimizer = true 12 | optimizer_runs = 1_000_000 13 | 14 | 15 | [rpc_endpoints] 16 | base_georli = "${BASE_GOERLI_RPC_URL}" 17 | base_sepolia = "${BASE_SEPOLIA_RPC_URL}" 18 | 19 | 20 | [fuzz] 21 | runs = 10000 22 | max_test_rejects = 0 23 | seed = '0x3e8' 24 | dictionary_weight = 40 25 | include_storage = true 26 | include_push_bytes = true 27 | 28 | 29 | [invariant] 30 | runs = 100 31 | depth = 50 32 | fail_on_revert = true 33 | dictionary_weight = 80 34 | 35 | 36 | [fmt] 37 | number_underscore = "thousands" 38 | 39 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options 40 | -------------------------------------------------------------------------------- /release.md: -------------------------------------------------------------------------------- 1 | # Descent protocol Release guide 2 | 3 | ## Version X.Y.Z (Release Name) 4 | We follow the Semantic Versioning specification (SemVer) to convey meaning about the underlying code changes. SemVer consists of three numbers: MAJOR.MINOR.PATCH. 5 | - MAJOR version for incompatible changes, 6 | - MINOR version for backward-compatible new features, and 7 | - PATCH version for backward-compatible bug fixes. 8 | 9 | **Release Date:** [Release Date] 10 | 11 | **Release Highlights:** 12 | - [Brief summary of the major changes, enhancements, and additions in this release.] 13 | 14 | ## Table of Contents 15 | 16 | 1. [Introduction](#introduction) 17 | 2. [Release Notes](#release-notes) 18 | - [Feature Additions](#feature-additions) 19 | - [Enhancements](#enhancements) 20 | - [Bug Fixes](#bug-fixes) 21 | - [Deprecations](#deprecations) 22 | - [Other Changes](#other-changes) 23 | 3. [Installation and Upgrade](#installation-and-upgrade) 24 | 4. [Compatibility](#compatibility) 25 | 5. [Testing](#testing) 26 | 6. [Contributing](#contributing) 27 | 7. [Security](#security) 28 | 8. [Acknowledgments](#acknowledgments) 29 | 9. [License](#license) 30 | 31 | ## Introduction 32 | 33 | Provide a brief overview of the purpose of this release and any critical information that users or contributors should be aware of. 34 | 35 | ## Release Notes 36 | 37 | ### Feature Additions 38 | 39 | - [Description of new features added in this release.] 40 | - ... 41 | 42 | ### Enhancements 43 | 44 | - [Description of enhancements or improvements made in this release.] 45 | - ... 46 | 47 | ### Bug Fixes 48 | 49 | - [Description of bug fixes included in this release.] 50 | - ... 51 | 52 | ### Deprecations 53 | 54 | - [List any deprecated features or functions in this release, if applicable.] 55 | - ... 56 | 57 | ### Other Changes 58 | 59 | - [Any other notable changes or additions that don't fit into the above categories.] 60 | - ... 61 | 62 | ## Installation and Upgrade 63 | 64 | Provide instructions on how to install the latest release or upgrade from a previous version. Include any dependencies or system requirements. 65 | 66 | ## Testing 67 | 68 | Describe the testing process followed for this release, including unit tests, integration tests, and any other relevant testing procedures. 69 | 70 | ## Contributing 71 | 72 | Encourage contributions from the community and provide guidelines on how developers can contribute to the project. Include information on code style, pull request requirements, and the contribution process. 73 | 74 | ## Security 75 | 76 | If applicable, highlight any security-related changes or updates in this release. Encourage users to report security vulnerabilities responsibly. 77 | 78 | ## Acknowledgments 79 | 80 | Acknowledge and thank contributors, individuals, or organizations that played a significant role in the development of this release. 81 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | solady/=lib/solady/src/ 2 | forge-std/=lib/forge-std/src/ 3 | descent-collective/oracle-module/=lib/oracle-module/src/ -------------------------------------------------------------------------------- /script/base.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.21; 3 | 4 | import {Script, console2} from "forge-std/Script.sol"; 5 | import {stdJson} from "forge-std/StdJson.sol"; 6 | 7 | /// modified from sablier base test fil e 8 | abstract contract BaseScript is Script { 9 | /// @dev Included to enable compilation of the script without a $MNEMONIC environment variable. 10 | string internal constant TEST_MNEMONIC = "test test test test test test test test test test test junk"; 11 | 12 | /// @dev Needed for the deterministic deployments. 13 | bytes32 internal constant ZERO_SALT = bytes32(0); 14 | 15 | /// @dev The address of the transaction broadcaster. 16 | address internal broadcaster; 17 | 18 | /// @dev Used to derive the broadcaster's address if $ETH_FROM is not defined. 19 | string internal mnemonic; 20 | 21 | /// @dev Initializes the transaction broadcaster like this: 22 | /// 23 | /// - If $ETH_FROM is defined, use it. 24 | /// - Otherwise, derive the broadcaster address from $MNEMONIC. 25 | /// - If $MNEMONIC is not defined, default to a test mnemonic. 26 | /// 27 | /// The use case for $ETH_FROM is to specify the broadcaster key and its address via the command line. 28 | constructor() { 29 | address from = vm.envOr({name: "ETH_FROM", defaultValue: address(0)}); 30 | if (from != address(0)) { 31 | broadcaster = from; 32 | } else { 33 | mnemonic = vm.envOr({name: "MNEMONIC", defaultValue: TEST_MNEMONIC}); 34 | uint256 walletIndex = vm.envOr({name: "WALLET_INDEX", defaultValue: uint256(0)}); 35 | require(walletIndex <= type(uint32).max, "Invalid wallet index"); 36 | 37 | (broadcaster,) = deriveRememberKey({mnemonic: mnemonic, index: uint32(walletIndex)}); 38 | } 39 | 40 | if (block.chainid == 31_337) { 41 | currentChain = Chains.Localnet; 42 | } else if (block.chainid == 84_531) { 43 | currentChain = Chains.BaseGoerli; 44 | } else if (block.chainid == 84_532) { 45 | currentChain = Chains.BaseSepolia; 46 | } else { 47 | revert("Unsupported chain for deployment"); 48 | } 49 | } 50 | 51 | Chains currentChain; 52 | 53 | enum Chains { 54 | Localnet, 55 | BaseGoerli, 56 | BaseSepolia 57 | } 58 | 59 | modifier broadcast() { 60 | vm.startBroadcast(broadcaster); 61 | _; 62 | vm.stopBroadcast(); 63 | } 64 | 65 | function getDeployConfigJson() internal view returns (string memory json) { 66 | if (currentChain == Chains.BaseGoerli) { 67 | json = vm.readFile(string.concat(vm.projectRoot(), "/deployConfigs/goerli.base.json")); 68 | } else if (currentChain == Chains.BaseSepolia) { 69 | json = vm.readFile(string.concat(vm.projectRoot(), "/deployConfigs/sepolia.base.json")); 70 | } else { 71 | json = vm.readFile(string.concat(vm.projectRoot(), "/deployConfigs/localnet.json")); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /script/deploy.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.21; 3 | 4 | import {Vault} from "../src/vault.sol"; 5 | import {Currency} from "../src/currency.sol"; 6 | import {Feed} from "../src/modules/feed.sol"; 7 | 8 | import {BaseScript, stdJson, console2} from "./base.s.sol"; 9 | import {ERC20Token} from "../src/mocks/ERC20Token.sol"; 10 | import {SimpleInterestRate, IRate} from "../src/modules/rate.sol"; 11 | 12 | contract DeployScript is BaseScript { 13 | using stdJson for string; 14 | 15 | function run() external broadcast returns (Currency xNGN, Vault vault, Feed feed, IRate rate) { 16 | string memory deployConfigJson = getDeployConfigJson(); 17 | uint256 baseRate = deployConfigJson.readUint(".baseRate"); 18 | uint256 debtCeiling = deployConfigJson.readUint(".debtCeiling"); 19 | 20 | console2.log("\n Deploying xNGN contract"); 21 | xNGN = new Currency("xNGN", "xNGN"); 22 | console2.log("xNGN deployed successfully at address:", address(xNGN)); 23 | 24 | console2.log("\n Deploying vault contract"); 25 | vault = new Vault(xNGN, baseRate, debtCeiling); 26 | console2.log("Vault deployed successfully at address:", address(vault)); 27 | 28 | console2.log("\n Deploying feed contract"); 29 | feed = new Feed(vault); 30 | console2.log("Feed deployed successfully at address:", address(feed)); 31 | 32 | console2.log("\n Deploying rate contract"); 33 | rate = new SimpleInterestRate(); 34 | console2.log("Rate deployed successfully at address:", address(rate)); 35 | 36 | console2.log("\n Getting or deploying usdc contract"); 37 | ERC20Token usdc = getOrCreateUsdc(); 38 | console2.log("Usdc gotten or deployed successfully at address:", address(usdc)); 39 | 40 | console2.log("\n Creating collateral type"); 41 | uint256 _rate = deployConfigJson.readUint(".collaterals.USDC.collateralRate"); 42 | uint256 _liquidationThreshold = deployConfigJson.readUint(".collaterals.USDC.liquidationThreshold"); 43 | uint256 _liquidationBonus = deployConfigJson.readUint(".collaterals.USDC.liquidationBonus"); 44 | uint256 _debtCeiling = deployConfigJson.readUint(".collaterals.USDC.debtCeiling"); 45 | uint256 _collateralFloorPerPosition = deployConfigJson.readUint(".collaterals.USDC.collateralFloorPerPosition"); 46 | vault.createCollateralType({ 47 | _collateralToken: usdc, 48 | _rate: _rate, 49 | _liquidationThreshold: _liquidationThreshold, 50 | _liquidationBonus: _liquidationBonus, 51 | _debtCeiling: _debtCeiling, 52 | _collateralFloorPerPosition: _collateralFloorPerPosition 53 | }); 54 | console2.log("Collateral type created successfully with info:"); 55 | console2.log(" Rate:", _rate); 56 | console2.log(" Liquidation threshold:", _liquidationThreshold); 57 | console2.log(" Liquidation bonus:", _liquidationBonus); 58 | console2.log(" Debt ceiling:", _debtCeiling); 59 | console2.log(" Collateral floor per position:", _collateralFloorPerPosition); 60 | 61 | console2.log("\n Setting feed contract in vault"); 62 | vault.updateFeedModule(address(feed)); 63 | console2.log("Feed contract in vault set successfully"); 64 | 65 | console2.log("\n Setting rate contract in vault"); 66 | vault.updateRateModule(rate); 67 | console2.log("Rate contract in vault set successfully"); 68 | 69 | console2.log("\n Updating price of usdc from feed"); 70 | uint256 _price = deployConfigJson.readUint(".collaterals.USDC.price"); 71 | feed.mockUpdatePrice(usdc, _price); 72 | console2.log("Updating price of usdc from feed done successfully to:", _price); 73 | 74 | console2.log("\n Giving vault minter role for xNGN"); 75 | xNGN.setMinterRole(address(vault), true); 76 | console2.log("Vault given miinter role for xnGN successfully"); 77 | } 78 | 79 | function getOrCreateUsdc() private returns (ERC20Token usdc) { 80 | if (currentChain == Chains.Localnet) { 81 | usdc = new ERC20Token("Circle USD", "USDC", 6); 82 | } else { 83 | usdc = ERC20Token(getDeployConfigJson().readAddress(".collaterals.USDC.collateralAddress")); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /shell/prepare-artifacts.sh: -------------------------------------------------------------------------------- 1 | # Delete the current artifacts 2 | abis=./abis 3 | rm -rf $abis 4 | 5 | # Create the new artifacts directories 6 | mkdir $abis \ 7 | 8 | # Generate the artifacts with Forge 9 | FOUNDRY_PROFILE=optimized forge build 10 | 11 | # Copy the production abis 12 | cp out/vault.sol/Vault.json $abis 13 | cp out/currency.sol/Currency.json $abis 14 | cp out/feed.sol/Feed.json $abis 15 | cp out/rate.sol/SimpleInterestRate.json $abis -------------------------------------------------------------------------------- /slither.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "filter_paths": "(lib|test|script)", 3 | "solc_remaps": [ 4 | "@openzeppelin/contracts=lib/openzeppelin-contracts/contracts/", 5 | "forge-std/=lib/forge-std/src/" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/currency.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.21; 3 | 4 | // ========== External imports ========== 5 | import {ICurrency} from "./interfaces/ICurrency.sol"; 6 | import {Ownable} from "solady/auth/Ownable.sol"; 7 | import {ERC20Token} from "./mocks/ERC20Token.sol"; 8 | import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; 9 | import {ERC20} from "solady/tokens/ERC20.sol"; 10 | 11 | contract Currency is Ownable, ERC20, ICurrency { 12 | error NotMinter(); 13 | 14 | address public constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3; // permit2 contract 15 | string _name; 16 | string _symbol; 17 | 18 | bool public permit2Enabled; // if permit 2 is enabled by default or not 19 | mapping(address => bool) public minterRole; 20 | 21 | constructor(string memory name_, string memory symbol_) { 22 | _initializeOwner(msg.sender); 23 | _name = name_; 24 | _symbol = symbol_; 25 | 26 | permit2Enabled = true; 27 | } 28 | 29 | function decimals() public pure override returns (uint8) { 30 | return 18; 31 | } 32 | 33 | function name() public view override returns (string memory) { 34 | return _name; 35 | } 36 | 37 | function symbol() public view override returns (string memory) { 38 | return _symbol; 39 | } 40 | 41 | modifier onlyMinter() { 42 | if (!minterRole[msg.sender]) revert NotMinter(); 43 | _; 44 | } 45 | 46 | /** 47 | * @dev sets a minter role 48 | * @param account address for the minter role 49 | */ 50 | function setMinterRole(address account, bool isMinter) external onlyOwner { 51 | minterRole[account] = isMinter; 52 | } 53 | 54 | /** 55 | * @dev Mints a new token 56 | * @param account address to send the minted tokens to 57 | * @param amount amount of tokens to mint 58 | */ 59 | function mint(address account, uint256 amount) external onlyMinter returns (bool) { 60 | _mint(account, amount); 61 | return true; 62 | } 63 | 64 | /** 65 | * @dev Burns a token 66 | * @param account address to burn tokens from 67 | * @param amount amount of tokens to burn 68 | */ 69 | function burn(address account, uint256 amount) external returns (bool) { 70 | if (account != msg.sender) { 71 | _spendAllowance(account, msg.sender, amount); 72 | } 73 | _burn(account, amount); 74 | return true; 75 | } 76 | 77 | /** 78 | * @dev used to update if to default approve permit2 address for all addresses 79 | * @param enabled if the default approval should be done or not 80 | */ 81 | function updatePermit2Allowance(bool enabled) external onlyOwner { 82 | emit Permit2AllowanceUpdated(enabled); 83 | permit2Enabled = enabled; 84 | } 85 | 86 | /// @dev The permit2 contract has full approval by default. If the approval is revoked, it can still be manually approved. 87 | function allowance(address _owner, address spender) public view override returns (uint256) { 88 | if (spender == PERMIT2 && permit2Enabled) return type(uint256).max; 89 | return super.allowance(_owner, spender); 90 | } 91 | 92 | /** 93 | * @dev withdraw token. For cases where people mistakenly send other tokens to this address 94 | * @param token address of the token to withdraw 95 | * @param to account to withdraw tokens to 96 | */ 97 | function recoverToken(ERC20Token token, address to) public onlyOwner { 98 | if (address(token) != address(0)) { 99 | SafeTransferLib.safeTransfer(address(token), to, token.balanceOf(address(this))); 100 | } else { 101 | (bool success,) = payable(to).call{value: address(this).balance}(""); 102 | require(success, "withdraw failed"); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/helpers/pausable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.21; 3 | 4 | contract Pausable { 5 | error Paused(); 6 | error NotPaused(); 7 | 8 | uint256 internal constant FALSE = 1; 9 | uint256 internal constant TRUE = 2; 10 | 11 | uint256 public status; // Active status 12 | 13 | constructor() { 14 | status = TRUE; 15 | } 16 | 17 | modifier whenNotPaused() { 18 | if (status == FALSE) revert Paused(); 19 | _; 20 | } 21 | 22 | modifier whenPaused() { 23 | if (status == TRUE) revert NotPaused(); 24 | _; 25 | } 26 | 27 | function unpause() external virtual { 28 | status = TRUE; 29 | } 30 | 31 | function pause() external virtual { 32 | status = FALSE; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/interfaces/ICurrency.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.21; 3 | 4 | interface ICurrency { 5 | event Permit2AllowanceUpdated(bool enabled); 6 | 7 | function mint(address account, uint256 amount) external returns (bool); 8 | 9 | function burn(address account, uint256 amount) external returns (bool); 10 | } 11 | -------------------------------------------------------------------------------- /src/interfaces/IFeed.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.21; 3 | 4 | import {ERC20Token} from "../vault.sol"; 5 | import {IOSM} from "../interfaces/IOSM.sol"; 6 | 7 | interface IFeed { 8 | error BadPrice(); 9 | 10 | event Read(address collateral, uint256 price); 11 | 12 | function updatePrice(ERC20Token _collateral) external; 13 | 14 | function setCollateralOSM(ERC20Token _collateral, IOSM _oracle) external; 15 | } 16 | -------------------------------------------------------------------------------- /src/interfaces/IOSM.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.21; 3 | 4 | interface IOSM { 5 | // Reads the price and the timestamp 6 | function current() external view returns (uint256); 7 | } 8 | -------------------------------------------------------------------------------- /src/interfaces/IRate.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.21; 3 | 4 | // ========== External imports ========== 5 | import {IVault} from "./IVault.sol"; 6 | 7 | interface IRate { 8 | /** 9 | * @dev returns the current total accumulated rate i.e current accumulated base rate + current accumulated collateral rate of the given collateral 10 | * @dev should never revert! 11 | */ 12 | function calculateCurrentTotalAccumulatedRate( 13 | IVault.RateInfo calldata _baseRateInfo, 14 | IVault.RateInfo calldata _collateralRateInfo 15 | ) external view returns (uint256); 16 | 17 | function calculateCurrentAccumulatedRate(IVault.RateInfo calldata _rateInfo) external view returns (uint256); 18 | } 19 | -------------------------------------------------------------------------------- /src/interfaces/IVault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.21; 3 | 4 | interface IVault { 5 | // ------------------------------------------------ CUSTOM ERROR ------------------------------------------------ 6 | error ZeroAddress(); 7 | error UnrecognizedParam(); 8 | error BadCollateralRatio(); 9 | error PositionIsSafe(); 10 | error ZeroCollateral(); 11 | error TotalUserCollateralBelowFloor(); 12 | error CollateralAlreadyExists(); 13 | error CollateralDoesNotExist(); 14 | error NotOwnerOrReliedUpon(); 15 | error CollateralRatioNotImproved(); 16 | error NotEnoughCollateralToPay(); 17 | error EthTransferFailed(); 18 | error GlobalDebtCeilingExceeded(); 19 | error CollateralDebtCeilingExceeded(); 20 | error InsufficientCurrencyAmountToPay(); 21 | error InvalidStabilityModule(); 22 | error NotFeedContract(); 23 | 24 | // ------------------------------------------------ EVENTS ------------------------------------------------ 25 | event CollateralTypeAdded(address collateralAddress); 26 | event CollateralDeposited(address indexed owner, uint256 amount); 27 | event CollateralWithdrawn(address indexed owner, address to, uint256 amount); 28 | event CurrencyMinted(address indexed owner, uint256 amount); 29 | event CurrencyBurned(address indexed owner, uint256 amount); 30 | event FeesPaid(address indexed owner, uint256 amount); 31 | event Liquidated( 32 | address indexed owner, address liquidator, uint256 currencyAmountPaid, uint256 collateralAmountCovered 33 | ); 34 | 35 | // ------------------------------------------------ CUSTOM TYPES ------------------------------------------------ 36 | struct RateInfo { 37 | uint256 rate; // interest rate per second 38 | uint256 accumulatedRate; // Fees rate relative to PRECISION (i.e 1e18), 1% would be 1e18 / 365 days, 0.1% would be 0.1e18 / 365 days), 0.25% would be 0.25e18 / 365 days 39 | uint256 lastUpdateTime; // lastUpdateTime of accumulated rate 40 | } 41 | 42 | struct CollateralInfo { 43 | uint256 totalDepositedCollateral; // total deposited collateral 44 | uint256 totalBorrowedAmount; // total borrowed amount 45 | uint256 liquidationThreshold; // denotes how many times more collateral value is expected relative to the PRECISION (i.e 1e18). E.g liquidationThreshold of 50e18 means 2x/200% more collateral since 100 / 50 is 2. 150% will be 66.666...67e18 46 | uint256 liquidationBonus; // bonus given to liquidator relative to PRECISION. 10% would be 10e18 47 | RateInfo rateInfo; 48 | uint256 price; // Price with precision of 6 decimal places 49 | uint256 debtCeiling; // Debt Ceiling 50 | uint256 collateralFloorPerPosition; // Debt floor per position to always make liquidations profitable after gas fees 51 | uint256 additionalCollateralPrecision; // precision scaler. basically `18 - decimal of token` 52 | } 53 | 54 | struct VaultInfo { 55 | uint256 depositedCollateral; // users Collateral in the system 56 | uint256 borrowedAmount; // borrowed amount (without fees) 57 | uint256 accruedFees; // fees accrued as at `lastUpdateTime` 58 | uint256 lastTotalAccumulatedRate; // last `collateral accumulated rate + base accumulated rate` 59 | } 60 | 61 | enum ModifiableParameters { 62 | RATE, 63 | DEBT_CEILING, 64 | COLLATERAL_FLOOR_PER_POSITION, 65 | LIQUIDATION_BONUS, 66 | LIQUIDATION_THRESHOLD 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/mocks/ERC20Token.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.21; 3 | 4 | // ========== External imports ========== 5 | import {Ownable} from "solady/auth/Ownable.sol"; 6 | import {ERC20} from "solady/tokens/ERC20.sol"; 7 | 8 | contract ERC20Token is Ownable, ERC20 { 9 | string _name; 10 | string _symbol; 11 | uint8 immutable _decimals; 12 | 13 | constructor(string memory name_, string memory symbol_, uint8 decimals_) { 14 | _initializeOwner(msg.sender); 15 | _decimals = decimals_; 16 | _name = name_; 17 | _symbol = symbol_; 18 | } 19 | 20 | function decimals() public view override returns (uint8) { 21 | return _decimals; 22 | } 23 | 24 | function name() public view override returns (string memory) { 25 | return _name; 26 | } 27 | 28 | function symbol() public view override returns (string memory) { 29 | return _symbol; 30 | } 31 | 32 | /** 33 | * @dev Mints a new token 34 | * @param account address to send the minted tokens to 35 | * @param amount amount of tokens to mint 36 | */ 37 | function mint(address account, uint256 amount) external onlyOwner returns (bool) { 38 | _mint(account, amount); 39 | return true; 40 | } 41 | 42 | /** 43 | * @dev Burns a token 44 | * @param account address to burn tokens from 45 | * @param amount amount of tokens to burn 46 | */ 47 | function burn(address account, uint256 amount) external returns (bool) { 48 | if (account != msg.sender) { 49 | _spendAllowance(account, msg.sender, amount); 50 | } 51 | _burn(account, amount); 52 | return true; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/modules/feed.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.21; 3 | 4 | // ========== External imports ========== 5 | import {Ownable} from "solady/auth/Ownable.sol"; 6 | import {IOSM} from "../interfaces/IOSM.sol"; 7 | import {IFeed} from "../interfaces/IFeed.sol"; 8 | import {Vault, ERC20Token} from "../vault.sol"; 9 | import {Pausable} from "../helpers/pausable.sol"; 10 | 11 | contract Feed is IFeed, Ownable, Pausable { 12 | Vault public vault; 13 | 14 | mapping(ERC20Token => IOSM) public collaterals; 15 | 16 | function unpause() external override whenPaused onlyOwner { 17 | status = TRUE; 18 | } 19 | 20 | function pause() external override whenNotPaused onlyOwner { 21 | status = FALSE; 22 | } 23 | 24 | constructor(Vault _vault) { 25 | _initializeOwner(msg.sender); 26 | vault = _vault; 27 | status = TRUE; 28 | } 29 | 30 | function setCollateralOSM(ERC20Token collateral, IOSM oracle) external whenNotPaused onlyOwner { 31 | collaterals[collateral] = oracle; 32 | } 33 | 34 | // Updates the price of a collateral in the accounting 35 | function updatePrice(ERC20Token collateral) external whenNotPaused { 36 | uint256 price = collaterals[collateral].current(); 37 | if (price == 0) revert BadPrice(); 38 | vault.updatePrice(collateral, price); 39 | } 40 | 41 | // Updates the price of a collateral in the accounting 42 | function mockUpdatePrice(ERC20Token collateral, uint256 price) external whenNotPaused onlyOwner { 43 | if (price == 0) revert BadPrice(); 44 | vault.updatePrice(collateral, price); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/modules/rate.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.21; 3 | 4 | // ========== External imports ========== 5 | import {Vault} from "../vault.sol"; 6 | import {IRate} from "../interfaces/IRate.sol"; 7 | 8 | contract SimpleInterestRate is IRate { 9 | /** 10 | * @dev returns the current total accumulated rate i.e current accumulated base rate + current accumulated collateral rate of the given collateral 11 | * @dev should never revert! 12 | */ 13 | function calculateCurrentTotalAccumulatedRate( 14 | Vault.RateInfo calldata _baseRateInfo, 15 | Vault.RateInfo calldata _collateralRateInfo 16 | ) external view returns (uint256) { 17 | // adds together to get total rate since inception 18 | return calculateCurrentAccumulatedRate(_collateralRateInfo) + calculateCurrentAccumulatedRate(_baseRateInfo); 19 | } 20 | 21 | function calculateCurrentAccumulatedRate(Vault.RateInfo calldata _rateInfo) public view returns (uint256) { 22 | // calculates pending rate and adds it to the last stored rate 23 | uint256 _currentAccumulatedRate = 24 | _rateInfo.accumulatedRate + (_rateInfo.rate * (block.timestamp - _rateInfo.lastUpdateTime)); 25 | 26 | // adds together to get total rate since inception 27 | return _currentAccumulatedRate; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/base.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.13; 3 | 4 | import {Test, StdInvariant, console2} from "forge-std/Test.sol"; 5 | import {Vault, IVault, Currency} from "../src/vault.sol"; 6 | import {Feed, IOSM} from "../src/modules/feed.sol"; 7 | import {ERC20Token} from "../src/mocks/ERC20Token.sol"; 8 | import {ErrorsAndEvents} from "./helpers/ErrorsAndEvents.sol"; 9 | import {IRate, SimpleInterestRate} from "../src/modules/rate.sol"; 10 | import {Median} from "descent-collective/oracle-module/median.sol"; 11 | import {OSM} from "descent-collective/oracle-module/osm.sol"; 12 | import {SignatureCheckerLib} from "solady/utils/SignatureCheckerLib.sol"; 13 | 14 | contract BaseTest is Test, ErrorsAndEvents { 15 | using SignatureCheckerLib for bytes32; 16 | 17 | bytes constant INTEGER_UNDERFLOW_OVERFLOW_PANIC_ERROR = 18 | abi.encodeWithSelector(bytes4(keccak256("Panic(uint256)")), 17); 19 | bytes constant ENUM_UNDERFLOW_OVERFLOW_PANIC_ERROR = abi.encodeWithSelector(bytes4(keccak256("Panic(uint256)")), 33); 20 | uint256 constant TEN_YEARS = 365 days * 10; 21 | uint256 constant MAX_TOKEN_DECIMALS = 18; 22 | uint256 constant FALSE = 1; 23 | uint256 constant TRUE = 2; 24 | 25 | uint256 creationBlockTimestamp = block.timestamp; 26 | uint256 PRECISION = 1e18; 27 | uint256 HUNDRED_PERCENTAGE = 100e18; 28 | Vault vault; 29 | Currency xNGN; 30 | ERC20Token usdc; 31 | Feed feed; 32 | IRate simpleInterestRate; 33 | OSM osm; 34 | Median median; 35 | address owner = vm.addr(uint256(keccak256("OWNER"))); 36 | address user1 = vm.addr(uint256(keccak256("User1"))); 37 | address user2 = vm.addr(uint256(keccak256("User2"))); 38 | address user3 = vm.addr(uint256(keccak256("User3"))); 39 | address user4 = vm.addr(uint256(keccak256("User4"))); 40 | address user5 = vm.addr(uint256(keccak256("User5"))); 41 | address liquidator = vm.addr(uint256(keccak256("liquidator"))); 42 | address node0 = vm.addr(uint256(keccak256("Node0"))); 43 | uint256 onePercentPerSecondInterestRate = uint256(1e18) / 365 days; 44 | uint256 oneAndHalfPercentPerSecondInterestRate = uint256(1.5e18) / 365 days; 45 | address testStabilityModule = address(uint160(uint256(keccak256("stability module")))); 46 | 47 | function labelAddresses() private { 48 | vm.label(owner, "Owner"); 49 | vm.label(user1, "User1"); 50 | vm.label(user2, "User2"); 51 | vm.label(user3, "User3"); 52 | vm.label(user4, "User4"); 53 | vm.label(user5, "User5"); 54 | vm.label(liquidator, "liquidator"); 55 | vm.label(node0, "node0"); 56 | vm.label(address(vault), "Vault"); 57 | vm.label(address(xNGN), "xNGN"); 58 | vm.label(address(feed), "Feed"); 59 | vm.label(address(usdc), "USDC"); 60 | vm.label(testStabilityModule, "Test stability module"); 61 | vm.label(address(simpleInterestRate), "Simple interest module"); 62 | } 63 | 64 | function setUp() public virtual { 65 | vm.warp(vm.unixTime() / 100); 66 | 67 | vm.startPrank(owner); 68 | 69 | xNGN = new Currency("xNGN", "xNGN"); 70 | 71 | usdc = new ERC20Token("Circle USD", "USDC", 6); // changing the last parameter her i.e decimals and running th tests shows that it works for all token decimals <= 18 72 | 73 | vault = new Vault(xNGN, onePercentPerSecondInterestRate, type(uint256).max); 74 | 75 | feed = new Feed(vault); 76 | 77 | simpleInterestRate = new SimpleInterestRate(); 78 | 79 | median = new Median(1, address(xNGN), address(usdc)); 80 | median.authorizeNode(node0); 81 | 82 | osm = new OSM(median); 83 | 84 | vault.createCollateralType( 85 | usdc, oneAndHalfPercentPerSecondInterestRate, 50e18, 10e18, type(uint256).max, 100 * (10 ** usdc.decimals()) 86 | ); 87 | vault.updateFeedModule(address(feed)); 88 | vault.updateRateModule(simpleInterestRate); 89 | vault.updateStabilityModule(testStabilityModule); // no implementation so set it to psuedo-random address 90 | xNGN.setMinterRole(address(vault), true); 91 | 92 | // set the osm of usdc collateral 93 | feed.setCollateralOSM(usdc, IOSM(address(osm))); 94 | // sign and update price 95 | (uint256[] memory _prices, uint256[] memory _timestamps, bytes[] memory _signatures) = 96 | updateParameters(median, uint256(keccak256("Node0")), 1000e6); 97 | median.update(_prices, _timestamps, _signatures); 98 | // update osm to track price of median 99 | osm.update(); 100 | // tell feed to store osm price 101 | feed.updatePrice(usdc); 102 | 103 | ERC20Token(address(usdc)).mint(user1, 100_000 * (10 ** usdc.decimals())); 104 | ERC20Token(address(usdc)).mint(user2, 100_000 * (10 ** usdc.decimals())); 105 | ERC20Token(address(usdc)).mint(user3, 100_000 * (10 ** usdc.decimals())); 106 | ERC20Token(address(usdc)).mint(user4, 100_000 * (10 ** usdc.decimals())); 107 | ERC20Token(address(usdc)).mint(user5, 100_000 * (10 ** usdc.decimals())); 108 | 109 | vm.stopPrank(); 110 | 111 | labelAddresses(); 112 | allUsersApproveTokensForVault(); 113 | } 114 | 115 | function allUsersApproveTokensForVault() private { 116 | vm.startPrank(user1); 117 | usdc.approve(address(vault), type(uint256).max); 118 | xNGN.approve(address(vault), type(uint256).max); 119 | vm.stopPrank(); 120 | 121 | vm.startPrank(user2); 122 | usdc.approve(address(vault), type(uint256).max); 123 | xNGN.approve(address(vault), type(uint256).max); 124 | vm.stopPrank(); 125 | 126 | vm.startPrank(user3); 127 | usdc.approve(address(vault), type(uint256).max); 128 | xNGN.approve(address(vault), type(uint256).max); 129 | vm.stopPrank(); 130 | 131 | vm.startPrank(user4); 132 | usdc.approve(address(vault), type(uint256).max); 133 | xNGN.approve(address(vault), type(uint256).max); 134 | vm.stopPrank(); 135 | 136 | vm.startPrank(user5); 137 | usdc.approve(address(vault), type(uint256).max); 138 | xNGN.approve(address(vault), type(uint256).max); 139 | vm.stopPrank(); 140 | } 141 | 142 | modifier useUser1() { 143 | vm.startPrank(user1); 144 | _; 145 | } 146 | 147 | modifier useReliedOnForUser1(address relyOn) { 148 | vm.prank(user1); 149 | vault.rely(relyOn); 150 | 151 | vm.startPrank(relyOn); 152 | _; 153 | } 154 | 155 | function updateParameters(Median _median, uint256 privKey, uint256 _price) 156 | private 157 | view 158 | returns (uint256[] memory _prices, uint256[] memory _timestamps, bytes[] memory _signatures) 159 | { 160 | _prices = new uint256[](1); 161 | _timestamps = new uint256[](1); 162 | _signatures = new bytes[](1); 163 | uint8[] memory _v = new uint8[](1); 164 | bytes32[] memory _r = new bytes32[](1); 165 | bytes32[] memory _s = new bytes32[](1); 166 | 167 | _prices[0] = _price; 168 | _timestamps[0] = block.timestamp; 169 | 170 | bytes32 messageDigest = 171 | keccak256(abi.encode(_prices[0], _timestamps[0], _median.currencyPair())).toEthSignedMessageHash(); 172 | (_v[0], _r[0], _s[0]) = vm.sign(privKey, messageDigest); 173 | 174 | _signatures[0] = abi.encodePacked(_r[0], _s[0], _v[0]); 175 | } 176 | 177 | function getBaseRateInfo() internal view returns (IVault.RateInfo memory) { 178 | (uint256 rate, uint256 accumulatedRate, uint256 lastUpdateTime) = vault.baseRateInfo(); 179 | 180 | return IVault.RateInfo(rate, accumulatedRate, lastUpdateTime); 181 | } 182 | 183 | function getVaultMapping(ERC20Token _collateralToken, address _owner) 184 | internal 185 | view 186 | returns (IVault.VaultInfo memory) 187 | { 188 | (uint256 depositedCollateral, uint256 borrowedAmount, uint256 accruedFees, uint256 lastTotalAccumulatedRate) = 189 | vault.vaultMapping(_collateralToken, _owner); 190 | 191 | return IVault.VaultInfo(depositedCollateral, borrowedAmount, accruedFees, lastTotalAccumulatedRate); 192 | } 193 | 194 | function getCollateralMapping(ERC20Token _collateralToken) internal view returns (IVault.CollateralInfo memory) { 195 | ( 196 | uint256 totalDepositedCollateral, 197 | uint256 totalBorrowedAmount, 198 | uint256 liquidationThreshold, 199 | uint256 liquidationBonus, 200 | Vault.RateInfo memory rateInfo, 201 | uint256 price, 202 | uint256 debtCeiling, 203 | uint256 collateralFloorPerPosition, 204 | uint256 additionalCollateralPrecision 205 | ) = vault.collateralMapping(_collateralToken); 206 | 207 | return IVault.CollateralInfo( 208 | totalDepositedCollateral, 209 | totalBorrowedAmount, 210 | liquidationThreshold, 211 | liquidationBonus, 212 | rateInfo, 213 | price, 214 | debtCeiling, 215 | collateralFloorPerPosition, 216 | additionalCollateralPrecision 217 | ); 218 | } 219 | 220 | function calculateCurrentTotalAccumulatedRate(ERC20Token _collateralToken) internal view returns (uint256) { 221 | IVault.CollateralInfo memory _collateral = getCollateralMapping(_collateralToken); 222 | // calculates pending collateral rate and adds it to the last stored collateral rate 223 | uint256 _collateralCurrentAccumulatedRate = _collateral.rateInfo.accumulatedRate 224 | + (_collateral.rateInfo.rate * (block.timestamp - _collateral.rateInfo.lastUpdateTime)); 225 | 226 | // calculates pending base rate and adds it to the last stored base rate 227 | (uint256 _rate, uint256 _accumulatedRate, uint256 _lastUpdateTime) = vault.baseRateInfo(); 228 | uint256 _baseCurrentAccumulatedRate = _accumulatedRate + (_rate * (block.timestamp - _lastUpdateTime)); 229 | 230 | // adds together to get total rate since inception 231 | return _collateralCurrentAccumulatedRate + _baseCurrentAccumulatedRate; 232 | } 233 | 234 | function calculateUserCurrentAccruedFees(ERC20Token _collateralToken, address _owner) 235 | internal 236 | view 237 | returns (uint256 accruedFees) 238 | { 239 | IVault.VaultInfo memory userVaultInfo = getVaultMapping(_collateralToken, _owner); 240 | accruedFees = userVaultInfo.accruedFees 241 | + ( 242 | (calculateCurrentTotalAccumulatedRate(usdc) - userVaultInfo.lastTotalAccumulatedRate) 243 | * userVaultInfo.borrowedAmount 244 | ) / HUNDRED_PERCENTAGE; 245 | } 246 | 247 | function mutateAddress(address addr) internal pure returns (address) { 248 | unchecked { 249 | return address(uint160(uint256(keccak256(abi.encode(addr))))); 250 | } 251 | } 252 | 253 | function divUp(uint256 _a, uint256 _b) internal pure returns (uint256 _c) { 254 | if (_b == 0) revert(); 255 | if (_a == 0) return 0; 256 | 257 | _c = 1 + ((_a - 1) / _b); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /test/fuzz/currency/currency.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.21; 3 | 4 | import {BaseTest, ERC20Token, Currency} from "../../base.t.sol"; 5 | 6 | contract CurrencyTest is BaseTest { 7 | bytes32 constant MINTER_ROLE = keccak256("MINTER_ROLE"); // Create a new role identifier for the minter role 8 | 9 | function test_setMinterRole(address newMinter) external { 10 | // should revert if not owner 11 | vm.expectRevert(Unauthorized.selector); 12 | xNGN.setMinterRole(newMinter, true); 13 | 14 | // otherwise should work 15 | vm.prank(owner); 16 | xNGN.setMinterRole(newMinter, true); 17 | assertTrue(xNGN.minterRole(newMinter)); 18 | } 19 | 20 | function test_mint(address to, uint256 amount) external { 21 | if (to == address(0)) to = mutateAddress(to); 22 | 23 | // set minter to owner for this test 24 | vm.prank(owner); 25 | xNGN.setMinterRole(owner, true); 26 | 27 | amount = bound(amount, 0, type(uint256).max - xNGN.totalSupply()); 28 | 29 | // should revert if not minter 30 | vm.expectRevert(NotMinter.selector); 31 | xNGN.mint(to, amount); 32 | 33 | // otherwise should work 34 | uint256 initialToBalance = xNGN.balanceOf(to); 35 | 36 | vm.prank(owner); 37 | assertTrue(xNGN.mint(to, amount)); 38 | assertEq(xNGN.balanceOf(to), initialToBalance + amount); 39 | } 40 | 41 | function test_burn(address from, uint256 amount) external { 42 | if (from == address(0)) from = mutateAddress(from); 43 | amount = bound(amount, 0, xNGN.balanceOf(from)); 44 | address caller = mutateAddress(from); 45 | 46 | // should revert if not called by from and caller has no allowance 47 | if (amount > 0) { 48 | vm.prank(caller); 49 | vm.expectRevert(abi.encodeWithSelector(ERC20InsufficientAllowance.selector, address(this), 0, amount)); 50 | xNGN.burn(from, amount); 51 | } 52 | 53 | uint256 initialFromBalance = xNGN.balanceOf(from); 54 | // should work if it's called by from 55 | vm.startPrank(from); 56 | assertTrue(xNGN.burn(from, amount / 2)); 57 | assertEq(xNGN.balanceOf(from), initialFromBalance + amount / 2); 58 | 59 | // should also work if it's called by caller if caller is approved 60 | initialFromBalance = xNGN.balanceOf(from); 61 | xNGN.approve(caller, amount / 2); 62 | vm.stopPrank(); 63 | vm.prank(caller); 64 | assertTrue(xNGN.burn(from, amount / 2)); 65 | assertEq(xNGN.balanceOf(from), initialFromBalance + amount / 2); 66 | } 67 | 68 | function test_updatePermit2Allowance(bool enabled) external { 69 | // should revert if not owner 70 | vm.expectRevert(Unauthorized.selector); 71 | xNGN.updatePermit2Allowance(enabled); 72 | 73 | // otherwise should work 74 | vm.prank(owner); 75 | xNGN.updatePermit2Allowance(enabled); 76 | assertEq(xNGN.permit2Enabled(), enabled); 77 | } 78 | 79 | function test_allowance_of_permit2(address _owner, bool enabled) external { 80 | vm.prank(owner); 81 | xNGN.updatePermit2Allowance(enabled); 82 | 83 | if (xNGN.permit2Enabled()) { 84 | assertEq(xNGN.allowance(_owner, xNGN.PERMIT2()), type(uint256).max); 85 | } else { 86 | assertEq(xNGN.allowance(_owner, xNGN.PERMIT2()), 0); 87 | } 88 | } 89 | 90 | function test_recoverToken(address to) external { 91 | if (to == address(0) || to.code.length > 0 || uint256(uint160(to)) < 10) to = mutateAddress(to); 92 | 93 | ERC20Token _xNGN = ERC20Token(address(xNGN)); 94 | 95 | // mint tokens and eth to xNGN 96 | vm.startPrank(owner); 97 | xNGN.setMinterRole(owner, true); 98 | xNGN.mint(address(xNGN), 1000e18); 99 | Currency(address(usdc)).mint(address(xNGN), 1000 * (10 ** usdc.decimals())); 100 | vm.deal(address(xNGN), 5 ether); 101 | vm.stopPrank(); 102 | 103 | // should revert if not owner 104 | vm.expectRevert(Unauthorized.selector); 105 | xNGN.recoverToken(_xNGN, to); 106 | vm.expectRevert(Unauthorized.selector); 107 | xNGN.recoverToken(usdc, to); 108 | vm.expectRevert(Unauthorized.selector); 109 | xNGN.recoverToken(ERC20Token(address(0)), to); 110 | 111 | // should work 112 | vm.startPrank(owner); 113 | uint256 initialBalance = xNGN.balanceOf(to); 114 | uint256 toBeWithdrawn = xNGN.balanceOf(address(xNGN)); 115 | xNGN.recoverToken(_xNGN, to); 116 | assertEq(xNGN.balanceOf(to), initialBalance + toBeWithdrawn); 117 | 118 | initialBalance = usdc.balanceOf(to); 119 | toBeWithdrawn = usdc.balanceOf(address(xNGN)); 120 | xNGN.recoverToken(usdc, to); 121 | assertEq(usdc.balanceOf(to), initialBalance + toBeWithdrawn); 122 | 123 | initialBalance = to.balance; 124 | toBeWithdrawn = address(xNGN).balance; 125 | xNGN.recoverToken(ERC20Token(address(0)), to); 126 | assertEq(to.balance, initialBalance + toBeWithdrawn); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /test/fuzz/vault/burnCurrency/burnCurrency.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.21; 3 | 4 | import {BaseTest, ERC20Token, IVault} from "../../../base.t.sol"; 5 | 6 | contract BurnCurrencyTest is BaseTest { 7 | function setUp() public override { 8 | super.setUp(); 9 | 10 | // use user1 as default for all tests 11 | vm.startPrank(user1); 12 | 13 | // deposit amount to be used when testing 14 | vault.depositCollateral(usdc, user1, 1000 * (10 ** usdc.decimals())); 15 | 16 | // mint max amount of xNGN allowed given my collateral deposited 17 | vault.mintCurrency(usdc, user1, user1, 500_000e18); 18 | 19 | vm.stopPrank(); 20 | 21 | // get xNGN for testing burn for 22 | vm.startPrank(user2); 23 | 24 | // deposit amount to be used when testing 25 | vault.depositCollateral(usdc, user2, 10_000 * (10 ** usdc.decimals())); 26 | 27 | // mint max amount of xNGN allowed given my collateral deposited 28 | vault.mintCurrency(usdc, user2, user2, 5_000_000e18); 29 | 30 | vm.stopPrank(); 31 | 32 | // get xNGN for testing burn for 33 | vm.startPrank(user3); 34 | 35 | // deposit amount to be used when testing 36 | vault.depositCollateral(usdc, user3, 10_000 * (10 ** usdc.decimals())); 37 | 38 | // mint max amount of xNGN allowed given my collateral deposited 39 | vault.mintCurrency(usdc, user3, user3, 5_000_000e18); 40 | 41 | vm.stopPrank(); 42 | } 43 | 44 | function test_WhenCollateralDoesNotExist(ERC20Token collateral, address user, uint256 amount) external useUser1 { 45 | if (collateral == usdc) collateral = ERC20Token(mutateAddress(address(usdc))); 46 | 47 | // it should revert with custom error CollateralDoesNotExist() 48 | vm.expectRevert(CollateralDoesNotExist.selector); 49 | 50 | // call with non existing collateral 51 | vault.burnCurrency(collateral, user, amount); 52 | } 53 | 54 | modifier whenCollateralExists() { 55 | _; 56 | } 57 | 58 | function test_WhenTheAmountToBurnIsLessThanOrEqualToTheOwnersBorrowedAmount_useUser1( 59 | uint256 amount, 60 | uint256 timeElapsed 61 | ) external whenCollateralExists useUser1 { 62 | // it should accrue fees 63 | // it should emit CurrencyBurned() event with with expected indexed and unindexed parameters 64 | // it should update the owner's borrowed amount, collateral borrowed amount and global debt 65 | // it should pay back part of or all of the borrowed amount 66 | // it should update the owner's accrued fees, collateral accrued fees and paid fees and global accrued fees and paid fees 67 | // it should not pay any accrued fees 68 | 69 | amount = bound(amount, 0, 500_000e18); 70 | whenTheAmountToBurnIsLessThanOrEqualToTheOwnersBorrowedAmount(user1, amount, timeElapsed); 71 | } 72 | 73 | function test_WhenTheAmountToBurnIsLessThanOrEqualToTheOwnersBorrowedAmount_useReliedOnForUser1( 74 | uint256 amount, 75 | uint256 timeElapsed 76 | ) external whenCollateralExists useReliedOnForUser1(user2) { 77 | // it should accrue fees 78 | // it should emit CurrencyBurned() event with with expected indexed and unindexed parameters 79 | // it should update the owner's borrowed amount, collateral borrowed amount and global debt 80 | // it should pay back part of or all of the borrowed amount 81 | // it should update the owner's accrued fees, collateral accrued fees and paid fees and global accrued fees and paid fees 82 | // it should not pay any accrued fees 83 | 84 | amount = bound(amount, 0, 500_000e18); 85 | whenTheAmountToBurnIsLessThanOrEqualToTheOwnersBorrowedAmount(user2, amount, timeElapsed); 86 | } 87 | 88 | function test_WhenTheAmountToBurnIsLessThanOrEqualToTheOwnersBorrowedAmount_useNonReliedOnForUser1( 89 | uint256 amount, 90 | uint256 timeElapsed 91 | ) external whenCollateralExists { 92 | vm.startPrank(user2); 93 | // it should accrue fees 94 | // it should emit CurrencyBurned() event with with expected indexed and unindexed parameters 95 | // it should update the owner's borrowed amount, collateral borrowed amount and global debt 96 | // it should pay back part of or all of the borrowed amount 97 | // it should update the owner's accrued fees, collateral accrued fees and paid fees and global accrued fees and paid fees 98 | // it should not pay any accrued fees 99 | 100 | amount = bound(amount, 0, 500_000e18); 101 | whenTheAmountToBurnIsLessThanOrEqualToTheOwnersBorrowedAmount(user2, amount, timeElapsed); 102 | } 103 | 104 | function whenTheAmountToBurnIsLessThanOrEqualToTheOwnersBorrowedAmount( 105 | address sender, 106 | uint256 amount, 107 | uint256 timeElapsed 108 | ) private { 109 | // skip time to make accrued fees and paid fees test be effective 110 | timeElapsed = bound(timeElapsed, 0, TEN_YEARS); 111 | skip(timeElapsed); 112 | 113 | IVault.VaultInfo memory initialUserVaultInfo = getVaultMapping(usdc, user1); 114 | IVault.CollateralInfo memory initialCollateralInfo = getCollateralMapping(usdc); 115 | uint256 initialDebt = vault.debt(); 116 | uint256 initialPaidFees = vault.paidFees(); 117 | uint256 initialUserBalance = xNGN.balanceOf(sender); 118 | uint256 initialTotalSupply = xNGN.totalSupply(); 119 | 120 | // make sure it's being tested for the right amount scenario/path 121 | assertTrue(initialUserVaultInfo.borrowedAmount >= amount); 122 | 123 | // it should emit CurrencyBurned() event with with expected indexed and unindexed parameters 124 | vm.expectEmit(true, false, false, true, address(vault)); 125 | emit CurrencyBurned(user1, amount); 126 | 127 | // burn currency 128 | vault.burnCurrency(usdc, user1, amount); 129 | 130 | IVault.VaultInfo memory afterUserVaultInfo = getVaultMapping(usdc, user1); 131 | IVault.CollateralInfo memory afterCollateralInfo = getCollateralMapping(usdc); 132 | 133 | // it should update the owner's borrowed amount, collateral borrowed amount and global debt 134 | assertEq(afterUserVaultInfo.borrowedAmount, initialUserVaultInfo.borrowedAmount - amount); 135 | assertEq(afterCollateralInfo.totalBorrowedAmount, initialCollateralInfo.totalBorrowedAmount - amount); 136 | assertEq(vault.debt(), initialDebt - amount); 137 | 138 | // get expected accrued fees 139 | uint256 accruedFees = ( 140 | (calculateCurrentTotalAccumulatedRate(usdc) - initialUserVaultInfo.lastTotalAccumulatedRate) 141 | * initialUserVaultInfo.borrowedAmount 142 | ) / HUNDRED_PERCENTAGE; 143 | 144 | // it should accrue fees, per user 145 | assertEq(afterUserVaultInfo.accruedFees, initialUserVaultInfo.accruedFees + accruedFees); 146 | 147 | // it should pay back part of or all of the borrowed amount 148 | assertEq(xNGN.balanceOf(sender), initialUserBalance - amount); 149 | assertEq(xNGN.totalSupply(), initialTotalSupply - amount); 150 | 151 | // it should not pay any accrued fees 152 | assertEq(initialPaidFees, vault.paidFees()); 153 | } 154 | 155 | modifier whenTheAmountToBurnIsGreaterThanTheOwnersBorrowedAmount() { 156 | _; 157 | } 158 | 159 | function test_WhenTheAmountToBurnIsGreaterThanTheOwnersBorrowedAmountAndAccruedFees(uint256 timeElapsed) 160 | external 161 | whenCollateralExists 162 | whenTheAmountToBurnIsGreaterThanTheOwnersBorrowedAmount 163 | { 164 | vm.startPrank(user2); 165 | // get more balance for user1 by borrowing with user2 and sending to user1 to prevent the test to revert with insufficient balance error) 166 | vault.depositCollateral(usdc, user2, 1000 * (10 ** usdc.decimals())); 167 | vault.mintCurrency(usdc, user2, user1, 200_000e18); 168 | 169 | // use user1 170 | vm.stopPrank(); 171 | vm.startPrank(user1); 172 | 173 | // to enable fee accrual 174 | timeElapsed = bound(timeElapsed, 0, TEN_YEARS); 175 | skip(timeElapsed); 176 | 177 | // get accrued fees 178 | IVault.VaultInfo memory userVaultInfo = getVaultMapping(usdc, user1); 179 | userVaultInfo.accruedFees += ( 180 | (calculateCurrentTotalAccumulatedRate(usdc) - userVaultInfo.lastTotalAccumulatedRate) * 500_000e18 181 | ) / HUNDRED_PERCENTAGE; 182 | 183 | // it should revert with underflow error 184 | vm.expectRevert(INTEGER_UNDERFLOW_OVERFLOW_PANIC_ERROR); 185 | vault.burnCurrency(usdc, user1, 500_000e18 + userVaultInfo.accruedFees + 1); 186 | } 187 | 188 | function test_WhenTheAmountToBurnIsNOTGreaterThanTheOwnersBorrowedAmountAndAccruedFees_useUser1(uint256 timeElapsed) 189 | external 190 | whenCollateralExists 191 | whenTheAmountToBurnIsGreaterThanTheOwnersBorrowedAmount 192 | useUser1 193 | { 194 | vm.stopPrank(); 195 | vm.startPrank(user2); 196 | // get more balance for user1 by borrowing with user2 and sending to user1 to prevent the test to revert with insufficient balance error) 197 | vault.depositCollateral(usdc, user2, 1000 * (10 ** usdc.decimals())); 198 | vault.mintCurrency(usdc, user2, user1, 200_000e18); 199 | 200 | // use user1 201 | vm.stopPrank(); 202 | vm.startPrank(user1); 203 | 204 | // it should accrue fees 205 | // it should emit CurrencyBurned() event with with expected indexed and unindexed parameters 206 | // it should update the owner's borrowed amount, collateral borrowed amount and global debt 207 | // it should pay off ALL borrowed amount 208 | // it should update the paid fees and global accrued fees and paid fees 209 | // it should pay pay back part of or all of the accrued fees 210 | 211 | // skip time to make accrued fees and paid fees test be effective 212 | timeElapsed = bound(timeElapsed, 1, TEN_YEARS); 213 | skip(timeElapsed); 214 | 215 | IVault.VaultInfo memory initialUserVaultInfo = getVaultMapping(usdc, user1); 216 | 217 | // get expected accrued fees 218 | uint256 accruedFees = ( 219 | (calculateCurrentTotalAccumulatedRate(usdc) - initialUserVaultInfo.lastTotalAccumulatedRate) * 500_000e18 220 | ) / HUNDRED_PERCENTAGE; 221 | 222 | whenTheAmountToBurnIsNOTGreaterThanTheOwnersBorrowedAmountAndAccruedFees(user1, 500_000e18 + (accruedFees / 2)); 223 | } 224 | 225 | function test_WhenTheAmountToBurnIsNOTGreaterThanTheOwnersBorrowedAmountAndAccruedFees_exhaustive_useUser1( 226 | uint256 timeElapsed 227 | ) external whenCollateralExists whenTheAmountToBurnIsGreaterThanTheOwnersBorrowedAmount useUser1 { 228 | vm.stopPrank(); 229 | vm.startPrank(user2); 230 | // get more balance for user1 by borrowing with user2 and sending to user1 to prevent the test to revert with insufficient balance error) 231 | vault.depositCollateral(usdc, user2, 1000 * (10 ** usdc.decimals())); 232 | vault.mintCurrency(usdc, user2, user1, 200_000e18); 233 | 234 | // use user1 235 | vm.stopPrank(); 236 | vm.startPrank(user1); 237 | 238 | // it should accrue fees 239 | // it should emit CurrencyBurned() event with with expected indexed and unindexed parameters 240 | // it should update the owner's borrowed amount, collateral borrowed amount and global debt 241 | // it should pay off ALL borrowed amount 242 | // it should update the paid fees and global accrued fees and paid fees 243 | // it should pay pay back part of or all of the accrued fees 244 | 245 | // skip time to make accrued fees and paid fees test be effective 246 | timeElapsed = bound(timeElapsed, 1, TEN_YEARS); 247 | skip(timeElapsed); 248 | 249 | IVault.VaultInfo memory initialUserVaultInfo = getVaultMapping(usdc, user1); 250 | 251 | // get expected accrued fees 252 | uint256 accruedFees = ( 253 | (calculateCurrentTotalAccumulatedRate(usdc) - initialUserVaultInfo.lastTotalAccumulatedRate) * 500_000e18 254 | ) / HUNDRED_PERCENTAGE; 255 | 256 | whenTheAmountToBurnIsNOTGreaterThanTheOwnersBorrowedAmountAndAccruedFees(user1, 500_000e18 + accruedFees); 257 | } 258 | 259 | function test_WhenTheAmountToBurnIsNOTGreaterThanTheOwnersBorrowedAmountAndAccruedFees_useReliedOnForUser1( 260 | uint256 timeElapsed 261 | ) 262 | external 263 | whenCollateralExists 264 | whenTheAmountToBurnIsGreaterThanTheOwnersBorrowedAmount 265 | useReliedOnForUser1(user2) 266 | { 267 | vm.stopPrank(); 268 | vm.startPrank(user2); 269 | // get more balance for user1 by borrowing with user2 and sending to user1 to prevent the test to revert with insufficient balance error) 270 | vault.depositCollateral(usdc, user2, 1000 * (10 ** usdc.decimals())); 271 | vault.mintCurrency(usdc, user2, user1, 200_000e18); 272 | 273 | // use user1 274 | vm.stopPrank(); 275 | vm.startPrank(user2); 276 | 277 | // it should accrue fees 278 | // it should emit CurrencyBurned() event with with expected indexed and unindexed parameters 279 | // it should update the owner's borrowed amount, collateral borrowed amount and global debt 280 | // it should pay off ALL borrowed amount 281 | // it should update the paid fees and global accrued fees and paid fees 282 | // it should pay pay back part of or all of the accrued fees 283 | 284 | // skip time to make accrued fees and paid fees test be effective 285 | timeElapsed = bound(timeElapsed, 1, TEN_YEARS); 286 | skip(timeElapsed); 287 | 288 | IVault.VaultInfo memory initialUserVaultInfo = getVaultMapping(usdc, user1); 289 | 290 | // get expected accrued fees 291 | uint256 accruedFees = ( 292 | (calculateCurrentTotalAccumulatedRate(usdc) - initialUserVaultInfo.lastTotalAccumulatedRate) * 500_000e18 293 | ) / HUNDRED_PERCENTAGE; 294 | 295 | whenTheAmountToBurnIsNOTGreaterThanTheOwnersBorrowedAmountAndAccruedFees(user2, 500_000e18 + (accruedFees / 2)); 296 | } 297 | 298 | function test_WhenTheAmountToBurnIsNOTGreaterThanTheOwnersBorrowedAmountAndAccruedFees_exhaustive_useReliedOnForUser1( 299 | uint256 timeElapsed 300 | ) 301 | external 302 | whenCollateralExists 303 | whenTheAmountToBurnIsGreaterThanTheOwnersBorrowedAmount 304 | useReliedOnForUser1(user2) 305 | { 306 | vm.stopPrank(); 307 | vm.startPrank(user2); 308 | // get more balance for user1 by borrowing with user2 and sending to user1 to prevent the test to revert with insufficient balance error) 309 | vault.depositCollateral(usdc, user2, 1000 * (10 ** usdc.decimals())); 310 | vault.mintCurrency(usdc, user2, user1, 200_000e18); 311 | 312 | // use user1 313 | vm.stopPrank(); 314 | vm.startPrank(user2); 315 | 316 | // it should accrue fees 317 | // it should emit CurrencyBurned() event with with expected indexed and unindexed parameters 318 | // it should update the owner's borrowed amount, collateral borrowed amount and global debt 319 | // it should pay off ALL borrowed amount 320 | // it should update the paid fees and global accrued fees and paid fees 321 | // it should pay pay back part of or all of the accrued fees 322 | 323 | // skip time to make accrued fees and paid fees test be effective 324 | timeElapsed = bound(timeElapsed, 1, TEN_YEARS); 325 | skip(timeElapsed); 326 | 327 | IVault.VaultInfo memory initialUserVaultInfo = getVaultMapping(usdc, user1); 328 | 329 | // get expected accrued fees 330 | uint256 accruedFees = ( 331 | (calculateCurrentTotalAccumulatedRate(usdc) - initialUserVaultInfo.lastTotalAccumulatedRate) * 500_000e18 332 | ) / HUNDRED_PERCENTAGE; 333 | 334 | whenTheAmountToBurnIsNOTGreaterThanTheOwnersBorrowedAmountAndAccruedFees(user2, 500_000e18 + accruedFees); 335 | } 336 | 337 | function test_WhenTheAmountToBurnIsNOTGreaterThanTheOwnersBorrowedAmountAndAccruedFees_useNonReliedOnForUser1( 338 | uint256 timeElapsed 339 | ) external whenCollateralExists whenTheAmountToBurnIsGreaterThanTheOwnersBorrowedAmount { 340 | vm.stopPrank(); 341 | vm.startPrank(user2); 342 | // get more balance for user1 by borrowing with user2 and sending to user1 to prevent the test to revert with insufficient balance error) 343 | vault.depositCollateral(usdc, user2, 1000 * (10 ** usdc.decimals())); 344 | vault.mintCurrency(usdc, user2, user1, 200_000e18); 345 | 346 | // use user1 347 | vm.stopPrank(); 348 | vm.startPrank(user2); 349 | 350 | // it should accrue fees 351 | // it should emit CurrencyBurned() event with with expected indexed and unindexed parameters 352 | // it should update the owner's borrowed amount, collateral borrowed amount and global debt 353 | // it should pay off ALL borrowed amount 354 | // it should update the paid fees and global accrued fees and paid fees 355 | // it should pay pay back part of or all of the accrued fees 356 | 357 | // skip time to make accrued fees and paid fees test be effective 358 | timeElapsed = bound(timeElapsed, 1, TEN_YEARS); 359 | skip(timeElapsed); 360 | 361 | IVault.VaultInfo memory initialUserVaultInfo = getVaultMapping(usdc, user1); 362 | 363 | // get expected accrued fees 364 | uint256 accruedFees = ( 365 | (calculateCurrentTotalAccumulatedRate(usdc) - initialUserVaultInfo.lastTotalAccumulatedRate) * 500_000e18 366 | ) / HUNDRED_PERCENTAGE; 367 | 368 | whenTheAmountToBurnIsNOTGreaterThanTheOwnersBorrowedAmountAndAccruedFees(user2, 500_000e18 + (accruedFees / 2)); 369 | } 370 | 371 | function test_WhenTheAmountToBurnIsNOTGreaterThanTheOwnersBorrowedAmountAndAccruedFees_exhaustive_useNonReliedOnForUser1( 372 | uint256 timeElapsed 373 | ) external whenCollateralExists whenTheAmountToBurnIsGreaterThanTheOwnersBorrowedAmount { 374 | vm.stopPrank(); 375 | vm.startPrank(user2); 376 | // get more balance for user1 by borrowing with user2 and sending to user1 to prevent the test to revert with insufficient balance error) 377 | vault.depositCollateral(usdc, user2, 1000 * (10 ** usdc.decimals())); 378 | vault.mintCurrency(usdc, user2, user1, 200_000e18); 379 | 380 | // use user1 381 | vm.stopPrank(); 382 | vm.startPrank(user2); 383 | 384 | // it should accrue fees 385 | // it should emit CurrencyBurned() event with with expected indexed and unindexed parameters 386 | // it should update the owner's borrowed amount, collateral borrowed amount and global debt 387 | // it should pay off ALL borrowed amount 388 | // it should update the paid fees and global accrued fees and paid fees 389 | // it should pay pay back part of or all of the accrued fees 390 | 391 | // skip time to make accrued fees and paid fees test be effective 392 | timeElapsed = bound(timeElapsed, 1, TEN_YEARS); 393 | skip(timeElapsed); 394 | 395 | IVault.VaultInfo memory initialUserVaultInfo = getVaultMapping(usdc, user1); 396 | 397 | // get expected accrued fees 398 | uint256 accruedFees = ( 399 | (calculateCurrentTotalAccumulatedRate(usdc) - initialUserVaultInfo.lastTotalAccumulatedRate) * 500_000e18 400 | ) / HUNDRED_PERCENTAGE; 401 | 402 | whenTheAmountToBurnIsNOTGreaterThanTheOwnersBorrowedAmountAndAccruedFees(user2, 500_000e18 + accruedFees); 403 | } 404 | 405 | function whenTheAmountToBurnIsNOTGreaterThanTheOwnersBorrowedAmountAndAccruedFees(address sender, uint256 amount) 406 | private 407 | { 408 | IVault.VaultInfo memory initialUserVaultInfo = getVaultMapping(usdc, user1); 409 | IVault.CollateralInfo memory initialCollateralInfo = getCollateralMapping(usdc); 410 | uint256 initialDebt = vault.debt(); 411 | uint256 initialPaidFees = vault.paidFees(); 412 | uint256 initialUserBalance = xNGN.balanceOf(sender); 413 | uint256 initialTotalSupply = xNGN.totalSupply(); 414 | 415 | // make sure it's being tested for the right amount scenario/path 416 | assertTrue(initialUserVaultInfo.borrowedAmount < amount); 417 | 418 | // it should emit CurrencyBurned() event with with expected indexed and unindexed parameters 419 | vm.expectEmit(true, false, false, true, address(vault)); 420 | emit CurrencyBurned(user1, 500_000e18); 421 | 422 | // it should emit FeesPaid() event with with expected indexed and unindexed parameters 423 | vm.expectEmit(true, false, false, true, address(vault)); 424 | emit FeesPaid(user1, amount - 500_000e18); 425 | 426 | // burn currency 427 | vault.burnCurrency(usdc, user1, amount); 428 | 429 | IVault.VaultInfo memory afterUserVaultInfo = getVaultMapping(usdc, user1); 430 | IVault.CollateralInfo memory afterCollateralInfo = getCollateralMapping(usdc); 431 | uint256 fees = amount - initialUserVaultInfo.borrowedAmount; 432 | 433 | // it should update the owner's borrowed amount, collateral borrowed amount and global debt 434 | assertEq(afterUserVaultInfo.borrowedAmount, 0); 435 | assertEq( 436 | afterCollateralInfo.totalBorrowedAmount, 437 | initialCollateralInfo.totalBorrowedAmount - initialUserVaultInfo.borrowedAmount 438 | ); 439 | assertEq(vault.debt(), initialDebt - initialUserVaultInfo.borrowedAmount); 440 | 441 | // get expected accrued fees 442 | uint256 accruedFees = ( 443 | (calculateCurrentTotalAccumulatedRate(usdc) - initialUserVaultInfo.lastTotalAccumulatedRate) * 500_000e18 444 | ) / HUNDRED_PERCENTAGE; 445 | 446 | // it should accrue fees, per user 447 | assertEq(initialUserVaultInfo.accruedFees + accruedFees - fees, afterUserVaultInfo.accruedFees); 448 | 449 | // it should pay back part of or all of the borrowed amount 450 | assertEq(xNGN.balanceOf(sender), initialUserBalance - amount); 451 | assertEq(xNGN.totalSupply(), initialTotalSupply - (amount - fees)); 452 | 453 | // it should update vault's paid fees 454 | assertEq(initialPaidFees, vault.paidFees() - fees); 455 | } 456 | } 457 | -------------------------------------------------------------------------------- /test/fuzz/vault/burnCurrency/burnCurrency.tree: -------------------------------------------------------------------------------- 1 | withdrawCollateralTest.t.sol 2 | ├── when collateral does not exist 3 | │ └── it should revert with custom error CollateralDoesNotExist() 4 | └── when collateral exists 5 | └── when caller is owner or relied upon by owner or none 6 | ├── when the amount to burn is less than or equal to the owner's borrowed amount 7 | │ ├── it should accrue fees 8 | │ ├── it should emit CurrencyBurned() event with with expected indexed and unindexed parameters 9 | │ ├── it should update the owner's borrowed amount, collateral borrowed amount and global debt 10 | │ ├── it should pay back part of or all of the borrowed amount 11 | │ └── it should not pay any accrued fees 12 | └── when the amount to burn is greater than the owner's borrowed amount 13 | ├── when the amount to burn is greater than the owner's borrowed amount and accrued fees 14 | │ └── it should revert with underflow error 15 | └── when the amount to burn is NOT greater than the owner's borrowed amount and accrued fees 16 | ├── it should accrue fees 17 | ├── it should emit CurrencyBurned() event with with expected indexed and unindexed parameters 18 | ├── it should update the owner's borrowed amount, collateral borrowed amount and global debt 19 | ├── it should pay off ALL borrowed amount 20 | ├── it should emit FeesPaid() event with with expected indexed and unindexed parameters 21 | ├── it should update the global paid fees and collateral paid fees 22 | └── it should pay back part of or all of the accrued fees -------------------------------------------------------------------------------- /test/fuzz/vault/depositCollateral/depositCollateral.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.21; 3 | 4 | import {BaseTest, ERC20Token, IVault} from "../../../base.t.sol"; 5 | 6 | contract DepositCollateralTest is BaseTest { 7 | function test_WhenVaultIsPaused(ERC20Token collateral, address user, uint256 amount) external useUser1 { 8 | // use owner to pause vault 9 | vm.stopPrank(); 10 | vm.prank(owner); 11 | vault.pause(); 12 | 13 | vm.startPrank(user1); 14 | 15 | // it should revert with custom error Paused() 16 | vm.expectRevert(Paused.selector); 17 | 18 | // call when vault is paused 19 | vault.depositCollateral(collateral, user, amount); 20 | } 21 | 22 | modifier whenVaultIsNotPaused() { 23 | _; 24 | } 25 | 26 | function test_WhenCollateralDoesNotExist(ERC20Token collateral, address user, uint256 amount) 27 | external 28 | whenVaultIsNotPaused 29 | useUser1 30 | { 31 | if (collateral == usdc) collateral = ERC20Token(mutateAddress(address(usdc))); 32 | 33 | // it should revert with custom error CollateralDoesNotExist() 34 | vm.expectRevert(CollateralDoesNotExist.selector); 35 | 36 | // call with non existing collateral 37 | vault.depositCollateral(collateral, user, amount); 38 | } 39 | 40 | modifier whenCollateralExist() { 41 | _; 42 | } 43 | 44 | function test_WhenCallerIsNotOwnerAndNotReliedUponByOwner(uint256 amount) 45 | external 46 | whenVaultIsNotPaused 47 | whenCollateralExist 48 | { 49 | // use unrelied upon user 50 | vm.startPrank(user2); 51 | 52 | // it should emit CollateralDeposited() event 53 | // it should update the _owner's deposited collateral and collateral's total deposit 54 | // it should send the collateral token to the vault from the _owner 55 | whenCallerIsOwnerOrReliedUponByOwner(user2, amount); 56 | } 57 | 58 | function test_WhenCallerIsReliedUponByOwner(uint256 amount) 59 | external 60 | whenVaultIsNotPaused 61 | whenCollateralExist 62 | useReliedOnForUser1(user2) 63 | { 64 | // it should emit CollateralDeposited() event 65 | // it should update the _owner's deposited collateral and collateral's total deposit 66 | // it should send the collateral token to the vault from the _owner 67 | whenCallerIsOwnerOrReliedUponByOwner(user2, amount); 68 | } 69 | 70 | function test_WhenCallerIsOwner(uint256 amount) external whenVaultIsNotPaused whenCollateralExist useUser1 { 71 | // it should emit CollateralDeposited() event 72 | // it should update the _owner's deposited collateral and collateral's total deposit 73 | // it should send the collateral token to the vault from the _owner 74 | whenCallerIsOwnerOrReliedUponByOwner(user1, amount); 75 | } 76 | 77 | function whenCallerIsOwnerOrReliedUponByOwner(address payer, uint256 amount) private { 78 | // bound amount so that it's within callers balance 79 | amount = bound(amount, 0, usdc.balanceOf(payer)); 80 | 81 | // cache pre balances 82 | uint256 userOldBalance = usdc.balanceOf(payer); 83 | uint256 vaultOldBalance = usdc.balanceOf(address(vault)); 84 | 85 | // it should emit CollateralDeposited() event 86 | vm.expectEmit(true, false, false, true, address(vault)); 87 | emit CollateralDeposited(user1, 1000 * (10 ** usdc.decimals())); 88 | 89 | // deposit 1,000 usdc into vault 90 | vault.depositCollateral(usdc, user1, 1000 * (10 ** usdc.decimals())); 91 | 92 | // it should update the _owner's deposited collateral and collateral's total deposit 93 | IVault.VaultInfo memory afterUserVaultInfo = getVaultMapping(usdc, user1); 94 | IVault.CollateralInfo memory afterCollateralInfo = getCollateralMapping(usdc); 95 | 96 | assertEq(afterCollateralInfo.totalDepositedCollateral, 1000 * (10 ** usdc.decimals())); 97 | assertEq(afterUserVaultInfo.depositedCollateral, 1000 * (10 ** usdc.decimals())); 98 | 99 | // it should send the collateral token to the vault from the _owner 100 | assertEq(usdc.balanceOf(address(vault)) - vaultOldBalance, 1000 * (10 ** usdc.decimals())); 101 | assertEq(userOldBalance - usdc.balanceOf(payer), 1000 * (10 ** usdc.decimals())); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /test/fuzz/vault/depositCollateral/depositCollateral.tree: -------------------------------------------------------------------------------- 1 | depositCollateralTest.t.sol 2 | ├── when vault is paused 3 | │ └── it should revert with custom error Paused() 4 | └── when vault is not paused 5 | ├── when collateral does not exist 6 | │ └── it should revert with custom error CollateralDoesNotExist() 7 | └── when collateral exist 8 | └── when caller is owner or relied upon by owner or none of both 9 | ├── it should emit CollateralDeposited() event 10 | ├── it should update the _owner's deposited collateral and collateral's total deposit 11 | └── it should send the collateral token to the vault from the _owner -------------------------------------------------------------------------------- /test/fuzz/vault/liquidate/liquidate.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.21; 3 | 4 | import {BaseTest, ERC20Token, IVault} from "../../../base.t.sol"; 5 | 6 | contract LiquidateTest is BaseTest { 7 | function setUp() public override { 8 | super.setUp(); 9 | 10 | // use user1 as default for all tests 11 | vm.startPrank(user1); 12 | 13 | // deposit amount to be used when testing 14 | vault.depositCollateral(usdc, user1, 1000 * (10 ** usdc.decimals())); 15 | 16 | // mint max amount 17 | vault.mintCurrency(usdc, user1, user1, 500_000e18); 18 | 19 | // skip time so that interest accrues and position is now under water 20 | skip(365 days); 21 | 22 | // deposit and mint with user 2, to be used for liquidation 23 | vm.stopPrank(); 24 | vm.startPrank(user2); 25 | vault.depositCollateral(usdc, user2, 10_000 * (10 ** usdc.decimals())); 26 | vault.mintCurrency(usdc, user2, user2, 5_000_000e18); 27 | 28 | vm.stopPrank(); 29 | } 30 | 31 | function test_WhenCollateralDoesNotExist(ERC20Token collateral, address user, uint256 amount) external { 32 | if (collateral == usdc) collateral = ERC20Token(mutateAddress(address(usdc))); 33 | 34 | // it should revert with custom error CollateralDoesNotExist() 35 | vm.expectRevert(CollateralDoesNotExist.selector); 36 | 37 | // call with non existing collateral 38 | vault.liquidate(collateral, user, user2, amount); 39 | } 40 | 41 | modifier whenCollateralExists() { 42 | _; 43 | } 44 | 45 | function test_WhenTheVaultIsSafe(uint256 amount) external whenCollateralExists useUser1 { 46 | // pay back some currency to make position safe 47 | vault.burnCurrency(usdc, user1, 100_000e18); 48 | 49 | // use user 2 50 | vm.stopPrank(); 51 | vm.startPrank(user2); 52 | 53 | // it should revert with custom error PositionIsSafe() 54 | vm.expectRevert(PositionIsSafe.selector); 55 | vault.liquidate(usdc, user1, user2, amount); 56 | } 57 | 58 | modifier whenTheVaultIsNotSafe() { 59 | _; 60 | } 61 | 62 | function test_WhenTheCurrencyAmountToBurnIsGreaterThanTheOwnersBorrowedAmountAndAccruedFees(uint256 amount) 63 | external 64 | whenCollateralExists 65 | whenTheVaultIsNotSafe 66 | { 67 | vm.startPrank(user2); 68 | 69 | uint256 accruedFees = calculateUserCurrentAccruedFees(usdc, user1); 70 | amount = bound(amount, 500_000e18 + accruedFees + 1, type(uint256).max - 1); // - 1 here because .max is used for un-frontrunnable full liquidation 71 | 72 | // it should revert with underflow error 73 | vm.expectRevert(INTEGER_UNDERFLOW_OVERFLOW_PANIC_ERROR); 74 | vault.liquidate(usdc, user1, user2, amount); 75 | } 76 | 77 | modifier whenTheCurrencyAmountToBurnIsLessThanOrEqualToTheOwnersBorrowedAmountAndAccruedFees() { 78 | _; 79 | } 80 | 81 | function test_WhenTheVaultsCollateralRatioDoesNotImproveAfterLiquidation() 82 | external 83 | whenCollateralExists 84 | whenTheVaultIsNotSafe 85 | whenTheCurrencyAmountToBurnIsLessThanOrEqualToTheOwnersBorrowedAmountAndAccruedFees 86 | { 87 | vm.startPrank(user2); 88 | 89 | // it should revert with custom error CollateralRatioNotImproved() 90 | vm.expectRevert(CollateralRatioNotImproved.selector); 91 | vault.liquidate(usdc, user1, user2, 1); 92 | } 93 | 94 | modifier whenVaultsCollateralRatioImprovesAfterLiquidation() { 95 | _; 96 | } 97 | 98 | function test_WhenThe_currencyAmountToPayIsUint256Max() 99 | external 100 | whenCollateralExists 101 | whenTheVaultIsNotSafe 102 | whenTheCurrencyAmountToBurnIsLessThanOrEqualToTheOwnersBorrowedAmountAndAccruedFees 103 | whenVaultsCollateralRatioImprovesAfterLiquidation 104 | { 105 | liquidate_exhaustively(true); 106 | } 107 | 108 | function test_WhenThe_currencyAmountToPayIsNOTUint256Max_fullyCoveringFees() 109 | external 110 | whenCollateralExists 111 | whenTheVaultIsNotSafe 112 | whenTheCurrencyAmountToBurnIsLessThanOrEqualToTheOwnersBorrowedAmountAndAccruedFees 113 | whenVaultsCollateralRatioImprovesAfterLiquidation 114 | { 115 | /// fully cover fees 116 | liquidate_exhaustively(false); 117 | } 118 | 119 | function liquidate_exhaustively(bool useUintMax) private { 120 | vm.startPrank(user2); 121 | 122 | uint256 oldTotalSupply = xNGN.totalSupply(); 123 | 124 | // cache pre storage vars and old accrued fees 125 | IVault.VaultInfo memory initialUserVaultInfo = getVaultMapping(usdc, user1); 126 | IVault.CollateralInfo memory initialCollateralInfo = getCollateralMapping(usdc); 127 | uint256 initialDebt = vault.debt(); 128 | uint256 initialPaidFees = vault.paidFees(); 129 | 130 | uint256 userAccruedFees = calculateUserCurrentAccruedFees(usdc, user1); 131 | uint256 totalCurrencyPaid = initialUserVaultInfo.borrowedAmount + userAccruedFees; 132 | uint256 collateralToPayOut = (totalCurrencyPaid * PRECISION) / (initialCollateralInfo.price * 1e12); 133 | collateralToPayOut = collateralToPayOut / (10 ** initialCollateralInfo.additionalCollateralPrecision); 134 | collateralToPayOut += (collateralToPayOut * initialCollateralInfo.liquidationBonus) / HUNDRED_PERCENTAGE; 135 | uint256 initialUser2Bal = usdc.balanceOf(user2); 136 | 137 | // it should emit Liquidated() event with with expected indexed and unindexed parameters 138 | vm.expectEmit(true, false, false, true, address(vault)); 139 | emit Liquidated(user1, user2, totalCurrencyPaid, collateralToPayOut); 140 | 141 | // it should emit CurrencyBurned() event with with expected indexed and unindexed parameters 142 | vm.expectEmit(true, false, false, true, address(vault)); 143 | emit CurrencyBurned(user1, initialUserVaultInfo.borrowedAmount); 144 | 145 | // it should emit FeesPaid() event with with expected indexed and unindexed parameters 146 | vm.expectEmit(true, false, false, true, address(vault)); 147 | emit FeesPaid(user1, userAccruedFees); 148 | 149 | // liquidate 150 | uint256 amount = useUintMax ? type(uint256).max : totalCurrencyPaid; 151 | vault.liquidate(usdc, user1, user2, amount); 152 | 153 | IVault.VaultInfo memory afterUserVaultInfo = getVaultMapping(usdc, user1); 154 | IVault.CollateralInfo memory afterCollateralInfo = getCollateralMapping(usdc); 155 | 156 | // it should accrue fees 157 | // the fact the tx did not revert with underflow error when moving accrued fees (vault and global) to paid fees (which we will assert below) proves this 158 | 159 | // it should update the vault's deposited collateral and collateral total deposited collateral 160 | assertEq(afterUserVaultInfo.depositedCollateral, initialUserVaultInfo.depositedCollateral - collateralToPayOut); 161 | assertEq( 162 | afterCollateralInfo.totalDepositedCollateral, 163 | initialCollateralInfo.totalDepositedCollateral - collateralToPayOut 164 | ); 165 | 166 | // it should pay out a max of covered collateral + 10% and a min of 0 167 | uint256 user2BalDiff = usdc.balanceOf(user2) - initialUser2Bal; 168 | assertTrue(user2BalDiff == collateralToPayOut); 169 | 170 | // it should update the vault's borrowed amount, collateral borrowed amount and global debt 171 | assertEq(oldTotalSupply - xNGN.totalSupply(), 500_000e18); 172 | assertEq(afterUserVaultInfo.borrowedAmount, initialUserVaultInfo.borrowedAmount - 500_000e18); 173 | assertEq(afterCollateralInfo.totalBorrowedAmount, initialCollateralInfo.totalBorrowedAmount - 500_000e18); 174 | assertEq(vault.debt(), initialDebt - 500_000e18); 175 | 176 | // it should pay off all of vaults borrowed amount 177 | assertEq(afterUserVaultInfo.borrowedAmount, 0); 178 | 179 | // it should update the global paid fees 180 | assertEq(vault.paidFees(), initialPaidFees + userAccruedFees); 181 | 182 | // it should pay off all of vaults fees (set to be 0) and update the global accrued fees 183 | assertEq(afterUserVaultInfo.accruedFees, 0); 184 | } 185 | 186 | function test_WhenThe_currencyAmountToPayIsNOTUint256Max_notCoveringFees(uint256 amountToLiquidate) 187 | external 188 | whenCollateralExists 189 | whenTheVaultIsNotSafe 190 | whenTheCurrencyAmountToBurnIsLessThanOrEqualToTheOwnersBorrowedAmountAndAccruedFees 191 | whenVaultsCollateralRatioImprovesAfterLiquidation 192 | { 193 | vm.startPrank(user2); 194 | 195 | uint256 oldTotalSupply = xNGN.totalSupply(); 196 | 197 | // cache pre storage vars and old accrued fees 198 | IVault.VaultInfo memory initialUserVaultInfo = getVaultMapping(usdc, user1); 199 | IVault.CollateralInfo memory initialCollateralInfo = getCollateralMapping(usdc); 200 | uint256 initialDebt = vault.debt(); 201 | uint256 initialPaidFees = vault.paidFees(); 202 | 203 | uint256 userAccruedFees = calculateUserCurrentAccruedFees(usdc, user1); 204 | 205 | amountToLiquidate = bound(amountToLiquidate, 1e18, initialUserVaultInfo.borrowedAmount); 206 | uint256 collateralToPayOut = (amountToLiquidate * PRECISION) / (initialCollateralInfo.price * 1e12); 207 | collateralToPayOut = collateralToPayOut / (10 ** initialCollateralInfo.additionalCollateralPrecision); 208 | collateralToPayOut += (collateralToPayOut * initialCollateralInfo.liquidationBonus) / HUNDRED_PERCENTAGE; 209 | 210 | uint256 initialUser2Bal = usdc.balanceOf(user2); 211 | 212 | // it should emit Liquidated() event with with expected indexed and unindexed parameters 213 | vm.expectEmit(true, false, false, true, address(vault)); 214 | emit Liquidated(user1, user2, amountToLiquidate, collateralToPayOut); 215 | 216 | // it should emit CurrencyBurned() event with with expected indexed and unindexed parameters 217 | vm.expectEmit(true, false, false, true, address(vault)); 218 | emit CurrencyBurned(user1, amountToLiquidate); 219 | 220 | // liquidate 221 | vault.liquidate(usdc, user1, user2, amountToLiquidate); 222 | 223 | IVault.VaultInfo memory afterUserVaultInfo = getVaultMapping(usdc, user1); 224 | IVault.CollateralInfo memory afterCollateralInfo = getCollateralMapping(usdc); 225 | 226 | // it should accrue fees 227 | // the fact the tx did not revert with underflow error when moving accrued fees (vault and global) to paid fees (which we will assert below) proves this 228 | 229 | // it should update the vault's deposited collateral and collateral total deposited collateral 230 | assertEq(afterUserVaultInfo.depositedCollateral, initialUserVaultInfo.depositedCollateral - collateralToPayOut); 231 | assertEq( 232 | afterCollateralInfo.totalDepositedCollateral, 233 | initialCollateralInfo.totalDepositedCollateral - collateralToPayOut 234 | ); 235 | 236 | // it should pay out a max of covered collateral + 10% and a min of 0 237 | uint256 user2BalDiff = usdc.balanceOf(user2) - initialUser2Bal; 238 | assertTrue(user2BalDiff == collateralToPayOut); 239 | 240 | // it should update the vault's borrowed amount, collateral borrowed amount and global debt 241 | assertEq(oldTotalSupply - xNGN.totalSupply(), amountToLiquidate); 242 | assertEq(afterUserVaultInfo.borrowedAmount, initialUserVaultInfo.borrowedAmount - amountToLiquidate); 243 | assertEq(afterCollateralInfo.totalBorrowedAmount, initialCollateralInfo.totalBorrowedAmount - amountToLiquidate); 244 | assertEq(vault.debt(), initialDebt - amountToLiquidate); 245 | 246 | // it should pay off all of or part of the vaults borrowed amount 247 | assertEq(afterUserVaultInfo.borrowedAmount, initialUserVaultInfo.borrowedAmount - amountToLiquidate); 248 | 249 | // it should update the global paid fees 250 | assertEq(vault.paidFees(), initialPaidFees); 251 | 252 | // it should update the vaults 253 | assertEq(afterUserVaultInfo.accruedFees, userAccruedFees); 254 | } 255 | 256 | function test_WhenThe_currencyAmountToPayIsNOTUint256Max_partiallyCoveringFees(uint256 feeToPay) 257 | external 258 | whenCollateralExists 259 | whenTheVaultIsNotSafe 260 | whenTheCurrencyAmountToBurnIsLessThanOrEqualToTheOwnersBorrowedAmountAndAccruedFees 261 | whenVaultsCollateralRatioImprovesAfterLiquidation 262 | { 263 | vm.startPrank(user2); 264 | 265 | uint256 oldTotalSupply = xNGN.totalSupply(); 266 | 267 | // cache pre storage vars and old accrued fees 268 | IVault.VaultInfo memory initialUserVaultInfo = getVaultMapping(usdc, user1); 269 | IVault.CollateralInfo memory initialCollateralInfo = getCollateralMapping(usdc); 270 | uint256 initialDebt = vault.debt(); 271 | uint256 initialPaidFees = vault.paidFees(); 272 | 273 | uint256 userAccruedFees = calculateUserCurrentAccruedFees(usdc, user1); 274 | 275 | feeToPay = bound(feeToPay, 1, userAccruedFees - 1); // - 1 because we are testing for when fees are not compleetely paid 276 | uint256 amountToLiquidate = 500_000e18 + feeToPay; 277 | uint256 collateralToPayOut = (amountToLiquidate * PRECISION) / (initialCollateralInfo.price * 1e12); 278 | collateralToPayOut = collateralToPayOut / (10 ** initialCollateralInfo.additionalCollateralPrecision); 279 | collateralToPayOut += (collateralToPayOut * initialCollateralInfo.liquidationBonus) / HUNDRED_PERCENTAGE; 280 | 281 | uint256 initialUser2Bal = usdc.balanceOf(user2); 282 | 283 | // it should emit Liquidated() event with with expected indexed and unindexed parameters 284 | vm.expectEmit(true, false, false, true, address(vault)); 285 | emit Liquidated(user1, user2, amountToLiquidate, collateralToPayOut); 286 | 287 | // it should emit CurrencyBurned() event with with expected indexed and unindexed parameters 288 | vm.expectEmit(true, false, false, true, address(vault)); 289 | emit CurrencyBurned(user1, initialUserVaultInfo.borrowedAmount); 290 | 291 | // it should emit FeesPaid() event with with expected indexed and unindexed parameters 292 | vm.expectEmit(true, false, false, true, address(vault)); 293 | emit FeesPaid(user1, amountToLiquidate - initialUserVaultInfo.borrowedAmount); 294 | 295 | // liquidate 296 | vault.liquidate(usdc, user1, user2, amountToLiquidate); 297 | 298 | IVault.VaultInfo memory afterUserVaultInfo = getVaultMapping(usdc, user1); 299 | IVault.CollateralInfo memory afterCollateralInfo = getCollateralMapping(usdc); 300 | 301 | // it should accrue fees 302 | // the fact the tx did not revert with underflow error when moving accrued fees (vault and global) to paid fees (which we will assert below) proves this 303 | 304 | // it should update the vault's deposited collateral and collateral total deposited collateral 305 | assertEq(afterUserVaultInfo.depositedCollateral, initialUserVaultInfo.depositedCollateral - collateralToPayOut); 306 | assertEq( 307 | afterCollateralInfo.totalDepositedCollateral, 308 | initialCollateralInfo.totalDepositedCollateral - collateralToPayOut 309 | ); 310 | 311 | // it should pay out a max of covered collateral + 10% and a min of 0 312 | assertEq(usdc.balanceOf(user2) - initialUser2Bal, collateralToPayOut); 313 | 314 | // it should update the vault's borrowed amount, collateral borrowed amount and global debt 315 | assertEq(oldTotalSupply - xNGN.totalSupply(), 500_000e18); 316 | assertEq(afterUserVaultInfo.borrowedAmount, initialUserVaultInfo.borrowedAmount - 500_000e18); 317 | assertEq(afterCollateralInfo.totalBorrowedAmount, initialCollateralInfo.totalBorrowedAmount - 500_000e18); 318 | assertEq(vault.debt(), initialDebt - 500_000e18); 319 | 320 | // it should update the global paid fees 321 | assertEq(vault.paidFees(), initialPaidFees + feeToPay); 322 | 323 | // it should pay off all of or part of the vaults fees 324 | // it should update the vaults 325 | assertEq(afterUserVaultInfo.accruedFees, userAccruedFees - feeToPay); 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /test/fuzz/vault/liquidate/liquidate.tree: -------------------------------------------------------------------------------- 1 | liquidate.t.sol 2 | ├── when collateral does not exist 3 | │ └── it should revert with custom error CollateralDoesNotExist() 4 | └── when collateral exists 5 | ├── when the vault is safe 6 | │ └── it should revert with custom error PositionIsSafe() 7 | └── when the vault is not safe 8 | ├── when the currency amount to burn is greater than the owner's borrowed amount and accrued fees 9 | │ └── it should revert with underflow error 10 | └── when the currency amount to burn is less than or equal to the owner's borrowed amount and accrued fees 11 | ├── when the vaults collateral ratio does not improve after liquidation 12 | │ └── it should revert with custom error CollateralRatioNotImproved() 13 | └── when vaults collateral ratio improves after liquidation 14 | ├── when the _currencyAmountToPay is uint256 max 15 | │ ├── it should accrue fees 16 | │ ├── it should emit Liquidated() event with with expected indexed and unindexed parameters 17 | │ ├── it should emit CurrencyBurned() event with with expected indexed and unindexed parameters 18 | │ ├── it should emit FeesPaid() event with with expected indexed and unindexed parameters 19 | │ ├── it should update the vault's deposited collateral and collateral total deposited collateral 20 | │ ├── it should pay out a max of covered collateral + 10% and a min of 0 21 | │ ├── it should update the vault's borrowed amount, collateral borrowed amount and global debt 22 | │ ├── it should pay off all of vaults borrowed amount 23 | │ ├── it should update the global paid fees and collateral paid fees 24 | │ └── it should pay off all of vaults fees (set to be 0) and update the global accrued fees 25 | └── when the _currencyAmountToPay is NOT uint256 max 26 | ├── it should accrue fees 27 | ├── it should emit Liquidated() event with with expected indexed and unindexed parameters 28 | ├── it should emit CurrencyBurned() event with with expected indexed and unindexed parameters 29 | ├── it should emit FeesPaid() event with with expected indexed and unindexed parameters 30 | ├── it should update the vault's deposited collateral and collateral total deposited collateral 31 | ├── it should pay out a max of covered collateral + 10% and a min of 0 32 | ├── it should update the vault's borrowed amount, collateral borrowed amount and global debt 33 | ├── it should pay off all of or part of the vaults borrowed amount 34 | ├── it should update the global paid fees and collateral paid fees 35 | └── it should update the vaults and global accrued fees -------------------------------------------------------------------------------- /test/fuzz/vault/mintCurrency/mintCurrency.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.21; 3 | 4 | import {BaseTest, ERC20Token, IVault} from "../../../base.t.sol"; 5 | 6 | contract MintCurrencyTest is BaseTest { 7 | function setUp() public override { 8 | super.setUp(); 9 | 10 | // use user1 as default for all tests 11 | vm.startPrank(user1); 12 | 13 | // deposit amount to be used when testing 14 | vault.depositCollateral(usdc, user1, 1000 * (10 ** usdc.decimals())); 15 | 16 | vm.stopPrank(); 17 | } 18 | 19 | function test_WhenVaultIsPaused(ERC20Token collateral, address user, uint256 amount) external useUser1 { 20 | // pause vault 21 | vm.stopPrank(); 22 | vm.prank(owner); 23 | 24 | // pause vault 25 | vault.pause(); 26 | 27 | // it should revert with custom error Paused() 28 | vm.expectRevert(Paused.selector); 29 | vault.mintCurrency(collateral, user, user, amount); 30 | } 31 | 32 | modifier whenVaultIsNotPaused() { 33 | _; 34 | } 35 | 36 | function test_WhenCollateralDoesNotExist(ERC20Token collateral, address user, uint256 amount) 37 | external 38 | whenVaultIsNotPaused 39 | useUser1 40 | { 41 | if (collateral == usdc) collateral = ERC20Token(mutateAddress(address(usdc))); 42 | 43 | // it should revert with custom error CollateralDoesNotExist() 44 | vm.expectRevert(CollateralDoesNotExist.selector); 45 | 46 | // call with non existing collateral 47 | vault.mintCurrency(collateral, user, user, amount); 48 | } 49 | 50 | modifier whenCollateralExists() { 51 | _; 52 | } 53 | 54 | function test_WhenCallerIsNotOwnerAndNotReliedUponByOwner(address caller, address user, uint256 amount) 55 | external 56 | whenVaultIsNotPaused 57 | whenCollateralExists 58 | { 59 | if (user == caller) user = mutateAddress(user); 60 | 61 | // use unrelied upon user2 62 | vm.prank(caller); 63 | 64 | // it should revert with custom error NotOwnerOrReliedUpon() 65 | vm.expectRevert(NotOwnerOrReliedUpon.selector); 66 | 67 | // call and try to interact with user1 vault with address user1 does not rely on 68 | vault.mintCurrency(usdc, user, user, amount); 69 | } 70 | 71 | modifier whenCallerIsOwnerOrReliedUponByOwner() { 72 | _; 73 | } 74 | 75 | function test_WhenTheBorrowMakesTheVaultsCollateralRatioAboveTheLiquidationThreshold(uint256 amount) 76 | external 77 | whenVaultIsNotPaused 78 | whenCollateralExists 79 | whenCallerIsOwnerOrReliedUponByOwner 80 | useUser1 81 | { 82 | amount = bound(amount, 500_000e18 + 1, type(uint256).max / HUNDRED_PERCENTAGE); 83 | 84 | // it should revert with custom error BadCollateralRatio() 85 | vm.expectRevert(BadCollateralRatio.selector); 86 | 87 | // try minting more than allowed 88 | vault.mintCurrency(usdc, user1, user1, amount); 89 | } 90 | 91 | modifier whenTheBorrowDoesNotMakeTheVaultsCollateralRatioAboveTheLiquidationThreshold() { 92 | _; 93 | } 94 | 95 | function test_WhenOwnersCollateralBalanceIsBelowTheCollateralFloor(uint256 amountToWithdraw, uint256 amountToMint) 96 | external 97 | whenVaultIsNotPaused 98 | whenCollateralExists 99 | whenCallerIsOwnerOrReliedUponByOwner 100 | whenTheBorrowDoesNotMakeTheVaultsCollateralRatioAboveTheLiquidationThreshold 101 | useUser1 102 | { 103 | amountToWithdraw = bound(amountToWithdraw, (900 * (10 ** usdc.decimals())) + 1, 1000 * (10 ** usdc.decimals())); 104 | // no need to bound amount to mint, as it won't get to debt ceiling if it reverts 105 | 106 | // user1 withdraws enough of their collateral to be below the floor 107 | vault.withdrawCollateral(usdc, user1, user1, amountToWithdraw); 108 | 109 | // it should revert with custom error TotalUserCollateralBelowFloor() 110 | vm.expectRevert(TotalUserCollateralBelowFloor.selector); 111 | 112 | // try minting even the lowest of amounts, should revert 113 | vault.mintCurrency(usdc, user1, user1, amountToMint); 114 | } 115 | 116 | modifier whenOwnersCollateralBalanceIsAboveOrEqualToTheCollateralFloor() { 117 | _; 118 | } 119 | 120 | function test_WhenTheMintTakesTheGlobalDebtAboveTheGlobalDebtCeiling(uint256 amount) 121 | external 122 | whenVaultIsNotPaused 123 | whenCollateralExists 124 | whenCallerIsOwnerOrReliedUponByOwner 125 | whenTheBorrowDoesNotMakeTheVaultsCollateralRatioAboveTheLiquidationThreshold 126 | whenOwnersCollateralBalanceIsAboveOrEqualToTheCollateralFloor 127 | { 128 | vm.prank(owner); 129 | vault.updateDebtCeiling(100e18); 130 | 131 | vm.startPrank(user1); 132 | amount = bound(amount, 100e18 + 1, type(uint256).max); 133 | 134 | // it should revert with custom error GlobalDebtCeilingExceeded() 135 | vm.expectRevert(GlobalDebtCeilingExceeded.selector); 136 | // try minting even the lowest of amounts, should revert 137 | vault.mintCurrency(usdc, user1, user1, amount); 138 | } 139 | 140 | modifier whenTheMintDoesNotTakeTheGlobalDebtAboveTheGlobalDebtCeiling() { 141 | _; 142 | } 143 | 144 | function test_WhenTheMintTakesTheGlobalDebtAboveTheGlobalDbetCeiling(uint256 amount) 145 | external 146 | whenVaultIsNotPaused 147 | whenCollateralExists 148 | whenCallerIsOwnerOrReliedUponByOwner 149 | whenTheBorrowDoesNotMakeTheVaultsCollateralRatioAboveTheLiquidationThreshold 150 | whenOwnersCollateralBalanceIsAboveOrEqualToTheCollateralFloor 151 | whenTheMintDoesNotTakeTheGlobalDebtAboveTheGlobalDebtCeiling 152 | { 153 | vm.prank(owner); 154 | vault.updateCollateralData(usdc, IVault.ModifiableParameters.DEBT_CEILING, 100e18); 155 | 156 | vm.startPrank(user1); 157 | amount = bound(amount, 100e18 + 1, type(uint256).max); 158 | 159 | // it should revert with custom error CollateralDebtCeilingExceeded() 160 | vm.expectRevert(CollateralDebtCeilingExceeded.selector); 161 | // try minting even the lowest of amounts, should revert 162 | vault.mintCurrency(usdc, user1, user1, amount); 163 | } 164 | 165 | function test_WhenTheOwnersBorrowedAmountIs0(uint256 amount, uint256 timeElapsed) 166 | external 167 | whenVaultIsNotPaused 168 | whenCollateralExists 169 | whenCallerIsOwnerOrReliedUponByOwner 170 | whenTheBorrowDoesNotMakeTheVaultsCollateralRatioAboveTheLiquidationThreshold 171 | whenOwnersCollateralBalanceIsAboveOrEqualToTheCollateralFloor 172 | useUser1 173 | { 174 | // it should update the owners accrued fees 175 | // it should emit CurrencyMinted() event with with expected indexed and unindexed parameters 176 | // it should update user's borrowed amount, collateral's borrowed amount and global debt 177 | // it should mint right amount of currency to the to address 178 | WhenOwnersBorrowedAmountIsAbove0OrIs0(user1, true, amount, timeElapsed); 179 | } 180 | 181 | function test_WhenTheOwnersBorrowedAmountIs0_useReliedOnForUser1(uint256 amount, uint256 timeElapsed) 182 | external 183 | whenVaultIsNotPaused 184 | whenCollateralExists 185 | whenCallerIsOwnerOrReliedUponByOwner 186 | whenTheBorrowDoesNotMakeTheVaultsCollateralRatioAboveTheLiquidationThreshold 187 | whenOwnersCollateralBalanceIsAboveOrEqualToTheCollateralFloor 188 | useReliedOnForUser1(user2) 189 | { 190 | // it should update the owners accrued fees 191 | // it should emit CurrencyMinted() event with with expected indexed and unindexed parameters 192 | // it should update user's borrowed amount, collateral's borrowed amount and global debt 193 | // it should mint right amount of currency to the to address 194 | WhenOwnersBorrowedAmountIsAbove0OrIs0(user1, true, amount, timeElapsed); 195 | } 196 | 197 | function test_WhenOwnersBorrowedAmountIsAbove0_useUser1(uint256 amount, uint256 timeElapsed) 198 | external 199 | whenVaultIsNotPaused 200 | whenCollateralExists 201 | whenCallerIsOwnerOrReliedUponByOwner 202 | whenTheBorrowDoesNotMakeTheVaultsCollateralRatioAboveTheLiquidationThreshold 203 | whenOwnersCollateralBalanceIsAboveOrEqualToTheCollateralFloor 204 | useUser1 205 | { 206 | // borrow first to make total borrowed amount > 0 207 | vault.mintCurrency(usdc, user1, user2, 1); 208 | 209 | // it should update the owners accrued fees 210 | // it should emit CurrencyMinted() event with with expected indexed and unindexed parameters 211 | // it should update user's borrowed amount, collateral's borrowed amount and global debt 212 | // it should mint right amount of currency to the to address 213 | WhenOwnersBorrowedAmountIsAbove0OrIs0(user2, false, amount, timeElapsed); 214 | } 215 | 216 | function test_WhenOwnersBorrowedAmountIsAbove0_useReliedOnForUser1(uint256 amount, uint256 timeElapsed) 217 | external 218 | whenVaultIsNotPaused 219 | whenCollateralExists 220 | whenCallerIsOwnerOrReliedUponByOwner 221 | whenTheBorrowDoesNotMakeTheVaultsCollateralRatioAboveTheLiquidationThreshold 222 | whenOwnersCollateralBalanceIsAboveOrEqualToTheCollateralFloor 223 | useReliedOnForUser1(user2) 224 | { 225 | // borrow first to make total borrowed amount > 0 226 | vault.mintCurrency(usdc, user1, user3, 100_000e18); 227 | 228 | // it should update the owners accrued fees 229 | // it should emit CurrencyMinted() event with with expected indexed and unindexed parameters 230 | // it should update user's borrowed amount, collateral's borrowed amount and global debt 231 | // it should mint right amount of currency to the to address 232 | WhenOwnersBorrowedAmountIsAbove0OrIs0(user3, false, amount, timeElapsed); 233 | } 234 | 235 | function WhenOwnersBorrowedAmountIsAbove0OrIs0( 236 | address recipient, 237 | bool isCurrentBorrowedAmount0, 238 | uint256 amount, 239 | uint256 timeElapsed 240 | ) private { 241 | amount = bound(amount, 0, 250_000e18); 242 | timeElapsed = bound(timeElapsed, 0, TEN_YEARS); 243 | 244 | // cache pre balances 245 | uint256 userOldBalance = xNGN.balanceOf(recipient); 246 | uint256 oldTotalSupply = xNGN.totalSupply(); 247 | 248 | // cache pre storage vars and old accrued fees 249 | IVault.VaultInfo memory initialUserVaultInfo = getVaultMapping(usdc, user1); 250 | IVault.CollateralInfo memory initialCollateralInfo = getCollateralMapping(usdc); 251 | 252 | // skip time to be able to check accrued interest; 253 | skip(timeElapsed); 254 | 255 | // it should emit CurrencyMinted() event with with expected indexed and unindexed parameters 256 | vm.expectEmit(true, false, false, true, address(vault)); 257 | emit CurrencyMinted(user1, amount); 258 | 259 | // mint currency 260 | vault.mintCurrency(usdc, user1, recipient, amount); 261 | 262 | // it should mint right amount of currency to the to address 263 | assertEq(xNGN.totalSupply() - oldTotalSupply, amount); 264 | assertEq(xNGN.balanceOf(recipient) - userOldBalance, amount); 265 | 266 | // it should update user's borrowed amount, collateral's borrowed amount and global debt 267 | IVault.VaultInfo memory afterUserVaultInfo = getVaultMapping(usdc, user1); 268 | IVault.CollateralInfo memory afterCollateralInfo = getCollateralMapping(usdc); 269 | 270 | if (isCurrentBorrowedAmount0) { 271 | assertEq( 272 | afterUserVaultInfo.lastTotalAccumulatedRate - initialUserVaultInfo.lastTotalAccumulatedRate, 273 | timeElapsed * (oneAndHalfPercentPerSecondInterestRate + onePercentPerSecondInterestRate) 274 | ); 275 | } else { 276 | // get expected accrued fees 277 | uint256 accruedFees = ( 278 | (calculateCurrentTotalAccumulatedRate(usdc) - initialUserVaultInfo.lastTotalAccumulatedRate) 279 | * initialUserVaultInfo.borrowedAmount 280 | ) / HUNDRED_PERCENTAGE; 281 | 282 | // it should update accrued fees for the user's position 283 | assertEq(initialUserVaultInfo.accruedFees + accruedFees, afterUserVaultInfo.accruedFees); 284 | } 285 | 286 | assertEq(afterCollateralInfo.totalBorrowedAmount, amount + initialCollateralInfo.totalBorrowedAmount); 287 | assertEq(afterUserVaultInfo.borrowedAmount, amount + initialUserVaultInfo.borrowedAmount); 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /test/fuzz/vault/mintCurrency/mintCurrency.tree: -------------------------------------------------------------------------------- 1 | mintCurrencyTest.t.sol 2 | ├── when vault is paused 3 | │ └── it should revert with custom error Paused() 4 | └── when vault is not paused 5 | ├── when collateral does not exist 6 | │ └── it should revert with custom error CollateralDoesNotExist() 7 | └── when collateral exists 8 | ├── when caller is not owner and not relied upon by owner 9 | │ └── it should revert with custom error NotOwnerOrReliedUpon() 10 | └── when caller is owner or relied upon by owner 11 | ├── when the borrow makes the vault's collateral ratio above the liquidation threshold 12 | │ └── it should revert with custom error BadCollateralRatio() 13 | └── when the borrow does not make the vault's collateral ratio above the liquidation threshold 14 | ├── when owners collateral balance is below the collateral floor 15 | │ └── it should revert with custom error TotalUserCollateralBelowFloor() 16 | └── when owners collateral balance is above or equal to the collateral floor 17 | ├── when the minting takes the global debt above the global debt ceiling 18 | │ └── it should revert with custom error GlobalDebtCeilingExceeded() 19 | └── when the minting does not take the global debt above the global debt ceiling 20 | ├── when the minting takes the collateral backed debt above the collateral debt ceiling 21 | │ └── it should revert with custom error CollateralDebtCeilingExceeded() 22 | └── when the minting takes the collateral backed debt above the collateral debt ceiling 23 | ├── when the owners borrowed amount is 0 24 | │ └── it should update the owners lastTotalAccumulatedRate 25 | │ ├── it should emit CurrencyMinted() event with with expected indexed and unindexed parameters 26 | │ ├── it should update user's borrowed amount, collateral's borrowed amount and global debt 27 | │ └── it should mint right amount of currency to the to address 28 | └── when owners borrowed amount is above 0 29 | ├── it should update the owners accrued fees 30 | ├── it should emit CurrencyMinted() event with with expected indexed and unindexed parameters 31 | ├── it should update user's borrowed amount, collateral's borrowed amount and global debt 32 | └── it should mint right amount of currency to the to address -------------------------------------------------------------------------------- /test/fuzz/vault/otherActions/otherActions.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.21; 3 | 4 | import {BaseTest, IVault, ERC20Token, IOSM} from "../../../base.t.sol"; 5 | 6 | contract OtherActionsTest is BaseTest { 7 | function test_rely(address caller, address reliedUpon, bool alreadyReliedUpon) external { 8 | vm.startPrank(caller); 9 | if (alreadyReliedUpon) vault.rely(reliedUpon); 10 | 11 | // reverts if paused 12 | vm.stopPrank(); 13 | vm.prank(owner); 14 | vault.pause(); 15 | vm.prank(caller); 16 | vm.expectRevert(Paused.selector); 17 | vault.deny(reliedUpon); 18 | // unpause 19 | vm.prank(owner); 20 | vault.unpause(); 21 | vm.prank(caller); 22 | 23 | // should not revert if not paused 24 | vault.rely(reliedUpon); 25 | assertTrue(vault.relyMapping(caller, reliedUpon)); 26 | } 27 | 28 | function test_deny(address caller, address reliedUpon, bool alreadyReliedUpon) external { 29 | vm.startPrank(caller); 30 | if (alreadyReliedUpon) vault.rely(reliedUpon); 31 | 32 | // reverts if paused 33 | vm.stopPrank(); 34 | vm.prank(owner); 35 | vault.pause(); 36 | vm.prank(caller); 37 | vm.expectRevert(Paused.selector); 38 | vault.deny(reliedUpon); 39 | // unpause 40 | vm.prank(owner); 41 | vault.unpause(); 42 | vm.prank(caller); 43 | 44 | // should not revert if not paused 45 | vault.deny(reliedUpon); 46 | assertFalse(vault.relyMapping(caller, reliedUpon)); 47 | } 48 | 49 | function accrue_payfees_getcurrency(uint256 timeElapsed) private { 50 | // accrue and pay some fees 51 | vm.startPrank(user1); 52 | vault.depositCollateral(usdc, user1, 1000 * (10 ** usdc.decimals())); 53 | vault.mintCurrency(usdc, user1, user1, 100_000e18); 54 | skip(timeElapsed); 55 | 56 | // get currency to pay fees from user2 borrowing 57 | vm.stopPrank(); 58 | vm.startPrank(user2); 59 | vault.depositCollateral(usdc, user2, 1000 * (10 ** usdc.decimals())); 60 | vault.mintCurrency(usdc, user2, user1, 100_000e18); // mint currency to user 1 61 | vm.stopPrank(); 62 | 63 | // time passes 64 | skip(timeElapsed); 65 | // pay back with fees if any 66 | vm.startPrank(user1); 67 | vault.burnCurrency(usdc, user1, 100_000e18 + calculateUserCurrentAccruedFees(usdc, user1)); 68 | vm.stopPrank(); 69 | } 70 | 71 | function test_withdrawFees(uint256 timeElapsed) external { 72 | timeElapsed = bound(timeElapsed, 0, 365 days * 10); 73 | accrue_payfees_getcurrency(timeElapsed); 74 | 75 | // should revert when paused 76 | vm.prank(owner); 77 | vault.pause(); 78 | vm.expectRevert(Paused.selector); 79 | vault.withdrawFees(); 80 | // unpause back 81 | vm.prank(owner); 82 | vault.unpause(); 83 | 84 | // should revert if stability module is unset 85 | vm.prank(owner); 86 | vault.updateStabilityModule(address(0)); 87 | vm.expectRevert(InvalidStabilityModule.selector); 88 | vault.withdrawFees(); 89 | 90 | // set back 91 | vm.prank(owner); 92 | vault.updateStabilityModule(testStabilityModule); // no implementation so set it to psuedo-random address 93 | 94 | // should work otherwise 95 | uint256 initialVaultPaidFees = vault.paidFees(); 96 | uint256 initialVaultBalance = xNGN.balanceOf(address(vault)); 97 | uint256 initialStabilityModuleBalance = xNGN.balanceOf(testStabilityModule); 98 | vault.withdrawFees(); 99 | assertEq(vault.paidFees(), 0); 100 | assertEq(xNGN.balanceOf(address(vault)), initialVaultBalance - initialVaultPaidFees); 101 | assertEq(xNGN.balanceOf(testStabilityModule), initialStabilityModuleBalance + initialVaultPaidFees); 102 | } 103 | 104 | function test_recoverToken(uint256 timeElapsed) external { 105 | timeElapsed = bound(timeElapsed, 0, 365 days * 10); 106 | accrue_payfees_getcurrency(timeElapsed); 107 | 108 | // should revert when paused 109 | vm.prank(owner); 110 | vault.pause(); 111 | vm.expectRevert(Paused.selector); 112 | vault.recoverToken(address(usdc), address(this)); 113 | // unpause back 114 | vm.prank(owner); 115 | vault.unpause(); 116 | 117 | // donate to vault 118 | vm.startPrank(user1); 119 | usdc.transfer(address(vault), 10_000 * (10 ** usdc.decimals())); 120 | xNGN.transfer(address(vault), 10_000e18); 121 | vm.deal(address(vault), 1 ether); 122 | vm.stopPrank(); 123 | 124 | // if currency token, it should transfer donations but never affect the paidFees 125 | uint256 initialPaidFees = vault.paidFees(); 126 | uint256 initialTotalDepositedCollateral = getCollateralMapping(usdc).totalDepositedCollateral; 127 | uint256 initialVaultXNGNBalance = xNGN.balanceOf(address(vault)); 128 | uint256 initialVaultUsdcBalance = usdc.balanceOf(address(vault)); 129 | uint256 initialVaultEtherBalance = address(vault).balance; 130 | uint256 initialThisXNGNBalance = xNGN.balanceOf(address(this)); 131 | uint256 initialThisUsdcBalance = usdc.balanceOf(address(this)); 132 | address iAcceptEther = address(new IAcceptEther()); 133 | uint256 initialIAcceptEtherEtherBalance = address(iAcceptEther).balance; 134 | 135 | // should never revert if it recovers xNGN 136 | vault.recoverToken(address(xNGN), address(this)); 137 | assertEq(initialPaidFees, vault.paidFees()); 138 | assertEq(initialPaidFees, xNGN.balanceOf(address(vault))); 139 | assertEq(xNGN.balanceOf(address(this)), initialThisXNGNBalance + (initialVaultXNGNBalance - initialPaidFees)); 140 | 141 | // should never revert if it recovers an ERC20Token token 142 | vault.recoverToken(address(usdc), address(this)); 143 | assertEq(initialPaidFees, vault.paidFees()); 144 | assertEq(initialPaidFees, xNGN.balanceOf(address(vault))); 145 | assertEq(xNGN.balanceOf(address(this)), initialThisXNGNBalance + (initialVaultXNGNBalance - initialPaidFees)); 146 | assertEq(initialTotalDepositedCollateral, getCollateralMapping(usdc).totalDepositedCollateral); 147 | assertEq(initialTotalDepositedCollateral, usdc.balanceOf(address(vault))); 148 | assertEq( 149 | usdc.balanceOf(address(this)), 150 | initialThisUsdcBalance + (initialVaultUsdcBalance - initialTotalDepositedCollateral) 151 | ); 152 | 153 | // if _to address does not accept ether should revert with `EthTransferFailed` 154 | vm.expectRevert(EthTransferFailed.selector); 155 | vault.recoverToken(address(0), address(this)); 156 | 157 | // should pass otherwise 158 | vault.recoverToken(address(0), iAcceptEther); 159 | assertEq(initialPaidFees, vault.paidFees()); 160 | assertEq(initialPaidFees, xNGN.balanceOf(address(vault))); 161 | assertEq(xNGN.balanceOf(address(this)), initialThisXNGNBalance + (initialVaultXNGNBalance - initialPaidFees)); 162 | assertEq(initialTotalDepositedCollateral, getCollateralMapping(usdc).totalDepositedCollateral); 163 | assertEq(initialTotalDepositedCollateral, usdc.balanceOf(address(vault))); 164 | assertEq( 165 | usdc.balanceOf(address(this)), 166 | initialThisUsdcBalance + (initialVaultUsdcBalance - initialTotalDepositedCollateral) 167 | ); 168 | assertEq(initialPaidFees, vault.paidFees()); 169 | assertEq(initialPaidFees, xNGN.balanceOf(address(vault))); 170 | assertEq(address(vault).balance, 0); 171 | assertEq(address(iAcceptEther).balance, initialIAcceptEtherEtherBalance + initialVaultEtherBalance); 172 | } 173 | } 174 | 175 | contract IAcceptEther { 176 | receive() external payable {} 177 | } 178 | -------------------------------------------------------------------------------- /test/fuzz/vault/otherActions/roleBasedActions.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.21; 3 | 4 | import {BaseTest, IVault, ERC20Token, IOSM, IRate} from "../../../base.t.sol"; 5 | 6 | contract RoleBasedActionsTest is BaseTest { 7 | function test_pause() external { 8 | // only default admin can call it successfully 9 | vm.expectRevert(Unauthorized.selector); 10 | vault.pause(); 11 | 12 | // pause works if called by owner 13 | vm.startPrank(owner); 14 | assertEq(vault.status(), TRUE); 15 | vault.pause(); 16 | assertEq(vault.status(), FALSE); 17 | } 18 | 19 | function test_unpause() external { 20 | // only default admin can call it successfully 21 | vm.expectRevert(Unauthorized.selector); 22 | vault.unpause(); 23 | 24 | // pause it with owner 25 | vm.startPrank(owner); 26 | vault.pause(); 27 | 28 | // owner can unpause it 29 | assertEq(vault.status(), FALSE); 30 | vault.unpause(); 31 | assertEq(vault.status(), TRUE); 32 | } 33 | 34 | function test_updateFeedModule(address newFeedModule) external { 35 | // only default admin can call it successfully 36 | vm.expectRevert(Unauthorized.selector); 37 | vault.updateFeedModule(newFeedModule); 38 | 39 | // owner can change it 40 | vm.startPrank(owner); 41 | if (vault.feedModule() == newFeedModule) newFeedModule = mutateAddress(newFeedModule); 42 | vault.updateFeedModule(newFeedModule); 43 | assertEq(vault.feedModule(), newFeedModule); 44 | } 45 | 46 | function test_updateRateModule(IRate newRateModule) external { 47 | // only default admin can call it successfully 48 | vm.expectRevert(Unauthorized.selector); 49 | vault.updateRateModule(newRateModule); 50 | 51 | // owner can change it 52 | vm.startPrank(owner); 53 | if (vault.rateModule() == newRateModule) newRateModule = IRate(mutateAddress(address(newRateModule))); 54 | vault.updateRateModule(newRateModule); 55 | assertEq(address(vault.rateModule()), address(newRateModule)); 56 | } 57 | 58 | function test_updateStabilityModule(address newStabilityModule) external { 59 | // only default admin can call it successfully 60 | vm.expectRevert(Unauthorized.selector); 61 | vault.updateStabilityModule(newStabilityModule); 62 | 63 | // owner can change it 64 | vm.startPrank(owner); 65 | if (vault.stabilityModule() == newStabilityModule) newStabilityModule = mutateAddress(newStabilityModule); 66 | vault.updateStabilityModule(newStabilityModule); 67 | assertEq(vault.stabilityModule(), newStabilityModule); 68 | } 69 | 70 | function test_updateGlobalDebtCeiling(uint256 newDebtCeiling) external { 71 | // only default admin can call it successfully 72 | vm.expectRevert(Unauthorized.selector); 73 | vault.updateDebtCeiling(newDebtCeiling); 74 | 75 | // owner can change it 76 | vm.startPrank(owner); 77 | unchecked { 78 | // unchecked in the case the fuzz parameter for newDebtCeiling is uint256.max 79 | if (vault.debtCeiling() == newDebtCeiling) newDebtCeiling = newDebtCeiling + 1; 80 | } 81 | vault.updateDebtCeiling(newDebtCeiling); 82 | assertEq(vault.debtCeiling(), newDebtCeiling); 83 | } 84 | 85 | function test_createCollateralType( 86 | uint256 rate, 87 | uint256 liquidationThreshold, 88 | uint256 liquidationBonus, 89 | uint256 debtCeiling, 90 | uint256 collateralFloorPerPosition 91 | ) external { 92 | ERC20Token collateralToken = ERC20Token(address(new ERC20Token("Tether USD", "USDT", 6))); 93 | 94 | // only default admin can call it successfully 95 | vm.expectRevert(Unauthorized.selector); 96 | vault.createCollateralType( 97 | collateralToken, rate, liquidationThreshold, liquidationBonus, debtCeiling, collateralFloorPerPosition 98 | ); 99 | 100 | // only callable when unpaused 101 | vm.startPrank(owner); 102 | vault.pause(); 103 | vm.expectRevert(Paused.selector); 104 | vault.createCollateralType( 105 | collateralToken, rate, liquidationThreshold, liquidationBonus, debtCeiling, collateralFloorPerPosition 106 | ); 107 | // unpause back 108 | vault.unpause(); 109 | 110 | // only callable when collateral rate is not 0 (i.e is considered existing) 111 | vm.expectRevert(CollateralAlreadyExists.selector); 112 | vault.createCollateralType( 113 | usdc, rate, liquidationThreshold, liquidationBonus, debtCeiling, collateralFloorPerPosition 114 | ); 115 | 116 | { 117 | ERC20Token tokenToTry = ERC20Token(address(1111)); 118 | // only works with address with code deployed to it and calling `decimals()` return a valid value that can be decode into a uint256 119 | 120 | // does not work for eoa 121 | assertEq(address(tokenToTry).code.length, 0); 122 | vm.expectRevert(new bytes(0)); 123 | vault.createCollateralType( 124 | tokenToTry, rate, liquidationThreshold, liquidationBonus, debtCeiling, collateralFloorPerPosition 125 | ); 126 | 127 | // does not work for contract with no function sig for decimals 128 | tokenToTry = ERC20Token(address(new BadCollateralNoFuncSigForDecimals())); 129 | vm.expectRevert(new bytes(0)); 130 | vault.createCollateralType( 131 | tokenToTry, rate, liquidationThreshold, liquidationBonus, debtCeiling, collateralFloorPerPosition 132 | ); 133 | 134 | // does not work for contract that returns nothing 135 | tokenToTry = ERC20Token(address(new BadCollateralReturnsNothing())); 136 | vm.expectRevert(new bytes(0)); 137 | vault.createCollateralType( 138 | tokenToTry, rate, liquidationThreshold, liquidationBonus, debtCeiling, collateralFloorPerPosition 139 | ); 140 | 141 | // does not work for contract that returns less data than expected 142 | tokenToTry = ERC20Token(address(new BadCollateralReturnsLittleData())); 143 | vm.expectRevert(new bytes(0)); 144 | vault.createCollateralType( 145 | tokenToTry, rate, liquidationThreshold, liquidationBonus, debtCeiling, collateralFloorPerPosition 146 | ); 147 | } 148 | 149 | // should work 150 | vault.createCollateralType( 151 | collateralToken, rate, liquidationThreshold, liquidationBonus, debtCeiling, collateralFloorPerPosition 152 | ); 153 | IVault.CollateralInfo memory collateralInfo = getCollateralMapping(collateralToken); 154 | assertEq(collateralInfo.totalDepositedCollateral, 0); 155 | assertEq(collateralInfo.totalBorrowedAmount, 0); 156 | assertEq(collateralInfo.liquidationThreshold, liquidationThreshold); 157 | assertEq(collateralInfo.liquidationBonus, liquidationBonus); 158 | assertEq(collateralInfo.rateInfo.rate, rate); 159 | assertEq(collateralInfo.rateInfo.lastUpdateTime, block.timestamp); 160 | assertEq(collateralInfo.rateInfo.accumulatedRate, 0); 161 | assertEq(collateralInfo.price, 0); 162 | assertEq(collateralInfo.debtCeiling, debtCeiling); 163 | assertEq(collateralInfo.collateralFloorPerPosition, collateralFloorPerPosition); 164 | assertEq(collateralInfo.additionalCollateralPrecision, MAX_TOKEN_DECIMALS - collateralToken.decimals()); 165 | } 166 | 167 | function test_updateCollateralData(ERC20Token nonExistentCollateral, uint8 param, uint256 data, uint256 timeElapsed) 168 | external 169 | { 170 | IVault.ModifiableParameters validParam = IVault.ModifiableParameters(uint8(bound(param, 0, 4))); 171 | timeElapsed = bound(timeElapsed, 0, 365 days * 10); 172 | skip(timeElapsed); 173 | 174 | // only default admin can call it successfully 175 | vm.expectRevert(Unauthorized.selector); 176 | vault.updateCollateralData(usdc, validParam, data); 177 | 178 | // only callable when unpaused 179 | vm.startPrank(owner); 180 | vault.pause(); 181 | vm.expectRevert(Paused.selector); 182 | vault.updateCollateralData(usdc, validParam, data); 183 | // unpause back 184 | vault.unpause(); 185 | 186 | // if collateral does not exist, revert 187 | if (nonExistentCollateral == usdc) { 188 | nonExistentCollateral = ERC20Token(mutateAddress(address(nonExistentCollateral))); 189 | } 190 | vm.expectRevert(CollateralDoesNotExist.selector); 191 | vault.updateCollateralData(nonExistentCollateral, validParam, data); 192 | 193 | // if enum index is outside max enum, should revert 194 | uint256 invalidParam = bound(param, 5, type(uint256).max); 195 | (bool success, bytes memory returnData) = address(vault).call( 196 | abi.encodePacked(vault.updateCollateralData.selector, abi.encode(usdc, invalidParam, data)) 197 | ); 198 | assertFalse(success); 199 | assertEq(keccak256(returnData), keccak256(new bytes(0))); 200 | 201 | // should not revert otherwise 202 | IVault.CollateralInfo memory initialCollateralInfo = getCollateralMapping(usdc); 203 | // call it 204 | vault.updateCollateralData(usdc, validParam, data); 205 | // checks, ensure everything is as expected 206 | IVault.CollateralInfo memory afterCollateralInfo = getCollateralMapping(usdc); 207 | 208 | // checks that apply regardless of enum variant 209 | assertEq(afterCollateralInfo.totalDepositedCollateral, initialCollateralInfo.totalDepositedCollateral); 210 | assertEq(afterCollateralInfo.totalBorrowedAmount, initialCollateralInfo.totalBorrowedAmount); 211 | assertEq(afterCollateralInfo.price, initialCollateralInfo.price); 212 | assertEq(afterCollateralInfo.additionalCollateralPrecision, initialCollateralInfo.additionalCollateralPrecision); 213 | if (validParam == IVault.ModifiableParameters.RATE) { 214 | assertEq(afterCollateralInfo.liquidationThreshold, initialCollateralInfo.liquidationThreshold); 215 | assertEq(afterCollateralInfo.liquidationBonus, initialCollateralInfo.liquidationBonus); 216 | assertEq(afterCollateralInfo.rateInfo.rate, data); 217 | assertEq(afterCollateralInfo.rateInfo.lastUpdateTime, block.timestamp); 218 | assertEq( 219 | afterCollateralInfo.rateInfo.accumulatedRate, 220 | (block.timestamp - initialCollateralInfo.rateInfo.lastUpdateTime) * initialCollateralInfo.rateInfo.rate 221 | ); 222 | assertEq(afterCollateralInfo.debtCeiling, initialCollateralInfo.debtCeiling); 223 | assertEq(afterCollateralInfo.collateralFloorPerPosition, initialCollateralInfo.collateralFloorPerPosition); 224 | } else if (validParam == IVault.ModifiableParameters.DEBT_CEILING) { 225 | assertEq(afterCollateralInfo.liquidationThreshold, initialCollateralInfo.liquidationThreshold); 226 | assertEq(afterCollateralInfo.liquidationBonus, initialCollateralInfo.liquidationBonus); 227 | assertEq(afterCollateralInfo.rateInfo.rate, initialCollateralInfo.rateInfo.rate); 228 | assertEq(afterCollateralInfo.rateInfo.lastUpdateTime, initialCollateralInfo.rateInfo.lastUpdateTime); 229 | assertEq(afterCollateralInfo.rateInfo.accumulatedRate, initialCollateralInfo.rateInfo.accumulatedRate); 230 | assertEq(afterCollateralInfo.debtCeiling, data); 231 | assertEq(afterCollateralInfo.collateralFloorPerPosition, initialCollateralInfo.collateralFloorPerPosition); 232 | } else if (validParam == IVault.ModifiableParameters.COLLATERAL_FLOOR_PER_POSITION) { 233 | assertEq(afterCollateralInfo.liquidationThreshold, initialCollateralInfo.liquidationThreshold); 234 | assertEq(afterCollateralInfo.liquidationBonus, initialCollateralInfo.liquidationBonus); 235 | assertEq(afterCollateralInfo.rateInfo.rate, initialCollateralInfo.rateInfo.rate); 236 | assertEq(afterCollateralInfo.rateInfo.lastUpdateTime, initialCollateralInfo.rateInfo.lastUpdateTime); 237 | assertEq(afterCollateralInfo.rateInfo.accumulatedRate, initialCollateralInfo.rateInfo.accumulatedRate); 238 | assertEq(afterCollateralInfo.debtCeiling, initialCollateralInfo.debtCeiling); 239 | assertEq(afterCollateralInfo.collateralFloorPerPosition, data); 240 | } else if (validParam == IVault.ModifiableParameters.LIQUIDATION_BONUS) { 241 | assertEq(afterCollateralInfo.liquidationThreshold, initialCollateralInfo.liquidationThreshold); 242 | assertEq(afterCollateralInfo.liquidationBonus, data); 243 | assertEq(afterCollateralInfo.rateInfo.rate, initialCollateralInfo.rateInfo.rate); 244 | assertEq(afterCollateralInfo.rateInfo.lastUpdateTime, initialCollateralInfo.rateInfo.lastUpdateTime); 245 | assertEq(afterCollateralInfo.rateInfo.accumulatedRate, initialCollateralInfo.rateInfo.accumulatedRate); 246 | assertEq(afterCollateralInfo.debtCeiling, initialCollateralInfo.debtCeiling); 247 | assertEq(afterCollateralInfo.collateralFloorPerPosition, initialCollateralInfo.collateralFloorPerPosition); 248 | } else if (validParam == IVault.ModifiableParameters.LIQUIDATION_THRESHOLD) { 249 | assertEq(afterCollateralInfo.liquidationThreshold, data); 250 | assertEq(afterCollateralInfo.liquidationBonus, initialCollateralInfo.liquidationBonus); 251 | assertEq(afterCollateralInfo.rateInfo.rate, initialCollateralInfo.rateInfo.rate); 252 | assertEq(afterCollateralInfo.rateInfo.lastUpdateTime, initialCollateralInfo.rateInfo.lastUpdateTime); 253 | assertEq(afterCollateralInfo.rateInfo.accumulatedRate, initialCollateralInfo.rateInfo.accumulatedRate); 254 | assertEq(afterCollateralInfo.debtCeiling, initialCollateralInfo.debtCeiling); 255 | assertEq(afterCollateralInfo.collateralFloorPerPosition, initialCollateralInfo.collateralFloorPerPosition); 256 | } 257 | } 258 | 259 | function test_updatePrice(ERC20Token unsupportedCollateral, uint256 price) external { 260 | if (unsupportedCollateral == usdc) { 261 | unsupportedCollateral = ERC20Token(mutateAddress(address(unsupportedCollateral))); 262 | } 263 | 264 | // deploy a mock oracle security module and set it to be the OSM of feed contract 265 | IOSM mockOsm = new MockOSM(); 266 | vm.prank(owner); 267 | feed.setCollateralOSM(usdc, mockOsm); 268 | 269 | // only default feed contract can call it successfully 270 | vm.expectRevert(NotFeedContract.selector); 271 | vault.updatePrice(usdc, price); 272 | 273 | // even if feed calls it and it's an unsupported collateral, reverty 274 | vm.startPrank(address(feed)); 275 | vm.expectRevert(CollateralDoesNotExist.selector); 276 | vault.updatePrice(unsupportedCollateral, price); 277 | 278 | // feed contract can 279 | vault.updatePrice(usdc, price); 280 | assertEq(getCollateralMapping(usdc).price, price); 281 | } 282 | 283 | function test_updateBaseRate(uint256 newBaseRate, uint256 timeElapsed) external { 284 | timeElapsed = bound(timeElapsed, 0, 365 days * 10); 285 | 286 | // only default admin can call it successfully 287 | vm.expectRevert(Unauthorized.selector); 288 | vault.updateBaseRate(newBaseRate); 289 | 290 | // owner can change it 291 | vm.startPrank(owner); 292 | (uint256 oldRate,, uint256 oldLastUpdateTime) = vault.baseRateInfo(); 293 | if (oldRate == newBaseRate) newBaseRate = newBaseRate + 1; 294 | vault.updateBaseRate(newBaseRate); 295 | 296 | (uint256 newRate, uint256 newAccumulatedRate, uint256 newLastUpdateTime) = vault.baseRateInfo(); 297 | assertEq(newRate, newBaseRate); 298 | assertEq(newAccumulatedRate, (block.timestamp - oldLastUpdateTime) * oldRate); 299 | assertEq(newLastUpdateTime, block.timestamp); 300 | } 301 | } 302 | 303 | contract BadCollateralNoFuncSigForDecimals {} 304 | 305 | contract BadCollateralReturnsNothing { 306 | function decimals() external view {} 307 | } 308 | 309 | contract BadCollateralReturnsLittleData { 310 | function decimals() external pure { 311 | assembly { 312 | mstore(0x00, hex"1111") 313 | return(0x00, 0x10) 314 | } 315 | } 316 | } 317 | 318 | contract MockOSM is IOSM { 319 | uint256 public current = 10_000e6; 320 | } 321 | -------------------------------------------------------------------------------- /test/fuzz/vault/withdrawCollateral/withdrawCollateral.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.21; 3 | 4 | import {BaseTest, ERC20Token, IVault} from "../../../base.t.sol"; 5 | 6 | contract WithdrawCollateralTest is BaseTest { 7 | function setUp() public override { 8 | super.setUp(); 9 | 10 | // use user1 as default for all tests 11 | vm.startPrank(user1); 12 | 13 | // deposit amount to be used when testing 14 | vault.depositCollateral(usdc, user1, 1000 * (10 ** usdc.decimals())); 15 | 16 | vm.stopPrank(); 17 | } 18 | 19 | function test_WhenCollateralDoesNotExist(ERC20Token collateral, address user, uint256 amount) external useUser1 { 20 | if (collateral == usdc) collateral = ERC20Token(mutateAddress(address(usdc))); 21 | 22 | // it should revert with custom error CollateralDoesNotExist() 23 | vm.expectRevert(CollateralDoesNotExist.selector); 24 | 25 | // call with non existing collateral 26 | vault.withdrawCollateral(collateral, user, user, amount); 27 | } 28 | 29 | modifier whenCollateralExists() { 30 | _; 31 | } 32 | 33 | function test_WhenCallerIsNotOwnerAndNotReliedUponByOwner(address caller, address user, uint256 amount) 34 | external 35 | whenCollateralExists 36 | { 37 | if (user == caller) user = mutateAddress(user); 38 | 39 | // use unrelied upon user2 40 | vm.prank(caller); 41 | 42 | // it should revert with custom error NotOwnerOrReliedUpon() 43 | vm.expectRevert(NotOwnerOrReliedUpon.selector); 44 | 45 | // call and try to interact with user1 vault with address user1 does not rely on 46 | vault.withdrawCollateral(usdc, user, user, amount); 47 | } 48 | 49 | modifier whenCallerIsOwnerOrReliedUponByOwner() { 50 | _; 51 | } 52 | 53 | function test_WhenTheAmountIsGreaterThanTheBorrowersDepositedCollateral(uint256 amount) 54 | external 55 | whenCollateralExists 56 | whenCallerIsOwnerOrReliedUponByOwner 57 | useUser1 58 | { 59 | amount = bound(amount, (1000 * (10 ** usdc.decimals())) + 1, type(uint256).max); 60 | 61 | // it should revert with solidity panic error underflow error 62 | vm.expectRevert(INTEGER_UNDERFLOW_OVERFLOW_PANIC_ERROR); 63 | vault.withdrawCollateral(usdc, user1, user1, amount); 64 | } 65 | 66 | modifier whenTheAmountIsLessThanOrEqualToTheBorrowersDepositedCollateral() { 67 | _; 68 | } 69 | 70 | function test_WhenTheWithdrawalMakesTheVaultsCollateralRatioAboveTheLiquidationThreshold_useUser1(uint256 amount) 71 | external 72 | whenCollateralExists 73 | whenCallerIsOwnerOrReliedUponByOwner 74 | whenTheAmountIsLessThanOrEqualToTheBorrowersDepositedCollateral 75 | useUser1 76 | { 77 | amount = bound(amount, 1, 1000 * (10 ** usdc.decimals())); 78 | 79 | // mint max amount possible of currency to make withdrawing any of my collateral bad for user1 vault position 80 | vault.mintCurrency(usdc, user1, user1, 500_000e18); 81 | 82 | // it should revert with custom error BadCollateralRatio() 83 | vm.expectRevert(BadCollateralRatio.selector); 84 | vault.withdrawCollateral(usdc, user1, user1, amount); 85 | } 86 | 87 | function test_WhenTheWithdrawalMakesTheVaultsCollateralRatioAboveTheLiquidationThreshold_useReliedOnForUser1( 88 | uint256 amount 89 | ) 90 | external 91 | whenCollateralExists 92 | whenCallerIsOwnerOrReliedUponByOwner 93 | whenTheAmountIsLessThanOrEqualToTheBorrowersDepositedCollateral 94 | useReliedOnForUser1(user2) 95 | { 96 | amount = bound(amount, 1, 1000 * (10 ** usdc.decimals())); 97 | 98 | // mint max amount possible of currency to make withdrawing any of my collateral bad for user1 vault position 99 | vault.mintCurrency(usdc, user1, user1, 500_000e18); 100 | 101 | // it should revert with custom error BadCollateralRatio() 102 | vm.expectRevert(BadCollateralRatio.selector); 103 | vault.withdrawCollateral(usdc, user1, user1, amount); 104 | } 105 | 106 | modifier whenTheWithdrawalDoesNotMakeTheVaultsCollateralRatioBelowTheLiquidationThreshold() { 107 | _; 108 | } 109 | 110 | function test_WhenTheAmountIsLessThanOrEqualToTheBorrowersDepositedCollateral_useUser1( 111 | uint256 amount, 112 | uint256 timeElapsed 113 | ) 114 | external 115 | whenCollateralExists 116 | whenCallerIsOwnerOrReliedUponByOwner 117 | whenTheAmountIsLessThanOrEqualToTheBorrowersDepositedCollateral 118 | whenTheWithdrawalDoesNotMakeTheVaultsCollateralRatioBelowTheLiquidationThreshold 119 | useUser1 120 | { 121 | // it should update accrued fees for the user's position 122 | // it should emit CollateralWithdrawn() event with expected indexed and unindexed parameters 123 | // it should update user's, collateral's and global pending fee to the right figures 124 | // it should update the _owner's deposited collateral and collateral's total deposit 125 | // it should send the collateral token to the to address from the vault 126 | 127 | runWithdrawCollateralTestWithChecks(user2, amount, timeElapsed); 128 | } 129 | 130 | function test_WhenTheAmountIsLessThanOrEqualToTheBorrowersDepositedCollateral_useReliedOnForUser1( 131 | uint256 amount, 132 | uint256 timeElapsed 133 | ) 134 | external 135 | whenCollateralExists 136 | whenCallerIsOwnerOrReliedUponByOwner 137 | whenTheAmountIsLessThanOrEqualToTheBorrowersDepositedCollateral 138 | whenTheWithdrawalDoesNotMakeTheVaultsCollateralRatioBelowTheLiquidationThreshold 139 | useReliedOnForUser1(user2) 140 | { 141 | // it should update accrued fees for the user's position 142 | // it should emit CollateralWithdrawn() event with expected indexed and unindexed parameters 143 | // it should update user's, collateral's and global pending fee to the right figures 144 | // it should update the _owner's deposited collateral and collateral's total deposit 145 | // it should send the collateral token to the to address from the vault 146 | 147 | runWithdrawCollateralTestWithChecks(user3, amount, timeElapsed); 148 | } 149 | 150 | function runWithdrawCollateralTestWithChecks(address recipient, uint256 amount, uint256 timeElapsed) private { 151 | amount = bound(amount, 0, 500 * (10 ** usdc.decimals())); 152 | timeElapsed = bound(timeElapsed, 0, TEN_YEARS); 153 | 154 | // cache pre balances 155 | uint256 userOldBalance = usdc.balanceOf(recipient); 156 | uint256 vaultOldBalance = usdc.balanceOf(address(vault)); 157 | 158 | // cache pre storage vars and old accrued fees 159 | IVault.VaultInfo memory initialUserVaultInfo = getVaultMapping(usdc, user1); 160 | IVault.CollateralInfo memory initialCollateralInfo = getCollateralMapping(usdc); 161 | 162 | // take a loan of xNGN to be able to calculate fees acrrual 163 | uint256 amountMinted = 100_000e18; 164 | vault.mintCurrency(usdc, user1, user1, amountMinted); 165 | 166 | // skip time to be able to check accrued interest 167 | skip(timeElapsed); 168 | 169 | // it should emit CollateralWithdrawn() event with expected indexed and unindexed parameters 170 | vm.expectEmit(true, false, false, true, address(vault)); 171 | emit CollateralWithdrawn(user1, recipient, amount); 172 | 173 | // call withdrawCollateral to deposit 1,000 usdc into user1's vault 174 | vault.withdrawCollateral(usdc, user1, recipient, amount); 175 | 176 | // // it should update the user1's deposited collateral and collateral's total deposit 177 | IVault.VaultInfo memory afterUserVaultInfo = getVaultMapping(usdc, user1); 178 | IVault.CollateralInfo memory afterCollateralInfo = getCollateralMapping(usdc); 179 | 180 | // get expected accrued fees 181 | uint256 accruedFees = ( 182 | (calculateCurrentTotalAccumulatedRate(usdc) - initialUserVaultInfo.lastTotalAccumulatedRate) * amountMinted 183 | ) / HUNDRED_PERCENTAGE; 184 | 185 | // it should update accrued fees for the user's position 186 | assertEq(initialUserVaultInfo.accruedFees + accruedFees, afterUserVaultInfo.accruedFees); 187 | 188 | // it should update the storage vars correctly 189 | assertEq(afterCollateralInfo.totalDepositedCollateral, initialCollateralInfo.totalDepositedCollateral - amount); 190 | assertEq(afterUserVaultInfo.depositedCollateral, initialUserVaultInfo.depositedCollateral - amount); 191 | 192 | // it should send the collateral token to the vault from the user1 193 | assertEq(vaultOldBalance - usdc.balanceOf(address(vault)), amount); 194 | assertEq(usdc.balanceOf(recipient) - userOldBalance, amount); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /test/fuzz/vault/withdrawCollateral/withdrawCollateral.tree: -------------------------------------------------------------------------------- 1 | withdrawCollateralTest.t.sol 2 | ├── when collateral does not exist 3 | │ └── it should revert with custom error CollateralDoesNotExist() 4 | └── when collateral exists 5 | ├── when caller is not owner and not relied upon by owner 6 | │ └── it should revert with custom error NotOwnerOrReliedUpon() 7 | └── when caller is owner or relied upon by owner 8 | ├── when the amount is greater than the borrowers deposited collateral 9 | │ └── it should revert with underflow error 10 | └── when the amount is less than or equal to the borrowers deposited collateral 11 | ├── when the withdrawal makes the vault's collateral ratio above the liquidation threshold 12 | │ └── it should revert with custom error BadCollateralRatio() 13 | └── when the withdrawal does not make the vault's collateral ratio above the liquidation threshold 14 | ├── it should update accrued fees for the user's position 15 | ├── it should emit CollateralWithdrawn() event with expected indexed and unindexed parameters 16 | ├── it should update user's, collateral's and global pending fee to the right figures 17 | ├── it should update the _owner's deposited collateral and collateral's total deposit 18 | └── it should send the collateral token to the to address from the vault -------------------------------------------------------------------------------- /test/helpers/ErrorsAndEvents.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.21; 3 | 4 | contract ErrorsAndEvents { 5 | event CollateralTypeAdded(address collateralAddress); 6 | event CollateralDeposited(address indexed owner, uint256 amount); 7 | event CollateralWithdrawn(address indexed owner, address to, uint256 amount); 8 | event CurrencyMinted(address indexed owner, uint256 amount); 9 | event CurrencyBurned(address indexed owner, uint256 amount); 10 | event FeesPaid(address indexed owner, uint256 amount); 11 | event Liquidated( 12 | address indexed owner, address liquidator, uint256 currencyAmountPaid, uint256 collateralAmountCovered 13 | ); 14 | 15 | error ZeroAddress(); 16 | error UnrecognizedParam(); 17 | error BadCollateralRatio(); 18 | error PositionIsSafe(); 19 | error ZeroCollateral(); 20 | error TotalUserCollateralBelowFloor(); 21 | error CollateralAlreadyExists(); 22 | error CollateralDoesNotExist(); 23 | error NotOwnerOrReliedUpon(); 24 | error CollateralRatioNotImproved(); 25 | error NotEnoughCollateralToPay(); 26 | error EthTransferFailed(); 27 | error Paused(); 28 | error NotPaused(); 29 | error GlobalDebtCeilingExceeded(); 30 | error CollateralDebtCeilingExceeded(); 31 | error Unauthorized(); 32 | error NotFeedContract(); 33 | error InvalidStabilityModule(); 34 | error ERC20InsufficientBalance(address, uint256, uint256); 35 | error ERC20InsufficientAllowance(address, uint256, uint256); 36 | error NotMinter(); 37 | } 38 | -------------------------------------------------------------------------------- /test/invariant/baseInvariant.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.21; 3 | 4 | import {BaseTest, IVault, Currency} from "../base.t.sol"; 5 | import {VaultHandler} from "./handlers/vaultHandler.sol"; 6 | import {ERC20Handler} from "./handlers/erc20Handler.sol"; 7 | import {OSMHandler} from "./handlers/osmHandler.sol"; 8 | import {MedianHandler} from "./handlers/medianHandler.sol"; 9 | import {FeedHandler} from "./handlers/feedHandler.sol"; 10 | import {VaultGetters} from "./helpers/vaultGetters.sol"; 11 | import {TimeManager} from "./helpers/timeManager.sol"; 12 | 13 | contract BaseInvariantTest is BaseTest { 14 | TimeManager timeManager; 15 | VaultGetters vaultGetters; 16 | VaultHandler vaultHandler; 17 | ERC20Handler usdcHandler; 18 | ERC20Handler xNGNHandler; 19 | OSMHandler osmHandler; 20 | MedianHandler medianHandler; 21 | FeedHandler feedHandler; 22 | 23 | modifier useCurrentTime() { 24 | vm.warp(timeManager.time()); 25 | _; 26 | } 27 | 28 | function setUp() public virtual override { 29 | super.setUp(); 30 | 31 | timeManager = new TimeManager(); 32 | vaultGetters = new VaultGetters(); 33 | vaultHandler = new VaultHandler(vault, usdc, xNGN, vaultGetters, timeManager); 34 | usdcHandler = new ERC20Handler(Currency(address(usdc)), timeManager); 35 | xNGNHandler = new ERC20Handler(xNGN, timeManager); 36 | osmHandler = new OSMHandler(osm, timeManager); 37 | medianHandler = new MedianHandler(median, timeManager); 38 | feedHandler = new FeedHandler(feed, timeManager, usdc); 39 | 40 | vm.label(address(timeManager), "timeManager"); 41 | vm.label(address(vaultHandler), "vaultHandler"); 42 | vm.label(address(vaultGetters), "vaultGetters"); 43 | vm.label(address(usdcHandler), "usdcHandler"); 44 | vm.label(address(xNGNHandler), "xNGNHandler"); 45 | vm.label(address(osmHandler), "osmHandler"); 46 | vm.label(address(medianHandler), "medianHandler"); 47 | vm.label(address(feedHandler), "feedHandler"); 48 | 49 | // target handlers 50 | targetContract(address(vaultHandler)); 51 | targetContract(address(usdcHandler)); 52 | targetContract(address(xNGNHandler)); 53 | targetContract(address(osmHandler)); 54 | targetContract(address(medianHandler)); 55 | targetContract(address(feedHandler)); 56 | 57 | bytes4[] memory vaultSelectors = new bytes4[](11); 58 | vaultSelectors[0] = VaultHandler.depositCollateral.selector; 59 | vaultSelectors[1] = VaultHandler.withdrawCollateral.selector; 60 | vaultSelectors[2] = VaultHandler.mintCurrency.selector; 61 | vaultSelectors[3] = VaultHandler.burnCurrency.selector; 62 | vaultSelectors[4] = VaultHandler.recoverToken.selector; 63 | vaultSelectors[5] = VaultHandler.withdrawFees.selector; 64 | vaultSelectors[6] = VaultHandler.rely.selector; 65 | vaultSelectors[7] = VaultHandler.deny.selector; 66 | vaultSelectors[8] = VaultHandler.updateBaseRate.selector; 67 | vaultSelectors[9] = VaultHandler.updateCollateralData.selector; 68 | vaultSelectors[10] = VaultHandler.liquidate.selector; 69 | 70 | bytes4[] memory xNGNSelectors = new bytes4[](4); 71 | xNGNSelectors[0] = ERC20Handler.transfer.selector; 72 | xNGNSelectors[1] = ERC20Handler.transferFrom.selector; 73 | xNGNSelectors[2] = ERC20Handler.approve.selector; 74 | xNGNSelectors[3] = ERC20Handler.burn.selector; 75 | 76 | bytes4[] memory usdcSelectors = new bytes4[](5); 77 | usdcSelectors[0] = ERC20Handler.transfer.selector; 78 | usdcSelectors[1] = ERC20Handler.transferFrom.selector; 79 | usdcSelectors[2] = ERC20Handler.approve.selector; 80 | usdcSelectors[3] = ERC20Handler.mint.selector; 81 | usdcSelectors[4] = ERC20Handler.burn.selector; 82 | 83 | bytes4[] memory osmSelectors = new bytes4[](1); 84 | osmSelectors[0] = OSMHandler.update.selector; 85 | 86 | bytes4[] memory medianSelectors = new bytes4[](1); 87 | medianSelectors[0] = MedianHandler.update.selector; 88 | 89 | bytes4[] memory feedSelectors = new bytes4[](1); 90 | feedSelectors[0] = FeedHandler.updatePrice.selector; 91 | 92 | // target selectors of handlers 93 | targetSelector(FuzzSelector({addr: address(vaultHandler), selectors: vaultSelectors})); 94 | targetSelector(FuzzSelector({addr: address(xNGNHandler), selectors: xNGNSelectors})); 95 | targetSelector(FuzzSelector({addr: address(usdcHandler), selectors: usdcSelectors})); 96 | targetSelector(FuzzSelector({addr: address(osmHandler), selectors: osmSelectors})); 97 | targetSelector(FuzzSelector({addr: address(medianHandler), selectors: medianSelectors})); 98 | targetSelector(FuzzSelector({addr: address(feedHandler), selectors: feedSelectors})); 99 | } 100 | 101 | // forgefmt: disable-start 102 | /**************************************************************************************************************************************/ 103 | /*** Invariant Tests ***/ 104 | /*************************************************************************************************************************************** 105 | 106 | * Vault Global Variables 107 | * baseRateInfo.lastUpdateTime: 108 | - must be <= block.timestamp 109 | * baseRateInfo.accumulatedRate: 110 | - must be >= accumulatedRate.rate 111 | * debtCeiling: 112 | - must be >= CURRENCY_TOKEN.totalSupply() 113 | * debt: 114 | - must be == CURRENCY_TOKEN.totalSupply() 115 | * paidFees: 116 | - must always be fully withdrawable 117 | 118 | * Vault Collateral Info Variables 119 | * collateral.totalDepositedCollateral: 120 | - must be <= collateralToken.balanceOf(vault) 121 | - after recoverToken(collateral, to) is called, it must be == collateralToken.balanceOf(vault) 122 | * collateral.totalBorrowedAmount: 123 | - must be <= CURRENCY_TOKEN.totalSupply() 124 | - must be <= collateral.debtCeiling 125 | - must be <= debtCeiling 126 | * collateral.liquidationThreshold: 127 | - any vault whose collateral to debt ratio is above this should be liquidatable 128 | * collateral.liquidationBonus: 129 | - NO INVARIANT 130 | * collateral.rateInfo.rate: 131 | - must be > 0 to be used as input to any function 132 | * collateral.rateInfo.accumulatedRate: 133 | - must be > collateral.rateInfo.rate 134 | * collateral.rateInfo.lastUpdateTime: 135 | - must be > block.timeatamp 136 | * collateral.price: 137 | - NO INVARIANT, checks are done in the Oracle security module 138 | * collateral.debtCeiling: 139 | - must be >= CURRENCY_TOKEN.totalSupply() 140 | * collateral.collateralFloorPerPosition: 141 | - At time `t` when collateral.collateralFloorPerPosition was last updated, 142 | any vault with a depositedCollateral < collateral.collateralFloorPerPosition 143 | must have a borrowedAmount == that vaults borrowedAmount as at time `t`. 144 | It can only change if the vault's depositedCollateral becomes > collateral.collateralFloorPerPosition 145 | * collateral.additionalCollateralPrecision: 146 | - must always be == `18 - token.decimals()` 147 | 148 | 149 | * Vault User Vault Info Variables 150 | * vault.depositedCollateral: 151 | - must be <= collateral.totalDepositedCollateral 152 | - after recoverToken(collateral, to) is called, it must be <= collateralToken.balanceOf(vault) 153 | - sum of all users own must == collateral.totalDepositedCollateral 154 | * vault.borrowedAmount: 155 | - must be <= collateral.totalBorrowedAmount 156 | - must be <= CURRENCY_TOKEN.totalSupply() 157 | - must be <= collateral.debtCeiling 158 | - must be <= debtCeiling 159 | - sum of all users own must == collateral.totalBorrowedAmount 160 | * vault.accruedFees: 161 | - TODO: 162 | * vault.lastTotalAccumulatedRate: 163 | - must be >= `baseRateInfo.rate + collateral.rateInfo.rate` 164 | 165 | /**************************************************************************************************************************************/ 166 | /*** Vault Invariants ***/ 167 | /**************************************************************************************************************************************/ 168 | // forgefmt: disable-end 169 | } 170 | -------------------------------------------------------------------------------- /test/invariant/collateralInvariant.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.21; 3 | 4 | import {BaseInvariantTest, Currency, IVault} from "./baseInvariant.t.sol"; 5 | 6 | // forgefmt: disable-start 7 | /**************************************************************************************************************************************/ 8 | /*** Invariant Tests ***/ 9 | /*************************************************************************************************************************************** 10 | 11 | * Vault Collateral Info Variables 12 | * collateral.totalDepositedCollateral: 13 | - must be <= collateralToken.balanceOf(vault) 14 | - after recoverToken(collateral, to) is called, it must be == collateralToken.balanceOf(vault) 15 | * collateral.totalBorrowedAmount: 16 | - must be <= CURRENCY_TOKEN.totalSupply() 17 | - must be <= collateral.debtCeiling 18 | - must be <= debtCeiling 19 | * collateral.liquidationThreshold: 20 | - any vault whose collateral to debt ratio is above this should be liquidatable 21 | * collateral.liquidationBonus: 22 | - NO INVARIANT 23 | * collateral.rateInfo.rate: 24 | - must be > 0 to be used as input to any function 25 | * collateral.rateInfo.lastUpdateTime: 26 | - must be <= block.timeatamp 27 | * collateral.price: 28 | - when feed.update is called, the osm current must be equal to the price 29 | * collateral.debtCeiling: 30 | - must be >= CURRENCY_TOKEN.totalSupply() as long as the value does not change afterwards to a value lower than collateral.debtCeiling 31 | * collateral.collateralFloorPerPosition: 32 | - At time `t` when collateral.collateralFloorPerPosition was last updated, 33 | any vault with a depositedCollateral < collateral.collateralFloorPerPosition 34 | must have a borrowedAmount == that vaults borrowedAmount as at time `t`. 35 | It can only change if the vault's depositedCollateral becomes > collateral.collateralFloorPerPosition 36 | - This is tested in fuzzed unit tests 37 | * collateral.additionalCollateralPrecision: 38 | - must always be == `18 - token.decimals()` 39 | 40 | 41 | /**************************************************************************************************************************************/ 42 | /*** Vault Invariants ***/ 43 | /**************************************************************************************************************************************/ 44 | // forgefmt: disable-end 45 | 46 | contract CollateralInvariantTest is BaseInvariantTest { 47 | function setUp() public override { 48 | super.setUp(); 49 | 50 | // FOR LIQUIDATIONS BY LIQUIDATOR 51 | // mint usdc to address(this) 52 | vm.startPrank(owner); 53 | Currency(address(usdc)).mint(liquidator, 100_000_000_000 * (10 ** usdc.decimals())); 54 | vm.stopPrank(); 55 | 56 | // use address(this) to deposit so that it can borrow currency needed for liquidation below 57 | vm.startPrank(liquidator); 58 | usdc.approve(address(vault), type(uint256).max); 59 | vault.depositCollateral(usdc, liquidator, 100_000_000_000 * (10 ** usdc.decimals())); 60 | vault.mintCurrency(usdc, liquidator, liquidator, 500_000_000_000e18); 61 | xNGN.approve(address(vault), type(uint256).max); 62 | vm.stopPrank(); 63 | } 64 | 65 | function invariant_collateral_totalDepositedCollateral() external useCurrentTime { 66 | assertLe(getCollateralMapping(usdc).totalDepositedCollateral, usdc.balanceOf(address(vault))); 67 | 68 | vault.recoverToken(address(usdc), address(this)); 69 | assertEq(getCollateralMapping(usdc).totalDepositedCollateral, usdc.balanceOf(address(vault))); 70 | } 71 | 72 | function invariant_collateral_totalBorrowedAmount() external useCurrentTime { 73 | uint256 totalBorrowedAmount = getCollateralMapping(usdc).totalBorrowedAmount; 74 | assertLe(totalBorrowedAmount, xNGN.totalSupply()); 75 | assertLe(totalBorrowedAmount, getCollateralMapping(usdc).debtCeiling); 76 | assertLe(totalBorrowedAmount, vault.debtCeiling()); 77 | } 78 | 79 | function invariant_collateral_liquidationThreshold() external useCurrentTime { 80 | vm.startPrank(liquidator); 81 | 82 | if (vaultGetters.getHealthFactor(vault, usdc, user1)) vm.expectRevert(PositionIsSafe.selector); 83 | vault.liquidate(usdc, user1, address(this), type(uint256).max); 84 | 85 | if (vaultGetters.getHealthFactor(vault, usdc, user2)) vm.expectRevert(PositionIsSafe.selector); 86 | vault.liquidate(usdc, user2, address(this), type(uint256).max); 87 | 88 | if (vaultGetters.getHealthFactor(vault, usdc, user3)) vm.expectRevert(PositionIsSafe.selector); 89 | vault.liquidate(usdc, user3, address(this), type(uint256).max); 90 | 91 | if (vaultGetters.getHealthFactor(vault, usdc, user4)) vm.expectRevert(PositionIsSafe.selector); 92 | vault.liquidate(usdc, user4, address(this), type(uint256).max); 93 | 94 | if (vaultGetters.getHealthFactor(vault, usdc, user5)) vm.expectRevert(PositionIsSafe.selector); 95 | vault.liquidate(usdc, user5, address(this), type(uint256).max); 96 | } 97 | 98 | function invariant_collateral_rateInfo_rate() external useCurrentTime { 99 | assertGt(getCollateralMapping(usdc).rateInfo.rate, 0); 100 | } 101 | 102 | function invariant_collateral_rateInfo_lastUpdateTime() external useCurrentTime { 103 | assertLe(getCollateralMapping(usdc).rateInfo.lastUpdateTime, block.timestamp); 104 | } 105 | 106 | function invariant_collateral_price() external { 107 | vm.startPrank(owner); 108 | feed.updatePrice(usdc); 109 | assertEq(getCollateralMapping(usdc).price, osm.current()); 110 | } 111 | 112 | function invariant_collateral_debtCeiling() external { 113 | assertGe(getCollateralMapping(usdc).debtCeiling, xNGN.totalSupply()); 114 | } 115 | 116 | function invariant_collateral_additionalCollateralPrecision() external { 117 | assertEq(getCollateralMapping(usdc).additionalCollateralPrecision, 18 - usdc.decimals()); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /test/invariant/globalInvariant.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.21; 3 | 4 | import {BaseInvariantTest} from "./baseInvariant.t.sol"; 5 | 6 | // forgefmt: disable-start 7 | /**************************************************************************************************************************************/ 8 | /*** Invariant Tests ***/ 9 | /*************************************************************************************************************************************** 10 | 11 | * Vault Global Variables 12 | * baseRateInfo.lastUpdateTime: 13 | - must be <= block.timestamp 14 | * debtCeiling: 15 | - must be >= CURRENCY_TOKEN.totalSupply() as long as the value does not change afterwards to a value lower than debtCeiling 16 | * debt: 17 | - must be == CURRENCY_TOKEN.totalSupply() 18 | * paidFees: 19 | - must always be fully withdrawable 20 | 21 | 22 | /**************************************************************************************************************************************/ 23 | /*** Vault Invariants ***/ 24 | /**************************************************************************************************************************************/ 25 | // forgefmt: disable-end 26 | 27 | contract GlobalInvariantTest is BaseInvariantTest { 28 | function setUp() public override { 29 | super.setUp(); 30 | } 31 | 32 | function invariant_baseRateInfo_lastUpdateTime() external useCurrentTime { 33 | assertLe(getBaseRateInfo().lastUpdateTime, block.timestamp); 34 | } 35 | 36 | function invariant_debtCeiling() external useCurrentTime { 37 | assertGe(vault.debtCeiling(), xNGN.totalSupply()); 38 | } 39 | 40 | function invariant_debt() external useCurrentTime { 41 | assertEq(vault.debt(), xNGN.totalSupply()); 42 | } 43 | 44 | function invariant_paidFees() external useCurrentTime { 45 | uint256 initialPaidFeed = vault.paidFees(); 46 | uint256 initialStabilityModuleBalance = xNGN.balanceOf(address(vault.stabilityModule())); 47 | vault.withdrawFees(); 48 | assertEq(vault.paidFees(), 0); 49 | assertEq(xNGN.balanceOf(address(vault.stabilityModule())), initialStabilityModuleBalance + initialPaidFeed); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/invariant/handlers/erc20Handler.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.21; 3 | 4 | import {Test, console2, Currency} from "../../base.t.sol"; 5 | import {TimeManager} from "../helpers/timeManager.sol"; 6 | 7 | contract ERC20Handler is Test { 8 | Currency token; 9 | address owner = vm.addr(uint256(keccak256("OWNER"))); 10 | address user1 = vm.addr(uint256(keccak256("User1"))); 11 | address user2 = vm.addr(uint256(keccak256("User2"))); 12 | address user3 = vm.addr(uint256(keccak256("User3"))); 13 | address user4 = vm.addr(uint256(keccak256("User4"))); 14 | address user5 = vm.addr(uint256(keccak256("User5"))); 15 | address liquidator = vm.addr(uint256(keccak256("liquidator"))); 16 | 17 | address[5] actors; 18 | address currentActor; 19 | address currentOwner; // address to be used as owner variable in the calls to be made 20 | 21 | TimeManager timeManager; 22 | 23 | constructor(Currency _token, TimeManager _timeManager) { 24 | timeManager = _timeManager; 25 | 26 | token = _token; 27 | 28 | actors[0] = user1; 29 | actors[1] = user2; 30 | actors[2] = user3; 31 | actors[3] = user4; 32 | actors[4] = user5; 33 | 34 | vm.prank(user1); 35 | token.approve(user1, type(uint256).max); 36 | 37 | vm.prank(user2); 38 | token.approve(user2, type(uint256).max); 39 | 40 | vm.prank(user3); 41 | token.approve(user3, type(uint256).max); 42 | 43 | vm.prank(user4); 44 | token.approve(user4, type(uint256).max); 45 | 46 | vm.prank(user5); 47 | token.approve(user5, type(uint256).max); 48 | } 49 | 50 | modifier skipTime(uint256 skipTimeSeed) { 51 | uint256 skipTimeBy = bound(skipTimeSeed, 0, 365 days); 52 | timeManager.skipTime(skipTimeBy); 53 | _; 54 | } 55 | 56 | modifier prankCurrentActor() { 57 | vm.startPrank(currentActor); 58 | _; 59 | vm.stopPrank(); 60 | } 61 | 62 | modifier setActor(uint256 actorIndexSeed) { 63 | currentActor = actors[bound(actorIndexSeed, 0, actors.length - 1)]; 64 | _; 65 | } 66 | 67 | modifier setOwner(uint256 actorIndexSeed) { 68 | currentOwner = actors[bound(actorIndexSeed, 0, actors.length - 1)]; 69 | _; 70 | } 71 | 72 | function useOwnerIfCurrentActorIsNotReliedOn(uint256 amount) internal { 73 | if (currentOwner != currentActor && token.allowance(currentOwner, currentActor) < amount) { 74 | currentActor = currentOwner; 75 | } 76 | } 77 | 78 | function transfer(uint256 skipTimeSeed, uint256 actorIndexSeed, address to, uint256 amount) 79 | external 80 | skipTime(skipTimeSeed) 81 | setActor(actorIndexSeed) 82 | prankCurrentActor 83 | { 84 | if (to == address(0)) to = address(uint160(uint256(keccak256(abi.encode(to))))); 85 | amount = bound(amount, 0, token.balanceOf(currentActor)); 86 | token.transfer(to, amount); 87 | } 88 | 89 | function approve(uint256 skipTimeSeed, uint256 actorIndexSeed, address to, uint256 amount) 90 | external 91 | skipTime(skipTimeSeed) 92 | setActor(actorIndexSeed) 93 | prankCurrentActor 94 | { 95 | if (to == address(0) || to == currentActor || to.code.length > 0) { 96 | to = address(uint160(uint256(keccak256(abi.encode(to))))); 97 | } 98 | token.approve(to, amount); 99 | } 100 | 101 | function transferFrom( 102 | uint256 skipTimeSeed, 103 | uint256 ownerIndexSeed, 104 | uint256 actorIndexSeed, 105 | address to, 106 | uint256 amount 107 | ) external skipTime(skipTimeSeed) setOwner(ownerIndexSeed) setActor(actorIndexSeed) prankCurrentActor { 108 | if (to == address(0)) to = address(uint160(uint256(keccak256(abi.encode(to))))); 109 | amount = bound(amount, 0, token.balanceOf(currentOwner)); 110 | useOwnerIfCurrentActorIsNotReliedOn(amount); 111 | vm.stopPrank(); 112 | vm.startPrank(currentActor); 113 | token.transferFrom(currentOwner, to, amount); 114 | vm.stopPrank(); 115 | } 116 | 117 | function mint(uint256 skipTimeSeed, address to, uint256 amount) external skipTime(skipTimeSeed) { 118 | if (to == address(0)) to = address(uint160(uint256(keccak256(abi.encode(to))))); 119 | amount = bound(amount, 0, 1000 * (10 ** token.decimals())); 120 | vm.prank(owner); 121 | token.mint(to, amount); 122 | } 123 | 124 | function burn(uint256 skipTimeSeed, uint256 ownerIndexSeed, uint256 actorIndexSeed, uint256 amount) 125 | external 126 | skipTime(skipTimeSeed) 127 | setOwner(ownerIndexSeed) 128 | setActor(actorIndexSeed) 129 | prankCurrentActor 130 | { 131 | amount = bound(amount, 0, token.balanceOf(currentOwner)); 132 | useOwnerIfCurrentActorIsNotReliedOn(amount); 133 | vm.stopPrank(); 134 | vm.startPrank(currentActor); 135 | token.burn(currentOwner, amount); 136 | vm.stopPrank(); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /test/invariant/handlers/feedHandler.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.21; 3 | 4 | import {Test, ERC20Token, console2, Feed} from "../../base.t.sol"; 5 | import {TimeManager} from "../helpers/timeManager.sol"; 6 | 7 | contract FeedHandler is Test { 8 | TimeManager timeManager; 9 | Feed feed; 10 | ERC20Token usdc; 11 | 12 | constructor(Feed _feed, TimeManager _timeManager, ERC20Token _usdc) { 13 | usdc = _usdc; 14 | feed = _feed; 15 | timeManager = _timeManager; 16 | } 17 | 18 | modifier skipTime(uint256 skipTimeSeed) { 19 | uint256 skipTimeBy = bound(skipTimeSeed, 0, 365 days); 20 | timeManager.skipTime(skipTimeBy); 21 | _; 22 | } 23 | 24 | function updatePrice(uint256 skipTimeSeed) external skipTime(skipTimeSeed) { 25 | feed.updatePrice(usdc); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/invariant/handlers/medianHandler.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.21; 3 | 4 | import {Test, console2, Median} from "../../base.t.sol"; 5 | import {TimeManager} from "../helpers/timeManager.sol"; 6 | 7 | contract MedianHandler is Test { 8 | address node0 = vm.addr(uint256(keccak256("Node0"))); 9 | 10 | TimeManager timeManager; 11 | Median median; 12 | 13 | constructor(Median _median, TimeManager _timeManager) { 14 | median = _median; 15 | timeManager = _timeManager; 16 | } 17 | 18 | modifier skipTime(uint256 skipTimeSeed) { 19 | uint256 skipTimeBy = bound(skipTimeSeed, 0, 365 days); 20 | timeManager.skipTime(skipTimeBy); 21 | _; 22 | } 23 | 24 | function update(uint256 skipTimeSeed, uint256 price) external skipTime(skipTimeSeed) { 25 | price = bound(price, 100e6, 10_000e6); 26 | // (uint256[] memory _prices, uint256[] memory _timestamps, bytes[] memory _signatures) = updateParameters(price); 27 | // median.update(_prices, _timestamps, _signatures); 28 | 29 | // doing elliptic curve operations in fuzz tests like the commented code above makes my laptop fans go brrrrrr 30 | // so i just update the storage slot of the median contract where `lastPrice` is stored directly 31 | vm.store(address(median), bytes32(uint256(3)), bytes32(price)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/invariant/handlers/osmHandler.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.21; 3 | 4 | import {Test, console2, OSM} from "../../base.t.sol"; 5 | import {TimeManager} from "../helpers/timeManager.sol"; 6 | 7 | contract OSMHandler is Test { 8 | TimeManager timeManager; 9 | OSM osm; 10 | 11 | constructor(OSM _osm, TimeManager _timeManager) { 12 | osm = _osm; 13 | timeManager = _timeManager; 14 | } 15 | 16 | modifier skipTime(uint256 skipTimeSeed) { 17 | uint256 skipTimeBy = bound(skipTimeSeed, 0, 365 days); 18 | timeManager.skipTime(skipTimeBy); 19 | _; 20 | } 21 | 22 | function update(uint256 skipTimeSeed) external skipTime(skipTimeSeed) { 23 | if (block.timestamp < (osm.lastUpdateHourStart() + 1 hours)) timeManager.skipTime(1 hours); 24 | osm.update(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/invariant/handlers/vaultHandler.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.21; 3 | 4 | import {Test, ERC20Token, IVault, Vault, console2, Currency} from "../../base.t.sol"; 5 | import {VaultGetters} from "../helpers/vaultGetters.sol"; 6 | import {TimeManager} from "../helpers/timeManager.sol"; 7 | 8 | contract VaultHandler is Test { 9 | TimeManager timeManager; 10 | VaultGetters vaultGetters; 11 | Vault vault; 12 | ERC20Token usdc; 13 | Currency xNGN; 14 | address owner = vm.addr(uint256(keccak256("OWNER"))); 15 | address user1 = vm.addr(uint256(keccak256("User1"))); 16 | address user2 = vm.addr(uint256(keccak256("User2"))); 17 | address user3 = vm.addr(uint256(keccak256("User3"))); 18 | address user4 = vm.addr(uint256(keccak256("User4"))); 19 | address user5 = vm.addr(uint256(keccak256("User5"))); 20 | address liquidator = vm.addr(uint256(keccak256("liquidator"))); 21 | 22 | address[5] actors; 23 | address currentActor; 24 | address currentOwner; // address to be used as owner variable in the calls to be made 25 | 26 | // Ghost variables 27 | uint256 public totalDeposits; 28 | uint256 public totalWithdrawals; 29 | uint256 public totalMints; 30 | uint256 public totalBurns; 31 | 32 | constructor(Vault _vault, ERC20Token _usdc, Currency _xNGN, VaultGetters _vaultGetters, TimeManager _timeManager) { 33 | timeManager = _timeManager; 34 | 35 | vault = _vault; 36 | usdc = _usdc; 37 | vaultGetters = _vaultGetters; 38 | xNGN = _xNGN; 39 | 40 | actors[0] = user1; 41 | actors[1] = user2; 42 | actors[2] = user3; 43 | actors[3] = user4; 44 | actors[4] = user5; 45 | 46 | // FOR LIQUIDATIONS BY LIQUIDATOR 47 | // mint usdc to address(this) 48 | vm.startPrank(owner); 49 | Currency(address(usdc)).mint(liquidator, 100_000_000_000 * (10 ** usdc.decimals())); 50 | vm.stopPrank(); 51 | 52 | // use address(this) to deposit so that it can borrow currency needed for liquidation below 53 | vm.startPrank(liquidator); 54 | usdc.approve(address(vault), type(uint256).max); 55 | vault.depositCollateral(usdc, liquidator, 100_000_000_000 * (10 ** usdc.decimals())); 56 | vault.mintCurrency(usdc, liquidator, liquidator, 500_000_000_000e18); 57 | xNGN.approve(address(vault), type(uint256).max); 58 | vm.stopPrank(); 59 | } 60 | 61 | modifier skipTime(uint256 skipTimeSeed) { 62 | uint256 skipTimeBy = bound(skipTimeSeed, 0, 365 days); 63 | timeManager.skipTime(skipTimeBy); 64 | _; 65 | } 66 | 67 | modifier prankCurrentActor() { 68 | vm.startPrank(currentActor); 69 | _; 70 | vm.stopPrank(); 71 | } 72 | 73 | modifier setActor(uint256 actorIndexSeed) { 74 | currentActor = actors[bound(actorIndexSeed, 0, actors.length - 1)]; 75 | _; 76 | } 77 | 78 | modifier setOwner(uint256 actorIndexSeed) { 79 | currentOwner = actors[bound(actorIndexSeed, 0, actors.length - 1)]; 80 | _; 81 | } 82 | 83 | modifier useOwnerIfCurrentActorIsNotReliedOn() { 84 | if (currentOwner != currentActor && !vault.relyMapping(currentOwner, currentActor)) currentActor = currentOwner; 85 | _; 86 | } 87 | 88 | function depositCollateral(uint256 skipTimeSeed, uint256 ownerIndexSeed, uint256 actorIndexSeed, uint256 amount) 89 | external 90 | skipTime(skipTimeSeed) 91 | setOwner(ownerIndexSeed) 92 | setActor(actorIndexSeed) 93 | prankCurrentActor 94 | { 95 | amount = bound(amount, 0, usdc.balanceOf(currentActor)); 96 | totalDeposits += amount; 97 | vault.depositCollateral(usdc, currentOwner, amount); 98 | } 99 | 100 | function withdrawCollateral( 101 | uint256 skipTimeSeed, 102 | uint256 ownerIndexSeed, 103 | uint256 actorIndexSeed, 104 | address to, 105 | uint256 amount 106 | ) 107 | external 108 | skipTime(skipTimeSeed) 109 | setOwner(ownerIndexSeed) 110 | setActor(actorIndexSeed) 111 | useOwnerIfCurrentActorIsNotReliedOn 112 | prankCurrentActor 113 | { 114 | if (to == address(0)) to = address(uint160(uint256(keccak256(abi.encode(to))))); 115 | int256 maxWithdrawable = vaultGetters.getMaxWithdrawable(vault, usdc, currentOwner); 116 | if (maxWithdrawable >= 0) { 117 | amount = bound(amount, 0, uint256(maxWithdrawable)); 118 | totalWithdrawals += amount; 119 | vault.withdrawCollateral(usdc, currentOwner, to, amount); 120 | } 121 | } 122 | 123 | function mintCurrency( 124 | uint256 skipTimeSeed, 125 | uint256 ownerIndexSeed, 126 | uint256 actorIndexSeed, 127 | address to, 128 | uint256 amount 129 | ) 130 | external 131 | skipTime(skipTimeSeed) 132 | setOwner(ownerIndexSeed) 133 | setActor(actorIndexSeed) 134 | useOwnerIfCurrentActorIsNotReliedOn 135 | prankCurrentActor 136 | { 137 | (uint256 depositedCollateral,,) = vaultGetters.getVault(vault, usdc, currentOwner); 138 | (, uint256 totalBorrowedAmount,,, uint256 debtCeiling, uint256 collateralFloorPerPosition,) = 139 | vaultGetters.getCollateralInfo(vault, usdc); 140 | 141 | if (depositedCollateral >= collateralFloorPerPosition) { 142 | if (to == address(0)) to = address(uint160(uint256(keccak256(abi.encode(to))))); 143 | int256 maxBorrowable = vaultGetters.getMaxBorrowable(vault, usdc, currentOwner); 144 | if (maxBorrowable > 0) { 145 | amount = bound(amount, 0, uint256(maxBorrowable)); 146 | if (debtCeiling >= totalBorrowedAmount + amount && vault.debtCeiling() >= vault.debt() + amount) { 147 | totalMints += amount; 148 | vault.mintCurrency(usdc, currentOwner, to, amount); 149 | } 150 | } 151 | } 152 | } 153 | 154 | function burnCurrency(uint256 skipTimeSeed, uint256 ownerIndexSeed, uint256 actorIndexSeed, uint256 amount) 155 | external 156 | skipTime(skipTimeSeed) 157 | setOwner(ownerIndexSeed) 158 | setActor(actorIndexSeed) 159 | prankCurrentActor 160 | { 161 | (, uint256 borrowedAmount, uint256 accruedFees) = vaultGetters.getVault(vault, usdc, currentOwner); 162 | uint256 maxAmount = borrowedAmount + accruedFees < xNGN.balanceOf(currentActor) 163 | ? borrowedAmount + accruedFees 164 | : xNGN.balanceOf(currentActor); 165 | amount = bound(amount, 0, maxAmount); 166 | totalBurns += amount; 167 | vault.burnCurrency(usdc, currentOwner, amount); 168 | } 169 | 170 | function liquidate(uint256 skipTimeSeed, uint256 ownerIndexSeed) 171 | external 172 | skipTime(skipTimeSeed) 173 | setOwner(ownerIndexSeed) 174 | { 175 | vm.startPrank(liquidator); 176 | 177 | if (vaultGetters.getHealthFactor(vault, usdc, currentOwner)) vm.expectRevert(IVault.PositionIsSafe.selector); 178 | vault.liquidate(usdc, currentOwner, address(this), type(uint256).max); 179 | 180 | vm.stopPrank(); 181 | } 182 | 183 | function recoverToken(uint256 skipTimeSeed, bool isUsdc, address to) external skipTime(skipTimeSeed) { 184 | if (to == address(0)) to = address(uint160(uint256(keccak256(abi.encode(to))))); 185 | if (isUsdc) { 186 | vault.recoverToken(address(usdc), to); 187 | } else { 188 | vault.recoverToken(address(xNGN), to); 189 | } 190 | } 191 | 192 | function withdrawFees(uint256 skipTimeSeed) external skipTime(skipTimeSeed) { 193 | vault.withdrawFees(); 194 | } 195 | 196 | function rely(uint256 skipTimeSeed, uint256 relyUponIndexSeed, uint256 actorIndexSeed) 197 | external 198 | skipTime(skipTimeSeed) 199 | setActor(actorIndexSeed) 200 | prankCurrentActor 201 | { 202 | address relyUpon = actors[bound(relyUponIndexSeed, 0, actors.length - 1)]; 203 | vault.rely(relyUpon); 204 | } 205 | 206 | function deny(uint256 skipTimeSeed, uint256 deniedIndexSeed, uint256 actorIndexSeed) 207 | external 208 | skipTime(skipTimeSeed) 209 | setActor(actorIndexSeed) 210 | prankCurrentActor 211 | { 212 | address denied = actors[bound(deniedIndexSeed, 0, actors.length - 1)]; 213 | vault.rely(denied); 214 | } 215 | 216 | function updateBaseRate(uint256 skipTimeSeed, uint256 value) external skipTime(skipTimeSeed) { 217 | value = bound(value, 0, 100e18); 218 | vm.startPrank(owner); 219 | 220 | vault.updateBaseRate(value); 221 | vm.stopPrank(); 222 | } 223 | 224 | function updateCollateralData(uint256 skipTimeSeed, uint256 paramIndex, uint256 value) 225 | external 226 | skipTime(skipTimeSeed) 227 | { 228 | IVault.ModifiableParameters param = 229 | IVault.ModifiableParameters(uint8(bound(paramIndex, 0, uint256(type(IVault.ModifiableParameters).max)))); 230 | 231 | vm.startPrank(owner); 232 | if (param == IVault.ModifiableParameters.RATE) { 233 | value = bound(value, 1, 100e18); 234 | vault.updateCollateralData(usdc, param, value); 235 | } else if (param == IVault.ModifiableParameters.COLLATERAL_FLOOR_PER_POSITION) { 236 | vault.updateCollateralData(usdc, param, value); 237 | } else if (param == IVault.ModifiableParameters.LIQUIDATION_BONUS) { 238 | vault.updateCollateralData(usdc, param, value); 239 | } else if (param == IVault.ModifiableParameters.LIQUIDATION_THRESHOLD) { 240 | value = bound(value, 10e18, 100e18); // let's not be outrageous now, shall we? 241 | vault.updateCollateralData(usdc, param, value); 242 | } 243 | vm.stopPrank(); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /test/invariant/helpers/timeManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.21; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | 6 | /// Foundry does not persist timestamp between invariant test runs so there's need to use a contract to persist the last a timestamp for manual time persisten 7 | contract TimeManager is Test { 8 | uint256 public time = block.timestamp; 9 | 10 | function skipTime(uint256 skipTimeBy) external { 11 | time += skipTimeBy; 12 | vm.warp(time); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/invariant/helpers/vaultGetters.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.21; 3 | 4 | // ========== External imports ========== 5 | import {Vault, IVault, ERC20Token} from "../../../src/vault.sol"; 6 | 7 | contract VaultGetters { 8 | uint256 private constant PRECISION_DEGREE = 18; 9 | uint256 private constant PRECISION = 1 * (10 ** PRECISION_DEGREE); 10 | uint256 private constant HUNDRED_PERCENTAGE = 100 * (10 ** PRECISION_DEGREE); 11 | uint256 private constant ADDITIONAL_FEED_PRECISION = 1e12; 12 | 13 | function _getVaultMapping(Vault _vaultContract, ERC20Token _collateralToken, address _owner) 14 | private 15 | view 16 | returns (IVault.VaultInfo memory) 17 | { 18 | (uint256 depositedCollateral, uint256 borrowedAmount, uint256 accruedFees, uint256 lastTotalAccumulatedRate) = 19 | _vaultContract.vaultMapping(_collateralToken, _owner); 20 | 21 | return IVault.VaultInfo(depositedCollateral, borrowedAmount, accruedFees, lastTotalAccumulatedRate); 22 | } 23 | 24 | function _getBaseRateInfo(Vault _vaultContract) private view returns (IVault.RateInfo memory) { 25 | (uint256 rate, uint256 accumulatedRate, uint256 lastUpdateTime) = _vaultContract.baseRateInfo(); 26 | 27 | return IVault.RateInfo(rate, accumulatedRate, lastUpdateTime); 28 | } 29 | 30 | function _getCollateralMapping(Vault _vaultContract, ERC20Token _collateralToken) 31 | private 32 | view 33 | returns (IVault.CollateralInfo memory) 34 | { 35 | ( 36 | uint256 totalDepositedCollateral, 37 | uint256 totalBorrowedAmount, 38 | uint256 liquidationThreshold, 39 | uint256 liquidationBonus, 40 | Vault.RateInfo memory rateInfo, 41 | uint256 price, 42 | uint256 debtCeiling, 43 | uint256 collateralFloorPerPosition, 44 | uint256 additionalCollateralPercision 45 | ) = _vaultContract.collateralMapping(_collateralToken); 46 | 47 | return IVault.CollateralInfo( 48 | totalDepositedCollateral, 49 | totalBorrowedAmount, 50 | liquidationThreshold, 51 | liquidationBonus, 52 | rateInfo, 53 | price, 54 | debtCeiling, 55 | collateralFloorPerPosition, 56 | additionalCollateralPercision 57 | ); 58 | } 59 | 60 | // ------------------------------------------------ GETTERS ------------------------------------------------ 61 | 62 | /** 63 | * @dev returns health factor (if a vault is liquidatable or not) of a vault 64 | */ 65 | function getHealthFactor(Vault _vaultContract, ERC20Token _collateralToken, address _owner) 66 | external 67 | view 68 | returns (bool) 69 | { 70 | IVault.VaultInfo memory _vault = _getVaultMapping(_vaultContract, _collateralToken, _owner); 71 | IVault.CollateralInfo memory _collateral = _getCollateralMapping(_vaultContract, _collateralToken); 72 | 73 | if (_collateral.rateInfo.rate == 0) return true; 74 | 75 | uint256 _collateralRatio = _getCollateralRatio(_vaultContract, _collateral, _vault); 76 | 77 | return _collateralRatio <= _collateral.liquidationThreshold; 78 | } 79 | 80 | /** 81 | * @dev returns the collateral ratio of a vault 82 | */ 83 | function getCollateralRatio(Vault _vaultContract, ERC20Token _collateralToken, address _owner) 84 | external 85 | view 86 | returns (uint256) 87 | { 88 | IVault.VaultInfo memory _vault = _getVaultMapping(_vaultContract, _collateralToken, _owner); 89 | IVault.CollateralInfo memory _collateral = _getCollateralMapping(_vaultContract, _collateralToken); 90 | 91 | if (_collateral.rateInfo.rate == 0) return 0; 92 | 93 | return _getCollateralRatio(_vaultContract, _collateral, _vault); 94 | } 95 | 96 | /** 97 | * @dev returns the max amount of currency a vault owner can mint for that vault without the tx reverting due to the vault's health factor falling below the min health factor 98 | * @dev if it's a negative number then the vault is below the min health factor already and paying back the additive inverse of the result will pay back both borrowed amount and interest accrued 99 | */ 100 | function getMaxBorrowable(Vault _vaultContract, ERC20Token _collateralToken, address _owner) 101 | external 102 | view 103 | returns (int256) 104 | { 105 | IVault.VaultInfo memory _vault = _getVaultMapping(_vaultContract, _collateralToken, _owner); 106 | IVault.CollateralInfo memory _collateral = _getCollateralMapping(_vaultContract, _collateralToken); 107 | 108 | // if no collateral it should return 0 109 | if (_vault.depositedCollateral == 0 || _collateral.rateInfo.rate == 0) return 0; 110 | 111 | // get value of collateral 112 | uint256 _collateralValueInCurrency = _getCurrencyValueOfCollateral(_collateral, _vault); 113 | 114 | // adjust this to consider liquidation ratio 115 | uint256 _adjustedCollateralValueInCurrency = 116 | (_collateralValueInCurrency * _collateral.liquidationThreshold) / HUNDRED_PERCENTAGE; 117 | 118 | // account for accrued fees 119 | (uint256 _currentAccruedFees,) = _calculateAccruedFees(_vaultContract, _collateral, _vault); 120 | uint256 _borrowedAmount = _vault.borrowedAmount + _vault.accruedFees + _currentAccruedFees; 121 | 122 | int256 maxBorrowableAmount = int256(_adjustedCollateralValueInCurrency) - int256(_borrowedAmount); 123 | 124 | // if maxBorrowable amount is positive (i.e user can still borrow and not in debt) and max borrowable amount is greater than debt ceiling, return debt ceiling as that is what's actually borrowable 125 | if (maxBorrowableAmount > 0 && _collateral.debtCeiling < uint256(maxBorrowableAmount)) { 126 | if (_collateral.debtCeiling > uint256(type(int256).max)) maxBorrowableAmount = type(int256).max; 127 | // at this point it is surely going not overflow when casting into int256 because of the check above 128 | else maxBorrowableAmount = int256(_collateral.debtCeiling); 129 | } 130 | 131 | // return the result minus already taken collateral. 132 | // this can be negative if health factor is below 1e18. 133 | // caller should know that if the result is negative then borrowing / removing collateral will fail 134 | return maxBorrowableAmount; 135 | } 136 | 137 | /** 138 | * @dev returns the max amount of collateral a vault owner can withdraw from a vault without the tx reverting due to the vault's health factor falling below the min health factor 139 | * @dev if it's a negative number then the vault is below the min health factor already and depositing the additive inverse will put the position at the min health factor saving it from liquidation. 140 | * @dev the recommended way to do this is to burn/pay back the additive inverse of the result of `getMaxBorrowable()` that way interest would not accrue after payment. 141 | */ 142 | function getMaxWithdrawable(Vault _vaultContract, ERC20Token _collateralToken, address _owner) 143 | external 144 | view 145 | returns (int256) 146 | { 147 | IVault.VaultInfo memory _vault = _getVaultMapping(_vaultContract, _collateralToken, _owner); 148 | IVault.CollateralInfo memory _collateral = _getCollateralMapping(_vaultContract, _collateralToken); 149 | 150 | if (_collateral.rateInfo.rate == 0) return 0; 151 | 152 | // account for accrued fees 153 | (uint256 _currentAccruedFees,) = _calculateAccruedFees(_vaultContract, _collateral, _vault); 154 | uint256 _borrowedAmount = _vault.borrowedAmount + _vault.accruedFees + _currentAccruedFees; 155 | 156 | // get cyrrency equivalent of borrowed currency 157 | uint256 _collateralAmountFromCurrencyValue = _getCollateralAmountFromCurrencyValue(_collateral, _borrowedAmount); 158 | 159 | // adjust for liquidation ratio 160 | uint256 _adjustedCollateralAmountFromCurrencyValue = 161 | _divUp((_collateralAmountFromCurrencyValue * HUNDRED_PERCENTAGE), _collateral.liquidationThreshold); 162 | 163 | // return diff in deposited and expected collaeral bal 164 | return int256(_vault.depositedCollateral) - int256(_adjustedCollateralAmountFromCurrencyValue); 165 | } 166 | 167 | /** 168 | * @dev returns a vault's relevant info i.e the depositedCollateral, borrowedAmount, and updated accruedFees 169 | * @dev recommended to read the accrued fees from here as it'll be updated before being returned. 170 | */ 171 | function getVault(Vault _vaultContract, ERC20Token _collateralToken, address _owner) 172 | external 173 | view 174 | returns (uint256, uint256, uint256) 175 | { 176 | IVault.VaultInfo memory _vault = _getVaultMapping(_vaultContract, _collateralToken, _owner); 177 | IVault.CollateralInfo memory _collateral = _getCollateralMapping(_vaultContract, _collateralToken); 178 | // account for accrued fees 179 | (uint256 _currentAccruedFees,) = _calculateAccruedFees(_vaultContract, _collateral, _vault); 180 | uint256 _accruedFees = _vault.accruedFees + _currentAccruedFees; 181 | 182 | return (_vault.depositedCollateral, _vault.borrowedAmount, _accruedFees); 183 | } 184 | 185 | /** 186 | * @dev returns a the relevant info for a collateral 187 | */ 188 | function getCollateralInfo(Vault _vaultContract, ERC20Token _collateralToken) 189 | external 190 | view 191 | returns (uint256, uint256, uint256, uint256, uint256, uint256, uint256) 192 | { 193 | IVault.CollateralInfo memory _collateral = _getCollateralMapping(_vaultContract, _collateralToken); 194 | 195 | IVault.RateInfo memory _baseRateInfo = _getBaseRateInfo(_vaultContract); 196 | uint256 _rate = (_collateral.rateInfo.rate + _baseRateInfo.rate) * 365 days; 197 | uint256 _minDeposit = _collateral.collateralFloorPerPosition; 198 | 199 | return ( 200 | _collateral.totalDepositedCollateral, 201 | _collateral.totalBorrowedAmount, 202 | _collateral.liquidationThreshold, 203 | _collateral.debtCeiling, 204 | _rate, 205 | _minDeposit, 206 | _collateral.price 207 | ); 208 | } 209 | 210 | /** 211 | * @dev returns if _owner has approved _reliedUpon to interact with _owner's vault on their behalf 212 | */ 213 | function isReliedUpon(Vault _vaultContract, address _owner, address _reliedUpon) external view returns (bool) { 214 | return _vaultContract.relyMapping(_owner, _reliedUpon); 215 | } 216 | 217 | // ------------------------------------------------ INTERNAL FUNCTIONS ------------------------------------------------ 218 | 219 | /** 220 | * @dev returns the collateral ratio of a vault where anything below 1e18 is liquidatable 221 | * @dev should never revert! 222 | */ 223 | function _getCollateralRatio( 224 | Vault _vaultContract, 225 | IVault.CollateralInfo memory _collateral, 226 | IVault.VaultInfo memory _vault 227 | ) internal view returns (uint256) { 228 | // get collateral value in currency 229 | // get total currency minted 230 | // if total currency minted == 0, return max uint 231 | // else, adjust collateral to liquidity threshold (multiply by liquidity threshold fraction) 232 | // divide by total currency minted to get a value. 233 | 234 | // prevent division by 0 revert below 235 | (uint256 _unaccountedAccruedFees,) = _calculateAccruedFees(_vaultContract, _collateral, _vault); 236 | uint256 _totalUserDebt = _vault.borrowedAmount + _vault.accruedFees + _unaccountedAccruedFees; 237 | // if user's debt is 0 return 0 238 | if (_totalUserDebt == 0) return 0; 239 | // if deposited collateral is 0 return type(uint256).max. The condition check above ensures that execution only reaches here if _totalUserDebt > 0 240 | if (_vault.depositedCollateral == 0) return type(uint256).max; 241 | 242 | // _collateralValueInCurrency: divDown (solidity default) since _collateralValueInCurrency is denominator 243 | uint256 _collateralValueInCurrency = _getCurrencyValueOfCollateral(_collateral, _vault); 244 | 245 | // divUp as this benefits the protocol 246 | return _divUp((_totalUserDebt * HUNDRED_PERCENTAGE), _collateralValueInCurrency); 247 | } 248 | 249 | /** 250 | * @dev returns the conversion of a vaults deposited collateral to the vault's currency 251 | * @dev should never revert! 252 | */ 253 | function _getCurrencyValueOfCollateral(IVault.CollateralInfo memory _collateral, IVault.VaultInfo memory _vault) 254 | internal 255 | pure 256 | returns (uint256) 257 | { 258 | uint256 _currencyValueOfCollateral = ( 259 | _scaleCollateralToExpectedPrecision(_collateral, _vault.depositedCollateral) * _collateral.price 260 | * ADDITIONAL_FEED_PRECISION 261 | ) / PRECISION; 262 | return _currencyValueOfCollateral; 263 | } 264 | 265 | /** 266 | * @dev returns the conversion of an amount of currency to a given supported collateral 267 | * @dev should never revert! 268 | */ 269 | function _getCollateralAmountFromCurrencyValue(IVault.CollateralInfo memory _collateral, uint256 _amount) 270 | internal 271 | pure 272 | returns (uint256) 273 | { 274 | return _divUp( 275 | (_amount * PRECISION), 276 | (_collateral.price * ADDITIONAL_FEED_PRECISION * (10 ** _collateral.additionalCollateralPrecision)) 277 | ); 278 | } 279 | 280 | /** 281 | * @dev returns the fees accrued by a user's vault since `_vault.lastUpdateTime` 282 | * @dev should never revert! 283 | */ 284 | function _calculateAccruedFees( 285 | Vault _vaultContract, 286 | IVault.CollateralInfo memory _collateral, 287 | IVault.VaultInfo memory _vault 288 | ) internal view returns (uint256, uint256) { 289 | uint256 _totalCurrentAccumulatedRate = _vaultContract.rateModule().calculateCurrentTotalAccumulatedRate( 290 | _getBaseRateInfo(_vaultContract), _collateral.rateInfo 291 | ); 292 | 293 | uint256 _accruedFees = ( 294 | (_totalCurrentAccumulatedRate - _vault.lastTotalAccumulatedRate) * _vault.borrowedAmount 295 | ) / HUNDRED_PERCENTAGE; 296 | 297 | return (_accruedFees, _totalCurrentAccumulatedRate); 298 | } 299 | 300 | /** 301 | * @dev returns the current total accumulated rate i.e current accumulated base rate + current accumulated collateral rate of the given collateral 302 | * @dev should never revert! 303 | */ 304 | function _calculateCurrentTotalAccumulatedRate(Vault _vaultContract, IVault.CollateralInfo memory _collateral) 305 | internal 306 | view 307 | returns (uint256) 308 | { 309 | // calculates pending collateral rate and adds it to the last stored collateral rate 310 | uint256 _collateralCurrentAccumulatedRate = _collateral.rateInfo.accumulatedRate 311 | + (_collateral.rateInfo.rate * (block.timestamp - _collateral.rateInfo.lastUpdateTime)); 312 | 313 | IVault.RateInfo memory _baseRateInfo = _getBaseRateInfo(_vaultContract); 314 | 315 | // calculates pending base rate and adds it to the last stored base rate 316 | uint256 _baseCurrentAccumulatedRate = 317 | _baseRateInfo.accumulatedRate + (_baseRateInfo.rate * (block.timestamp - _baseRateInfo.lastUpdateTime)); 318 | 319 | // adds together to get total rate since inception 320 | return _collateralCurrentAccumulatedRate + _baseCurrentAccumulatedRate; 321 | } 322 | 323 | /** 324 | * @dev scales a given collateral to be represented in 1e18 325 | * @dev should never revert! 326 | */ 327 | function _scaleCollateralToExpectedPrecision(IVault.CollateralInfo memory _collateral, uint256 amount) 328 | internal 329 | pure 330 | returns (uint256) 331 | { 332 | return amount * (10 ** _collateral.additionalCollateralPrecision); 333 | } 334 | 335 | /** 336 | * @dev divides `_a` by `_b` and rounds the result `_c` up to the next whole number 337 | * 338 | * @dev if `_a` is 0, return 0 early as it will revert with underflow error when calculating divUp below 339 | * @dev reverts if `_b` is 0 340 | */ 341 | function _divUp(uint256 _a, uint256 _b) private pure returns (uint256 _c) { 342 | if (_b == 0) revert(); 343 | if (_a == 0) return 0; 344 | 345 | _c = 1 + ((_a - 1) / _b); 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /test/invariant/userVaultInvariant.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.21; 3 | 4 | import {BaseInvariantTest, IVault} from "./baseInvariant.t.sol"; 5 | 6 | // forgefmt: disable-start 7 | /**************************************************************************************************************************************/ 8 | /*** Invariant Tests ***/ 9 | /*************************************************************************************************************************************** 10 | 11 | * Vault User Vault Info Variables 12 | * vault.depositedCollateral: 13 | - must be <= collateral.totalDepositedCollateral 14 | - after recoverToken(collateral, to) is called, it must be <= collateralToken.balanceOf(vault) 15 | - sum of all users own must == collateral.totalDepositedCollateral 16 | * vault.borrowedAmount: 17 | - must be <= collateral.totalBorrowedAmount 18 | - must be <= CURRENCY_TOKEN.totalSupply() 19 | - must be <= collateral.debtCeiling 20 | - must be <= debtCeiling 21 | - sum of all users own must == collateral.totalBorrowedAmount 22 | * vault.accruedFees: 23 | - TODO: 24 | * vault.lastTotalAccumulatedRate: 25 | - must be >= `baseRateInfo.rate + collateral.rateInfo.rate` 26 | 27 | 28 | /**************************************************************************************************************************************/ 29 | /*** Vault Invariants ***/ 30 | /**************************************************************************************************************************************/ 31 | // forgefmt: disable-end 32 | 33 | contract UserVaultInvariantTest is BaseInvariantTest { 34 | function setUp() public override { 35 | super.setUp(); 36 | } 37 | 38 | function invariant_user_vault_depositedCollateral() external { 39 | // recover token first 40 | vault.recoverToken(address(usdc), address(this)); 41 | 42 | assert_user_vault_depositedCollateral(user1); 43 | assert_user_vault_depositedCollateral(user2); 44 | assert_user_vault_depositedCollateral(user3); 45 | assert_user_vault_depositedCollateral(user4); 46 | assert_user_vault_depositedCollateral(user5); 47 | 48 | assertEq(_sumUsdcBalances(), getCollateralMapping(usdc).totalDepositedCollateral); 49 | } 50 | 51 | function invariant_user_vault_borrowedAmount() external { 52 | assert_user_vault_borrowedAmount(user1); 53 | assert_user_vault_borrowedAmount(user2); 54 | assert_user_vault_borrowedAmount(user3); 55 | assert_user_vault_borrowedAmount(user4); 56 | assert_user_vault_borrowedAmount(user5); 57 | 58 | assertEq(_sumxNGNBalances(), getCollateralMapping(usdc).totalBorrowedAmount); 59 | } 60 | 61 | function invariant_user_vault_accruedFees() external { 62 | // TODO: 63 | } 64 | 65 | function invariant_user_vault_lastTotalAccumulatedRate() external { 66 | assert_user_vault_lastTotalAccumulatedRate(user1); 67 | assert_user_vault_lastTotalAccumulatedRate(user2); 68 | assert_user_vault_lastTotalAccumulatedRate(user3); 69 | assert_user_vault_lastTotalAccumulatedRate(user4); 70 | assert_user_vault_lastTotalAccumulatedRate(user5); 71 | } 72 | 73 | // forgefmt: disable-start 74 | /**************************************************************************************************************************************/ 75 | /*** Helpers ***/ 76 | /**************************************************************************************************************************************/ 77 | // forgefmt: disable-end 78 | function assert_user_vault_depositedCollateral(address user) private { 79 | uint256 depositedCollateral = getVaultMapping(usdc, user).depositedCollateral; 80 | assertLe(depositedCollateral, getCollateralMapping(usdc).totalDepositedCollateral); 81 | assertLe(depositedCollateral, usdc.balanceOf(address(vault))); 82 | } 83 | 84 | function assert_user_vault_borrowedAmount(address user) private { 85 | uint256 borrowedAmount = getVaultMapping(usdc, user).borrowedAmount; 86 | assertLe(borrowedAmount, getCollateralMapping(usdc).totalBorrowedAmount); 87 | assertLe(borrowedAmount, xNGN.totalSupply()); 88 | assertLe(borrowedAmount, getCollateralMapping(usdc).debtCeiling); 89 | assertLe(borrowedAmount, vault.debtCeiling()); 90 | } 91 | 92 | function assert_user_vault_lastTotalAccumulatedRate(address user) private { 93 | IVault.VaultInfo memory userVault = getVaultMapping(usdc, user); 94 | if (userVault.accruedFees > 0) { 95 | assertGe( 96 | userVault.lastTotalAccumulatedRate, getBaseRateInfo().rate + getCollateralMapping(usdc).rateInfo.rate 97 | ); 98 | } 99 | } 100 | 101 | function _sumUsdcBalances() private view returns (uint256 sum) { 102 | sum = ( 103 | getVaultMapping(usdc, user1).depositedCollateral + getVaultMapping(usdc, user2).depositedCollateral 104 | + getVaultMapping(usdc, user3).depositedCollateral + getVaultMapping(usdc, user4).depositedCollateral 105 | + getVaultMapping(usdc, user5).depositedCollateral + getVaultMapping(usdc, liquidator).depositedCollateral 106 | ); 107 | } 108 | 109 | function _sumxNGNBalances() private view returns (uint256 sum) { 110 | sum = ( 111 | getVaultMapping(usdc, user1).borrowedAmount + getVaultMapping(usdc, user2).borrowedAmount 112 | + getVaultMapping(usdc, user3).borrowedAmount + getVaultMapping(usdc, user4).borrowedAmount 113 | + getVaultMapping(usdc, user5).borrowedAmount + getVaultMapping(usdc, liquidator).borrowedAmount 114 | ); 115 | } 116 | } 117 | --------------------------------------------------------------------------------