main 125071f38b63 cached
156 files
419.9 KB
107.2k tokens
1 requests
Download .txt
Showing preview only (460K chars total). Download the full file or copy to clipboard to get everything.
Repository: devdacian/solidity-fuzzing-comparison
Branch: main
Commit: 125071f38b63
Files: 156
Total size: 419.9 KB

Directory structure:
gitextract_67c2mjes/

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

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/test.yml
================================================
name: test

on: workflow_dispatch

env:
  FOUNDRY_PROFILE: ci

jobs:
  check:
    strategy:
      fail-fast: true

    name: Foundry project
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive

      - name: Install Foundry
        uses: foundry-rs/foundry-toolchain@v1
        with:
          version: nightly

      - name: Run Forge build
        run: |
          forge --version
          forge build --sizes
        id: build

      - name: Run Forge tests
        run: |
          forge test -vvv
        id: test


================================================
FILE: .gitignore
================================================
# Compiler files
cache/
out/
crytic-export/

# Ignores development broadcast logs
!/broadcast
/broadcast/*/31337/
/broadcast/**/dry-run/

# Docs
docs/

# Dotenv file
.env

# test coverage files
**/coverage**

# misc
.DS_Store
.certora_internal
slither_results.json

================================================
FILE: .gitmodules
================================================
[submodule "lib/forge-std"]
	path = lib/forge-std
	url = https://github.com/foundry-rs/forge-std
[submodule "lib/openzeppelin-contracts"]
	path = lib/openzeppelin-contracts
	url = https://github.com/OpenZeppelin/openzeppelin-contracts
[submodule "lib/openzeppelin-contracts-upgradeable"]
	path = lib/openzeppelin-contracts-upgradeable
	url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
[submodule "lib/chimera"]
	path = lib/chimera
	url = https://github.com/Recon-Fuzz/chimera


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2023 Dacian

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Solidity Fuzzing Challenge: Foundry vs Echidna vs Medusa (plus Halmos & Certora) #

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.

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.

## Setup ##

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).

