// File: @openzeppelin/contracts/access/Ownable.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (access/Ownable.sol)

pragma solidity ^0.8.0;

import "../utils/Context.sol";

/**
 * @dev Contract module which provides a basic access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * By default, the owner account will be the one that deploys the contract. This
 * can later be changed with {transferOwnership}.
 *
 * This module is used through inheritance. It will make available the modifier
 * `onlyOwner`, which can be applied to your functions to restrict their use to
 * the owner.
 */
abstract contract Ownable is Context {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the deployer as the initial owner.
     */
    constructor() {
        _transferOwnership(_msgSender());
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        _checkOwner();
        _;
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if the sender is not the owner.
     */
    function _checkOwner() internal view virtual {
        require(owner() == _msgSender(), "Ownable: caller is not the owner");
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions anymore. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby removing any functionality that is only available to the owner.
     */
    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}

// File: @openzeppelin/contracts/security/ReentrancyGuard.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (security/ReentrancyGuard.sol)

pragma solidity ^0.8.0;

/**
 * @dev Contract module that helps prevent reentrant calls to a function.
 *
 * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
 * available, which can be applied to functions to make sure there are no nested
 * (reentrant) calls to them.
 *
 * Note that because there is a single `nonReentrant` guard, functions marked as
 * `nonReentrant` may not call one another. This can be worked around by making
 * those functions `private`, and then adding `external` `nonReentrant` entry
 * points to them.
 *
 * TIP: If you would like to learn more about reentrancy and alternative ways
 * to protect against it, check out our blog post
 * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
 */
abstract contract ReentrancyGuard {
    // Booleans are more expensive than uint256 or any type that takes up a full
    // word because each write operation emits an extra SLOAD to first read the
    // slot's contents, replace the bits taken up by the boolean, and then write
    // back. This is the compiler's defense against contract upgrades and
    // pointer aliasing, and it cannot be disabled.

    // The values being non-zero value makes deployment a bit more expensive,
    // but in exchange the refund on every call to nonReentrant will be lower in
    // amount. Since refunds are capped to a percentage of the total
    // transaction's gas, it is best to keep them low in cases like this one, to
    // increase the likelihood of the full refund coming into effect.
    uint256 private constant _NOT_ENTERED = 1;
    uint256 private constant _ENTERED = 2;

    uint256 private _status;

    constructor() {
        _status = _NOT_ENTERED;
    }

    /**
     * @dev Prevents a contract from calling itself, directly or indirectly.
     * Calling a `nonReentrant` function from another `nonReentrant`
     * function is not supported. It is possible to prevent this from happening
     * by making the `nonReentrant` function external, and making it call a
     * `private` function that does the actual work.
     */
    modifier nonReentrant() {
        // On the first call to nonReentrant, _notEntered will be true
        require(_status != _ENTERED, "ReentrancyGuard: reentrant call");

        // Any calls to nonReentrant after this point will fail
        _status = _ENTERED;

        _;

        // By storing the original value once again, a refund is triggered (see
        // https://eips.ethereum.org/EIPS/eip-2200)
        _status = _NOT_ENTERED;
    }
}

// File: @openzeppelin/contracts/utils/Context.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)

pragma solidity ^0.8.0;

/**
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }
}

// File: contracts/extensions/CometPrePay.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.9;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import { SafeTransferLib } from "solmate/src/utils/SafeTransferLib.sol";

interface ERC721Airdroppable {
    /// @notice A token receiver
    struct Receiver {
        address to;
        uint256 quantity;
    }

    function airDrop(Receiver[] memory receivers) external;
}

/**
 * @notice A contract for accepting crypto in exchange for a future airdrop.
 */
contract CometPrePay is Ownable, ReentrancyGuard {
    /// @notice Reservation struct
    struct Reservation {
        address to;
        uint256 quantity;
    }

    /// @notice Sale Config struct
    struct SaleConfig {
        uint256 startTime;
        uint256 endTime;
        uint256 price;
        bool isOpen;
        string creditUrl;
    }

    /// @notice The address of the ERC721 token to sell.
    address private _tokenAddress;

    /// @notice The address of the admin who can set the sale config.
    address private _administrator;

    /// @notice The address of the beneficiary to get paid.
    address private _beneficiary;

    /// @notice The owner address of the tokens that will be sold.
    uint256 private _maxSupply;

    /// @notice The supply we reserve if tokens are purchased in other ways.
    uint256 private _reserveSupply;

    /// @notice The total supply purchased.
    uint256 private _totalSupply;

    /// @notice The pointer to the current index of reservations.
    uint256 private _redeemPointer = 0;

    /// @notice Reservations array.
    Reservation[] private _reservations;

    /// @notice The sale configuration.
    SaleConfig internal _saleConfig;

    /// @notice The interface to the ERC721 token.
    ERC721Airdroppable private _token;

    event AdministratorUpdated(address newAdministrator);
    event BeneficiaryUpdated(address newBeneficiary);
    event MaxSupplyUpdated(uint256 newMaxSupply);
    event PrePayCompleted(uint256 quantity, address sender);
    event PriceUpdated(uint256 newPrice);
    event Redeem(uint256 reservations, uint256 redeemPointer, uint256 tokens);
    event ReserveSupplyUpdated(uint256 newReserveSupply);
    event SaleConfigUpdated(SaleConfig newSaleConfig);
    event SaleIsOpenUpdated(bool isOpen);
    event TokenAddressUpdated(address newTokenAddress);

    error AdministratorCannotBeNullAddress();
    error BeneficiaryCannotBeNullAddress();
    error IncorrectPayment(uint256 received, uint256 needs);
    error MaxSupplyExceeded();
    error OnlyOwnerOrAdministrator();
    error PreSaleNotOpen();
    error TokenCannotBeNullAddress();

    modifier onlyOwnerOrAdministrator() virtual {
        if (msg.sender != owner()) {
            if (msg.sender != _administrator) {
                revert OnlyOwnerOrAdministrator();
            }
        }
        _;
    }

    /**
     * @notice Comet PrePay constructor.
     *
     * @param initialMaxSupply The max supply of the sale.
     * @param price The price of an item.
     * @param slug The slug of the collection.
     */
    constructor(uint256 initialMaxSupply, uint256 price, string memory slug) {
        _maxSupply = initialMaxSupply;
        _saleConfig.price = price;

        _beneficiary = owner();

        string memory subdomain = block.chainid == 1 ? "www" : "staging";

        _saleConfig.creditUrl = string(
            abi.encodePacked(
                "https://",
                subdomain,
                ".crossmint.com/collections/",
                slug,
                "/prepay"
            )
        );
    }

    /**
     * @notice Revert if the payment is incorrect.
     *
     * @param quantity  The quantity of tokens to mint.
     */
    function _checkPayment(uint256 quantity) private view {
        // Revert if the math isn't right.
        if (msg.value != quantity * _saleConfig.price) {
            revert IncorrectPayment(msg.value, quantity * _saleConfig.price);
        }
    }

    /**
     * @notice Revert if the max supply is exceeded.
     *
     * @param quantity  The quantity of tokens to mint.
     */
    function _checkMaxSupply(uint256 quantity) private view {
        // Revert if max supply is exceeded
        if (_totalSupply + _reserveSupply + quantity > _maxSupply) {
            revert MaxSupplyExceeded();
        }
    }

    /**
     * @notice Sets the admin wallet address.
     *
     * @param wallet The new wallet address.
     */
    function setAdministrator(address wallet) external onlyOwner {
        // Revert if the new address is the null address.
        if (wallet == address(0)) {
            revert AdministratorCannotBeNullAddress();
        }

        _administrator = wallet;

        // Emit updated event
        emit AdministratorUpdated(wallet);
    }

    /**
     * @notice Sets the beneficiary wallet address.
     *
     * @param wallet The new wallet address.
     */
    function setBeneficiary(address wallet) external onlyOwner {
        // Revert if the new address is the null address.
        if (wallet == address(0)) {
            revert BeneficiaryCannotBeNullAddress();
        }

        _beneficiary = wallet;

        // Emit updated event
        emit BeneficiaryUpdated(wallet);
    }

    /**
     * @notice Set the max supply of the prePay sale.
     *
     * @param value  The max supply.
     */
    function setMaxSupply(uint256 value) external onlyOwnerOrAdministrator {
        _maxSupply = value;

        emit MaxSupplyUpdated(value);
    }

    /**
     * @notice Set the reserve supply of the prePay sale.
     *
     * @param value  The reserve supply.
     */
    function setReserveSupply(uint256 value) external onlyOwnerOrAdministrator {
        _reserveSupply = value;

        emit ReserveSupplyUpdated(value);
    }

    /**
     * @notice Settting the status to closed forces closure
     *
     * @param value The is-open status.
     */
    function setIsOpen(bool value) external onlyOwnerOrAdministrator {
        _saleConfig.isOpen = value;

        emit SaleIsOpenUpdated(value);
    }

    /**
     * @notice Set the price of the prePay sale.
     *
     * @param price The price in wei.
     */
    function setPrice(uint256 price) external onlyOwnerOrAdministrator {
        _saleConfig.price = price;

        emit PriceUpdated(price);
    }

    /**
     * @notice Set the sale config
     *
     * @param config  The sale config.
     */
    function setSaleConfig(
        SaleConfig memory config
    ) external onlyOwnerOrAdministrator {
        _saleConfig = config;

        emit SaleConfigUpdated(config);
    }

    /**
     * @notice Set the ERC721 token address that reseverations will redeem.
     *
     * @param newTokenAddress  The ERC721 token address.
     */
    function setTokenAddress(address newTokenAddress) external onlyOwner {
        _tokenAddress = newTokenAddress;
        _token = ERC721Airdroppable(newTokenAddress);

        emit TokenAddressUpdated(newTokenAddress);
    }

    /**
     * @notice Redeem reservations by dropping tokens.
     *
     * @param quantity The number of reservations to redeem.
     */
    function redeem(uint256 quantity) external onlyOwner {
        // Revert if the new address is the null address.
        if (_tokenAddress == address(0)) {
            revert TokenCannotBeNullAddress();
        }

        uint256 tokens = 0;
        uint256 remaining = _totalSupply - _redeemPointer;

        if (remaining < quantity) {
            quantity = remaining;
        }

        ERC721Airdroppable.Receiver[]
            memory receivers = new ERC721Airdroppable.Receiver[](quantity);

        for (uint256 i = 0; i < quantity; i++) {
            Reservation memory reservation = _reservations[_redeemPointer + i];
            receivers[i] = ERC721Airdroppable.Receiver(
                reservation.to,
                reservation.quantity
            );

            tokens += reservation.quantity;
        }

        _token.airDrop(receivers);

        _redeemPointer += quantity;

        emit Redeem(quantity, _redeemPointer, tokens);
    }

    /**
     * @notice Get the administrator wallet
     *
     * @return uint256 the wallet
     */
    function administrator() external view returns (address) {
        return _administrator;
    }

    /**
     * @notice Get the beneficiary wallet
     *
     * @return uint256 the wallet
     */
    function beneficiary() external view returns (address) {
        return _beneficiary;
    }

    /**
     * @notice Get the max supply
     *
     * @return uint256 the max supply
     */
    function maxSupply() external view returns (uint256) {
        return _maxSupply;
    }

    /**
     * @notice Get the position of the redeem pointer
     *
     * @return uint256 the redeem pointer
     */
    function redeemPointer() external view returns (uint256) {
        return _redeemPointer;
    }

    /**
     * @notice Gets the current sale config.
     *
     * @return SaleConfig the config
     */
    function saleConfig() external view returns (SaleConfig memory) {
        return _saleConfig;
    }

    /**
     * @notice Gets the reserve supply
     *
     * @return uint256 the reserve supply
     */
    function reserveSupply() external view returns (uint256) {
        return _reserveSupply;
    }

    /**
     * @notice Gets the total supply purchased
     *
     * @return uint256 the total supply
     */
    function totalPurchased() external view returns (uint256) {
        return _totalSupply;
    }

    /**
     * @notice Gets the total supply including the reserve supply
     *
     * @return uint256 the total supply
     */
    function totalSupply() external view returns (uint256) {
        return _totalSupply + _reserveSupply;
    }

    /**
     * @notice Pre Pay! Get a reservation for your NFT
     *
     * @param quantity the amount to buy (reserve)
     */
    function prePay(uint256 quantity) external payable nonReentrant {
        // Is the sale open??
        if (!isOpen()) {
            revert PreSaleNotOpen();
        }

        // Check the math!
        _checkPayment(quantity);

        // Don't go over!
        _checkMaxSupply(quantity);

        // Store the reservation
        _reservations.push(Reservation(msg.sender, quantity));

        // Update the total supply
        _totalSupply += quantity;

        // Transfer value immediately
        SafeTransferLib.safeTransferETH(_beneficiary, msg.value);

        emit PrePayCompleted(quantity, msg.sender);
    }

    /**
     * @notice Gets the open status of the sale configuration.
     *
     * @return bool true/false if the sale is open.
     */
    function isOpen() public view returns (bool) {
        // if the sale config isOpen is set to false then that takes precedence
        if (!_saleConfig.isOpen) {
            return false;
        }

        // otherwise check if the sale is open using the block timestamp
        if (
            block.timestamp >= _saleConfig.startTime &&
            block.timestamp < _saleConfig.endTime
        ) {
            return true;
        }

        return false;
    }
}

// File: contracts/mocks/ShatnerCosmicExplorerPrePay.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.9;

import { CometPrePay } from "../extensions/CometPrePay.sol";

/**
 * @dev This implements an optional extension of {CometPrePay}
 */
contract ShatnerCosmicExplorerPrePay is CometPrePay {
    /**
     * @notice Comet PrePay constructor.
     *
     * @param initialMaxSupply The max supply of the sale.
     * @param price The price of an item.
     * @param slug The slug of the collection.
     */
    constructor(
        uint256 initialMaxSupply,
        uint256 price,
        string memory slug
    ) CometPrePay(initialMaxSupply, price, slug) {}
}

// File: solmate/src/tokens/ERC20.sol



// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;

/// @notice Modern and gas efficient ERC20 + EIP-2612 implementation.
/// @author Solmate (https://github.com/transmissions11/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
    //////////////////////////////////////////////////////////////*/

    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 {
            address recoveredAddress = ecrecover(
                keccak256(
                    abi.encodePacked(
                        "\x19\x01",
                        DOMAIN_SEPARATOR(),
                        keccak256(
                            abi.encode(
                                keccak256(
                                    "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
                                ),
                                owner,
                                spender,
                                value,
                                nonces[owner]++,
                                deadline
                            )
                        )
                    )
                ),
                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);
    }
}

// File: solmate/src/utils/SafeTransferLib.sol



// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;

import {ERC20} from "../tokens/ERC20.sol";

/// @notice Safe ETH and ERC20 transfer library that gracefully handles missing return values.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/SafeTransferLib.sol)
/// @dev Use with caution! Some functions in this library knowingly create dirty bits at the destination of the free memory pointer.
/// @dev Note that none of the functions in this library check that a token has code at all! That responsibility is delegated to the caller.
library SafeTransferLib {
    /*//////////////////////////////////////////////////////////////
                             ETH OPERATIONS
    //////////////////////////////////////////////////////////////*/

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

        /// @solidity memory-safe-assembly
        assembly {
            // Transfer the ETH and store if it succeeded or not.
            success := call(gas(), to, amount, 0, 0, 0, 0)
        }

        require(success, "ETH_TRANSFER_FAILED");
    }

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

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

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

            // Write the abi-encoded calldata into memory, beginning with the function selector.
            mstore(freeMemoryPointer, 0x23b872dd00000000000000000000000000000000000000000000000000000000)
            mstore(add(freeMemoryPointer, 4), from) // Append the "from" argument.
            mstore(add(freeMemoryPointer, 36), to) // Append the "to" argument.
            mstore(add(freeMemoryPointer, 68), amount) // Append the "amount" argument.

            success := and(
                // Set success to whether the call reverted, if not we check it either
                // returned exactly 1 (can't just be non-zero data), or had no return data.
                or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())),
                // We use 100 because the length of our calldata totals up like so: 4 + 32 * 3.
                // We use 0 and 32 to copy up to 32 bytes of return data into the scratch space.
                // Counterintuitively, this call must be positioned second to the or() call in the
                // surrounding and() call or else returndatasize() will be zero during the computation.
                call(gas(), token, 0, freeMemoryPointer, 100, 0, 32)
            )
        }

        require(success, "TRANSFER_FROM_FAILED");
    }

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

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

            // Write the abi-encoded calldata into memory, beginning with the function selector.
            mstore(freeMemoryPointer, 0xa9059cbb00000000000000000000000000000000000000000000000000000000)
            mstore(add(freeMemoryPointer, 4), to) // Append the "to" argument.
            mstore(add(freeMemoryPointer, 36), amount) // Append the "amount" argument.

            success := and(
                // Set success to whether the call reverted, if not we check it either
                // returned exactly 1 (can't just be non-zero data), or had no return data.
                or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())),
                // We use 68 because the length of our calldata totals up like so: 4 + 32 * 2.
                // We use 0 and 32 to copy up to 32 bytes of return data into the scratch space.
                // Counterintuitively, this call must be positioned second to the or() call in the
                // surrounding and() call or else returndatasize() will be zero during the computation.
                call(gas(), token, 0, freeMemoryPointer, 68, 0, 32)
            )
        }

        require(success, "TRANSFER_FAILED");
    }

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

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

            // Write the abi-encoded calldata into memory, beginning with the function selector.
            mstore(freeMemoryPointer, 0x095ea7b300000000000000000000000000000000000000000000000000000000)
            mstore(add(freeMemoryPointer, 4), to) // Append the "to" argument.
            mstore(add(freeMemoryPointer, 36), amount) // Append the "amount" argument.

            success := and(
                // Set success to whether the call reverted, if not we check it either
                // returned exactly 1 (can't just be non-zero data), or had no return data.
                or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())),
                // We use 68 because the length of our calldata totals up like so: 4 + 32 * 2.
                // We use 0 and 32 to copy up to 32 bytes of return data into the scratch space.
                // Counterintuitively, this call must be positioned second to the or() call in the
                // surrounding and() call or else returndatasize() will be zero during the computation.
                call(gas(), token, 0, freeMemoryPointer, 68, 0, 32)
            )
        }

        require(success, "APPROVE_FAILED");
    }
}