├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── foundry.toml ├── src ├── 01-naive-receiver │ ├── FlashLoanReceiver.sol │ └── NaiveReceiverLenderPool.sol ├── 02-unstoppable │ ├── ReceiverUnstoppable.sol │ └── UnstoppableLender.sol ├── 03-proposal │ └── Proposal.sol ├── 04-voting-nft │ ├── VotingNft.sol │ └── VotingNftForFuzz.sol ├── 05-token-sale │ └── TokenSale.sol ├── 06-rarely-false │ └── RarelyFalse.sol ├── 07-byte-battle │ └── ByteBattle.sol ├── 08-omni-protocol │ ├── IRM.sol │ ├── OmniOracle.sol │ ├── OmniPool.sol │ ├── OmniToken.sol │ ├── OmniTokenNoBorrow.sol │ ├── SubAccount.sol │ ├── WETHGateway.sol │ ├── WithUnderlying.sol │ ├── interfaces │ │ ├── IBandReference.sol │ │ ├── IChainlinkAggregator.sol │ │ ├── ICustomOmniOracle.sol │ │ ├── IIRM.sol │ │ ├── IOmniOracle.sol │ │ ├── IOmniPool.sol │ │ ├── IOmniToken.sol │ │ ├── IOmniTokenBase.sol │ │ ├── IOmniTokenNoBorrow.sol │ │ ├── IWETH9.sol │ │ └── IWithUnderlying.sol │ └── oracles │ │ └── WstETHCustomOracle.sol ├── 09-vesting │ └── Vesting.sol ├── 10-vesting-ext │ └── VestingExt.sol ├── 11-op-reg │ └── OperatorRegistry.sol ├── 12-liquidate-dos │ └── LiquidateDos.sol ├── 13-stability-pool │ └── StabilityPool.sol ├── 14-priority │ └── Priority.sol ├── MockERC20.sol ├── TestToken.sol └── TestToken2.sol └── test ├── 01-naive-receiver ├── NaiveReceiverAdvancedEchidna.t.sol ├── NaiveReceiverAdvancedEchidna.yaml ├── NaiveReceiverAdvancedFoundry.t.sol ├── NaiveReceiverAdvancedMedusa.json ├── NaiveReceiverBasicEchidna.t.sol ├── NaiveReceiverBasicEchidna.yaml ├── NaiveReceiverBasicFoundry.t.sol └── NaiveReceiverBasicMedusa.json ├── 02-unstoppable ├── UnstoppableBasicEchidna.t.sol ├── UnstoppableBasicEchidna.yaml ├── UnstoppableBasicFoundry.t.sol ├── UnstoppableBasicMedusa.json ├── certora.conf └── certora.spec ├── 03-proposal ├── Properties.sol ├── ProposalCryticTester.sol ├── ProposalCryticTesterToFoundry.sol ├── Setup.sol ├── certora.conf ├── certora.spec ├── echidna.yaml └── medusa.json ├── 04-voting-nft ├── Properties.sol ├── Setup.sol ├── VotingNftCryticTester.sol ├── VotingNftCryticToFoundry.sol ├── certora.conf ├── certora.spec ├── echidna.yaml └── medusa.json ├── 05-token-sale ├── TokenSaleAdvancedEchidna.t.sol ├── TokenSaleAdvancedEchidna.yaml ├── TokenSaleAdvancedFoundry.t.sol ├── TokenSaleBasicEchidna.t.sol ├── TokenSaleBasicEchidna.yaml ├── TokenSaleBasicFoundry.t.sol ├── TokenSaleBasicMedusa.json ├── certora.conf └── certora.spec ├── 06-rarely-false ├── RarelyFalseCryticTester.sol ├── RarelyFalseCryticToFoundry.sol ├── TargetFunctions.sol ├── certora.conf ├── certora.spec ├── echidna.yaml └── medusa.json ├── 07-byte-battle ├── ByteBattleCryticTester.sol ├── ByteBattleCryticToFoundry.sol ├── TargetFunctions.sol ├── certora.conf ├── certora.spec ├── echidna.yaml └── medusa.json ├── 08-omni-protocol ├── MockOracle.sol ├── OmniAdvancedEchidna.yaml ├── OmniAdvancedFoundry.t.sol ├── OmniAdvancedMedusa.json └── OmniAdvancedMedusa.t.sol ├── 09-vesting ├── Properties.sol ├── Setup.sol ├── TargetFunctions.sol ├── VestingCryticTester.sol ├── VestingCryticToFoundry.sol ├── certora.conf ├── certora.spec ├── echidna.yaml └── medusa.json ├── 10-vesting-ext ├── Properties.sol ├── Setup.sol ├── TargetFunctions.sol ├── VestingExtCryticTester.sol ├── VestingExtCryticToFoundry.sol ├── certora.conf ├── certora.spec ├── echidna.yaml └── medusa.json ├── 11-op-reg ├── OpRegCryticTester.sol ├── OpRegCryticToFoundry.sol ├── Properties.sol ├── Setup.sol ├── TargetFunctions.sol ├── certora.conf ├── certora.spec ├── echidna.yaml └── medusa.json ├── 12-liquidate-dos ├── LiquidateDosCryticTester.sol ├── LiquidateDosCryticToFoundry.sol ├── Properties.sol ├── Setup.sol ├── TargetFunctions.sol ├── echidna.yaml └── medusa.json ├── 13-stability-pool ├── Properties.sol ├── Setup.sol ├── StabilityPoolCryticTester.sol ├── StabilityPoolCryticToFoundry.sol ├── TargetFunctions.sol ├── certora.conf ├── certora.spec ├── echidna.yaml └── medusa.json ├── 14-priority ├── PriorityCryticTester.sol ├── PriorityCryticToFoundry.sol ├── Properties.sol ├── Setup.sol ├── TargetFunctions.sol ├── certora.conf ├── certora.spec ├── echidna.yaml └── medusa.json └── TestUtils.sol /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | crytic-export/ 5 | 6 | # Ignores development broadcast logs 7 | !/broadcast 8 | /broadcast/*/31337/ 9 | /broadcast/**/dry-run/ 10 | 11 | # Docs 12 | docs/ 13 | 14 | # Dotenv file 15 | .env 16 | 17 | # test coverage files 18 | **/coverage** 19 | 20 | # misc 21 | .DS_Store 22 | .certora_internal 23 | slither_results.json -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/openzeppelin-contracts"] 5 | path = lib/openzeppelin-contracts 6 | url = https://github.com/OpenZeppelin/openzeppelin-contracts 7 | [submodule "lib/openzeppelin-contracts-upgradeable"] 8 | path = lib/openzeppelin-contracts-upgradeable 9 | url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable 10 | [submodule "lib/chimera"] 11 | path = lib/chimera 12 | url = https://github.com/Recon-Fuzz/chimera 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Dacian 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solidity Fuzzing Challenge: Foundry vs Echidna vs Medusa (plus Halmos & Certora) # 2 | 3 | A comparison of solidity fuzzing tools [Foundry](https://book.getfoundry.sh/), [Echidna](https://secure-contracts.com/program-analysis/echidna/index.html) & [Medusa](https://github.com/crytic/medusa) also considering Formal Verification tools such as [Halmos](https://github.com/a16z/halmos) and [Certora](https://docs.certora.com/en/latest/docs/user-guide/tutorials.html). This challenge set is not intended to be an academically rigorous benchmark but rather to present the experiences of an auditor "in the trenches"; the primary goal is finding the best performance "out of the box" with as little guidance & tweaking as possible. 4 | 5 | Many of the challenges are simplified versions of audit findings from my private audits at [Cyfrin](https://www.cyfrin.io). These findings could have been found by the protocol developers themselves prior to an external audit if the protocol had written the correct [fuzz testing invariants](https://dacian.me/find-highs-before-external-auditors-using-invariant-fuzz-testing). Hence a secondary goal of this repo is to show developers how to write better fuzz testing invariants to improve their protocol security prior to engaging external auditors. 6 | 7 | ## Setup ## 8 | 9 | Ensure you are using recent versions of [Foundry](https://github.com/foundry-rs/foundry), [Echidna](https://github.com/crytic/echidna) and [Medusa](https://github.com/crytic/medusa). 10 | 11 | Configure [solc-select](https://github.com/crytic/solc-select) for Echidna & Medusa: 12 | 13 | `solc-select install 0.8.23`\ 14 | `solc-select use 0.8.23` 15 | 16 | To compile this project: 17 | 18 | `forge build` 19 | 20 | Every exercise has a `basic` configuration and/or `advanced` fuzz configuration for Foundry, Echidna & Medusa. The `basic` configuration does not guide the fuzzer at all; it simply sets up the scenario and allows the fuzzer to do whatever it wants. The `advanced` configuration guides the fuzzer to the functions it should call and helps to eliminate invalid inputs which result in useless fuzz runs. 21 | 22 | ## Results ## 23 | 24 | ### Challenge #1 Naive Receiver: (Winner TIED ALL) ### 25 | 26 | In `basic` configuration Foundry, Echidna & Medusa are able to break the simpler invariant but not the more valuable and difficult one. In `advanced` configuration all 3 fuzzers can break both invariants. All 3 fuzzers reduce the exploit chain to a very concise & optimized transaction set and present this to the user in an easy to understand output. As a result they are tied and there is no clear winner. 27 | 28 | ### Challenge #2 Unstoppable: (Winner TIED ALL) ### 29 | 30 | All Fuzzers in `basic` configuration can break both invariants; Foundry appears to be the slightly faster. 31 | 32 | ### Challenge #3 Proposal: (Winner TIED ALL) ### 33 | 34 | Foundry, Echidna & Medusa in `basic` mode are able to easily break the invariant, resulting in a tie. 35 | 36 | ### Challenge #4 Voting NFT: (Winner TIED ALL) ### 37 | 38 | In `basic` configuration Foundry, Echidna & Medusa are all able to break the easier invariant but not the more difficult one. All Fuzzers are able to provide the user with a minimal transaction set to generate the exploit. Hence they are tied, there is no clear winner. 39 | 40 | ### Challenge #5 Token Sale: (Winner MEDUSA) ### 41 | 42 | In `basic` configuration Foundry & Echidna can only break the easier and more valuable invariant which leads to a Critical exploit but not the harder though less valuable invariant which leads to a High/Medium. However Medusa is able to almost immediately break both invariants in unguided `basic` mode, making Medusa the clear winner. 43 | 44 | ### Challenge #6 Rarely False: (Winner TIED HALMOS & CERTORA) ### 45 | 46 | Both Echidna & Foundry are unable to break the assertion in this stateless fuzzing challenge. Medusa [used](https://twitter.com/DevDacian/status/1732199452344221913) to be able to break it almost instantly but has [regressed](https://github.com/crytic/medusa/issues/305) in performance after recent changes and is now unable to break it. Halmos and Certora can break it so they are the winners. 47 | 48 | ### Challenge #7 Byte Battle: (Winner TIED ALL) 49 | 50 | All tools are able to quickly break this challenge. 51 | 52 | ### Challenge #8 Omni Protocol: (Winner MEDUSA) 53 | 54 | All 3 Fuzzers configured in `advanced` guided mode attempted to break 16 invariants on Beta Finance [Omni Protocol](https://github.com/beta-finance/Omni-Protocol). Medusa is typically able to break 2 invariants within 5 minutes (often much sooner on subsequent runs) though on the first run can take a bit longer. Echidna can sometimes break 1 invariant within 5 minutes and Foundry appears to never be able to break any invariants within 5 minutes. Hence Medusa is the clear winner. The fuzzers written for this challenge were [contributed](https://github.com/beta-finance/Omni-Protocol/pull/2) to Beta Finance. 55 | 56 | ### Challenge #9 -> #14 57 | 58 | Some additional solvers have been added based upon real-world findings from my private audits. 59 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | show_progress = true 6 | 7 | # include remappings 8 | remappings = [ 9 | "@openzeppelin/=lib/openzeppelin-contracts/", 10 | "@openzeppelin-upgradeable/=lib/openzeppelin-contracts-upgradeable/", 11 | "@chimera/=lib/chimera/src/", 12 | ] 13 | 14 | [fuzz] 15 | runs = 500 16 | max_test_rejects = 999999999 17 | 18 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options 19 | -------------------------------------------------------------------------------- /src/01-naive-receiver/FlashLoanReceiver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.23; 4 | 5 | import "@openzeppelin/contracts/utils/Address.sol"; 6 | 7 | /** 8 | * @title FlashLoanReceiver 9 | * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) 10 | */ 11 | contract FlashLoanReceiver { 12 | using Address for address payable; 13 | 14 | address payable private pool; 15 | 16 | constructor(address payable poolAddress) { 17 | pool = poolAddress; 18 | } 19 | 20 | // Function called by the pool during flash loan 21 | function receiveEther(uint256 fee) public payable { 22 | require(msg.sender == pool, "Sender must be pool"); 23 | 24 | uint256 amountToBeRepaid = msg.value + fee; 25 | 26 | require(address(this).balance >= amountToBeRepaid, "Cannot borrow that much"); 27 | 28 | _executeActionDuringFlashLoan(); 29 | 30 | // Return funds to pool 31 | pool.sendValue(amountToBeRepaid); 32 | } 33 | 34 | // Internal function where the funds received are used 35 | function _executeActionDuringFlashLoan() internal { } 36 | 37 | // Allow deposits of ETH 38 | receive () external payable {} 39 | } -------------------------------------------------------------------------------- /src/01-naive-receiver/NaiveReceiverLenderPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; 5 | import "@openzeppelin/contracts/utils/Address.sol"; 6 | 7 | /** 8 | * @title NaiveReceiverLenderPool 9 | * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) 10 | */ 11 | contract NaiveReceiverLenderPool is ReentrancyGuard { 12 | 13 | using Address for address; 14 | 15 | uint256 private constant FIXED_FEE = 1 ether; // not the cheapest flash loan 16 | 17 | function fixedFee() external pure returns (uint256) { 18 | return FIXED_FEE; 19 | } 20 | 21 | function flashLoan(address borrower, uint256 borrowAmount) external nonReentrant { 22 | 23 | uint256 balanceBefore = address(this).balance; 24 | require(balanceBefore >= borrowAmount, "Not enough ETH in pool"); 25 | 26 | 27 | require(borrower.code.length > 0, "Borrower must be a deployed contract"); 28 | // Transfer ETH and handle control to receiver 29 | borrower.functionCallWithValue( 30 | abi.encodeWithSignature( 31 | "receiveEther(uint256)", 32 | FIXED_FEE 33 | ), 34 | borrowAmount 35 | ); 36 | 37 | require( 38 | address(this).balance >= balanceBefore + FIXED_FEE, 39 | "Flash loan hasn't been paid back" 40 | ); 41 | } 42 | 43 | // Allow deposits of ETH 44 | receive () external payable {} 45 | } 46 | -------------------------------------------------------------------------------- /src/02-unstoppable/ReceiverUnstoppable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.23; 4 | 5 | import "./UnstoppableLender.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | 8 | /** 9 | * @title ReceiverUnstoppable 10 | * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) 11 | */ 12 | contract ReceiverUnstoppable { 13 | 14 | UnstoppableLender private immutable pool; 15 | address private immutable owner; 16 | 17 | constructor(address poolAddress) { 18 | pool = UnstoppableLender(poolAddress); 19 | owner = msg.sender; 20 | } 21 | 22 | // Pool will call this function during the flash loan 23 | function receiveTokens(address tokenAddress, uint256 amount) external { 24 | require(msg.sender == address(pool), "Sender must be pool"); 25 | // Return all tokens to the pool 26 | require(IERC20(tokenAddress).transfer(msg.sender, amount), "Transfer of tokens failed"); 27 | } 28 | 29 | function executeFlashLoan(uint256 amount) external { 30 | require(msg.sender == owner, "Only owner can execute flash loan"); 31 | pool.flashLoan(amount); 32 | } 33 | } -------------------------------------------------------------------------------- /src/02-unstoppable/UnstoppableLender.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.23; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; 7 | 8 | interface IReceiver { 9 | function receiveTokens(address tokenAddress, uint256 amount) external; 10 | } 11 | 12 | /** 13 | * @title UnstoppableLender 14 | * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) 15 | */ 16 | contract UnstoppableLender is ReentrancyGuard { 17 | 18 | IERC20 public immutable damnValuableToken; 19 | uint256 public poolBalance; 20 | 21 | constructor(address tokenAddress) { 22 | require(tokenAddress != address(0), "Token address cannot be zero"); 23 | damnValuableToken = IERC20(tokenAddress); 24 | } 25 | 26 | function depositTokens(uint256 amount) external nonReentrant { 27 | require(amount > 0, "Must deposit at least one token"); 28 | // Transfer token from sender. Sender must have first approved them. 29 | damnValuableToken.transferFrom(msg.sender, address(this), amount); 30 | poolBalance = poolBalance + amount; 31 | } 32 | 33 | function flashLoan(uint256 borrowAmount) external nonReentrant { 34 | require(borrowAmount > 0, "Must borrow at least one token"); 35 | 36 | uint256 balanceBefore = damnValuableToken.balanceOf(address(this)); 37 | require(balanceBefore >= borrowAmount, "Not enough tokens in pool"); 38 | 39 | // Ensured by the protocol via the `depositTokens` function 40 | assert(poolBalance == balanceBefore); 41 | 42 | damnValuableToken.transfer(msg.sender, borrowAmount); 43 | 44 | IReceiver(msg.sender).receiveTokens(address(damnValuableToken), borrowAmount); 45 | 46 | uint256 balanceAfter = damnValuableToken.balanceOf(address(this)); 47 | require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back"); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/06-rarely-false/RarelyFalse.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | // 4 | // 5 | // stateless tests in test/06-rarely-false 6 | // 7 | // placeholder so nothing else gets put in this folder -------------------------------------------------------------------------------- /src/07-byte-battle/ByteBattle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | // 4 | // 5 | // stateless tests in test/07-byte-battle 6 | // 7 | // placeholder so nothing else gets put in this folder -------------------------------------------------------------------------------- /src/08-omni-protocol/IRM.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.23; 3 | 4 | import "@openzeppelin/contracts/access/AccessControl.sol"; 5 | import "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; 6 | 7 | import "./interfaces/IIRM.sol"; 8 | 9 | /** 10 | * @title Interest Rate Model (IRM) Contract 11 | * @notice This contract defines the interest rate model for different markets and tranches. 12 | * @dev It inherits from the IIRM interface and the AccessControl contract from the OpenZeppelin library. 13 | * @dev It is important that contracts that integrate this IRM appropriately scale interest rate values. 14 | */ 15 | contract IRM is IIRM, AccessControl, Initializable { 16 | uint256 public constant UTILIZATION_SCALE = 1e9; 17 | uint256 public constant MAX_INTEREST_RATE = 10e9; // Scale must match OmniToken.sol, 1e9 18 | mapping(address => mapping(uint8 => IRMConfig)) public marketIRMConfigs; 19 | 20 | /** 21 | * @notice Initializes the admin role with the contract deployer/upgrader. 22 | * @param _admin The address of the multisig admin. 23 | */ 24 | function initialize(address _admin) external initializer { 25 | _grantRole(DEFAULT_ADMIN_ROLE, _admin); 26 | } 27 | 28 | /** 29 | * @notice Calculates the interest rate for a specific OmniToken market, tranche, total deposit and total borrow. 30 | * @param _market The address of the market 31 | * @param _tranche The tranche number 32 | * @param _totalDeposit The total amount deposited in the market 33 | * @param _totalBorrow The total amount borrowed from the market 34 | * @return The calculated interest rate 35 | */ 36 | function getInterestRate(address _market, uint8 _tranche, uint256 _totalDeposit, uint256 _totalBorrow) 37 | external 38 | view 39 | returns (uint256) 40 | { 41 | uint256 utilization; 42 | if (_totalBorrow <= _totalDeposit) { 43 | utilization = _totalDeposit == 0 ? 0 : (_totalBorrow * UTILIZATION_SCALE) / _totalDeposit; 44 | } else { 45 | utilization = UTILIZATION_SCALE; 46 | } 47 | return _getInterestRateLinear(marketIRMConfigs[_market][_tranche], utilization); 48 | } 49 | 50 | /** 51 | * @notice Internal function to calculate the interest rate linearly based on utilization and IRMConfig. 52 | * @param _config The IRM configuration structure 53 | * @param _utilization The current utilization rate 54 | * @return interestRate The calculated interest rate 55 | */ 56 | function _getInterestRateLinear(IRMConfig memory _config, uint256 _utilization) 57 | internal 58 | pure 59 | returns (uint256 interestRate) 60 | { 61 | if (_config.kink == 0) { 62 | revert("IRM::_getInterestRateLinear: Interest config not set."); 63 | } 64 | if (_utilization <= _config.kink) { 65 | interestRate = _config.start; 66 | interestRate += (_utilization * (_config.mid - _config.start)) / _config.kink; 67 | } else { 68 | interestRate = _config.mid; 69 | interestRate += 70 | ((_utilization - _config.kink) * (_config.end - _config.mid)) / (UTILIZATION_SCALE - _config.kink); 71 | } 72 | } 73 | 74 | /** 75 | * @notice Sets the IRM configuration for a specific OmniToken market and tranches. 76 | * @param _market The address of the market 77 | * @param _tranches An array of tranche numbers 78 | * @param _configs An array of IRMConfig configurations 79 | */ 80 | function setIRMForMarket(address _market, uint8[] calldata _tranches, IRMConfig[] calldata _configs) 81 | external 82 | onlyRole(DEFAULT_ADMIN_ROLE) 83 | { 84 | if (_tranches.length != _configs.length) { 85 | revert("IRM::setIRMForMarket: Tranches and configs length mismatch."); 86 | } 87 | for (uint256 i = 0; i < _tranches.length; ++i) { 88 | if (_configs[i].kink == 0 || _configs[i].kink >= UTILIZATION_SCALE) { 89 | revert("IRM::setIRMForMarket: Bad kink value."); 90 | } 91 | if ( 92 | _configs[i].start > _configs[i].mid || _configs[i].mid > _configs[i].end 93 | || _configs[i].end > MAX_INTEREST_RATE 94 | ) { 95 | revert("IRM::setIRMForMarket: Bad interest value."); 96 | } 97 | marketIRMConfigs[_market][_tranches[i]] = _configs[i]; 98 | } 99 | emit SetIRMForMarket(_market, _tranches, _configs); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/08-omni-protocol/SubAccount.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.23; 3 | 4 | /** 5 | * @title SubAccount 6 | * @notice This library provides utility functions to handle sub-accounts using bytes32 types, where id is most significant bytes. 7 | */ 8 | library SubAccount { 9 | /** 10 | * @notice Combines an address and a sub-account identifier into a bytes32 account representation. 11 | * @param _sender The address component. 12 | * @param _subId The sub-account identifier component. 13 | * @return A bytes32 representation of the account. 14 | */ 15 | function toAccount(address _sender, uint96 _subId) internal pure returns (bytes32) { 16 | return bytes32(uint256(uint160(_sender)) | (uint256(_subId) << 160)); 17 | } 18 | 19 | /** 20 | * @notice Extracts the address component from a bytes32 account representation. 21 | * @param _account The bytes32 representation of the account. 22 | * @return The address component. 23 | */ 24 | function toAddress(bytes32 _account) internal pure returns (address) { 25 | return address(uint160(uint256(_account))); 26 | } 27 | 28 | /** 29 | * @notice Extracts the sub-account identifier component from a bytes32 account representation. 30 | * @param _account The bytes32 representation of the account. 31 | * @return The sub-account identifier component. 32 | */ 33 | function toSubId(bytes32 _account) internal pure returns (uint96) { 34 | return uint96(uint256(_account) >> 160); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/08-omni-protocol/WETHGateway.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; 5 | 6 | import "./interfaces/IOmniToken.sol"; 7 | import "./interfaces/IWETH9.sol"; 8 | import "./interfaces/IWithUnderlying.sol"; 9 | import "./SubAccount.sol"; 10 | 11 | /** 12 | * @title WETHGateway 13 | * @notice Handles native ETH deposits directly to contract through WETH, but does not handle native ETH withdrawals. 14 | * @dev This contract serves as a gateway for handling deposits of native ETH, which are then wrapped into WETH tokens. 15 | */ 16 | contract WETHGateway is Initializable { 17 | using SubAccount for address; 18 | 19 | address public oweth; 20 | address public weth; 21 | uint96 private constant SUBACCOUNT_ID = 0; 22 | 23 | event Deposit(bytes32 indexed account, uint8 indexed trancheId, uint256 amount, uint256 share); 24 | 25 | /** 26 | * @notice Initializes the contract with the OWETH contract address. 27 | * @param _oweth The address of the OWETH contract. 28 | */ 29 | function initialize(address _oweth) external initializer { 30 | address _weth = IWithUnderlying(_oweth).underlying(); 31 | IWETH9(_weth).approve(_oweth, type(uint256).max); 32 | oweth = _oweth; 33 | weth = _weth; 34 | } 35 | 36 | /** 37 | * @notice Deposits native ETH to the contract, wraps it into WETH tokens, and handles the deposit operation 38 | * through the Omni Token contract. 39 | * @dev The function is payable to accept ETH deposits. 40 | * @param _subId The subscription ID related to the depositor's account. 41 | * @param _trancheId The identifier of the tranche where the deposit is occurring. 42 | * @return share The number of shares received in exchange for the deposited ETH. 43 | */ 44 | function deposit(uint96 _subId, uint8 _trancheId) external payable returns (uint256 share) { 45 | bytes32 to = msg.sender.toAccount(_subId); 46 | IWETH9(weth).deposit{value: msg.value}(); 47 | share = IOmniToken(oweth).deposit(SUBACCOUNT_ID, _trancheId, msg.value); 48 | IOmniToken(oweth).transfer(SUBACCOUNT_ID, to, _trancheId, share); 49 | emit Deposit(to, _trancheId, msg.value, share); 50 | } 51 | 52 | /** 53 | * @notice Fallback function that reverts if ETH is sent directly to the contract. 54 | * @dev Any attempts to send ETH directly to the contract will cause a transaction revert. 55 | */ 56 | receive() external payable { 57 | revert("This contract should not accept ETH directly."); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/08-omni-protocol/WithUnderlying.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.23; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; 7 | import "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; 8 | import "./interfaces/IWithUnderlying.sol"; 9 | 10 | /** 11 | * @title WithUnderlying 12 | * @notice A helper contract to handle the inflow and outflow of ERC20 tokens. 13 | * @dev Utilizes OpenZeppelin's SafeERC20 library to handle ERC20 transactions. 14 | */ 15 | abstract contract WithUnderlying is Initializable, IWithUnderlying { 16 | using SafeERC20 for IERC20; 17 | 18 | address public underlying; 19 | 20 | /** 21 | * @notice Initialies the abstract contract instance. 22 | * @param _underlying The address of the underlying ERC20 token. 23 | */ 24 | function __WithUnderlying_init(address _underlying) internal onlyInitializing { 25 | underlying = _underlying; 26 | } 27 | 28 | /** 29 | * @notice Retrieves the name of the token. 30 | * @return The name of the token, either prefixed from the underlying token or the default "Omni Token". 31 | */ 32 | function name() external view returns (string memory) { 33 | try IERC20Metadata(underlying).name() returns (string memory data) { 34 | return string(abi.encodePacked("Omni ", data)); 35 | } catch (bytes memory) { 36 | return "Omni Token"; 37 | } 38 | } 39 | 40 | /** 41 | * @notice Retrieves the symbol of the token. 42 | * @return The symbol of the token, either prefixed from the underlying token or the default "oToken". 43 | */ 44 | function symbol() external view returns (string memory) { 45 | try IERC20Metadata(underlying).symbol() returns (string memory data) { 46 | return string(abi.encodePacked("o", data)); 47 | } catch (bytes memory) { 48 | return "oToken"; 49 | } 50 | } 51 | 52 | /** 53 | * @notice Retrieves the number of decimals the token uses. 54 | * @return The number of decimals of the token, either from the underlying token or the default 18. 55 | */ 56 | function decimals() external view returns (uint8) { 57 | try IERC20Metadata(underlying).decimals() returns (uint8 data) { 58 | return data; 59 | } catch (bytes memory) { 60 | return 18; 61 | } 62 | } 63 | 64 | /** 65 | * @notice Handles the inflow of tokens to the contract. 66 | * @dev Transfers `_amount` tokens from `_from` to this contract and returns the actual amount received. 67 | * @param _from The address from which tokens are transferred. 68 | * @param _amount The amount of tokens to transfer. 69 | * @return The actual amount of tokens received by the contract. 70 | */ 71 | function _inflowTokens(address _from, uint256 _amount) internal returns (uint256) { 72 | uint256 balanceBefore = IERC20(underlying).balanceOf(address(this)); 73 | IERC20(underlying).safeTransferFrom(_from, address(this), _amount); 74 | uint256 balanceAfter = IERC20(underlying).balanceOf(address(this)); 75 | return balanceAfter - balanceBefore; 76 | } 77 | 78 | /** 79 | * @notice Handles the outflow of tokens from the contract. 80 | * @dev Transfers `_amount` tokens from this contract to `_to` and returns the actual amount sent. 81 | * @param _to The address to which tokens are transferred. 82 | * @param _amount The amount of tokens to transfer. 83 | * @return The actual amount of tokens sent from the contract. 84 | */ 85 | function _outflowTokens(address _to, uint256 _amount) internal returns (uint256) { 86 | uint256 balanceBefore = IERC20(underlying).balanceOf(address(this)); 87 | IERC20(underlying).safeTransfer(_to, _amount); 88 | uint256 balanceAfter = IERC20(underlying).balanceOf(address(this)); 89 | return balanceBefore - balanceAfter; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/08-omni-protocol/interfaces/IBandReference.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | interface IStdReference { 5 | /// A structure returned whenever someone requests for standard reference data. 6 | struct ReferenceData { 7 | uint256 rate; // base/quote exchange rate, multiplied by 1e18. 8 | uint256 lastUpdatedBase; // UNIX epoch of the last time when base price gets updated. 9 | uint256 lastUpdatedQuote; // UNIX epoch of the last time when quote price gets updated. 10 | } 11 | 12 | /// @dev Returns the price data for the given base/quote pair. Revert if not available. 13 | function getReferenceData(string memory _base, string memory _quote) external view returns (ReferenceData memory); 14 | 15 | /// @dev Similar to getReferenceData, but with multiple base/quote pairs at once. 16 | function getReferenceDataBulk(string[] memory _bases, string[] memory _quotes) 17 | external 18 | view 19 | returns (ReferenceData[] memory); 20 | } 21 | -------------------------------------------------------------------------------- /src/08-omni-protocol/interfaces/IChainlinkAggregator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | interface IChainlinkAggregator { 5 | function decimals() external view returns (uint8); 6 | 7 | function description() external view returns (string memory); 8 | 9 | function version() external view returns (uint256); 10 | 11 | function getRoundData(uint80 _roundId) 12 | external 13 | view 14 | returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); 15 | 16 | function latestRoundData() 17 | external 18 | view 19 | returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); 20 | } 21 | -------------------------------------------------------------------------------- /src/08-omni-protocol/interfaces/ICustomOmniOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | /** 5 | * @title ICustomOmniOracle Interface 6 | * @notice Interface for the custom oracle used by OmniOracle contract. 7 | */ 8 | interface ICustomOmniOracle { 9 | /** 10 | * @notice Fetches the price of the specified asset. 11 | * @param _underlying The address of the asset. 12 | * @return The price of the asset, normalized to 1e18. 13 | */ 14 | function getPrice(address _underlying) external view returns (uint256); 15 | } 16 | -------------------------------------------------------------------------------- /src/08-omni-protocol/interfaces/IIRM.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.23; 3 | 4 | /** 5 | * @title Interest Rate Model (IRM) Interface 6 | * @notice This interface describes the publicly accessible functions implemented by the IRM contract. 7 | */ 8 | interface IIRM { 9 | /// Events 10 | event SetIRMForMarket(address indexed market, uint8[] tranches, IRMConfig[] configs); 11 | 12 | /** 13 | * @notice This structure defines the configuration for the interest rate model. 14 | * @dev It contains the kink utilization point, and the interest rates at 0%, kink, and 100% utilization. 15 | */ 16 | struct IRMConfig { 17 | uint64 kink; // utilization at mid point (1e9 is 100%) 18 | uint64 start; // interest rate at 0% utlization 19 | uint64 mid; // interest rate at kink utlization 20 | uint64 end; // interest rate at 100% utlization 21 | } 22 | 23 | /** 24 | * @notice Calculates the interest rate for a specific market, tranche, total deposit, and total borrow. 25 | * @param _market The address of the market 26 | * @param _tranche The tranche number 27 | * @param _totalDeposit The total amount deposited in the market 28 | * @param _totalBorrow The total amount borrowed from the market 29 | * @return The calculated interest rate 30 | */ 31 | 32 | function getInterestRate(address _market, uint8 _tranche, uint256 _totalDeposit, uint256 _totalBorrow) 33 | external 34 | view 35 | returns (uint256); 36 | 37 | /** 38 | * @notice Sets the IRM configuration for a specific market and tranches. 39 | * @param _market The address of the market 40 | * @param _tranches An array of tranche numbers 41 | * @param _configs An array of IRMConfig structures 42 | */ 43 | function setIRMForMarket(address _market, uint8[] calldata _tranches, IRMConfig[] calldata _configs) external; 44 | } 45 | -------------------------------------------------------------------------------- /src/08-omni-protocol/interfaces/IOmniOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | /** 5 | * @title IOmniOracle Interface 6 | * @notice Interface for the OmniOracle contract. 7 | */ 8 | interface IOmniOracle { 9 | /// Events 10 | event SetOracle( 11 | address indexed underlying, 12 | address indexed oracle, 13 | Provider provider, 14 | uint32 delay, 15 | uint32 delayQuote, 16 | uint8 underlyingDecimals 17 | ); 18 | event RemoveOracle(address indexed underlying); 19 | 20 | /// Structs 21 | enum Provider { 22 | Invalid, 23 | Band, 24 | Chainlink, 25 | Other // Must implement the ICustomOmniOracle interface, use very carefully should return 1 full unit price multiplied by 1e18 26 | } 27 | 28 | struct OracleConfig { 29 | // One storage slot 30 | address oracleAddress; // 160 bits 31 | Provider provider; // 8 bits 32 | uint32 delay; // 32 bits, because this is time-based in unix 33 | uint32 delayQuote; // 32 bits, for Band quote delay 34 | uint8 underlyingDecimals; // 8 bits, decimals of underlying token 35 | } 36 | 37 | /** 38 | * @notice Fetches the price of the specified asset. 39 | * @param _underlying The address of the asset. 40 | * @return The price of the asset, normalized to 1e18. 41 | */ 42 | function getPrice(address _underlying) external view returns (uint256); 43 | } 44 | -------------------------------------------------------------------------------- /src/08-omni-protocol/interfaces/IOmniTokenBase.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.23; 3 | 4 | /** 5 | * @title IOmniTokenBase 6 | * @notice Base interface shared by the IOmniToken and IOmniTokenNoBorrow interfaces. 7 | */ 8 | interface IOmniTokenBase { 9 | /** 10 | * @notice Retrieves the total deposit amount for a specific account. 11 | * @param _account The account identifier. 12 | * @return The total deposit amount. 13 | */ 14 | function getAccountDepositInUnderlying(bytes32 _account) external view returns (uint256); 15 | 16 | /** 17 | * @notice Calculates the total deposited amount for a specific owner across sub-accounts. This funciton is for wallets and Etherscan to pick up balances. 18 | * @param _owner The address of the owner. 19 | * @return The total deposited amount. 20 | */ 21 | function balanceOf(address _owner) external view returns (uint256); 22 | 23 | /** 24 | * @notice Seizes funds from a user's account in the event of a liquidation. This is a priveleged function only callable by the OmniPool and must be implemented carefully. 25 | * @param _account The account from which funds will be seized. 26 | * @param _to The account to which seized funds will be sent. 27 | * @param _amount The amount of funds to seize. 28 | * @return The shares seized from each tranche. 29 | */ 30 | function seize(bytes32 _account, bytes32 _to, uint256 _amount) external returns (uint256[] memory); 31 | } 32 | -------------------------------------------------------------------------------- /src/08-omni-protocol/interfaces/IOmniTokenNoBorrow.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.23; 3 | 4 | import "./IOmniTokenBase.sol"; 5 | 6 | /** 7 | * @title IOmniTokenNoBorrow 8 | * @notice Interface for the OmniTokenNoBorrow contract which provides deposit and withdrawal features, without borrowing features. 9 | */ 10 | interface IOmniTokenNoBorrow is IOmniTokenBase { 11 | /// Events 12 | event Deposit(bytes32 indexed account, uint256 amount); 13 | event Withdraw(bytes32 indexed account, uint256 amount); 14 | event Seize(bytes32 indexed account, bytes32 indexed to, uint256 amount, uint256[] seizeShares); 15 | event SetSupplyCap(uint256 supplyCap); 16 | event Transfer(bytes32 indexed from, bytes32 indexed to, uint256 amount); 17 | 18 | /** 19 | * @notice Deposits a specified amount to the account. 20 | * @param _subId The sub-account identifier. 21 | * @param _amount The amount to deposit. 22 | * @return amount The actual amount deposited. 23 | */ 24 | function deposit(uint96 _subId, uint256 _amount) external returns (uint256 amount); 25 | 26 | /** 27 | * @notice Withdraws a specified amount from the account. 28 | * @param _subId The sub-account identifier. 29 | * @param _amount The amount to withdraw. 30 | * @return amount The actual amount withdrawn. 31 | */ 32 | function withdraw(uint96 _subId, uint256 _amount) external returns (uint256 amount); 33 | 34 | /** 35 | * @notice Transfers a specified amount of tokens from the sender's account to another account. 36 | * @param _subId The subscription ID associated with the sender's account. 37 | * @param _to The account identifier to which the tokens are being transferred. 38 | * @param _amount The amount of tokens to transfer. 39 | * @return A boolean value indicating whether the transfer was successful. 40 | */ 41 | function transfer(uint96 _subId, bytes32 _to, uint256 _amount) external returns (bool); 42 | 43 | /** 44 | * @notice Sets a new supply cap for the contract. 45 | * @param _supplyCap The new supply cap amount. 46 | */ 47 | function setSupplyCap(uint256 _supplyCap) external; 48 | } 49 | -------------------------------------------------------------------------------- /src/08-omni-protocol/interfaces/IWETH9.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.23; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | /// @title Interface for WETH9 7 | interface IWETH9 is IERC20 { 8 | /// @notice Deposit ether to get wrapped ether 9 | function deposit() external payable; 10 | 11 | /// @notice Withdraw wrapped ether to get ether 12 | function withdraw(uint256) external; 13 | } 14 | -------------------------------------------------------------------------------- /src/08-omni-protocol/interfaces/IWithUnderlying.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.23; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | /** 7 | * @title IWithUnderlying 8 | * @notice Interface for the WithUnderlying contract to handle the inflow and outflow of ERC20 tokens. 9 | */ 10 | interface IWithUnderlying { 11 | /** 12 | * @notice Gets the address of the underlying ERC20 token. 13 | * @return The address of the underlying ERC20 token. 14 | */ 15 | function underlying() external view returns (address); 16 | } 17 | -------------------------------------------------------------------------------- /src/08-omni-protocol/oracles/WstETHCustomOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.23; 3 | 4 | import "../interfaces/ICustomOmniOracle.sol"; 5 | import "../interfaces/IChainlinkAggregator.sol"; 6 | 7 | 8 | interface ILidoETH { 9 | function getPooledEthByShares(uint256 _sharesAmount) external view returns (uint256); 10 | } 11 | 12 | contract WstETHCustomOracle is ICustomOmniOracle { 13 | address public immutable stETH; 14 | address public immutable wstETH; 15 | address public immutable chainlinkStETHUSD; 16 | uint256 private constant MAX_DELAY = 1 days; 17 | 18 | /** 19 | * @notice Constructor for the WstETHCustomOracle 20 | * @param _stETH The address of the stETH contract. 21 | * @param _wstETH The address of the wstETH contract. 22 | * @param _chainlinkStETHUSD The address of the Chainlink aggregator contract. 23 | */ 24 | constructor(address _stETH, address _wstETH, address _chainlinkStETHUSD) { 25 | stETH = _stETH; 26 | wstETH = _wstETH; 27 | chainlinkStETHUSD = _chainlinkStETHUSD; 28 | } 29 | 30 | /** 31 | * @notice Fetches the price of the specified asset. 32 | * @param _underlying The address of the asset. 33 | * @return The price of the asset, normalized to 1e18. 34 | */ 35 | function getPrice(address _underlying) external view returns (uint256) { 36 | require(_underlying == wstETH, "Invalid address for oracle"); 37 | (, int256 stETHPrice,,uint256 updatedAt,) = IChainlinkAggregator(chainlinkStETHUSD).latestRoundData(); 38 | if (stETHPrice <= 0) return 0; 39 | require(updatedAt >= block.timestamp - MAX_DELAY, "Stale price for stETH"); 40 | 41 | uint256 stEthPerWstETH = ILidoETH(stETH).getPooledEthByShares(1e18); 42 | 43 | return (stEthPerWstETH * uint256(stETHPrice)) / (10 ** IChainlinkAggregator(chainlinkStETHUSD).decimals()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/09-vesting/Vesting.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | contract Vesting { 5 | uint24 public constant TOTAL_POINTS_PCT = 100_000; 6 | 7 | struct AllocationInput { 8 | address recipient; 9 | uint24 points; 10 | uint8 vestingWeeks; 11 | } 12 | 13 | struct AllocationData { 14 | uint24 points; 15 | uint8 vestingWeeks; 16 | bool claimed; 17 | } 18 | 19 | mapping(address recipient => AllocationData data) public allocations; 20 | 21 | constructor(AllocationInput[] memory allocInput) { 22 | uint256 inputLength = allocInput.length; 23 | require(inputLength > 0, "No allocations"); 24 | 25 | uint24 totalPoints; 26 | for(uint256 i; i= points, "Insufficient points"); 47 | require(!fromAllocation.claimed, "Already claimed"); 48 | 49 | AllocationData memory toAllocation = allocations[to]; 50 | require(!toAllocation.claimed, "Already claimed"); 51 | 52 | // enforce identical vesting periods if `to` has an active vesting period 53 | if(toAllocation.vestingWeeks != 0) { 54 | require(fromAllocation.vestingWeeks == toAllocation.vestingWeeks, "Vesting mismatch"); 55 | } 56 | 57 | allocations[msg.sender].points = fromAllocation.points - points; 58 | allocations[to].points = toAllocation.points + points; 59 | 60 | // if `to` had no active vesting period, copy from `from` 61 | if (toAllocation.vestingWeeks == 0) { 62 | allocations[to].vestingWeeks = fromAllocation.vestingWeeks; 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /src/10-vesting-ext/VestingExt.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | contract VestingExt { 5 | uint24 public constant TOTAL_POINTS_PCT = 100_000; 6 | uint256 public constant TOTAL_PRECLAIM_PCT = 100; 7 | uint256 public constant MAX_PRECLAIM_PCT = 10; 8 | uint96 public constant TOTAL_TOKEN_ALLOCATION = 1_000_000e18; 9 | 10 | struct AllocationInput { 11 | address recipient; 12 | uint24 points; 13 | uint8 vestingWeeks; 14 | } 15 | 16 | struct AllocationData { 17 | uint24 points; 18 | uint8 vestingWeeks; 19 | bool claimed; 20 | uint96 preclaimed; 21 | } 22 | 23 | mapping(address recipient => AllocationData data) public allocations; 24 | 25 | constructor(AllocationInput[] memory allocInput) { 26 | uint256 inputLength = allocInput.length; 27 | require(inputLength > 0, "No allocations"); 28 | 29 | uint24 totalPoints; 30 | for(uint256 i; i= points, "Insufficient points"); 52 | require(!fromAllocation.claimed, "Already claimed"); 53 | 54 | AllocationData memory toAllocation = allocations[to]; 55 | require(!toAllocation.claimed, "Already claimed"); 56 | 57 | // enforce identical vesting periods if `to` has an active vesting period 58 | if(toAllocation.vestingWeeks != 0) { 59 | require(fromAllocation.vestingWeeks == toAllocation.vestingWeeks, "Vesting mismatch"); 60 | } 61 | 62 | allocations[msg.sender].points = fromAllocation.points - points; 63 | allocations[to].points = toAllocation.points + points; 64 | 65 | // if `to` had no active vesting period, copy from `from` 66 | if (toAllocation.vestingWeeks == 0) { 67 | allocations[to].vestingWeeks = fromAllocation.vestingWeeks; 68 | } 69 | } 70 | 71 | // calculates how many tokens user is entitled to based on their points 72 | function getUserTokenAllocation(uint24 points) public pure returns(uint96 allocatedTokens) { 73 | allocatedTokens = (points * TOTAL_TOKEN_ALLOCATION) / TOTAL_POINTS_PCT; 74 | } 75 | 76 | // calculates max preclaimable token amount given a user's total allocated tokens 77 | function getUserMaxPreclaimable(uint96 allocatedTokens) public pure returns(uint96 maxPreclaimable) { 78 | // unsafe cast OK here 79 | maxPreclaimable 80 | = uint96(MAX_PRECLAIM_PCT * allocatedTokens/ TOTAL_PRECLAIM_PCT); 81 | } 82 | 83 | // allows users to preclaim part of their token allocation 84 | function preclaim() external returns(uint96 userPreclaimAmount) { 85 | AllocationData memory userAllocation = allocations[msg.sender]; 86 | 87 | require(!userAllocation.claimed, "Already claimed"); 88 | require(userAllocation.preclaimed == 0, "Already preclaimed"); 89 | 90 | userPreclaimAmount = getUserMaxPreclaimable(getUserTokenAllocation(userAllocation.points)); 91 | require(userPreclaimAmount > 0, "Zero preclaim amount"); 92 | 93 | allocations[msg.sender].preclaimed = userPreclaimAmount; 94 | } 95 | } -------------------------------------------------------------------------------- /src/11-op-reg/OperatorRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | contract OperatorRegistry { 5 | uint128 public numOperators; 6 | 7 | mapping(uint128 operatorId => address operatorAddress) public operatorIdToAddress; 8 | mapping(address operatorAddress => uint128 operatorId) public operatorAddressToId; 9 | 10 | // anyone can register their address as an operator 11 | function register() external returns(uint128 newOperatorId) { 12 | require(operatorAddressToId[msg.sender] == 0, "Address already registered"); 13 | 14 | newOperatorId = ++numOperators; 15 | 16 | operatorAddressToId[msg.sender] = newOperatorId; 17 | operatorIdToAddress[newOperatorId] = msg.sender; 18 | } 19 | 20 | // an operator can update their address 21 | function updateAddress(address newOperatorAddress) external { 22 | require(msg.sender != newOperatorAddress, "Updated address must be different"); 23 | 24 | uint128 operatorId = _getOperatorIdSafe(msg.sender); 25 | 26 | operatorAddressToId[newOperatorAddress] = operatorId; 27 | operatorIdToAddress[operatorId] = newOperatorAddress; 28 | 29 | delete operatorAddressToId[msg.sender]; 30 | } 31 | 32 | function _getOperatorIdSafe(address operatorAddress) internal view returns (uint128 operatorId) { 33 | operatorId = operatorAddressToId[operatorAddress]; 34 | 35 | require(operatorId != 0, "Operator not registered"); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/12-liquidate-dos/LiquidateDos.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; 5 | 6 | interface ILiquidateDos { 7 | error InvalidMarketId(); 8 | error UserAlreadyInMarket(); 9 | error LiquidationsDisabled(); 10 | error LiquidateUserNotInAnyMarkets(); 11 | } 12 | 13 | contract LiquidateDos is ILiquidateDos { 14 | using EnumerableSet for EnumerableSet.UintSet; 15 | 16 | // 10 possible markets for users to trade in 17 | uint8 public constant MIN_MARKET_ID = 1; 18 | uint8 public constant MAX_MARKET_ID = 10; 19 | 20 | bool liquidationsEnabled; 21 | 22 | // tracks open markets for each user 23 | mapping(address user => EnumerableSet.UintSet activeMarkets) userActiveMarkets; 24 | 25 | // users can only have 1 open position in each market 26 | function openPosition(uint8 marketId) external { 27 | if(marketId < MIN_MARKET_ID || marketId > MAX_MARKET_ID) revert InvalidMarketId(); 28 | 29 | if(!userActiveMarkets[msg.sender].add(marketId)) revert UserAlreadyInMarket(); 30 | } 31 | 32 | function toggleLiquidations(bool toggle) external { 33 | liquidationsEnabled = toggle; 34 | } 35 | 36 | function liquidate(address user) external { 37 | if(!liquidationsEnabled) revert LiquidationsDisabled(); 38 | 39 | uint8 userActiveMarketsNum = uint8(userActiveMarkets[user].length()); 40 | if(userActiveMarketsNum == 0) revert LiquidateUserNotInAnyMarkets(); 41 | 42 | // in our simple implementation users are always liquidated 43 | for(uint8 i; i MAX_COLLATERAL_ID) revert InvalidCollateralId(); 28 | 29 | if(!collateralPriority.add(collateralId)) revert CollateralAlreadyAdded(); 30 | } 31 | 32 | function removeCollateral(uint8 collateralId) external { 33 | if(collateralId < MIN_COLLATERAL_ID || collateralId > MAX_COLLATERAL_ID) revert InvalidCollateralId(); 34 | 35 | if(!collateralPriority.remove(collateralId)) revert CollateralNotAdded(); 36 | } 37 | 38 | function getCollateralAtPriority(uint8 index) external view returns(uint8 val) { 39 | if(index >= MAX_COLLATERAL_ID) revert InvalidIndex(); 40 | 41 | val = uint8(collateralPriority.at(index)); 42 | } 43 | 44 | function containsCollateral(uint8 collateralId) external view returns(bool result) { 45 | if(collateralId < MIN_COLLATERAL_ID || collateralId > MAX_COLLATERAL_ID) revert InvalidCollateralId(); 46 | 47 | result = collateralPriority.contains(collateralId); 48 | } 49 | 50 | function numCollateral() external view returns(uint256 length) { 51 | length = collateralPriority.length(); 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /src/MockERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.23; 3 | 4 | import "@openzeppelin/contracts/access/AccessControl.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | 7 | contract MockERC20 is AccessControl, ERC20 { 8 | bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); 9 | uint8 private __decimals = 18; 10 | 11 | constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol) { 12 | _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); 13 | _grantRole(MINTER_ROLE, msg.sender); 14 | } 15 | 16 | function decimals() public view override returns (uint8) { 17 | return __decimals; 18 | } 19 | 20 | function setDecimals(uint8 _decimals) external onlyRole(DEFAULT_ADMIN_ROLE) { 21 | __decimals = _decimals; 22 | } 23 | 24 | function mint(address _to, uint256 _value) external onlyRole(MINTER_ROLE) { 25 | _mint(_to, _value); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/TestToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract TestToken is ERC20 { 7 | 8 | uint8 immutable private s_decimals; 9 | 10 | // mint initial supply to msg.sender. Used in test 11 | // setups so test setup can then distribute initial 12 | // tokens to different participants 13 | constructor(uint256 initialMint, uint8 decimal) ERC20("TTKN", "TTKN") { 14 | _mint(msg.sender, initialMint); 15 | s_decimals = decimal; 16 | } 17 | 18 | function decimals() public view override returns (uint8) { 19 | return s_decimals; 20 | } 21 | } -------------------------------------------------------------------------------- /src/TestToken2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract TestToken2 is ERC20 { 7 | 8 | uint8 immutable private s_decimals; 9 | 10 | // mint initial supply to msg.sender. Used in test 11 | // setups so test setup can then distribute initial 12 | // tokens to different participants 13 | constructor(uint256 initialMint, uint8 decimal) ERC20("TTKN", "TTKN") { 14 | _mint(msg.sender, initialMint); 15 | s_decimals = decimal; 16 | } 17 | 18 | function decimals() public view override returns (uint8) { 19 | return s_decimals; 20 | } 21 | } -------------------------------------------------------------------------------- /test/01-naive-receiver/NaiveReceiverAdvancedEchidna.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import "./NaiveReceiverBasicEchidna.t.sol"; 5 | 6 | // configure solc-select to use compiler version: 7 | // solc-select use 0.8.23 8 | // 9 | // run from base project directory with: 10 | // echidna --config test/01-naive-receiver/NaiveReceiverAdvancedEchidna.yaml ./ --contract NaiveReceiverAdvancedEchidna 11 | // medusa --config test/01-naive-receiver/NaiveReceiverAdvancedMedusa.json fuzz 12 | contract NaiveReceiverAdvancedEchidna is NaiveReceiverBasicEchidna { 13 | 14 | // constructor has to be payable if balanceContract > 0 in yaml config 15 | constructor() payable NaiveReceiverBasicEchidna() { 16 | // advanced test with guiding of the fuzzer 17 | // 18 | // make this contract into a handler to wrap the pool's flashLoan() 19 | // function and instruct echidna to call it passing receiver's 20 | // address as the parameter. 21 | // 22 | // This is done in the yaml configuration file by setting 23 | // `allContracts: false` then creating a wrapper function in this 24 | // contract. With `allContracts: false` fuzzing will only call 25 | // functions in this or parent contracts. 26 | // 27 | // advanced echidna is able to break both invariants and find 28 | // much more simplified exploit chains than advanced foundry! 29 | } 30 | 31 | // wrapper around pool.flashLoan() to "guide" the fuzz test 32 | function flashLoanWrapper(uint256 borrowAmount) public { 33 | // instruct fuzzer to cap borrowAmount under pool's 34 | // available amount to prevent wasted runs 35 | // 36 | // commented out as echidna is faster at breaking the invariant 37 | // without this 38 | //borrowAmount = borrowAmount % INIT_ETH_POOL; 39 | 40 | // call underlying function being tested with the receiver address 41 | // to prevent wasted runs. Initially tried it with address as fuzz 42 | // input parameter but this was unable to break the harder invariant 43 | pool.flashLoan(address(receiver), borrowAmount); 44 | } 45 | 46 | // invariants inherited from base contract 47 | } 48 | -------------------------------------------------------------------------------- /test/01-naive-receiver/NaiveReceiverAdvancedEchidna.yaml: -------------------------------------------------------------------------------- 1 | # 1010 ether is placed in the echidna testing contract 2 | # which then transfers ether to contracts being tested 3 | # as part of setup in constructor. Constructor must be 4 | # payable! This value should be in 18 decimals 5 | balanceContract: 1010000000000000000000 6 | 7 | # Don't allow fuzzer to use public/external functions 8 | # from all contracts as advanced version wraps specific 9 | # functions to focus on 10 | allContracts: false 11 | 12 | # record fuzzer coverage to see what parts of the code 13 | # fuzzer executes 14 | corpusDir: "./test/01-naive-receiver/coverage-echidna-advanced" 15 | 16 | # use same prefix as Foundry invariant tests 17 | prefix: "invariant_" 18 | 19 | # instruct foundry to compile tests 20 | cryticArgs: ["--foundry-compile-all"] 21 | -------------------------------------------------------------------------------- /test/01-naive-receiver/NaiveReceiverAdvancedFoundry.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import "./NaiveReceiverBasicFoundry.t.sol"; 5 | 6 | // run from base project directory with: 7 | // forge test --match-contract NaiveReceiverAdvancedFoundry 8 | // 9 | // get coverage report (see https://medium.com/@rohanzarathustra/forge-coverage-overview-744d967e112f): 10 | // 1) forge coverage --report lcov --report-file test/01-naive-receiver/coverage-foundry-advanced.lcov --match-contract NaiveReceiverAdvancedFoundry 11 | // 2) genhtml test/01-naive-receiver/coverage-foundry-advanced.lcov -o test/01-naive-receiver/coverage-foundry-advanced 12 | // 3) open test/01-naive-receiver/coverage-foundry-advanced/index.html in your browser and 13 | // navigate to the relevant source file to see line-by-line execution records 14 | contract NaiveReceiverAdvancedFoundry is NaiveReceiverBasicFoundry { 15 | 16 | function setUp() public override { 17 | // call parent first to setup test environment 18 | super.setUp(); 19 | 20 | // advanced test with guiding of the fuzzer 21 | // 22 | // make this contract into a handler to wrap the pool's flashLoan() 23 | // function and instruct foundry to call it passing receiver's 24 | // address as the parameter. This significantly reduces 25 | // the amount of useless fuzz runs 26 | // 27 | // advanced foundry is able to break both invariants 28 | targetContract(address(this)); 29 | 30 | // functions to target during invariant tests 31 | bytes4[] memory selectors = new bytes4[](1); 32 | selectors[0] = this.flashLoanWrapper.selector; 33 | 34 | targetSelector(FuzzSelector({ 35 | addr: address(this), 36 | selectors: selectors 37 | })); 38 | } 39 | 40 | // wrapper around pool.flashLoan() to "guide" the fuzz test 41 | function flashLoanWrapper(uint256 borrowAmount) public { 42 | // instruct fuzzer to cap borrowAmount under pool's 43 | // available amount to prevent wasted runs 44 | vm.assume(borrowAmount <= INIT_ETH_POOL); 45 | 46 | // call underlying function being tested with the receiver address 47 | // to prevent wasted runs. Initially tried it with address as fuzz 48 | // input parameter but this was unable to break the harder invariant 49 | pool.flashLoan(address(receiver), borrowAmount); 50 | } 51 | 52 | // invariants inherited from base contract 53 | } 54 | -------------------------------------------------------------------------------- /test/01-naive-receiver/NaiveReceiverAdvancedMedusa.json: -------------------------------------------------------------------------------- 1 | { 2 | "fuzzing": { 3 | "workers": 10, 4 | "workerResetLimit": 50, 5 | "_COMMENT_TESTING_1": "changed timeout to limit fuzzing time", 6 | "timeout": 10, 7 | "testLimit": 0, 8 | "shrinkLimit": 500, 9 | "callSequenceLength": 100, 10 | "_COMMENT_TESTING_8": "added directory to store coverage data", 11 | "corpusDirectory": "coverage-medusa-basic", 12 | "coverageEnabled": true, 13 | "_COMMENT_TESTING_2": "added test contract to deploymentOrder", 14 | "targetContracts": ["NaiveReceiverAdvancedEchidna"], 15 | "targetContractsBalances": ["0x36c090d0ca68880000"], 16 | "constructorArgs": {}, 17 | "deployerAddress": "0x30000", 18 | "_COMMENT_TESTING_4": "changed senderAddresses to use permissionless attacker address", 19 | "senderAddresses": ["0x1337000000000000000000000000000000000000"], 20 | "blockNumberDelayMax": 60480, 21 | "blockTimestampDelayMax": 604800, 22 | "blockGasLimit": 125000000, 23 | "transactionGasLimit": 12500000, 24 | "testing": { 25 | "_COMMENT_TESTING_4": "stopOnFailedTest to false as there are 2 invariants to break", 26 | "stopOnFailedTest": false, 27 | "stopOnFailedContractMatching": true, 28 | "stopOnNoTests": true, 29 | "_COMMENT_TESTING_5": "changed testAllContracts to true", 30 | "testAllContracts": true, 31 | "traceAll": false, 32 | "assertionTesting": { 33 | "enabled": false, 34 | "testViewMethods": false, 35 | "panicCodeConfig": { 36 | "failOnCompilerInsertedPanic": false, 37 | "failOnAssertion": true, 38 | "failOnArithmeticUnderflow": false, 39 | "failOnDivideByZero": false, 40 | "failOnEnumTypeConversionOutOfBounds": false, 41 | "failOnIncorrectStorageAccess": false, 42 | "failOnPopEmptyArray": false, 43 | "failOnOutOfBoundsArrayAccess": false, 44 | "failOnAllocateTooMuchMemory": false, 45 | "failOnCallUninitializedVariable": false 46 | } 47 | }, 48 | "propertyTesting": { 49 | "enabled": true, 50 | "_COMMENT_TESTING_6": "changed prefix to use existing Echidna test files", 51 | "testPrefixes": [ 52 | "invariant_" 53 | ] 54 | }, 55 | "optimizationTesting": { 56 | "enabled": false, 57 | "testPrefixes": [ 58 | "optimize_" 59 | ] 60 | } 61 | }, 62 | "chainConfig": { 63 | "codeSizeCheckDisabled": true, 64 | "cheatCodes": { 65 | "cheatCodesEnabled": true, 66 | "enableFFI": false 67 | } 68 | } 69 | }, 70 | "compilation": { 71 | "platform": "crytic-compile", 72 | "platformConfig": { 73 | "_COMMENT_TESTING_7": "changed target to point to main directory where command is run from", 74 | "target": "./../../.", 75 | "solcVersion": "", 76 | "exportDirectory": "", 77 | "args": ["--foundry-compile-all"] 78 | } 79 | }, 80 | "logging": { 81 | "level": "info", 82 | "logDirectory": "" 83 | } 84 | } -------------------------------------------------------------------------------- /test/01-naive-receiver/NaiveReceiverBasicEchidna.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import "../../src/01-naive-receiver/NaiveReceiverLenderPool.sol"; 5 | import "../../src/01-naive-receiver/FlashLoanReceiver.sol"; 6 | import "@openzeppelin/contracts/utils/Address.sol"; 7 | 8 | // configure solc-select to use compiler version: 9 | // solc-select use 0.8.23 10 | // 11 | // run from base project directory with: 12 | // echidna --config test/01-naive-receiver/NaiveReceiverBasicEchidna.yaml ./ --contract NaiveReceiverBasicEchidna 13 | // medusa --config test/01-naive-receiver/NaiveReceiverBasicMedusa.json fuzz 14 | contract NaiveReceiverBasicEchidna { 15 | using Address for address payable; 16 | 17 | // initial eth flash loan pool 18 | uint256 constant INIT_ETH_POOL = 1000e18; 19 | // initial eth flash loan receiver 20 | uint256 constant INIT_ETH_RECEIVER = 10e18; 21 | 22 | // contracts required for test 23 | NaiveReceiverLenderPool pool; 24 | FlashLoanReceiver receiver; 25 | 26 | // constructor has to be payable if balanceContract > 0 in yaml config 27 | constructor() payable { 28 | // create contracts to be tested 29 | pool = new NaiveReceiverLenderPool(); 30 | receiver = new FlashLoanReceiver(payable(address(pool))); 31 | 32 | // set their initial eth balances by sending them ether. This contract 33 | // starts with `balanceContract` defined in yaml config 34 | payable(address(pool)).sendValue(INIT_ETH_POOL); 35 | payable(address(receiver)).sendValue(INIT_ETH_RECEIVER); 36 | 37 | // basic test with no advanced guiding of the fuzzer 38 | // echidna doesn't tell us how many fuzz runs reverted 39 | // 40 | // echidna is able to break invariant 2) but not 1) 41 | } 42 | 43 | // two possible invariants in order of importance: 44 | // 45 | // 1) receiver's balance is not 0 46 | // breaking this invariant is very valuable but much harder 47 | function invariant_receiver_balance_not_zero() public view returns (bool) { 48 | return(address(receiver).balance != 0); 49 | } 50 | 51 | // 2) receiver's balance is not less than starting balance 52 | // breaking this invariant is less valuable but much easier 53 | function invariant_receiver_balance_not_less_initial() public view returns (bool) { 54 | return(address(receiver).balance >= INIT_ETH_RECEIVER); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /test/01-naive-receiver/NaiveReceiverBasicEchidna.yaml: -------------------------------------------------------------------------------- 1 | # 1010 ether is placed in the echidna testing contract 2 | # which then transfers ether to contract's being tested 3 | # as part of setup in constructor. Constructor must be 4 | # payable! This value should be in 18 decimals 5 | balanceContract: 1010000000000000000000 6 | 7 | # Allow fuzzer to use public/external functions from all contracts 8 | allContracts: true 9 | 10 | # record fuzzer coverage to see what parts of the code 11 | # fuzzer executes 12 | corpusDir: "./test/01-naive-receiver/coverage-echidna-basic" 13 | 14 | # use same prefix as Foundry invariant tests 15 | prefix: "invariant_" 16 | 17 | # instruct foundry to compile tests 18 | cryticArgs: ["--foundry-compile-all"] 19 | -------------------------------------------------------------------------------- /test/01-naive-receiver/NaiveReceiverBasicFoundry.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import "../../src/01-naive-receiver/NaiveReceiverLenderPool.sol"; 5 | import "../../src/01-naive-receiver/FlashLoanReceiver.sol"; 6 | 7 | import "forge-std/Test.sol"; 8 | 9 | // run from base project directory with: 10 | // forge test --match-contract NaiveReceiverBasicFoundry 11 | // 12 | // get coverage report (see https://medium.com/@rohanzarathustra/forge-coverage-overview-744d967e112f): 13 | // 1) forge coverage --report lcov --report-file test/01-naive-receiver/coverage-foundry-basic.lcov --match-contract NaiveReceiverBasicFoundry 14 | // 2) genhtml test/01-naive-receiver/coverage-foundry-basic.lcov -o test/01-naive-receiver/coverage-foundry-basic 15 | // 3) open test/01-naive-receiver/coverage-foundry-basic/index.html in your browser and 16 | // navigate to the relevant source file to see line-by-line execution records 17 | contract NaiveReceiverBasicFoundry is Test { 18 | 19 | // initial eth flash loan pool 20 | uint256 constant INIT_ETH_POOL = 1000e18; 21 | // initial eth flash loan receiver 22 | uint256 constant INIT_ETH_RECEIVER = 10e18; 23 | 24 | // contracts required for test 25 | NaiveReceiverLenderPool pool; 26 | FlashLoanReceiver receiver; 27 | 28 | function setUp() public virtual { 29 | // setup contracts to be tested 30 | pool = new NaiveReceiverLenderPool(); 31 | receiver = new FlashLoanReceiver(payable(address(pool))); 32 | 33 | // set their initial eth balances 34 | deal(address(pool), INIT_ETH_POOL); 35 | deal(address(receiver), INIT_ETH_RECEIVER); 36 | 37 | // basic test with no advanced guiding of the fuzzer 38 | // most of the fuzz runs revert and are useless 39 | // 40 | // basic foundry is able to break invariant 2) but not 1) 41 | } 42 | 43 | // two possible invariants in order of importance: 44 | // 45 | // 1) receiver's balance is not 0 46 | // breaking this invariant is very valuable but much harder 47 | function invariant_receiver_balance_not_zero() public view { 48 | assert(address(receiver).balance != 0); 49 | } 50 | 51 | // 2) receiver's balance is not less than starting balance 52 | // breaking this invariant is less valuable but much easier 53 | function invariant_receiver_balance_not_less_initial() public view { 54 | assert(address(receiver).balance >= INIT_ETH_RECEIVER); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /test/01-naive-receiver/NaiveReceiverBasicMedusa.json: -------------------------------------------------------------------------------- 1 | { 2 | "fuzzing": { 3 | "workers": 10, 4 | "workerResetLimit": 50, 5 | "_COMMENT_TESTING_1": "changed timeout to limit fuzzing time", 6 | "timeout": 10, 7 | "testLimit": 0, 8 | "shrinkLimit": 5000, 9 | "callSequenceLength": 100, 10 | "_COMMENT_TESTING_8": "added directory to store coverage data", 11 | "corpusDirectory": "coverage-medusa-basic", 12 | "coverageEnabled": true, 13 | "_COMMENT_TESTING_2": "added test contract to deploymentOrder", 14 | "targetContracts": ["NaiveReceiverBasicEchidna"], 15 | "predeployedContracts": {}, 16 | "targetContractsBalances": ["0x36c090d0ca68880000"], 17 | "constructorArgs": {}, 18 | "deployerAddress": "0x30000", 19 | "_COMMENT_TESTING_4": "changed senderAddresses to use permissionless attacker address", 20 | "senderAddresses": ["0x1337000000000000000000000000000000000000"], 21 | "blockNumberDelayMax": 60480, 22 | "blockTimestampDelayMax": 604800, 23 | "blockGasLimit": 125000000, 24 | "transactionGasLimit": 12500000, 25 | "testing": { 26 | "_COMMENT_TESTING_4": "stopOnFailedTest to false as there are 2 invariants to break", 27 | "stopOnFailedTest": false, 28 | "stopOnFailedContractMatching": true, 29 | "stopOnNoTests": true, 30 | "_COMMENT_TESTING_5": "changed testAllContracts to true", 31 | "testAllContracts": true, 32 | "traceAll": false, 33 | "assertionTesting": { 34 | "enabled": false, 35 | "testViewMethods": false, 36 | "assertionModes": { 37 | "failOnCompilerInsertedPanic": false, 38 | "failOnAssertion": true, 39 | "failOnArithmeticUnderflow": false, 40 | "failOnDivideByZero": false, 41 | "failOnEnumTypeConversionOutOfBounds": false, 42 | "failOnIncorrectStorageAccess": false, 43 | "failOnPopEmptyArray": false, 44 | "failOnOutOfBoundsArrayAccess": false, 45 | "failOnAllocateTooMuchMemory": false, 46 | "failOnCallUninitializedVariable": false 47 | } 48 | }, 49 | "propertyTesting": { 50 | "enabled": true, 51 | "_COMMENT_TESTING_6": "changed prefix to use existing Echidna test files", 52 | "testPrefixes": [ 53 | "invariant_" 54 | ] 55 | }, 56 | "optimizationTesting": { 57 | "enabled": false, 58 | "testPrefixes": [ 59 | "optimize_" 60 | ] 61 | }, 62 | "targetFunctionSignatures": [], 63 | "excludeFunctionSignatures": [] 64 | }, 65 | "chainConfig": { 66 | "codeSizeCheckDisabled": true, 67 | "cheatCodes": { 68 | "cheatCodesEnabled": true, 69 | "enableFFI": false 70 | } 71 | } 72 | }, 73 | "compilation": { 74 | "platform": "crytic-compile", 75 | "platformConfig": { 76 | "_COMMENT_TESTING_7": "changed target to point to main directory where command is run from", 77 | "target": "./../../.", 78 | "solcVersion": "", 79 | "exportDirectory": "", 80 | "args": ["--foundry-compile-all"] 81 | } 82 | }, 83 | "logging": { 84 | "level": "info", 85 | "logDirectory": "" 86 | } 87 | } -------------------------------------------------------------------------------- /test/02-unstoppable/UnstoppableBasicEchidna.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import "../../src/02-unstoppable/UnstoppableLender.sol"; 5 | import "../../src/02-unstoppable/ReceiverUnstoppable.sol"; 6 | 7 | import "../../src/TestToken.sol"; 8 | 9 | // configure solc-select to use compiler version: 10 | // solc-select use 0.8.23 11 | // 12 | // run from base project directory with: 13 | // echidna --config test/02-unstoppable/UnstoppableBasicEchidna.yaml ./ --contract UnstoppableBasicEchidna 14 | // medusa --config test/02-unstoppable/UnstoppableBasicMedusa.json fuzz 15 | contract UnstoppableBasicEchidna { 16 | 17 | // initial tokens in pool 18 | uint256 constant INIT_TOKENS_POOL = 1000000e18; 19 | // initial tokens attacker 20 | uint256 constant INIT_TOKENS_ATTACKER = 100e18; 21 | 22 | // contracts required for test 23 | ERC20 token; 24 | UnstoppableLender pool; 25 | ReceiverUnstoppable receiver; 26 | address attacker = address(0x1337000000000000000000000000000000000000); 27 | 28 | // constructor has to be payable if balanceContract > 0 in yaml config 29 | constructor() payable { 30 | // setup contracts to be tested 31 | token = new TestToken(INIT_TOKENS_POOL + INIT_TOKENS_ATTACKER, 18); 32 | pool = new UnstoppableLender(address(token)); 33 | receiver = new ReceiverUnstoppable(payable(address(pool))); 34 | 35 | // transfer deposit initial tokens into pool 36 | token.approve(address(pool), INIT_TOKENS_POOL); 37 | pool.depositTokens(INIT_TOKENS_POOL); 38 | 39 | // transfer remaining tokens to the attacker 40 | token.transfer(attacker, INIT_TOKENS_ATTACKER); 41 | 42 | // attacker configured as msg.sender in yaml config 43 | } 44 | 45 | // invariant #1 very generic but Echidna can still break it even 46 | // if this is the only invariant 47 | function invariant_receiver_can_take_flash_loan() public returns (bool) { 48 | receiver.executeFlashLoan(10); 49 | return true; 50 | } 51 | 52 | // invariant #2 is more specific and Echidna can easily break it 53 | function invariant_pool_bal_equal_token_pool_bal() public view returns(bool) { 54 | return(pool.poolBalance() == token.balanceOf(address(pool))); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/02-unstoppable/UnstoppableBasicEchidna.yaml: -------------------------------------------------------------------------------- 1 | # no initial eth required 2 | balanceContract: 0 3 | 4 | # increase test limit 5 | testLimit: 100000 6 | 7 | # Allow fuzzer to use public/external functions from all contracts 8 | allContracts: true 9 | 10 | # specify address to use for fuzz transactions 11 | sender: ["0x1337000000000000000000000000000000000000"] 12 | 13 | # record fuzzer coverage to see what parts of the code 14 | # fuzzer executes 15 | corpusDir: "./test/02-unstoppable/coverage-echidna-basic" 16 | 17 | # use same prefix as Foundry invariant tests 18 | prefix: "invariant_" 19 | 20 | # instruct foundry to compile tests 21 | cryticArgs: ["--foundry-compile-all"] -------------------------------------------------------------------------------- /test/02-unstoppable/UnstoppableBasicFoundry.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import "../../src/02-unstoppable/UnstoppableLender.sol"; 5 | import "../../src/02-unstoppable/ReceiverUnstoppable.sol"; 6 | import "../../src/TestToken.sol"; 7 | 8 | import "forge-std/Test.sol"; 9 | 10 | // run from base project directory with: 11 | // forge test --match-contract UnstoppableBasicFoundry 12 | // 13 | // get coverage report (see https://medium.com/@rohanzarathustra/forge-coverage-overview-744d967e112f): 14 | // 1) forge coverage --report lcov --report-file test/02-unstoppable/coverage-foundry-basic.lcov --match-contract UnstoppableBasicFoundry 15 | // 2) genhtml test/02-unstoppable/coverage-foundry-basic.lcov -o test/02-unstoppable/coverage-foundry-basic 16 | // 3) open test/02-unstoppable/coverage-foundry-basic/index.html in your browser and 17 | // navigate to the relevant source file to see line-by-line execution records 18 | contract UnstoppableBasicFoundry is Test { 19 | 20 | // initial tokens in pool 21 | uint256 constant INIT_TOKENS_POOL = 1000000e18; 22 | // initial tokens attacker 23 | uint256 constant INIT_TOKENS_ATTACKER = 100e18; 24 | 25 | // contracts required for test 26 | ERC20 token; 27 | UnstoppableLender pool; 28 | ReceiverUnstoppable receiver; 29 | address attacker = address(0x1337); 30 | 31 | function setUp() public virtual { 32 | // setup contracts to be tested 33 | token = new TestToken(INIT_TOKENS_POOL + INIT_TOKENS_ATTACKER, 18); 34 | pool = new UnstoppableLender(address(token)); 35 | receiver = new ReceiverUnstoppable(payable(address(pool))); 36 | 37 | // transfer deposit initial tokens into pool 38 | token.approve(address(pool), INIT_TOKENS_POOL); 39 | pool.depositTokens(INIT_TOKENS_POOL); 40 | 41 | // transfer remaining tokens to the attacker 42 | token.transfer(attacker, INIT_TOKENS_ATTACKER); 43 | 44 | // only one attacker 45 | targetSender(attacker); 46 | } 47 | 48 | // invariant #1 very generic, harder to break 49 | function invariant_receiver_can_take_flash_loan() public { 50 | receiver.executeFlashLoan(10); 51 | assert(true); 52 | } 53 | 54 | // invariant #2 more specific, should be easier to break 55 | function invariant_pool_bal_equal_token_pool_bal() public view { 56 | assert(pool.poolBalance() == token.balanceOf(address(pool))); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/02-unstoppable/UnstoppableBasicMedusa.json: -------------------------------------------------------------------------------- 1 | { 2 | "fuzzing": { 3 | "workers": 10, 4 | "workerResetLimit": 50, 5 | "_COMMENT_TESTING_1": "changed timeout to limit fuzzing time", 6 | "timeout": 10, 7 | "testLimit": 0, 8 | "shrinkLimit": 500, 9 | "callSequenceLength": 100, 10 | "_COMMENT_TESTING_8": "added directory to store coverage data", 11 | "corpusDirectory": "coverage-medusa-basic", 12 | "coverageEnabled": true, 13 | "_COMMENT_TESTING_2": "added test contract to deploymentOrder", 14 | "targetContracts": ["UnstoppableBasicEchidna"], 15 | "predeployedContracts": {}, 16 | "targetContractsBalances": [], 17 | "constructorArgs": {}, 18 | "deployerAddress": "0x30000", 19 | "_COMMENT_TESTING_3": "changed senderAddresses to use custom senders", 20 | "senderAddresses": ["0x1337000000000000000000000000000000000000"], 21 | "blockNumberDelayMax": 60480, 22 | "blockTimestampDelayMax": 604800, 23 | "blockGasLimit": 125000000, 24 | "transactionGasLimit": 12500000, 25 | "testing": { 26 | "_COMMENT_TESTING_4": "stopOnFailedTest to false as there are 2 invariants to break", 27 | "stopOnFailedTest": false, 28 | "stopOnFailedContractMatching": true, 29 | "stopOnNoTests": true, 30 | "_COMMENT_TESTING_5": "changed testAllContracts to true", 31 | "testAllContracts": true, 32 | "traceAll": false, 33 | "assertionTesting": { 34 | "enabled": false, 35 | "testViewMethods": false, 36 | "panicCodeConfig": { 37 | "failOnCompilerInsertedPanic": false, 38 | "failOnAssertion": true, 39 | "failOnArithmeticUnderflow": false, 40 | "failOnDivideByZero": false, 41 | "failOnEnumTypeConversionOutOfBounds": false, 42 | "failOnIncorrectStorageAccess": false, 43 | "failOnPopEmptyArray": false, 44 | "failOnOutOfBoundsArrayAccess": false, 45 | "failOnAllocateTooMuchMemory": false, 46 | "failOnCallUninitializedVariable": false 47 | } 48 | }, 49 | "propertyTesting": { 50 | "enabled": true, 51 | "_COMMENT_TESTING_6": "changed prefix to use existing Echidna test files", 52 | "testPrefixes": [ 53 | "invariant_" 54 | ] 55 | }, 56 | "optimizationTesting": { 57 | "enabled": false, 58 | "testPrefixes": [ 59 | "optimize_" 60 | ] 61 | }, 62 | "targetFunctionSignatures": [], 63 | "excludeFunctionSignatures": [] 64 | }, 65 | "chainConfig": { 66 | "codeSizeCheckDisabled": true, 67 | "cheatCodes": { 68 | "cheatCodesEnabled": true, 69 | "enableFFI": false 70 | } 71 | } 72 | }, 73 | "compilation": { 74 | "platform": "crytic-compile", 75 | "platformConfig": { 76 | "_COMMENT_TESTING_7": "changed target to point to main directory where command is run from", 77 | "target": "./../../.", 78 | "solcVersion": "", 79 | "exportDirectory": "", 80 | "args": ["--foundry-compile-all"] 81 | } 82 | }, 83 | "logging": { 84 | "level": "info", 85 | "logDirectory": "" 86 | } 87 | } -------------------------------------------------------------------------------- /test/02-unstoppable/certora.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "src/02-unstoppable/ReceiverUnstoppable.sol", 4 | "src/02-unstoppable/UnstoppableLender.sol", 5 | "src/TestToken.sol" 6 | ], 7 | "verify": "ReceiverUnstoppable:test/02-unstoppable/certora.spec", 8 | "link": [ 9 | "ReceiverUnstoppable:pool=UnstoppableLender", 10 | "UnstoppableLender:damnValuableToken=TestToken" 11 | ], 12 | "packages":[ 13 | "@openzeppelin=lib/openzeppelin-contracts" 14 | ], 15 | "optimistic_loop": true 16 | } -------------------------------------------------------------------------------- /test/02-unstoppable/certora.spec: -------------------------------------------------------------------------------- 1 | // run from base folder: 2 | // certoraRun test/02-unstoppable/certora.conf 3 | using ReceiverUnstoppable as receiver; 4 | using UnstoppableLender as lender; 5 | using TestToken as token; 6 | 7 | methods { 8 | // `dispatcher` summary to prevent HAVOC 9 | function _.receiveTokens(address tokenAddress, uint256 amount) external => DISPATCHER(true); 10 | 11 | // `envfree` definitions to call functions without explicit `env` 12 | function token.balanceOf(address) external returns (uint256) envfree; 13 | } 14 | 15 | // executeFlashLoan() -> f() -> executeFlashLoan() should always succeed 16 | rule executeFlashLoan_mustNotRevert(uint256 loanAmount) { 17 | // enforce valid msg.sender: 18 | // 1) not a protocol contract 19 | // 2) equal to ReceiverUnstoppable::owner 20 | env e1; 21 | require e1.msg.sender != currentContract && 22 | e1.msg.sender != lender && 23 | e1.msg.sender != receiver && 24 | e1.msg.sender != token && 25 | e1.msg.sender == receiver.owner && 26 | e1.msg.value == 0; // not payable 27 | 28 | // enforce sufficient tokens exist to take out flash loan 29 | require loanAmount > 0 && loanAmount <= token.balanceOf(lender); 30 | 31 | // first executeFlashLoan() succeeds 32 | executeFlashLoan(e1, loanAmount); 33 | 34 | // perform another arbitrary successful transaction f() 35 | env e2; 36 | require e2.msg.sender != currentContract && 37 | e2.msg.sender != lender && 38 | e2.msg.sender != receiver && 39 | e2.msg.sender != token; 40 | method f; 41 | calldataarg args; 42 | f(e2, args); 43 | 44 | // second executeFlashLoan() should always succeed; there should 45 | // exist no previous transaction f() that could make it fail 46 | executeFlashLoan@withrevert(e1, loanAmount); 47 | assert !lastReverted; 48 | } -------------------------------------------------------------------------------- /test/03-proposal/Properties.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import {Asserts} from "@chimera/Asserts.sol"; 5 | import {Setup} from "./Setup.sol"; 6 | 7 | abstract contract Properties is Setup, Asserts { 8 | 9 | // event to raise if invariant broken to see interesting state 10 | event ProposalBalance(uint256 balance); 11 | 12 | // once the proposal has completed, all the eth should be distributed 13 | // either to the owner if the proposal failed or to the winners if 14 | // the proposal succeeded. no eth should remain forever stuck in the 15 | // contract 16 | function property_proposal_complete_all_rewards_distributed() public returns(bool) { 17 | uint256 proposalBalance = address(prop).balance; 18 | 19 | // only visible when invariant fails 20 | emit ProposalBalance(proposalBalance); 21 | 22 | return( 23 | // either proposal is active and contract balance > 0 24 | (prop.isActive() && proposalBalance > 0) || 25 | 26 | // or proposal is not active and contract balance == 0 27 | (!prop.isActive() && proposalBalance == 0) 28 | ); 29 | } 30 | } -------------------------------------------------------------------------------- /test/03-proposal/ProposalCryticTester.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import {Properties} from "./Properties.sol"; 5 | import {CryticAsserts} from "@chimera/CryticAsserts.sol"; 6 | 7 | // run from base project directory with: 8 | // echidna --config test/03-proposal/echidna.yaml ./ --contract ProposalCryticTester 9 | // medusa --config test/03-proposal/medusa.json fuzz 10 | contract ProposalCryticTester is Properties, CryticAsserts { 11 | constructor() payable { 12 | setup(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/03-proposal/ProposalCryticTesterToFoundry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import {Properties} from "./Properties.sol"; 5 | import {FoundryAsserts} from "@chimera/FoundryAsserts.sol"; 6 | import {Test} from "forge-std/Test.sol"; 7 | 8 | // run from base project directory with: 9 | // forge test --match-contract ProposalCryticTesterToFoundry -vvv 10 | // 11 | // get coverage report (see https://medium.com/@rohanzarathustra/forge-coverage-overview-744d967e112f): 12 | // 1) forge coverage --report lcov --report-file test/03-proposal/coverage-foundry.lcov --match-contract ProposalCryticTesterToFoundry 13 | // 2) genhtml test/03-proposal/coverage-foundry.lcov -o test/03-proposal/coverage-foundry 14 | // 3) open test/03-proposal/coverage-foundry/index.html in your browser and 15 | // navigate to the relevant source file to see line-by-line execution records 16 | contract ProposalCryticTesterToFoundry is Test, Properties, FoundryAsserts { 17 | function setUp() public virtual { 18 | setup(); 19 | 20 | // constrain fuzz test senders to the set of allowed voting addresses 21 | for(uint256 i; i= min_funding OR 15 | // 2) not active with balance == 0 16 | invariant proposal_complete_all_rewards_distributed() 17 | (isActive() && nativeBalances[currentContract] >= MIN_FUNDING()) || 18 | (!isActive() && nativeBalances[currentContract] == 0) 19 | { 20 | // enforce state requirements to prevent HAVOC into invalid state 21 | preserved { 22 | // enforce valid total allowed voters 23 | require(currentContract.s_totalAllowedVoters >= MIN_VOTERS() && 24 | currentContract.s_totalAllowedVoters <= MAX_VOTERS() && 25 | // must be odd number 26 | currentContract.s_totalAllowedVoters % 2 == 1); 27 | 28 | // enforce valid for/against votes matches total current votes 29 | require(currentContract.s_votersFor.length + 30 | currentContract.s_votersAgainst.length 31 | == currentContract.s_totalCurrentVotes); 32 | 33 | // enforce that when a proposal is active, the total number of current 34 | // votes must be at maximum half the total allowed voters, since proposal 35 | // is automatically finalized once >= 51% votes are cast 36 | require(!isActive() || 37 | (isActive() && 38 | currentContract.s_totalCurrentVotes <= currentContract.s_totalAllowedVoters/2) 39 | ); 40 | } 41 | } -------------------------------------------------------------------------------- /test/03-proposal/echidna.yaml: -------------------------------------------------------------------------------- 1 | # 10 ether is placed in the echidna testing contract 2 | # which then transfers ether to contracts being tested 3 | # as part of setup in constructor. Constructor must be 4 | # payable! This value should be in 18 decimals 5 | balanceContract: 10000000000000000000 6 | 7 | # Allow fuzzer to use public/external functions from all contracts 8 | allContracts: true 9 | 10 | # specify address to use for fuzz transations 11 | # limit this to the allowed voting addresses 12 | sender: ["0x1000000000000000000000000000000000000000", "0x2000000000000000000000000000000000000000", "0x3000000000000000000000000000000000000000", "0x4000000000000000000000000000000000000000", "0x5000000000000000000000000000000000000000"] 13 | 14 | # record fuzzer coverage to see what parts of the code 15 | # fuzzer executes 16 | corpusDir: "./test/03-proposal/coverage-echidna" 17 | 18 | # common invariant prefix 19 | prefix: "property_" 20 | 21 | # instruct foundry to compile tests 22 | cryticArgs: ["--foundry-compile-all"] -------------------------------------------------------------------------------- /test/03-proposal/medusa.json: -------------------------------------------------------------------------------- 1 | { 2 | "fuzzing": { 3 | "workers": 10, 4 | "workerResetLimit": 50, 5 | "_COMMENT_TESTING_1": "changed timeout to limit fuzzing time", 6 | "timeout": 10, 7 | "testLimit": 0, 8 | "shrinkLimit": 500, 9 | "callSequenceLength": 100, 10 | "_COMMENT_TESTING_8": "added directory to store coverage data", 11 | "corpusDirectory": "coverage-medusa", 12 | "coverageEnabled": true, 13 | "_COMMENT_TESTING_2": "added test contract to deploymentOrder", 14 | "targetContracts": ["ProposalCryticTester"], 15 | "predeployedContracts": {}, 16 | "targetContractsBalances": ["0x8ac7230489e80000"], 17 | "constructorArgs": {}, 18 | "deployerAddress": "0x99999", 19 | "_COMMENT_TESTING_3": "changed senderAddresses to use custom senders", 20 | "senderAddresses": ["0x1000000000000000000000000000000000000000", 21 | "0x2000000000000000000000000000000000000000", 22 | "0x3000000000000000000000000000000000000000", 23 | "0x4000000000000000000000000000000000000000", 24 | "0x5000000000000000000000000000000000000000"], 25 | "blockNumberDelayMax": 60480, 26 | "blockTimestampDelayMax": 604800, 27 | "blockGasLimit": 125000000, 28 | "transactionGasLimit": 12500000, 29 | "testing": { 30 | "_COMMENT_TESTING_4": "stopOnFailedTest to false as there are 2 invariants to break", 31 | "stopOnFailedTest": false, 32 | "stopOnFailedContractMatching": true, 33 | "stopOnNoTests": true, 34 | "_COMMENT_TESTING_5": "changed testAllContracts to true", 35 | "testAllContracts": true, 36 | "traceAll": false, 37 | "assertionTesting": { 38 | "enabled": false, 39 | "testViewMethods": false, 40 | "panicCodeConfig": { 41 | "failOnCompilerInsertedPanic": false, 42 | "failOnAssertion": true, 43 | "failOnArithmeticUnderflow": false, 44 | "failOnDivideByZero": false, 45 | "failOnEnumTypeConversionOutOfBounds": false, 46 | "failOnIncorrectStorageAccess": false, 47 | "failOnPopEmptyArray": false, 48 | "failOnOutOfBoundsArrayAccess": false, 49 | "failOnAllocateTooMuchMemory": false, 50 | "failOnCallUninitializedVariable": false 51 | } 52 | }, 53 | "propertyTesting": { 54 | "enabled": true, 55 | "_COMMENT_TESTING_6": "changed prefix to use existing Echidna test files", 56 | "testPrefixes": [ 57 | "property_" 58 | ] 59 | }, 60 | "optimizationTesting": { 61 | "enabled": false, 62 | "testPrefixes": [ 63 | "optimize_" 64 | ] 65 | }, 66 | "targetFunctionSignatures": [], 67 | "excludeFunctionSignatures": [] 68 | }, 69 | "chainConfig": { 70 | "codeSizeCheckDisabled": true, 71 | "cheatCodes": { 72 | "cheatCodesEnabled": true, 73 | "enableFFI": false 74 | } 75 | } 76 | }, 77 | "compilation": { 78 | "platform": "crytic-compile", 79 | "platformConfig": { 80 | "_COMMENT_TESTING_7": "changed target to point to main directory where command is run from", 81 | "target": "./../../.", 82 | "solcVersion": "", 83 | "exportDirectory": "", 84 | "args": ["--foundry-compile-all"] 85 | } 86 | }, 87 | "logging": { 88 | "level": "info", 89 | "logDirectory": "" 90 | } 91 | } -------------------------------------------------------------------------------- /test/04-voting-nft/Properties.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import {Asserts} from "@chimera/Asserts.sol"; 5 | import {Setup} from "./Setup.sol"; 6 | 7 | abstract contract Properties is Setup, Asserts { 8 | // two possible invariants in order of importance: 9 | // 10 | // 1) at power calculation timestamp, total voting power is not 0 11 | // breaking this invariant is very valuable but much harder 12 | // if it can break this invariant, it has pulled off the epic hack 13 | function property_total_power_gt_zero_power_calc_start() public view returns(bool) { 14 | return votingNft.getTotalPower() != 0; 15 | } 16 | 17 | 18 | // 2) at power calculation timestamp, total voting power is equal 19 | // to the initial max nft power 20 | // breaking this invariant is less valuable but much easier 21 | // if it can break this invariant, it has found the problem that would 22 | // then lead a human auditor to the big hack 23 | function property_total_power_eq_init_max_power_calc_start() public view returns(bool) { 24 | return votingNft.getTotalPower() == initMaxNftPower; 25 | } 26 | } -------------------------------------------------------------------------------- /test/04-voting-nft/Setup.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import {VotingNftForFuzz} from "../../src/04-voting-nft/VotingNftForFuzz.sol"; 5 | import {BaseSetup} from "@chimera/BaseSetup.sol"; 6 | 7 | abstract contract Setup is BaseSetup { 8 | uint256 constant requiredCollateral = 100000000000000000000; 9 | uint256 constant maxNftPower = 1000000000000000000000000000; 10 | uint256 constant nftPowerReductionPercent = 100000000000000000000000000; 11 | uint256 constant nftsToMint = 10; 12 | uint256 constant initMaxNftPower = maxNftPower * nftsToMint; 13 | uint256 constant timeUntilPowerCalc = 1000; 14 | 15 | uint256 powerCalcTimestamp; 16 | 17 | // contracts required for test 18 | VotingNftForFuzz votingNft; 19 | 20 | function setup() internal override { 21 | powerCalcTimestamp = block.timestamp + timeUntilPowerCalc; 22 | 23 | // setup contract to be tested 24 | votingNft = new VotingNftForFuzz(requiredCollateral, 25 | powerCalcTimestamp, 26 | maxNftPower, 27 | nftPowerReductionPercent); 28 | 29 | // no nfts deployed yet so total power should be 0 30 | assert(votingNft.getTotalPower() == 0); 31 | 32 | // create 10 power nfts 33 | for(uint i=1; i<11; ++i) { 34 | votingNft.safeMint(address(0x1234), i); 35 | } 36 | 37 | // verify max power has been correctly increased 38 | assert(votingNft.getTotalPower() == initMaxNftPower); 39 | 40 | // this contract is the owner 41 | assert(votingNft.owner() == address(this)); 42 | 43 | // advance time to power calculation start; we modify the 44 | // contract to use hard-coded constant instead of block.timestamp 45 | // such that the fuzzer can focus on probing the initial power 46 | // calculation state, without the fuzzer moving block.timestamp 47 | // passed the initial power calculation timestamp 48 | votingNft.setFuzzerConstantBlockTimestamp(powerCalcTimestamp); 49 | } 50 | } -------------------------------------------------------------------------------- /test/04-voting-nft/VotingNftCryticTester.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import {Properties} from "./Properties.sol"; 5 | import {CryticAsserts} from "@chimera/CryticAsserts.sol"; 6 | 7 | // run from base project directory with: 8 | // echidna --config test/04-voting-nft/echidna.yaml ./ --contract VotingNftCryticTester 9 | // medusa --config test/04-voting-nft/medusa.json fuzz 10 | contract VotingNftCryticTester is Properties, CryticAsserts { 11 | constructor() payable { 12 | setup(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/04-voting-nft/VotingNftCryticToFoundry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import {Properties} from "./Properties.sol"; 5 | import {FoundryAsserts} from "@chimera/FoundryAsserts.sol"; 6 | import {Test} from "forge-std/Test.sol"; 7 | 8 | // run from base project directory with: 9 | // forge test --match-contract VotingNftCryticToFoundry 10 | // 11 | // get coverage report (see https://medium.com/@rohanzarathustra/forge-coverage-overview-744d967e112f): 12 | // 1) forge coverage --report lcov --report-file test/04-voting-nft/coverage-foundry.lcov --match-contract VotingNftCryticToFoundry 13 | // 2) genhtml test/04-voting-nft/coverage-foundry.lcov -o test/04-voting-nft/coverage-foundry 14 | // 3) open test/04-voting-nft/coverage-foundry/index.html in your browser and 15 | // navigate to the relevant source file to see line-by-line execution records 16 | contract VotingNftCryticToFoundry is Test, Properties, FoundryAsserts { 17 | function setUp() public virtual { 18 | setup(); 19 | 20 | // use specific attacker address; attacker has no assets or 21 | // any special permissions for the contract being attacked 22 | targetSender(address(0x1337)); 23 | } 24 | 25 | // wrap common invariants for foundry 26 | function invariant_total_power_gt_zero_power_calc_start() external { 27 | t(property_total_power_gt_zero_power_calc_start(), "Total voting power not zero when power calculation starts"); 28 | } 29 | 30 | function invariant_total_power_eq_init_max_power_calc_start() external { 31 | t(property_total_power_eq_init_max_power_calc_start(), "Total voting power correct when power calculation starts"); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/04-voting-nft/certora.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "src/04-voting-nft/VotingNft.sol" 4 | ], 5 | "verify": "VotingNft:test/04-voting-nft/certora.spec", 6 | "packages":[ 7 | "@openzeppelin=lib/openzeppelin-contracts" 8 | ], 9 | "optimistic_fallback": true, 10 | "optimistic_loop": true 11 | } -------------------------------------------------------------------------------- /test/04-voting-nft/certora.spec: -------------------------------------------------------------------------------- 1 | // run from base folder: 2 | // certoraRun test/04-voting-nft/certora.conf 3 | methods { 4 | // `envfree` definitions to call functions without explicit `env` 5 | function getTotalPower() external returns (uint256) envfree; 6 | function totalSupply() external returns (uint256) envfree; 7 | function owner() external returns (address) envfree; 8 | function ownerOf(uint256) external returns (address) envfree; 9 | function balanceOf(address) external returns (uint256) envfree; 10 | } 11 | 12 | // define constants and require them later to prevent HAVOC into invalid state 13 | definition PERCENTAGE_100() returns uint256 = 1000000000000000000000000000; 14 | 15 | // given: safeMint() -> power calculation start time -> f() 16 | // there should exist no f() where a permissionless attacker 17 | // could nuke total power to 0 when power calculation starts 18 | rule total_power_gt_zero_power_calc_start(address to, uint256 tokenId) { 19 | // enforce basic sanity checks on variables set during constructor 20 | require currentContract.s_requiredCollateral > 0 && 21 | currentContract.s_powerCalcTimestamp > 0 && 22 | currentContract.s_maxNftPower > 0 && 23 | currentContract.s_nftPowerReductionPercent > 0 && 24 | currentContract.s_nftPowerReductionPercent < PERCENTAGE_100(); 25 | 26 | // enforce no nfts have yet been created; in practice some may 27 | // exist in storage though due to certora havoc 28 | require totalSupply() == 0 && getTotalPower() == 0 && balanceOf(to) == 0; 29 | 30 | // enforce msg.sender as owner required to mint nfts 31 | env e1; 32 | require e1.msg.sender == currentContract.owner(); 33 | 34 | // enforce block.timestamp < power calculation start time 35 | // so new nfts can still be minted 36 | require e1.block.timestamp < currentContract.s_powerCalcTimestamp; 37 | 38 | // first safeMint() succeeds 39 | safeMint(e1, to, tokenId); 40 | 41 | // sanity check results of first mint 42 | assert totalSupply() == 1 && balanceOf(to) == 1 && ownerOf(tokenId) == to && 43 | getTotalPower() == currentContract.s_maxNftPower; 44 | 45 | // perform any arbitrary successful transaction at power calculation 46 | // start time, where msg.sender is not an nft owner or an admin 47 | env e2; 48 | require e2.msg.sender != currentContract && 49 | e2.msg.sender != to && 50 | e2.msg.sender != currentContract.owner() && 51 | balanceOf(e2.msg.sender) == 0 && 52 | e2.block.timestamp == currentContract.s_powerCalcTimestamp; 53 | method f; 54 | calldataarg args; 55 | f(e2, args); 56 | 57 | // total power should not equal to 0 58 | assert getTotalPower() != 0; 59 | } -------------------------------------------------------------------------------- /test/04-voting-nft/echidna.yaml: -------------------------------------------------------------------------------- 1 | # no eth required 2 | balanceContract: 0 3 | 4 | # Allow fuzzer to use public/external functions from all contracts 5 | allContracts: true 6 | 7 | # specify address to use for fuzz transactions; for this test 8 | # we want only one sender who has no assets or permissions on 9 | # the contract being fuzzed; a permission-less attacker 10 | sender: ["0x1337000000000000000000000000000000000000"] 11 | 12 | # common invariant prefix 13 | prefix: "property_" 14 | 15 | # increase number of works to speed up test 16 | workers: 10 17 | 18 | # increase test limit to around 1 minute 19 | testLimit: 3300000 20 | 21 | # record fuzzer coverage to see what parts of the code 22 | # fuzzer executes 23 | corpusDir: "./test/04-voting-nft/coverage-echidna" 24 | 25 | # instruct foundry to compile tests 26 | cryticArgs: ["--foundry-compile-all"] 27 | -------------------------------------------------------------------------------- /test/04-voting-nft/medusa.json: -------------------------------------------------------------------------------- 1 | { 2 | "fuzzing": { 3 | "workers": 10, 4 | "workerResetLimit": 50, 5 | "_COMMENT_TESTING_1": "changed timeout to give fuzzer 1 minute", 6 | "timeout": 60, 7 | "testLimit": 0, 8 | "shrinkLimit": 500, 9 | "callSequenceLength": 100, 10 | "_COMMENT_TESTING_8": "added directory to store coverage data", 11 | "corpusDirectory": "coverage-medusa", 12 | "coverageEnabled": true, 13 | "_COMMENT_TESTING_2": "added test contract to deploymentOrder", 14 | "targetContracts": ["VotingNftCryticTester"], 15 | "predeployedContracts": {}, 16 | "targetContractsBalances": [], 17 | "constructorArgs": {}, 18 | "deployerAddress": "0x30000", 19 | "_COMMENT_TESTING_3": "changed senderAddresses to use permissionless attacker address", 20 | "senderAddresses": ["0x1337000000000000000000000000000000000000"], 21 | "blockNumberDelayMax": 60480, 22 | "blockTimestampDelayMax": 604800, 23 | "blockGasLimit": 125000000, 24 | "transactionGasLimit": 12500000, 25 | "testing": { 26 | "_COMMENT_TESTING_4": "stopOnFailedTest to false as there are 2 invariants to break", 27 | "stopOnFailedTest": false, 28 | "stopOnFailedContractMatching": true, 29 | "stopOnNoTests": true, 30 | "_COMMENT_TESTING_5": "changed testAllContracts to true", 31 | "testAllContracts": true, 32 | "traceAll": false, 33 | "assertionTesting": { 34 | "enabled": false, 35 | "testViewMethods": true, 36 | "panicCodeConfig": { 37 | "failOnCompilerInsertedPanic": false, 38 | "failOnAssertion": true, 39 | "failOnArithmeticUnderflow": false, 40 | "failOnDivideByZero": false, 41 | "failOnEnumTypeConversionOutOfBounds": false, 42 | "failOnIncorrectStorageAccess": false, 43 | "failOnPopEmptyArray": false, 44 | "failOnOutOfBoundsArrayAccess": false, 45 | "failOnAllocateTooMuchMemory": false, 46 | "failOnCallUninitializedVariable": false 47 | } 48 | }, 49 | "propertyTesting": { 50 | "enabled": true, 51 | "testPrefixes": [ 52 | "property_" 53 | ] 54 | }, 55 | "optimizationTesting": { 56 | "enabled": false, 57 | "testPrefixes": [ 58 | "optimize_" 59 | ] 60 | }, 61 | "targetFunctionSignatures": [], 62 | "excludeFunctionSignatures": [] 63 | }, 64 | "chainConfig": { 65 | "codeSizeCheckDisabled": true, 66 | "cheatCodes": { 67 | "cheatCodesEnabled": true, 68 | "enableFFI": false 69 | } 70 | } 71 | }, 72 | "compilation": { 73 | "platform": "crytic-compile", 74 | "platformConfig": { 75 | "_COMMENT_TESTING_7": "changed target to point to main directory where command is run from", 76 | "target": "./../../.", 77 | "solcVersion": "", 78 | "exportDirectory": "", 79 | "args": ["--foundry-compile-all"] 80 | } 81 | }, 82 | "logging": { 83 | "level": "info", 84 | "logDirectory": "" 85 | } 86 | } -------------------------------------------------------------------------------- /test/05-token-sale/TokenSaleAdvancedEchidna.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import "./TokenSaleBasicEchidna.t.sol"; 5 | 6 | // configure solc-select to use compiler version: 7 | // solc-select use 0.8.23 8 | // 9 | // run from base project directory with: 10 | // echidna --config test/05-token-sale/TokenSaleAdvancedEchidna.yaml ./ --contract TokenSaleAdvancedEchidna 11 | contract TokenSaleAdvancedEchidna is TokenSaleBasicEchidna { 12 | 13 | // constructor has to be payable if balanceContract > 0 in yaml config 14 | constructor() payable TokenSaleBasicEchidna() { 15 | // advanced test with guiding of the fuzzer 16 | // 17 | // ideally we would like a quick way to just point Echidna 18 | // at only the `tokenSale` contract, but since I'm not aware 19 | // of one we just wrap every function from that contract 20 | // into this one. 21 | // 22 | // Also in the yaml config set `allContracts: false` 23 | // 24 | // advanced echidna is able to break both invariants and find 25 | // much more simplified exploit chains than advanced foundry! 26 | } 27 | 28 | // dumb wrappers around the non-view `tokenSale` contract functions 29 | // would be nice if there was a simple way to just point Echidna 30 | // at the contract 31 | function buy(uint256 amountToBuy) public { 32 | hevm.prank(msg.sender); 33 | tokenSale.buy(amountToBuy); 34 | } 35 | 36 | function endSale() public { 37 | hevm.prank(msg.sender); 38 | tokenSale.endSale(); 39 | } 40 | 41 | // invariants inherited from base contract 42 | } 43 | -------------------------------------------------------------------------------- /test/05-token-sale/TokenSaleAdvancedEchidna.yaml: -------------------------------------------------------------------------------- 1 | # no eth required 2 | balanceContract: 0 3 | 4 | # constraint fuzzer to token sale contract functions 5 | allContracts: false 6 | 7 | # specify address to use for fuzz transations 8 | # limit this to the allowed buyer addresses 9 | sender: ["0x1000000000000000000000000000000000000000", "0x2000000000000000000000000000000000000000", "0x3000000000000000000000000000000000000000", "0x4000000000000000000000000000000000000000", "0x5000000000000000000000000000000000000000"] 10 | 11 | # record fuzzer coverage to see what parts of the code 12 | # fuzzer executes 13 | corpusDir: "./test/05-token-sale/coverage-echidna-advanced" 14 | 15 | # use same prefix as Foundry invariant tests 16 | prefix: "invariant_" 17 | 18 | # instruct foundry to compile tests 19 | cryticArgs: ["--foundry-compile-all"] -------------------------------------------------------------------------------- /test/05-token-sale/TokenSaleAdvancedFoundry.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import "./TokenSaleBasicFoundry.t.sol"; 5 | 6 | // run from base project directory with: 7 | // forge test --match-contract TokenSaleAdvancedFoundry 8 | // 9 | // get coverage report (see https://medium.com/@rohanzarathustra/forge-coverage-overview-744d967e112f): 10 | // 1) forge coverage --report lcov --report-file test/05-token-sale/coverage-foundry-advanced.lcov --match-contract TokenSaleAdvancedFoundry 11 | // 2) genhtml test/05-token-sale/coverage-foundry-advanced.lcov -o test/05-token-sale/coverage-foundry-advanced 12 | // 3) open test/05-token-sale/coverage-foundry-advanced/index.html in your browser and 13 | // navigate to the relevant source file to see line-by-line execution records 14 | contract TokenSaleAdvancedFoundry is TokenSaleBasicFoundry { 15 | 16 | function setUp() public override { 17 | // call parent first to setup test environment 18 | super.setUp(); 19 | 20 | // advanced test with guiding of the fuzzer 21 | // 22 | // guide Foundry to focus only on the `tokenSale` contract 23 | // 24 | // advanced foundry is able to break both invariants! 25 | targetContract(address(tokenSale)); 26 | } 27 | 28 | // invariants inherited from base contract 29 | } 30 | -------------------------------------------------------------------------------- /test/05-token-sale/TokenSaleBasicEchidna.yaml: -------------------------------------------------------------------------------- 1 | # no eth required 2 | balanceContract: 0 3 | 4 | # Allow fuzzer to use public/external functions from all contracts 5 | allContracts: true 6 | 7 | # specify address to use for fuzz transations 8 | # limit this to the allowed buyer addresses 9 | sender: ["0x1000000000000000000000000000000000000000", "0x2000000000000000000000000000000000000000", "0x3000000000000000000000000000000000000000", "0x4000000000000000000000000000000000000000", "0x5000000000000000000000000000000000000000"] 10 | 11 | # record fuzzer coverage to see what parts of the code 12 | # fuzzer executes 13 | corpusDir: "./test/05-token-sale/coverage-echidna-basic" 14 | 15 | # use same prefix as Foundry invariant tests 16 | prefix: "invariant_" 17 | 18 | # instruct foundry to compile tests 19 | cryticArgs: ["--foundry-compile-all"] -------------------------------------------------------------------------------- /test/05-token-sale/TokenSaleBasicMedusa.json: -------------------------------------------------------------------------------- 1 | { 2 | "fuzzing": { 3 | "workers": 10, 4 | "workerResetLimit": 50, 5 | "_COMMENT_TESTING_1": "changed timeout to limit fuzzing time", 6 | "timeout": 10, 7 | "testLimit": 0, 8 | "shrinkLimit": 500, 9 | "callSequenceLength": 100, 10 | "_COMMENT_TESTING_8": "added directory to store coverage data", 11 | "corpusDirectory": "coverage-medusa-basic", 12 | "coverageEnabled": true, 13 | "_COMMENT_TESTING_2": "added test contract to deploymentOrder", 14 | "targetContracts": ["TokenSaleBasicEchidna"], 15 | "predeployedContracts": {}, 16 | "targetContractsBalances": [], 17 | "constructorArgs": {}, 18 | "deployerAddress": "0x30000", 19 | "_COMMENT_TESTING_3": "changed senderAddresses to use custom senders", 20 | "senderAddresses": [ 21 | "0x1000000000000000000000000000000000000000", 22 | "0x2000000000000000000000000000000000000000", 23 | "0x3000000000000000000000000000000000000000", 24 | "0x4000000000000000000000000000000000000000", 25 | "0x5000000000000000000000000000000000000000" 26 | ], 27 | "blockNumberDelayMax": 60480, 28 | "blockTimestampDelayMax": 604800, 29 | "blockGasLimit": 125000000, 30 | "transactionGasLimit": 12500000, 31 | "testing": { 32 | "_COMMENT_TESTING_4": "stopOnFailedTest to false as there are 2 invariants to break", 33 | "stopOnFailedTest": false, 34 | "stopOnFailedContractMatching": true, 35 | "stopOnNoTests": true, 36 | "_COMMENT_TESTING_5": "changed testAllContracts to true", 37 | "testAllContracts": true, 38 | "traceAll": false, 39 | "assertionTesting": { 40 | "enabled": false, 41 | "testViewMethods": false, 42 | "panicCodeConfig": { 43 | "failOnCompilerInsertedPanic": false, 44 | "failOnAssertion": true, 45 | "failOnArithmeticUnderflow": false, 46 | "failOnDivideByZero": false, 47 | "failOnEnumTypeConversionOutOfBounds": false, 48 | "failOnIncorrectStorageAccess": false, 49 | "failOnPopEmptyArray": false, 50 | "failOnOutOfBoundsArrayAccess": false, 51 | "failOnAllocateTooMuchMemory": false, 52 | "failOnCallUninitializedVariable": false 53 | } 54 | }, 55 | "propertyTesting": { 56 | "enabled": true, 57 | "_COMMENT_TESTING_6": "changed prefix to use existing Echidna test files", 58 | "testPrefixes": [ 59 | "invariant_" 60 | ] 61 | }, 62 | "optimizationTesting": { 63 | "enabled": false, 64 | "testPrefixes": [ 65 | "optimize_" 66 | ] 67 | }, 68 | "targetFunctionSignatures": [], 69 | "excludeFunctionSignatures": [] 70 | }, 71 | "chainConfig": { 72 | "codeSizeCheckDisabled": true, 73 | "cheatCodes": { 74 | "cheatCodesEnabled": true, 75 | "enableFFI": false 76 | } 77 | } 78 | }, 79 | "compilation": { 80 | "platform": "crytic-compile", 81 | "platformConfig": { 82 | "_COMMENT_TESTING_7": "changed target to point to main directory where command is run from", 83 | "target": "./../../.", 84 | "solcVersion": "", 85 | "exportDirectory": "", 86 | "args": ["--foundry-compile-all"] 87 | } 88 | }, 89 | "logging": { 90 | "level": "info", 91 | "logDirectory": "" 92 | } 93 | } -------------------------------------------------------------------------------- /test/05-token-sale/certora.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "src/05-token-sale/TokenSale.sol", 4 | "src/TestToken.sol", 5 | "src/TestToken2.sol" 6 | ], 7 | "verify": "TokenSale:test/05-token-sale/certora.spec", 8 | "link": [ 9 | "TokenSale:s_sellToken=TestToken", 10 | "TokenSale:s_buyToken=TestToken2" 11 | ], 12 | "packages":[ 13 | "@openzeppelin=lib/openzeppelin-contracts" 14 | ], 15 | "optimistic_fallback": true, 16 | "optimistic_loop": true 17 | } -------------------------------------------------------------------------------- /test/05-token-sale/certora.spec: -------------------------------------------------------------------------------- 1 | // run from base folder: 2 | // certoraRun test/05-token-sale/certora.conf 3 | methods { 4 | // `envfree` definitions to call functions without explicit `env` 5 | function getSellTokenSoldAmount() external returns (uint256) envfree; 6 | } 7 | 8 | // define constants and require them later to prevent HAVOC into invalid state 9 | definition MIN_PRECISION_BUY() returns uint256 = 6; 10 | definition PRECISION_SELL() returns uint256 = 18; 11 | definition FUNDING_MIN() returns uint256 = 100; 12 | definition BUYERS_MIN() returns uint256 = 3; 13 | 14 | // the amount of tokens bought should equal the amount of tokens sold 15 | // due to 1:1 exchange ratio 16 | rule tokens_bought_eq_tokens_sold(uint256 amountToBuy) { 17 | env e1; 18 | 19 | uint8 sellTokenDecimals = currentContract.s_sellToken.decimals(e1); 20 | uint8 buyTokenDecimals = currentContract.s_buyToken.decimals(e1); 21 | 22 | // enforce basic sanity checks on variables set during constructor 23 | require currentContract.s_sellToken != currentContract.s_buyToken && 24 | sellTokenDecimals == PRECISION_SELL() && 25 | buyTokenDecimals >= MIN_PRECISION_BUY() && 26 | buyTokenDecimals <= PRECISION_SELL() && 27 | currentContract.s_sellTokenTotalAmount >= FUNDING_MIN() * 10 ^ sellTokenDecimals && 28 | currentContract.s_maxTokensPerBuyer <= currentContract.s_sellTokenTotalAmount && 29 | currentContract.s_totalBuyers >= BUYERS_MIN(); 30 | 31 | // enforce valid msg.sender 32 | require e1.msg.sender != currentContract.s_creator && 33 | e1.msg.sender != currentContract.s_buyToken && 34 | e1.msg.sender != currentContract.s_sellToken && 35 | e1.msg.value == 0; 36 | 37 | // enforce buyer has not yet bought any tokens being sold 38 | require currentContract.s_sellToken.balanceOf(e1, e1.msg.sender) == 0 && 39 | getSellTokenSoldAmount() == 0; 40 | 41 | // enforce buyer has tokens with which to buy tokens being sold 42 | uint256 buyerBuyTokenBalPre = currentContract.s_buyToken.balanceOf(e1, e1.msg.sender); 43 | require buyerBuyTokenBalPre > 0 && amountToBuy > 0; 44 | 45 | // perform a successful `buy` transaction 46 | buy(e1, amountToBuy); 47 | 48 | // buyer must have received some tokens from the sale 49 | assert getSellTokenSoldAmount() > 0; 50 | uint256 buyerSellTokensBalPost = currentContract.s_sellToken.balanceOf(e1, e1.msg.sender); 51 | assert buyerSellTokensBalPost > 0; 52 | 53 | uint256 buyerBuyTokenBalPost = currentContract.s_buyToken.balanceOf(e1, e1.msg.sender); 54 | 55 | // verify buyer paid 1:1 for the tokens they bought when accounting for decimal difference 56 | assert getSellTokenSoldAmount() == (buyerBuyTokenBalPre - buyerBuyTokenBalPost) 57 | * 10 ^ (sellTokenDecimals - buyTokenDecimals); 58 | } 59 | 60 | // this rule was provided by https://x.com/alexzoid_eth 61 | rule max_token_buy_per_user(env e1, env e2, uint256 amountToBuy1, uint256 amountToBuy2) { 62 | // enforce valid initial state 63 | require(currentContract.s_maxTokensPerBuyer <= currentContract.s_sellTokenTotalAmount); 64 | 65 | // enforce same buyer in different calls 66 | require e1.msg.sender == e2.msg.sender; 67 | require e1.msg.sender != currentContract && e1.msg.sender != currentContract.s_creator; 68 | 69 | // save initial balance 70 | mathint balanceBefore = currentContract.s_sellToken.balanceOf(e1, e1.msg.sender); 71 | 72 | // prevent over-flow in ERC20 (otherwise totalSupply and balances synchronization required) 73 | require balanceBefore < max_uint128 && amountToBuy1 < max_uint128 && amountToBuy2 < max_uint128; 74 | 75 | // perform two separate buy transactions 76 | buy(e1, amountToBuy1); 77 | buy(e2, amountToBuy2); 78 | 79 | // save final balance 80 | mathint balanceAfter = currentContract.s_sellToken.balanceOf(e1, e1.msg.sender); 81 | 82 | // verify total bought by same user must not exceed max limit per user 83 | mathint totalBuy = balanceAfter > balanceBefore ? balanceAfter - balanceBefore : 0; 84 | assert totalBuy <= currentContract.s_maxTokensPerBuyer; 85 | } -------------------------------------------------------------------------------- /test/06-rarely-false/RarelyFalseCryticTester.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import {TargetFunctions} from "./TargetFunctions.sol"; 5 | import {CryticAsserts} from "@chimera/CryticAsserts.sol"; 6 | 7 | // run from base project directory with: 8 | // echidna --config test/06-rarely-false/echidna.yaml ./ --contract RarelyFalseCryticTester 9 | // medusa --config test/06-rarely-false/medusa.json fuzz 10 | contract RarelyFalseCryticTester is TargetFunctions, CryticAsserts { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /test/06-rarely-false/RarelyFalseCryticToFoundry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import {TargetFunctions} from "./TargetFunctions.sol"; 5 | import {FoundryAsserts} from "@chimera/FoundryAsserts.sol"; 6 | import {Test} from "forge-std/Test.sol"; 7 | 8 | // run from base project directory with: 9 | // forge test --match-contract RarelyFalseCryticToFoundry --fuzz-runs 2000000 10 | // 11 | // get coverage report ( can be imported into https://lcov-viewer.netlify.app/ ) 12 | // forge coverage --report lcov --report-file test/06-rarely-false/coverage-foundry.lcov --match-contract RarelyFalseCryticToFoundry 13 | // 14 | // run halmos from base project directory: 15 | // halmos --function test_ --match-contract RarelyFalseCryticToFoundry 16 | contract RarelyFalseCryticToFoundry is Test, TargetFunctions, FoundryAsserts { 17 | 18 | } 19 | -------------------------------------------------------------------------------- /test/06-rarely-false/TargetFunctions.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import {Asserts} from "@chimera/Asserts.sol"; 5 | 6 | // target functions to test 7 | abstract contract TargetFunctions is Asserts { 8 | 9 | uint256 constant private OFFSET = 1234; 10 | uint256 constant private POW = 80; 11 | uint256 constant private LIMIT = type(uint256).max - OFFSET; 12 | 13 | // fuzzers call this function 14 | function test_RarelyFalse(uint256 n) external { 15 | // input preconditions 16 | n = between(n, 1, LIMIT); 17 | 18 | // assertion to break 19 | t(_rarelyFalse(n + OFFSET, POW), "Should not be false"); 20 | } 21 | 22 | // actual implementation to test 23 | function _rarelyFalse(uint256 n, uint256 e) private pure returns(bool) { 24 | if(n % 2**e == 0) return false; 25 | return true; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /test/06-rarely-false/certora.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "test/06-rarely-false/RarelyFalseCryticToFoundry.sol" 4 | ], 5 | "verify": "RarelyFalseCryticToFoundry:test/06-rarely-false/certora.spec", 6 | "packages":[ 7 | "@chimera=lib/chimera/src", 8 | "forge-std=lib/forge-std/src" 9 | ], 10 | "foundry_tests_mode": true 11 | } -------------------------------------------------------------------------------- /test/06-rarely-false/certora.spec: -------------------------------------------------------------------------------- 1 | // run from base folder: 2 | // certoraRun test/06-rarely-false/certora.conf 3 | use builtin rule verifyFoundryFuzzTests; -------------------------------------------------------------------------------- /test/06-rarely-false/echidna.yaml: -------------------------------------------------------------------------------- 1 | # no eth required 2 | balanceContract: 0 3 | 4 | # Allow fuzzer to use public/external functions from all contracts 5 | allContracts: false 6 | 7 | # using assertion mode 8 | testMode: "assertion" 9 | 10 | # increase number of works to speed up test 11 | workers: 10 12 | 13 | # increase test limit to around 1 minute 14 | testLimit: 10000000 15 | 16 | # record fuzzer coverage to see what parts of the code 17 | # fuzzer executes 18 | corpusDir: "./test/06-rarely-false/coverage-echidna" 19 | 20 | # instruct foundry to compile tests 21 | cryticArgs: ["--foundry-compile-all"] -------------------------------------------------------------------------------- /test/06-rarely-false/medusa.json: -------------------------------------------------------------------------------- 1 | { 2 | "fuzzing": { 3 | "workers": 10, 4 | "workerResetLimit": 50, 5 | "_COMMENT_TESTING_1": "changed timeout to limit fuzzing time", 6 | "timeout": 60, 7 | "testLimit": 0, 8 | "shrinkLimit": 500, 9 | "callSequenceLength": 100, 10 | "_COMMENT_TESTING_8": "added directory to store coverage data", 11 | "corpusDirectory": "coverage-medusa", 12 | "coverageEnabled": true, 13 | "_COMMENT_TESTING_2": "added test contract to deploymentOrder", 14 | "targetContracts": ["RarelyFalseCryticTester"], 15 | "predeployedContracts": {}, 16 | "targetContractsBalances": [], 17 | "constructorArgs": {}, 18 | "deployerAddress": "0x30000", 19 | "_COMMENT_TESTING_3": "changed senderAddresses to use custom senders", 20 | "senderAddresses": ["0x1337000000000000000000000000000000000000"], 21 | "blockNumberDelayMax": 60480, 22 | "blockTimestampDelayMax": 604800, 23 | "blockGasLimit": 125000000, 24 | "transactionGasLimit": 12500000, 25 | "testing": { 26 | "stopOnFailedTest": true, 27 | "stopOnFailedContractMatching": true, 28 | "stopOnNoTests": true, 29 | "testAllContracts": false, 30 | "traceAll": false, 31 | "assertionTesting": { 32 | "_COMMENT_TESTING_5": "enabled assertion mode", 33 | "enabled": true, 34 | "testViewMethods": false, 35 | "panicCodeConfig": { 36 | "failOnCompilerInsertedPanic": false, 37 | "failOnAssertion": true, 38 | "failOnArithmeticUnderflow": false, 39 | "failOnDivideByZero": false, 40 | "failOnEnumTypeConversionOutOfBounds": false, 41 | "failOnIncorrectStorageAccess": false, 42 | "failOnPopEmptyArray": false, 43 | "failOnOutOfBoundsArrayAccess": false, 44 | "failOnAllocateTooMuchMemory": false, 45 | "failOnCallUninitializedVariable": false 46 | } 47 | }, 48 | "propertyTesting": { 49 | "enabled": true, 50 | "_COMMENT_TESTING_6": "changed prefix to use existing Echidna test files", 51 | "testPrefixes": [ 52 | "invariant_" 53 | ] 54 | }, 55 | "optimizationTesting": { 56 | "enabled": false, 57 | "testPrefixes": [ 58 | "optimize_" 59 | ] 60 | }, 61 | "targetFunctionSignatures": [], 62 | "excludeFunctionSignatures": [] 63 | }, 64 | "chainConfig": { 65 | "codeSizeCheckDisabled": true, 66 | "cheatCodes": { 67 | "cheatCodesEnabled": true, 68 | "enableFFI": false 69 | } 70 | } 71 | }, 72 | "compilation": { 73 | "platform": "crytic-compile", 74 | "platformConfig": { 75 | "_COMMENT_TESTING_7": "changed target to point to main directory where command is run from", 76 | "target": "./../../.", 77 | "solcVersion": "", 78 | "exportDirectory": "", 79 | "args": ["--foundry-compile-all"] 80 | } 81 | }, 82 | "logging": { 83 | "level": "info", 84 | "logDirectory": "" 85 | } 86 | } -------------------------------------------------------------------------------- /test/07-byte-battle/ByteBattleCryticTester.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import {TargetFunctions} from "./TargetFunctions.sol"; 5 | import {CryticAsserts} from "@chimera/CryticAsserts.sol"; 6 | 7 | // configure solc-select to use compiler version: 8 | // solc-select use 0.8.23 9 | // 10 | // run from base project directory with: 11 | // echidna --config test/07-byte-battle/echidna.yaml ./ --contract ByteBattleCryticTester 12 | // medusa --config test/07-byte-battle/medusa.json fuzz 13 | contract ByteBattleCryticTester is TargetFunctions, CryticAsserts { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /test/07-byte-battle/ByteBattleCryticToFoundry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import {TargetFunctions} from "./TargetFunctions.sol"; 5 | import {FoundryAsserts} from "@chimera/FoundryAsserts.sol"; 6 | import {Test} from "forge-std/Test.sol"; 7 | 8 | // run from base project directory with: 9 | // forge test --match-contract ByteBattleCryticToFoundry 10 | // 11 | // get coverage report ( can be imported into https://lcov-viewer.netlify.app/ ) 12 | // forge coverage --report lcov --report-file test/07-byte-battle/coverage-foundry.lcov --match-contract ByteBattleCryticToFoundry 13 | // 14 | // run halmos from base project directory: 15 | // halmos --function test_ --match-contract ByteBattleCryticToFoundry 16 | contract ByteBattleCryticToFoundry is Test, TargetFunctions, FoundryAsserts { 17 | 18 | } 19 | -------------------------------------------------------------------------------- /test/07-byte-battle/TargetFunctions.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import {Asserts} from "@chimera/Asserts.sol"; 5 | 6 | // target functions to test 7 | abstract contract TargetFunctions is Asserts { 8 | 9 | // fuzzers call this function 10 | function test_ByteBattle(bytes32 a, bytes32 b) external { 11 | // input precondition 12 | precondition(a != b); 13 | 14 | // assertion to break 15 | t(_convertIt(a) != _convertIt(b), "Different inputs should not convert to the same value"); 16 | } 17 | 18 | // actual implementation to test 19 | function _convertIt(bytes32 b) private pure returns (uint96) { 20 | return uint96(uint256(b) >> 160); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/07-byte-battle/certora.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "test/07-byte-battle/ByteBattleCryticToFoundry.sol" 4 | ], 5 | "verify": "ByteBattleCryticToFoundry:test/07-byte-battle/certora.spec", 6 | "packages":[ 7 | "@chimera=lib/chimera/src", 8 | "forge-std=lib/forge-std/src" 9 | ], 10 | "foundry_tests_mode": true 11 | } -------------------------------------------------------------------------------- /test/07-byte-battle/certora.spec: -------------------------------------------------------------------------------- 1 | // run from base folder: 2 | // certoraRun test/07-byte-battle/certora.conf 3 | use builtin rule verifyFoundryFuzzTests; -------------------------------------------------------------------------------- /test/07-byte-battle/echidna.yaml: -------------------------------------------------------------------------------- 1 | # no eth required 2 | balanceContract: 0 3 | 4 | # Allow fuzzer to use public/external functions from all contracts 5 | allContracts: false 6 | 7 | # using assertion mode 8 | testMode: "assertion" 9 | 10 | # record fuzzer coverage to see what parts of the code 11 | # fuzzer executes 12 | corpusDir: "./test/07-byte-battle/coverage-echidna" 13 | 14 | # instruct foundry to compile tests 15 | cryticArgs: ["--foundry-compile-all"] -------------------------------------------------------------------------------- /test/07-byte-battle/medusa.json: -------------------------------------------------------------------------------- 1 | { 2 | "fuzzing": { 3 | "workers": 10, 4 | "workerResetLimit": 50, 5 | "_COMMENT_TESTING_1": "changed timeout to limit fuzzing time", 6 | "timeout": 10, 7 | "testLimit": 0, 8 | "shrinkLimit": 500, 9 | "callSequenceLength": 100, 10 | "_COMMENT_TESTING_8": "added directory to store coverage data", 11 | "corpusDirectory": "coverage-medusa", 12 | "coverageEnabled": true, 13 | "_COMMENT_TESTING_2": "added test contract to deploymentOrder", 14 | "targetContracts": ["ByteBattleCryticTester"], 15 | "predeployedContracts": {}, 16 | "targetContractsBalances": [], 17 | "constructorArgs": {}, 18 | "deployerAddress": "0x30000", 19 | "_COMMENT_TESTING_3": "changed senderAddresses to use custom senders", 20 | "senderAddresses": ["0x1337000000000000000000000000000000000000"], 21 | "blockNumberDelayMax": 60480, 22 | "blockTimestampDelayMax": 604800, 23 | "blockGasLimit": 125000000, 24 | "transactionGasLimit": 12500000, 25 | "testing": { 26 | "stopOnFailedTest": true, 27 | "stopOnFailedContractMatching": true, 28 | "stopOnNoTests": true, 29 | "testAllContracts": false, 30 | "traceAll": false, 31 | "assertionTesting": { 32 | "_COMMENT_TESTING_5": "enabled assertion mode", 33 | "enabled": true, 34 | "testViewMethods": false, 35 | "panicCodeConfig": { 36 | "failOnCompilerInsertedPanic": false, 37 | "failOnAssertion": true, 38 | "failOnArithmeticUnderflow": false, 39 | "failOnDivideByZero": false, 40 | "failOnEnumTypeConversionOutOfBounds": false, 41 | "failOnIncorrectStorageAccess": false, 42 | "failOnPopEmptyArray": false, 43 | "failOnOutOfBoundsArrayAccess": false, 44 | "failOnAllocateTooMuchMemory": false, 45 | "failOnCallUninitializedVariable": false 46 | } 47 | }, 48 | "propertyTesting": { 49 | "enabled": true, 50 | "_COMMENT_TESTING_6": "changed prefix to use existing Echidna test files", 51 | "testPrefixes": [ 52 | "invariant_" 53 | ] 54 | }, 55 | "optimizationTesting": { 56 | "enabled": false, 57 | "testPrefixes": [ 58 | "optimize_" 59 | ] 60 | }, 61 | "targetFunctionSignatures": [], 62 | "excludeFunctionSignatures": [] 63 | }, 64 | "chainConfig": { 65 | "codeSizeCheckDisabled": true, 66 | "cheatCodes": { 67 | "cheatCodesEnabled": true, 68 | "enableFFI": false 69 | } 70 | } 71 | }, 72 | "compilation": { 73 | "platform": "crytic-compile", 74 | "platformConfig": { 75 | "_COMMENT_TESTING_7": "changed target to point to main directory where command is run from", 76 | "target": "./../../.", 77 | "solcVersion": "", 78 | "exportDirectory": "", 79 | "args": ["--foundry-compile-all"] 80 | } 81 | }, 82 | "logging": { 83 | "level": "info", 84 | "logDirectory": "" 85 | } 86 | } -------------------------------------------------------------------------------- /test/08-omni-protocol/MockOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.19; 3 | 4 | import "@openzeppelin/contracts/access/AccessControl.sol"; 5 | import "../../src/08-omni-protocol/interfaces/IOmniOracle.sol"; 6 | 7 | contract MockOracle is AccessControl, IOmniOracle { 8 | event SetPrice(address underlying, uint256 price); 9 | 10 | bytes32 public constant UPDATER_ROLE = keccak256("UPDATER_ROLE"); 11 | mapping(address => uint256) public prices; 12 | 13 | constructor() { 14 | _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); 15 | _grantRole(UPDATER_ROLE, msg.sender); 16 | } 17 | 18 | function setPrices(address[] calldata _underlyings, uint256[] calldata _prices) external onlyRole(UPDATER_ROLE) { 19 | require(_underlyings.length == _prices.length, "MockOracle::setPrices: bad data length"); 20 | for (uint256 index = 0; index < _underlyings.length; ++index) { 21 | prices[_underlyings[index]] = _prices[index]; 22 | emit SetPrice(_underlyings[index], _prices[index]); 23 | } 24 | } 25 | 26 | function getPrice(address _underlying) external view returns (uint256) { 27 | uint256 price = prices[_underlying]; 28 | require(price != 0, "MockOracle::getPrice: no price available"); 29 | return price; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/08-omni-protocol/OmniAdvancedEchidna.yaml: -------------------------------------------------------------------------------- 1 | # no eth required 2 | balanceContract: 0 3 | 4 | # constraint fuzzer as we are using handlers 5 | allContracts: false 6 | 7 | # specify address to use for fuzz transactions 8 | sender: ["0x1000000000000000000000000000000000000000", "0x2000000000000000000000000000000000000000"] 9 | 10 | # record fuzzer coverage to see what parts of the code 11 | # fuzzer executes 12 | corpusDir: "./test/08-omni-protocol/coverage-echidna" 13 | 14 | # changed prefix 15 | prefix: "medusa_" 16 | 17 | # increase number of works to speed up test 18 | workers: 10 19 | 20 | # limit attempts to shrink broken invariant transaction chain 21 | shrinkLimit: 500 22 | 23 | # increase test limit to give fuzzer approx 5 minutes 24 | testLimit: 900000 25 | 26 | # instruct foundry to compile tests 27 | cryticArgs: ["--foundry-compile-all"] -------------------------------------------------------------------------------- /test/08-omni-protocol/OmniAdvancedMedusa.json: -------------------------------------------------------------------------------- 1 | { 2 | "fuzzing": { 3 | "workers": 10, 4 | "workerResetLimit": 50, 5 | "_COMMENT_TESTING_1": "increase timeout to give fuzzer approx 5 minutes", 6 | "timeout": 300, 7 | "testLimit": 0, 8 | "shrinkLimit": 500, 9 | "callSequenceLength": 100, 10 | "_COMMENT_TESTING_8": "added directory to store coverage data", 11 | "corpusDirectory": "coverage-medusa", 12 | "coverageEnabled": true, 13 | "_COMMENT_TESTING_2": "added test contract to deploymentOrder", 14 | "targetContracts": ["OmniAdvancedMedusa"], 15 | "predeployedContracts": {}, 16 | "targetContractsBalances": [], 17 | "constructorArgs": {}, 18 | "deployerAddress": "0x30000", 19 | "_COMMENT_TESTING_3": "changed senderAddresses to use custom senders", 20 | "senderAddresses": [ 21 | "0x1000000000000000000000000000000000000000", 22 | "0x2000000000000000000000000000000000000000" 23 | ], 24 | "blockNumberDelayMax": 60480, 25 | "blockTimestampDelayMax": 604800, 26 | "blockGasLimit": 125000000, 27 | "transactionGasLimit": 12500000, 28 | "testing": { 29 | "_COMMENT_TESTING_4": "stopOnFailedTest to false as there are multiple invariants to break", 30 | "stopOnFailedTest": false, 31 | "stopOnFailedContractMatching": true, 32 | "stopOnNoTests": true, 33 | "_COMMENT_TESTING_5": "changed testAllContracts to false as using handlers", 34 | "testAllContracts": false, 35 | "traceAll": false, 36 | "assertionTesting": { 37 | "enabled": false, 38 | "testViewMethods": false, 39 | "panicCodeConfig": { 40 | "failOnCompilerInsertedPanic": false, 41 | "failOnAssertion": true, 42 | "failOnArithmeticUnderflow": false, 43 | "failOnDivideByZero": false, 44 | "failOnEnumTypeConversionOutOfBounds": false, 45 | "failOnIncorrectStorageAccess": false, 46 | "failOnPopEmptyArray": false, 47 | "failOnOutOfBoundsArrayAccess": false, 48 | "failOnAllocateTooMuchMemory": false, 49 | "failOnCallUninitializedVariable": false 50 | } 51 | }, 52 | "propertyTesting": { 53 | "enabled": true, 54 | "_COMMENT_TESTING_6": "changed prefix", 55 | "testPrefixes": [ 56 | "medusa_" 57 | ] 58 | }, 59 | "optimizationTesting": { 60 | "enabled": false, 61 | "testPrefixes": [ 62 | "optimize_" 63 | ] 64 | }, 65 | "targetFunctionSignatures": [], 66 | "excludeFunctionSignatures": [] 67 | }, 68 | "chainConfig": { 69 | "codeSizeCheckDisabled": true, 70 | "cheatCodes": { 71 | "cheatCodesEnabled": true, 72 | "enableFFI": false 73 | } 74 | } 75 | }, 76 | "compilation": { 77 | "platform": "crytic-compile", 78 | "platformConfig": { 79 | "_COMMENT_TESTING_7": "changed target to point to main directory where command is run from", 80 | "target": "./../../.", 81 | "solcVersion": "", 82 | "exportDirectory": "", 83 | "args": ["--foundry-compile-all"] 84 | } 85 | }, 86 | "logging": { 87 | "level": "info", 88 | "logDirectory": "" 89 | } 90 | } -------------------------------------------------------------------------------- /test/09-vesting/Properties.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import { Setup } from "./Setup.sol"; 5 | import { Asserts } from "@chimera/Asserts.sol"; 6 | 7 | abstract contract Properties is Setup, Asserts { 8 | 9 | function property_users_points_sum_eq_total_points() public view returns(bool result) { 10 | uint24 totalPoints; 11 | 12 | // sum up all user points 13 | for(uint256 i; i 0; 12 | 13 | // user performs any arbitrary successful transaction f() 14 | env e; 15 | require e.msg.sender == user; 16 | method f; 17 | calldataarg args; 18 | f(e, args); 19 | 20 | // verify that no transaction exists which allows user to 21 | // increase their allocated points 22 | assert userPointsPre >= currentContract.allocations[user].points; 23 | } 24 | 25 | 26 | // the same property can also be expressed in another way: 27 | // that the sum of users' individual points should always remain equal to TOTAL_POINTS 28 | // solution provided by https://x.com/alexzoid_eth 29 | methods { 30 | function TOTAL_POINTS_PCT() external returns uint24 envfree => ALWAYS(100000); 31 | } 32 | 33 | // tracks the address of user whose points have increased 34 | ghost address targetUser; 35 | 36 | // ghost mapping to track points for each user address 37 | ghost mapping (address => mathint) ghostPoints { 38 | axiom forall address user. ghostPoints[user] >= 0 && ghostPoints[user] <= max_uint24; 39 | } 40 | 41 | // hook to verify storage reads match ghost state 42 | hook Sload uint24 val allocations[KEY address user].points { 43 | require(require_uint24(ghostPoints[user]) == val); 44 | } 45 | 46 | // hook to update ghost state on storage writes 47 | // also tracks first user to receive a points increase 48 | hook Sstore allocations[KEY address user].points uint24 val { 49 | // Update targetUser only if not set and points are increasing 50 | targetUser = (targetUser == 0 && val > ghostPoints[user]) ? user : targetUser; 51 | ghostPoints[user] = val; 52 | } 53 | 54 | function initialize_constructor(address user1, address user2, address user3) { 55 | // Only user1, user2, and user3 can have non-zero points 56 | require(forall address user. user != user1 && user != user2 && user != user3 => ghostPoints[user] == 0); 57 | // Sum of their points must equal total allocation (100%) 58 | require(ghostPoints[user1] + ghostPoints[user2] + ghostPoints[user3] == TOTAL_POINTS_PCT()); 59 | } 60 | 61 | function initialize_env(env e) { 62 | // Ensure message sender is a valid address 63 | require(e.msg.sender != 0); 64 | } 65 | 66 | function initialize_users(address user1, address user2, address user3) { 67 | // Validate user addresses: 68 | // - Must be non-zero addresses 69 | require(user1 != 0 && user2 != 0 && user3 != 0); 70 | // - Must be unique addresses 71 | require(user1 != user2 && user1 != user3 && user2 != user3); 72 | // Initialize targetUser to zero address 73 | require(targetUser == 0); 74 | } 75 | 76 | // Verify that total points always equal TOTAL_POINTS_PCT (100%) 77 | rule users_points_sum_eq_total_points(env e, address user1, address user2, address user3) { 78 | // Set up initial state 79 | initialize_constructor(user1, user2, user3); 80 | initialize_env(e); 81 | initialize_users(user1, user2, user3); 82 | 83 | // Execute any method with any arguments 84 | method f; 85 | calldataarg args; 86 | f(e, args); 87 | 88 | // Calculate points for targetUser if it's not one of the initial users 89 | mathint targetUserPoints = targetUser != user1 && targetUser != user2 && targetUser != user3 ? ghostPoints[targetUser] : 0; 90 | 91 | // Assert total points remain constant at 100% 92 | assert(ghostPoints[user1] + ghostPoints[user2] + ghostPoints[user3] + targetUserPoints == TOTAL_POINTS_PCT()); 93 | 94 | // All other addresses must have zero points 95 | assert(forall address user. user != user1 && user != user2 && user != user3 && user != targetUser 96 | => ghostPoints[user] == 0 97 | ); 98 | } -------------------------------------------------------------------------------- /test/09-vesting/echidna.yaml: -------------------------------------------------------------------------------- 1 | # don't allow fuzzer to use all functions 2 | # since we are using handlers 3 | allContracts: false 4 | 5 | # record fuzzer coverage to see what parts of the code 6 | # fuzzer executes 7 | corpusDir: "./test/09-vesting/coverage-echidna" 8 | 9 | # prefix of invariant function 10 | prefix: "property_" 11 | 12 | # instruct foundry to compile tests 13 | cryticArgs: ["--foundry-compile-all"] -------------------------------------------------------------------------------- /test/09-vesting/medusa.json: -------------------------------------------------------------------------------- 1 | { 2 | "fuzzing": { 3 | "workers": 10, 4 | "workerResetLimit": 50, 5 | "_COMMENT_TESTING_1": "changed timeout to limit fuzzing time", 6 | "timeout": 10, 7 | "testLimit": 0, 8 | "shrinkLimit": 500, 9 | "callSequenceLength": 100, 10 | "_COMMENT_TESTING_8": "added directory to store coverage data", 11 | "corpusDirectory": "coverage-medusa", 12 | "coverageEnabled": true, 13 | "_COMMENT_TESTING_2": "added test contract to deploymentOrder", 14 | "targetContracts": ["VestingCryticTester"], 15 | "predeployedContracts": {}, 16 | "targetContractsBalances": [], 17 | "constructorArgs": {}, 18 | "deployerAddress": "0x30000", 19 | "senderAddresses": [ 20 | "0x10000", 21 | "0x20000", 22 | "0x30000" 23 | ], 24 | "blockNumberDelayMax": 60480, 25 | "blockTimestampDelayMax": 604800, 26 | "blockGasLimit": 125000000, 27 | "transactionGasLimit": 12500000, 28 | "testing": { 29 | "stopOnFailedTest": true, 30 | "stopOnFailedContractMatching": true, 31 | "stopOnNoTests": true, 32 | "testAllContracts": false, 33 | "traceAll": false, 34 | "assertionTesting": { 35 | "enabled": false, 36 | "testViewMethods": false, 37 | "panicCodeConfig": { 38 | "failOnCompilerInsertedPanic": false, 39 | "failOnAssertion": true, 40 | "failOnArithmeticUnderflow": false, 41 | "failOnDivideByZero": false, 42 | "failOnEnumTypeConversionOutOfBounds": false, 43 | "failOnIncorrectStorageAccess": false, 44 | "failOnPopEmptyArray": false, 45 | "failOnOutOfBoundsArrayAccess": false, 46 | "failOnAllocateTooMuchMemory": false, 47 | "failOnCallUninitializedVariable": false 48 | } 49 | }, 50 | "propertyTesting": { 51 | "enabled": true, 52 | "_COMMENT_TESTING_6": "changed prefix to match invariant function", 53 | "testPrefixes": [ 54 | "property_" 55 | ] 56 | }, 57 | "optimizationTesting": { 58 | "enabled": false, 59 | "testPrefixes": [ 60 | "optimize_" 61 | ] 62 | }, 63 | "targetFunctionSignatures": [], 64 | "excludeFunctionSignatures": [] 65 | }, 66 | "chainConfig": { 67 | "codeSizeCheckDisabled": true, 68 | "cheatCodes": { 69 | "cheatCodesEnabled": true, 70 | "enableFFI": false 71 | } 72 | } 73 | }, 74 | "compilation": { 75 | "platform": "crytic-compile", 76 | "platformConfig": { 77 | "_COMMENT_TESTING_7": "changed target to point to main directory where command is run from", 78 | "target": "./../../.", 79 | "solcVersion": "", 80 | "exportDirectory": "", 81 | "args": ["--foundry-compile-all"] 82 | } 83 | }, 84 | "logging": { 85 | "level": "info", 86 | "logDirectory": "" 87 | } 88 | } -------------------------------------------------------------------------------- /test/10-vesting-ext/Properties.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import { Setup } from "./Setup.sol"; 5 | import { Asserts } from "@chimera/Asserts.sol"; 6 | 7 | abstract contract Properties is Setup, Asserts { 8 | 9 | function property_users_points_sum_eq_total_points() public view returns(bool result) { 10 | uint24 totalPoints; 11 | 12 | // sum up all user points 13 | for(uint256 i; i 0) { 17 | address[] memory values = foundAddresses.values(); 18 | 19 | for(uint256 i; i 0) { 29 | // operator ids start at 1 30 | for(uint128 operatorId = 1; operatorId <= numOperators; operatorId++) { 31 | if(!foundAddresses.add(operatorRegistry.operatorIdToAddress(operatorId))) { 32 | return false; 33 | } 34 | } 35 | } 36 | 37 | result = true; 38 | } 39 | } -------------------------------------------------------------------------------- /test/11-op-reg/Setup.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import { OperatorRegistry } from "../../src/11-op-reg/OperatorRegistry.sol"; 5 | import { BaseSetup } from "@chimera/BaseSetup.sol"; 6 | 7 | abstract contract Setup is BaseSetup { 8 | // contract being tested 9 | OperatorRegistry operatorRegistry; 10 | 11 | // ghost variables 12 | address[] addressPool; 13 | uint8 internal ADDRESS_POOL_LENGTH; 14 | 15 | function setup() internal virtual override { 16 | addressPool.push(address(0x1111)); 17 | addressPool.push(address(0x2222)); 18 | addressPool.push(address(0x3333)); 19 | addressPool.push(address(0x4444)); 20 | addressPool.push(address(0x5555)); 21 | addressPool.push(address(0x6666)); 22 | addressPool.push(address(0x7777)); 23 | addressPool.push(address(0x8888)); 24 | addressPool.push(address(0x9999)); 25 | ADDRESS_POOL_LENGTH = uint8(addressPool.length); 26 | 27 | operatorRegistry = new OperatorRegistry(); 28 | } 29 | } -------------------------------------------------------------------------------- /test/11-op-reg/TargetFunctions.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import { Properties } from "./Properties.sol"; 5 | import { BaseTargetFunctions } from "@chimera/BaseTargetFunctions.sol"; 6 | import { IHevm, vm } from "@chimera/Hevm.sol"; 7 | 8 | abstract contract TargetFunctions is BaseTargetFunctions, Properties { 9 | 10 | // gets a random non-zero address from `Setup::addressPool` 11 | function _getRandomAddress(uint8 index) internal returns(address addr) { 12 | index = uint8(between(index, 0, ADDRESS_POOL_LENGTH - 1)); 13 | addr = addressPool[index]; 14 | } 15 | 16 | function handler_register(uint8 callerIndex) external { 17 | address caller = _getRandomAddress(callerIndex); 18 | 19 | vm.prank(caller); 20 | operatorRegistry.register(); 21 | } 22 | 23 | function handler_updateAddress(uint8 callerIndex, uint8 updateIndex) external { 24 | address caller = _getRandomAddress(callerIndex); 25 | address update = _getRandomAddress(updateIndex); 26 | 27 | vm.prank(caller); 28 | operatorRegistry.updateAddress(update); 29 | } 30 | } -------------------------------------------------------------------------------- /test/11-op-reg/certora.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "src/11-op-reg/OperatorRegistry.sol" 4 | ], 5 | "verify": "OperatorRegistry:test/11-op-reg/certora.spec" 6 | } -------------------------------------------------------------------------------- /test/11-op-reg/certora.spec: -------------------------------------------------------------------------------- 1 | // run from base folder: 2 | // certoraRun test/11-op-reg/certora.conf 3 | 4 | // given two registered operators, there should be no f() that could 5 | // corrupt the unique relationship between operator_id : operator_address 6 | rule operator_addresses_have_unique_ids(address opAddr1, address opAddr2) { 7 | // enforce unique addresses in `operatorAddressToId` mapping 8 | require opAddr1 != opAddr2; 9 | 10 | uint128 op1AddrToId = currentContract.operatorAddressToId[opAddr1]; 11 | uint128 op2AddrToId = currentContract.operatorAddressToId[opAddr2]; 12 | 13 | // enforce valid and unique operator_ids in `operatorAddressToId` mapping 14 | require op1AddrToId != op2AddrToId && op1AddrToId > 0 && op2AddrToId > 0; 15 | 16 | // enforce matching addresses in `operatorIdToAddress` mapping 17 | require currentContract.operatorIdToAddress[op1AddrToId] == opAddr1 && 18 | currentContract.operatorIdToAddress[op2AddrToId] == opAddr2; 19 | 20 | // perform any arbitrary successful transaction f() 21 | env e; 22 | method f; 23 | calldataarg args; 24 | f(e, args); 25 | 26 | // verify that no transaction exists which corrupts the uniqueness 27 | // property between operator_id : operator_address 28 | assert currentContract.operatorIdToAddress[op1AddrToId] != 29 | currentContract.operatorIdToAddress[op2AddrToId]; 30 | } -------------------------------------------------------------------------------- /test/11-op-reg/echidna.yaml: -------------------------------------------------------------------------------- 1 | # don't allow fuzzer to use all functions 2 | # since we are using handlers 3 | allContracts: false 4 | 5 | # record fuzzer coverage to see what parts of the code 6 | # fuzzer executes 7 | corpusDir: "./test/11-op-reg/coverage-echidna" 8 | 9 | # prefix of invariant function 10 | prefix: "property_" 11 | 12 | # instruct foundry to compile tests 13 | cryticArgs: ["--foundry-compile-all"] -------------------------------------------------------------------------------- /test/11-op-reg/medusa.json: -------------------------------------------------------------------------------- 1 | { 2 | "fuzzing": { 3 | "workers": 10, 4 | "workerResetLimit": 50, 5 | "_COMMENT_TESTING_1": "changed timeout to limit fuzzing time", 6 | "timeout": 10, 7 | "testLimit": 0, 8 | "shrinkLimit": 500, 9 | "callSequenceLength": 100, 10 | "_COMMENT_TESTING_8": "added directory to store coverage data", 11 | "corpusDirectory": "coverage-medusa", 12 | "coverageEnabled": true, 13 | "_COMMENT_TESTING_2": "added test contract to deploymentOrder", 14 | "targetContracts": ["OpRegCryticTester"], 15 | "predeployedContracts": {}, 16 | "targetContractsBalances": [], 17 | "constructorArgs": {}, 18 | "deployerAddress": "0x30000", 19 | "senderAddresses": [ 20 | "0x10000", 21 | "0x20000", 22 | "0x30000" 23 | ], 24 | "blockNumberDelayMax": 60480, 25 | "blockTimestampDelayMax": 604800, 26 | "blockGasLimit": 125000000, 27 | "transactionGasLimit": 12500000, 28 | "testing": { 29 | "stopOnFailedTest": true, 30 | "stopOnFailedContractMatching": true, 31 | "stopOnNoTests": true, 32 | "testAllContracts": false, 33 | "traceAll": false, 34 | "assertionTesting": { 35 | "enabled": false, 36 | "testViewMethods": false, 37 | "panicCodeConfig": { 38 | "failOnCompilerInsertedPanic": false, 39 | "failOnAssertion": true, 40 | "failOnArithmeticUnderflow": false, 41 | "failOnDivideByZero": false, 42 | "failOnEnumTypeConversionOutOfBounds": false, 43 | "failOnIncorrectStorageAccess": false, 44 | "failOnPopEmptyArray": false, 45 | "failOnOutOfBoundsArrayAccess": false, 46 | "failOnAllocateTooMuchMemory": false, 47 | "failOnCallUninitializedVariable": false 48 | } 49 | }, 50 | "propertyTesting": { 51 | "enabled": true, 52 | "_COMMENT_TESTING_6": "changed prefix to match invariant function", 53 | "testPrefixes": [ 54 | "property_" 55 | ] 56 | }, 57 | "optimizationTesting": { 58 | "enabled": false, 59 | "testPrefixes": [ 60 | "optimize_" 61 | ] 62 | }, 63 | "targetFunctionSignatures": [], 64 | "excludeFunctionSignatures": [] 65 | }, 66 | "chainConfig": { 67 | "codeSizeCheckDisabled": true, 68 | "cheatCodes": { 69 | "cheatCodesEnabled": true, 70 | "enableFFI": false 71 | } 72 | } 73 | }, 74 | "compilation": { 75 | "platform": "crytic-compile", 76 | "platformConfig": { 77 | "_COMMENT_TESTING_7": "changed target to point to main directory where command is run from", 78 | "target": "./../../.", 79 | "solcVersion": "", 80 | "exportDirectory": "", 81 | "args": ["--foundry-compile-all"] 82 | } 83 | }, 84 | "logging": { 85 | "level": "info", 86 | "logDirectory": "" 87 | } 88 | } -------------------------------------------------------------------------------- /test/12-liquidate-dos/LiquidateDosCryticTester.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import { TargetFunctions } from "./TargetFunctions.sol"; 5 | import { CryticAsserts } from "@chimera/CryticAsserts.sol"; 6 | 7 | // configure solc-select to use compiler version: 8 | // solc-select install 0.8.23 9 | // solc-select use 0.8.23 10 | // 11 | // run from base project directory with: 12 | // echidna . --contract LiquidateDosCryticTester --config test/12-liquidate-dos/echidna.yaml 13 | // medusa --config test/12-liquidate-dos/medusa.json fuzz 14 | contract LiquidateDosCryticTester is TargetFunctions, CryticAsserts { 15 | constructor() payable { 16 | setup(); 17 | } 18 | } -------------------------------------------------------------------------------- /test/12-liquidate-dos/LiquidateDosCryticToFoundry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import { TargetFunctions } from "./TargetFunctions.sol"; 5 | import { FoundryAsserts } from "@chimera/FoundryAsserts.sol"; 6 | import { Test } from "forge-std/Test.sol"; 7 | 8 | // run from base project directory with: 9 | // forge test --match-contract LiquidateDosCryticToFoundry 10 | // (if an invariant fails add -vvvvv on the end to see what failed) 11 | // 12 | // get coverage report (see https://medium.com/@rohanzarathustra/forge-coverage-overview-744d967e112f): 13 | // 14 | // 1) forge coverage --report lcov --report-file test/12-liquidate-dos/coverage-foundry.lcov --match-contract LiquidateDosCryticToFoundry 15 | // 2) genhtml test/12-liquidate-dos/coverage-foundry.lcov -o test/12-liquidate-dos/coverage-foundry 16 | // 3) open test/12-liquidate-dos/coverage-foundry/index.html in your browser and 17 | // navigate to the relevant source file to see line-by-line execution records 18 | 19 | contract LiquidateDosCryticToFoundry is Test, TargetFunctions, FoundryAsserts { 20 | function setUp() public { 21 | setup(); 22 | 23 | // Foundry doesn't use config files but does 24 | // the setup programmatically here 25 | 26 | // target the fuzzer on this contract as it will 27 | // contain the handler functions 28 | targetContract(address(this)); 29 | 30 | // handler functions to target during invariant tests 31 | bytes4[] memory selectors = new bytes4[](3); 32 | selectors[0] = this.handler_openPosition.selector; 33 | selectors[1] = this.handler_toggleLiquidations.selector; 34 | selectors[2] = this.handler_liquidate.selector; 35 | 36 | targetSelector(FuzzSelector({ addr: address(this), selectors: selectors })); 37 | } 38 | 39 | function invariant_user_active_markets_correct() public { 40 | t(property_user_active_markets_correct(), "User active markets correct"); 41 | } 42 | 43 | function invariant_property_liquidate_no_unexpected_error() public { 44 | t(property_liquidate_no_unexpected_error(), "Liquidate failed with unexpected error"); 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /test/12-liquidate-dos/Properties.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import { Setup } from "./Setup.sol"; 5 | import { Asserts } from "@chimera/Asserts.sol"; 6 | 7 | abstract contract Properties is Setup, Asserts { 8 | 9 | function property_user_active_markets_correct() public view returns(bool result) { 10 | // for each possible user 11 | for(uint8 i; i uint8 activeMarketCount) userActiveMarketsCount; 18 | mapping(address user => mapping(uint8 marketId => bool userInMarket)) userActiveMarkets; 19 | 20 | // track unexpected errors 21 | bool liquidateUnexpectedError; 22 | 23 | function setup() internal virtual override { 24 | addressPool.push(address(0x1111)); 25 | addressPool.push(address(0x2222)); 26 | addressPool.push(address(0x3333)); 27 | addressPool.push(address(0x4444)); 28 | addressPool.push(address(0x5555)); 29 | addressPool.push(address(0x6666)); 30 | addressPool.push(address(0x7777)); 31 | addressPool.push(address(0x8888)); 32 | addressPool.push(address(0x9999)); 33 | ADDRESS_POOL_LENGTH = uint8(addressPool.length); 34 | 35 | liquidateDos = new LiquidateDos(); 36 | } 37 | } -------------------------------------------------------------------------------- /test/12-liquidate-dos/TargetFunctions.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import { ILiquidateDos } from "../../src/12-liquidate-dos/LiquidateDos.sol"; 5 | import { Properties } from "./Properties.sol"; 6 | import { BaseTargetFunctions } from "@chimera/BaseTargetFunctions.sol"; 7 | import { IHevm, vm } from "@chimera/Hevm.sol"; 8 | 9 | abstract contract TargetFunctions is BaseTargetFunctions, Properties { 10 | 11 | // gets a random non-zero address from `Setup::addressPool` 12 | function _getRandomAddress(uint8 index) internal returns(address addr) { 13 | index = uint8(between(index, 0, ADDRESS_POOL_LENGTH - 1)); 14 | addr = addressPool[index]; 15 | } 16 | 17 | function handler_openPosition(uint8 callerIndex, uint8 marketId) external { 18 | address caller = _getRandomAddress(callerIndex); 19 | 20 | vm.prank(caller); 21 | liquidateDos.openPosition(marketId); 22 | 23 | // update ghost variables 24 | ++userActiveMarketsCount[caller]; 25 | userActiveMarkets[caller][marketId] = true; 26 | } 27 | 28 | function handler_toggleLiquidations(bool toggle) external { 29 | liquidateDos.toggleLiquidations(toggle); 30 | } 31 | 32 | function handler_liquidate(uint8 victimIndex) external { 33 | address victim = _getRandomAddress(victimIndex); 34 | 35 | try liquidateDos.liquidate(victim) { 36 | // update ghost variables 37 | delete userActiveMarketsCount[victim]; 38 | 39 | for(uint8 marketId = liquidateDos.MIN_MARKET_ID(); 40 | marketId <= liquidateDos.MAX_MARKET_ID(); 41 | marketId++) { 42 | delete userActiveMarkets[victim][marketId]; 43 | } 44 | } 45 | catch(bytes memory err) { 46 | bytes4[] memory allowedErrors = new bytes4[](2); 47 | allowedErrors[0] = ILiquidateDos.LiquidationsDisabled.selector; 48 | allowedErrors[1] = ILiquidateDos.LiquidateUserNotInAnyMarkets.selector; 49 | 50 | if(_isUnexpectedError(bytes4(err), allowedErrors)) { 51 | liquidateUnexpectedError = true; 52 | } 53 | } 54 | } 55 | 56 | // returns whether error was unexpected 57 | function _isUnexpectedError( 58 | bytes4 errorSelector, 59 | bytes4[] memory allowedErrors 60 | ) internal pure returns(bool isUnexpectedError) { 61 | for (uint256 i; i < allowedErrors.length; i++) { 62 | if (errorSelector == allowedErrors[i]) { 63 | return false; 64 | } 65 | } 66 | 67 | isUnexpectedError = true; 68 | } 69 | } -------------------------------------------------------------------------------- /test/12-liquidate-dos/echidna.yaml: -------------------------------------------------------------------------------- 1 | # don't allow fuzzer to use all functions 2 | # since we are using handlers 3 | allContracts: false 4 | 5 | # record fuzzer coverage to see what parts of the code 6 | # fuzzer executes 7 | corpusDir: "./test/12-liquidate-dos/coverage-echidna" 8 | 9 | # prefix of invariant function 10 | prefix: "property_" 11 | 12 | # instruct foundry to compile tests 13 | cryticArgs: ["--foundry-compile-all"] -------------------------------------------------------------------------------- /test/12-liquidate-dos/medusa.json: -------------------------------------------------------------------------------- 1 | { 2 | "fuzzing": { 3 | "workers": 10, 4 | "workerResetLimit": 50, 5 | "_COMMENT_TESTING_1": "changed timeout to limit fuzzing time", 6 | "timeout": 30, 7 | "testLimit": 0, 8 | "shrinkLimit": 500, 9 | "callSequenceLength": 100, 10 | "_COMMENT_TESTING_8": "added directory to store coverage data", 11 | "corpusDirectory": "coverage-medusa", 12 | "coverageEnabled": true, 13 | "_COMMENT_TESTING_2": "added test contract to deploymentOrder", 14 | "targetContracts": ["LiquidateDosCryticTester"], 15 | "predeployedContracts": {}, 16 | "targetContractsBalances": [], 17 | "constructorArgs": {}, 18 | "deployerAddress": "0x30000", 19 | "senderAddresses": [ 20 | "0x10000", 21 | "0x20000", 22 | "0x30000" 23 | ], 24 | "blockNumberDelayMax": 60480, 25 | "blockTimestampDelayMax": 604800, 26 | "blockGasLimit": 125000000, 27 | "transactionGasLimit": 12500000, 28 | "testing": { 29 | "stopOnFailedTest": true, 30 | "stopOnFailedContractMatching": true, 31 | "stopOnNoTests": true, 32 | "testAllContracts": false, 33 | "traceAll": false, 34 | "assertionTesting": { 35 | "enabled": false, 36 | "testViewMethods": false, 37 | "panicCodeConfig": { 38 | "failOnCompilerInsertedPanic": false, 39 | "failOnAssertion": false, 40 | "failOnArithmeticUnderflow": false, 41 | "failOnDivideByZero": false, 42 | "failOnEnumTypeConversionOutOfBounds": false, 43 | "failOnIncorrectStorageAccess": false, 44 | "failOnPopEmptyArray": false, 45 | "failOnOutOfBoundsArrayAccess": false, 46 | "failOnAllocateTooMuchMemory": false, 47 | "failOnCallUninitializedVariable": false 48 | } 49 | }, 50 | "propertyTesting": { 51 | "enabled": true, 52 | "_COMMENT_TESTING_6": "changed prefix to match invariant function", 53 | "testPrefixes": [ 54 | "property_" 55 | ] 56 | }, 57 | "optimizationTesting": { 58 | "enabled": false, 59 | "testPrefixes": [ 60 | "optimize_" 61 | ] 62 | }, 63 | "targetFunctionSignatures": [], 64 | "excludeFunctionSignatures": [] 65 | }, 66 | "chainConfig": { 67 | "codeSizeCheckDisabled": true, 68 | "cheatCodes": { 69 | "cheatCodesEnabled": true, 70 | "enableFFI": false 71 | } 72 | } 73 | }, 74 | "compilation": { 75 | "platform": "crytic-compile", 76 | "platformConfig": { 77 | "_COMMENT_TESTING_7": "changed target to point to main directory where command is run from", 78 | "target": "./../../.", 79 | "solcVersion": "", 80 | "exportDirectory": "", 81 | "args": ["--foundry-compile-all"] 82 | } 83 | }, 84 | "logging": { 85 | "level": "info", 86 | "logDirectory": "" 87 | } 88 | } -------------------------------------------------------------------------------- /test/13-stability-pool/Properties.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import { Setup } from "./Setup.sol"; 5 | import { Asserts } from "@chimera/Asserts.sol"; 6 | 7 | abstract contract Properties is Setup, Asserts { 8 | 9 | function property_stability_pool_solvent() public view returns(bool result) { 10 | uint256 totalClaimableRewards; 11 | 12 | // sum total claimable rewards for each possible user 13 | for(uint8 i; i= 1000000000000000000 && user2DebtTokens >= 1000000000000000000 && 42 | user1DebtTokens == user2DebtTokens; 43 | 44 | // both users deposit their debt tokens into the stability pool 45 | env e1; 46 | require e1.msg.sender == spDep1; 47 | provideToSP(e1, user1DebtTokens); 48 | env e2; 49 | require e2.msg.sender == spDep2; 50 | provideToSP(e2, user2DebtTokens); 51 | 52 | // stability pool is used to offset debt from a liquidation 53 | uint256 debtTokensToOffset = require_uint256(user1DebtTokens + user2DebtTokens); 54 | uint256 seizedCollateral = debtTokensToOffset; // 1:1 55 | env e3; 56 | registerLiquidation(e3, debtTokensToOffset, seizedCollateral); 57 | 58 | require(currentContract.collateralToken.balanceOf(e, currentContract) == seizedCollateral); 59 | 60 | // enforce each user is owed same reward since they deposited the same 61 | uint256 rewardPerUser = getDepositorCollateralGain(spDep1); 62 | require rewardPerUser > 0; 63 | require rewardPerUser == getDepositorCollateralGain(spDep2); 64 | 65 | // enforce contract has enough reward tokens to pay both users 66 | require(currentContract.collateralToken.balanceOf(e, currentContract) >= require_uint256(rewardPerUser * 2)); 67 | 68 | // first user withdraws their reward 69 | env e4; 70 | require e4.msg.sender == spDep1; 71 | claimCollateralGains(e4); 72 | 73 | // enforce contract has enough reward tokens to pay second user 74 | require(currentContract.collateralToken.balanceOf(e, currentContract) >= rewardPerUser); 75 | 76 | // first user perform any arbitrary successful transaction f() 77 | env e5; 78 | require e5.msg.sender == spDep1; 79 | method f; 80 | calldataarg args; 81 | f(e5, args); 82 | 83 | // second user withdraws their reward 84 | env e6; 85 | require e6.msg.sender == spDep2 && 86 | e6.msg.value == 0; 87 | claimCollateralGains@withrevert(e6); 88 | 89 | // verify first user was not able to do anything that would make 90 | // second user's withdrawal revert 91 | assert !lastReverted; 92 | } -------------------------------------------------------------------------------- /test/13-stability-pool/echidna.yaml: -------------------------------------------------------------------------------- 1 | # don't allow fuzzer to use all functions 2 | # since we are using handlers 3 | allContracts: false 4 | 5 | # record fuzzer coverage to see what parts of the code 6 | # fuzzer executes 7 | corpusDir: "./test/13-stability-pool/coverage-echidna" 8 | 9 | # prefix of invariant function 10 | prefix: "property_" 11 | 12 | # instruct foundry to compile tests 13 | cryticArgs: ["--foundry-compile-all"] -------------------------------------------------------------------------------- /test/13-stability-pool/medusa.json: -------------------------------------------------------------------------------- 1 | { 2 | "fuzzing": { 3 | "workers": 10, 4 | "workerResetLimit": 50, 5 | "_COMMENT_TESTING_1": "changed timeout to limit fuzzing time", 6 | "timeout": 30, 7 | "testLimit": 0, 8 | "shrinkLimit": 500, 9 | "callSequenceLength": 100, 10 | "_COMMENT_TESTING_8": "added directory to store coverage data", 11 | "corpusDirectory": "coverage-medusa", 12 | "coverageEnabled": true, 13 | "_COMMENT_TESTING_2": "added test contract to deploymentOrder", 14 | "targetContracts": ["StabilityPoolCryticTester"], 15 | "predeployedContracts": {}, 16 | "targetContractsBalances": [], 17 | "constructorArgs": {}, 18 | "deployerAddress": "0x30000", 19 | "senderAddresses": [ 20 | "0x10000", 21 | "0x20000", 22 | "0x30000" 23 | ], 24 | "blockNumberDelayMax": 60480, 25 | "blockTimestampDelayMax": 604800, 26 | "blockGasLimit": 125000000, 27 | "transactionGasLimit": 12500000, 28 | "testing": { 29 | "stopOnFailedTest": true, 30 | "stopOnFailedContractMatching": true, 31 | "stopOnNoTests": true, 32 | "testAllContracts": false, 33 | "traceAll": false, 34 | "assertionTesting": { 35 | "enabled": false, 36 | "testViewMethods": false, 37 | "panicCodeConfig": { 38 | "failOnCompilerInsertedPanic": false, 39 | "failOnAssertion": false, 40 | "failOnArithmeticUnderflow": false, 41 | "failOnDivideByZero": false, 42 | "failOnEnumTypeConversionOutOfBounds": false, 43 | "failOnIncorrectStorageAccess": false, 44 | "failOnPopEmptyArray": false, 45 | "failOnOutOfBoundsArrayAccess": false, 46 | "failOnAllocateTooMuchMemory": false, 47 | "failOnCallUninitializedVariable": false 48 | } 49 | }, 50 | "propertyTesting": { 51 | "enabled": true, 52 | "_COMMENT_TESTING_6": "changed prefix to match invariant function", 53 | "testPrefixes": [ 54 | "property_" 55 | ] 56 | }, 57 | "optimizationTesting": { 58 | "enabled": false, 59 | "testPrefixes": [ 60 | "optimize_" 61 | ] 62 | }, 63 | "targetFunctionSignatures": [], 64 | "excludeFunctionSignatures": [] 65 | }, 66 | "chainConfig": { 67 | "codeSizeCheckDisabled": true, 68 | "cheatCodes": { 69 | "cheatCodesEnabled": true, 70 | "enableFFI": false 71 | } 72 | } 73 | }, 74 | "compilation": { 75 | "platform": "crytic-compile", 76 | "platformConfig": { 77 | "_COMMENT_TESTING_7": "changed target to point to main directory where command is run from", 78 | "target": "./../../.", 79 | "solcVersion": "", 80 | "exportDirectory": "", 81 | "args": ["--foundry-compile-all"] 82 | } 83 | }, 84 | "logging": { 85 | "level": "info", 86 | "logDirectory": "" 87 | } 88 | } -------------------------------------------------------------------------------- /test/14-priority/PriorityCryticTester.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import { TargetFunctions } from "./TargetFunctions.sol"; 5 | import { CryticAsserts } from "@chimera/CryticAsserts.sol"; 6 | 7 | // configure solc-select to use compiler version: 8 | // solc-select install 0.8.23 9 | // solc-select use 0.8.23 10 | // 11 | // run from base project directory with: 12 | // echidna . --contract PriorityCryticTester --config test/14-priority/echidna.yaml 13 | // medusa --config test/14-priority/medusa.json fuzz 14 | contract PriorityCryticTester is TargetFunctions, CryticAsserts { 15 | constructor() payable { 16 | setup(); 17 | } 18 | } -------------------------------------------------------------------------------- /test/14-priority/PriorityCryticToFoundry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import { TargetFunctions } from "./TargetFunctions.sol"; 5 | import { FoundryAsserts } from "@chimera/FoundryAsserts.sol"; 6 | import { Test } from "forge-std/Test.sol"; 7 | 8 | // run from base project directory with: 9 | // forge test --match-contract PriorityCryticToFoundry 10 | // (if an invariant fails add -vvvvv on the end to see what failed) 11 | // 12 | // get coverage report (see https://medium.com/@rohanzarathustra/forge-coverage-overview-744d967e112f): 13 | // 14 | // 1) forge coverage --report lcov --report-file test/14-priority/coverage-foundry.lcov --match-contract PriorityCryticToFoundry 15 | // 2) genhtml test/14-priority/coverage-foundry.lcov -o test/14-priority/coverage-foundry 16 | // 3) open test/14-priority/coverage-foundry/index.html in your browser and 17 | // navigate to the relevant source file to see line-by-line execution records 18 | 19 | contract PriorityCryticToFoundry is Test, TargetFunctions, FoundryAsserts { 20 | function setUp() public { 21 | setup(); 22 | 23 | // Foundry doesn't use config files but does 24 | // the setup programmatically here 25 | 26 | // target the fuzzer on this contract as it will 27 | // contain the handler functions 28 | targetContract(address(this)); 29 | 30 | // handler functions to target during invariant tests 31 | bytes4[] memory selectors = new bytes4[](2); 32 | selectors[0] = this.handler_addCollateral.selector; 33 | selectors[1] = this.handler_removeCollateral.selector; 34 | 35 | targetSelector(FuzzSelector({ addr: address(this), selectors: selectors })); 36 | } 37 | 38 | function invariant_property_priority_order_correct() public { 39 | t(property_priority_order_correct(), "Collateral priority order maintained"); 40 | } 41 | } -------------------------------------------------------------------------------- /test/14-priority/Properties.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import { Setup } from "./Setup.sol"; 5 | import { Asserts } from "@chimera/Asserts.sol"; 6 | 7 | abstract contract Properties is Setup, Asserts { 8 | 9 | function property_priority_order_correct() public view returns(bool result) { 10 | if(priority0 != 0) { 11 | if(priority.getCollateralAtPriority(0) != priority0) return false; 12 | } 13 | if(priority1 != 0) { 14 | if(priority.getCollateralAtPriority(1) != priority1) return false; 15 | } 16 | if(priority2 != 0) { 17 | if(priority.getCollateralAtPriority(2) != priority2) return false; 18 | } 19 | if(priority3 != 0) { 20 | if(priority.getCollateralAtPriority(3) != priority3) return false; 21 | } 22 | 23 | result = true; 24 | } 25 | } -------------------------------------------------------------------------------- /test/14-priority/Setup.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import { Priority } from "../../src/14-priority/Priority.sol"; 5 | import { BaseSetup } from "@chimera/BaseSetup.sol"; 6 | 7 | abstract contract Setup is BaseSetup { 8 | // contract being tested 9 | Priority priority; 10 | 11 | // ghost variables 12 | uint8 priority0; 13 | uint8 priority1; 14 | uint8 priority2; 15 | uint8 priority3; 16 | 17 | function setup() internal virtual override { 18 | priority = new Priority(); 19 | } 20 | } -------------------------------------------------------------------------------- /test/14-priority/TargetFunctions.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import { Properties } from "./Properties.sol"; 5 | import { BaseTargetFunctions } from "@chimera/BaseTargetFunctions.sol"; 6 | import { IHevm, vm } from "@chimera/Hevm.sol"; 7 | 8 | abstract contract TargetFunctions is BaseTargetFunctions, Properties { 9 | 10 | function handler_addCollateral(uint8 collateralId) external { 11 | collateralId = uint8(between(collateralId, 12 | priority.MIN_COLLATERAL_ID(), 13 | priority.MAX_COLLATERAL_ID())); 14 | 15 | priority.addCollateral(collateralId); 16 | 17 | // update ghost variables with expected order 18 | if(priority0 == 0) priority0 = collateralId; 19 | else if(priority1 == 0) priority1 = collateralId; 20 | else if(priority2 == 0) priority2 = collateralId; 21 | else priority3 = collateralId; 22 | } 23 | 24 | function handler_removeCollateral(uint8 collateralId) external { 25 | collateralId = uint8(between(collateralId, 26 | priority.MIN_COLLATERAL_ID(), 27 | priority.MAX_COLLATERAL_ID())); 28 | 29 | priority.removeCollateral(collateralId); 30 | 31 | // update ghost variables with expected order 32 | if(priority0 == collateralId) { 33 | priority0 = priority1; 34 | priority1 = priority2; 35 | priority2 = priority3; 36 | } 37 | else if(priority1 == collateralId) { 38 | priority1 = priority2; 39 | priority2 = priority3; 40 | } 41 | else if(priority2 == collateralId) { 42 | priority2 = priority3; 43 | } 44 | 45 | delete priority3; 46 | } 47 | } -------------------------------------------------------------------------------- /test/14-priority/certora.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "src/14-priority/Priority.sol" 4 | ], 5 | "verify": "Priority:test/14-priority/certora.spec", 6 | "packages":[ 7 | "@openzeppelin=lib/openzeppelin-contracts" 8 | ], 9 | } -------------------------------------------------------------------------------- /test/14-priority/certora.spec: -------------------------------------------------------------------------------- 1 | // run from base folder: 2 | // certoraRun test/14-priority/certora.conf 3 | methods { 4 | // `envfree` definitions to call functions without explicit `env` 5 | function getCollateralAtPriority(uint8) external returns(uint8) envfree; 6 | function containsCollateral(uint8) external returns(bool) envfree; 7 | function numCollateral() external returns(uint256) envfree; 8 | } 9 | 10 | // verify priority queue order is correctly maintained: 11 | // - `addCollateral` adds new id to end of the queue 12 | // - `removeCollateral` maintains existing order minus removed id 13 | rule priority_order_correct() { 14 | // require no initial collateral to prevent HAVOC corrupting 15 | // EnumerableSet _positions -> _values references 16 | require numCollateral() == 0 && !containsCollateral(1) && 17 | !containsCollateral(2) && !containsCollateral(3); 18 | 19 | // setup initial state with collateral_id order: 1,2,3 which ensures 20 | // EnumerableSet _positions -> _values references are correct 21 | env e1; 22 | addCollateral(e1, 1); 23 | addCollateral(e1, 2); 24 | addCollateral(e1, 3); 25 | 26 | // sanity check initial state 27 | assert numCollateral() == 3 && containsCollateral(1) && 28 | containsCollateral(2) && containsCollateral(3); 29 | 30 | // sanity check initial order; this also verifies that 31 | // `addCollateral` worked as expected 32 | assert getCollateralAtPriority(0) == 1 && 33 | getCollateralAtPriority(1) == 2 && 34 | getCollateralAtPriority(2) == 3; 35 | 36 | // successfully remove first id 1 37 | removeCollateral(e1, 1); 38 | 39 | // verify it was removed and other ids still exist 40 | assert numCollateral() == 2 && !containsCollateral(1) && 41 | containsCollateral(2) && containsCollateral(3); 42 | 43 | // assert existing order 2,3 preserved minus the removed first id 1 44 | assert getCollateralAtPriority(0) == 2 && 45 | getCollateralAtPriority(1) == 3; 46 | } -------------------------------------------------------------------------------- /test/14-priority/echidna.yaml: -------------------------------------------------------------------------------- 1 | # don't allow fuzzer to use all functions 2 | # since we are using handlers 3 | allContracts: false 4 | 5 | # record fuzzer coverage to see what parts of the code 6 | # fuzzer executes 7 | corpusDir: "./test/14-priority/coverage-echidna" 8 | 9 | # prefix of invariant function 10 | prefix: "property_" 11 | 12 | # instruct foundry to compile tests 13 | cryticArgs: ["--foundry-compile-all"] -------------------------------------------------------------------------------- /test/14-priority/medusa.json: -------------------------------------------------------------------------------- 1 | { 2 | "fuzzing": { 3 | "workers": 10, 4 | "workerResetLimit": 50, 5 | "_COMMENT_TESTING_1": "changed timeout to limit fuzzing time", 6 | "timeout": 30, 7 | "testLimit": 0, 8 | "shrinkLimit": 500, 9 | "callSequenceLength": 100, 10 | "_COMMENT_TESTING_8": "added directory to store coverage data", 11 | "corpusDirectory": "coverage-medusa", 12 | "coverageEnabled": true, 13 | "_COMMENT_TESTING_2": "added test contract to deploymentOrder", 14 | "targetContracts": ["PriorityCryticTester"], 15 | "predeployedContracts": {}, 16 | "targetContractsBalances": [], 17 | "constructorArgs": {}, 18 | "deployerAddress": "0x30000", 19 | "senderAddresses": [ 20 | "0x10000", 21 | "0x20000", 22 | "0x30000" 23 | ], 24 | "blockNumberDelayMax": 60480, 25 | "blockTimestampDelayMax": 604800, 26 | "blockGasLimit": 125000000, 27 | "transactionGasLimit": 12500000, 28 | "testing": { 29 | "stopOnFailedTest": true, 30 | "stopOnFailedContractMatching": true, 31 | "stopOnNoTests": true, 32 | "testAllContracts": false, 33 | "traceAll": false, 34 | "assertionTesting": { 35 | "enabled": false, 36 | "testViewMethods": false, 37 | "panicCodeConfig": { 38 | "failOnCompilerInsertedPanic": false, 39 | "failOnAssertion": false, 40 | "failOnArithmeticUnderflow": false, 41 | "failOnDivideByZero": false, 42 | "failOnEnumTypeConversionOutOfBounds": false, 43 | "failOnIncorrectStorageAccess": false, 44 | "failOnPopEmptyArray": false, 45 | "failOnOutOfBoundsArrayAccess": false, 46 | "failOnAllocateTooMuchMemory": false, 47 | "failOnCallUninitializedVariable": false 48 | } 49 | }, 50 | "propertyTesting": { 51 | "enabled": true, 52 | "_COMMENT_TESTING_6": "changed prefix to match invariant function", 53 | "testPrefixes": [ 54 | "property_" 55 | ] 56 | }, 57 | "optimizationTesting": { 58 | "enabled": false, 59 | "testPrefixes": [ 60 | "optimize_" 61 | ] 62 | }, 63 | "targetFunctionSignatures": [], 64 | "excludeFunctionSignatures": [] 65 | }, 66 | "chainConfig": { 67 | "codeSizeCheckDisabled": true, 68 | "cheatCodes": { 69 | "cheatCodesEnabled": true, 70 | "enableFFI": false 71 | } 72 | } 73 | }, 74 | "compilation": { 75 | "platform": "crytic-compile", 76 | "platformConfig": { 77 | "_COMMENT_TESTING_7": "changed target to point to main directory where command is run from", 78 | "target": "./../../.", 79 | "solcVersion": "", 80 | "exportDirectory": "", 81 | "args": ["--foundry-compile-all"] 82 | } 83 | }, 84 | "logging": { 85 | "level": "info", 86 | "logDirectory": "" 87 | } 88 | } -------------------------------------------------------------------------------- /test/TestUtils.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | // adapted from https://github.com/crytic/properties/blob/main/contracts/util/PropertiesHelper.sol#L240-L259 5 | library TestUtils { 6 | 7 | // platform-agnostic input restriction to easily 8 | // port fuzz tests between different fuzzers 9 | function clampBetween(uint256 value, 10 | uint256 low, 11 | uint256 high 12 | ) internal pure returns (uint256) { 13 | if (value < low || value > high) { 14 | return (low + (value % (high - low + 1))); 15 | } 16 | return value; 17 | } 18 | } 19 | 20 | --------------------------------------------------------------------------------