Configure [solc-select](https://github.com/crytic/solc-select) for Echidna & Medusa:

`solc-select install 0.8.23`\
`solc-select use 0.8.23`

To compile this project:

`forge build`

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.

## Results ##

### Challenge #1 Naive Receiver: (Winner TIED ALL) ###

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.

### Challenge #2 Unstoppable: (Winner TIED ALL) ###

All Fuzzers in `basic` configuration can break both invariants; Foundry appears to be the slightly faster.

### Challenge #3 Proposal: (Winner TIED ALL) ###

Foundry, Echidna & Medusa in `basic` mode are able to easily break the invariant, resulting in a tie.

### Challenge #4 Voting NFT: (Winner TIED ALL) ###

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.

### Challenge #5 Token Sale: (Winner MEDUSA) ###

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.

### Challenge #6 Rarely False: (Winner TIED HALMOS & CERTORA) ###

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.

### Challenge #7 Byte Battle: (Winner TIED ALL)

All tools are able to quickly break this challenge.

### Challenge #8 Omni Protocol: (Winner MEDUSA)

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.

### Challenge #9 -> #14

Some additional solvers have been added based upon real-world findings from my private audits.


================================================
FILE: foundry.toml
================================================
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
show_progress = true

# include remappings
remappings = [
    "@openzeppelin/=lib/openzeppelin-contracts/",
    "@openzeppelin-upgradeable/=lib/openzeppelin-contracts-upgradeable/",
    "@chimera/=lib/chimera/src/",
]

[fuzz]
runs = 500
max_test_rejects = 999999999

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options


================================================
FILE: src/01-naive-receiver/FlashLoanReceiver.sol
================================================
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.23;

import "@openzeppelin/contracts/utils/Address.sol";

/**
 * @title FlashLoanReceiver
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
contract FlashLoanReceiver {
    using Address for address payable;

    address payable private pool;

    constructor(address payable poolAddress) {
        pool = poolAddress;
    }

    // Function called by the pool during flash loan
    function receiveEther(uint256 fee) public payable {
        require(msg.sender == pool, "Sender must be pool");

        uint256 amountToBeRepaid = msg.value + fee;

        require(address(this).balance >= amountToBeRepaid, "Cannot borrow that much");
        
        _executeActionDuringFlashLoan();
        
        // Return funds to pool
        pool.sendValue(amountToBeRepaid);
    }

    // Internal function where the funds received are used
    function _executeActionDuringFlashLoan() internal { }

    // Allow deposits of ETH
    receive () external payable {}
}

================================================
FILE: src/01-naive-receiver/NaiveReceiverLenderPool.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Address.sol";

/**
 * @title NaiveReceiverLenderPool
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
contract NaiveReceiverLenderPool is ReentrancyGuard {

    using Address for address;

    uint256 private constant FIXED_FEE = 1 ether; // not the cheapest flash loan

    function fixedFee() external pure returns (uint256) {
        return FIXED_FEE;
    }

    function flashLoan(address borrower, uint256 borrowAmount) external nonReentrant {

        uint256 balanceBefore = address(this).balance;
        require(balanceBefore >= borrowAmount, "Not enough ETH in pool");


        require(borrower.code.length > 0, "Borrower must be a deployed contract");
        // Transfer ETH and handle control to receiver
        borrower.functionCallWithValue(
            abi.encodeWithSignature(
                "receiveEther(uint256)",
                FIXED_FEE
            ),
            borrowAmount
        );
        
        require(
            address(this).balance >= balanceBefore + FIXED_FEE,
            "Flash loan hasn't been paid back"
        );
    }

    // Allow deposits of ETH
    receive () external payable {}
}


================================================
FILE: src/02-unstoppable/ReceiverUnstoppable.sol
================================================
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.23;

import "./UnstoppableLender.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

/**
 * @title ReceiverUnstoppable
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
contract ReceiverUnstoppable {

    UnstoppableLender private immutable pool;
    address private immutable owner;

    constructor(address poolAddress) {
        pool = UnstoppableLender(poolAddress);
        owner = msg.sender;
    }

    // Pool will call this function during the flash loan
    function receiveTokens(address tokenAddress, uint256 amount) external {
        require(msg.sender == address(pool), "Sender must be pool");
        // Return all tokens to the pool
        require(IERC20(tokenAddress).transfer(msg.sender, amount), "Transfer of tokens failed");
    }

    function executeFlashLoan(uint256 amount) external {
        require(msg.sender == owner, "Only owner can execute flash loan");
        pool.flashLoan(amount);
    }
}

================================================
FILE: src/02-unstoppable/UnstoppableLender.sol
================================================
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.23;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

interface IReceiver {
    function receiveTokens(address tokenAddress, uint256 amount) external;
}

/**
 * @title UnstoppableLender
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
contract UnstoppableLender is ReentrancyGuard {

    IERC20 public immutable damnValuableToken;
    uint256 public poolBalance;

    constructor(address tokenAddress) {
        require(tokenAddress != address(0), "Token address cannot be zero");
        damnValuableToken = IERC20(tokenAddress);
    }

    function depositTokens(uint256 amount) external nonReentrant {
        require(amount > 0, "Must deposit at least one token");
        // Transfer token from sender. Sender must have first approved them.
        damnValuableToken.transferFrom(msg.sender, address(this), amount);
        poolBalance = poolBalance + amount;
    }

    function flashLoan(uint256 borrowAmount) external nonReentrant {
        require(borrowAmount > 0, "Must borrow at least one token");

        uint256 balanceBefore = damnValuableToken.balanceOf(address(this));
        require(balanceBefore >= borrowAmount, "Not enough tokens in pool");

        // Ensured by the protocol via the `depositTokens` function
        assert(poolBalance == balanceBefore);
        
        damnValuableToken.transfer(msg.sender, borrowAmount);
        
        IReceiver(msg.sender).receiveTokens(address(damnValuableToken), borrowAmount);
        
        uint256 balanceAfter = damnValuableToken.balanceOf(address(this));
        require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back");
    }
}


================================================
FILE: src/03-proposal/Proposal.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import "@openzeppelin/contracts/utils/math/Math.sol";

//
// This contract is a simplified version of a real contract which
// was audited by Cyfrin in a private audit and contained the same bug.
//
// Your mission, should you choose to accept it, is to find that bug!
//
// This contract allows the creator to invite a select group of people
// to vote on something and provides an eth reward to the `for` voters
// if the proposal passes, otherwise refunds the reward to the creator.
// The creator of the contract is considered "Trusted".
//
// This contract has been intentionally simplified to remove much of
// the extra complexity in order to help you find the particular bug without
// other distractions. Please read the comments carefully as they note
// specific findings that are excluded as the implementation has been
// purposefully kept simple to help you focus on finding the harder
// to find and more interesting bug.
//
// This contract intentionally has no time-out period for the voting
// to complete; lack of a time-out period resulting in voting never
// completing is not a valid finding as this has been intentionally 
// omitted to simplify the codebase.
//
// This contract should only contain 1 intentional High finding, but
// if you find others they were not intentional :-) This contract should
// not be used in any live/production environment; it is purely an
// educational bug-hunting exercise based on a real-world example.
//
contract Proposal {
    // smallest amount proposal creator can fund contract with
    uint256 private constant MIN_FUNDING = 1 ether;

    // min/max number of voters
    uint256 private constant MIN_VOTERS  = 3;
    uint256 private constant MAX_VOTERS  = 9;

    // min quorum
    uint256 private constant MIN_QUORUM  = 51;

    // constants used for `voterState` in `s_voters` mapping
    uint8 private constant DISALLOWED    = 0;
    uint8 private constant ALLOWED       = 1;
    uint8 private constant VOTED         = 2;

    // only permitted addresses can vote, each address gets 1 vote
    mapping(address voter => uint8 voterState) private s_voters;

    // creator of this proposal. Any findings related to the creator
    // not being able to update this address are invalid; this has
    // intentionally been omitted to simplify the contract so you can
    // focus on finding the cool bug instead of lame/easy stuff. Proposal
    // creator is trusted to create the proposal from an address that
    // can receive eth
    address private s_creator;

    // total number of allowed voters
    uint256 private s_totalAllowedVoters;

    // total number of current votes
    uint256 private s_totalCurrentVotes;

    // list of users who voted for
    address[] private s_votersFor;

    // list of users who votes against
    address[] private s_votersAgainst;

    // whether voting has been completed
    bool private s_votingComplete;

    // create the contract
    constructor(address[] memory allowList) payable {
        // require minimum eth proposal reward
        require(msg.value >= MIN_FUNDING, "DP: Minimum 1 eth proposal reward required");

        // cache list length
        uint256 allowListLength = allowList.length;

        // perform some sanity checks. NOTE: checks for duplicate inputs
        // are performed by entity creating the proposal who is
        // supplying the eth and is trusted, so the contract intentionally
        // does not re-check for duplicate inputs. Findings related to
        // not checking for duplicate inputs are invalid.
        require(allowListLength >= MIN_VOTERS, "DP: Minimum 3 voters required");
        require(allowListLength <= MAX_VOTERS, "DP: Maximum 9 voters allowed");

        // odd number of voters required to simplify quorum check
        require(allowListLength % 2 != 0, "DP: Odd number of voters required");

        // cache total voters to prevent multiple storage writes
        uint256 totalVoters;

        // store addresses allowed to vote on this proposal
        for(; totalVoters<allowListLength; ++totalVoters) {
            // sanity check to prevent address(0) as a valid voter
            address voter = allowList[totalVoters];
            require(voter != address(0), "DP: address(0) not a valid voter");

            s_voters[voter] = ALLOWED;
        }

        // update storage of total voters only once
        s_totalAllowedVoters = totalVoters;

        // update the proposal creator
        s_creator = msg.sender;

        // eth stored in this contract to be distributed once
        // voting is complete
    }

    // record a vote
    function vote(bool voteInput) external {
        // prevent voting if already completed
        require(isActive(), "DP: voting has been completed on this proposal");

        // current voter
        address voter = msg.sender;

        // prevent voting if not allowed or already voted
        require(s_voters[voter] == ALLOWED, "DP: voter not allowed or already voted");

        // update storage to record that this user has voted
        s_voters[voter] = VOTED;

        // update storage to increment total current votes
        // and store new value on the stack
        uint256 totalCurrentVotes = ++s_totalCurrentVotes;

        // add user to either the `for` or `against` list
        if(voteInput) s_votersFor.push(voter);
        else s_votersAgainst.push(voter);

        // check if quorum has been reached. Quorum is reached
        // when at least 51% of the total allowed voters have cast
        // their vote. For example if there are 5 allowed voters:
        //
        // first votes For
        // second votes For
        // third votes Against
        //
        // Quorum has now been reached (3/5) and the vote will pass as
        // votesFor (2) > votesAgainst (1).
        //
        // This system of voting doesn't require a strict majority to
        // pass the proposal (it didn't require 3 For votes), it just
        // requires the quorum to be reached (enough people to vote)
        //
        if(totalCurrentVotes * 100 / s_totalAllowedVoters >= MIN_QUORUM) {
            // mark voting as having been completed
            s_votingComplete = true;

            // distribute the voting rewards
            _distributeRewards();
        }
    }

    // distributes rewards to the `for` voters if the proposal has
    // passed or refunds the rewards back to the creator if the proposal
    // failed
    function _distributeRewards() private {
        // get number of voters for & against
        uint256 totalVotesFor     = s_votersFor.length;
        uint256 totalVotesAgainst = s_votersAgainst.length;
        uint256 totalVotes        = totalVotesFor + totalVotesAgainst;

        // rewards to distribute or refund. This is guaranteed to be
        // greater or equal to the minimum funding amount by a check
        // in the constructor, and there is intentionally by design
        // no way to decrease or increase this amount. Any findings
        // related to not being able to increase/decrease the total
        // reward amount are invalid
        uint256 totalRewards = address(this).balance;

        // if the proposal was defeated refund reward back to the creator
        // for the proposal to be successful it must have had more `For` votes
        // than `Against` votes
        if(totalVotesAgainst >= totalVotesFor) {
            // proposal creator is trusted to create a proposal from an address
            // that can receive ETH. See comment before declaration of `s_creator`
            _sendEth(s_creator, totalRewards);
        }
        // otherwise the proposal passed so distribute rewards to the `For` voters
        else{
            uint256 rewardPerVoter = totalRewards / totalVotes;

            for(uint256 i; i<totalVotesFor; ++i) {
                // proposal creator is trusted when creating allowed list of voters,
                // findings related to gas griefing attacks or sending eth
                // to an address reverting thereby stopping the reward payouts are
                // invalid. Yes pull is to be preferred to push but this
                // has not been implemented in this simplified version to
                // reduce complexity & help you focus on finding the
                // harder to find bug

                // if at the last voter round up to avoid leaving dust; this means that
                // the last voter can get 1 wei more than the rest - this is not
                // a valid finding, it is simply how we deal with imperfect division
                if(i == totalVotesFor-1) {
                    rewardPerVoter = Math.mulDiv(totalRewards, 1, totalVotes, Math.Rounding.Ceil);
                }
                _sendEth(s_votersFor[i], rewardPerVoter);
            }
        }
    }

    // sends eth using low-level call as we don't care about returned data
    function _sendEth(address dest, uint256 amount) private {
        bool sendStatus;
        assembly {
            sendStatus := call(gas(), dest, amount, 0, 0, 0, 0)
        }
        require(sendStatus, "DP: failed to send eth");
    }

    // returns true if the proposal is active or false if finished,
    // used internally and also externally to validate setup
    function isActive() public view returns(bool) {
        return !s_votingComplete;
    }

    // returns total number of allowed voters, used externally to validate setup
    function getTotalAllowedVoters() external view returns(uint256) {
        return s_totalAllowedVoters;
    }
    
    // returns the proposal creator, used externally to validate setup
    function getCreator() external view returns(address) {
        return s_creator;
    }
}

================================================
FILE: src/04-voting-nft/VotingNft.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";

// using regular and old Ownable for simplicity, any findings
// related to using newer Ownable or 2-step are invalid
import "@openzeppelin/contracts/access/Ownable.sol";

//
// This contract is a simplified version of a real contract which
// was audited by Cyfrin in a private audit and contained the same bug.
//
// Your mission, should you choose to accept it, is to find that bug!
//
// This is an nft contract that allows users to have nfts which
// have voting power in a DAO. These nfts can lose power over time if
// the required collateral is not deposited. The power of these nfts
// can never increase, only decrease or remain the same. 
//
// This contract has been intentionally simplified to remove much of
// the extra complexity in order to help you find the particular bug without
// other distractions. Please read the comments carefully as they note
// specific findings that are excluded as the implementation has been
// purposefully kept simple to help you focus on finding the harder
// to find and more interesting bug.
//
// This contract should only contain 1 intentional High finding, but
// if you find others they were not intentional :-) This contract should
// not be used in any live/production environment; it is purely an
// educational bug-hunting exercise based on a real-world example.
//
contract VotingNft is ERC721Enumerable, Ownable {

    // useful constants
    uint256 private constant PERCENTAGE_100 = 10 ** 27;

    // required collateral in eth to be deposited per NFT in order to
    // prevent voting power from decreasing
    uint256 private s_requiredCollateral;

    // time when power calculation begins
    uint256 private s_powerCalcTimestamp;

    // max power an nft can have. Power can only decrease or stay the same
    uint256 private s_maxNftPower;

    // % by which nft power decreases if required collateral not deposited
    uint256 private s_nftPowerReductionPercent;

    // current total power; will increase before power calculation starts
    // as nfts are created. once power calculation starts can only decrease
    // if nfts don't have the required collateral deposited
    uint256 private s_totalPower;

    // current total collateral which has been deposited for nfts
    uint256 private s_totalCollateral;

    struct NftInfo {
        uint256 lastUpdate;
        uint256 currentPower;
        uint256 currentCollateral;
    }
    // keeps track of contract-specific nft info
    mapping(uint256 tokenId => NftInfo) s_nftInfo;


    // create the contract
    constructor(
        uint256 requiredCollateral,
        uint256 powerCalcTimestamp,
        uint256 maxNftPower,
        uint256 nftPowerReductionPercent) 
        ERC721("VNFT", "VNFT")
        Ownable(msg.sender) {

        // input sanity checks
        require(requiredCollateral > 0, "VNFT: required collateral must be > 0");
        require(powerCalcTimestamp > block.timestamp, "VNFT: power calc timestamp must be in the future");
        require(maxNftPower > 0, "VNFT: max nft power must be > 0");
        require(nftPowerReductionPercent > 0, "VNFT: nft power reduction must be > 0");
        require(nftPowerReductionPercent < PERCENTAGE_100, "VNFT: nft power reduction too big");

        s_requiredCollateral = requiredCollateral;
        s_powerCalcTimestamp = powerCalcTimestamp;
        s_maxNftPower        = maxNftPower;
        s_nftPowerReductionPercent = nftPowerReductionPercent;
    }


    // some operations can only be performed before
    // power calculation has started 
    modifier onlyBeforePowerCalc() {
        _onlyBeforePowerCalc();
        _;
    }
    function _onlyBeforePowerCalc() private view {
        require(
            block.timestamp < s_powerCalcTimestamp,
            "VNFT: power calculation has already started"
        );
    }
    
    // allows contract owner to mint nfts to an address
    // can only be called before power calculation starts
    // all new nfts start at max power
    function safeMint(address to, uint256 tokenId) external onlyOwner onlyBeforePowerCalc {
        _safeMint(to, tokenId, "");

        s_totalPower += s_maxNftPower;
    }

    // allows nft holders to deposit collateral for their nft
    // nfts which have their required collateral deposited
    // don't lose power
    function addCollateral(uint256 tokenId) external payable {
        // sanity checks
        require(ownerOf(tokenId) == msg.sender, "VNFT: only nft owner can deposit collateral");

        uint256 amount = msg.value;
        require(amount > 0, "VNFT: collateral deposit amount must be > 0");

        require(s_nftInfo[tokenId].currentCollateral + amount <= s_requiredCollateral,
        "VNFT: collateral deposit must not exceeed required collateral");

        // recalculation intentionally takes place before storage update
        recalculateNftPower(tokenId);

        // update storage
        s_nftInfo[tokenId].currentCollateral += amount;
        s_totalCollateral += amount;
    }

    // allows nft holders to withdraw collateral which had been
    // deposited for their nfts. This will cause those nfts
    // to subsequently lose power
    function removeCollateral(uint256 tokenId) external payable {
        // sanity checks
        address tokenOwner = ownerOf(tokenId);
        require(tokenOwner == msg.sender, "VNFT: only nft owner can remove collateral");

        uint256 amount = msg.value;
        require(amount > 0, "VNFT: collateral remove amount must be > 0");

        // recalculation intentionally takes place before storage update
        recalculateNftPower(tokenId);

        // update storage
        s_nftInfo[tokenId].currentCollateral -= amount;
        s_totalCollateral -= amount;

        // send withdrawn collateral to token owner
        _sendEth(tokenOwner, amount);
    }

    // recalculated nft power. Used internally and also called externally by
    // other operations within the DAO
    function recalculateNftPower(uint256 tokenId) public returns (uint256 newPower) {
        // nfts have no power until power calculation starts
        if (block.timestamp < s_powerCalcTimestamp) {
            return 0;
        }

        newPower = getNftPower(tokenId);

        NftInfo storage nftInfo = s_nftInfo[tokenId];

        s_totalPower -= nftInfo.lastUpdate != 0 ? nftInfo.currentPower : s_maxNftPower;
        s_totalPower += newPower;

        nftInfo.lastUpdate   = block.timestamp;
        nftInfo.currentPower = newPower;
    }

    // make sure to recalculate nft power if these nfts are transferred
    function _update(address to, uint256 tokenId, address auth) internal override returns (address) {
        // if users have collateral deposited for their nfts when they transfer
        // them to another user, the other users effectively becomes the owner
        // of the collateral. The nfts are mostly worthless without the collateral since
        // their voting power drops without it, so by design the deposited
        // collateral moves with the nfts
        recalculateNftPower(tokenId);

        return super._update(to, tokenId, auth);
    }

    // bunch of getter functions
    function getRequiredCollateral() public view returns (uint256) {
        return s_requiredCollateral;
    }

    function getPowerCalcTimestamp() public view returns (uint256) {
        return s_powerCalcTimestamp;
    }

    function getMaxNftPower() public view returns (uint256) {
        return s_maxNftPower;
    }

    function getNftPowerReductionPercent() public view returns (uint256) {
        return s_nftPowerReductionPercent;
    }

    function getTotalPower() public view returns (uint256) {
        return s_totalPower;
    }

    function getTotalCollateral() public view returns (uint256) {
        return s_totalCollateral;
    }

    function getDepositedCollateral(uint256 tokenId) public view returns (uint256) {
        _requireOwned(tokenId);

        return s_nftInfo[tokenId].currentCollateral;
    }

    function getNftPower(uint256 tokenId) public view returns (uint256) {
        // ensure token has already been minted
        _requireOwned(tokenId);

        if (block.timestamp <= s_powerCalcTimestamp) {
            return 0;
        }

        uint256 collateral   = s_nftInfo[tokenId].currentCollateral;

        // Calculate the minimum possible power based on the collateral of the nft
        uint256 maxNftPower  = s_maxNftPower;
        uint256 minNftPower  = maxNftPower * collateral / s_requiredCollateral;
        minNftPower          = Math.min(maxNftPower, minNftPower);

        // Get last update and current power. Or set them to default if it is first iteration
        uint256 lastUpdate   = s_nftInfo[tokenId].lastUpdate;
        uint256 currentPower = s_nftInfo[tokenId].currentPower;

        if (lastUpdate == 0) {
            lastUpdate       = s_powerCalcTimestamp;
            currentPower     = maxNftPower;
        }

        // Calculate reduction amount
        uint256 powerReductionPercent = s_nftPowerReductionPercent * (block.timestamp - lastUpdate);
        uint256 powerReduction = Math.min(currentPower, (maxNftPower * powerReductionPercent) / PERCENTAGE_100);
        uint256 newPotentialPower = currentPower - powerReduction;

        if (minNftPower <= newPotentialPower) {
            return newPotentialPower;
        }

        if (minNftPower <= currentPower) {
            return minNftPower;
        }

        return currentPower;
    }

    // sends eth using low-level call as we don't care about returned data
    function _sendEth(address dest, uint256 amount) private {
        bool sendStatus;
        assembly {
            sendStatus := call(gas(), dest, amount, 0, 0, 0, 0)
        }
        require(sendStatus, "VNFT: failed to send eth");
    }

}

================================================
FILE: src/04-voting-nft/VotingNftForFuzz.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";

// using regular and old Ownable for simplicity, any findings
// related to using newer Ownable or 2-step are invalid
import "@openzeppelin/contracts/access/Ownable.sol";

//
// This contract is a simplified version of a real contract which
// was audited by Cyfrin in a private audit and contained the same bug.
//
// Your mission, should you choose to accept it, is to find that bug!
//
// This is an nft contract that allows users to have nfts which
// have voting power in a DAO. These nfts can lose power over time if
// the required collateral is not deposited. The power of these nfts
// can never increase, only decrease or remain the same. 
//
// This contract has been intentionally simplified to remove much of
// the extra complexity in order to help you find the particular bug without
// other distractions. Please read the comments carefully as they note
// specific findings that are excluded as the implementation has been
// purposefully kept simple to help you focus on finding the harder
// to find and more interesting bug.
//
// This contract should only contain 1 intentional High finding, but
// if you find others they were not intentional :-) This contract should
// not be used in any live/production environment; it is purely an
// educational bug-hunting exercise based on a real-world example.
//
// This contract has had changes made for fuzzing test its initial 
// power calculation state; instead of using block.timestamp it uses
// a hard-coded value that the test setup sets. This allows a fuzzer to
// explore the initial power calculation state without skipping past it
// by changing block.timestamp
contract VotingNftForFuzz is ERC721, Ownable {

    // useful constants
    uint256 private constant PERCENTAGE_100 = 10 ** 27;

    // required collateral in eth to be deposited per NFT in order to
    // prevent voting power from decreasing
    uint256 private s_requiredCollateral;

    // time when power calculation begins
    uint256 private s_powerCalcTimestamp;

    // max power an nft can have. Power can only decrease or stay the same
    uint256 private s_maxNftPower;

    // % by which nft power decreases if required collateral not deposited
    uint256 private s_nftPowerReductionPercent;

    // current total power; will increase before power calculation starts
    // as nfts are created. once power calculation starts can only decrease
    // if nfts don't have the required collateral deposited
    uint256 private s_totalPower;

    // current total collateral which has been deposited for nfts
    uint256 private s_totalCollateral;

    // required for fuzz testing initial state once power calculation starts
    // replaces block.timestamp when fuzzing
    uint256 private s_FuzzerConstantBlockTimestamp;

    struct NftInfo {
        uint256 lastUpdate;
        uint256 currentPower;
        uint256 currentCollateral;
    }
    // keeps track of contract-specific nft info
    mapping(uint256 tokenId => NftInfo) s_nftInfo;


    // create the contract
    constructor(
        uint256 requiredCollateral,
        uint256 powerCalcTimestamp,
        uint256 maxNftPower,
        uint256 nftPowerReductionPercent) 
        ERC721("VNFT", "VNFT")
        Ownable(msg.sender) {

        // input sanity checks
        require(requiredCollateral > 0, "VNFT: required collateral must be > 0");
        require(powerCalcTimestamp > block.timestamp, "VNFT: power calc timestamp must be in the future");
        require(maxNftPower > 0, "VNFT: max nft power must be > 0");
        require(nftPowerReductionPercent > 0, "VNFT: nft power reduction must be > 0");
        require(nftPowerReductionPercent < PERCENTAGE_100, "VNFT: nft power reduction too big");

        s_requiredCollateral = requiredCollateral;
        s_powerCalcTimestamp = powerCalcTimestamp;
        s_maxNftPower        = maxNftPower;
        s_nftPowerReductionPercent = nftPowerReductionPercent;
    }


    // some operations can only be performed before
    // power calculation has started 
    modifier onlyBeforePowerCalc() {
        _onlyBeforePowerCalc();
        _;
    }
    function _onlyBeforePowerCalc() private view {
        require(
            // replace block.timestamp with constant value
            // for fuzzing initial power calc state
            //block.timestamp < s_powerCalcTimestamp,
            s_FuzzerConstantBlockTimestamp < s_powerCalcTimestamp,
            "VNFT: power calculation has already started"
        );
    }
    
    // allows contract owner to mint nfts to an address
    // can only be called before power calculation starts
    // all new nfts start at max power
    function safeMint(address to, uint256 tokenId) external onlyOwner onlyBeforePowerCalc {
        _safeMint(to, tokenId, "");

        s_totalPower += s_maxNftPower;
    }

    // allows nft holders to deposit collateral for their nft
    // nfts which have their required collateral deposited
    // don't lose power
    function addCollateral(uint256 tokenId) external payable {
        // sanity checks
        require(ownerOf(tokenId) == msg.sender, "VNFT: only nft owner can deposit collateral");

        uint256 amount = msg.value;
        require(amount > 0, "VNFT: collateral deposit amount must be > 0");

        require(s_nftInfo[tokenId].currentCollateral + amount <= s_requiredCollateral,
        "VNFT: collateral deposit must not exceeed required collateral");

        // recalculation intentionally takes place before storage update
        recalculateNftPower(tokenId);

        // update storage
        s_nftInfo[tokenId].currentCollateral += amount;
        s_totalCollateral += amount;
    }

    // allows nft holders to withdraw collateral which had been
    // deposited for their nfts. This will cause those nfts
    // to subsequently lose power
    function removeCollateral(uint256 tokenId) external payable {
        // sanity checks
        address tokenOwner = ownerOf(tokenId);
        require(tokenOwner == msg.sender, "VNFT: only nft owner can remove collateral");

        uint256 amount = msg.value;
        require(amount > 0, "VNFT: collateral remove amount must be > 0");

        // recalculation intentionally takes place before storage update
        recalculateNftPower(tokenId);

        // update storage
        s_nftInfo[tokenId].currentCollateral -= amount;
        s_totalCollateral -= amount;

        // send withdrawn collateral to token owner
        _sendEth(tokenOwner, amount);
    }

    // recalculated nft power. Used internally and also called externally by
    // other operations within the DAO
    function recalculateNftPower(uint256 tokenId) public returns (uint256 newPower) {
        // nfts have no power until power calculation starts
        // replace block.timestamp with constant value
        // for fuzzing initial power calc state
        // if (block.timestamp < s_powerCalcTimestamp) {
        if(s_FuzzerConstantBlockTimestamp < s_powerCalcTimestamp) {
            return 0;
        }

        newPower = getNftPower(tokenId);

        NftInfo storage nftInfo = s_nftInfo[tokenId];

        s_totalPower -= nftInfo.lastUpdate != 0 ? nftInfo.currentPower : s_maxNftPower;
        s_totalPower += newPower;

        // replace block.timestamp with constant value
        // for fuzzing initial power calc state
        // nftInfo.lastUpdate   = block.timestamp;
        nftInfo.lastUpdate   = s_FuzzerConstantBlockTimestamp;
        nftInfo.currentPower = newPower;
    }

    // make sure to recalculate nft power if these nfts are transferred
    function _update(address to, uint256 tokenId, address auth) internal override returns (address) {
        // if users have collateral deposited for their nfts when they transfer
        // them to another user, the other users effectively becomes the owner
        // of the collateral. The nfts are mostly worthless without the collateral since
        // their voting power drops without it, so by design the deposited
        // collateral moves with the nfts
        recalculateNftPower(tokenId);

        return super._update(to, tokenId, auth);
    }

    // bunch of getter functions
    function getRequiredCollateral() public view returns (uint256) {
        return s_requiredCollateral;
    }

    function getPowerCalcTimestamp() public view returns (uint256) {
        return s_powerCalcTimestamp;
    }

    function getMaxNftPower() public view returns (uint256) {
        return s_maxNftPower;
    }

    function getNftPowerReductionPercent() public view returns (uint256) {
        return s_nftPowerReductionPercent;
    }

    function getTotalPower() public view returns (uint256) {
        return s_totalPower;
    }

    function getTotalCollateral() public view returns (uint256) {
        return s_totalCollateral;
    }

    function getDepositedCollateral(uint256 tokenId) public view returns (uint256) {
        _requireOwned(tokenId);

        return s_nftInfo[tokenId].currentCollateral;
    }

    function getNftPower(uint256 tokenId) public view returns (uint256) {
        // ensure token has already been minted
        _requireOwned(tokenId);

        // replace block.timestamp with constant value
        // for fuzzing initial power calc state
        // if (block.timestamp <= s_powerCalcTimestamp) {
        if(s_FuzzerConstantBlockTimestamp <= s_powerCalcTimestamp) {
            return 0;
        }

        uint256 collateral   = s_nftInfo[tokenId].currentCollateral;

        // Calculate the minimum possible power based on the collateral of the nft
        uint256 maxNftPower  = s_maxNftPower;
        uint256 minNftPower  = maxNftPower * collateral / s_requiredCollateral;
        minNftPower          = Math.min(maxNftPower, minNftPower);

        // Get last update and current power. Or set them to default if it is first iteration
        uint256 lastUpdate   = s_nftInfo[tokenId].lastUpdate;
        uint256 currentPower = s_nftInfo[tokenId].currentPower;

        if (lastUpdate == 0) {
            lastUpdate       = s_powerCalcTimestamp;
            currentPower     = maxNftPower;
        }

        // Calculate reduction amount
        // replace block.timestamp with constant value
        // for fuzzing initial power calc state
        // uint256 powerReductionPercent = s_nftPowerReductionPercent * (block.timestamp - lastUpdate);
        uint256 powerReductionPercent = s_nftPowerReductionPercent * (s_FuzzerConstantBlockTimestamp - lastUpdate);
        uint256 powerReduction = Math.min(currentPower, (maxNftPower * powerReductionPercent) / PERCENTAGE_100);
        uint256 newPotentialPower = currentPower - powerReduction;

        if (minNftPower <= newPotentialPower) {
            return newPotentialPower;
        }

        if (minNftPower <= currentPower) {
            return minNftPower;
        }

        return currentPower;
    }

    // sends eth using low-level call as we don't care about returned data
    function _sendEth(address dest, uint256 amount) private {
        bool sendStatus;
        assembly {
            sendStatus := call(gas(), dest, amount, 0, 0, 0, 0)
        }
        require(sendStatus, "VNFT: failed to send eth");
    }

    // used by the fuzzer when fuzz testing
    function setFuzzerConstantBlockTimestamp(uint256 input) onlyOwner public {
        s_FuzzerConstantBlockTimestamp = input;
    }
    

}

================================================
FILE: src/05-token-sale/TokenSale.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

//
// This contract is a simplified version of a real contract which
// was audited by Cyfrin in a private audit and contained the same bugs.
//
// Your mission, should you choose to accept it, is to find those bugs!
//
// This contract allows the creator to invite a select group of people
// to participate in a token sale. Users can exchange an allowed token
// for the token being sold. This could be used by a DAO to distribute
// their governance token in exchange for DAI, but having a lot more control
// over how that distribution takes place compared to using a uniswap pool.
//
// This contract has been intentionally simplified to remove much of
// the extra complexity in order to help you find the particular bugs without
// other distractions. Please read the comments carefully as they note
// specific findings that are excluded as the implementation has been
// purposefully kept simple to help you focus on finding the harder
// to find and more interesting bugs.
//
// This contract intentionally has no time-out period for the token sale
// to complete; lack of a time-out period resulting in the token sale never
// completing is not a valid finding as this has been intentionally 
// omitted to simplify the codebase.
//
// This contract intentionally does not support fee-on-transfer, rebasing
// ERC777 or any non-standard, weird ERC20s. It only supports ERC20s
// that conform to the ERC20 implementation. Any findings related to 
// weird/non-standard ERC20s are invalid. Any findings related to blacklists are invalid.
//
// This contract intentionally has no rescue function; any tokens that
// are sent to this contract are lost forever. Once this contract is
// created the fixed amount of tokens to be sold can't be changed. Any
// findings related to these issues are invalid.
//
// This contract should only contain 2 intentional High findings, but
// if you find others they were not intentional :-) This contract should
// not be used in any live/production environment; it is purely an
// educational bug-hunting exercise based on a real-world example.
//
contract TokenSale {
    // min buy precision enforced
    uint256 public constant MIN_BUY_PRECISION = 6;

    // sell precision always 18
    uint256 public constant SELL_PRECISION = 18;

    // smallest amount proposal creator can fund contract with
    uint256 public constant MIN_FUNDING = 100;

    // min number of buyers
    uint256 public constant MIN_BUYERS  = 3;

    // creator of this proposal. Any findings related to the creator
    // not being able to update this address are invalid; this has
    // intentionally been omitted to simplify the contract so you can
    // focus on finding the cool bug instead of lame/easy stuff. 
    // Eg: all findings related to blacklists are invalid; Proposal
    // creator can always receive and send tokens.
    address private immutable s_creator;

    // token to be sold by creator
    ERC20 private immutable s_sellToken;

    // token which buyers can use to buy the token being sold
    // for simplicity sake exchange rate is always 1:1
    // any findings related to not having a dynamic exchange rate are invalid
    ERC20 private immutable s_buyToken;

    // maximum amount any single buyer should be able to buy
    uint256 private immutable s_maxTokensPerBuyer;

    // total amount of tokens to be sold
    uint256 private immutable s_sellTokenTotalAmount;

    // total amount of tokens currently sold
    uint256 private s_sellTokenSoldAmount;

    // total number of allowed buyers
    uint256 private s_totalBuyers;

    // only permitted addresses can buy
    enum BuyerState {
        DISALLOWED,
        ALLOWED
    }

    mapping(address buyer => BuyerState) private s_buyers;


    // create the contract
    constructor(address[] memory allowList,
                address sellToken,
                address buyToken,
                uint256 maxTokensPerBuyer,
                uint256 sellTokenAmount) {
        require(sellToken != address(0), "TS: invalid sell token");
        require(buyToken  != address(0), "TS: invalid sell token");

        // save tokens to storage
        s_sellToken = ERC20(sellToken);
        s_buyToken  = ERC20(buyToken);

        // save tokens to stack since to prevent multiple storage reads during constructor
        ERC20 sToken = ERC20(sellToken);
        ERC20 bToken = ERC20(buyToken);

        // enforce precision
        require(sToken.decimals() == SELL_PRECISION, "TS: sell token invalid precision");
        require(bToken.decimals() >= MIN_BUY_PRECISION, "TS: buy token invalid min precision");
        require(bToken.decimals() <= SELL_PRECISION, "TS: buy token precision must <= sell token");

        // require minimum sell token amount
        require(sellTokenAmount >= MIN_FUNDING * 10 ** sToken.decimals(), "TS: Minimum funding required");

        // sanity check
        require(maxTokensPerBuyer <= sellTokenAmount, "TS: invalid max tokens per buyer");
        s_maxTokensPerBuyer = maxTokensPerBuyer;

        // cache list length
        uint256 allowListLength = allowList.length;

        // perform some sanity checks. NOTE: checks for duplicate inputs
        // are performed by entity creating the proposal who is trusted,
        // so the contract intentionally does not re-check for duplicate inputs. 
        // Findings related to not checking for duplicate inputs are invalid.
        require(allowListLength >= MIN_BUYERS, "TS: Minimum 3 buyers required");

        uint256 totalBuyers;

        // store addresses allowed to buy
        for(; totalBuyers<allowListLength; ++totalBuyers) {
            // sanity check to prevent address(0)
            address buyer = allowList[totalBuyers];
            require(buyer != address(0), "TS: address(0) invalid");

            s_buyers[buyer] = BuyerState.ALLOWED;
        }

        s_totalBuyers = totalBuyers;

        // update the token sale creator
        s_creator = msg.sender;

        // transfer the tokens to be sold into this contract
        //
        // fee-on-transfer & other weird stuff not suppported, see notes at top
        s_sellTokenTotalAmount = sellTokenAmount;

        // contract creator is trusted to immediately fund the contract after creation
        // with the correct amount; more complicated funding scheme not implemented
        // to avoid complexity. Any findings related to improper funding of contract
        // are invalid.
    }


    // buy some tokens from the token sale
    // no slippage required since exchange rate always 1:1
    // caller just specifies the amount of sell tokens they want to buy
    function buy(uint256 amountToBuy) external {
        // prevent sale if all tokens have been sold
        uint256 remainingSellTokens = getRemainingSellTokens();
        require(remainingSellTokens != 0, "TS: token sale is complete");

        // current buyer
        address buyer = msg.sender;

        // prevent buying if not allowed
        require(s_buyers[buyer] == BuyerState.ALLOWED, "TS: buyer not allowed");

        // if `amountToBuy` greater than remaining cap, cap it to buy the remainder
        if(amountToBuy > remainingSellTokens) {
            amountToBuy = remainingSellTokens;
        }

        // prevent user from buying more than max
        require(amountToBuy <= s_maxTokensPerBuyer, "TS: buy over max per user");

        // update storage to increase total bought
        s_sellTokenSoldAmount += amountToBuy;

        // transfer purchase tokens from buyer to creator
        SafeERC20.safeTransferFrom(s_buyToken, buyer, s_creator, _convert(amountToBuy, s_buyToken.decimals()));

        // transfer sell tokens from this contract to buyer
        SafeERC20.safeTransfer(s_sellToken, buyer, amountToBuy);
    }

    
    // ends the sale and refunds creator remaining unsold tokens
    function endSale() external {
        // cache creator address
        address creator = s_creator;

        // only creator can end the sale
        require(msg.sender == s_creator, "TS: only creator can end sale");

        // sale must not have been completed
        uint256 remainingSellTokens = getRemainingSellTokens();
        require(remainingSellTokens != 0, "TS: token sale is complete");

        // update storage with tokens sent to creator to mark the
        // sale as closed
        s_sellTokenSoldAmount += remainingSellTokens;

        // send remaining unsold tokens back to creator
        SafeERC20.safeTransfer(s_sellToken, creator, remainingSellTokens);
    }


    // used internally and externally to determine whether the sale
    // has been completed (no tokens remain unsold)
    function getRemainingSellTokens() public view returns(uint256) {
        return s_sellTokenTotalAmount - s_sellTokenSoldAmount;
    }


    // bunch of getters
    function getBuyTokenAddress() external view returns(address) {
        return address(s_buyToken);
    }
    function getSellTokenAddress() external view returns(address) {
        return address(s_sellToken);
    }
    function getMaxTokensPerBuyer() external view returns(uint256) {
        return s_maxTokensPerBuyer;
    }
    function getSellTokenTotalAmount() external view returns(uint256) {
        return s_sellTokenTotalAmount;
    }
    function getSellTokenSoldAmount() external view returns(uint256) {
        return s_sellTokenSoldAmount;
    }
    function getTotalAllowedBuyers() external view returns(uint256) {
        return s_totalBuyers;
    }
    function getCreator() external view returns(address) {
        return s_creator;
    }


    // handles conversions
    function _convert(uint256 amount_, uint256 destDecimals_) internal pure returns (uint256) {
        if (SELL_PRECISION > destDecimals_) {
            amount_ = amount_ / 10 ** (SELL_PRECISION - destDecimals_);
        } else if (SELL_PRECISION < destDecimals_) {
            amount_ = amount_ * 10 ** (destDecimals_ - SELL_PRECISION);
        }

        return amount_;
    }



}

================================================
FILE: src/06-rarely-false/RarelyFalse.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;
//
//
// stateless tests in test/06-rarely-false
//
// placeholder so nothing else gets put in this folder

================================================
FILE: src/07-byte-battle/ByteBattle.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;
//
//
// stateless tests in test/07-byte-battle
//
// placeholder so nothing else gets put in this folder

================================================
FILE: src/08-omni-protocol/IRM.sol
================================================
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.23;

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol";

import "./interfaces/IIRM.sol";

/**
 * @title Interest Rate Model (IRM) Contract
 * @notice This contract defines the interest rate model for different markets and tranches.
 * @dev It inherits from the IIRM interface and the AccessControl contract from the OpenZeppelin library.
 * @dev It is important that contracts that integrate this IRM appropriately scale interest rate values.
 */
contract IRM is IIRM, AccessControl, Initializable {
    uint256 public constant UTILIZATION_SCALE = 1e9;
    uint256 public constant MAX_INTEREST_RATE = 10e9; // Scale must match OmniToken.sol, 1e9
    mapping(address => mapping(uint8 => IRMConfig)) public marketIRMConfigs;

    /**
     * @notice Initializes the admin role with the contract deployer/upgrader.
     * @param _admin The address of the multisig admin.
     */
    function initialize(address _admin) external initializer {
        _grantRole(DEFAULT_ADMIN_ROLE, _admin);
    }

    /**
     * @notice Calculates the interest rate for a specific OmniToken market, tranche, total deposit and total borrow.
     * @param _market The address of the market
     * @param _tranche The tranche number
     * @param _totalDeposit The total amount deposited in the market
     * @param _totalBorrow The total amount borrowed from the market
     * @return The calculated interest rate
     */
    function getInterestRate(address _market, uint8 _tranche, uint256 _totalDeposit, uint256 _totalBorrow)
        external
        view
        returns (uint256)
    {
        uint256 utilization;
        if (_totalBorrow <= _totalDeposit) {
            utilization = _totalDeposit == 0 ? 0 : (_totalBorrow * UTILIZATION_SCALE) / _totalDeposit;
        } else {
            utilization = UTILIZATION_SCALE;
        }
        return _getInterestRateLinear(marketIRMConfigs[_market][_tranche], utilization);
    }

    /**
     * @notice Internal function to calculate the interest rate linearly based on utilization and IRMConfig.
     * @param _config The IRM configuration structure
     * @param _utilization The current utilization rate
     * @return interestRate The calculated interest rate
     */
    function _getInterestRateLinear(IRMConfig memory _config, uint256 _utilization)
        internal
        pure
        returns (uint256 interestRate)
    {
        if (_config.kink == 0) {
            revert("IRM::_getInterestRateLinear: Interest config not set.");
        }
        if (_utilization <= _config.kink) {
            interestRate = _config.start;
            interestRate += (_utilization * (_config.mid - _config.start)) / _config.kink;
        } else {
            interestRate = _config.mid;
            interestRate +=
                ((_utilization - _config.kink) * (_config.end - _config.mid)) / (UTILIZATION_SCALE - _config.kink);
        }
    }

    /**
     * @notice Sets the IRM configuration for a specific OmniToken market and tranches.
     * @param _market The address of the market
     * @param _tranches An array of tranche numbers
     * @param _configs An array of IRMConfig configurations
     */
    function setIRMForMarket(address _market, uint8[] calldata _tranches, IRMConfig[] calldata _configs)
        external
        onlyRole(DEFAULT_ADMIN_ROLE)
    {
        if (_tranches.length != _configs.length) {
            revert("IRM::setIRMForMarket: Tranches and configs length mismatch.");
        }
        for (uint256 i = 0; i < _tranches.length; ++i) {
            if (_configs[i].kink == 0 || _configs[i].kink >= UTILIZATION_SCALE) {
                revert("IRM::setIRMForMarket: Bad kink value.");
            }
            if (
                _configs[i].start > _configs[i].mid || _configs[i].mid > _configs[i].end
                    || _configs[i].end > MAX_INTEREST_RATE
            ) {
                revert("IRM::setIRMForMarket: Bad interest value.");
            }
            marketIRMConfigs[_market][_tranches[i]] = _configs[i];
        }
        emit SetIRMForMarket(_market, _tranches, _configs);
    }
}


================================================
FILE: src/08-omni-protocol/OmniOracle.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
import "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol";

import "./interfaces/IBandReference.sol";
import "./interfaces/IChainlinkAggregator.sol";
import "./interfaces/ICustomOmniOracle.sol";
import "./interfaces/IOmniOracle.sol";

/**
 * @title OmniOracle contract
 * @notice This contract facilitates USD base price retrieval from Chainlink, Band, or a custom oracle integrating the IOmniOracle interface.
 * Special attention must be paid by the admin to ensure oracle configurations are valid given the below specifications.
 * @dev Inherits from AccessControl and implements IOmniOracle interface.
 * Makes assumptions about oracle feeds, e.g. the decimals, delay, and base currency. Configurator must pay special attention.
 * @dev This oracle contract does not handle Chainlink L2 Sequencer Uptime Feeds requirements, and should only be used for L1 deployments.
 */
contract OmniOracle is IOmniOracle, AccessControl, Initializable {
    uint256 public constant PRICE_SCALE = 1e36; // Gives enough precision for the price of one base unit of token, as most tokens have at most 18 decimals
    string private constant USD = "USD";

    mapping(address => OracleConfig) public oracleConfigs;
    mapping(address => string) public oracleSymbols;

    /**
     * @notice Initializes the admin role with the contract deployer/upgrader.
     * @param _admin The address of the multisig admin.
     */
    function initialize(address _admin) external initializer {
        _grantRole(DEFAULT_ADMIN_ROLE, _admin);
    }

    /**
     * @notice Fetches the price of the specified asset in USD for the base unit of the underlying token.
     * @dev Band oracle documentation says they always return price multiplied by 1e18 (https://docs.bandchain.org/products/band-standard-dataset/using-band-standard-dataset/contract#getreferencedata)
     * @param _underlying The address of the asset.
     * @return The price of the asset in USD, in the base unit of the underlying token.
     */
    function getPrice(address _underlying) external view returns (uint256) {
        OracleConfig memory config = oracleConfigs[_underlying];
        if (config.provider == Provider.Band) {
            IStdReference.ReferenceData memory data;
            data = IStdReference(config.oracleAddress).getReferenceData(oracleSymbols[_underlying], USD);
            require(
                data.lastUpdatedBase >= block.timestamp - config.delay, "OmniOracle::getPrice: Stale price for base."
            );
            require(
                data.lastUpdatedQuote >= block.timestamp - config.delayQuote,
                "OmniOracle::getPrice: Stale price for quote."
            );
            return data.rate * (PRICE_SCALE / 1e18) / (10 ** config.underlyingDecimals); // Price in one base unit with 1e36 precision
        } else if (config.provider == Provider.Chainlink) {
            (, int256 answer,, uint256 updatedAt,) = IChainlinkAggregator(config.oracleAddress).latestRoundData();
            require(
                answer > 0 && updatedAt >= block.timestamp - config.delay,
                "OmniOracle::getPrice: Invalid chainlink price."
            );
            return uint256(answer) * (PRICE_SCALE / (10 ** IChainlinkAggregator(config.oracleAddress).decimals()))
                / (10 ** config.underlyingDecimals);
        } else if (config.provider == Provider.Other) {
            return ICustomOmniOracle(config.oracleAddress).getPrice(_underlying) * (PRICE_SCALE / 1e18)
                / (10 ** config.underlyingDecimals);
        } else {
            revert("OmniOracle::getPrice: Invalid provider.");
        }
    }

    /**
     * @notice Sets the oracle configuration for the specified asset. Chainlink addresses must use the USD price feed.
     * @param _underlying The address of the asset.
     * @param _oracleConfig The oracle configuration for the asset. Must be Chainlink, Band, or implement the IOmniOracle interface.
     */
    function setOracleConfig(address _underlying, OracleConfig calldata _oracleConfig, string calldata _symbol)
        external
        onlyRole(DEFAULT_ADMIN_ROLE)
    {
        require(
            _oracleConfig.oracleAddress != address(0) && _underlying != address(0),
            "OmniOracle::setOracleConfig: Can never use zero address."
        );
        require(_oracleConfig.provider != Provider.Invalid, "OmniOracle::setOracleConfig: Invalid provider.");
        require(_oracleConfig.delay > 0, "OmniOracle::setOracleConfig: Invalid delay.");
        require(_oracleConfig.delayQuote > 0, "OmniOracle::setOracleConfig: Invalid delay quote.");
        oracleConfigs[_underlying] = _oracleConfig;
        oracleSymbols[_underlying] = _symbol;
        emit SetOracle(
            _underlying,
            _oracleConfig.oracleAddress,
            _oracleConfig.provider,
            _oracleConfig.delay,
            _oracleConfig.delayQuote,
            _oracleConfig.underlyingDecimals
        );
    }

    /**
     * @notice Removes the oracle configuration for the specified asset.
     * @param _underlying The address of the asset.
     */
    function removeOracleConfig(address _underlying) external onlyRole(DEFAULT_ADMIN_ROLE) {
        delete oracleConfigs[_underlying];
        emit RemoveOracle(_underlying);
    }
}


================================================
FILE: src/08-omni-protocol/OmniPool.sol
================================================
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.23;

import "@openzeppelin-upgradeable/contracts/access/AccessControlUpgradeable.sol";
import "@openzeppelin-upgradeable/contracts/utils/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";

import "./interfaces/IOmniOracle.sol";
import "./interfaces/IOmniPool.sol";
import "./interfaces/IOmniToken.sol";
import "./interfaces/IOmniTokenNoBorrow.sol";
import "./interfaces/IWithUnderlying.sol";
import "./SubAccount.sol";

/**
 * @title OmniPool
 * @notice This contract implements a manager for handling loans, protocol market, mode, and account configurations, and liquidations.
 * @dev This contract implements a lending pool with various modes and market configurations.
 * It utilizes different structs to keep track of market, mode, account configurations, evaluations,
 * and liquidation bonuses. It has a variety of external and public functions to manage and interact with
 * the lending pool, along with internal utility functions. Includes AccessContral, Pausable, and ReentrancyGuardUpgradeable (includes Initializable)
 * from OpenZeppelin.
 */
contract OmniPool is IOmniPool, AccessControlUpgradeable, ReentrancyGuardUpgradeable, PausableUpgradeable {
    using SubAccount for address;

    bytes32 public constant SOFT_LIQUIDATION_ROLE = keccak256("SOFT_LIQUIDATION_ROLE");
    bytes32 public constant MARKET_CONFIGURATOR_ROLE = keccak256("MARKET_CONFIGURATOR_ROLE");

    uint256 public constant SELF_COLLATERALIZATION_FACTOR = 0.96e9; // 0.96
    uint256 public constant FACTOR_PRECISION_SCALE = 1e9;
    uint256 public constant LIQ_BONUS_PRECISION_SCALE = 1e9;
    uint256 public constant HEALTH_FACTOR_SCALE = 1e9;
    uint256 public constant MAX_BASE_SOFT_LIQUIDATION = 1.4e9;
    uint256 public constant MAX_LIQ_KINK = 0.2e9; // Borrow value exceeds deposit value by 20%
    uint256 public constant PRICE_SCALE = 1e18; // Must match up with PRICE_SCALE in OmniOracle
    uint256 public constant MAX_MARKETS_PER_ACCOUNT = 9; // Will be 10 including isolated collateral market

    mapping(bytes32 => AccountInfo) public accountInfos;
    mapping(bytes32 => address[]) public accountMarkets;

    uint256 public modeCount;
    mapping(uint256 => ModeConfiguration) public modeConfigurations;
    mapping(address => MarketConfiguration) public marketConfigurations;
    mapping(address => LiquidationBonusConfiguration) public liquidationBonusConfigurations;

    address public oracle;
    uint8 public pauseTranche;
    bytes32 public reserveReceiver;

    /**
     * @notice Initializes a new instance of the contract, setting up the oracle, reserve receiver, pause tranche, and various roles.
     * This constructor sets the oracle address, initializes the pause tranche to its maximum value, and sets the reserve receiver to the provided address.
     * It also sets up the DEFAULT_ADMIN_ROLE, SOFT_LIQUIDATION_ROLE, and MARKET_CONFIGURATOR_ROLE, assigning them to the account deploying the contract.
     * @param _oracle The address of the oracle contract to be used for price information.
     * @param _reserveReceiver The address of the reserve receiver. This address will be converted to an account with a subId of 0.
     * @param _admin The address of the multisig admin
     */
    function initialize(address _oracle, address _reserveReceiver, address _admin) external initializer {
        __ReentrancyGuard_init();
        __AccessControl_init();
        __Pausable_init();
        oracle = _oracle;
        pauseTranche = type(uint8).max;
        reserveReceiver = _reserveReceiver.toAccount(0);
        _grantRole(DEFAULT_ADMIN_ROLE, _admin); // Additionally set up other roles?
        _grantRole(SOFT_LIQUIDATION_ROLE, _admin);
        _grantRole(MARKET_CONFIGURATOR_ROLE, _admin);
    }

    /**
     * @notice Allows a user to enter an isolated market, the market configuration must be for isolated collateral.
     * @dev The function checks whether the market is valid for isolation and updates the account's isolatedCollateralMarket field.
     * A subaccount is only allowed to have 1 isolated collateral market at a time.
     * @param _subId The sub-account identifier.
     * @param _isolatedMarket The address of the isolated market to enter.
     */
    function enterIsolatedMarket(uint96 _subId, address _isolatedMarket) external {
        bytes32 accountId = msg.sender.toAccount(_subId);
        AccountInfo memory account = accountInfos[accountId];
        require(account.modeId == 0, "OmniPool::enterIsolatedMarket: Already in a mode.");
        require(
            account.isolatedCollateralMarket == address(0),
            "OmniPool::enterIsolatedMarket: Already has isolated collateral."
        );
        MarketConfiguration memory marketConfig = marketConfigurations[_isolatedMarket];
        if (marketConfig.expirationTimestamp <= block.timestamp || !marketConfig.isIsolatedCollateral) {
            revert("OmniPool::enterIsolatedMarket: Isolated market invalid.");
        }
        Evaluation memory eval = evaluateAccount(accountId);
        require(eval.numBorrow == 0, "OmniPool::enterIsolatedMarket: Non-zero borrow count.");
        accountInfos[accountId].isolatedCollateralMarket = _isolatedMarket;
        emit EnteredIsolatedMarket(accountId, _isolatedMarket);
    }

    /**
     * @notice Allows a user to enter multiple unique markets, none of them are isolated collateral markets.
     * @dev The function checks the validity of each market and updates the account's market list. Markets must not already be entered.
     * @param _subId The sub-account identifier.
     * @param _markets The addresses of the markets to enter.
     */
    function enterMarkets(uint96 _subId, address[] calldata _markets) external {
        bytes32 accountId = msg.sender.toAccount(_subId);
        require(accountInfos[accountId].modeId == 0, "OmniPool::enterMarkets: Already in a mode.");
        address[] memory existingMarkets = accountMarkets[accountId];
        address[] memory newMarkets = new address[](existingMarkets.length + _markets.length);
        require(newMarkets.length <= MAX_MARKETS_PER_ACCOUNT, "OmniPool::enterMarkets: Too many markets.");
        for (uint256 i = 0; i < existingMarkets.length; ++i) {
            // Copy over existing markets
            newMarkets[i] = existingMarkets[i];
        }
        for (uint256 i = 0; i < _markets.length; ++i) {
            address market = _markets[i];
            MarketConfiguration memory marketConfig = marketConfigurations[market];
            require(
                marketConfig.expirationTimestamp > block.timestamp && !marketConfig.isIsolatedCollateral,
                "OmniPool::enterMarkets: Market invalid."
            );
            require(!_contains(newMarkets, market), "OmniPool::enterMarkets: Already in the market.");
            require(
                IOmniToken(market).getBorrowCap(0) > 0,
                "OmniPool::enterMarkets: Market has no borrow cap for 0 tranche."
            );
            newMarkets[i + existingMarkets.length] = market;
        }
        accountMarkets[accountId] = newMarkets;
        emit EnteredMarkets(accountId, _markets);
    }

    /**
     * @notice Allows a user to exit multiple markets including their isolated market. There must be no borrows active on the subaccount to exit a market.
     * @dev The function removes the specified markets from the account's market list after ensuring the account has no outstanding borrows.
     * @param _subId The sub-account identifier.
     * @param _market The address of the market to exit.
     */
    function exitMarket(uint96 _subId, address _market) external {
        bytes32 accountId = msg.sender.toAccount(_subId);
        AccountInfo memory account = accountInfos[accountId];
        require(account.modeId == 0, "OmniPool::exitMarkets: In a mode, need to call exitMode.");
        address[] memory markets_ = getAccountPoolMarkets(accountId, account);
        Evaluation memory eval = _evaluateAccountInternal(accountId, markets_, account);
        require(eval.numBorrow == 0, "OmniPool::exitMarkets: Non-zero borrow count.");
        if (_market == account.isolatedCollateralMarket) {
            accountInfos[accountId].isolatedCollateralMarket = address(0);
        } else {
            require(markets_.length > 0, "OmniPool::exitMarkets: No markets to exit");
            require(_contains(markets_, _market), "OmniPool::exitMarkets: Market not entered");
            uint256 newMarketsLength = markets_.length - 1;
            if (newMarketsLength > 0) {
                address[] memory newMarkets = new address[](markets_.length - 1);
                uint256 newIndex = 0;
                for (uint256 i = 0; i < markets_.length; ++i) {
                    if (markets_[i] != _market) {
                        newMarkets[newIndex] = markets_[i];
                        ++newIndex;
                    }
                }
                delete accountMarkets[accountId]; // Gas refund?
                accountMarkets[accountId] = newMarkets;
            } else {
                delete accountMarkets[accountId];
            }
        }
        emit ExitedMarket(accountId, _market);
    }

    /**
     * @notice Clears all markets for a user including isolated collateral. The subaccount must have no active borrows to clear markets.
     * @dev The function checks that the account has no outstanding borrows before clearing all markets.
     * @param _subId The sub-account identifier.
     */
    function clearMarkets(uint96 _subId) external {
        bytes32 accountId = msg.sender.toAccount(_subId);
        AccountInfo memory account = accountInfos[accountId];
        require(account.modeId == 0, "OmniPool::clearMarkets: Already in a mode.");
        Evaluation memory eval = evaluateAccount(accountId);
        require(eval.numBorrow == 0, "OmniPool::clearMarkets: Non-zero borrow count.");
        accountInfos[accountId].isolatedCollateralMarket = address(0);
        delete accountMarkets[accountId];
        emit ClearedMarkets(accountId);
    }

    /**
     * @notice Allows a user to enter a mode. The subaccount must not already be in a mode. The mode must not have expired.
     * @dev The function sets the modeId field in the account's info and emits an EnteredMode event.
     * @param _subId The sub-account identifier.
     * @param _modeId The mode identifier to enter.
     */
    function enterMode(uint96 _subId, uint8 _modeId) external {
        bytes32 accountId = msg.sender.toAccount(_subId);
        require(_modeId > 0 && _modeId <= modeCount, "OmniPool::enterMode: Invalid mode ID.");
        AccountInfo memory account = accountInfos[accountId];
        require(account.modeId == 0, "OmniPool::enterMode: Already in a mode.");
        require(
            accountMarkets[accountId].length == 0 && account.isolatedCollateralMarket == address(0),
            "OmniPool::enterMode: Non-zero market count."
        );
        require(modeConfigurations[_modeId].expirationTimestamp > block.timestamp, "OmniPool::enterMode: Mode expired.");
        account.modeId = _modeId;
        accountInfos[accountId] = account;
        emit EnteredMode(accountId, _modeId);
    }

    /**
     * @notice Allows a user to exit a mode. There must be no active borrows in the subaccount to exit.
     * @dev The function resets the modeId field in the account's info and emits an ExitedMode event.
     * @param _subId The sub-account identifier.
     */
    function exitMode(uint96 _subId) external {
        bytes32 accountId = msg.sender.toAccount(_subId);
        AccountInfo memory account = accountInfos[accountId];
        require(account.modeId != 0, "OmniPool::exitMode: Not in a mode.");
        Evaluation memory eval = evaluateAccount(accountId);
        require(eval.numBorrow == 0, "OmniPool::exitMode: Non-zero borrow count.");
        account.modeId = 0;
        accountInfos[accountId] = account;
        emit ExitedMode(accountId);
    }

    /**
     * @notice Evaluates an account's deposits and borrows values.
     * @dev The function computes the true and adjusted values of deposits and borrows for the account.
     * @param _accountId The account identifier.
     * @return eval An Evaluation struct containing the account's financial information.
     */
    function evaluateAccount(bytes32 _accountId) public returns (Evaluation memory eval) {
        AccountInfo memory account = accountInfos[_accountId];
        address[] memory poolMarkets = getAccountPoolMarkets(_accountId, account);
        return _evaluateAccountInternal(_accountId, poolMarkets, account);
    }

    /**
     * @notice Evaluates an account's financial standing within a lending pool.
     * @dev This function accrues interest, computes market prices, deposit and borrow balances, and calculates the adjusted values of
     * deposits and borrows based on the account's mode and market configurations.
     * @param _accountId The unique identifier of the account to be evaluated.
     * @param _poolMarkets An array of addresses representing the markets in which the account has activity. Excludes the isolated collateral market if it exists.
     * @param _account The AccountInfo struct containing the account's mode, isolated collateral market, and other relevant data.
     * @return eval An Evaluation struct containing data on the account's deposit and borrow balances, both true and adjusted values.
     */
    function _evaluateAccountInternal(bytes32 _accountId, address[] memory _poolMarkets, AccountInfo memory _account)
        internal
        returns (Evaluation memory eval)
    {
        ModeConfiguration memory mode;
        if (_account.modeId != 0) mode = modeConfigurations[_account.modeId];
        for (uint256 i = 0; i < _poolMarkets.length; ++i) {
            // Accrue interest for all borrowable markets
            IOmniToken(_poolMarkets[i]).accrue();
        }
        uint256 marketCount = _poolMarkets.length;
        if (_account.isolatedCollateralMarket != address(0)) {
            ++marketCount;
        }
        for (uint256 i = 0; i < marketCount; ++i) {
            address market;
            // A market is either a pool market or the isolated collateral market (last index).
            if (i < _poolMarkets.length) {
                market = _poolMarkets[i];
            } else {
                market = _account.isolatedCollateralMarket;
            }
            MarketConfiguration memory marketConfiguration_ = marketConfigurations[market];
            if (marketConfiguration_.expirationTimestamp <= block.timestamp) {
                eval.isExpired = true; // Must repay all debts and exit market to get rid of unhealthy account status if expired
            }
            address underlying = IWithUnderlying(market).underlying();
            uint256 price = IOmniOracle(oracle).getPrice(underlying); // Returns price in base units multiplied by 1e36
            uint256 depositAmount = IOmniTokenBase(market).getAccountDepositInUnderlying(_accountId);
            if (depositAmount != 0) {
                ++eval.numDeposit;
                uint256 depositValue = (depositAmount * price) / PRICE_SCALE; // Rounds down
                eval.depositTrueValue += depositValue;
                uint256 collateralFactor = marketCount == 1
                    ? SELF_COLLATERALIZATION_FACTOR
                    : _account.modeId == 0 ? uint256(marketConfiguration_.collateralFactor) : uint256(mode.collateralFactor);
                eval.depositAdjValue += (depositValue * collateralFactor) / FACTOR_PRECISION_SCALE; // Rounds down
            }
            if (i >= _poolMarkets.length) {
                // Isolated collateral market. No borrow.
                continue;
            }
            uint8 borrowTier = getAccountBorrowTier(_account);
            uint256 borrowAmount = IOmniToken(market).getAccountBorrowInUnderlying(_accountId, borrowTier);
            if (borrowAmount != 0) {
                ++eval.numBorrow;
                uint256 borrowValue = (borrowAmount * price) / PRICE_SCALE; // Rounds down
                eval.borrowTrueValue += borrowValue;
                uint256 borrowFactor = marketCount == 1
                    ? SELF_COLLATERALIZATION_FACTOR
                    : _account.modeId == 0 ? uint256(marketConfiguration_.borrowFactor) : uint256(mode.borrowFactor);
                eval.borrowAdjValue += (borrowValue * FACTOR_PRECISION_SCALE) / borrowFactor; // Rounds down
            }
        }
    }

    /**
     * @notice Allows an account to borrow funds from a specified market the subaccount has entered, provided the account remains in a healthy financial standing post-borrow.
     * @param _subId The sub-account identifier from which to borrow.
     * @param _market The address of the market from which to borrow.
     * @param _amount The amount of funds to borrow.
     */
    function borrow(uint96 _subId, address _market, uint256 _amount) external nonReentrant whenNotPaused {
        bytes32 accountId = msg.sender.toAccount(_subId);
        AccountInfo memory account = accountInfos[accountId];
        address[] memory poolMarkets = getAccountPoolMarkets(accountId, account);
        require(_contains(poolMarkets, _market), "OmniPool::borrow: Not in pool markets.");
        uint8 borrowTier = getAccountBorrowTier(account);
        IOmniToken(_market).borrow(accountId, borrowTier, _amount);
        Evaluation memory eval = _evaluateAccountInternal(accountId, poolMarkets, account);
        require(
            eval.depositAdjValue >= eval.borrowAdjValue && !eval.isExpired,
            "OmniPool::borrow: Not healthy after borrow."
        );
    }

    /**
     * @notice Allows an account to repay borrowed funds to a specified market the subaccount has entered.
     * @param _subId The sub-account identifier from which to repay.
     * @param _market The address of the market to which to repay.
     * @param _amount The amount of funds to repay. If _amount is 0, the contract will repay the entire borrow balance.
     */
    function repay(uint96 _subId, address _market, uint256 _amount) external {
        bytes32 accountId = msg.sender.toAccount(_subId);
        AccountInfo memory account = accountInfos[accountId];
        address[] memory poolMarkets = getAccountPoolMarkets(accountId, account);
        require(_contains(poolMarkets, _market), "OmniPool::repay: Not in pool markets.");
        uint8 borrowTier = getAccountBorrowTier(account);
        IOmniToken(_market).repay(accountId, msg.sender, borrowTier, _amount);
    }

    /**
     * @notice Initiates the liquidation process on an undercollateralized or expired account, repaying some or all of the target account's borrow balance
     * while seizing a portion of the target's collateral. The amount of collateral seized is determined by the liquidation bonus and the price of the
     * assets involved. Soft liquidation is only allowed if there is no bad debt, otherwise if bad debt exists a full liquidation is bypassed.
     * @dev Liquidation configuration must be set for the _collateralMarket or else will revert.
     * The seized amount of shares is not guaranteed to compensate the value of the repayment during liquidation. Liquidators should check the returned value if they have a
     * minimum expectation of payout from liquidating, and perform necessary logic to revert if necessary.
     * @param _params The LiquidationParams struct containing the target account's identifier, the liquidator's identifier, the market to be liquidated,
     * @return seizedShares The amount of shares seized from the liquidated account.
     */
    function liquidate(LiquidationParams calldata _params)
        external
        whenNotPaused
        nonReentrant
        returns (uint256[] memory seizedShares)
    {
        AccountInfo memory targetAccount = accountInfos[_params.targetAccountId];
        address[] memory poolMarkets = getAccountPoolMarkets(_params.targetAccountId, targetAccount);
        require(
            _contains(poolMarkets, _params.liquidateMarket), "OmniPool::liquidate: LiquidateMarket not in pool markets."
        );
        require(
            _contains(poolMarkets, _params.collateralMarket)
                || targetAccount.isolatedCollateralMarket == _params.collateralMarket,
            "OmniPool::liquidate: CollateralMarket not available to seize."
        );
        Evaluation memory evalBefore = _evaluateAccountInternal(_params.targetAccountId, poolMarkets, targetAccount);
        require(evalBefore.numBorrow > 0, "OmniPool::liquidate: No borrow to liquidate.");
        require(
            (evalBefore.depositAdjValue < evalBefore.borrowAdjValue)
                || marketConfigurations[_params.collateralMarket].expirationTimestamp <= block.timestamp,
            "OmniPool::liquidate: Account still healthy."
        );
        uint8 borrowTier = getAccountBorrowTier(targetAccount);
        uint256 amount =
            IOmniToken(_params.liquidateMarket).repay(_params.targetAccountId, msg.sender, borrowTier, _params.amount);
        (uint256 liquidationBonus, uint256 softThreshold) = getLiquidationBonusAndThreshold(
            evalBefore.depositAdjValue, evalBefore.borrowAdjValue, _params.collateralMarket
        );
        {
            // Avoid stack too deep
            uint256 borrowPrice = IOmniOracle(oracle).getPrice(IWithUnderlying(_params.liquidateMarket).underlying());
            uint256 depositPrice = IOmniOracle(oracle).getPrice(IWithUnderlying(_params.collateralMarket).underlying());
            uint256 seizeAmount = Math.ceilDiv(
                Math.ceilDiv(amount * borrowPrice, depositPrice) * (LIQ_BONUS_PRECISION_SCALE + liquidationBonus), // Need to add base since liquidationBonus < LIQ_BONUS_PRECISION_SCALE
                LIQ_BONUS_PRECISION_SCALE
            ); // round up
            seizedShares = IOmniTokenBase(_params.collateralMarket).seize(
                _params.targetAccountId, _params.liquidatorAccountId, seizeAmount
            );
        }
        Evaluation memory evalAfter = _evaluateAccountInternal(_params.targetAccountId, poolMarkets, targetAccount);
        if (evalAfter.borrowTrueValue > evalAfter.depositTrueValue) {
            pauseTranche = borrowTier > pauseTranche ? pauseTranche : borrowTier;
            emit PausedTranche(pauseTranche);
        } else if (!evalAfter.isExpired) {
            // If expired, no liquidation threshold
            require(
                checkSoftLiquidation(evalAfter.depositAdjValue, evalAfter.borrowAdjValue, softThreshold, targetAccount),
                "OmniPool::liquidate: Too much has been liquidated."
            );
        }
        emit Liquidated(
            msg.sender,
            _params.targetAccountId,
            _params.liquidatorAccountId,
            _params.liquidateMarket,
            _params.collateralMarket,
            amount
        );
    }

    /**
     * @notice Checks whether a soft liquidation condition is met based on the account's adjusted deposit and borrow values.
     * @param _depositAdjValue The adjusted value of the account's deposits.
     * @param _borrowAdjValue The adjusted value of the account's borrows.
     * @param _softThreshold The threshold value for soft liquidation.
     * @param _account The AccountInfo struct containing the account's mode, isolated collateral market, and other relevant data.
     * @return A boolean indicating whether a soft liquidation condition is met.
     */
    function checkSoftLiquidation(
        uint256 _depositAdjValue,
        uint256 _borrowAdjValue,
        uint256 _softThreshold,
        AccountInfo memory _account
    ) public pure returns (bool) {
        if (_borrowAdjValue == 0) {
            return false;
        }
        uint256 healthFactor = (_depositAdjValue * HEALTH_FACTOR_SCALE) / _borrowAdjValue; // Round down
        uint256 threshold = _account.softThreshold != 0 ? _account.softThreshold : _softThreshold;
        return healthFactor <= threshold;
    }

    /**
     * @notice Initiates the process of socializing a fully liquidated account's remaining loss to the users of the specified market and tranche, discretion to admin.
     * @dev There is a separate call that must be made to unpause the tranches, discretion to admin. Due to potential problems w/ a full liquidation
     * allow for 0.1bps ($10 for $1M) difference in deposit and borrow values. However, it is expected that admin calls liquidate prior to calling socializeLoss in script.
     * @param _market The address of the market in which the loss is socialized.
     * @param _account The unique identifier of the fully liquidated account.
     */
    function socializeLoss(address _market, bytes32 _account) external onlyRole(DEFAULT_ADMIN_ROLE) {
        uint8 borrowTier = getAccountBorrowTier(accountInfos[_account]);
        Evaluation memory eval = evaluateAccount(_account);
        uint256 percentDiff = eval.depositTrueValue * 1e18 / eval.borrowTrueValue;
        require(
            percentDiff < 0.00001e18,
            "OmniPool::socializeLoss: Account not fully liquidated, please call liquidate prior to fully liquidate account."
        );
        IOmniToken(_market).socializeLoss(_account, borrowTier);
        emit SocializedLoss(_market, borrowTier, _account);
    }

    /**
     * @notice Determines the risk tier associated with an subaccount's borrow activity. The tier is derived from the subaccount's isolated collateral market.
     * @param _account The AccountInfo struct containing the subaccount's mode, isolated collateral market, and other relevant data.
     * @return The risk tier associated with the subaccount's borrow activity.
     */
    function getAccountBorrowTier(AccountInfo memory _account) public view returns (uint8) {
        address isolatedCollateralMarket = _account.isolatedCollateralMarket;
        if (_account.modeId == 0) {
            if (isolatedCollateralMarket == address(0)) {
                // Account has no isolated collateral market. Use tier 0 (lowest risk).
                return 0;
            } else {
                // Account has isolated collateral market. Use the market's risk tranche.
                return marketConfigurations[isolatedCollateralMarket].riskTranche;
            }
        } else {
            // Account is in a mode. Use the mode's risk tranche.
            return modeConfigurations[_account.modeId].modeTranche;
        }
    }

    /**
     * @notice Retrieves all markets, except for the isolated collateral market, associated with an subaccount.
     * @param _accountId The unique identifier of the subaccount whose markets are to be retrieved.
     * @param _account The AccountInfo struct containing the subaccount's mode, isolated collateral market, and other relevant data.
     * @return An array of addresses representing the markets associated with the subaccount.
     */
    function getAccountPoolMarkets(bytes32 _accountId, AccountInfo memory _account)
        public
        view
        returns (address[] memory)
    {
        if (_account.modeId == 0) {
            // Account is not in a mode. Use the account's markets.
            return accountMarkets[_accountId];
        } else {
            // Account is in a mode. Use the mode's markets.
            assert(_account.modeId <= modeCount);
            return modeConfigurations[_account.modeId].markets;
        }
    }

    /**
     * @notice Computes the liquidation bonus and soft threshold values based on the account's adjusted deposit and borrow values and the specified collateral market.
     * @param _depositAdjValue The adjusted value of the account's deposits.
     * @param _borrowAdjValue The adjusted value of the account's borrows.
     * @param _collateralMarket The address of the collateral market.
     * @return bonus The computed liquidation bonus value.
     * @return softThreshold The computed soft threshold value.
     */
    function getLiquidationBonusAndThreshold(
        uint256 _depositAdjValue,
        uint256 _borrowAdjValue,
        address _collateralMarket
    ) public view returns (uint256 bonus, uint256 softThreshold) {
        if (_borrowAdjValue > _depositAdjValue) {
            // Prioritize unhealthiness over expiry in case where is expired and unhealthy is true
            LiquidationBonusConfiguration memory liquidationBonusConfiguration_ =
                liquidationBonusConfigurations[_collateralMarket];
            softThreshold = liquidationBonusConfiguration_.softThreshold;
            uint256 pctDiff =
                Math.ceilDiv(_borrowAdjValue * LIQ_BONUS_PRECISION_SCALE, _depositAdjValue) - LIQ_BONUS_PRECISION_SCALE; // Round up
            if (pctDiff <= liquidationBonusConfiguration_.kink) {
                bonus = liquidationBonusConfiguration_.start;
                bonus += Math.ceilDiv(
                    pctDiff * (liquidationBonusConfiguration_.end - liquidationBonusConfiguration_.start),
                    liquidationBonusConfiguration_.kink
                );
            } else {
                bonus = liquidationBonusConfiguration_.end;
            }
        } else if (marketConfigurations[_collateralMarket].expirationTimestamp <= block.timestamp) {
            LiquidationBonusConfiguration memory liquidationBonusConfiguration_ =
                liquidationBonusConfigurations[_collateralMarket];
            softThreshold = liquidationBonusConfiguration_.softThreshold;
            bonus = liquidationBonusConfiguration_.expiredBonus;
        } else {
            revert("OmniPool::getLiquidationBonus: No liquidation bonus, account is not liquidatable ");
        }
    }

    /**
     * @notice Determines if an account is healthy by comparing the factor adjusted price weighted values of deposits and borrows.
     * @dev The function evaluates the account and returns true if the account is healthy. Intentionally do not check expiration here.
     * @param _accountId The account identifier.
     * @return A boolean indicating whether the account is healthy.
     */
    function isAccountHealthy(bytes32 _accountId) external returns (bool) {
        Evaluation memory eval = evaluateAccount(_accountId);
        return eval.depositAdjValue >= eval.borrowAdjValue && !eval.isExpired;
    }

    /**
     * @notice Resets the pause tranche to its default value. This function should only be called after all bad debt is resolved.
     * Must be called by an account with the DEFAULT_ADMIN_ROLE.
     */
    function resetPauseTranche() public onlyRole(DEFAULT_ADMIN_ROLE) {
        pauseTranche = type(uint8).max;
        emit UnpausedTranche();
    }

    /**
     * @notice Configures a market with specific parameters. This function can only be called by an account with the MARKET_CONFIGURATOR_ROLE.
     * It validates the configuration provided especially focusing on isolated collateral settings, borrow factors and risk tranches.
     * Should never configure a IOmniTokenNoBorrow (non-borrwable) token with a borrowFactor > 0 and not as isolated, otherwise will break.
     * @dev Setting markets to the 0 riskTranche comes with special privileges and should be used carefully after strict risk analysis.
     * @param _market The address of the market to be configured.
     * @param _marketConfig The MarketConfiguration struct containing the market's configurations.
     */
    function setMarketConfiguration(address _market, MarketConfiguration calldata _marketConfig)
        external
        onlyRole(MARKET_CONFIGURATOR_ROLE)
    {
        // Set to block.timestamp value to have the market expire in that block for emergencies
        if (_marketConfig.expirationTimestamp <= block.timestamp) {
            revert("OmniPool::setMarketConfiguration: Bad expiration timestamp.");
        }
        if (_marketConfig.isIsolatedCollateral && (_marketConfig.borrowFactor > 0 || _marketConfig.riskTranche == 0)) {
            revert("OmniPool::setMarketConfiguration: Bad configuration for isolated collateral.");
        }
        if (
            _marketConfig.collateralFactor == 0
                && (_marketConfig.borrowFactor == 0 || _marketConfig.riskTranche != type(uint8).max)
        ) {
            revert("OmniPool::setMarketConfiguration: Invalid configuration for borrowable long tail asset.");
        }
        MarketConfiguration memory currentConfig = marketConfigurations[_market];
        if (currentConfig.collateralFactor != 0) {
            require(
                _marketConfig.isIsolatedCollateral == currentConfig.isIsolatedCollateral,
                "OmniPool::setMarketConfiguration: Cannot change isolated collateral status."
            );
        }
        marketConfigurations[_market] = _marketConfig;
        emit SetMarketConfiguration(_market, _marketConfig);
    }

    /**
     * @notice Removes the market configuration for a specified market.
     * @dev This function can only be called by an account with the `MARKET_CONFIGURATOR_ROLE` role.
     * It checks if the market's underlying asset balance is zero before allowing removal.
     * @param _market The address of the market whose configuration is to be removed.
     */
    function removeMarketConfiguration(address _market) external onlyRole(MARKET_CONFIGURATOR_ROLE) {
        require(
            IERC20(IWithUnderlying(_market).underlying()).balanceOf(_market) == 0,
            "OmniPool::removeMarketConfiguration: Market still has balance."
        );
        delete marketConfigurations[_market];
        emit RemovedMarketConfiguration(_market);
    }

    /**
     * @notice Sets the configurations for a mode. This function can only be called by an account with the MARKET_CONFIGURATOR_ROLE.
     * Each mode configuration overrides all borrow and collateral factors for markets within that mode and should be used cautiously.
     * @dev This is a privileged function that should be used carefully after strict risk analysis, as it overrides factors for all markets in the mode.
     * Modes should never include markets that are considered isolated assets.
     * @param _modeConfiguration A ModeConfiguration struct containing the configuration for the mode.
     */
    function setModeConfiguration(ModeConfiguration calldata _modeConfiguration)
        external
        onlyRole(MARKET_CONFIGURATOR_ROLE)
    {
        if (_modeConfiguration.expirationTimestamp <= block.timestamp) {
            revert("OmniPool::setModeConfiguration: Bad expiration timestamp.");
        }
        for (uint256 i = 0; i < _modeConfiguration.markets.length; ++i) {
            for (uint256 j = i + 1; j < _modeConfiguration.markets.length; j++) {
                if (_modeConfiguration.markets[i] == _modeConfiguration.markets[j]) {
                    revert("OmniPool:setModeConfiguration: No duplicate markets allowed.");
                }
            }
        }
        modeCount++;
        modeConfigurations[modeCount] = _modeConfiguration;
        emit SetModeConfiguration(modeCount, _modeConfiguration);
    }

    /**
     * @notice Sets the expiration timestamp for a specified mode. This expiration only signifies the mode can no longer be entered, but does not force exit exisitng subaccounts from the mode.
     * This function allows for updating the expiration timestamp of a specific mode, given its mode ID.
     * It reverts if the provided expiration timestamp is in the past or if the mode ID is invalid.
     * Only an account with the MARKET_CONFIGURATOR_ROLE can call this function.
     * @param _modeId The ID of the mode whose expiration timestamp is to be updated.
     * @param _expirationTimestamp The new expiration timestamp for the mode.
     */
    function setModeExpiration(uint256 _modeId, uint32 _expirationTimestamp)
        external
        onlyRole(MARKET_CONFIGURATOR_ROLE)
    {
        require(_expirationTimestamp > block.timestamp, "OmniPool::setModeExpiration: Bad expiration timestamp.");
        require(_modeId != 0 && _modeId <= modeCount, "OmniPool::setModeExpiration: Bad mode ID.");
        modeConfigurations[_modeId].expirationTimestamp = _expirationTimestamp;
    }

    /**
     * @notice Sets a specific soft liquidation threshold for an account. This function can only be called by an account with the SOFT_LIQUIDATION_ROLE.
     * The soft liquidation threshold determines the health factor below which an account is considered for soft liquidation.
     * @dev The soft liquidation role should only be assigned to the admin or a smart contract that implements a strategy for why a user should receive a special soft liquidation.
     * @param _accountId The unique identifier of the account for which to set the soft liquidation threshold.
     * @param _softThreshold The soft liquidation threshold to set for the account.
     */
    function setAccountSoftLiquidation(bytes32 _accountId, uint32 _softThreshold)
        external
        onlyRole(SOFT_LIQUIDATION_ROLE)
    {
        if (_softThreshold > MAX_BASE_SOFT_LIQUIDATION || _softThreshold < HEALTH_FACTOR_SCALE) {
            revert(
                "OmniPool::setSoftLiquidation: Soft liquidation health factor threshold cannot be greater than the standard max and must be greater than 1."
            );
        }
        accountInfos[_accountId].softThreshold = _softThreshold;
    }

    /**
     * @notice Sets the configuration for liquidation bonuses for a specific market. This function can only be called by an account with the MARKET_CONFIGURATOR_ROLE.
     * The configuration includes parameters that affect the calculation of liquidation bonuses during the liquidation process.
     * @param _market The address of the market for which to set the liquidation bonus configuration.
     * @param _config The LiquidationBonusConfiguration struct containing the configuration for liquidation bonuses.
     */
    function setLiquidationBonusConfiguration(address _market, LiquidationBonusConfiguration calldata _config)
        external
        onlyRole(MARKET_CONFIGURATOR_ROLE)
    {
        require(
            _config.kink <= MAX_LIQ_KINK,
            "OmniPool::setLiquidationBonusConfiguration: Bad kink for maximum liquidation."
        );
        require(
            _config.start <= _config.end && _config.end <= LIQ_BONUS_PRECISION_SCALE,
            "OmniPool::setLiquidationBonusConfiguration: Bad start and end bonus values."
        );
        if (_config.expiredBonus > LIQ_BONUS_PRECISION_SCALE) {
            revert("OmniPool::setLiquidationBonusConfiguration: Bad expired bonus value.");
        }
        if (_config.softThreshold > MAX_BASE_SOFT_LIQUIDATION || _config.softThreshold < HEALTH_FACTOR_SCALE) {
            revert(
                "OmniPool::setSoftLiquidation: Soft liquidation health factor threshold cannot be greater than the standard max and must be greater than 1."
            );
        }
        liquidationBonusConfigurations[_market] = _config;
    }

    /**
     * @notice Sets the tranche count for a specific market.
     * @dev This function allows to set the number of tranches for a given market.
     * It's an external function that can only be called by an account with the `MARKET_CONFIGURATOR_ROLE`.
     * @param _market The address of the market contract.
     * @param _trancheCount The number of tranches to be set for the market.
     */
    function setTrancheCount(address _market, uint8 _trancheCount) external onlyRole(MARKET_CONFIGURATOR_ROLE) {
        IOmniToken(_market).setTrancheCount(_trancheCount);
    }

    /**
     * @notice Sets the borrow cap for each tranche of a specific market.
     * @dev This function can only be called by an account with the MARKET_CONFIGURATOR_ROLE.
     * It invokes the setTrancheBorrowCaps function of the IOmniToken contract associated with the specified market.
     * @param _market The address of the market for which to set the borrow caps.
     * @param _borrowCaps An array of borrow cap values, one for each tranche of the market.
     */
    function setBorrowCap(address _market, uint256[] calldata _borrowCaps)
        external
        onlyRole(MARKET_CONFIGURATOR_ROLE)
    {
        for (uint256 i = 0; i < _borrowCaps.length - 1; ++i) {
            require(_borrowCaps[i] >= _borrowCaps[i + 1], "OmniPool::setBorrowCap: Invalid borrow cap.");
        }
        IOmniToken(_market).setTrancheBorrowCaps(_borrowCaps);
    }

    /**
     * @notice Sets the supply cap for a market that doesn't allow borrowing.
     * @dev This function can only be called by an account with the MARKET_CONFIGURATOR_ROLE.
     * It invokes the setSupplyCap function of the IOmniTokenNoBorrow contract associated with the specified market.
     * @param _market The address of the market for which to set the no-borrow supply cap.
     * @param _noBorrowSupplyCap The value of the no-borrow supply cap to set.
     */
    function setNoBorrowSupplyCap(address _market, uint256 _noBorrowSupplyCap)
        external
        onlyRole(MARKET_CONFIGURATOR_ROLE)
    {
        IOmniTokenNoBorrow(_market).setSupplyCap(_noBorrowSupplyCap);
    }

    /**
     * @notice Sets the reserve receiver's address. This function can only be called by an account with the DEFAULT_ADMIN_ROLE.
     * @dev The reserve receiver's address is converted to a bytes32 account identifier using the toAccount function with a subId of 0.
     * @param _reserveReceiver The address of the reserve receiver to be set.
     */
    function setReserveReceiver(address _reserveReceiver) external onlyRole(DEFAULT_ADMIN_ROLE) {
        reserveReceiver = _reserveReceiver.toAccount(0);
    }

    /**
     * @notice Pauses the protocol, halting certain functionalities, i.e. withdraw, borrow, repay, liquidate.
     * @dev This function triggers the `_pause()` internal function and sets `pauseTranche` to 0.
     * It's an external function that can only be called by an account with the `DEFAULT_ADMIN_ROLE`.
     * The function can only be executed when the contract is not already paused,
     * which is checked by the `whenNotPaused` modifier.
     */
    function pause() external whenNotPaused onlyRole(DEFAULT_ADMIN_ROLE) {
        _pause();
        pauseTranche = 0;
        emit PausedTranche(0);
    }

    /**
     * @notice Unpauses the protocol, re-enabling certain functionalities, i.e. withdraw, borrow, repay, liquidate.
     * @dev This function triggers the `_unpause()` internal function and calls `resetPauseTranche()` to reset tranche pause state.
     * It's an external function that can only be called by an account with the `DEFAULT_ADMIN_ROLE`.
     * The function can only be executed when the contract is paused,
     * which is checked by the `whenPaused` modifier.
     */
    function unpause() external whenPaused onlyRole(DEFAULT_ADMIN_ROLE) {
        _unpause();
        resetPauseTranche();
    }

    /**
     * @dev Internal utility function to check if a specific value exists within an array of addresses.
     * @param _arr The array of addresses to search.
     * @param _value The address value to look for within the array.
     * @return A boolean indicating whether the value exists within the array.
     */
    function _contains(address[] memory _arr, address _value) internal pure returns (bool) {
        for (uint256 i = 0; i < _arr.length; ++i) {
            if (_arr[i] == _value) {
                return true;
            }
        }
        return false;
    }
}


================================================
FILE: src/08-omni-protocol/OmniToken.sol
================================================
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.23;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin-upgradeable/contracts/utils/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";

import "./interfaces/IIRM.sol";
import "./interfaces/IOmniPool.sol";
import "./interfaces/IOmniToken.sol";
import "./SubAccount.sol";
import "./WithUnderlying.sol";

/**
 * @title OmniToken Contract
 * @notice This contract manages deposits, withdrawals, borrowings, and repayments within the Omni protocol. There is only borrow caps, no supply caps.
 * @dev It has multiple tranches, each with its own borrowing and depositing conditions. This contract does not handle rebasing tokens.
 * Inherits from IOmniToken, WithUnderlying, and ReentrancyGuardUpgradeable (includes Initializable) from the OpenZeppelin library.
 * Utilizes the SafeERC20, SubAccount libraries for safe token transfers and account management.
 * Emits events for significant state changes like deposits, withdrawals, borrowings, repayments, and tranches updates.
 */
contract OmniToken is IOmniToken, WithUnderlying, ReentrancyGuardUpgradeable {
    struct OmniTokenTranche {
        uint256 totalDepositAmount;
        uint256 totalBorrowAmount;
        uint256 totalDepositShare;
        uint256 totalBorrowShare;
    }

    using SafeERC20 for IERC20;
    using SubAccount for address;
    using SubAccount for bytes32;

    uint256 public constant RESERVE_FEE = 0.1e9;
    uint256 public constant FEE_SCALE = 1e9;
    uint256 public constant IRM_SCALE = 1e9; // Must match IRM.sol
    uint256 private constant MAX_VIEW_ACCOUNTS = 25;

    address public omniPool;
    address public irm;
    uint256 public lastAccrualTime;
    uint8 public trancheCount;
    bytes32 public reserveReceiver;
    mapping(uint8 => mapping(bytes32 => uint256)) private trancheAccountDepositShares;
    mapping(uint8 => mapping(bytes32 => uint256)) private trancheAccountBorrowShares;
    uint256[] public trancheBorrowCaps;
    OmniTokenTranche[] public tranches;

    /**
     * @notice Contract initializes the OmniToken with required parameters.
     * @param _omniPool Address of the OmniPool contract.
     * @param _underlying Address of the underlying asset.
     * @param _irm Address of the Interest Rate Model contract.
     * @param _borrowCaps Initial borrow caps for each tranche.
     */
    function initialize(address _omniPool, address _underlying, address _irm, uint256[] calldata _borrowCaps)
        external
        initializer
    {
        __ReentrancyGuard_init();
        __WithUnderlying_init(_underlying);
        omniPool = _omniPool;
        irm = _irm;
        lastAccrualTime = block.timestamp;
        trancheBorrowCaps = _borrowCaps;
        trancheCount = uint8(_borrowCaps.length);
        for (uint8 i = 0; i < _borrowCaps.length; ++i) {
            tranches.push(OmniTokenTranche(0, 0, 0, 0));
        }
        reserveReceiver = IOmniPool(omniPool).reserveReceiver();
    }

    /**
     * @notice Accrues interest for all tranches, calculates and distributes the interest among the depositors and updates tranche balances.
     * The function also handles reserve payments. This method needs to be called before any deposit, withdrawal, borrow, or repayment actions to update the state of the contract.
     * @dev Interest is paid out proportionately to more risky tranche deposits per tranche
     */
    function accrue() public {
        uint256 timePassed = block.timestamp - lastAccrualTime;
        if (timePassed == 0) {
            return;
        }
        uint8 trancheIndex = trancheCount;
        uint256 totalBorrow = 0;
        uint256 totalDeposit = 0;
        uint256[] memory trancheDepositAmounts_ = new uint256[](trancheIndex); // trancheIndeex == trancheCount initially
        uint256[] memory trancheAccruedDepositCache = new uint256[](trancheIndex);
        uint256[] memory reserveFeeCache = new uint256[](trancheIndex);
        while (trancheIndex != 0) {
            unchecked {
                --trancheIndex;
            }
            OmniTokenTranche storage tranche = tranches[trancheIndex];
            uint256 trancheDepositAmount_ = tranche.totalDepositAmount;
            uint256 trancheBorrowAmount_ = tranche.totalBorrowAmount;
            totalBorrow += trancheBorrowAmount_;
            totalDeposit += trancheDepositAmount_;
            trancheDepositAmounts_[trancheIndex] = trancheDepositAmount_;
            trancheAccruedDepositCache[trancheIndex] = trancheDepositAmount_;

            if (trancheBorrowAmount_ == 0) {
                continue;
            }
            uint256 interestAmount;
            {
                uint256 interestRate = IIRM(irm).getInterestRate(address(this), trancheIndex, totalDeposit, totalBorrow);
                interestAmount = (trancheBorrowAmount_ * interestRate * timePassed) / 365 days / IRM_SCALE;
            }

            // Handle reserve payments
            uint256 reserveInterestAmount = interestAmount * RESERVE_FEE / FEE_SCALE;
            reserveFeeCache[trancheIndex] = reserveInterestAmount;

            // Handle deposit interest
            interestAmount -= reserveInterestAmount;
            {
                uint256 depositInterestAmount = 0;
                uint256 interestAmountProportion;
                for (uint8 ti = trancheCount; ti > trancheIndex;) {
                    unchecked {
                        --ti;
                    }
                    interestAmountProportion = interestAmount * trancheDepositAmounts_[ti] / totalDeposit;
                    trancheAccruedDepositCache[ti] += interestAmountProportion;
                    depositInterestAmount += interestAmountProportion;
                }
                tranche.totalBorrowAmount = trancheBorrowAmount_ + depositInterestAmount + reserveInterestAmount;
            }
        }
        for (uint8 ti = 0; ti < trancheCount; ++ti) {
            OmniTokenTranche memory tranche_ = tranches[ti];
            // Pay the reserve
            uint256 reserveShare;
            if (reserveFeeCache[ti] > 0) {
                if (trancheAccruedDepositCache[ti] == 0) {
                    reserveShare = reserveFeeCache[ti];
                } else {
                    reserveShare = (reserveFeeCache[ti] * tranche_.totalDepositShare) / trancheAccruedDepositCache[ti];
                }
                trancheAccruedDepositCache[ti] += reserveFeeCache[ti];
                trancheAccountDepositShares[ti][reserveReceiver] += reserveShare;
                tranche_.totalDepositShare += reserveShare;
            }
            tranche_.totalDepositAmount = trancheAccruedDepositCache[ti];
            tranches[ti] = tranche_;
        }
        lastAccrualTime = block.timestamp;
        emit Accrue();
    }

    /**
     * @notice Allows a user to deposit a specified amount into a specified tranche.
     * @param _subId Sub-account identifier for the depositor.
     * @param _trancheId Identifier of the tranche to deposit into.
     * @param _amount Amount to deposit.
     * @return share Amount of deposit shares received in exchange for the deposit.
     */
    function deposit(uint96 _subId, uint8 _trancheId, uint256 _amount) external nonReentrant returns (uint256 share) {
        require(_trancheId < IOmniPool(omniPool).pauseTranche(), "OmniToken::deposit: Tranche paused.");
        require(_trancheId < trancheCount, "OmniToken::deposit: Invalid tranche id.");
        accrue();
        bytes32 account = msg.sender.toAccount(_subId);
        uint256 amount = _inflowTokens(account.toAddress(), _amount);
        OmniTokenTranche storage tranche = tranches[_trancheId];
        uint256 totalDepositShare_ = tranche.totalDepositShare;
        uint256 totalDepositAmount_ = tranche.totalDepositAmount;
        if (totalDepositShare_ == 0) {
            share = amount;
        } else {
            assert(totalDepositAmount_ > 0);
            share = (amount * totalDepositShare_) / totalDepositAmount_;
        }
        tranche.totalDepositAmount = totalDepositAmount_ + amount;
        tranche.totalDepositShare = totalDepositShare_ + share;
        trancheAccountDepositShares[_trancheId][account] += share;
        emit Deposit(account, _trancheId, amount, share);
    }

    /**
     * @notice Allows a user to withdraw their funds from a specified tranche.
     * @param _subId The ID of the sub-account.
     * @param _trancheId The ID of the tranche.
     * @param _share The share of the user in the tranche.
     * @return amount The amount of funds withdrawn.
     */
    function withdraw(uint96 _subId, uint8 _trancheId, uint256 _share) external nonReentrant returns (uint256 amount) {
        require(_trancheId < IOmniPool(omniPool).pauseTranche(), "OmniToken::withdraw: Tranche paused.");
        require(_trancheId < trancheCount, "OmniToken::withdraw: Invalid tranche id.");
        accrue();
        bytes32 account = msg.sender.toAccount(_subId);
        OmniTokenTranche storage tranche = tranches[_trancheId];
        uint256 totalDepositAmount_ = tranche.totalDepositAmount;
        uint256 totalDepositShare_ = tranche.totalDepositShare;
        uint256 accountDepositShares_ = trancheAccountDepositShares[_trancheId][account];
        if (_share == 0) {
            _share = accountDepositShares_;
        }
        amount = (_share * totalDepositAmount_) / totalDepositShare_;
        tranche.totalDepositAmount = totalDepositAmount_ - amount;
        tranche.totalDepositShare = totalDepositShare_ - _share;
        trancheAccountDepositShares[_trancheId][account] = accountDepositShares_ - _share;
        require(_checkBorrowAllocationOk(), "OmniToken::withdraw: Insufficient withdrawals available.");
        _outflowTokens(account.toAddress(), amount);
        require(IOmniPool(omniPool).isAccountHealthy(account), "OmniToken::withdraw: Not healthy.");
        emit Withdraw(account, _trancheId, amount, _share);
    }

    /**
     * @notice Allows a user to borrow funds from a specified tranche.
     * @param _account The account of the user.
     * @param _trancheId The ID of the tranche.
     * @param _amount The amount to borrow.
     * @return share The share of the borrowed amount in the tranche.
     */
    function borrow(bytes32 _account, uint8 _trancheId, uint256 _amount)
        external
        nonReentrant
        returns (uint256 share)
    {
        require(_trancheId < IOmniPool(omniPool).pauseTranche(), "OmniToken::borrow: Tranche paused.");
        require(msg.sender == omniPool, "OmniToken::borrow: Bad caller.");
        accrue();
        OmniTokenTranche storage tranche = tranches[_trancheId];
        uint256 totalBorrowAmount_ = tranche.totalBorrowAmount;
        uint256 totalBorrowShare_ = tranche.totalBorrowShare;
        require(totalBorrowAmount_ + _amount <= trancheBorrowCaps[_trancheId], "OmniToken::borrow: Borrow cap reached.");
        if (totalBorrowShare_ == 0) {
            share = _amount;
        } else {
            assert(totalBorrowAmount_ > 0); // Should only happen if bad debt exists & all other debts repaid
            share = Math.ceilDiv(_amount * totalBorrowShare_, totalBorrowAmount_);
        }
        tranche.totalBorrowAmount = totalBorrowAmount_ + _amount;
        tranche.totalBorrowShare = totalBorrowShare_ + share;
        trancheAccountBorrowShares[_trancheId][_account] += share;
        require(_checkBorrowAllocationOk(), "OmniToken::borrow: Invalid borrow allocation.");
        _outflowTokens(_account.toAddress(), _amount);
        emit Borrow(_account, _trancheId, _amount, share);
    }

    /**
     * @notice Allows a user or another account to repay borrowed funds.
     * @param _account The account of the user.
     * @param _payer The account that will pay the borrowed amount.
     * @param _trancheId The ID of the tranche.
     * @param _amount The amount to repay.
     * @return amount The amount of the repaid amount in the tranche.
     */
    function repay(bytes32 _account, address _payer, uint8 _trancheId, uint256 _amount)
        external
        nonReentrant
        returns (uint256 amount)
    {
        require(msg.sender == omniPool, "OmniToken::repay: Bad caller.");
        accrue();
        OmniTokenTranche storage tranche = tranches[_trancheId];
        uint256 totalBorrowAmount_ = tranche.totalBorrowAmount;
        uint256 totalBorrowShare_ = tranche.totalBorrowShare;
        uint256 accountBorrowShares_ = trancheAccountBorrowShares[_trancheId][_account];
        if (_amount == 0) {
            _amount = Math.ceilDiv(accountBorrowShares_ * totalBorrowAmount_, totalBorrowShare_);
        }
        amount = _inflowTokens(_payer, _amount);
        uint256 share = (amount * totalBorrowShare_) / totalBorrowAmount_;    
        tranche.totalBorrowAmount = totalBorrowAmount_ - amount;
        tranche.totalBorrowShare = totalBorrowShare_ - share;
        trancheAccountBorrowShares[_trancheId][_account] = accountBorrowShares_ - share;
        emit Repay(_account, _payer, _trancheId, amount, share);
    }

    /**
     * @notice Transfers specified shares from one account to another within a specified tranche.
     * @dev This function can only be called externally and is protected against reentrancy.
     * Requires the tranche to be unpaused and the sender account to remain healthy post-transfer.
     * @param _subId The subscription ID related to the sender's account.
     * @param _to The account identifier to which shares are being transferred.
     * @param _trancheId The identifier of the tranche where the transfer is occurring.
     * @param _shares The amount of shares to transfer.
     * @return A boolean value indicating whether the transfer was successful.
     */
    function transfer(uint96 _subId, bytes32 _to, uint8 _trancheId, uint256 _shares)
        external
        nonReentrant
        returns (bool)
    {
        require(_trancheId < IOmniPool(omniPool).pauseTranche(), "OmniToken::transfer: Tranche paused.");
        accrue();
        bytes32 from = msg.sender.toAccount(_subId);
        trancheAccountDepositShares[_trancheId][from] -= _shares;
        trancheAccountDepositShares[_trancheId][_to] += _shares;
        require(IOmniPool(omniPool).isAccountHealthy(from), "OmniToken::transfer: Not healthy.");
        emit Transfer(from, _to, _trancheId, _shares);
        return true;
    }

    /**
     * @notice Allows the a liquidator to seize funds from a user's account. OmniPool is responsible for defining how this function is called.
     * Greedily seizes as much collateral as possible, does not revert if no more collateral is left to seize and _amount is nonzero.
     * @param _account The account from which funds will be seized.
     * @param _to The account to which seized funds will be sent.
     * @param _amount The amount of funds to seize.
     * @return seizedShares The shares seized from each tranche.
     */
    function seize(bytes32 _account, bytes32 _to, uint256 _amount)
        external
        override
        nonReentrant
        returns (uint256[] memory)
    {
        require(msg.sender == omniPool, "OmniToken::seize: Bad caller");
        accrue();
        uint256 amount_ = _amount;
        uint256[] memory seizedShares = new uint256[](trancheCount);
        for (uint8 ti = 0; ti < trancheCount; ++ti) {
            uint256 totalShare = tranches[ti].totalDepositShare;
            if (totalShare == 0) {
                continue;
            }
            uint256 totalAmount = tranches[ti].totalDepositAmount;
            uint256 share = trancheAccountDepositShares[ti][_account];
            uint256 amount = (share * totalAmount) / totalShare;
            if (amount_ > amount) {
                amount_ -= amount;
                trancheAccountDepositShares[ti][_account] = 0;
                trancheAccountDepositShares[ti][_to] += share;
                seizedShares[ti] = share;
            } else {
                uint256 transferShare = (share * amount_) / amount;
                trancheAccountDepositShares[ti][_account] = share - transferShare;
                trancheAccountDepositShares[ti][_to] += transferShare;
                seizedShares[ti] = transferShare;
                break;
            }
        }
        emit Seize(_account, _to, _amount, seizedShares);
        return seizedShares;
    }

    /**
     * @notice Distributes the bad debt loss in a tranche among all tranche members in cases of bad debt. OmniPool is responsible for defining how this function is called.
     * @dev This should only be called when the _account does not have any collateral left to seize.
     * @param _account The account that incurred a loss.
     * @param _trancheId The ID of the tranche.
     */
    function socializeLoss(bytes32 _account, uint8 _trancheId) external nonReentrant {
        require(msg.sender == omniPool, "OmniToken::socializeLoss: Bad caller");
        uint256 totalDeposits = 0;
        for (uint8 i = _trancheId; i < trancheCount; ++i) {
            totalDeposits += tranches[i].totalDepositAmount;
        }
        OmniTokenTranche storage tranche = tranches[_trancheId];
        uint256 share = trancheAccountBorrowShares[_trancheId][_account];
        uint256 amount = Math.ceilDiv(share * tranche.totalBorrowAmount, tranche.totalBorrowShare); // Represents amount of bad debt there still is (need to ensure user's account is emptied of collateral before this is called)
        uint256 leftoverAmount = amount;
        for (uint8 ti = trancheCount - 1; ti > _trancheId; --ti) {
            OmniTokenTranche storage upperTranche = tranches[ti];
            uint256 amountProp = (amount * upperTranche.totalDepositAmount) / totalDeposits;
            upperTranche.totalDepositAmount -= amountProp;
            leftoverAmount -= amountProp;
        }
        tranche.totalDepositAmount -= leftoverAmount;
        tranche.totalBorrowAmount -= amount;
        tranche.totalBorrowShare -= share;
        trancheAccountBorrowShares[_trancheId][_account] = 0;
        emit SocializedLoss(_account, _trancheId, amount, share);
    }

    /**
     * @notice Computes the borrowing amount of a specific account in the underlying asset for a given borrow tier.
     * @dev The division is ceiling division.
     * @param _account The account identifier for which the borrowing amount is to be computed.
     * @param _borrowTier The borrow tier identifier from which the borrowing amount is to be computed.
     * @return The borrowing amount of the account in the underlying asset for the given borrow tier.
     */
    function getAccountBorrowInUnderlying(bytes32 _account, uint8 _borrowTier) external view returns (uint256) {
        OmniTokenTranche storage tranche = tranches[_borrowTier];
        uint256 share = trancheAccountBorrowShares[_borrowTier][_account];
        if (share == 0) {
            return 0;
        } else {
            return Math.ceilDiv(share * tranche.totalBorrowAmount, tranche.totalBorrowShare);
        }
    }

    /**
     * @notice Retrieves the total deposit amount for a specific account across all tranches.
     * @param _account The account identifier.
     * @return The total deposit amount.
     */
    function getAccountDepositInUnderlying(bytes32 _account) public view returns (uint256) {
        uint256 totalDeposit = 0;
        for (uint8 trancheIndex = 0; trancheIndex < trancheCount; ++trancheIndex) {
            OmniTokenTranche storage tranche = tranches[trancheIndex];
            uint256 share = trancheAccountDepositShares[trancheIndex][_account];
            if (share > 0) {
                totalDeposit += (share * tranche.totalDepositAmount) / tranche.totalDepositShare;
            }
        }
        return totalDeposit;
    }

    /**
     * @notice Retrieves the deposit and borrow shares for a specific account in a specific tranche.
     * @param _account The account identifier.
     * @param _trancheId The tranche identifier.
     * @return depositShare The deposit share.
     * @return borrowShare The borrow share.
     */
    function getAccountSharesByTranche(bytes32 _account, uint8 _trancheId)
        external
        view
        returns (uint256 depositShare, uint256 borrowShare)
    {
        depositShare = trancheAccountDepositShares[_trancheId][_account];
        borrowShare = trancheAccountBorrowShares[_trancheId][_account];
    }

    /**
     * @notice Gets the borrow cap for a specific tranche.
     * @param _trancheId The ID of the tranche for which to retrieve the borrow cap.
     * @return The borrow cap for the specified tranche.
     */
    function getBorrowCap(uint8 _trancheId) external view returns (uint256) {
        return trancheBorrowCaps[_trancheId];
    }

    /**
     * @notice Sets the borrow caps for each tranche.
     * @param _borrowCaps An array of borrow caps in the underlying's decimals.
     */
    function setTrancheBorrowCaps(uint256[] calldata _borrowCaps) external {
        require(msg.sender == omniPool, "OmniToken::setTrancheBorrowCaps: Bad caller.");
        require(_borrowCaps.length == trancheCount, "OmniToken::setTrancheBorrowCaps: Invalid borrow caps length.");
        require(
            _borrowCaps[0] > 0, "OmniToken::setTrancheBorrowCaps: Invalid borrow caps, must always allow 0 to borrow."
        );
        trancheBorrowCaps = _borrowCaps;
        emit SetTrancheBorrowCaps(_borrowCaps);
    }

    /**
     * @notice Sets the number of tranches. Can only increase the number of tranches by one at a time, never decrease.
     * @param _trancheCount The new tranche count.
     */
    function setTrancheCount(uint8 _trancheCount) external {
        require(msg.sender == omniPool, "OmniToken::setTrancheCount: Bad caller.");
        require(_trancheCount == trancheCount + 1, "OmniToken::setTrancheCount: Invalid tranche count.");
        trancheCount = _trancheCount;
        OmniTokenTranche memory tranche = OmniTokenTranche(0, 0, 0, 0);
        tranches.push(tranche);
        emit SetTrancheCount(_trancheCount);
    }

    /**
     * @notice Fetches and updates the reserve receiver from the OmniPool contract. Anyone can call.
     */
    function fetchReserveReceiver() external {
        reserveReceiver = IOmniPool(omniPool).reserveReceiver();
    }

    /**
     * @notice Calculates the total deposited amount for a specific owner across MAX_VIEW_ACCOUNTS sub-accounts. Above will be excluded, function is imperfect.
     * @dev This is just for wallets and Etherscan to pick up the deposit balance of a user for the first MAX_VIEW_ACCOUNTS sub-accounts.
     * @param _owner The address of the owner.
     * @return The total deposited amount.
     */
    function balanceOf(address _owner) external view returns (uint256) {
        uint256 totalDeposit = 0;
        for (uint96 i = 0; i < MAX_VIEW_ACCOUNTS; ++i) {
            totalDeposit += getAccountDepositInUnderlying(_owner.toAccount(i));
        }
        return totalDeposit;
    }

    /**
     * @notice Checks if the borrow allocation is valid across all tranches, through the invariant cumulative totalBorrow <= totalDeposit from highest to lowest tranche.
     * @return A boolean value indicating the validity of the borrow allocation.
     */
    function _checkBorrowAllocationOk() internal view returns (bool) {
        uint8 trancheIndex = trancheCount;
        uint256 totalBorrow = 0;
        uint256 totalDeposit = 0;
        while (trancheIndex != 0) {
            unchecked {
                --trancheIndex;
            }
            totalBorrow += tranches[trancheIndex].totalBorrowAmount;
            totalDeposit += tranches[trancheIndex].totalDepositAmount;
            if (totalBorrow > totalDeposit) {
                return false;
            }
        }
        return true;
    }
}


================================================
FILE: src/08-omni-protocol/OmniTokenNoBorrow.sol
================================================
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.23;

import "@openzeppelin-upgradeable/contracts/utils/ReentrancyGuardUpgradeable.sol";

import "./interfaces/IOmniPool.sol";
import "./interfaces/IOmniTokenNoBorrow.sol";
import "./SubAccount.sol";
import "./WithUnderlying.sol";

/**
 * @title OmniTokenNoBorrow
 * @notice This contract represents a token pool with deposit and withdrawal capabilities, without borrowing features. Should only be used for isolated collateral, never borrowable. There is only supply caps.
 * @dev It inherits functionalities from WithUnderlying, ReentrancyGuardUpgradeable (includes Initializable), and implements IOmniTokenNoBorrow interface.
 * The contract allows depositors to deposit and withdraw their funds, and for the OmniPool to seize funds if necessary.
 * It keeps track of the total supply and individual balances, and enforces a supply cap. This contract does not handle rebasing tokens.
 */
contract OmniTokenNoBorrow is IOmniTokenNoBorrow, WithUnderlying, ReentrancyGuardUpgradeable {
    using SubAccount for address;

    uint256 private constant MAX_VIEW_ACCOUNTS = 25;

    address public omniPool;
    uint256 public totalSupply;
    uint256 public supplyCap;
    mapping(bytes32 => uint256) public balanceOfAccount;

    /**
     * @notice Contract initializes the OmniTokenNoBorrow with required parameters.
     * @param _omniPool Address of the OmniPool contract.
     * @param _underlying Address of the underlying asset.
     * @param _supplyCap Initial supply cap.
     */
    function initialize(address _omniPool, address _underlying, uint256 _supplyCap) external initializer {
        __ReentrancyGuard_init();
        __WithUnderlying_init(_underlying);
        omniPool = _omniPool;
        supplyCap = _supplyCap;
    }

    /**
     * @notice Deposits a specified amount to the account associated with the message sender and the specified subId.
     * @param _subId The sub-account identifier.
     * @param _amount The amount to deposit.
     * @return amount The actual amount deposited.
     */
    function deposit(uint96 _subId, uint256 _amount) external nonReentrant returns (uint256 amount) {
        bytes32 account = msg.sender.toAccount(_subId);
        amount = _inflowTokens(msg.sender, _amount);
        require(totalSupply + amount <= supplyCap, "OmniTokenNoBorrow::deposit: Supply cap exceeded.");
        totalSupply += amount;
        balanceOfAccount[account] += amount;
        emit Deposit(account, amount);
    }

    /**
     * @notice Withdraws a specified amount from the account associated with the message sender and the specified subId.
     * @param _subId The sub-account identifier.
     * @param _amount The amount to withdraw.
     * @return amount The actual amount withdrawn.
     */
    function withdraw(uint96 _subId, uint256 _amount) external nonReentrant returns (uint256 amount) {
        bytes32 account = msg.sender.toAccount(_subId);
        if (_amount == 0) {
            _amount = balanceOfAccount[account];
        }
        balanceOfAccount[account] -= _amount;
        totalSupply -= _amount;
        amount = _outflowTokens(msg.sender, _amount);
        require(IOmniPool(omniPool).isAccountHealthy(account), "OmniTokenNoBorrow::withdraw: Not healthy.");
        emit Withdraw(account, amount);
    }

    /**
     * @notice Transfers a specified amount of tokens from the sender's account to another account.
     * The transfer operation is subject to the sender's account remaining healthy post-transfer.
     * @dev This function can only be called externally and is protected against reentrant calls.
     * @param _subId The subscription ID associated with the sender's account.
     * @param _to The account identifier to which the tokens are being transferred.
     * @param _amount The amount of tokens to transfer.
     * @return A boolean value indicating whether the transfer was successful.
     */
    function transfer(uint96 _subId, bytes32 _to, uint256 _amount) external nonReentrant returns (bool) {
        bytes32 from = msg.sender.toAccount(_subId);
        balanceOfAccount[from] -= _amount;
        balanceOfAccount[_to] += _amount;
        require(IOmniPool(omniPool).isAccountHealthy(from), "OmniTokenNoBorrow::transfer: Not healthy.");
        emit Transfer(from, _to, _amount);
        return true;
    }

    /**
     * @notice Allows the a liquidator to seize funds from a user's account. OmniPool is responsible for defining how this function is called. Should be called carefully, as it has strong privileges.
     * @param _account The account from which funds are seized.
     * @param _to The account to which funds are transferred.
     * @param _amount The amount of funds to seize.
     * @return seizedShares The shares corresponding to the seized amount.
     */
    function seize(bytes32 _account, bytes32 _to, uint256 _amount)
        external
        override
        nonReentrant
        returns (uint256[] memory)
    {
        require(msg.sender == omniPool, "OmniTokenNoBorrow::seize: Bad caller.");
        uint256 accountBalance = balanceOfAccount[_account];
        if (accountBalance < _amount) {
            _amount = accountBalance;
            balanceOfAccount[_account] = 0;
            balanceOfAccount[_to] += accountBalance;
        } else {
            balanceOfAccount[_account] -= _amount;
            balanceOfAccount[_to] += _amount;
        }
        uint256[] memory seizedShares = new uint256[](1);
        seizedShares[0] = _amount;
        emit Seize(_account, _to, _amount, seizedShares);
        return seizedShares;
    }

    /**
     * @notice Returns the deposit balance of a specific account.
     * @param _account The account identifier.
     * @return The deposit balance of the account.
     */
    function getAccountDepositInUnderlying(bytes32 _account) external view override returns (uint256) {
        return balanceOfAccount[_account];
    }

    /**
     * @notice Sets a new supply cap for the contract.
     * @param _supplyCap The new supply cap amount.
     */
    function setSupplyCap(uint256 _supplyCap) external {
        require(msg.sender == omniPool, "OmniTokenNoBorrow::setSupplyCap: Bad caller.");
        supplyCap = _supplyCap;
        emit SetSupplyCap(_supplyCap);
    }

    /**
     * @notice Calculates the total deposited amount for a specific owner across MAX_VIEW_ACCOUNTS sub-accounts. Above will be excluded, function is imperfect.
     * @dev This is just for wallets and Etherscan to pick up the deposit balance of a user for the first MAX_VIEW_ACCOUNTS sub-accounts.
     * @param _owner The address of the owner.
     * @return The total deposited amount.
     */
    function balanceOf(address _owner) external view returns (uint256) {
        uint256 totalDeposit = 0;
        for (uint96 i = 0; i < MAX_VIEW_ACCOUNTS; ++i) {
            totalDeposit += balanceOfAccount[_owner.toAccount(i)];
        }
        return totalDeposit;
    }
}


================================================
FILE: src/08-omni-protocol/SubAccount.sol
================================================
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.23;

/**
 * @title SubAccount
 * @notice This library provides utility functions to handle sub-accounts using bytes32 types, where id is most significant bytes.
 */
library SubAccount {
    /**
     * @notice Combines an address and a sub-account identifier into a bytes32 account representation.
     * @param _sender The address component.
     * @param _subId The sub-account identifier component.
     * @return A bytes32 representation of the account.
     */
    function toAccount(address _sender, uint96 _subId) internal pure returns (bytes32) {
        return bytes32(uint256(uint160(_sender)) | (uint256(_subId) << 160));
    }

    /**
     * @notice Extracts the address component from a bytes32 account representation.
     * @param _account The bytes32 representation of the account.
     * @return The address component.
     */
    function toAddress(bytes32 _account) internal pure returns (address) {
        return address(uint160(uint256(_account)));
    }

    /**
     * @notice Extracts the sub-account identifier component from a bytes32 account representation.
     * @param _account The bytes32 representation of the account.
     * @return The sub-account identifier component.
     */
    function toSubId(bytes32 _account) internal pure returns (uint96) {
        return uint96(uint256(_account) >> 160);
    }
}


================================================
FILE: src/08-omni-protocol/WETHGateway.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol";

import "./interfaces/IOmniToken.sol";
import "./interfaces/IWETH9.sol";
import "./interfaces/IWithUnderlying.sol";
import "./SubAccount.sol";

/**
 * @title WETHGateway
 * @notice Handles native ETH deposits directly to contract through WETH, but does not handle native ETH withdrawals.
 * @dev This contract serves as a gateway for handling deposits of native ETH, which are then wrapped into WETH tokens.
 */
contract WETHGateway is Initializable {
    using SubAccount for address;

    address public oweth;
    address public weth;
    uint96 private constant SUBACCOUNT_ID = 0;

    event Deposit(bytes32 indexed account, uint8 indexed trancheId, uint256 amount, uint256 share);

    /**
     * @notice Initializes the contract with the OWETH contract address.
     * @param _oweth The address of the OWETH contract.
     */
    function initialize(address _oweth) external initializer {
        address _weth = IWithUnderlying(_oweth).underlying();
        IWETH9(_weth).approve(_oweth, type(uint256).max);
        oweth = _oweth;
        weth = _weth;
    }

    /**
     * @notice Deposits native ETH to the contract, wraps it into WETH tokens, and handles the deposit operation
     * through the Omni Token contract.
     * @dev The function is payable to accept ETH deposits.
     * @param _subId The subscription ID related to the depositor's account.
     * @param _trancheId The identifier of the tranche where the deposit is occurring.
     * @return share The number of shares received in exchange for the deposited ETH.
     */
    function deposit(uint96 _subId, uint8 _trancheId) external payable returns (uint256 share) {
        bytes32 to = msg.sender.toAccount(_subId);
        IWETH9(weth).deposit{value: msg.value}();
        share = IOmniToken(oweth).deposit(SUBACCOUNT_ID, _trancheId, msg.value);
        IOmniToken(oweth).transfer(SUBACCOUNT_ID, to, _trancheId, share);
        emit Deposit(to, _trancheId, msg.value, share);
    }

    /**
     * @notice Fallback function that reverts if ETH is sent directly to the contract.
     * @dev Any attempts to send ETH directly to the contract will cause a transaction revert.
     */
    receive() external payable {
        revert("This contract should not accept ETH directly.");
    }
}


================================================
FILE: src/08-omni-protocol/WithUnderlying.sol
================================================
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.23;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol";
import "./interfaces/IWithUnderlying.sol";

/**
 * @title WithUnderlying
 * @notice A helper contract to handle the inflow and outflow of ERC20 tokens.
 * @dev Utilizes OpenZeppelin's SafeERC20 library to handle ERC20 transactions.
 */
abstract contract WithUnderlying is Initializable, IWithUnderlying {
    using SafeERC20 for IERC20;

    address public underlying;

    /**
     * @notice Initialies the abstract contract instance.
     * @param _underlying The address of the underlying ERC20 token.
     */
    function __WithUnderlying_init(address _underlying) internal onlyInitializing {
        underlying = _underlying;
    }

    /**
     * @notice Retrieves the name of the token.
     * @return The name of the token, either prefixed from the underlying token or the default "Omni Token".
     */
    function name() external view returns (string memory) {
        try IERC20Metadata(underlying).name() returns (string memory data) {
            return string(abi.encodePacked("Omni ", data));
        } catch (bytes memory) {
            return "Omni Token";
        }
    }

    /**
     * @notice Retrieves the symbol of the token.
     * @return The symbol of the token, either prefixed from the underlying token or the default "oToken".
     */
    function symbol() external view returns (string memory) {
        try IERC20Metadata(underlying).symbol() returns (string memory data) {
            return string(abi.encodePacked("o", data));
        } catch (bytes memory) {
            return "oToken";
        }
    }

    /**
     * @notice Retrieves the number of decimals the token uses.
     * @return The number of decimals of the token, either from the underlying token or the default 18.
     */
    function decimals() external view returns (uint8) {
        try IERC20Metadata(underlying).decimals() returns (uint8 data) {
            return data;
        } catch (bytes memory) {
            return 18;
        }
    }

    /**
     * @notice Handles the inflow of tokens to the contract.
     * @dev Transfers `_amount` tokens from `_from` to this contract and returns the actual amount received.
     * @param _from The address from which tokens are transferred.
     * @param _amount The amount of tokens to transfer.
     * @return The actual amount of tokens received by the contract.
     */
    function _inflowTokens(address _from, uint256 _amount) internal returns (uint256) {
        uint256 balanceBefore = IERC20(underlying).balanceOf(address(this));
        IERC20(underlying).safeTransferFrom(_from, address(this), _amount);
        uint256 balanceAfter = IERC20(underlying).balanceOf(address(this));
        return balanceAfter - balanceBefore;
    }

    /**
     * @notice Handles the outflow of tokens from the contract.
     * @dev Transfers `_amount` tokens from this contract to `_to` and returns the actual amount sent.
     * @param _to The address to which tokens are transferred.
     * @param _amount The amount of tokens to transfer.
     * @return The actual amount of tokens sent from the contract.
     */
    function _outflowTokens(address _to, uint256 _amount) internal returns (uint256) {
        uint256 balanceBefore = IERC20(underlying).balanceOf(address(this));
        IERC20(underlying).safeTransfer(_to, _amount);
        uint256 balanceAfter = IERC20(underlying).balanceOf(address(this));
        return balanceBefore - balanceAfter;
    }
}


================================================
FILE: src/08-omni-protocol/interfaces/IBandReference.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

interface IStdReference {
    /// A structure returned whenever someone requests for standard reference data.
    struct ReferenceData {
        uint256 rate; // base/quote exchange rate, multiplied by 1e18.
        uint256 lastUpdatedBase; // UNIX epoch of the last time when base price gets updated.
        uint256 lastUpdatedQuote; // UNIX epoch of the last time when quote price gets updated.
    }

    /// @dev Returns the price data for the given base/quote pair. Revert if not available.
    function getReferenceData(string memory _base, string memory _quote) external view returns (ReferenceData memory);

    /// @dev Similar to getReferenceData, but with multiple base/quote pairs at once.
    function getReferenceDataBulk(string[] memory _bases, string[] memory _quotes)
        external
        view
        returns (ReferenceData[] memory);
}


================================================
FILE: src/08-omni-protocol/interfaces/IChainlinkAggregator.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

interface IChainlinkAggregator {
    function decimals() external view returns (uint8);

    function description() external view returns (string memory);

    function version() external view returns (uint256);

    function getRoundData(uint80 _roundId)
        external
        view
        returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);

    function latestRoundData()
        external
        view
        returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);
}


