// SPDX-License-Identifier: Unlicense
pragma solidity >=0.8.12;

/// @notice Gas optimized reentrancy protection for smart contracts.
/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/ReentrancyGuard.sol)
/// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/ReentrancyGuard.sol)
abstract contract ReentrancyGuard {
    uint256 private locked = 1;

    modifier nonReentrant() {
        require(locked == 1, "REENTRANCY");

        locked = 2;

        _;

        locked = 1;
    }
} /// @notice Modern and gas efficient ERC20 + EIP-2612 implementation.

/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC20.sol)
/// @author Modified from Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol)
/// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it.
abstract contract ERC20 {
    /*///////////////////////////////////////////////////////////////
                                  EVENTS
    //////////////////////////////////////////////////////////////*/

    event Transfer(address indexed from, address indexed to, uint256 amount);

    event Approval(
        address indexed owner,
        address indexed spender,
        uint256 amount
    );

    /*///////////////////////////////////////////////////////////////
                             METADATA STORAGE
    //////////////////////////////////////////////////////////////*/

    string public name;

    string public symbol;

    uint8 public immutable decimals;

    /*///////////////////////////////////////////////////////////////
                              ERC20 STORAGE
    //////////////////////////////////////////////////////////////*/

    uint256 public totalSupply;

    mapping(address => uint256) public balanceOf;

    mapping(address => mapping(address => uint256)) public allowance;

    /*///////////////////////////////////////////////////////////////
                             EIP-2612 STORAGE
    //////////////////////////////////////////////////////////////*/

    bytes32 public constant PERMIT_TYPEHASH =
        keccak256(
            "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
        );

    uint256 internal immutable INITIAL_CHAIN_ID;

    bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR;

    mapping(address => uint256) public nonces;

    /*///////////////////////////////////////////////////////////////
                               CONSTRUCTOR
    //////////////////////////////////////////////////////////////*/

    constructor(
        string memory _name,
        string memory _symbol,
        uint8 _decimals
    ) {
        name = _name;
        symbol = _symbol;
        decimals = _decimals;

        INITIAL_CHAIN_ID = block.chainid;
        INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator();
    }

    /*///////////////////////////////////////////////////////////////
                              ERC20 LOGIC
    //////////////////////////////////////////////////////////////*/

    function approve(address spender, uint256 amount)
        public
        virtual
        returns (bool)
    {
        allowance[msg.sender][spender] = amount;

        emit Approval(msg.sender, spender, amount);

        return true;
    }

    function transfer(address to, uint256 amount)
        public
        virtual
        returns (bool)
    {
        balanceOf[msg.sender] -= amount;

        // Cannot overflow because the sum of all user
        // balances can't exceed the max uint256 value.
        unchecked {
            balanceOf[to] += amount;
        }

        emit Transfer(msg.sender, to, amount);

        return true;
    }

    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) public virtual returns (bool) {
        uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals.

        if (allowed != type(uint256).max)
            allowance[from][msg.sender] = allowed - amount;

        balanceOf[from] -= amount;

        // Cannot overflow because the sum of all user
        // balances can't exceed the max uint256 value.
        unchecked {
            balanceOf[to] += amount;
        }

        emit Transfer(from, to, amount);

        return true;
    }

    /*///////////////////////////////////////////////////////////////
                              EIP-2612 LOGIC
    //////////////////////////////////////////////////////////////*/

    function permit(
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) public virtual {
        require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED");

        // Unchecked because the only math done is incrementing
        // the owner's nonce which cannot realistically overflow.
        unchecked {
            bytes32 digest = keccak256(
                abi.encodePacked(
                    "\x19\x01",
                    DOMAIN_SEPARATOR(),
                    keccak256(
                        abi.encode(
                            PERMIT_TYPEHASH,
                            owner,
                            spender,
                            value,
                            nonces[owner]++,
                            deadline
                        )
                    )
                )
            );

            address recoveredAddress = ecrecover(digest, v, r, s);

            require(
                recoveredAddress != address(0) && recoveredAddress == owner,
                "INVALID_SIGNER"
            );

            allowance[recoveredAddress][spender] = value;
        }

        emit Approval(owner, spender, value);
    }

    function DOMAIN_SEPARATOR() public view virtual returns (bytes32) {
        return
            block.chainid == INITIAL_CHAIN_ID
                ? INITIAL_DOMAIN_SEPARATOR
                : computeDomainSeparator();
    }

    function computeDomainSeparator() internal view virtual returns (bytes32) {
        return
            keccak256(
                abi.encode(
                    keccak256(
                        "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
                    ),
                    keccak256(bytes(name)),
                    keccak256("1"),
                    block.chainid,
                    address(this)
                )
            );
    }

    /*///////////////////////////////////////////////////////////////
                       INTERNAL MINT/BURN LOGIC
    //////////////////////////////////////////////////////////////*/

    function _mint(address to, uint256 amount) internal virtual {
        totalSupply += amount;

        // Cannot overflow because the sum of all user
        // balances can't exceed the max uint256 value.
        unchecked {
            balanceOf[to] += amount;
        }

        emit Transfer(address(0), to, amount);
    }

    function _burn(address from, uint256 amount) internal virtual {
        balanceOf[from] -= amount;

        // Cannot underflow because a user's balance
        // will never be larger than the total supply.
        unchecked {
            totalSupply -= amount;
        }

        emit Transfer(from, address(0), amount);
    }
} // OpenZeppelin Contracts v4.4.1 (utils/Strings.sol)

/**
 * @dev String operations.
 */
library Strings {
    bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef";

    /**
     * @dev Converts a `uint256` to its ASCII `string` decimal representation.
     */
    function toString(uint256 value) internal pure returns (string memory) {
        // Inspired by OraclizeAPI's implementation - MIT licence
        // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol

        if (value == 0) {
            return "0";
        }
        uint256 temp = value;
        uint256 digits;
        while (temp != 0) {
            digits++;
            temp /= 10;
        }
        bytes memory buffer = new bytes(digits);
        while (value != 0) {
            digits -= 1;
            buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
            value /= 10;
        }
        return string(buffer);
    }

    /**
     * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.
     */
    function toHexString(uint256 value) internal pure returns (string memory) {
        if (value == 0) {
            return "0x00";
        }
        uint256 temp = value;
        uint256 length = 0;
        while (temp != 0) {
            length++;
            temp >>= 8;
        }
        return toHexString(value, length);
    }

    /**
     * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.
     */
    function toHexString(uint256 value, uint256 length)
        internal
        pure
        returns (string memory)
    {
        bytes memory buffer = new bytes(2 * length + 2);
        buffer[0] = "0";
        buffer[1] = "x";
        for (uint256 i = 2 * length + 1; i > 1; --i) {
            buffer[i] = _HEX_SYMBOLS[value & 0xf];
            value >>= 4;
        }
        require(value == 0, "Strings: hex length insufficient");
        return string(buffer);
    }
} /// @title Interface for verifying contract-based account signatures

/// @notice Interface that verifies provided signature for the data
/// @dev Interface defined by EIP-1271
interface IERC1271 {
    /// @notice Returns whether the provided signature is valid for the provided data
    /// @dev MUST return the bytes4 magic value 0x1626ba7e when function passes.
    /// MUST NOT modify state (using STATICCALL for solc < 0.5, view modifier for solc > 0.5).
    /// MUST allow external calls.
    /// @param hash Hash of the data to be signed
    /// @param signature Signature byte array associated with _data
    /// @return magicValue The bytes4 magic value 0x1626ba7e
    function isValidSignature(bytes32 hash, bytes memory signature)
        external
        view
        returns (bytes4 magicValue);
}

// OpenZeppelin Contracts (last updated v4.5.0) (utils/Address.sol)

/**
 * @dev Collection of functions related to the address type
 */