================================================
FILE: src/08-omni-protocol/interfaces/ICustomOmniOracle.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

/**
 * @title ICustomOmniOracle Interface
 * @notice Interface for the custom oracle used by OmniOracle contract.
 */
interface ICustomOmniOracle {
    /**
     * @notice Fetches the price of the specified asset.
     * @param _underlying The address of the asset.
     * @return The price of the asset, normalized to 1e18.
     */
    function getPrice(address _underlying) external view returns (uint256);
}


================================================
FILE: src/08-omni-protocol/interfaces/IIRM.sol
================================================
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.23;

/**
 * @title Interest Rate Model (IRM) Interface
 * @notice This interface describes the publicly accessible functions implemented by the IRM contract.
 */
interface IIRM {
    /// Events
    event SetIRMForMarket(address indexed market, uint8[] tranches, IRMConfig[] configs);

    /**
     * @notice This structure defines the configuration for the interest rate model.
     * @dev It contains the kink utilization point, and the interest rates at 0%, kink, and 100% utilization.
     */
    struct IRMConfig {
        uint64 kink; // utilization at mid point (1e9 is 100%)
        uint64 start; // interest rate at 0% utlization
        uint64 mid; // interest rate at kink utlization
        uint64 end; // interest rate at 100% utlization
    }

    /**
     * @notice Calculates the interest rate for a specific market, tranche, total deposit, and total borrow.
     * @param _market The address of the market
     * @param _tranche The tranche number
     * @param _totalDeposit The total amount deposited in the market
     * @param _totalBorrow The total amount borrowed from the market
     * @return The calculated interest rate
     */

    function getInterestRate(address _market, uint8 _tranche, uint256 _totalDeposit, uint256 _totalBorrow)
        external
        view
        returns (uint256);

    /**
     * @notice Sets the IRM configuration for a specific market and tranches.
     * @param _market The address of the market
     * @param _tranches An array of tranche numbers
     * @param _configs An array of IRMConfig structures
     */
    function setIRMForMarket(address _market, uint8[] calldata _tranches, IRMConfig[] calldata _configs) external;
}


================================================
FILE: src/08-omni-protocol/interfaces/IOmniOracle.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

/**
 * @title IOmniOracle Interface
 * @notice Interface for the OmniOracle contract.
 */
interface IOmniOracle {
    /// Events
    event SetOracle(
        address indexed underlying,
        address indexed oracle,
        Provider provider,
        uint32 delay,
        uint32 delayQuote,
        uint8 underlyingDecimals
    );
    event RemoveOracle(address indexed underlying);

    /// Structs
    enum Provider {
        Invalid,
        Band,
        Chainlink,
        Other // Must implement the ICustomOmniOracle interface, use very carefully should return 1 full unit price multiplied by 1e18
    }