library Address {
    /**
     * @dev Returns true if `account` is a contract.
     *
     * [IMPORTANT]
     * ====
     * It is unsafe to assume that an address for which this function returns
     * false is an externally-owned account (EOA) and not a contract.
     *
     * Among others, `isContract` will return false for the following
     * types of addresses:
     *
     *  - an externally-owned account
     *  - a contract in construction
     *  - an address where a contract will be created
     *  - an address where a contract lived, but was destroyed
     * ====
     *
     * [IMPORTANT]
     * ====
     * You shouldn't rely on `isContract` to protect against flash loan attacks!
     *
     * Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets
     * like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract
     * constructor.
     * ====
     */
    function isContract(address account) internal view returns (bool) {
        // This method relies on extcodesize/address.code.length, which returns 0
        // for contracts in construction, since the code is only stored at the end
        // of the constructor execution.

        return account.code.length > 0;
    }

    /**
     * @dev Replacement for Solidity's `transfer`: sends `amount` wei to
     * `recipient`, forwarding all available gas and reverting on errors.
     *
     * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost
     * of certain opcodes, possibly making contracts go over the 2300 gas limit
     * imposed by `transfer`, making them unable to receive funds via
     * `transfer`. {sendValue} removes this limitation.
     *
     * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more].
     *
     * IMPORTANT: because control is transferred to `recipient`, care must be
     * taken to not create reentrancy vulnerabilities. Consider using
     * {ReentrancyGuard} or the
     * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].
     */
    function sendValue(address payable recipient, uint256 amount) internal {
        require(
            address(this).balance >= amount,
            "Address: insufficient balance"
        );

        (bool success, ) = recipient.call{value: amount}("");
        require(
            success,
            "Address: unable to send value, recipient may have reverted"
        );
    }

    /**
     * @dev Performs a Solidity function call using a low level `call`. A
     * plain `call` is an unsafe replacement for a function call: use this
     * function instead.
     *
     * If `target` reverts with a revert reason, it is bubbled up by this
     * function (like regular Solidity function calls).
     *
     * Returns the raw returned data. To convert to the expected return value,
     * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].
     *
     * Requirements:
     *
     * - `target` must be a contract.
     * - calling `target` with `data` must not revert.
     *
     * _Available since v3.1._
     */
    function functionCall(address target, bytes memory data)
        internal
        returns (bytes memory)
    {
        return functionCall(target, data, "Address: low-level call failed");
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with
     * `errorMessage` as a fallback revert reason when `target` reverts.
     *
     * _Available since v3.1._
     */
    function functionCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) internal returns (bytes memory) {
        return functionCallWithValue(target, data, 0, errorMessage);
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
     * but also transferring `value` wei to `target`.
     *
     * Requirements:
     *
     * - the calling contract must have an ETH balance of at least `value`.
     * - the called Solidity function must be `payable`.
     *
     * _Available since v3.1._
     */
    function functionCallWithValue(
        address target,
        bytes memory data,
        uint256 value
    ) internal returns (bytes memory) {
        return
            functionCallWithValue(
                target,
                data,
                value,
                "Address: low-level call with value failed"
            );
    }

    /**
     * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but
     * with `errorMessage` as a fallback revert reason when `target` reverts.
     *
     * _Available since v3.1._
     */
    function functionCallWithValue(
        address target,
        bytes memory data,
        uint256 value,
        string memory errorMessage
    ) internal returns (bytes memory) {
        require(
            address(this).balance >= value,
            "Address: insufficient balance for call"
        );
        require(isContract(target), "Address: call to non-contract");

        (bool success, bytes memory returndata) = target.call{value: value}(
            data
        );
        return verifyCallResult(success, returndata, errorMessage);
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
     * but performing a static call.
     *
     * _Available since v3.3._
     */
    function functionStaticCall(address target, bytes memory data)
        internal
        view
        returns (bytes memory)
    {
        return
            functionStaticCall(
                target,
                data,
                "Address: low-level static call failed"
            );
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
     * but performing a static call.
     *
     * _Available since v3.3._
     */
    function functionStaticCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) internal view returns (bytes memory) {
        require(isContract(target), "Address: static call to non-contract");

        (bool success, bytes memory returndata) = target.staticcall(data);
        return verifyCallResult(success, returndata, errorMessage);
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
     * but performing a delegate call.
     *
     * _Available since v3.4._
     */
    function functionDelegateCall(address target, bytes memory data)
        internal
        returns (bytes memory)
    {
        return
            functionDelegateCall(
                target,
                data,
                "Address: low-level delegate call failed"
            );
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
     * but performing a delegate call.
     *
     * _Available since v3.4._
     */
    function functionDelegateCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) internal returns (bytes memory) {
        require(isContract(target), "Address: delegate call to non-contract");

        (bool success, bytes memory returndata) = target.delegatecall(data);
        return verifyCallResult(success, returndata, errorMessage);
    }

    /**
     * @dev Tool to verifies that a low level call was successful, and revert if it wasn't, either by bubbling the
     * revert reason using the provided one.
     *
     * _Available since v4.3._
     */
    function verifyCallResult(
        bool success,
        bytes memory returndata,
        string memory errorMessage
    ) internal pure returns (bytes memory) {
        if (success) {
            return returndata;
        } else {
            // Look for revert reason and bubble it up if present
            if (returndata.length > 0) {
                // The easiest way to bubble the revert reason is using memory via assembly

                assembly {
                    let returndata_size := mload(returndata)
                    revert(add(32, returndata), returndata_size)
                }
            } else {
                revert(errorMessage);
            }
        }
    }
}

library Signature {
    function recover(
        bytes32 hash,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) internal pure returns (address) {
        // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
        // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
        // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most
        // signatures from current libraries generate a unique signature with an s-value in the lower half order.
        //
        // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
        // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
        // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
        // these malleable signatures as well.
        require(
            uint256(s) <=
                0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0,
            "ODYSSEY: INVALID_SIGNATURE_S_VALUE"
        );
        require(v == 27 || v == 28, "ODYSSEY: INVALID_SIGNATURE_V_VALUE");

        // If the signature is valid (and not malleable), return the signer address
        address signer = ecrecover(hash, v, r, s);
        require(signer != address(0), "ODYSSEY: INVALID_SIGNATURE");

        return signer;
    }

    function verify(
        bytes32 hash,
        address signer,
        uint8 v,
        bytes32 r,
        bytes32 s,
        bytes32 domainSeparator
    ) internal view {
        bytes32 digest = keccak256(
            abi.encodePacked("\x19\x01", domainSeparator, hash)
        );
        if (Address.isContract(signer)) {
            require(
                IERC1271(signer).isValidSignature(
                    digest,
                    abi.encodePacked(r, s, v)
                ) == 0x1626ba7e,
                "ODYSSEY: UNAUTHORIZED"
            );
        } else {
            require(
                recover(digest, v, r, s) == signer,
                "ODYSSEY: UNAUTHORIZED"
            );
        }
    }
} // OpenZeppelin Contracts (last updated v4.5.0) (utils/cryptography/MerkleProof.sol)

/**
 * @dev These functions deal with verification of Merkle Trees proofs.
 *
 * The proofs can be generated using the JavaScript library
 * https://github.com/miguelmota/merkletreejs[merkletreejs].
 * Note: the hashing algorithm should be keccak256 and pair sorting should be enabled.
 *
 * See `test/utils/cryptography/MerkleProof.test.js` for some examples.
 *
 * WARNING: You should avoid using leaf values that are 64 bytes long prior to
 * hashing, or use a hash function other than keccak256 for hashing leaves.
 * This is because the concatenation of a sorted pair of internal nodes in
 * the merkle tree could be reinterpreted as a leaf value.
 */
library MerkleProof {
    /**
     * @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree
     * defined by `root`. For this, a `proof` must be provided, containing
     * sibling hashes on the branch from the leaf to the root of the tree. Each
     * pair of leaves and each pair of pre-images are assumed to be sorted.
     */
    function verify(
        bytes32[] memory proof,
        bytes32 root,
        bytes32 leaf
    ) internal pure returns (bool) {
        return processProof(proof, leaf) == root;
    }

    /**
     * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up
     * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt
     * hash matches the root of the tree. When processing the proof, the pairs
     * of leafs & pre-images are assumed to be sorted.
     *
     * _Available since v4.4._
     */
    function processProof(bytes32[] memory proof, bytes32 leaf)
        internal
        pure
        returns (bytes32)
    {
        bytes32 computedHash = leaf;
        for (uint256 i = 0; i < proof.length; i++) {
            bytes32 proofElement = proof[i];
            if (computedHash <= proofElement) {
                // Hash(current computed hash + current element of the proof)
                computedHash = _efficientHash(computedHash, proofElement);
            } else {
                // Hash(current element of the proof + current computed hash)
                computedHash = _efficientHash(proofElement, computedHash);
            }
        }
        return computedHash;
    }

    function _efficientHash(bytes32 a, bytes32 b)
        private
        pure
        returns (bytes32 value)
    {
        assembly {
            mstore(0x00, a)
            mstore(0x20, b)
            value := keccak256(0x00, 0x40)
        }
    }
}

library MerkleWhiteList {
    function verify(
        address sender,
        bytes32[] calldata merkleProof,
        bytes32 merkleRoot
    ) internal pure {
        // Verify whitelist
        require(address(0) != sender);
        bytes32 leaf = keccak256(abi.encodePacked(sender));
        require(
            MerkleProof.verify(merkleProof, merkleRoot, leaf),
            "Not whitelisted"
        );
    }
} /// @notice Modern, minimalist, and gas efficient ERC-721 implementation.

/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC721.sol)
/// @dev Note that balanceOf does not revert if passed the zero address, in defiance of the ERC.
abstract contract ERC721 {
    /*///////////////////////////////////////////////////////////////
                                 EVENTS
    //////////////////////////////////////////////////////////////*/

    event Transfer(
        address indexed from,
        address indexed to,
        uint256 indexed id
    );

    event Approval(
        address indexed owner,
        address indexed spender,
        uint256 indexed id
    );

    event ApprovalForAll(
        address indexed owner,
        address indexed operator,
        bool approved
    );

    /*///////////////////////////////////////////////////////////////
                          METADATA STORAGE/LOGIC
    //////////////////////////////////////////////////////////////*/

    string public name;

    string public symbol;

    function tokenURI(uint256 id) public view virtual returns (string memory);

    /*///////////////////////////////////////////////////////////////
                            ERC721 STORAGE                        
    //////////////////////////////////////////////////////////////*/

    mapping(address => uint256) public balanceOf;

    mapping(uint256 => address) public ownerOf;

    mapping(uint256 => address) public getApproved;

    mapping(address => mapping(address => bool)) public isApprovedForAll;

    /*///////////////////////////////////////////////////////////////
                              CONSTRUCTOR
    //////////////////////////////////////////////////////////////*/

    constructor(string memory _name, string memory _symbol) {
        name = _name;
        symbol = _symbol;
    }

    /*///////////////////////////////////////////////////////////////
                              ERC721 LOGIC
    //////////////////////////////////////////////////////////////*/

    function approve(address spender, uint256 id) public virtual {
        address owner = ownerOf[id];

        require(
            msg.sender == owner || isApprovedForAll[owner][msg.sender],
            "NOT_AUTHORIZED"
        );

        getApproved[id] = spender;

        emit Approval(owner, spender, id);
    }

    function setApprovalForAll(address operator, bool approved) public virtual {
        isApprovedForAll[msg.sender][operator] = approved;

        emit ApprovalForAll(msg.sender, operator, approved);
    }

    function transferFrom(
        address from,
        address to,
        uint256 id
    ) public virtual {
        require(from == ownerOf[id], "WRONG_FROM");

        require(to != address(0), "INVALID_RECIPIENT");

        require(
            msg.sender == from ||
                msg.sender == getApproved[id] ||
                isApprovedForAll[from][msg.sender],
            "NOT_AUTHORIZED"
        );

        // Underflow of the sender's balance is impossible because we check for
        // ownership above and the recipient's balance can't realistically overflow.
        unchecked {
            balanceOf[from]--;

            balanceOf[to]++;
        }

        ownerOf[id] = to;

        delete getApproved[id];

        emit Transfer(from, to, id);
    }

    function safeTransferFrom(
        address from,
        address to,
        uint256 id
    ) public virtual {
        transferFrom(from, to, id);

        require(
            to.code.length == 0 ||
                ERC721TokenReceiver(to).onERC721Received(
                    msg.sender,
                    from,
                    id,
                    ""
                ) ==
                ERC721TokenReceiver.onERC721Received.selector,
            "UNSAFE_RECIPIENT"
        );
    }

    function safeTransferFrom(
        address from,
        address to,
        uint256 id,
        bytes memory data
    ) public virtual {
        transferFrom(from, to, id);

        require(
            to.code.length == 0 ||
                ERC721TokenReceiver(to).onERC721Received(
                    msg.sender,
                    from,
                    id,
                    data
                ) ==
                ERC721TokenReceiver.onERC721Received.selector,
            "UNSAFE_RECIPIENT"
        );
    }

    /*///////////////////////////////////////////////////////////////
                              ERC165 LOGIC
    //////////////////////////////////////////////////////////////*/

    function supportsInterface(bytes4 interfaceId)
        public
        pure
        virtual
        returns (bool)
    {
        return
            interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165
            interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721
            interfaceId == 0x5b5e139f; // ERC165 Interface ID for ERC721Metadata
    }

    /*///////////////////////////////////////////////////////////////
                       INTERNAL MINT/BURN LOGIC
    //////////////////////////////////////////////////////////////*/

    function _mint(address to, uint256 id) internal virtual {
        require(to != address(0), "INVALID_RECIPIENT");

        require(ownerOf[id] == address(0), "ALREADY_MINTED");

        // Counter overflow is incredibly unrealistic.
        unchecked {
            balanceOf[to]++;
        }

        ownerOf[id] = to;

        emit Transfer(address(0), to, id);
    }

    function _burn(uint256 id) internal virtual {
        address owner = ownerOf[id];

        require(ownerOf[id] != address(0), "NOT_MINTED");

        // Ownership check above ensures no underflow.
        unchecked {
            balanceOf[owner]--;
        }

        delete ownerOf[id];

        delete getApproved[id];

        emit Transfer(owner, address(0), id);
    }

    /*///////////////////////////////////////////////////////////////
                       INTERNAL SAFE MINT LOGIC
    //////////////////////////////////////////////////////////////*/

    function _safeMint(address to, uint256 id) internal virtual {
        _mint(to, id);

        require(
            to.code.length == 0 ||
                ERC721TokenReceiver(to).onERC721Received(
                    msg.sender,
                    address(0),
                    id,
                    ""
                ) ==
                ERC721TokenReceiver.onERC721Received.selector,
            "UNSAFE_RECIPIENT"
        );
    }

    function _safeMint(
        address to,
        uint256 id,
        bytes memory data
    ) internal virtual {
        _mint(to, id);

        require(
            to.code.length == 0 ||
                ERC721TokenReceiver(to).onERC721Received(
                    msg.sender,
                    address(0),
                    id,
                    data
                ) ==
                ERC721TokenReceiver.onERC721Received.selector,
            "UNSAFE_RECIPIENT"
        );
    }
}

/// @notice A generic interface for a contract which properly accepts ERC721 tokens.
/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC721.sol)
interface ERC721TokenReceiver {
    function onERC721Received(
        address operator,
        address from,
        uint256 id,
        bytes calldata data
    ) external returns (bytes4);
}

library UInt2Str {
    function uint2str(uint256 _i)
        internal
        pure
        returns (string memory _uintAsString)
    {
        if (_i == 0) {
            return "0";
        }
        uint256 j = _i;
        uint256 len;
        while (j != 0) {
            len++;
            j /= 10;
        }
        bytes memory bstr = new bytes(len);
        uint256 k = len;
        while (_i != 0) {
            k = k - 1;
            uint8 temp = (48 + uint8(_i - (_i / 10) * 10));
            bytes1 b1 = bytes1(temp);
            bstr[k] = b1;
            _i /= 10;
        }
        return string(bstr);
    }
}

contract OdysseyERC721 is ERC721("", "") {
    using UInt2Str for uint256;

    /*///////////////////////////////////////////////////////////////
                            CUSTOM ERRORS
    //////////////////////////////////////////////////////////////*/
    error OdysseyERC721_AlreadyInit();
    error OdysseyERC721_Unauthorized();
    error OdysseyERC721_BadAddress();

    /*///////////////////////////////////////////////////////////////
                            METADATA STORAGE
    //////////////////////////////////////////////////////////////*/
    address launcher;
    address public owner;
    bool initialized;
    string public baseURI;
    uint256 public royaltyFeeInBips; // 1% = 100
    address public royaltyReceiver;
    string public contractURI;

    /*///////////////////////////////////////////////////////////////
                              METADATA LOGIC
    //////////////////////////////////////////////////////////////*/

    function tokenURI(uint256 id)
        public
        view
        virtual
        override
        returns (string memory)
    {
        return string(abi.encodePacked(baseURI, id.uint2str()));
    }

    /*///////////////////////////////////////////////////////////////
                              FACTORY LOGIC
    //////////////////////////////////////////////////////////////*/

    function initialize(
        address _launcher,
        address _owner,
        string calldata _name,
        string calldata _symbol,
        string calldata _baseURI
    ) external {
        if (initialized) {
            revert OdysseyERC721_AlreadyInit();
        }
        initialized = true;
        launcher = _launcher;
        owner = _owner;
        name = _name;
        symbol = _symbol;
        baseURI = _baseURI;
    }

    /*///////////////////////////////////////////////////////////////
                              ERC721 LOGIC
    //////////////////////////////////////////////////////////////*/

    function transferOwnership(address newOwner) public virtual {
        if (newOwner == address(0)) {
            revert OdysseyERC721_BadAddress();
        }
        if (msg.sender != owner) {
            revert OdysseyERC721_Unauthorized();
        }
        owner = newOwner;
    }

    function mint(address user, uint256 id) external {
        if (msg.sender != launcher) {
            revert OdysseyERC721_Unauthorized();
        }
        _mint(user, id);
    }

    /*///////////////////////////////////////////////////////////////
                              EIP2981 LOGIC
    //////////////////////////////////////////////////////////////*/

    function royaltyInfo(uint256, uint256 _salePrice)
        external
        view
        returns (address receiver, uint256 royaltyAmount)
    {
        return (royaltyReceiver, (_salePrice / 10000) * royaltyFeeInBips);
    }

    function setRoyaltyInfo(address _royaltyReceiver, uint256 _royaltyFeeInBips)
        external
    {
        if (_royaltyReceiver == address(0)) {
            revert OdysseyERC721_BadAddress();
        }
        if (msg.sender != owner) {
            revert OdysseyERC721_Unauthorized();
        }
        royaltyReceiver = _royaltyReceiver;
        royaltyFeeInBips = _royaltyFeeInBips;
    }

    function setContractURI(string memory _uri) public {
        if (msg.sender != owner) {
            revert OdysseyERC721_Unauthorized();
        }
        contractURI = _uri;
    }

    /*///////////////////////////////////////////////////////////////
                              ERC165 LOGIC
    //////////////////////////////////////////////////////////////*/

    function supportsInterface(bytes4 interfaceID)
        public
        pure
        override(ERC721)
        returns (bool)
    {
        return
            bytes4(keccak256("royaltyInfo(uint256,uint256)")) == interfaceID ||
            super.supportsInterface(interfaceID);
    }
} /// @notice Minimalist and gas efficient standard ERC1155 implementation.

/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC1155.sol)
abstract contract ERC1155 {
    /*///////////////////////////////////////////////////////////////
                                EVENTS
    //////////////////////////////////////////////////////////////*/

    event TransferSingle(
        address indexed operator,
        address indexed from,
        address indexed to,
        uint256 id,
        uint256 amount
    );

    event TransferBatch(
        address indexed operator,
        address indexed from,
        address indexed to,
        uint256[] ids,
        uint256[] amounts
    );

    event ApprovalForAll(
        address indexed owner,
        address indexed operator,
        bool approved
    );

    event URI(string value, uint256 indexed id);

    /*///////////////////////////////////////////////////////////////
                            ERC1155 STORAGE
    //////////////////////////////////////////////////////////////*/

    mapping(address => mapping(uint256 => uint256)) public balanceOf;

    mapping(address => mapping(address => bool)) public isApprovedForAll;

    /*///////////////////////////////////////////////////////////////
                             METADATA LOGIC
    //////////////////////////////////////////////////////////////*/

    function uri(uint256 id) public view virtual returns (string memory);

    /*///////////////////////////////////////////////////////////////
                             ERC1155 LOGIC
    //////////////////////////////////////////////////////////////*/

    function setApprovalForAll(address operator, bool approved) public virtual {
        isApprovedForAll[msg.sender][operator] = approved;

        emit ApprovalForAll(msg.sender, operator, approved);
    }

    function safeTransferFrom(
        address from,
        address to,
        uint256 id,
        uint256 amount,
        bytes memory data
    ) public virtual {
        require(
            msg.sender == from || isApprovedForAll[from][msg.sender],
            "NOT_AUTHORIZED"
        );

        balanceOf[from][id] -= amount;
        balanceOf[to][id] += amount;

        emit TransferSingle(msg.sender, from, to, id, amount);

        require(
            to.code.length == 0
                ? to != address(0)
                : ERC1155TokenReceiver(to).onERC1155Received(
                    msg.sender,
                    from,
                    id,
                    amount,
                    data
                ) == ERC1155TokenReceiver.onERC1155Received.selector,
            "UNSAFE_RECIPIENT"
        );
    }

    function safeBatchTransferFrom(
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    ) public virtual {
        uint256 idsLength = ids.length; // Saves MLOADs.

        require(idsLength == amounts.length, "LENGTH_MISMATCH");

        require(
            msg.sender == from || isApprovedForAll[from][msg.sender],
            "NOT_AUTHORIZED"
        );

        for (uint256 i = 0; i < idsLength; ) {
            uint256 id = ids[i];
            uint256 amount = amounts[i];

            balanceOf[from][id] -= amount;
            balanceOf[to][id] += amount;

            // An array can't have a total length
            // larger than the max uint256 value.
            unchecked {
                i++;
            }
        }

        emit TransferBatch(msg.sender, from, to, ids, amounts);

        require(
            to.code.length == 0
                ? to != address(0)
                : ERC1155TokenReceiver(to).onERC1155BatchReceived(
                    msg.sender,
                    from,
                    ids,
                    amounts,
                    data
                ) == ERC1155TokenReceiver.onERC1155BatchReceived.selector,
            "UNSAFE_RECIPIENT"
        );
    }

    function balanceOfBatch(address[] memory owners, uint256[] memory ids)
        public
        view
        virtual
        returns (uint256[] memory balances)
    {
        uint256 ownersLength = owners.length; // Saves MLOADs.

        require(ownersLength == ids.length, "LENGTH_MISMATCH");

        balances = new uint256[](owners.length);

        // Unchecked because the only math done is incrementing
        // the array index counter which cannot possibly overflow.
        unchecked {
            for (uint256 i = 0; i < ownersLength; i++) {
                balances[i] = balanceOf[owners[i]][ids[i]];
            }
        }
    }

    /*///////////////////////////////////////////////////////////////
                              ERC165 LOGIC
    //////////////////////////////////////////////////////////////*/

    function supportsInterface(bytes4 interfaceId)
        public
        pure
        virtual
        returns (bool)
    {
        return
            interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165
            interfaceId == 0xd9b67a26 || // ERC165 Interface ID for ERC1155
            interfaceId == 0x0e89341c; // ERC165 Interface ID for ERC1155MetadataURI
    }

    /*///////////////////////////////////////////////////////////////
                        INTERNAL MINT/BURN LOGIC
    //////////////////////////////////////////////////////////////*/

    function _mint(
        address to,
        uint256 id,
        uint256 amount,
        bytes memory data
    ) internal {
        balanceOf[to][id] += amount;

        emit TransferSingle(msg.sender, address(0), to, id, amount);

        require(
            to.code.length == 0
                ? to != address(0)
                : ERC1155TokenReceiver(to).onERC1155Received(
                    msg.sender,
                    address(0),
                    id,
                    amount,
                    data
                ) == ERC1155TokenReceiver.onERC1155Received.selector,
            "UNSAFE_RECIPIENT"
        );
    }

    function _batchMint(
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    ) internal {
        uint256 idsLength = ids.length; // Saves MLOADs.

        require(idsLength == amounts.length, "LENGTH_MISMATCH");

        for (uint256 i = 0; i < idsLength; ) {
            balanceOf[to][ids[i]] += amounts[i];

            // An array can't have a total length
            // larger than the max uint256 value.
            unchecked {
                i++;
            }
        }

        emit TransferBatch(msg.sender, address(0), to, ids, amounts);

        require(
            to.code.length == 0
                ? to != address(0)
                : ERC1155TokenReceiver(to).onERC1155BatchReceived(
                    msg.sender,
                    address(0),
                    ids,
                    amounts,
                    data
                ) == ERC1155TokenReceiver.onERC1155BatchReceived.selector,
            "UNSAFE_RECIPIENT"
        );
    }

    function _batchBurn(
        address from,
        uint256[] memory ids,
        uint256[] memory amounts
    ) internal {
        uint256 idsLength = ids.length; // Saves MLOADs.

        require(idsLength == amounts.length, "LENGTH_MISMATCH");

        for (uint256 i = 0; i < idsLength; ) {
            balanceOf[from][ids[i]] -= amounts[i];

            // An array can't have a total length
            // larger than the max uint256 value.
            unchecked {
                i++;
            }
        }

        emit TransferBatch(msg.sender, from, address(0), ids, amounts);
    }

    function _burn(
        address from,
        uint256 id,
        uint256 amount
    ) internal {
        balanceOf[from][id] -= amount;

        emit TransferSingle(msg.sender, from, address(0), id, amount);
    }
}