    struct OracleConfig {
        // One storage slot
        address oracleAddress; // 160 bits
        Provider provider; // 8 bits
        uint32 delay; // 32 bits, because this is time-based in unix
        uint32 delayQuote; // 32 bits, for Band quote delay
        uint8 underlyingDecimals; // 8 bits, decimals of underlying token
    }

    /**
     * @notice Fetches the price of the specified asset.
     * @param _underlying The address of the asset.
     * @return The price of the asset, normalized to 1e18.
     */
    function getPrice(address _underlying) external view returns (uint256);
}


================================================
FILE: src/08-omni-protocol/interfaces/IOmniPool.sol
================================================
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.23;

/**
 * @title IOmniPool Interface
 * @dev This interface outlines the functions available in the OmniPool contract.
 */
interface IOmniPool {
    /// Events
    event ClearedMarkets(bytes32 indexed account);
    event EnteredIsolatedMarket(bytes32 indexed account, address market);
    event EnteredMarkets(bytes32 indexed account, address[] markets);
    event EnteredMode(bytes32 indexed account, uint256 modeId);
    event ExitedMarket(bytes32 indexed account, address market);
    event ExitedMode(bytes32 indexed account);
    event Liquidated(
        address indexed liquidator,
        bytes32 indexed targetAccount,
        bytes32 liquidatorAccount,
        address liquidateMarket,
        address collateralMarket,
        uint256 amount
    );
    event PausedTranche(uint8 trancheId);
    event UnpausedTranche();
    event SetMarketConfiguration(address indexed market, MarketConfiguration marketConfig);
    event RemovedMarketConfiguration(address indexed market);
    event SetModeConfiguration(uint256 indexed modeId, ModeConfiguration modeConfig);
    event SocializedLoss(address indexed market, uint8 trancheId, bytes32 account);

    // Structs
    /**
     * @dev Structure to hold market configuration data.
     */
    struct MarketConfiguration {
        uint32 collateralFactor;
        uint32 borrowFactor; // Set to 0 if not borrowable.
        uint32 expirationTimestamp;
        uint8 riskTranche;
        bool isIsolatedCollateral; // If this is false, riskTranche must be 0
    }

    /**
     * @dev Structure to hold mode configuration data.
     */
    struct ModeConfiguration {
        uint32 collateralFactor;
        uint32 borrowFactor;
        uint8 modeTranche;
        uint32 expirationTimestamp; // Only prevents people from entering a mode, does not affect users already in existing mode
        address[] markets;
    }

    /**
     * @dev Structure to hold account specific data.
     */
    struct AccountInfo {
        uint8 modeId;
        address isolatedCollateralMarket;
        uint32 softThreshold;
    }

    /**
     * @dev Structure to hold evaluation data for an account.
     */
    struct Evaluation {
        uint256 depositTrueValue;
        uint256 borrowTrueValue;
        uint256 depositAdjValue;
        uint256 borrowAdjValue;
        uint64 numDeposit; // To combine into 1 storage slot
        uint64 numBorrow;
        bool isExpired;
    }

    /**
     * @dev Structure to hold liquidation bonus configuration data.
     */
    struct LiquidationBonusConfiguration {
        uint64 start; // 1e9 precision
        uint64 end; // 1e9 precision
        uint64 kink; // 1e9 precision
        uint32 expiredBonus; // 1e9 precision
        uint32 softThreshold; // 1e9 precision
    }

    /**
     * @dev Structure to hold liquidation arguments.
     */
    struct LiquidationParams {
        bytes32 targetAccountId; // The unique identifier of the target account to be liquidated.
        bytes32 liquidatorAccountId; // The unique identifier of the account initiating the liquidation.
        address liquidateMarket; // The address of the market from which to repay the borrow.
        address collateralMarket; // The address of the market from which to seize collateral.
        uint256 amount; // The amount of the target account's borrow balance to repay. If _amount is 0, liquidator will repay the entire borrow balance, and will error if the repayment is too large.
    }

    // Function Signatures
    /**
     * @dev Returns the address of the oracle contract.
     * @return The address of the oracle.
     */
    function oracle() external view returns (address);

    /**
     * @dev Returns the pause tranche value.
     * @return The pause tranche value.
     */
    function pauseTranche() external view returns (uint8);

    /**
     * @dev Returns the reserve receiver.
     * @return The reserve receiver identifier.
     */
    function reserveReceiver() external view returns (bytes32);

    /**
     * @dev Allows a user to enter an isolated market, the market configuration must be for isolated collateral.
     * @param _subId The identifier of the sub-account.
     * @param _isolatedMarket The address of the isolated market to enter.
     */
    function enterIsolatedMarket(uint96 _subId, address _isolatedMarket) external;

    /**
     * @dev Allows a user to enter multiple unique markets, none of them are isolated collateral markets.
     * @param _subId The identifier of the sub-account.
     * @param _markets The addresses of the markets to enter.
     */
    function enterMarkets(uint96 _subId, address[] calldata _markets) external;

    /**
     * @dev Allows a user to exit a single market including their isolated market. There must be no borrows active on the subaccount to exit a market.
     * @param _subId The identifier of the sub-account.
     * @param _market The addresses of the markets to exit.
     */
    function exitMarket(uint96 _subId, address _market) external;

    /**
     * @dev Clears all markets for a user. The subaccount must have no active borrows to clear markets.
     * @param _subId The identifier of the sub-account.
     */
    function clearMarkets(uint96 _subId) external;

    /**
     * @dev Sets a mode for a sub-account.
     * @param _subId The identifier of the sub-account.
     * @param _modeId The identifier of the mode to enter.
     */
    function enterMode(uint96 _subId, uint8 _modeId) external;

    /**
     * @dev Exits the mode currently set for a sub-account.
     * @param _subId The identifier of the sub-account.
     */
    function exitMode(uint96 _subId) external;

    /**
     * @dev Evaluates an account's financial metrics.
     * @param _accountId The identifier of the account.
     * @return eval A struct containing the evaluated metrics of the account.
     */
    function evaluateAccount(bytes32 _accountId) external returns (Evaluation memory eval);

    /**
     * @dev Allows a sub-account to borrow assets from a specified market.
     * @param _subId The identifier of the sub-account.
     * @param _market The address of the market to borrow from.
     * @param _amount The amount of assets to borrow.
     */
    function borrow(uint96 _subId, address _market, uint256 _amount) external;

    /**
     * @dev Allows a sub-account to repay borrowed assets to a specified market.
     * @param _subId The identifier of the sub-account.
     * @param _market The address of the market to repay to.
     * @param _amount The amount of assets to repay.
     */
    function repay(uint96 _subId, address _market, uint256 _amount) external;

    /**
     * @dev Initiates a liquidation process to recover assets from an under-collateralized account.
     * @param _params The liquidation parameters.
     * @return seizedShares The amount of shares seized from the liquidated account.
     */
    function liquidate(LiquidationParams calldata _params) external returns (uint256[] memory seizedShares);

    /**
     * @dev Distributes loss incurred in a market to a specified tranche of accounts.
     * @param _market The address of the market where the loss occurred.
     * @param _account The account identifier to record the loss.
     */
    function socializeLoss(address _market, bytes32 _account) external;

    /**
     * @dev Retrieves the borrow tier of an account.
     * @param _account The account info struct containing the account's details.
     * @return The borrowing tier of the account.
     */
    function getAccountBorrowTier(AccountInfo memory _account) external view returns (uint8);

    /**
     * @dev Retrieves the market addresses associated with an account.
     * @param _accountId The identifier of the account.
     * @param _account The account info struct containing the account's details.
     * @return A list of market addresses associated with the account.
     */
    function getAccountPoolMarkets(bytes32 _accountId, AccountInfo memory _account)
        external
        view
        returns (address[] memory);

    /**
     * @dev Retrieves the liquidation bonus and soft threshold values for a market.
     * @param _depositAdjValue The adjusted value of deposits in the market.
     * @param _borrowAdjValue The adjusted value of borrows in the market.
     * @param _collateralMarket The address of the collateral market.
     * @return bonus The liquidation bonus value.
     * @return softThreshold The soft liquidation threshold value.
     */
    function getLiquidationBonusAndThreshold(
        uint256 _depositAdjValue,
        uint256 _borrowAdjValue,
        address _collateralMarket
    ) external view returns (uint256 bonus, uint256 softThreshold);

    /**
     * @dev Checks if an account is healthy based on its financial metrics.
     * @param _accountId The identifier of the account.
     * @return A boolean indicating whether the account is healthy.
     */
    function isAccountHealthy(bytes32 _accountId) external returns (bool);

    /**
     * @dev Resets the pause tranche to its initial state.
     */
    function resetPauseTranche() external;

    /**
     * @dev Updates the market configuration.
     * @param _market The address of the market.
     * @param _marketConfig The market configuration data.
     */
    function setMarketConfiguration(address _market, MarketConfiguration calldata _marketConfig) external;

    /**
     * @dev Updates mode configurations one at a time.
     * @param _modeConfiguration An single mode configuration.
     */
    function setModeConfiguration(ModeConfiguration calldata _modeConfiguration) external;

    /**
     * @dev Updates the soft liquidation threshold for an account.
     * @param _accountId The account identifier.
     * @param _softThreshold The soft liquidation threshold value.
     */
    function setAccountSoftLiquidation(bytes32 _accountId, uint32 _softThreshold) external;

    /**
     * @dev Updates the liquidation bonus configuration for a market.
     * @param _market The address of the market.
     * @param _config The liquidation bonus configuration data.
     */
    function setLiquidationBonusConfiguration(address _market, LiquidationBonusConfiguration calldata _config)
        external;

    /**
     * @notice Sets the tranche count for a specific market.
     * @dev This function allows to set the number of tranches for a given market.
     * It's an external function that can only be called by an account with the `MARKET_CONFIGURATOR_ROLE`.
     * @param _market The address of the market contract.
     * @param _trancheCount The number of tranches to be set for the market.
     */
    function setTrancheCount(address _market, uint8 _trancheCount) external;

    /**
     * @dev This function can only be called by an account with the MARKET_CONFIGURATOR_ROLE.
     * It invokes the setTrancheBorrowCaps function of the IOmniToken contract associated with the specified market.
     * @param _market The address of the market for which to set the borrow caps.
     * @param _borrowCaps An array of borrow cap values, one for each tranche of the market.
     */
    function setBorrowCap(address _market, uint256[] calldata _borrowCaps) external;

    /**
     * @dev This function can only be called by an account with the MARKET_CONFIGURATOR_ROLE.
     * It invokes the setSupplyCap function of the IOmniTokenNoBorrow contract associated with the specified market.
     * @param _market The address of the market for which to set the no-borrow supply cap.
     * @param _noBorrowSupplyCap The value of the no-borrow supply cap to set.
     */
    function setNoBorrowSupplyCap(address _market, uint256 _noBorrowSupplyCap) external;

    /**
     * @notice Sets the reserve receiver's address. This function can only be called by an account with the DEFAULT_ADMIN_ROLE.
     * @dev The reserve receiver's address is converted to a bytes32 account identifier using the toAccount function with a subId of 0.
     * @param _reserveReceiver The address of the reserve receiver to be set.
     */
    function setReserveReceiver(address _reserveReceiver) external;
}


================================================
FILE: src/08-omni-protocol/interfaces/IOmniToken.sol
================================================
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.23;

import "./IOmniTokenBase.sol";

/**
 * @title IOmniToken
 * @notice Interface for the OmniToken contract which manages deposits, withdrawals, borrowings, and repayments within the Omni protocol.
 */
interface IOmniToken is IOmniTokenBase {
    /// Events
    event Accrue();
    event Deposit(bytes32 indexed account, uint8 indexed trancheId, uint256 amount, uint256 share);
    event Withdraw(bytes32 indexed account, uint8 indexed trancheId, uint256 amount, uint256 share);
    event Borrow(bytes32 indexed account, uint8 indexed trancheId, uint256 amount, uint256 share);
    event Repay(bytes32 indexed account, address indexed payer, uint8 indexed trancheId, uint256 amount, uint256 share);
    event Seize(bytes32 indexed account, bytes32 indexed to, uint256 amount, uint256[] seizedShares);
    event SetTrancheCount(uint8 trancheCount);
    event SetTrancheBorrowCaps(uint256[] borrowCaps);
    event SocializedLoss(bytes32 indexed account, uint8 indexed trancheId, uint256 amount, uint256 share);
    event Transfer(bytes32 indexed from, bytes32 indexed to, uint8 indexed trancheId, uint256 share);

    /**
     * @notice Gets the address of the OmniPool contract.
     * @return The address of the OmniPool contract.
     */
    function omniPool() external view returns (address);

    /**
     * @notice Gets the address of the Interest Rate Model (IRM) contract.
     * @return The address of the IRM contract.
     */
    function irm() external view returns (address);

    /**
     * @notice Gets the last accrual time.
     * @return The timestamp of the last accrual time.
     */
    function lastAccrualTime() external view returns (uint256);

    /**
     * @notice Gets the count of tranches.
     * @return The total number of tranches.
     */
    function trancheCount() external view returns (uint8);

    /**
     * @notice Gets the reserve receiver.
     * @return The bytes32 identifier of the reserve receiver.
     */
    function reserveReceiver() external view returns (bytes32);

    /**
     * @notice Gets the borrow cap for a specific tranche.
     * @param _trancheId The ID of the tranche for which to retrieve the borrow cap.
     * @return The borrow cap for the specified tranche.
     */
    function getBorrowCap(uint8 _trancheId) external view returns (uint256);

    /**
     * @notice Accrues interest for all tranches, calculates and distributes the interest among the depositors and updates tranche balances.
     * The function also handles reserve payments. This method needs to be called before any deposit, withdrawal, borrow, or repayment actions to update the state of the contract.
     * @dev Interest is paid out proportionately to more risky tranche deposits per tranche
     */
    function accrue() external;

    /**
     * @notice Deposits a specified amount into a specified tranche.
     * @param _subId Sub-account identifier for the depositor.
     * @param _trancheId Identifier of the tranche to deposit into.
     * @param _amount Amount to deposit.
     * @return share Amount of deposit shares received in exchange for the deposit.
     */
    function deposit(uint96 _subId, uint8 _trancheId, uint256 _amount) external returns (uint256 share);

    /**
     * @notice Withdraws funds from a specified tranche.
     * @param _subId The ID of the sub-account.
     * @param _trancheId The ID of the tranche.
     * @param _share The share of the user in the tranche.
     * @return amount The amount of funds withdrawn.
     */
    function withdraw(uint96 _subId, uint8 _trancheId, uint256 _share) external returns (uint256 amount);

    /**
     * @notice Borrows funds from a specified tranche.
     * @param _account The account of the user.
     * @param _trancheId The ID of the tranche.
     * @param _amount The amount to borrow.
     * @return share The share of the borrowed amount in the tranche.
     */
    function borrow(bytes32 _account, uint8 _trancheId, uint256 _amount) external returns (uint256 share);

    /**
     * @notice Repays borrowed funds.
     * @param _account The account of the user.
     * @param _payer The account that will pay the borrowed amount.
     * @param _trancheId The ID of the tranche.
     * @param _amount The amount to repay.
     * @return amount The amount of the repaid amount in the tranche.
     */
    function repay(bytes32 _account, address _payer, uint8 _trancheId, uint256 _amount)
        external
        returns (uint256 amount);

    /**
     * @notice Transfers specified shares from one account to another within a specified tranche.
     * @param _subId The subscription ID related to the sender's account.
     * @param _to The account identifier to which shares are being transferred.
     * @param _trancheId The identifier of the tranche where the transfer is occurring.
     * @param _shares The amount of shares to transfer.
     * @return A boolean value indicating whether the transfer was successful.
     */
    function transfer(uint96 _subId, bytes32 _to, uint8 _trancheId, uint256 _shares) external returns (bool);

    /**
     * @notice Distributes the bad debt loss in a tranche among all tranche members. This function should only be called by the OmniPool.
     * @param _account The account that incurred a loss.
     * @param _trancheId The ID of the tranche.
     */
    function socializeLoss(bytes32 _account, uint8 _trancheId) external;

    /**
     * @notice Computes the borrowing amount of a specific account in the underlying asset for a given borrow tier.
     * @dev The division is ceiling division.
     * @param _account The account identifier for which the borrowing amount is to be computed.
     * @param _borrowTier The borrow tier identifier from which the borrowing amount is to be computed.
     * @return The borrowing amount of the account in the underlying asset for the given borrow tier.
     */
    function getAccountBorrowInUnderlying(bytes32 _account, uint8 _borrowTier) external view returns (uint256);

    /**
     * @notice Retrieves the deposit and borrow shares for a specific account in a specific tranche.
     * @param _account The account identifier.
     * @param _trancheId The tranche identifier.
     * @return depositShare The deposit share.
     * @return borrowShare The borrow share.
     */
    function getAccountSharesByTranche(bytes32 _account, uint8 _trancheId)
        external
        view
        returns (uint256 depositShare, uint256 borrowShare);

    /**
     * @notice Sets the borrow caps for each tranche.
     * @param _borrowCaps An array of borrow caps in the underlying's decimals.
     */
    function setTrancheBorrowCaps(uint256[] calldata _borrowCaps) external;

    /**
     * @notice Sets the number of tranches.
     * @param _trancheCount The new tranche count.
     */
    function setTrancheCount(uint8 _trancheCount) external;

    /**
     * @notice Fetches and updates the reserve receiver from the OmniPool contract.
     */
    function fetchReserveReceiver() external;
}


================================================
FILE: src/08-omni-protocol/interfaces/IOmniTokenBase.sol
================================================
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.23;

/**
 * @title IOmniTokenBase
 * @notice Base interface shared by the IOmniToken and IOmniTokenNoBorrow interfaces.
 */
interface IOmniTokenBase {
    /**
     * @notice Retrieves the total deposit amount for a specific account.
     * @param _account The account identifier.
     * @return The total deposit amount.
     */
    function getAccountDepositInUnderlying(bytes32 _account) external view returns (uint256);

    /**
     * @notice Calculates the total deposited amount for a specific owner across sub-accounts. This funciton is for wallets and Etherscan to pick up balances.
     * @param _owner The address of the owner.
     * @return The total deposited amount.
     */
    function balanceOf(address _owner) external view returns (uint256);

    /**
     * @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.
     * @param _account The account from which funds will be seized.
     * @param _to The account to which seized funds will be sent.
     * @param _amount The amount of funds to seize.
     * @return The shares seized from each tranche.
     */
    function seize(bytes32 _account, bytes32 _to, uint256 _amount) external returns (uint256[] memory);
}


================================================
FILE: src/08-omni-protocol/interfaces/IOmniTokenNoBorrow.sol
================================================
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.23;

import "./IOmniTokenBase.sol";

/**
 * @title IOmniTokenNoBorrow
 * @notice Interface for the OmniTokenNoBorrow contract which provides deposit and withdrawal features, without borrowing features.
 */
interface IOmniTokenNoBorrow is IOmniTokenBase {
    /// Events
    event Deposit(bytes32 indexed account, uint256 amount);
    event Withdraw(bytes32 indexed account, uint256 amount);
    event Seize(bytes32 indexed account, bytes32 indexed to, uint256 amount, uint256[] seizeShares);
    event SetSupplyCap(uint256 supplyCap);
    event Transfer(bytes32 indexed from, bytes32 indexed to, uint256 amount);

    /**
     * @notice Deposits a specified amount to the account.
     * @param _subId The sub-account identifier.
     * @param _amount The amount to deposit.
     * @return amount The actual amount deposited.
     */
    function deposit(uint96 _subId, uint256 _amount) external returns (uint256 amount);

    /**
     * @notice Withdraws a specified amount from the account.
     * @param _subId The sub-account identifier.
     * @param _amount The amount to withdraw.
     * @return amount The actual amount withdrawn.
     */
    function withdraw(uint96 _subId, uint256 _amount) external returns (uint256 amount);

    /**
     * @notice Transfers a specified amount of tokens from the sender's account to another account.
     * @param _subId The subscription ID associated with the sender's account.
     * @param _to The account identifier to which the tokens are being transferred.
     * @param _amount The amount of tokens to transfer.
     * @return A boolean value indicating whether the transfer was successful.
     */
    function transfer(uint96 _subId, bytes32 _to, uint256 _amount) external returns (bool);

    /**
     * @notice Sets a new supply cap for the contract.
     * @param _supplyCap The new supply cap amount.
     */
    function setSupplyCap(uint256 _supplyCap) external;
}


================================================
FILE: src/08-omni-protocol/interfaces/IWETH9.sol
================================================
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.23;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

/// @title Interface for WETH9
interface IWETH9 is IERC20 {
    /// @notice Deposit ether to get wrapped ether
    function deposit() external payable;

    /// @notice Withdraw wrapped ether to get ether
    function withdraw(uint256) external;
}


================================================
FILE: src/08-omni-protocol/interfaces/IWithUnderlying.sol
================================================
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.23;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

/**
 * @title IWithUnderlying
 * @notice Interface for the WithUnderlying contract to handle the inflow and outflow of ERC20 tokens.
 */
interface IWithUnderlying {
    /**
     * @notice Gets the address of the underlying ERC20 token.
     * @return The address of the underlying ERC20 token.
     */
    function underlying() external view returns (address);
}


================================================
FILE: src/08-omni-protocol/oracles/WstETHCustomOracle.sol
================================================
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.23;

import "../interfaces/ICustomOmniOracle.sol";
import "../interfaces/IChainlinkAggregator.sol";


interface ILidoETH {
    function getPooledEthByShares(uint256 _sharesAmount) external view returns (uint256);
}

contract WstETHCustomOracle is ICustomOmniOracle {
    address public immutable stETH;
    address public immutable wstETH;
    address public immutable chainlinkStETHUSD;
    uint256 private constant MAX_DELAY = 1 days;

    /**
     * @notice Constructor for the WstETHCustomOracle
     * @param _stETH The address of the stETH contract.
     * @param _wstETH The address of the wstETH contract. 
     * @param _chainlinkStETHUSD The address of the Chainlink aggregator contract.
     */
    constructor(address _stETH, address _wstETH, address _chainlinkStETHUSD) {
        stETH = _stETH;
        wstETH = _wstETH;
        chainlinkStETHUSD = _chainlinkStETHUSD;
    }

    /**
     * @notice Fetches the price of the specified asset.
     * @param _underlying The address of the asset.
     * @return The price of the asset, normalized to 1e18.
     */
    function getPrice(address _underlying) external view returns (uint256) {
        require(_underlying == wstETH, "Invalid address for oracle");
        (, int256 stETHPrice,,uint256 updatedAt,) = IChainlinkAggregator(chainlinkStETHUSD).latestRoundData();
        if (stETHPrice <= 0) return 0;
        require(updatedAt >= block.timestamp - MAX_DELAY, "Stale price for stETH");

        uint256 stEthPerWstETH = ILidoETH(stETH).getPooledEthByShares(1e18);

        return (stEthPerWstETH * uint256(stETHPrice)) / (10 ** IChainlinkAggregator(chainlinkStETHUSD).decimals());
    }
}