/// @notice A generic interface for a contract which properly accepts ERC1155 tokens.
/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC1155.sol)
interface ERC1155TokenReceiver {
    function onERC1155Received(
        address operator,
        address from,
        uint256 id,
        uint256 amount,
        bytes calldata data
    ) external returns (bytes4);

    function onERC1155BatchReceived(
        address operator,
        address from,
        uint256[] calldata ids,
        uint256[] calldata amounts,
        bytes calldata data
    ) external returns (bytes4);
}

contract OdysseyERC1155 is ERC1155 {
    using UInt2Str for uint256;

    /*///////////////////////////////////////////////////////////////
                            CUSTOM ERRORS
    //////////////////////////////////////////////////////////////*/
    error OdysseyERC1155_AlreadyInit();
    error OdysseyERC1155_Unauthorized();
    error OdysseyERC1155_BadAddress();

    /*///////////////////////////////////////////////////////////////
                            METADATA STORAGE
    //////////////////////////////////////////////////////////////*/
    address launcher;
    address public owner;
    string public name;
    string public symbol;
    string public baseURI;
    bool initialized;
    uint256 public royaltyFeeInBips; // 1% = 100
    address public royaltyReceiver;
    string public contractURI;

    /*///////////////////////////////////////////////////////////////
                             METADATA LOGIC
    //////////////////////////////////////////////////////////////*/

    function uri(uint256 id)
        public
        view
        virtual
        override
        returns (string memory)
    {
        return string(abi.encodePacked(baseURI, id.uint2str()));
    }

    /*///////////////////////////////////////////////////////////////
                              FACTORY LOGIC
    //////////////////////////////////////////////////////////////*/

    function initialize(
        address _launcher,
        address _owner,
        string calldata _name,
        string calldata _symbol,
        string calldata _baseURI
    ) external {
        if (isInit()) {
            revert OdysseyERC1155_AlreadyInit();
        }
        initialized = true;
        launcher = _launcher;
        owner = _owner;
        name = _name;
        symbol = _symbol;
        baseURI = _baseURI;
    }

    function isInit() internal view returns (bool) {
        return initialized;
    }

    /*///////////////////////////////////////////////////////////////
                              ERC1155 LOGIC
    //////////////////////////////////////////////////////////////*/

    function transferOwnership(address newOwner) public virtual {
        if (newOwner == address(0)) {
            revert OdysseyERC1155_BadAddress();
        }
        if (msg.sender != owner) {
            revert OdysseyERC1155_Unauthorized();
        }
        owner = newOwner;
    }

    function mint(address user, uint256 id) external {
        if (msg.sender != launcher) {
            revert OdysseyERC1155_Unauthorized();
        }
        _mint(user, id, 1, "");
    }

    function mintBatch(
        address user,
        uint256 id,
        uint256 amount
    ) external {
        if (msg.sender != launcher) {
            revert OdysseyERC1155_Unauthorized();
        }
        _mint(user, id, amount, "");
    }

    /*///////////////////////////////////////////////////////////////
                              EIP2981 LOGIC
    //////////////////////////////////////////////////////////////*/

    function royaltyInfo(uint256, uint256 _salePrice)
        external
        view
        returns (address receiver, uint256 royaltyAmount)
    {
        return (royaltyReceiver, (_salePrice / 10000) * royaltyFeeInBips);
    }

    function setRoyaltyInfo(address _royaltyReceiver, uint256 _royaltyFeeInBips)
        external
    {
        if (_royaltyReceiver == address(0)) {
            revert OdysseyERC1155_BadAddress();
        }
        if (msg.sender != owner) {
            revert OdysseyERC1155_Unauthorized();
        }
        royaltyReceiver = _royaltyReceiver;
        royaltyFeeInBips = _royaltyFeeInBips;
    }

    function setContractURI(string memory _uri) public {
        if (msg.sender != owner) {
            revert OdysseyERC1155_Unauthorized();
        }
        contractURI = _uri;
    }

    /*///////////////////////////////////////////////////////////////
                              ERC165 LOGIC
    //////////////////////////////////////////////////////////////*/

    function supportsInterface(bytes4 interfaceID)
        public
        pure
        override(ERC1155)
        returns (bool)
    {
        return
            bytes4(keccak256("royaltyInfo(uint256,uint256)")) == interfaceID ||
            super.supportsInterface(interfaceID);
    }
}

contract OdysseyTokenFactory {
    /*///////////////////////////////////////////////////////////////
                            CUSTOM ERRORS
    //////////////////////////////////////////////////////////////*/
    error OdysseyTokenFactory_TokenAlreadyExists();
    /*///////////////////////////////////////////////////////////////
                                EVENTS
    //////////////////////////////////////////////////////////////*/

    event TokenCreated(
        string indexed name,
        string indexed symbol,
        address addr,
        bool isERC721,
        uint256 length
    );

    /*///////////////////////////////////////////////////////////////
                            FACTORY STORAGE
    //////////////////////////////////////////////////////////////*/

    mapping(string => mapping(string => address)) public getToken;
    mapping(address => uint256) public tokenExists;
    address[] public allTokens;

    /*///////////////////////////////////////////////////////////////
                            FACTORY LOGIC
    //////////////////////////////////////////////////////////////*/

    function allTokensLength() external view returns (uint256) {
        return allTokens.length;
    }

    function create1155(
        address owner,
        string calldata name,
        string calldata symbol,
        string calldata baseURI
    ) external returns (address token) {
        if (getToken[name][symbol] != address(0)) {
            revert OdysseyTokenFactory_TokenAlreadyExists();
        }
        bytes memory bytecode = type(OdysseyERC1155).creationCode;
        bytes32 salt = keccak256(abi.encodePacked(name, symbol));
        assembly {
            token := create2(0, add(bytecode, 32), mload(bytecode), salt)
        }
        getToken[name][symbol] = token;
        tokenExists[token] = 1;
        // Run the proper initialize function
        OdysseyERC1155(token).initialize(
            msg.sender,
            owner,
            name,
            symbol,
            string(
                abi.encodePacked(
                    baseURI,
                    Strings.toString(block.chainid),
                    "/",
                    Strings.toHexString(uint160(token)),
                    "/"
                )
            )
        );
        emit TokenCreated(name, symbol, token, false, allTokens.length);
        return token;
    }

    function create721(
        address owner,
        string calldata name,
        string calldata symbol,
        string calldata baseURI
    ) external returns (address token) {
        if (getToken[name][symbol] != address(0)) {
            revert OdysseyTokenFactory_TokenAlreadyExists();
        }
        bytes memory bytecode = type(OdysseyERC721).creationCode;
        bytes32 salt = keccak256(abi.encodePacked(name, symbol));
        assembly {
            token := create2(0, add(bytecode, 32), mload(bytecode), salt)
        }
        getToken[name][symbol] = token;
        tokenExists[token] = 1;
        // Run the proper initialize function
        OdysseyERC721(token).initialize(
            msg.sender,
            owner,
            name,
            symbol,
            string(
                abi.encodePacked(
                    baseURI,
                    Strings.toString(block.chainid),
                    "/",
                    Strings.toHexString(uint160(token)),
                    "/"
                )
            )
        );
        emit TokenCreated(name, symbol, token, true, allTokens.length);
    }
}

library OdysseyLib {
    struct Odyssey1155Info {
        uint256[] maxSupply;
        uint256[] tokenIds;
        uint256[] reserveAmounts;
    }

    struct BatchMint {
        bytes32[][] merkleProof;
        bytes32[] merkleRoot;
        uint256[] minPrice;
        uint256[] mintsPerUser;
        uint256[] tokenId;
        address[] tokenAddress;
        address[] currency;
        uint8[] v;
        bytes32[] r;
        bytes32[] s;
    }

    struct Percentage {
        uint256 numerator;
        uint256 denominator;
    }

    function compareDefaultPercentage(OdysseyLib.Percentage calldata percent)
        internal
        pure
        returns (bool result)
    {
        if (percent.numerator > percent.denominator) {
            // Can't have a percent greater than 100
            return false;
        }

        if (percent.numerator == 0 || percent.denominator == 0) {
            // Can't use 0 in percentage
            return false;
        }

        //Check cross multiplication of 3/100
        uint256 crossMultiple1 = percent.numerator * 100;
        uint256 crossMultiple2 = percent.denominator * 3;
        if (crossMultiple1 < crossMultiple2) {
            return false;
        }
        return true;
    }
}