================================================
FILE: src/09-vesting/Vesting.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

contract Vesting {
    uint24 public constant TOTAL_POINTS_PCT = 100_000;

    struct AllocationInput {
        address recipient;
        uint24 points;
        uint8  vestingWeeks;
    }

    struct AllocationData {
        uint24 points;
        uint8  vestingWeeks;
        bool   claimed;
    }

    mapping(address recipient => AllocationData data) public allocations;

    constructor(AllocationInput[] memory allocInput) {
        uint256 inputLength = allocInput.length;
        require(inputLength > 0, "No allocations");

        uint24 totalPoints;
        for(uint256 i; i<inputLength; i++) {
            require(allocInput[i].points != 0, "Zero points invalid");
            require(allocations[allocInput[i].recipient].points == 0, "Already set");

            totalPoints += allocInput[i].points;
            require(totalPoints <= TOTAL_POINTS_PCT, "Too many points");

            allocations[allocInput[i].recipient].points = allocInput[i].points;
            allocations[allocInput[i].recipient].vestingWeeks = allocInput[i].vestingWeeks;
        }
        
        require(totalPoints == TOTAL_POINTS_PCT, "Not enough points");
    }

    // users entitled to an allocation can transfer their points to
    // another address if they haven't claimed
    function transferPoints(address to, uint24 points) external {
        require(points != 0, "Zero points invalid");

        AllocationData memory fromAllocation = allocations[msg.sender];
        require(fromAllocation.points >= points, "Insufficient points");
        require(!fromAllocation.claimed, "Already claimed");

        AllocationData memory toAllocation = allocations[to];
        require(!toAllocation.claimed, "Already claimed");

        // enforce identical vesting periods if `to` has an active vesting period
        if(toAllocation.vestingWeeks != 0) {
            require(fromAllocation.vestingWeeks == toAllocation.vestingWeeks, "Vesting mismatch");
        }

        allocations[msg.sender].points = fromAllocation.points - points;
        allocations[to].points = toAllocation.points + points;

        // if `to` had no active vesting period, copy from `from`
        if (toAllocation.vestingWeeks == 0) {
            allocations[to].vestingWeeks = fromAllocation.vestingWeeks;
        }
    }
}

================================================
FILE: src/10-vesting-ext/VestingExt.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

contract VestingExt {
    uint24  public  constant TOTAL_POINTS_PCT   = 100_000;
    uint256 public  constant TOTAL_PRECLAIM_PCT = 100;
    uint256 public  constant MAX_PRECLAIM_PCT   =  10;
    uint96  public  constant TOTAL_TOKEN_ALLOCATION = 1_000_000e18;

    struct AllocationInput {
        address recipient;
        uint24  points;
        uint8   vestingWeeks;
    }

    struct AllocationData {
        uint24  points;
        uint8   vestingWeeks;
        bool    claimed;
        uint96  preclaimed;
    }

    mapping(address recipient => AllocationData data) public allocations;

    constructor(AllocationInput[] memory allocInput) {
        uint256 inputLength = allocInput.length;
        require(inputLength > 0, "No allocations");

        uint24 totalPoints;
        for(uint256 i; i<inputLength; i++) {
            require(allocInput[i].points != 0, "Zero points invalid");
            require(allocations[allocInput[i].recipient].points == 0, "Already set");

            totalPoints += allocInput[i].points;
            require(totalPoints <= TOTAL_POINTS_PCT, "Too many points");

            allocations[allocInput[i].recipient].points = allocInput[i].points;
            allocations[allocInput[i].recipient].vestingWeeks = allocInput[i].vestingWeeks;
        }
        
        require(totalPoints == TOTAL_POINTS_PCT, "Not enough points");
    }

    // users entitled to an allocation can transfer their points to
    // another address if they haven't claimed
    function transferPoints(address to, uint24 points) external {
        require(msg.sender != to, "Self transfer invalid");
        require(points != 0, "Zero points invalid");

        AllocationData memory fromAllocation = allocations[msg.sender];
        require(fromAllocation.points >= points, "Insufficient points");
        require(!fromAllocation.claimed, "Already claimed");

        AllocationData memory toAllocation = allocations[to];
        require(!toAllocation.claimed, "Already claimed");

        // enforce identical vesting periods if `to` has an active vesting period
        if(toAllocation.vestingWeeks != 0) {
            require(fromAllocation.vestingWeeks == toAllocation.vestingWeeks, "Vesting mismatch");
        }

        allocations[msg.sender].points = fromAllocation.points - points;
        allocations[to].points = toAllocation.points + points;

        // if `to` had no active vesting period, copy from `from`
        if (toAllocation.vestingWeeks == 0) {
            allocations[to].vestingWeeks = fromAllocation.vestingWeeks;
        }
    }

    // calculates how many tokens user is entitled to based on their points
    function getUserTokenAllocation(uint24 points) public pure returns(uint96 allocatedTokens) {
        allocatedTokens = (points * TOTAL_TOKEN_ALLOCATION) / TOTAL_POINTS_PCT;
    }

    // calculates max preclaimable token amount given a user's total allocated tokens
    function getUserMaxPreclaimable(uint96 allocatedTokens) public pure returns(uint96 maxPreclaimable) {
        // unsafe cast OK here
        maxPreclaimable
            = uint96(MAX_PRECLAIM_PCT * allocatedTokens/ TOTAL_PRECLAIM_PCT);
    }

    // allows users to preclaim part of their token allocation
    function preclaim() external returns(uint96 userPreclaimAmount) {
        AllocationData memory userAllocation = allocations[msg.sender];

        require(!userAllocation.claimed, "Already claimed");
        require(userAllocation.preclaimed == 0, "Already preclaimed");

        userPreclaimAmount = getUserMaxPreclaimable(getUserTokenAllocation(userAllocation.points));
        require(userPreclaimAmount > 0, "Zero preclaim amount");

        allocations[msg.sender].preclaimed = userPreclaimAmount;
    }
}

================================================
FILE: src/11-op-reg/OperatorRegistry.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

contract OperatorRegistry {
    uint128 public numOperators;

    mapping(uint128 operatorId      => address operatorAddress) public operatorIdToAddress;
    mapping(address operatorAddress => uint128 operatorId) public operatorAddressToId;

    // anyone can register their address as an operator
    function register() external returns(uint128 newOperatorId) {
        require(operatorAddressToId[msg.sender] == 0, "Address already registered");

        newOperatorId = ++numOperators;

        operatorAddressToId[msg.sender] = newOperatorId;
        operatorIdToAddress[newOperatorId] = msg.sender;
    }

    // an operator can update their address
    function updateAddress(address newOperatorAddress) external {
        require(msg.sender != newOperatorAddress, "Updated address must be different");

        uint128 operatorId = _getOperatorIdSafe(msg.sender);

        operatorAddressToId[newOperatorAddress] = operatorId;
        operatorIdToAddress[operatorId] = newOperatorAddress;

        delete operatorAddressToId[msg.sender];
    }

    function _getOperatorIdSafe(address operatorAddress) internal view returns (uint128 operatorId) {
        operatorId = operatorAddressToId[operatorAddress];

        require(operatorId != 0, "Operator not registered");
    }
}


================================================
FILE: src/12-liquidate-dos/LiquidateDos.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";

interface ILiquidateDos {
    error InvalidMarketId();
    error UserAlreadyInMarket();
    error LiquidationsDisabled();
    error LiquidateUserNotInAnyMarkets();
}

contract LiquidateDos is ILiquidateDos {
    using EnumerableSet for EnumerableSet.UintSet;

    // 10 possible markets for users to trade in
    uint8 public constant MIN_MARKET_ID = 1;
    uint8 public constant MAX_MARKET_ID = 10;

    bool liquidationsEnabled;

    // tracks open markets for each user
    mapping(address user => EnumerableSet.UintSet activeMarkets) userActiveMarkets;

    // users can only have 1 open position in each market
    function openPosition(uint8 marketId) external {
        if(marketId < MIN_MARKET_ID || marketId > MAX_MARKET_ID) revert InvalidMarketId();

        if(!userActiveMarkets[msg.sender].add(marketId)) revert UserAlreadyInMarket();
    }

    function toggleLiquidations(bool toggle) external {
        liquidationsEnabled = toggle;
    }

    function liquidate(address user) external {
        if(!liquidationsEnabled) revert LiquidationsDisabled();

        uint8 userActiveMarketsNum = uint8(userActiveMarkets[user].length());
        if(userActiveMarketsNum == 0) revert LiquidateUserNotInAnyMarkets();

        // in our simple implementation users are always liquidated
        for(uint8 i; i<userActiveMarketsNum; i++) {
            uint8 marketId = uint8(userActiveMarkets[user].at(i));
            userActiveMarkets[user].remove(marketId);
        }
    }

    function userActiveInMarket(address user, uint8 marketId) external view returns(bool isActive) {
        isActive = userActiveMarkets[user].contains(marketId);
    }
}


================================================
FILE: src/13-stability-pool/StabilityPool.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";

// A simplified version of Prisma Finance's StabilityPool where users
// can deposit `debtToken` to receive a share of `collateralToken`
// rewards from liquidations
//
// Challenge: write an invariant to test the solvency of the pool;
// can the pool reach a state where it owes more `collateralToken` to
// depositors than it has available?
contract StabilityPool {
    using SafeERC20 for IERC20;

    uint256 constant DECIMAL_PRECISION = 1e18;
    uint256 constant SCALE_FACTOR = 1e9;
    uint256 constant REWARD_DURATION = 1 weeks;
    uint8 constant COLLATERAL_DECIMALS = 18;

    IERC20 public immutable debtToken;
    IERC20 public immutable collateralToken;

    uint128 public currentScale;
    uint128 public currentEpoch;
    uint256 public lastBabelError;
    uint256 public lastCollateralError_Offset;
    uint256 public lastDebtLossError_Offset;
    uint256 public P = DECIMAL_PRECISION;
    uint256 public totalDebtTokenDeposits;

    // mappings
    mapping(address depositor => AccountDeposit) public accountDeposits;
    mapping(address depositor => Snapshots) public depositSnapshots;
    mapping(address depositor => uint256 deposits) public depositSums;
    mapping(address depositor => uint80 gains) public collateralGainsByDepositor;
    mapping(uint128 epoch => mapping(uint128 scale => uint256 sumS)) public epochToScaleToSums;

    // structs
    struct AccountDeposit {
        uint128 amount;
        uint128 timestamp; // timestamp of the last deposit
    }
    struct Snapshots {
        uint256 P;
        uint128 scale;
        uint128 epoch;
    }

    constructor(IERC20 _debtTokenAddress, IERC20 _collateralToken) {
        debtToken = _debtTokenAddress;
        collateralToken = _collateralToken;
    }

    // provides collateral tokens to the stability pool
    function provideToSP(uint256 _amount) external {
        require(_amount > 0, "StabilityPool: Amount must be non-zero");

        _accrueDepositorCollateralGain(msg.sender);

        uint256 compoundedDebtDeposit = getCompoundedDebtDeposit(msg.sender);

        debtToken.safeTransferFrom(msg.sender, address(this), _amount);

        uint256 newTotalDebtTokenDeposits = totalDebtTokenDeposits + _amount;
        totalDebtTokenDeposits = newTotalDebtTokenDeposits;

        uint256 newTotalDeposited = compoundedDebtDeposit + _amount;

        accountDeposits[msg.sender] = AccountDeposit({
            amount: SafeCast.toUint128(newTotalDeposited),
            timestamp: uint128(block.timestamp)
        });

        _updateSnapshots(msg.sender, newTotalDeposited);
    }

    function registerLiquidation(uint256 _debtToOffset, uint256 _collToAdd) external {
        uint256 totalDebt = totalDebtTokenDeposits;
        if (totalDebt == 0 || _debtToOffset == 0) {
            return;
        }

        (uint256 collateralGainPerUnitStaked, uint256 debtLossPerUnitStaked) = _computeRewardsPerUnitStaked(
            _collToAdd,
            _debtToOffset,
            totalDebt
        );

        _updateRewardSumAndProduct(collateralGainPerUnitStaked, debtLossPerUnitStaked);

        _decreaseDebt(_debtToOffset);
    }

    function _computeRewardsPerUnitStaked(
        uint256 _collToAdd,
        uint256 _debtToOffset,
        uint256 _totalDebtTokenDeposits
    ) internal returns (uint256 collateralGainPerUnitStaked, uint256 debtLossPerUnitStaked) {
        /*
         * Compute the Debt and collateral rewards. Uses a "feedback" error correction, to keep
         * the cumulative error in the P and S state variables low:
         *
         * 1) Form numerators which compensate for the floor division errors that occurred the last time this
         * function was called.
         * 2) Calculate "per-unit-staked" ratios.
         * 3) Multiply each ratio back by its denominator, to reveal the current floor division error.
         * 4) Store these errors for use in the next correction when this function is called.
         * 5) Note: static analysis tools complain about this "division before multiplication", however, it is intended.
         */
        uint256 collateralNumerator = (_collToAdd * DECIMAL_PRECISION) + lastCollateralError_Offset;

        if (_debtToOffset == _totalDebtTokenDeposits) {
            debtLossPerUnitStaked = DECIMAL_PRECISION; // When the Pool depletes to 0, so does each deposit
            lastDebtLossError_Offset = 0;
        } else {
            uint256 debtLossNumerator = (_debtToOffset * DECIMAL_PRECISION) - lastDebtLossError_Offset;
            /*
             * Add 1 to make error in quotient positive. We want "slightly too much" Debt loss,
             * which ensures the error in any given compoundedDebtDeposit favors the Stability Pool.
             */
            debtLossPerUnitStaked = (debtLossNumerator / _totalDebtTokenDeposits) + 1;
            lastDebtLossError_Offset = (debtLossPerUnitStaked * _totalDebtTokenDeposits) - debtLossNumerator;
        }

        collateralGainPerUnitStaked = collateralNumerator / _totalDebtTokenDeposits;
       lastCollateralError_Offset = collateralNumerator - (collateralGainPerUnitStaked * _totalDebtTokenDeposits);
    }

    // Update the Stability Pool reward sum S and product P
    function _updateRewardSumAndProduct(
        uint256 _collateralGainPerUnitStaked,
        uint256 _debtLossPerUnitStaked
    ) internal {
        uint256 currentP = P;
        uint256 newP;

        /*
         * The newProductFactor is the factor by which to change all deposits, due to the depletion of Stability Pool Debt in the liquidation.
         * We make the product factor 0 if there was a pool-emptying. Otherwise, it is (1 - DebtLossPerUnitStaked)
         */
        uint256 newProductFactor = DECIMAL_PRECISION - _debtLossPerUnitStaked;

        uint128 currentScaleCached = currentScale;
        uint128 currentEpochCached = currentEpoch;
        uint256 currentS = epochToScaleToSums[currentEpochCached][currentScaleCached];

        /*
         * Calculate the new S first, before we update P.
         * The collateral gain for any given depositor from a liquidation depends on the value of their deposit
         * (and the value of totalDeposits) prior to the Stability being depleted by the debt in the liquidation.
         *
         * Since S corresponds to collateral gain, and P to deposit loss, we update S first.
         */
        uint256 marginalCollateralGain = _collateralGainPerUnitStaked * currentP;
        uint256 newS = currentS + marginalCollateralGain;
        epochToScaleToSums[currentEpochCached][currentScaleCached] = newS;

        // If the Stability Pool was emptied, increment the epoch, and reset the scale and product P
        if (newProductFactor == 0) {
            currentEpoch = currentEpochCached + 1;
            currentScale = 0;
            newP = DECIMAL_PRECISION;

            // If multiplying P by a non-zero product factor would reduce P below the scale boundary, increment the scale
        } else if ((currentP * newProductFactor) / DECIMAL_PRECISION < SCALE_FACTOR) {
            newP = (currentP * newProductFactor * SCALE_FACTOR) / DECIMAL_PRECISION;
            currentScale = currentScaleCached + 1;
        } else {
            newP = (currentP * newProductFactor) / DECIMAL_PRECISION;
        }

        require(newP > 0, "NewP");
        P = newP;
    }

    function _decreaseDebt(uint256 _amount) internal {
        uint256 newTotalDebtTokenDeposits = totalDebtTokenDeposits - _amount;
        totalDebtTokenDeposits = newTotalDebtTokenDeposits;
    }

    // --- Reward calculator functions for depositor and front end ---

    /* Calculates the collateral gain earned by the deposit since its last snapshots were taken.
     * Given
Download .txt
gitextract_67c2mjes/

├── .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
Condensed preview — 156 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (459K chars).
[
  {
    "path": ".github/workflows/test.yml",
    "chars": 586,
    "preview": "name: test\n\non: workflow_dispatch\n\nenv:\n  FOUNDRY_PROFILE: ci\n\njobs:\n  check:\n    strategy:\n      fail-fast: true\n\n    n"
  },
  {
    "path": ".gitignore",
    "chars": 264,
    "preview": "# Compiler files\ncache/\nout/\ncrytic-export/\n\n# Ignores development broadcast logs\n!/broadcast\n/broadcast/*/31337/\n/broad"
  },
  {
    "path": ".gitmodules",
    "chars": 500,
    "preview": "[submodule \"lib/forge-std\"]\n\tpath = lib/forge-std\n\turl = https://github.com/foundry-rs/forge-std\n[submodule \"lib/openzep"
  },
  {
    "path": "LICENSE",
    "chars": 1063,
    "preview": "MIT License\n\nCopyright (c) 2023 Dacian\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof "
  },
  {
    "path": "README.md",
    "chars": 4908,
    "preview": "# Solidity Fuzzing Challenge: Foundry vs Echidna vs Medusa (plus Halmos & Certora) #\n\nA comparison of solidity fuzzing t"
  },
  {
    "path": "foundry.toml",
    "chars": 436,
    "preview": "[profile.default]\nsrc = \"src\"\nout = \"out\"\nlibs = [\"lib\"]\nshow_progress = true\n\n# include remappings\nremappings = [\n    \""
  },
  {
    "path": "src/01-naive-receiver/FlashLoanReceiver.sol",
    "chars": 1032,
    "preview": "// SPDX-License-Identifier: MIT\n\npragma solidity ^0.8.23;\n\nimport \"@openzeppelin/contracts/utils/Address.sol\";\n\n/**\n * @"
  },
  {
    "path": "src/01-naive-receiver/NaiveReceiverLenderPool.sol",
    "chars": 1311,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport \"@openzeppelin/contracts/utils/ReentrancyGuard.sol\";\nim"
  },
  {
    "path": "src/02-unstoppable/ReceiverUnstoppable.sol",
    "chars": 1010,
    "preview": "// SPDX-License-Identifier: MIT\n\npragma solidity ^0.8.23;\n\nimport \"./UnstoppableLender.sol\";\nimport \"@openzeppelin/contr"
  },
  {
    "path": "src/02-unstoppable/UnstoppableLender.sol",
    "chars": 1765,
    "preview": "// SPDX-License-Identifier: MIT\n\npragma solidity ^0.8.23;\n\nimport \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimpo"
  },
  {
    "path": "src/03-proposal/Proposal.sol",
    "chars": 9809,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport \"@openzeppelin/contracts/utils/math/Math.sol\";\n\n//\n// T"
  },
  {
    "path": "src/04-voting-nft/VotingNft.sol",
    "chars": 9991,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport \"@openzeppelin/contracts/token/ERC721/extensions/ERC721"
  },
  {
    "path": "src/04-voting-nft/VotingNftForFuzz.sol",
    "chars": 11557,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport \"@openzeppelin/contracts/token/ERC721/ERC721.sol\";\nimpo"
  },
  {
    "path": "src/05-token-sale/TokenSale.sol",
    "chars": 10194,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport"
  },
  {
    "path": "src/06-rarely-false/RarelyFalse.sol",
    "chars": 163,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n//\n//\n// stateless tests in test/06-rarely-false\n//\n// placehol"
  },
  {
    "path": "src/07-byte-battle/ByteBattle.sol",
    "chars": 162,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n//\n//\n// stateless tests in test/07-byte-battle\n//\n// placehold"
  },
  {
    "path": "src/08-omni-protocol/IRM.sol",
    "chars": 4221,
    "preview": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.23;\n\nimport \"@openzeppelin/contracts/access/AccessControl.sol\""
  },
  {
    "path": "src/08-omni-protocol/OmniOracle.sol",
    "chars": 5551,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport \"@openzeppelin/contracts/access/AccessControl.sol\";\nimp"
  },
  {
    "path": "src/08-omni-protocol/OmniPool.sol",
    "chars": 43629,
    "preview": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.23;\n\nimport \"@openzeppelin-upgradeable/contracts/access/Access"
  },
  {
    "path": "src/08-omni-protocol/OmniToken.sol",
    "chars": 23912,
    "preview": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.23;\n\nimport \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\n"
  },
  {
    "path": "src/08-omni-protocol/OmniTokenNoBorrow.sol",
    "chars": 6984,
    "preview": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.23;\n\nimport \"@openzeppelin-upgradeable/contracts/utils/Reentra"
  },
  {
    "path": "src/08-omni-protocol/SubAccount.sol",
    "chars": 1400,
    "preview": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.23;\n\n/**\n * @title SubAccount\n * @notice This library provides"
  },
  {
    "path": "src/08-omni-protocol/WETHGateway.sol",
    "chars": 2401,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport \"@openzeppelin-upgradeable/contracts/proxy/utils/Initia"
  },
  {
    "path": "src/08-omni-protocol/WithUnderlying.sol",
    "chars": 3753,
    "preview": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.23;\n\nimport \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\n"
  },
  {
    "path": "src/08-omni-protocol/interfaces/IBandReference.sol",
    "chars": 918,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\ninterface IStdReference {\n    /// A structure returned wheneve"
  },
  {
    "path": "src/08-omni-protocol/interfaces/IChainlinkAggregator.sol",
    "chars": 630,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\ninterface IChainlinkAggregator {\n    function decimals() exter"
  },
  {
    "path": "src/08-omni-protocol/interfaces/ICustomOmniOracle.sol",
    "chars": 468,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\n/**\n * @title ICustomOmniOracle Interface\n * @notice Interface"
  },
  {
    "path": "src/08-omni-protocol/interfaces/IIRM.sol",
    "chars": 1745,
    "preview": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.23;\n\n/**\n * @title Interest Rate Model (IRM) Interface\n * @not"
  },
  {
    "path": "src/08-omni-protocol/interfaces/IOmniOracle.sol",
    "chars": 1279,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\n/**\n * @title IOmniOracle Interface\n * @notice Interface for t"
  },
  {
    "path": "src/08-omni-protocol/interfaces/IOmniPool.sol",
    "chars": 12187,
    "preview": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.23;\n\n/**\n * @title IOmniPool Interface\n * @dev This interface "
  },
  {
    "path": "src/08-omni-protocol/interfaces/IOmniToken.sol",
    "chars": 7077,
    "preview": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.23;\n\nimport \"./IOmniTokenBase.sol\";\n\n/**\n * @title IOmniToken\n"
  },
  {
    "path": "src/08-omni-protocol/interfaces/IOmniTokenBase.sol",
    "chars": 1356,
    "preview": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.23;\n\n/**\n * @title IOmniTokenBase\n * @notice Base interface sh"
  },
  {
    "path": "src/08-omni-protocol/interfaces/IOmniTokenNoBorrow.sol",
    "chars": 1983,
    "preview": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.23;\n\nimport \"./IOmniTokenBase.sol\";\n\n/**\n * @title IOmniTokenN"
  },
  {
    "path": "src/08-omni-protocol/interfaces/IWETH9.sol",
    "chars": 377,
    "preview": "// SPDX-License-Identifier: GPL-2.0-or-later\npragma solidity ^0.8.23;\n\nimport \"@openzeppelin/contracts/token/ERC20/IERC2"
  },
  {
    "path": "src/08-omni-protocol/interfaces/IWithUnderlying.sol",
    "chars": 484,
    "preview": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.23;\n\nimport \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\n"
  },
  {
    "path": "src/08-omni-protocol/oracles/WstETHCustomOracle.sol",
    "chars": 1724,
    "preview": "// SPDX-License-Identifier: GPL-2.0-or-later\npragma solidity ^0.8.23;\n\nimport \"../interfaces/ICustomOmniOracle.sol\";\nimp"
  },
  {
    "path": "src/09-vesting/Vesting.sol",
    "chars": 2353,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\ncontract Vesting {\n    uint24 public constant TOTAL_POINTS_PCT"
  },
  {
    "path": "src/10-vesting-ext/VestingExt.sol",
    "chars": 3800,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\ncontract VestingExt {\n    uint24  public  constant TOTAL_POINT"
  },
  {
    "path": "src/11-op-reg/OperatorRegistry.sol",
    "chars": 1342,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\ncontract OperatorRegistry {\n    uint128 public numOperators;\n\n"
  },
  {
    "path": "src/12-liquidate-dos/LiquidateDos.sol",
    "chars": 1803,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { EnumerableSet } from \"@openzeppelin/contracts/utils/s"
  },
  {
    "path": "src/13-stability-pool/StabilityPool.sol",
    "chars": 12452,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC"
  },
  {
    "path": "src/14-priority/Priority.sol",
    "chars": 1994,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { EnumerableSet } from \"@openzeppelin/contracts/utils/s"
  },
  {
    "path": "src/MockERC20.sol",
    "chars": 850,
    "preview": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.23;\n\nimport \"@openzeppelin/contracts/access/AccessControl.sol\""
  },
  {
    "path": "src/TestToken.sol",
    "chars": 588,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\n\ncontr"
  },
  {
    "path": "src/TestToken2.sol",
    "chars": 589,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\n\ncontr"
  },
  {
    "path": "test/01-naive-receiver/NaiveReceiverAdvancedEchidna.t.sol",
    "chars": 2045,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport \"./NaiveReceiverBasicEchidna.t.sol\";\n\n// configure solc"
  },
  {
    "path": "test/01-naive-receiver/NaiveReceiverAdvancedEchidna.yaml",
    "chars": 687,
    "preview": "# 1010 ether is placed in the echidna testing contract\n# which then transfers ether to contracts being tested\n# as part "
  },
  {
    "path": "test/01-naive-receiver/NaiveReceiverAdvancedFoundry.t.sol",
    "chars": 2283,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport \"./NaiveReceiverBasicFoundry.t.sol\";\n\n// run from base "
  },
  {
    "path": "test/01-naive-receiver/NaiveReceiverAdvancedMedusa.json",
    "chars": 2477,
    "preview": "{\n\t\"fuzzing\": {\n\t\t\"workers\": 10,\n\t\t\"workerResetLimit\": 50,\n\t\t\"_COMMENT_TESTING_1\": \"changed timeout to limit fuzzing tim"
  },
  {
    "path": "test/01-naive-receiver/NaiveReceiverBasicEchidna.t.sol",
    "chars": 2216,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport \"../../src/01-naive-receiver/NaiveReceiverLenderPool.so"
  },
  {
    "path": "test/01-naive-receiver/NaiveReceiverBasicEchidna.yaml",
    "chars": 617,
    "preview": "# 1010 ether is placed in the echidna testing contract\n# which then transfers ether to contract's being tested\n# as part"
  },
  {
    "path": "test/01-naive-receiver/NaiveReceiverBasicFoundry.t.sol",
    "chars": 2258,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport \"../../src/01-naive-receiver/NaiveReceiverLenderPool.so"
  },
  {
    "path": "test/01-naive-receiver/NaiveReceiverBasicMedusa.json",
    "chars": 2575,
    "preview": "{\n\t\"fuzzing\": {\n\t\t\"workers\": 10,\n\t\t\"workerResetLimit\": 50,\n\t\t\"_COMMENT_TESTING_1\": \"changed timeout to limit fuzzing tim"
  },
  {
    "path": "test/02-unstoppable/UnstoppableBasicEchidna.t.sol",
    "chars": 2102,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport \"../../src/02-unstoppable/UnstoppableLender.sol\";\nimpor"
  },
  {
    "path": "test/02-unstoppable/UnstoppableBasicEchidna.yaml",
    "chars": 549,
    "preview": "# no initial eth required\nbalanceContract: 0\n\n# increase test limit\ntestLimit: 100000\n\n# Allow fuzzer to use public/exte"
  },
  {
    "path": "test/02-unstoppable/UnstoppableBasicFoundry.t.sol",
    "chars": 2262,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport \"../../src/02-unstoppable/UnstoppableLender.sol\";\nimpor"
  },
  {
    "path": "test/02-unstoppable/UnstoppableBasicMedusa.json",
    "chars": 2534,
    "preview": "{\n\t\"fuzzing\": {\n\t\t\"workers\": 10,\n\t\t\"workerResetLimit\": 50,\n\t\t\"_COMMENT_TESTING_1\": \"changed timeout to limit fuzzing tim"
  },
  {
    "path": "test/02-unstoppable/certora.conf",
    "chars": 439,
    "preview": "{\n  \"files\": [\n    \"src/02-unstoppable/ReceiverUnstoppable.sol\",\n    \"src/02-unstoppable/UnstoppableLender.sol\",\n    \"sr"
  },
  {
    "path": "test/02-unstoppable/certora.spec",
    "chars": 1702,
    "preview": "// run from base folder:\n// certoraRun test/02-unstoppable/certora.conf\nusing ReceiverUnstoppable as receiver;\nusing Uns"
  },
  {
    "path": "test/03-proposal/Properties.sol",
    "chars": 1056,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport {Asserts} from \"@chimera/Asserts.sol\";\nimport {Setup} f"
  },
  {
    "path": "test/03-proposal/ProposalCryticTester.sol",
    "chars": 454,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport {Properties} from \"./Properties.sol\";\nimport {CryticAss"
  },
  {
    "path": "test/03-proposal/ProposalCryticTesterToFoundry.sol",
    "chars": 1358,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport {Properties} from \"./Properties.sol\";\nimport {FoundryAs"
  },
  {
    "path": "test/03-proposal/Setup.sol",
    "chars": 1785,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport {Proposal} from \"../../src/03-proposal/Proposal.sol\";\ni"
  },
  {
    "path": "test/03-proposal/certora.conf",
    "chars": 261,
    "preview": "{\n  \"files\": [\n    \"src/03-proposal/Proposal.sol\"\n  ],\n  \"verify\": \"Proposal:test/03-proposal/certora.spec\",\n  \"packages"
  },
  {
    "path": "test/03-proposal/certora.spec",
    "chars": 1773,
    "preview": "// run from base folder:\n// certoraRun test/03-proposal/certora.conf\nmethods {\n    // `envfree` definitions to call func"
  },
  {
    "path": "test/03-proposal/echidna.yaml",
    "chars": 910,
    "preview": "# 10 ether is placed in the echidna testing contract\n# which then transfers ether to contracts being tested\n# as part of"
  },
  {
    "path": "test/03-proposal/medusa.json",
    "chars": 2756,
    "preview": "{\n\t\"fuzzing\": {\n\t\t\"workers\": 10,\n\t\t\"workerResetLimit\": 50,\n\t\t\"_COMMENT_TESTING_1\": \"changed timeout to limit fuzzing tim"
  },
  {
    "path": "test/04-voting-nft/Properties.sol",
    "chars": 1060,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport {Asserts} from \"@chimera/Asserts.sol\";\nimport {Setup} f"
  },
  {
    "path": "test/04-voting-nft/Setup.sol",
    "chars": 1996,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport {VotingNftForFuzz} from \"../../src/04-voting-nft/Voting"
  },
  {
    "path": "test/04-voting-nft/VotingNftCryticTester.sol",
    "chars": 460,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport {Properties} from \"./Properties.sol\";\nimport {CryticAss"
  },
  {
    "path": "test/04-voting-nft/VotingNftCryticToFoundry.sol",
    "chars": 1555,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport {Properties} from \"./Properties.sol\";\nimport {FoundryAs"
  },
  {
    "path": "test/04-voting-nft/certora.conf",
    "chars": 246,
    "preview": "{\n  \"files\": [\n    \"src/04-voting-nft/VotingNft.sol\"\n  ],\n  \"verify\": \"VotingNft:test/04-voting-nft/certora.spec\",\n  \"pa"
  },
  {
    "path": "test/04-voting-nft/certora.spec",
    "chars": 2558,
    "preview": "// run from base folder:\n// certoraRun test/04-voting-nft/certora.conf\nmethods {\n    // `envfree` definitions to call fu"
  },
  {
    "path": "test/04-voting-nft/echidna.yaml",
    "chars": 725,
    "preview": "# no eth required\nbalanceContract: 0\n\n# Allow fuzzer to use public/external functions from all contracts\nallContracts: t"
  },
  {
    "path": "test/04-voting-nft/medusa.json",
    "chars": 2464,
    "preview": "{\n\t\"fuzzing\": {\n\t\t\"workers\": 10,\n\t\t\"workerResetLimit\": 50,\n\t\t\"_COMMENT_TESTING_1\": \"changed timeout to give fuzzer 1 min"
  },
  {
    "path": "test/05-token-sale/TokenSaleAdvancedEchidna.t.sol",
    "chars": 1473,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport \"./TokenSaleBasicEchidna.t.sol\";\n\n// configure solc-sel"
  },
  {
    "path": "test/05-token-sale/TokenSaleAdvancedEchidna.yaml",
    "chars": 716,
    "preview": "# no eth required\nbalanceContract: 0\n\n# constraint fuzzer to token sale contract functions\nallContracts: false\n\n# specif"
  },
  {
    "path": "test/05-token-sale/TokenSaleAdvancedFoundry.t.sol",
    "chars": 1207,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport \"./TokenSaleBasicFoundry.t.sol\";\n\n// run from base proj"
  },
  {
    "path": "test/05-token-sale/TokenSaleBasicEchidna.t.sol",
    "chars": 7004,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport \"../../src/05-token-sale/TokenSale.sol\";\nimport \"../../"
  },
  {
    "path": "test/05-token-sale/TokenSaleBasicEchidna.yaml",
    "chars": 726,
    "preview": "# no eth required\nbalanceContract: 0\n\n# Allow fuzzer to use public/external functions from all contracts\nallContracts: t"
  },
  {
    "path": "test/05-token-sale/TokenSaleBasicFoundry.t.sol",
    "chars": 6344,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport \"../../src/05-token-sale/TokenSale.sol\";\nimport \"../../"
  },
  {
    "path": "test/05-token-sale/TokenSaleBasicMedusa.json",
    "chars": 2735,
    "preview": "{\n\t\"fuzzing\": {\n\t\t\"workers\": 10,\n\t\t\"workerResetLimit\": 50,\n\t\t\"_COMMENT_TESTING_1\": \"changed timeout to limit fuzzing tim"
  },
  {
    "path": "test/05-token-sale/certora.conf",
    "chars": 397,
    "preview": "{\n  \"files\": [\n    \"src/05-token-sale/TokenSale.sol\",\n    \"src/TestToken.sol\",\n    \"src/TestToken2.sol\"\n  ],\n  \"verify\":"
  },
  {
    "path": "test/05-token-sale/certora.spec",
    "chars": 3918,
    "preview": "// run from base folder:\n// certoraRun test/05-token-sale/certora.conf\nmethods {\n    // `envfree` definitions to call fu"
  },
  {
    "path": "test/06-rarely-false/RarelyFalseCryticTester.sol",
    "chars": 438,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport {TargetFunctions} from \"./TargetFunctions.sol\";\nimport "
  },
  {
    "path": "test/06-rarely-false/RarelyFalseCryticToFoundry.sol",
    "chars": 750,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport {TargetFunctions} from \"./TargetFunctions.sol\";\nimport "
  },
  {
    "path": "test/06-rarely-false/TargetFunctions.sol",
    "chars": 773,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport {Asserts} from \"@chimera/Asserts.sol\";\n\n// target funct"
  },
  {
    "path": "test/06-rarely-false/certora.conf",
    "chars": 280,
    "preview": "{\n  \"files\": [\n    \"test/06-rarely-false/RarelyFalseCryticToFoundry.sol\"\n  ],\n  \"verify\": \"RarelyFalseCryticToFoundry:te"
  },
  {
    "path": "test/06-rarely-false/certora.spec",
    "chars": 113,
    "preview": "// run from base folder:\n// certoraRun test/06-rarely-false/certora.conf\nuse builtin rule verifyFoundryFuzzTests;"
  },
  {
    "path": "test/06-rarely-false/echidna.yaml",
    "chars": 491,
    "preview": "# no eth required\nbalanceContract: 0\n\n# Allow fuzzer to use public/external functions from all contracts\nallContracts: f"
  },
  {
    "path": "test/06-rarely-false/medusa.json",
    "chars": 2429,
    "preview": "{\n\t\"fuzzing\": {\n\t\t\"workers\": 10,\n\t\t\"workerResetLimit\": 50,\n\t\t\"_COMMENT_TESTING_1\": \"changed timeout to limit fuzzing tim"
  },
  {
    "path": "test/07-byte-battle/ByteBattleCryticTester.sol",
    "chars": 510,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport {TargetFunctions} from \"./TargetFunctions.sol\";\nimport "
  },
  {
    "path": "test/07-byte-battle/ByteBattleCryticToFoundry.sol",
    "chars": 725,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport {TargetFunctions} from \"./TargetFunctions.sol\";\nimport "
  },
  {
    "path": "test/07-byte-battle/TargetFunctions.sol",
    "chars": 629,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport {Asserts} from \"@chimera/Asserts.sol\";\n\n// target funct"
  },
  {
    "path": "test/07-byte-battle/certora.conf",
    "chars": 276,
    "preview": "{\n  \"files\": [\n    \"test/07-byte-battle/ByteBattleCryticToFoundry.sol\"\n  ],\n  \"verify\": \"ByteBattleCryticToFoundry:test/"
  },
  {
    "path": "test/07-byte-battle/certora.spec",
    "chars": 112,
    "preview": "// run from base folder:\n// certoraRun test/07-byte-battle/certora.conf\nuse builtin rule verifyFoundryFuzzTests;"
  },
  {
    "path": "test/07-byte-battle/echidna.yaml",
    "chars": 371,
    "preview": "# no eth required\nbalanceContract: 0\n\n# Allow fuzzer to use public/external functions from all contracts\nallContracts: f"
  },
  {
    "path": "test/07-byte-battle/medusa.json",
    "chars": 2428,
    "preview": "{\n\t\"fuzzing\": {\n\t\t\"workers\": 10,\n\t\t\"workerResetLimit\": 50,\n\t\t\"_COMMENT_TESTING_1\": \"changed timeout to limit fuzzing tim"
  },
  {
    "path": "test/08-omni-protocol/MockOracle.sol",
    "chars": 1190,
    "preview": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.19;\n\nimport \"@openzeppelin/contracts/access/AccessControl.sol\""
  },
  {
    "path": "test/08-omni-protocol/OmniAdvancedEchidna.yaml",
    "chars": 700,
    "preview": "# no eth required\nbalanceContract: 0\n\n# constraint fuzzer as we are using handlers\nallContracts: false\n\n# specify addres"
  },
  {
    "path": "test/08-omni-protocol/OmniAdvancedFoundry.t.sol",
    "chars": 37796,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\n\nimpo"
  },
  {
    "path": "test/08-omni-protocol/OmniAdvancedMedusa.json",
    "chars": 2580,
    "preview": "{\n\t\"fuzzing\": {\n\t\t\"workers\": 10,\n\t\t\"workerResetLimit\": 50,\n\t\t\"_COMMENT_TESTING_1\": \"increase timeout to give fuzzer appr"
  },
  {
    "path": "test/08-omni-protocol/OmniAdvancedMedusa.t.sol",
    "chars": 37232,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\n\nimpo"
  },
  {
    "path": "test/09-vesting/Properties.sol",
    "chars": 799,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { Setup } from \"./Setup.sol\";\nimport { Asserts } from \""
  },
  {
    "path": "test/09-vesting/Setup.sol",
    "chars": 974,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { Vesting } from \"../../src/09-vesting/Vesting.sol\";\nim"
  },
  {
    "path": "test/09-vesting/TargetFunctions.sol",
    "chars": 1319,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { Properties } from \"./Properties.sol\";\nimport { BaseTa"
  },
  {
    "path": "test/09-vesting/VestingCryticTester.sol",
    "chars": 569,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { TargetFunctions } from \"./TargetFunctions.sol\";\nimpor"
  },
  {
    "path": "test/09-vesting/VestingCryticToFoundry.sol",
    "chars": 1711,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { TargetFunctions } from \"./TargetFunctions.sol\";\nimpor"
  },
  {
    "path": "test/09-vesting/certora.conf",
    "chars": 132,
    "preview": "{\n  \"files\": [\n    \"src/09-vesting/Vesting.sol\"\n  ],\n  \"verify\": \"Vesting:test/09-vesting/certora.spec\",\n  \"optimistic_l"
  },
  {
    "path": "test/09-vesting/certora.spec",
    "chars": 3694,
    "preview": "// run from base folder:\n// certoraRun test/09-vesting/certora.conf\n\n// there should exist no function f() that allows a"
  },
  {
    "path": "test/09-vesting/echidna.yaml",
    "chars": 340,
    "preview": "# don't allow fuzzer to use all functions\n# since we are using handlers\nallContracts: false\n\n# record fuzzer coverage to"
  },
  {
    "path": "test/09-vesting/medusa.json",
    "chars": 2293,
    "preview": "{\n\t\"fuzzing\": {\n\t\t\"workers\": 10,\n\t\t\"workerResetLimit\": 50,\n\t\t\"_COMMENT_TESTING_1\": \"changed timeout to limit fuzzing tim"
  },
  {
    "path": "test/10-vesting-ext/Properties.sol",
    "chars": 961,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { Setup } from \"./Setup.sol\";\nimport { Asserts } from \""
  },
  {
    "path": "test/10-vesting-ext/Setup.sol",
    "chars": 1331,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { VestingExt } from \"../../src/10-vesting-ext/VestingEx"
  },
  {
    "path": "test/10-vesting-ext/TargetFunctions.sol",
    "chars": 1620,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { Properties } from \"./Properties.sol\";\nimport { BaseTa"
  },
  {
    "path": "test/10-vesting-ext/VestingExtCryticTester.sol",
    "chars": 583,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { TargetFunctions } from \"./TargetFunctions.sol\";\nimpor"
  },
  {
    "path": "test/10-vesting-ext/VestingExtCryticToFoundry.sol",
    "chars": 1994,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { TargetFunctions } from \"./TargetFunctions.sol\";\nimpor"
  },
  {
    "path": "test/10-vesting-ext/certora.conf",
    "chars": 146,
    "preview": "{\n  \"files\": [\n    \"src/10-vesting-ext/VestingExt.sol\"\n  ],\n  \"verify\": \"VestingExt:test/10-vesting-ext/certora.spec\",\n "
  },
  {
    "path": "test/10-vesting-ext/certora.spec",
    "chars": 2232,
    "preview": "// run from base folder:\n// certoraRun test/10-vesting-ext/certora.conf\n//\n// solution provided by https://x.com/alexzoi"
  },
  {
    "path": "test/10-vesting-ext/echidna.yaml",
    "chars": 344,
    "preview": "# don't allow fuzzer to use all functions\n# since we are using handlers\nallContracts: false\n\n# record fuzzer coverage to"
  },
  {
    "path": "test/10-vesting-ext/medusa.json",
    "chars": 2296,
    "preview": "{\n\t\"fuzzing\": {\n\t\t\"workers\": 10,\n\t\t\"workerResetLimit\": 50,\n\t\t\"_COMMENT_TESTING_1\": \"changed timeout to limit fuzzing tim"
  },
  {
    "path": "test/11-op-reg/OpRegCryticTester.sol",
    "chars": 563,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { TargetFunctions } from \"./TargetFunctions.sol\";\nimpor"
  },
  {
    "path": "test/11-op-reg/OpRegCryticToFoundry.sol",
    "chars": 1861,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { TargetFunctions } from \"./TargetFunctions.sol\";\nimpor"
  },
  {
    "path": "test/11-op-reg/Properties.sol",
    "chars": 1396,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { Setup } from \"./Setup.sol\";\nimport { Asserts } from \""
  },
  {
    "path": "test/11-op-reg/Setup.sol",
    "chars": 939,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { OperatorRegistry } from \"../../src/11-op-reg/Operator"
  },
  {
    "path": "test/11-op-reg/TargetFunctions.sol",
    "chars": 1015,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { Properties } from \"./Properties.sol\";\nimport { BaseTa"
  },
  {
    "path": "test/11-op-reg/certora.conf",
    "chars": 121,
    "preview": "{\n  \"files\": [\n    \"src/11-op-reg/OperatorRegistry.sol\"\n  ],\n  \"verify\": \"OperatorRegistry:test/11-op-reg/certora.spec\"\n"
  },
  {
    "path": "test/11-op-reg/certora.spec",
    "chars": 1274,
    "preview": "// run from base folder:\n// certoraRun test/11-op-reg/certora.conf\n\n// given two registered operators, there should be n"
  },
  {
    "path": "test/11-op-reg/echidna.yaml",
    "chars": 339,
    "preview": "# don't allow fuzzer to use all functions\n# since we are using handlers\nallContracts: false\n\n# record fuzzer coverage to"
  },
  {
    "path": "test/11-op-reg/medusa.json",
    "chars": 2291,
    "preview": "{\n\t\"fuzzing\": {\n\t\t\"workers\": 10,\n\t\t\"workerResetLimit\": 50,\n\t\t\"_COMMENT_TESTING_1\": \"changed timeout to limit fuzzing tim"
  },
  {
    "path": "test/12-liquidate-dos/LiquidateDosCryticTester.sol",
    "chars": 591,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { TargetFunctions } from \"./TargetFunctions.sol\";\nimpor"
  },
  {
    "path": "test/12-liquidate-dos/LiquidateDosCryticToFoundry.sol",
    "chars": 1954,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { TargetFunctions } from \"./TargetFunctions.sol\";\nimpor"
  },
  {
    "path": "test/12-liquidate-dos/Properties.sol",
    "chars": 1421,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { Setup } from \"./Setup.sol\";\nimport { Asserts } from \""
  },
  {
    "path": "test/12-liquidate-dos/Setup.sol",
    "chars": 1267,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { LiquidateDos } from \"../../src/12-liquidate-dos/Liqui"
  },
  {
    "path": "test/12-liquidate-dos/TargetFunctions.sol",
    "chars": 2384,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { ILiquidateDos } from \"../../src/12-liquidate-dos/Liqu"
  },
  {
    "path": "test/12-liquidate-dos/echidna.yaml",
    "chars": 346,
    "preview": "# don't allow fuzzer to use all functions\n# since we are using handlers\nallContracts: false\n\n# record fuzzer coverage to"
  },
  {
    "path": "test/12-liquidate-dos/medusa.json",
    "chars": 2299,
    "preview": "{\n\t\"fuzzing\": {\n\t\t\"workers\": 10,\n\t\t\"workerResetLimit\": 50,\n\t\t\"_COMMENT_TESTING_1\": \"changed timeout to limit fuzzing tim"
  },
  {
    "path": "test/13-stability-pool/Properties.sol",
    "chars": 796,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { Setup } from \"./Setup.sol\";\nimport { Asserts } from \""
  },
  {
    "path": "test/13-stability-pool/Setup.sol",
    "chars": 888,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { MockERC20 } from \"../../src/MockERC20.sol\";\nimport { "
  },
  {
    "path": "test/13-stability-pool/StabilityPoolCryticTester.sol",
    "chars": 595,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { TargetFunctions } from \"./TargetFunctions.sol\";\nimpor"
  },
  {
    "path": "test/13-stability-pool/StabilityPoolCryticToFoundry.sol",
    "chars": 1791,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { TargetFunctions } from \"./TargetFunctions.sol\";\nimpor"
  },
  {
    "path": "test/13-stability-pool/TargetFunctions.sol",
    "chars": 1753,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { Properties } from \"./Properties.sol\";\nimport { BaseTa"
  },
  {
    "path": "test/13-stability-pool/certora.conf",
    "chars": 426,
    "preview": "{\n  \"files\": [\n    \"src/13-stability-pool/StabilityPool.sol\",\n    \"src/TestToken.sol\",\n    \"src/TestToken2.sol\"\n  ],\n  \""
  },
  {
    "path": "test/13-stability-pool/certora.spec",
    "chars": 4105,
    "preview": "// run from base folder:\n// certoraRun test/13-stability-pool/certora.conf\nmethods {\n    // `envfree` definitions to cal"
  },
  {
    "path": "test/13-stability-pool/echidna.yaml",
    "chars": 347,
    "preview": "# don't allow fuzzer to use all functions\n# since we are using handlers\nallContracts: false\n\n# record fuzzer coverage to"
  },
  {
    "path": "test/13-stability-pool/medusa.json",
    "chars": 2300,
    "preview": "{\n\t\"fuzzing\": {\n\t\t\"workers\": 10,\n\t\t\"workerResetLimit\": 50,\n\t\t\"_COMMENT_TESTING_1\": \"changed timeout to limit fuzzing tim"
  },
  {
    "path": "test/14-priority/PriorityCryticTester.sol",
    "chars": 573,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { TargetFunctions } from \"./TargetFunctions.sol\";\nimpor"
  },
  {
    "path": "test/14-priority/PriorityCryticToFoundry.sol",
    "chars": 1697,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { TargetFunctions } from \"./TargetFunctions.sol\";\nimpor"
  },
  {
    "path": "test/14-priority/Properties.sol",
    "chars": 779,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { Setup } from \"./Setup.sol\";\nimport { Asserts } from \""
  },
  {
    "path": "test/14-priority/Setup.sol",
    "chars": 465,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { Priority } from \"../../src/14-priority/Priority.sol\";"
  },
  {
    "path": "test/14-priority/TargetFunctions.sol",
    "chars": 1648,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\nimport { Properties } from \"./Properties.sol\";\nimport { BaseTa"
  },
  {
    "path": "test/14-priority/certora.conf",
    "chars": 181,
    "preview": "{\n  \"files\": [\n    \"src/14-priority/Priority.sol\"\n  ],\n  \"verify\": \"Priority:test/14-priority/certora.spec\",\n  \"packages"
  },
  {
    "path": "test/14-priority/certora.spec",
    "chars": 1873,
    "preview": "// run from base folder:\n// certoraRun test/14-priority/certora.conf\nmethods {\n    // `envfree` definitions to call func"
  },
  {
    "path": "test/14-priority/echidna.yaml",
    "chars": 341,
    "preview": "# don't allow fuzzer to use all functions\n# since we are using handlers\nallContracts: false\n\n# record fuzzer coverage to"
  },
  {
    "path": "test/14-priority/medusa.json",
    "chars": 2295,
    "preview": "{\n\t\"fuzzing\": {\n\t\t\"workers\": 10,\n\t\t\"workerResetLimit\": 50,\n\t\t\"_COMMENT_TESTING_1\": \"changed timeout to limit fuzzing tim"
  },
  {
    "path": "test/TestUtils.sol",
    "chars": 590,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\n// adapted from https://github.com/crytic/properties/blob/main"
  }
]

About this extraction

This page contains the full source code of the devdacian/solidity-fuzzing-comparison GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 156 files (419.9 KB), approximately 107.2k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!