abstract contract OdysseyDatabase {
    // Custom Errors
    error OdysseyLaunchPlatform_TokenDoesNotExist();
    error OdysseyLaunchPlatform_AlreadyClaimed();
    error OdysseyLaunchPlatform_MaxSupplyCap();
    error OdysseyLaunchPlatform_InsufficientFunds();
    error OdysseyLaunchPlatform_TreasuryPayFailure();
    error OdysseyLaunchPlatform_FailedToPayEther();
    error OdysseyLaunchPlatform_FailedToPayERC20();
    error OdysseyLaunchPlatform_ReservedOrClaimedMax();

    // Constants
    // keccak256("whitelistMint721(bytes32 merkleRoot,uint256 minPrice,uint256 mintsPerUser,address tokenAddress,address currency)").toString('hex')
    bytes32 public constant MERKLE_TREE_ROOT_ERC721_TYPEHASH =
        0xf0f6f256599682b9387f45fc268ed696625f835d98d64b8967134239e103fc6c;
    // keccak256("whitelistMint1155(bytes32 merkleRoot,uint256 minPrice,uint256 mintsPerUser,uint256 tokenId,address tokenAddress,address currency)").toString('hex')
    bytes32 public constant MERKLE_TREE_ROOT_ERC1155_TYPEHASH =
        0x0a52f6e0133eadd055cc5703844e676242c3b461d85fb7ce7f74becd7e40edd1;

    // Def understand this before writing code:
    // https://docs.soliditylang.org/en/v0.8.12/internals/layout_in_storage.html
    //--------------------------------------------------------------------------------//
    // Slot       |  Type                  | Description                              //
    //--------------------------------------------------------------------------------//
    // 0x00       |  address               | OdysseyLaunchPlatform.sol                //
    // 0x01       |  address               | OdysseyFactory.sol                       //
    // 0x02       |  address               | Treasury Multisig                        //
    // 0x03       |  address               | Admin Address                            //
    // 0x04       |  address               | OdysseyXp.sol                            //
    //--------------------------------------------------------------------------------//
    // Slot storage
    address launchPlatform; // slot 0
    address factory; // slot 1
    address treasury; // slot 2
    address admin; //slot 3
    address xp; //slot 4

    // Common Storage
    mapping(address => bytes32) public domainSeparator;
    mapping(address => uint256) public whitelistActive;
    mapping(address => address) public ownerOf;
    mapping(address => address) public royaltyRecipient;
    mapping(address => OdysseyLib.Percentage) public treasuryCommission;
    mapping(address => uint256) public ohmFamilyCurrencies;
    // ERC721 Storage
    mapping(address => mapping(address => uint256)) public whitelistClaimed721;
    mapping(address => mapping(address => uint256)) public isReserved721;
    mapping(address => uint256) public cumulativeSupply721;
    mapping(address => uint256) public mintedSupply721;
    mapping(address => uint256) public maxSupply721;
    // ERC1155 Storage
    mapping(address => mapping(address => mapping(uint256 => uint256)))
        public whitelistClaimed1155;
    mapping(address => mapping(address => mapping(uint256 => uint256)))
        public isReserved1155;
    mapping(address => mapping(uint256 => uint256)) public cumulativeSupply1155;
    mapping(address => mapping(uint256 => uint256)) public maxSupply1155;

    function readSlotAsAddress(uint256 slot)
        public
        view
        returns (address data)
    {
        assembly {
            data := sload(slot)
        }
    }
} /// @notice Safe ETH and ERC20 transfer library that gracefully handles missing return values.

/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/SafeTransferLib.sol)
/// @author Modified from Gnosis (https://github.com/gnosis/gp-v2-contracts/blob/main/src/contracts/libraries/GPv2SafeERC20.sol)
/// @dev Use with caution! Some functions in this library knowingly create dirty bits at the destination of the free memory pointer.
library SafeTransferLib {
    /*///////////////////////////////////////////////////////////////
                            ETH OPERATIONS
    //////////////////////////////////////////////////////////////*/

    function safeTransferETH(address to, uint256 amount) internal {
        bool callStatus;

        assembly {
            // Transfer the ETH and store if it succeeded or not.
            callStatus := call(gas(), to, amount, 0, 0, 0, 0)
        }

        require(callStatus, "ETH_TRANSFER_FAILED");
    }

    /*///////////////////////////////////////////////////////////////
                           ERC20 OPERATIONS
    //////////////////////////////////////////////////////////////*/

    function safeTransferFrom(
        ERC20 token,
        address from,
        address to,
        uint256 amount
    ) internal {
        bool callStatus;

        assembly {
            // Get a pointer to some free memory.
            let freeMemoryPointer := mload(0x40)

            // Write the abi-encoded calldata to memory piece by piece:
            mstore(
                freeMemoryPointer,
                0x23b872dd00000000000000000000000000000000000000000000000000000000
            ) // Begin with the function selector.
            mstore(
                add(freeMemoryPointer, 4),
                and(from, 0xffffffffffffffffffffffffffffffffffffffff)
            ) // Mask and append the "from" argument.
            mstore(
                add(freeMemoryPointer, 36),
                and(to, 0xffffffffffffffffffffffffffffffffffffffff)
            ) // Mask and append the "to" argument.
            mstore(add(freeMemoryPointer, 68), amount) // Finally append the "amount" argument. No mask as it's a full 32 byte value.

            // Call the token and store if it succeeded or not.
            // We use 100 because the calldata length is 4 + 32 * 3.
            callStatus := call(gas(), token, 0, freeMemoryPointer, 100, 0, 0)
        }

        require(
            didLastOptionalReturnCallSucceed(callStatus),
            "TRANSFER_FROM_FAILED"
        );
    }

    function safeTransfer(
        ERC20 token,
        address to,
        uint256 amount
    ) internal {
        bool callStatus;

        assembly {
            // Get a pointer to some free memory.
            let freeMemoryPointer := mload(0x40)

            // Write the abi-encoded calldata to memory piece by piece:
            mstore(
                freeMemoryPointer,
                0xa9059cbb00000000000000000000000000000000000000000000000000000000
            ) // Begin with the function selector.
            mstore(
                add(freeMemoryPointer, 4),
                and(to, 0xffffffffffffffffffffffffffffffffffffffff)
            ) // Mask and append the "to" argument.
            mstore(add(freeMemoryPointer, 36), amount) // Finally append the "amount" argument. No mask as it's a full 32 byte value.

            // Call the token and store if it succeeded or not.
            // We use 68 because the calldata length is 4 + 32 * 2.
            callStatus := call(gas(), token, 0, freeMemoryPointer, 68, 0, 0)
        }

        require(
            didLastOptionalReturnCallSucceed(callStatus),
            "TRANSFER_FAILED"
        );
    }

    function safeApprove(
        ERC20 token,
        address to,
        uint256 amount
    ) internal {
        bool callStatus;

        assembly {
            // Get a pointer to some free memory.
            let freeMemoryPointer := mload(0x40)

            // Write the abi-encoded calldata to memory piece by piece:
            mstore(
                freeMemoryPointer,
                0x095ea7b300000000000000000000000000000000000000000000000000000000
            ) // Begin with the function selector.
            mstore(
                add(freeMemoryPointer, 4),
                and(to, 0xffffffffffffffffffffffffffffffffffffffff)
            ) // Mask and append the "to" argument.
            mstore(add(freeMemoryPointer, 36), amount) // Finally append the "amount" argument. No mask as it's a full 32 byte value.

            // Call the token and store if it succeeded or not.
            // We use 68 because the calldata length is 4 + 32 * 2.
            callStatus := call(gas(), token, 0, freeMemoryPointer, 68, 0, 0)
        }

        require(didLastOptionalReturnCallSucceed(callStatus), "APPROVE_FAILED");
    }

    /*///////////////////////////////////////////////////////////////
                         INTERNAL HELPER LOGIC
    //////////////////////////////////////////////////////////////*/

    function didLastOptionalReturnCallSucceed(bool callStatus)
        private
        pure
        returns (bool success)
    {
        assembly {
            // Get how many bytes the call returned.
            let returnDataSize := returndatasize()

            // If the call reverted:
            if iszero(callStatus) {
                // Copy the revert message into memory.
                returndatacopy(0, 0, returnDataSize)

                // Revert with the same message.
                revert(0, returnDataSize)
            }

            switch returnDataSize
            case 32 {
                // Copy the return data into memory.
                returndatacopy(0, 0, returnDataSize)

                // Set success to whether it returned true.
                success := iszero(iszero(mload(0)))
            }
            case 0 {
                // There was no return data.
                success := 1
            }
            default {
                // It returned some malformed input.
                success := 0
            }
        }
    }
} /// @notice Arithmetic library with operations for fixed-point numbers.

/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/FixedPointMathLib.sol)
/// @author Inspired by USM (https://github.com/usmfum/USM/blob/master/contracts/WadMath.sol)
library FixedPointMathLib {
    /*//////////////////////////////////////////////////////////////
                    SIMPLIFIED FIXED POINT OPERATIONS
    //////////////////////////////////////////////////////////////*/

    uint256 internal constant WAD = 1e18; // The scalar of ETH and most ERC20s.

    function mulWadDown(uint256 x, uint256 y) internal pure returns (uint256) {
        return mulDivDown(x, y, WAD); // Equivalent to (x * y) / WAD rounded down.
    }

    function mulWadUp(uint256 x, uint256 y) internal pure returns (uint256) {
        return mulDivUp(x, y, WAD); // Equivalent to (x * y) / WAD rounded up.
    }

    function divWadDown(uint256 x, uint256 y) internal pure returns (uint256) {
        return mulDivDown(x, WAD, y); // Equivalent to (x * WAD) / y rounded down.
    }

    function divWadUp(uint256 x, uint256 y) internal pure returns (uint256) {
        return mulDivUp(x, WAD, y); // Equivalent to (x * WAD) / y rounded up.
    }

    /*//////////////////////////////////////////////////////////////
                    LOW LEVEL FIXED POINT OPERATIONS
    //////////////////////////////////////////////////////////////*/

    function mulDivDown(
        uint256 x,
        uint256 y,
        uint256 denominator
    ) internal pure returns (uint256 z) {
        assembly {
            // Store x * y in z for now.
            z := mul(x, y)

            // Equivalent to require(denominator != 0 && (x == 0 || (x * y) / x == y))
            if iszero(
                and(
                    iszero(iszero(denominator)),
                    or(iszero(x), eq(div(z, x), y))
                )
            ) {
                revert(0, 0)
            }

            // Divide z by the denominator.
            z := div(z, denominator)
        }
    }

    function mulDivUp(
        uint256 x,
        uint256 y,
        uint256 denominator
    ) internal pure returns (uint256 z) {
        assembly {
            // Store x * y in z for now.
            z := mul(x, y)

            // Equivalent to require(denominator != 0 && (x == 0 || (x * y) / x == y))
            if iszero(
                and(
                    iszero(iszero(denominator)),
                    or(iszero(x), eq(div(z, x), y))
                )
            ) {
                revert(0, 0)
            }

            // First, divide z - 1 by the denominator and add 1.
            // We allow z - 1 to underflow if z is 0, because we multiply the
            // end result by 0 if z is zero, ensuring we return 0 if z is zero.
            z := mul(iszero(iszero(z)), add(div(sub(z, 1), denominator), 1))
        }
    }

    function rpow(
        uint256 x,
        uint256 n,
        uint256 scalar
    ) internal pure returns (uint256 z) {
        assembly {
            switch x
            case 0 {
                switch n
                case 0 {
                    // 0 ** 0 = 1
                    z := scalar
                }
                default {
                    // 0 ** n = 0
                    z := 0
                }
            }
            default {
                switch mod(n, 2)
                case 0 {
                    // If n is even, store scalar in z for now.
                    z := scalar
                }
                default {
                    // If n is odd, store x in z for now.
                    z := x
                }

                // Shifting right by 1 is like dividing by 2.
                let half := shr(1, scalar)

                for {
                    // Shift n right by 1 before looping to halve it.
                    n := shr(1, n)
                } n {
                    // Shift n right by 1 each iteration to halve it.
                    n := shr(1, n)
                } {
                    // Revert immediately if x ** 2 would overflow.
                    // Equivalent to iszero(eq(div(xx, x), x)) here.
                    if shr(128, x) {
                        revert(0, 0)
                    }

                    // Store x squared.
                    let xx := mul(x, x)

                    // Round to the nearest number.
                    let xxRound := add(xx, half)

                    // Revert if xx + half overflowed.
                    if lt(xxRound, xx) {
                        revert(0, 0)
                    }

                    // Set x to scaled xxRound.
                    x := div(xxRound, scalar)

                    // If n is even:
                    if mod(n, 2) {
                        // Compute z * x.
                        let zx := mul(z, x)

                        // If z * x overflowed:
                        if iszero(eq(div(zx, x), z)) {
                            // Revert if x is non-zero.
                            if iszero(iszero(x)) {
                                revert(0, 0)
                            }
                        }

                        // Round to the nearest number.
                        let zxRound := add(zx, half)

                        // Revert if zx + half overflowed.
                        if lt(zxRound, zx) {
                            revert(0, 0)
                        }

                        // Return properly scaled zxRound.
                        z := div(zxRound, scalar)
                    }
                }
            }
        }
    }

    /*//////////////////////////////////////////////////////////////
                        GENERAL NUMBER UTILITIES
    //////////////////////////////////////////////////////////////*/

    function sqrt(uint256 x) internal pure returns (uint256 z) {
        assembly {
            // Start off with z at 1.
            z := 1

            // Used below to help find a nearby power of 2.
            let y := x

            // Find the lowest power of 2 that is at least sqrt(x).
            if iszero(lt(y, 0x100000000000000000000000000000000)) {
                y := shr(128, y) // Like dividing by 2 ** 128.
                z := shl(64, z) // Like multiplying by 2 ** 64.
            }
            if iszero(lt(y, 0x10000000000000000)) {
                y := shr(64, y) // Like dividing by 2 ** 64.
                z := shl(32, z) // Like multiplying by 2 ** 32.
            }
            if iszero(lt(y, 0x100000000)) {
                y := shr(32, y) // Like dividing by 2 ** 32.
                z := shl(16, z) // Like multiplying by 2 ** 16.
            }
            if iszero(lt(y, 0x10000)) {
                y := shr(16, y) // Like dividing by 2 ** 16.
                z := shl(8, z) // Like multiplying by 2 ** 8.
            }
            if iszero(lt(y, 0x100)) {
                y := shr(8, y) // Like dividing by 2 ** 8.
                z := shl(4, z) // Like multiplying by 2 ** 4.
            }
            if iszero(lt(y, 0x10)) {
                y := shr(4, y) // Like dividing by 2 ** 4.
                z := shl(2, z) // Like multiplying by 2 ** 2.
            }
            if iszero(lt(y, 0x8)) {
                // Equivalent to 2 ** z.
                z := shl(1, z)
            }

            // Shifting right by 1 is like dividing by 2.
            z := shr(1, add(z, div(x, z)))
            z := shr(1, add(z, div(x, z)))
            z := shr(1, add(z, div(x, z)))
            z := shr(1, add(z, div(x, z)))
            z := shr(1, add(z, div(x, z)))
            z := shr(1, add(z, div(x, z)))
            z := shr(1, add(z, div(x, z)))

            // Compute a rounded down version of z.
            let zRoundDown := div(x, z)

            // If zRoundDown is smaller, use it.
            if lt(zRoundDown, z) {
                z := zRoundDown
            }
        }
    }
} // OpenZeppelin Contracts v4.4.1 (utils/introspection/ERC165Checker.sol)

// OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC165.sol)

/**
 * @dev Interface of the ERC165 standard, as defined in the
 * https://eips.ethereum.org/EIPS/eip-165[EIP].
 *
 * Implementers can declare support of contract interfaces, which can then be
 * queried by others ({ERC165Checker}).
 *
 * For an implementation, see {ERC165}.
 */
interface IERC165 {
    /**
     * @dev Returns true if this contract implements the interface defined by
     * `interfaceId`. See the corresponding
     * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
     * to learn more about how these ids are created.
     *
     * This function call must use less than 30 000 gas.
     */
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

/**
 * @dev Library used to query support of an interface declared via {IERC165}.
 *
 * Note that these functions return the actual result of the query: they do not
 * `revert` if an interface is not supported. It is up to the caller to decide
 * what to do in these cases.
 */
library ERC165Checker {
    // As per the EIP-165 spec, no interface should ever match 0xffffffff
    bytes4 private constant _INTERFACE_ID_INVALID = 0xffffffff;

    /**
     * @dev Returns true if `account` supports the {IERC165} interface,
     */
    function supportsERC165(address account) internal view returns (bool) {
        // Any contract that implements ERC165 must explicitly indicate support of
        // InterfaceId_ERC165 and explicitly indicate non-support of InterfaceId_Invalid
        return
            _supportsERC165Interface(account, type(IERC165).interfaceId) &&
            !_supportsERC165Interface(account, _INTERFACE_ID_INVALID);
    }

    /**
     * @dev Returns true if `account` supports the interface defined by
     * `interfaceId`. Support for {IERC165} itself is queried automatically.
     *
     * See {IERC165-supportsInterface}.
     */
    function supportsInterface(address account, bytes4 interfaceId)
        internal
        view
        returns (bool)
    {
        // query support of both ERC165 as per the spec and support of _interfaceId
        return
            supportsERC165(account) &&
            _supportsERC165Interface(account, interfaceId);
    }

    /**
     * @dev Returns a boolean array where each value corresponds to the
     * interfaces passed in and whether they're supported or not. This allows
     * you to batch check interfaces for a contract where your expectation
     * is that some interfaces may not be supported.
     *
     * See {IERC165-supportsInterface}.
     *
     * _Available since v3.4._
     */
    function getSupportedInterfaces(
        address account,
        bytes4[] memory interfaceIds
    ) internal view returns (bool[] memory) {
        // an array of booleans corresponding to interfaceIds and whether they're supported or not
        bool[] memory interfaceIdsSupported = new bool[](interfaceIds.length);

        // query support of ERC165 itself
        if (supportsERC165(account)) {
            // query support of each interface in interfaceIds
            for (uint256 i = 0; i < interfaceIds.length; i++) {
                interfaceIdsSupported[i] = _supportsERC165Interface(
                    account,
                    interfaceIds[i]
                );
            }
        }

        return interfaceIdsSupported;
    }

    /**
     * @dev Returns true if `account` supports all the interfaces defined in
     * `interfaceIds`. Support for {IERC165} itself is queried automatically.
     *
     * Batch-querying can lead to gas savings by skipping repeated checks for
     * {IERC165} support.
     *
     * See {IERC165-supportsInterface}.
     */
    function supportsAllInterfaces(
        address account,
        bytes4[] memory interfaceIds
    ) internal view returns (bool) {
        // query support of ERC165 itself
        if (!supportsERC165(account)) {
            return false;
        }

        // query support of each interface in _interfaceIds
        for (uint256 i = 0; i < interfaceIds.length; i++) {
            if (!_supportsERC165Interface(account, interfaceIds[i])) {
                return false;
            }
        }

        // all interfaces supported
        return true;
    }

    /**
     * @notice Query if a contract implements an interface, does not check ERC165 support
     * @param account The address of the contract to query for support of an interface
     * @param interfaceId The interface identifier, as specified in ERC-165
     * @return true if the contract at account indicates support of the interface with
     * identifier interfaceId, false otherwise
     * @dev Assumes that account contains a contract that supports ERC165, otherwise
     * the behavior of this method is undefined. This precondition can be checked
     * with {supportsERC165}.
     * Interface identification is specified in ERC-165.
     */
    function _supportsERC165Interface(address account, bytes4 interfaceId)
        private
        view
        returns (bool)
    {
        bytes memory encodedParams = abi.encodeWithSelector(
            IERC165.supportsInterface.selector,
            interfaceId
        );
        (bool success, bytes memory result) = account.staticcall{gas: 30000}(
            encodedParams
        );
        if (result.length < 32) return false;
        return success && abi.decode(result, (bool));
    }
}
struct Rewards {
    uint256 sale;
    uint256 purchase;
    uint256 mint;
    uint256 ohmPurchase;
    uint256 ohmMint;
    uint256 multiplier;
}

struct NFT {
    address contractAddress;
    uint256 id;
}

enum NftType {
    ERC721,
    ERC1155
}

error OdysseyXpDirectory_Unauthorized();

contract OdysseyXpDirectory {
    using ERC165Checker for address;

    Rewards public defaultRewards;
    mapping(address => Rewards) public erc721rewards;
    mapping(address => mapping(uint256 => Rewards)) public erc1155rewards;
    NFT[] public customRewardTokens;
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    // modifier substitute
    function notOwner() internal view returns (bool) {
        return msg.sender != owner;
    }

    function transferOwnership(address newOwner) external {
        if (notOwner()) revert OdysseyXpDirectory_Unauthorized();
        owner = newOwner;
    }

    /*///////////////////////////////////////////////////////////////
                            Reward Setters
    //////////////////////////////////////////////////////////////*/

    /// @notice Set default rewards for contracts without a custom reward set
    /// @param sale XP reward for selling an NFT
    /// @param purchase XP reward for purchasing an NFT
    /// @param mint XP reward for minting an NFT
    /// @param ohmPurchase XP reward for purchasing an NFT with OHM
    /// @param ohmMint XP reward for minting an NFT with OHM
    /// @param multiplier XP reward multiplier for wallets holding an NFT
    function setDefaultRewards(
        uint256 sale,
        uint256 purchase,
        uint256 mint,
        uint256 ohmPurchase,
        uint256 ohmMint,
        uint256 multiplier
    ) public {
        if (notOwner()) revert OdysseyXpDirectory_Unauthorized();
        defaultRewards = Rewards(
            sale,
            purchase,
            mint,
            ohmPurchase,
            ohmMint,
            multiplier
        );
    }

    /// @notice Set custom rewards for an ERC721 contract
    /// @param sale XP reward for selling this NFT
    /// @param purchase XP reward for purchasing this NFT
    /// @param mint XP reward for minting this NFT
    /// @param ohmPurchase XP reward for purchasing this NFT with OHM
    /// @param ohmMint XP reward for minting this NFT with OHM
    /// @param multiplier XP reward multiplier for wallets holding this NFT
    function setErc721CustomRewards(
        address tokenAddress,
        uint256 sale,
        uint256 purchase,
        uint256 mint,
        uint256 ohmPurchase,
        uint256 ohmMint,
        uint256 multiplier
    ) public {
        if (notOwner()) revert OdysseyXpDirectory_Unauthorized();
        customRewardTokens.push(NFT(tokenAddress, 0));
        erc721rewards[tokenAddress] = Rewards(
            sale,
            purchase,
            mint,
            ohmPurchase,
            ohmMint,
            multiplier
        );
    }

    /// @notice Set custom rewards for an ERC1155 contract and token ID
    /// @param sale XP reward for selling this NFT
    /// @param purchase XP reward for purchasing this NFT
    /// @param mint XP reward for minting this NFT
    /// @param ohmPurchase XP reward for purchasing this NFT with OHM
    /// @param ohmMint XP reward for minting this NFT with OHM
    /// @param multiplier XP reward multiplier for wallets holding this NFT
    function setErc1155CustomRewards(
        address tokenAddress,
        uint256 tokenId,
        uint256 sale,
        uint256 purchase,
        uint256 mint,
        uint256 ohmPurchase,
        uint256 ohmMint,
        uint256 multiplier
    ) public {
        if (notOwner()) revert OdysseyXpDirectory_Unauthorized();
        customRewardTokens.push(NFT(tokenAddress, tokenId));
        erc1155rewards[tokenAddress][tokenId] = Rewards(
            sale,
            purchase,
            mint,
            ohmPurchase,
            ohmMint,
            multiplier
        );
    }

    /*///////////////////////////////////////////////////////////////
                            Reward Getters
    //////////////////////////////////////////////////////////////*/

    /// @notice Get the XP reward for selling an NFT
    /// @param seller Seller of the NFT
    /// @param contractAddress Address of the NFT being sold
    /// @param tokenId ID of the NFT being sold
    function getSaleReward(
        address seller,
        address contractAddress,
        uint256 tokenId
    ) public view returns (uint256) {
        (
            bool isCustomErc721,
            bool isCustomErc1155,
            uint256 multiplier
        ) = _getRewardDetails(seller, contractAddress, tokenId);
        if (isCustomErc721) {
            return erc721rewards[contractAddress].sale * multiplier;
        } else if (isCustomErc1155) {
            return erc1155rewards[contractAddress][tokenId].sale * multiplier;
        } else {
            return defaultRewards.sale * multiplier;
        }
    }

    /// @notice Get the XP reward for buying an NFT
    /// @param buyer Buyer of the NFT
    /// @param contractAddress Address of the NFT being sold
    /// @param tokenId ID of the NFT being sold
    function getPurchaseReward(
        address buyer,
        address contractAddress,
        uint256 tokenId
    ) public view returns (uint256) {
        (
            bool isCustomErc721,
            bool isCustomErc1155,
            uint256 multiplier
        ) = _getRewardDetails(buyer, contractAddress, tokenId);
        if (isCustomErc721) {
            return erc721rewards[contractAddress].purchase * multiplier;
        } else if (isCustomErc1155) {
            return
                erc1155rewards[contractAddress][tokenId].purchase * multiplier;
        } else {
            return defaultRewards.purchase * multiplier;
        }
    }

    /// @notice Get the XP reward for minting an NFT
    /// @param buyer Buyer of the NFT
    /// @param contractAddress Address of the NFT being sold
    /// @param tokenId ID of the NFT being sold
    function getMintReward(
        address buyer,
        address contractAddress,
        uint256 tokenId
    ) public view returns (uint256) {
        (
            bool isCustomErc721,
            bool isCustomErc1155,
            uint256 multiplier
        ) = _getRewardDetails(buyer, contractAddress, tokenId);
        if (isCustomErc721) {
            return erc721rewards[contractAddress].mint * multiplier;
        } else if (isCustomErc1155) {
            return erc1155rewards[contractAddress][tokenId].mint * multiplier;
        } else {
            return defaultRewards.mint * multiplier;
        }
    }

    /// @notice Get the XP reward for buying an NFT with OHM
    /// @param buyer Buyer of the NFT
    /// @param contractAddress Address of the NFT being sold
    /// @param tokenId ID of the NFT being sold
    function getOhmPurchaseReward(
        address buyer,
        address contractAddress,
        uint256 tokenId
    ) public view returns (uint256) {
        (
            bool isCustomErc721,
            bool isCustomErc1155,
            uint256 multiplier
        ) = _getRewardDetails(buyer, contractAddress, tokenId);
        if (isCustomErc721) {
            return erc721rewards[contractAddress].ohmPurchase * multiplier;
        } else if (isCustomErc1155) {
            return
                erc1155rewards[contractAddress][tokenId].ohmPurchase *
                multiplier;
        } else {
            return defaultRewards.ohmPurchase * multiplier;
        }
    }

    /// @notice Get the XP reward for minting an NFT with OHM
    /// @param buyer Buyer of the NFT
    /// @param contractAddress Address of the NFT being sold
    /// @param tokenId ID of the NFT being sold
    function getOhmMintReward(
        address buyer,
        address contractAddress,
        uint256 tokenId
    ) public view returns (uint256) {
        (
            bool isCustomErc721,
            bool isCustomErc1155,
            uint256 multiplier
        ) = _getRewardDetails(buyer, contractAddress, tokenId);
        if (isCustomErc721) {
            return erc721rewards[contractAddress].ohmMint * multiplier;
        } else if (isCustomErc1155) {
            return
                erc1155rewards[contractAddress][tokenId].ohmMint * multiplier;
        } else {
            return defaultRewards.ohmMint * multiplier;
        }
    }

    /// @notice Determine if an NFT has custom rewards and any multiplier based on the user's held NFTs
    /// @dev The multiplier and custom rewards are determined simultaneously to save on gas costs of iteration
    /// @param user Wallet address with potential multiplier NFTs
    /// @param contractAddress Address of the NFT being sold
    /// @param tokenId ID of the NFT being sold
    function _getRewardDetails(
        address user,
        address contractAddress,
        uint256 tokenId
    )
        internal
        view
        returns (
            bool isCustomErc721,
            bool isCustomErc1155,
            uint256 multiplier
        )
    {
        NFT[] memory _customRewardTokens = customRewardTokens; // save an SLOAD from length reading
        for (uint256 i = 0; i < _customRewardTokens.length; i++) {
            NFT memory token = _customRewardTokens[i];
            if (token.contractAddress.supportsInterface(0x80ac58cd)) {
                // is ERC721
                if (OdysseyERC721(token.contractAddress).balanceOf(user) > 0) {
                    uint256 reward = erc721rewards[token.contractAddress]
                        .multiplier;
                    multiplier = reward > 1 ? multiplier + reward : multiplier; // only increment if multiplier is non-one
                }
                if (contractAddress == token.contractAddress) {
                    isCustomErc721 = true;
                }
            } else if (token.contractAddress.supportsInterface(0xd9b67a26)) {
                // is isERC1155
                if (
                    OdysseyERC1155(token.contractAddress).balanceOf(
                        user,
                        token.id
                    ) > 0
                ) {
                    uint256 reward = erc1155rewards[token.contractAddress][
                        token.id
                    ].multiplier;
                    multiplier = reward > 1 ? multiplier + reward : multiplier; // only increment if multiplier is non-one
                    if (
                        contractAddress == token.contractAddress &&
                        tokenId == token.id
                    ) {
                        isCustomErc1155 = true;
                    }
                }
            }
        }
        multiplier = multiplier == 0 ? defaultRewards.multiplier : multiplier; // if no custom multiplier, use default
        multiplier = multiplier > 4 ? 4 : multiplier; // multiplier caps at 4
    }
}
error OdysseyXp_Unauthorized();
error OdysseyXp_NonTransferable();
error OdysseyXp_ZeroAssets();

contract OdysseyXp is ERC20 {
    using SafeTransferLib for ERC20;
    using FixedPointMathLib for uint256;

    struct UserHistory {
        uint256 balanceAtLastRedeem;
        uint256 globallyWithdrawnAtLastRedeem;
    }

    /*///////////////////////////////////////////////////////////////
                                 EVENTS
    //////////////////////////////////////////////////////////////*/

    event Mint(address indexed owner, uint256 assets, uint256 xp);

    event Redeem(address indexed owner, uint256 assets, uint256 xp);

    /*///////////////////////////////////////////////////////////////
                            STATE VARIABLES
    //////////////////////////////////////////////////////////////*/

    address public router;
    address public exchange;
    address public owner;
    uint256 public globallyWithdrawn;
    ERC20 public immutable asset;
    OdysseyXpDirectory public directory;
    mapping(address => UserHistory) public userHistories;

    constructor(
        ERC20 _asset,
        OdysseyXpDirectory _directory,
        address _router,
        address _exchange,
        address _owner
    ) ERC20("Odyssey XP", "XP", 0) {
        asset = _asset;
        directory = _directory;
        router = _router;
        exchange = _exchange;
        owner = _owner;
    }

    /*///////////////////////////////////////////////////////////////
                            MODIFIERS
    //////////////////////////////////////////////////////////////*/

    function notOwner() internal view returns (bool) {
        return msg.sender != owner;
    }

    function notRouter() internal view returns (bool) {
        return msg.sender != router;
    }

    function notExchange() internal view returns (bool) {
        return msg.sender != exchange;
    }

    /*///////////////////////////////////////////////////////////////
                        RESTRICTED SETTERS
    //////////////////////////////////////////////////////////////*/

    function setExchange(address _exchange) external {
        if (notOwner()) revert OdysseyXp_Unauthorized();
        exchange = _exchange;
    }

    function setRouter(address _router) external {
        if (notOwner()) revert OdysseyXp_Unauthorized();
        router = _router;
    }

    function setDirectory(address _directory) external {
        if (notOwner()) revert OdysseyXp_Unauthorized();
        directory = OdysseyXpDirectory(_directory);
    }

    function transferOwnership(address _newOwner) external {
        if (notOwner()) revert OdysseyXp_Unauthorized();
        owner = _newOwner;
    }

    /*///////////////////////////////////////////////////////////////
                        XP Granting Methods
    //////////////////////////////////////////////////////////////*/

    function saleReward(
        address seller,
        address contractAddress,
        uint256 tokenId
    ) external {
        if (notExchange()) revert OdysseyXp_Unauthorized();
        _grantXP(
            seller,
            directory.getSaleReward(seller, contractAddress, tokenId)
        );
    }

    function purchaseReward(
        address buyer,
        address contractAddress,
        uint256 tokenId
    ) external {
        if (notExchange()) revert OdysseyXp_Unauthorized();
        _grantXP(
            buyer,
            directory.getPurchaseReward(buyer, contractAddress, tokenId)
        );
    }

    function mintReward(
        address buyer,
        address contractAddress,
        uint256 tokenId
    ) external {
        if (notRouter()) revert OdysseyXp_Unauthorized();
        _grantXP(
            buyer,
            directory.getMintReward(buyer, contractAddress, tokenId)
        );
    }

    function ohmPurchaseReward(
        address buyer,
        address contractAddress,
        uint256 tokenId
    ) external {
        if (notExchange()) revert OdysseyXp_Unauthorized();
        _grantXP(
            buyer,
            directory.getOhmPurchaseReward(buyer, contractAddress, tokenId)
        );
    }

    function ohmMintReward(
        address buyer,
        address contractAddress,
        uint256 tokenId
    ) external {
        if (notRouter()) revert OdysseyXp_Unauthorized();
        _grantXP(
            buyer,
            directory.getOhmMintReward(buyer, contractAddress, tokenId)
        );
    }

    /*///////////////////////////////////////////////////////////////
                            MINT LOGIC
    //////////////////////////////////////////////////////////////*/

    /// @notice Grants the receiver the given amount of XP
    /// @dev Forces the receiver to redeem if they have rewards available
    /// @param receiver The address to grant XP to
    /// @param xp The amount of XP to grant
    function _grantXP(address receiver, uint256 xp)
        internal
        returns (uint256 assets)
    {
        uint256 currentXp = balanceOf[receiver];
        if ((assets = previewRedeem(receiver, currentXp)) > 0)
            _redeem(receiver, assets, currentXp); // force redeeming to keep portions in line
        else if (currentXp == 0)
            userHistories[receiver]
                .globallyWithdrawnAtLastRedeem = globallyWithdrawn; // if a new user, adjust their history to calculate withdrawn at their first redeem
        _mint(receiver, xp);

        emit Mint(msg.sender, assets, xp);

        afterMint(assets, xp);
    }

    /*///////////////////////////////////////////////////////////////
                        REDEEM LOGIC
    //////////////////////////////////////////////////////////////*/

    /// @notice external redeem method
    /// @dev will revert if there is nothing to redeem
    function redeem() public returns (uint256 assets) {
        uint256 xp = balanceOf[msg.sender];
        if ((assets = previewRedeem(msg.sender, xp)) == 0)
            revert OdysseyXp_ZeroAssets();
        _redeem(msg.sender, assets, xp);
    }

    /// @notice Internal logic for redeeming rewards
    /// @param receiver The receiver of rewards
    /// @param assets The amount of assets to grant
    /// @param xp The amount of XP the user is redeeming with
    function _redeem(
        address receiver,
        uint256 assets,
        uint256 xp
    ) internal virtual {
        beforeRedeem(assets, xp);

        userHistories[receiver].balanceAtLastRedeem =
            asset.balanceOf(address(this)) -
            assets;
        userHistories[receiver].globallyWithdrawnAtLastRedeem =
            globallyWithdrawn +
            assets;
        globallyWithdrawn += assets;

        asset.safeTransfer(receiver, assets);

        emit Redeem(receiver, assets, xp);
    }

    /*///////////////////////////////////////////////////////////////
                           ACCOUNTING LOGIC
    //////////////////////////////////////////////////////////////*/

    /// @notice Preview the result of a redeem for the given user with the given XP amount
    /// @param recipient The user to check potential rewards for
    /// @param xp The amount of XP the user is previewing a redeem for
    function previewRedeem(address recipient, uint256 xp)
        public
        view
        virtual
        returns (uint256)
    {
        uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero.

        return
            supply == 0 || xp == 0
                ? 0
                : xp.mulDivDown(totalAssets(recipient), supply);
    }

    /// @notice The total amount of available assets for the user, adjusted based on their history
    /// @param user The user to check assets for
    function totalAssets(address user) internal view returns (uint256) {
        uint256 balance = asset.balanceOf(address(this)); // Saves an extra SLOAD if balance is non-zero.
        return
            balance +
            (globallyWithdrawn -
                userHistories[user].globallyWithdrawnAtLastRedeem) -
            userHistories[user].balanceAtLastRedeem;
    }

    /*///////////////////////////////////////////////////////////////
                       OVERRIDE TRANSFERABILITY
    //////////////////////////////////////////////////////////////*/

    function transfer(address to, uint256 amount)
        public
        override
        returns (bool)
    {
        revert OdysseyXp_NonTransferable();
    }

    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) public override returns (bool) {
        revert OdysseyXp_NonTransferable();
    }

    /*///////////////////////////////////////////////////////////////
                         INTERNAL HOOKS LOGIC
    //////////////////////////////////////////////////////////////*/

    function beforeRedeem(uint256 assets, uint256 xp) internal virtual {}

    function afterMint(uint256 assets, uint256 xp) internal virtual {}
}

contract OdysseyLaunchPlatform is OdysseyDatabase, ReentrancyGuard {
    /*///////////////////////////////////////////////////////////////
                                ACTIONS
    //////////////////////////////////////////////////////////////*/
    function mintERC721(
        bytes32[] calldata merkleProof,
        bytes32 merkleRoot,
        uint256 minPrice,
        uint256 mintsPerUser,
        address tokenAddress,
        address currency,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external payable nonReentrant {
        if (OdysseyTokenFactory(factory).tokenExists(tokenAddress) == 0) {
            revert OdysseyLaunchPlatform_TokenDoesNotExist();
        }
        if (whitelistClaimed721[tokenAddress][msg.sender] >= mintsPerUser) {
            revert OdysseyLaunchPlatform_AlreadyClaimed();
        }
        // Check if user is already reserved + paid
        if (isReserved721[tokenAddress][msg.sender] == 0) {
            if (
                cumulativeSupply721[tokenAddress] >= maxSupply721[tokenAddress]
            ) {
                revert OdysseyLaunchPlatform_MaxSupplyCap();
            }
            {
                // Verify merkle root and minPrice signed by owner (all id's have same min price)
                bytes32 hash = keccak256(
                    abi.encode(
                        MERKLE_TREE_ROOT_ERC721_TYPEHASH,
                        merkleRoot,
                        minPrice,
                        mintsPerUser,
                        tokenAddress,
                        currency
                    )
                );
                Signature.verify(
                    hash,
                    ownerOf[tokenAddress],
                    v,
                    r,
                    s,
                    domainSeparator[tokenAddress]
                );
            }
            if (whitelistActive[tokenAddress] == 1) {
                // Verify user whitelisted
                MerkleWhiteList.verify(msg.sender, merkleProof, merkleRoot);
            }
            cumulativeSupply721[tokenAddress]++;

            OdysseyLib.Percentage storage percent = treasuryCommission[
                tokenAddress
            ];
            uint256 commission = (minPrice * percent.numerator) /
                percent.denominator;

            if (currency == address(0)) {
                if (msg.value < minPrice) {
                    revert OdysseyLaunchPlatform_InsufficientFunds();
                }
                (bool treasurySuccess, ) = treasury.call{value: commission}("");
                if (!treasurySuccess) {
                    revert OdysseyLaunchPlatform_TreasuryPayFailure();
                }
                (bool success, ) = royaltyRecipient[tokenAddress].call{
                    value: minPrice - commission
                }("");
                if (!success) {
                    revert OdysseyLaunchPlatform_FailedToPayEther();
                }
            } else {
                if (
                    ERC20(currency).allowance(msg.sender, address(this)) <
                    minPrice
                ) {
                    revert OdysseyLaunchPlatform_InsufficientFunds();
                }
                bool result = ERC20(currency).transferFrom(
                    msg.sender,
                    treasury,
                    commission
                );
                if (!result) {
                    revert OdysseyLaunchPlatform_TreasuryPayFailure();
                }
                result = ERC20(currency).transferFrom(
                    msg.sender,
                    royaltyRecipient[tokenAddress],
                    minPrice - commission
                );
                if (!result) {
                    revert OdysseyLaunchPlatform_FailedToPayERC20();
                }
                if (ohmFamilyCurrencies[currency] == 1) {
                    OdysseyXp(xp).ohmMintReward(msg.sender, tokenAddress, 0);
                }
            }
        } else {
            isReserved721[tokenAddress][msg.sender]--;
        }
        // Update State
        whitelistClaimed721[tokenAddress][msg.sender]++;
        OdysseyERC721(tokenAddress).mint(
            msg.sender,
            mintedSupply721[tokenAddress]++
        );
    }

    function reserveERC721(
        bytes32[] calldata merkleProof,
        bytes32 merkleRoot,
        uint256 minPrice,
        uint256 mintsPerUser,
        address tokenAddress,
        address currency,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external payable nonReentrant {
        if (OdysseyTokenFactory(factory).tokenExists(tokenAddress) == 0) {
            revert OdysseyLaunchPlatform_TokenDoesNotExist();
        }
        if (cumulativeSupply721[tokenAddress] >= maxSupply721[tokenAddress]) {
            revert OdysseyLaunchPlatform_MaxSupplyCap();
        }
        if (
            isReserved721[tokenAddress][msg.sender] +
                whitelistClaimed721[tokenAddress][msg.sender] >=
            mintsPerUser
        ) {
            revert OdysseyLaunchPlatform_ReservedOrClaimedMax();
        }
        {
            // Verify merkle root and minPrice signed by owner (all id's have same min price)
            bytes32 hash = keccak256(
                abi.encode(
                    MERKLE_TREE_ROOT_ERC721_TYPEHASH,
                    merkleRoot,
                    minPrice,
                    mintsPerUser,
                    tokenAddress,
                    currency
                )
            );
            Signature.verify(
                hash,
                ownerOf[tokenAddress],
                v,
                r,
                s,
                domainSeparator[tokenAddress]
            );
        }
        if (whitelistActive[tokenAddress] == 1) {
            // Verify user whitelisted
            MerkleWhiteList.verify(msg.sender, merkleProof, merkleRoot);
        }

        // Set user is reserved
        isReserved721[tokenAddress][msg.sender]++;
        // Increate Reserved + minted supply
        cumulativeSupply721[tokenAddress]++;

        OdysseyLib.Percentage storage percent = treasuryCommission[
            tokenAddress
        ];
        uint256 commission = (minPrice * percent.numerator) /
            percent.denominator;

        if (currency == address(0)) {
            if (msg.value < minPrice) {
                revert OdysseyLaunchPlatform_InsufficientFunds();
            }
            (bool treasurySuccess, ) = treasury.call{value: commission}("");
            if (!treasurySuccess) {
                revert OdysseyLaunchPlatform_TreasuryPayFailure();
            }
            (bool success, ) = royaltyRecipient[tokenAddress].call{
                value: minPrice - commission
            }("");
            if (!success) {
                revert OdysseyLaunchPlatform_FailedToPayEther();
            }
        } else {
            if (
                ERC20(currency).allowance(msg.sender, address(this)) < minPrice
            ) {
                revert OdysseyLaunchPlatform_InsufficientFunds();
            }
            bool result = ERC20(currency).transferFrom(
                msg.sender,
                treasury,
                commission
            );
            if (!result) {
                revert OdysseyLaunchPlatform_TreasuryPayFailure();
            }
            result = ERC20(currency).transferFrom(
                msg.sender,
                royaltyRecipient[tokenAddress],
                minPrice - commission
            );
            if (!result) {
                revert OdysseyLaunchPlatform_FailedToPayERC20();
            }
            if (ohmFamilyCurrencies[currency] == 1) {
                OdysseyXp(xp).ohmMintReward(msg.sender, tokenAddress, 0);
            }
        }
    }

    function mintERC1155(
        bytes32[] calldata merkleProof,
        bytes32 merkleRoot,
        uint256 minPrice,
        uint256 mintsPerUser,
        uint256 tokenId,
        address tokenAddress,
        address currency,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external payable nonReentrant {
        if (OdysseyTokenFactory(factory).tokenExists(tokenAddress) == 0) {
            revert OdysseyLaunchPlatform_TokenDoesNotExist();
        }
        if (
            whitelistClaimed1155[tokenAddress][msg.sender][tokenId] >=
            mintsPerUser
        ) {
            revert OdysseyLaunchPlatform_AlreadyClaimed();
        }
        // Check if user is already reserved + paid
        if (isReserved1155[tokenAddress][msg.sender][tokenId] == 0) {
            if (
                cumulativeSupply1155[tokenAddress][tokenId] >=
                maxSupply1155[tokenAddress][tokenId]
            ) {
                revert OdysseyLaunchPlatform_MaxSupplyCap();
            }
            {
                // Verify merkle root and minPrice signed by owner (all id's have same min price)
                bytes32 hash = keccak256(
                    abi.encode(
                        MERKLE_TREE_ROOT_ERC1155_TYPEHASH,
                        merkleRoot,
                        minPrice,
                        mintsPerUser,
                        tokenId,
                        tokenAddress,
                        currency
                    )
                );
                Signature.verify(
                    hash,
                    ownerOf[tokenAddress],
                    v,
                    r,
                    s,
                    domainSeparator[tokenAddress]
                );
            }

            if (whitelistActive[tokenAddress] == 1) {
                // Verify user whitelisted
                MerkleWhiteList.verify(msg.sender, merkleProof, merkleRoot);
            }
            cumulativeSupply1155[tokenAddress][tokenId]++;

            OdysseyLib.Percentage storage percent = treasuryCommission[
                tokenAddress
            ];
            uint256 commission = (minPrice * percent.numerator) /
                percent.denominator;

            if (currency == address(0)) {
                if (msg.value < minPrice) {
                    revert OdysseyLaunchPlatform_InsufficientFunds();
                }
                (bool treasurySuccess, ) = treasury.call{value: commission}("");
                if (!treasurySuccess) {
                    revert OdysseyLaunchPlatform_TreasuryPayFailure();
                }
                (bool success, ) = royaltyRecipient[tokenAddress].call{
                    value: minPrice - commission
                }("");
                if (!success) {
                    revert OdysseyLaunchPlatform_FailedToPayEther();
                }
            } else {
                if (
                    ERC20(currency).allowance(msg.sender, address(this)) <
                    minPrice
                ) {
                    revert OdysseyLaunchPlatform_InsufficientFunds();
                }
                bool result = ERC20(currency).transferFrom(
                    msg.sender,
                    treasury,
                    commission
                );
                if (!result) {
                    revert OdysseyLaunchPlatform_TreasuryPayFailure();
                }
                result = ERC20(currency).transferFrom(
                    msg.sender,
                    royaltyRecipient[tokenAddress],
                    minPrice - commission
                );
                if (!result) {
                    revert OdysseyLaunchPlatform_FailedToPayERC20();
                }
                if (ohmFamilyCurrencies[currency] == 1) {
                    OdysseyXp(xp).ohmMintReward(
                        msg.sender,
                        tokenAddress,
                        tokenId
                    );
                }
            }
        } else {
            isReserved1155[tokenAddress][msg.sender][tokenId]--;
        }
        // Update State
        whitelistClaimed1155[tokenAddress][msg.sender][tokenId]++;

        OdysseyERC1155(tokenAddress).mint(msg.sender, tokenId);
    }

    function reserveERC1155(
        bytes32[] calldata merkleProof,
        bytes32 merkleRoot,
        uint256 minPrice,
        uint256 mintsPerUser,
        uint256 tokenId,
        address tokenAddress,
        address currency,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external payable nonReentrant {
        if (OdysseyTokenFactory(factory).tokenExists(tokenAddress) == 0) {
            revert OdysseyLaunchPlatform_TokenDoesNotExist();
        }
        if (
            cumulativeSupply1155[tokenAddress][tokenId] >=
            maxSupply1155[tokenAddress][tokenId]
        ) {
            revert OdysseyLaunchPlatform_MaxSupplyCap();
        }
        if (
            isReserved1155[tokenAddress][msg.sender][tokenId] +
                whitelistClaimed1155[tokenAddress][msg.sender][tokenId] >=
            mintsPerUser
        ) {
            revert OdysseyLaunchPlatform_ReservedOrClaimedMax();
        }
        {
            // Verify merkle root and minPrice signed by owner (all id's have same min price)
            bytes32 hash = keccak256(
                abi.encode(
                    MERKLE_TREE_ROOT_ERC1155_TYPEHASH,
                    merkleRoot,
                    minPrice,
                    mintsPerUser,
                    tokenId,
                    tokenAddress,
                    currency
                )
            );
            Signature.verify(
                hash,
                ownerOf[tokenAddress],
                v,
                r,
                s,
                domainSeparator[tokenAddress]
            );
        }

        if (whitelistActive[tokenAddress] == 1) {
            // Verify user whitelisted
            MerkleWhiteList.verify(msg.sender, merkleProof, merkleRoot);
        }

        // Set user is reserved
        isReserved1155[tokenAddress][msg.sender][tokenId]++;
        // Increase Reserved + minted supply
        cumulativeSupply1155[tokenAddress][tokenId]++;

        OdysseyLib.Percentage storage percent = treasuryCommission[
            tokenAddress
        ];
        uint256 commission = (minPrice * percent.numerator) /
            percent.denominator;

        if (currency == address(0)) {
            if (msg.value < minPrice) {
                revert OdysseyLaunchPlatform_InsufficientFunds();
            }
            (bool treasurySuccess, ) = treasury.call{value: commission}("");
            if (!treasurySuccess) {
                revert OdysseyLaunchPlatform_TreasuryPayFailure();
            }
            (bool success, ) = royaltyRecipient[tokenAddress].call{
                value: minPrice - commission
            }("");
            if (!success) {
                revert OdysseyLaunchPlatform_FailedToPayEther();
            }
        } else {
            if (
                ERC20(currency).allowance(msg.sender, address(this)) < minPrice
            ) {
                revert OdysseyLaunchPlatform_InsufficientFunds();
            }
            bool result = ERC20(currency).transferFrom(
                msg.sender,
                treasury,
                commission
            );
            if (!result) {
                revert OdysseyLaunchPlatform_TreasuryPayFailure();
            }
            result = ERC20(currency).transferFrom(
                msg.sender,
                royaltyRecipient[tokenAddress],
                minPrice - commission
            );
            if (!result) {
                revert OdysseyLaunchPlatform_FailedToPayERC20();
            }
            if (ohmFamilyCurrencies[currency] == 1) {
                OdysseyXp(xp).ohmMintReward(msg.sender, tokenAddress, tokenId);
            }
        }
    }

    function setWhitelistStatus(address addr, bool active)
        external
        nonReentrant
    {
        if (OdysseyTokenFactory(factory).tokenExists(addr) == 0) {
            revert OdysseyLaunchPlatform_TokenDoesNotExist();
        }
        whitelistActive[addr] = active ? 1 : 0;
    }

    function mint721OnCreate(uint256 amount, address token)
        external
        nonReentrant
    {
        cumulativeSupply721[token] = amount;
        mintedSupply721[token] = amount;
        uint256 i;
        for (; i < amount; ++i) {
            OdysseyERC721(token).mint(msg.sender, i);
        }
    }

    function mint1155OnCreate(
        uint256[] calldata tokenIds,
        uint256[] calldata amounts,
        address token
    ) external nonReentrant {
        uint256 i;
        for (; i < tokenIds.length; ++i) {
            cumulativeSupply1155[token][tokenIds[i]] = amounts[i];
            OdysseyERC1155(token).mintBatch(
                msg.sender,
                tokenIds[i],
                amounts[i]
            );
        }
    }

    function ownerMint721(address token, address to) external nonReentrant {
        if (cumulativeSupply721[token] >= maxSupply721[token]) {
            revert OdysseyLaunchPlatform_MaxSupplyCap();
        }
        cumulativeSupply721[token]++;
        OdysseyERC721(token).mint(to, mintedSupply721[token]++);
    }

    function ownerMint1155(
        uint256 id,
        uint256 amount,
        address token,
        address to
    ) external nonReentrant {
        if (
            cumulativeSupply1155[token][id] + amount > maxSupply1155[token][id]
        ) {
            revert OdysseyLaunchPlatform_MaxSupplyCap();
        }
        cumulativeSupply1155[token][id] += amount;
        OdysseyERC1155(token).mintBatch(to, id, amount);
    }
}

contract OdysseyRouter is OdysseyDatabase, ReentrancyGuard {
    error OdysseyRouter_TokenIDSupplyMismatch();
    error OdysseyRouter_WhitelistUpdateFail();
    error OdysseyRouter_Unauthorized();
    error OdysseyRouter_OwnerMintFailure();
    error OdysseyRouter_BadTokenAddress();
    error OdysseyRouter_BadOwnerAddress();
    error OdysseyRouter_BadSenderAddress();
    error OdysseyRouter_BadRecipientAddress();
    error OdysseyRouter_BadTreasuryAddress();
    error OdysseyRouter_BadAdminAddress();

    constructor(
        address treasury_,
        address xpDirectory_,
        address xp_,
        address[] memory ohmCurrencies_
    ) {
        launchPlatform = address(new OdysseyLaunchPlatform());
        factory = address(new OdysseyTokenFactory());
        treasury = treasury_;
        admin = msg.sender;
        uint256 i;
        for (; i < ohmCurrencies_.length; i++) {
            ohmFamilyCurrencies[ohmCurrencies_[i]] = 1;
        }
        if (xp_ == address(0)) {
            if (xpDirectory_ == address(0)) {
                xpDirectory_ = address(new OdysseyXpDirectory());
                OdysseyXpDirectory(xpDirectory_).setDefaultRewards(
                    1,
                    1,
                    1,
                    3,
                    3,
                    1
                );
                OdysseyXpDirectory(xpDirectory_).transferOwnership(admin);
            }
            xp_ = address(
                new OdysseyXp(
                    ERC20(ohmCurrencies_[0]),
                    OdysseyXpDirectory(xpDirectory_),
                    address(this),
                    address(this),
                    admin
                )
            );
        }
        xp = xp_;
    }

    function Factory() public view returns (OdysseyTokenFactory) {
        return OdysseyTokenFactory(readSlotAsAddress(1));
    }

    function create1155(
        string calldata name,
        string calldata symbol,
        string calldata baseURI,
        OdysseyLib.Odyssey1155Info calldata info,
        OdysseyLib.Percentage calldata treasuryPercentage,
        address royaltyReceiver,
        bool whitelist
    ) external returns (address token) {
        if (info.maxSupply.length != info.tokenIds.length) {
            revert OdysseyRouter_TokenIDSupplyMismatch();
        }
        token = Factory().create1155(msg.sender, name, symbol, baseURI);
        ownerOf[token] = msg.sender;
        whitelistActive[token] = whitelist ? 1 : 0;
        royaltyRecipient[token] = royaltyReceiver;
        uint256 i;
        for (; i < info.tokenIds.length; ++i) {
            maxSupply1155[token][info.tokenIds[i]] = (info.maxSupply[i] == 0)
                ? type(uint256).max
                : info.maxSupply[i];
        }

        domainSeparator[token] = keccak256(
            abi.encode(
                // keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)')
                0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f,
                keccak256(bytes(Strings.toHexString(uint160(token)))),
                0xc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc6, // keccak256(bytes("1"))
                block.chainid,
                token
            )
        );

        if (OdysseyLib.compareDefaultPercentage(treasuryPercentage)) {
            // Treasury % was greater than 3/100
            treasuryCommission[token] = treasuryPercentage;
        } else {
            // Treasury % was less than 3/100, using 3/100 as default
            treasuryCommission[token] = OdysseyLib.Percentage(3, 100);
        }

        if (info.reserveAmounts.length > 0) {
            (bool success, bytes memory data) = launchPlatform.delegatecall(
                abi.encodeWithSignature(
                    "mint1155OnCreate(uint256[],uint256[],address)",
                    info.tokenIds,
                    info.reserveAmounts,
                    token
                )
            );
            if (!success) {
                if (data.length == 0) revert();
                assembly {
                    revert(add(32, data), mload(data))
                }
            }
        }
        return token;
    }

    function create721(
        string calldata name,
        string calldata symbol,
        string calldata baseURI,
        uint256 maxSupply,
        uint256 reserveAmount,
        OdysseyLib.Percentage calldata treasuryPercentage,
        address royaltyReceiver,
        bool whitelist
    ) external returns (address token) {
        token = Factory().create721(msg.sender, name, symbol, baseURI);
        ownerOf[token] = msg.sender;
        maxSupply721[token] = (maxSupply == 0) ? type(uint256).max : maxSupply;
        whitelistActive[token] = whitelist ? 1 : 0;
        royaltyRecipient[token] = royaltyReceiver;
        domainSeparator[token] = keccak256(
            abi.encode(
                // keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)')
                0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f,
                keccak256(bytes(Strings.toHexString(uint160(token)))),
                0xc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc6, // keccak256(bytes("1"))
                block.chainid,
                token
            )
        );

        if (OdysseyLib.compareDefaultPercentage(treasuryPercentage)) {
            // Treasury % was greater than 3/100
            treasuryCommission[token] = treasuryPercentage;
        } else {
            // Treasury % was less than 3/100, using 3/100 as default
            treasuryCommission[token] = OdysseyLib.Percentage(3, 100);
        }

        if (reserveAmount > 0) {
            (bool success, bytes memory data) = launchPlatform.delegatecall(
                abi.encodeWithSignature(
                    "mint721OnCreate(uint256,address)",
                    reserveAmount,
                    token
                )
            );
            if (!success) {
                if (data.length == 0) revert();
                assembly {
                    revert(add(32, data), mload(data))
                }
            }
        }

        return token;
    }

    function mintERC721(
        bytes32[] calldata merkleProof,
        bytes32 merkleRoot,
        uint256 minPrice,
        uint256 mintsPerUser,
        address tokenAddress,
        address currency,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) public payable {
        (bool success, bytes memory data) = launchPlatform.delegatecall(
            abi.encodeWithSignature(
                "mintERC721(bytes32[],bytes32,uint256,uint256,address,address,uint8,bytes32,bytes32)",
                merkleProof,
                merkleRoot,
                minPrice,
                mintsPerUser,
                tokenAddress,
                currency,
                v,
                r,
                s
            )
        );
        if (!success) {
            if (data.length == 0) revert();
            assembly {
                revert(add(32, data), mload(data))
            }
        }
    }

    function batchMintERC721(OdysseyLib.BatchMint calldata batch)
        public
        payable
    {
        for (uint256 i = 0; i < batch.tokenAddress.length; i++) {
            (bool success, bytes memory data) = launchPlatform.delegatecall(
                abi.encodeWithSignature(
                    "mintERC721(bytes32[],bytes32,uint256,uint256,address,address,uint8,bytes32,bytes32)",
                    batch.merkleProof[i],
                    batch.merkleRoot[i],
                    batch.minPrice[i],
                    batch.mintsPerUser[i],
                    batch.tokenAddress[i],
                    batch.currency[i],
                    batch.v[i],
                    batch.r[i],
                    batch.s[i]
                )
            );
            if (!success) {
                if (data.length == 0) revert();
                assembly {
                    revert(add(32, data), mload(data))
                }
            }
        }
    }

    function reserveERC721(
        bytes32[] calldata merkleProof,
        bytes32 merkleRoot,
        uint256 minPrice,
        uint256 mintsPerUser,
        address tokenAddress,
        address currency,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) public payable {
        (bool success, bytes memory data) = launchPlatform.delegatecall(
            abi.encodeWithSignature(
                "reserveERC721(bytes32[],bytes32,uint256,uint256,address,address,uint8,bytes32,bytes32)",
                merkleProof,
                merkleRoot,
                minPrice,
                mintsPerUser,
                tokenAddress,
                currency,
                v,
                r,
                s
            )
        );
        if (!success) {
            if (data.length == 0) revert();
            assembly {
                revert(add(32, data), mload(data))
            }
        }
    }

    function batchReserveERC721(OdysseyLib.BatchMint calldata batch)
        public
        payable
    {
        for (uint256 i = 0; i < batch.tokenAddress.length; i++) {
            (bool success, bytes memory data) = launchPlatform.delegatecall(
                abi.encodeWithSignature(
                    "reserveERC721(bytes32[],bytes32,uint256,uint256,address,address,uint8,bytes32,bytes32)",
                    batch.merkleProof[i],
                    batch.merkleRoot[i],
                    batch.minPrice[i],
                    batch.mintsPerUser[i],
                    batch.tokenAddress[i],
                    batch.currency[i],
                    batch.v[i],
                    batch.r[i],
                    batch.s[i]
                )
            );
            if (!success) {
                if (data.length == 0) revert();
                assembly {
                    revert(add(32, data), mload(data))
                }
            }
        }
    }

    function mintERC1155(
        bytes32[] calldata merkleProof,
        bytes32 merkleRoot,
        uint256 minPrice,
        uint256 mintsPerUser,
        uint256 tokenId,
        address tokenAddress,
        address currency,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) public payable {
        (bool success, bytes memory data) = launchPlatform.delegatecall(
            abi.encodeWithSignature(
                "mintERC1155(bytes32[],bytes32,uint256,uint256,uint256,address,address,uint8,bytes32,bytes32)",
                merkleProof,
                merkleRoot,
                minPrice,
                mintsPerUser,
                tokenId,
                tokenAddress,
                currency,
                v,
                r,
                s
            )
        );
        if (!success) {
            if (data.length == 0) revert();
            assembly {
                revert(add(32, data), mload(data))
            }
        }
    }

    function batchMintERC1155(OdysseyLib.BatchMint calldata batch)
        public
        payable
    {
        for (uint256 i = 0; i < batch.tokenAddress.length; i++) {
            (bool success, bytes memory data) = launchPlatform.delegatecall(
                abi.encodeWithSignature(
                    "mintERC1155(bytes32[],bytes32,uint256,uint256,uint256,address,address,uint8,bytes32,bytes32)",
                    batch.merkleProof[i],
                    batch.merkleRoot[i],
                    batch.minPrice[i],
                    batch.mintsPerUser[i],
                    batch.tokenId[i],
                    batch.tokenAddress[i],
                    batch.currency[i],
                    batch.v[i],
                    batch.r[i],
                    batch.s[i]
                )
            );
            if (!success) {
                if (data.length == 0) revert();
                assembly {
                    revert(add(32, data), mload(data))
                }
            }
        }
    }

    function reserveERC1155(
        bytes32[] calldata merkleProof,
        bytes32 merkleRoot,
        uint256 minPrice,
        uint256 mintsPerUser,
        uint256 tokenId,
        address tokenAddress,
        address currency,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) public payable {
        (bool success, bytes memory data) = launchPlatform.delegatecall(
            abi.encodeWithSignature(
                "reserveERC1155(bytes32[],bytes32,uint256,uint256,uint256,address,address,uint8,bytes32,bytes32)",
                merkleProof,
                merkleRoot,
                minPrice,
                mintsPerUser,
                tokenId,
                tokenAddress,
                currency,
                v,
                r,
                s
            )
        );
        if (!success) {
            if (data.length == 0) revert();
            assembly {
                revert(add(32, data), mload(data))
            }
        }
    }

    function batchReserveERC1155(OdysseyLib.BatchMint calldata batch)
        public
        payable
    {
        for (uint256 i = 0; i < batch.tokenAddress.length; i++) {
            (bool success, bytes memory data) = launchPlatform.delegatecall(
                abi.encodeWithSignature(
                    "reserveERC1155(bytes32[],bytes32,uint256,uint256,uint256,address,address,uint8,bytes32,bytes32)",
                    batch.merkleProof[i],
                    batch.merkleRoot[i],
                    batch.minPrice[i],
                    batch.mintsPerUser[i],
                    batch.tokenId[i],
                    batch.tokenAddress[i],
                    batch.currency[i],
                    batch.v[i],
                    batch.r[i],
                    batch.s[i]
                )
            );
            if (!success) {
                if (data.length == 0) revert();
                assembly {
                    revert(add(32, data), mload(data))
                }
            }
        }
    }

    function setWhitelistStatus(address addr, bool active) public {
        if (msg.sender != ownerOf[addr]) {
            revert OdysseyRouter_Unauthorized();
        }
        (bool success, ) = launchPlatform.delegatecall(
            abi.encodeWithSignature(
                "setWhitelistStatus(address,bool)",
                addr,
                active
            )
        );
        if (!success) {
            revert OdysseyRouter_WhitelistUpdateFail();
        }
    }

    function ownerMint721(address token, address to) public {
        if (ownerOf[token] != msg.sender) {
            revert OdysseyRouter_Unauthorized();
        }
        (bool success, ) = launchPlatform.delegatecall(
            abi.encodeWithSignature("ownerMint721(address,address)", token, to)
        );
        if (!success) {
            revert OdysseyRouter_OwnerMintFailure();
        }
    }

    function ownerMint1155(
        uint256 id,
        uint256 amount,
        address token,
        address to
    ) public {
        if (ownerOf[token] != msg.sender) {
            revert OdysseyRouter_Unauthorized();
        }
        (bool success, ) = launchPlatform.delegatecall(
            abi.encodeWithSignature(
                "ownerMint1155(uint256,uint256,address,address)",
                id,
                amount,
                token,
                to
            )
        );
        if (!success) {
            revert OdysseyRouter_OwnerMintFailure();
        }
    }

    function setOwnerShip(address token, address newOwner) public {
        if (token == address(0)) {
            revert OdysseyRouter_BadTokenAddress();
        }
        if (newOwner == address(0)) {
            revert OdysseyRouter_BadOwnerAddress();
        }
        if (msg.sender == address(0)) {
            revert OdysseyRouter_BadSenderAddress();
        }
        if (ownerOf[token] != msg.sender) {
            revert OdysseyRouter_Unauthorized();
        }
        ownerOf[token] = newOwner;
    }

    function setRoyaltyRecipient(address token, address recipient) public {
        if (token == address(0)) {
            revert OdysseyRouter_BadTokenAddress();
        }
        if (recipient == address(0)) {
            revert OdysseyRouter_BadRecipientAddress();
        }
        if (msg.sender == address(0)) {
            revert OdysseyRouter_BadSenderAddress();
        }
        if (ownerOf[token] != msg.sender) {
            revert OdysseyRouter_Unauthorized();
        }
        royaltyRecipient[token] = recipient;
    }

    function setTreasury(address newTreasury) public {
        if (msg.sender != admin) {
            revert OdysseyRouter_Unauthorized();
        }
        if (msg.sender == address(0)) {
            revert OdysseyRouter_BadSenderAddress();
        }
        if (newTreasury == address(0)) {
            revert OdysseyRouter_BadTreasuryAddress();
        }
        treasury = newTreasury;
    }

    function setXP(address newXp) public {
        if (msg.sender != admin) {
            revert OdysseyRouter_Unauthorized();
        }
        if (msg.sender == address(0)) {
            revert OdysseyRouter_BadSenderAddress();
        }
        if (newXp == address(0)) {
            revert OdysseyRouter_BadTokenAddress();
        }
        xp = newXp;
    }

    function setAdmin(address newAdmin) public {
        if (msg.sender != admin) {
            revert OdysseyRouter_Unauthorized();
        }
        if (msg.sender == address(0)) {
            revert OdysseyRouter_BadSenderAddress();
        }
        if (newAdmin == address(0)) {
            revert OdysseyRouter_BadAdminAddress();
        }
        admin = newAdmin;
    }

    function setMaxSupply721(address token, uint256 amount) public {
        if (ownerOf[token] != msg.sender) {
            revert OdysseyRouter_Unauthorized();
        }
        maxSupply721[token] = amount;
    }

    function setMaxSupply1155(
        address token,
        uint256[] calldata tokenIds,
        uint256[] calldata amounts
    ) public {
        if (ownerOf[token] != msg.sender) {
            revert OdysseyRouter_Unauthorized();
        }
        uint256 i;
        for (; i < tokenIds.length; ++i) {
            maxSupply1155[token][tokenIds[i]] = amounts[i];
        }
    }
}