// File: src/VEZTH.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import "@solmate/auth/Owned.sol";
import "@solmate/utils/ReentrancyGuard.sol";
import "@solmate/utils/SafeCastLib.sol";

import {IVotingEscrow} from "./interfaces/IVotingEscrow.sol";
import {IBlocklist} from "./interfaces/IBlocklist.sol";
import {SafeTransferLib, ERC20} from "@solmate/utils/SafeTransferLib.sol";

contract VEZTH is IVotingEscrow, Owned {
    using SafeCastLib for uint256;

    struct LockedBalance {
        uint128 amount;
        uint64 end;
        uint64 start;
    }

    struct Point {
        uint128 amount;
        uint128 index;
    }

    uint256 private constant _SCALE = 7 days;
    uint256 private constant _MAX_TIME = 365 days;
    uint256 private constant _MIN_TIME = 14 days;
    uint256 private constant _INCREASE = 4;

    // comming soon
    // bytes32 private constant _DELEGATION_TYPEHASH =
    // keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)");

    // Voting token
    string public constant name = "Vote Escrowed ZTH";
    string public constant symbol = "veZTH";
    uint8 public constant decimals = 18;
    ERC20 public immutable token;

    mapping(uint256 => Point) private _totalUnlockAtEpoch;
    mapping(address => mapping(uint256 => Point)) private _userUnlockAtEpoch;

    address public penaltyRecipient; // receives collected penalty payments
    uint256 public maxPenalty = 1e18; // penalty for quitters with MAXTIME remaining lock
    address public blocklist;
    mapping(address => LockedBalance) public locked;

    /**
     * @notice Initializes state
     * @param _penaltyRecipient The recipient of penalty paid by lock quitters
     * @param _token The token locked in order to obtain voting power
     */
    constructor(address _penaltyRecipient, address _token) Owned(msg.sender) {
        token = ERC20(_token);
        penaltyRecipient = _penaltyRecipient;
    }

    function lockEnd(address account) external view returns (uint256) {
        return locked[account].end;
    }

    /**
     * @dev Gets the current votes balance for `account`
     */
    function balanceOf(address account) external view returns (uint256) {
        return getVotes(account);
    }

    /**
     * @dev Gets the current total votes balance for all locked tokens.
     */
    function totalSupply() external view returns (uint256) {
        return _getVotes(_totalUnlockAtEpoch);
    }

    /**
     * @dev Gets the current votes balance for `account`
     */
    function getVotes(address account) public view returns (uint256) {
        return _getVotes(_userUnlockAtEpoch[account]);
    }

    function _getVotes(mapping(uint256 => Point) storage points) private view returns (uint256) {
        uint256 epoch = _getEpoch(block.timestamp); 
        uint256 max = _MAX_TIME / _SCALE;
        uint256 powers;
        //  max lookup is 52 times
        for (uint256 i = 1; i <= max; i++) {
            Point memory p = points[epoch + i];
            powers += uint256(p.index) - uint256(p.amount) * block.timestamp;
        }
        return (powers * _INCREASE) / _MAX_TIME;
    }

    function createLock(uint256 value, uint256 unlockTime) external checkBlocklist {
        _createLock(msg.sender, value, unlockTime);
    }

    function createLockWithPermit(uint256 value, uint256 unlockTime, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
        external
        checkBlocklist
    {
        // permit max
        ERC20(address(token)).permit(msg.sender, address(this), type(uint256).max, deadline, v, r, s);
        _createLock(msg.sender, value, unlockTime);
    }

    function _createLock(address account, uint256 value, uint256 unlockTime) internal {
        LockedBalance memory lock = locked[account];
        if (lock.amount > 0) revert WithdrawOldTokensFirst();
        if (value == 0) revert ZeroValue();
        if (unlockTime <  block.timestamp + _MIN_TIME) revert InsufficientLockTime();
        if (unlockTime > block.timestamp + _MAX_TIME) {
            revert ExceedsMaxLockTime();
        }

        // Transfer tokens
        SafeTransferLib.safeTransferFrom(token, msg.sender, address(this), value);

        locked[account] = LockedBalance({
            amount: value.safeCastTo128(),
            end: unlockTime.safeCastTo64(),
            start: block.timestamp.safeCastTo64()
        });
        uint256 epoch = _getEpoch(unlockTime);
        _writePoint(account, epoch, value, unlockTime, true);
        emit CreateLock(account, value, unlockTime);
    }

    function _writePoint(address account, uint256 epoch, uint256 value, uint256 unlocktime, bool isAdd) private {
        Point memory point = _userUnlockAtEpoch[account][epoch];
        Point memory totalPoint = _totalUnlockAtEpoch[epoch];

        if (isAdd) {
            uint128 delta = (value * unlocktime).safeCastTo128();
            uint128 v = value.safeCastTo128();
            point.amount += v;
            point.index += delta;
            totalPoint.amount += v;
            totalPoint.index += delta;
        } else {
            uint128 delta = (value * unlocktime).safeCastTo128();
            uint128 v = value.safeCastTo128();
            point.amount -= v;
            point.index -= delta;
            totalPoint.amount -= v;
            totalPoint.index -= delta;
        }
        _userUnlockAtEpoch[account][epoch] = point;
        _totalUnlockAtEpoch[epoch] = totalPoint;
    }

    function increaseAmount(uint256 value) external {
        LockedBalance memory lock = locked[msg.sender];
        if (value == 0) revert ZeroValue();
        if (lock.amount == 0) revert CreateLockFirst();
        if (lock.end <= block.timestamp) revert LockExpired();

        // Update state
        // Transfer tokens
        SafeTransferLib.safeTransferFrom(token, msg.sender, address(this), value);
        // Update state
        locked[msg.sender].amount = (lock.amount + value).safeCastTo128();

        uint256 epoch = _getEpoch(lock.end);
        _writePoint(msg.sender, epoch, value, lock.end, true);
        emit IncreaseAmount(msg.sender, value, lock.end);
    }

    function increaseUnlockTime(uint256 unlockTime) external {
        LockedBalance memory lock = locked[msg.sender];
        if (lock.amount == 0) revert CreateLockFirst();

        if (unlockTime > block.timestamp + _MAX_TIME) {
            revert ExceedsMaxLockTime();
        }
        if (unlockTime <= block.timestamp) revert InsufficientLockTime();
        if (unlockTime <= lock.end) revert InsufficientLockTime();

        // Update state
        locked[msg.sender].end = unlockTime.safeCastTo64();
        uint256 oldEpoch = _getEpoch(lock.end);
        uint256 epoch = _getEpoch(unlockTime);
        _writePoint(msg.sender, oldEpoch, lock.amount, lock.end, false);
        _writePoint(msg.sender, epoch, lock.amount, unlockTime, true);

        emit IncreaseUnlockTime(msg.sender, lock.amount, lock.end);
    }

    function withdraw() external {
        LockedBalance memory lock = locked[msg.sender];
        if (lock.amount == 0) revert MissingLock();
        if (lock.end > block.timestamp) revert LockNotExpired();

        // Update state
        locked[msg.sender] = LockedBalance(0, 0, 0);
        uint256 epoch = _getEpoch(lock.end);
        _writePoint(msg.sender, epoch, lock.amount, lock.end, false);

        // Transfer tokens
        SafeTransferLib.safeTransfer(token, msg.sender, lock.amount);

        emit Unlock(msg.sender, lock.amount, 0);
    }

    function quitLock() external {
        LockedBalance memory lock = locked[msg.sender];
        if (lock.amount == 0) revert MissingLock();
        if (lock.end <= block.timestamp) revert LockExpired();

        // Update state
        locked[msg.sender].amount = 0;
        locked[msg.sender].end = 0;

        uint256 epoch = _getEpoch(lock.end);
        _writePoint(msg.sender, epoch, lock.amount, lock.end, false);

        // Collect penalty
        uint256 penalty = (lock.amount * _calculatePenaltyRate(lock.end)) / 1e18;
        if (lock.amount < penalty) penalty = lock.amount;

        SafeTransferLib.safeTransfer(token, penaltyRecipient, penalty);
        SafeTransferLib.safeTransfer(token, msg.sender, lock.amount - penalty);
        emit Unlock(msg.sender, lock.amount, penalty);
    }

    /// @notice Returns the penalty rate for a given lock expiration
    /// @param end The lock's expiration
    /// @return The penalty rate applicable to the lock
    /// @dev The penalty rate decreases linearly at the same rate as a lock's voting power
    /// in order to compensate for votes unlocked without committing to the lock expiration
    function getPenaltyRate(uint256 end) external view returns (uint256) {
        if (end <= block.timestamp) return 0;
        return _calculatePenaltyRate(end);
    }

    // Calculate penalty rate
    // Penalty rate decreases linearly at the same rate as a lock's voting power
    // in order to compensate for votes used
    function _calculatePenaltyRate(uint256 end) internal view returns (uint256) {
        // We know that end > block.timestamp because expired locks cannot be quitted
        return ((end - block.timestamp) * maxPenalty) / _MAX_TIME;
    }

    function _getEpoch(uint256 unlockTime) private pure returns (uint256) {
        return unlockTime / _SCALE;
    }

    /// ~~~~~~~~~~~~~~~~~~~~~~~~~~~ ///
    ///       Owner Functions       ///
    /// ~~~~~~~~~~~~~~~~~~~~~~~~~~~ ///

    /// @notice Updates the blocklist contract
    function updateBlocklist(address _addr) external onlyOwner {
        blocklist = _addr;
        emit UpdateBlocklist(_addr);
    }

    /// @notice Updates the recipient of the accumulated penalty paid by quitters
    function updatePenaltyRecipient(address _addr) external onlyOwner {
        penaltyRecipient = _addr;
        emit UpdatePenaltyRecipient(_addr);
    }

    /**
     * @notice Updates the quitlock penalty
     * @dev use case:
     *  1. Removes quitlock penalty by setting it to zero.
     *  2. Migrat to a new  VotingEscrow contract.
     */
    function updatePenalty(uint256 penalty) external onlyOwner {
        maxPenalty = penalty;
        emit UpdatePenalty(penalty);
    }

    /// ~~~~~~~~~~~~~~~~~~~~~~~~~~~ ///
    ///      Disable ERC20 functions ///
    /// ~~~~~~~~~~~~~~~~~~~~~~~~~~~ ///
    function allowance(address, address) external pure returns (uint256) {
        return 0;
    }

    function approve(address, uint256) external pure returns (bool) {
        return false;
    }

    function transfer(address, uint256) external pure returns (bool) {
        revert("Disabled");
    }

    function transferFrom(address, address, uint256) external pure returns (bool) {
        revert("Disabled");
    }

    /**
     * ====== others
     */

    modifier checkBlocklist() {
        if (blocklist != address(0)) {
            require(!IBlocklist(blocklist).isBlocked(msg.sender), "Blocked contract");
        }
        _;
    }

    event UpdatePenalty(uint256 penalty);
    event UpdateBlocklist(address indexed blocklist);
    event UpdatePenaltyRecipient(address indexed recipient);
    event CollectPenalty(uint256 amount, address indexed recipient);
    event Unlock(address account, uint256 value, uint256 penalty);
    event CreateLock(address indexed account, uint256 value, uint256 unlockTime);
    event IncreaseUnlockTime(address account, uint256 value, uint256 unlockTime);
    event IncreaseAmount(address account, uint256 value, uint256 unlockTime);

    error LockExpired(); // lock expired
    error CreateLockFirst(); // create lock first
    error InsufficientLockTime(); // insufficient lock time
    error ExceedsMaxLockTime(); // exceeds max lock time
    error MissingLock(); // no lock found
    error LockNotExpired(); // lock not expired
    error OnlyBlocklist(); // only callable by the blocklist contract
    error WithdrawOldTokensFirst(); // Withdraw old tokens first
    error ZeroValue(); // Zero value
}

// File: lib/solmate/src/auth/Owned.sol



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

/// @notice Simple single owner authorization mixin.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/auth/Owned.sol)
abstract contract Owned {
    /*//////////////////////////////////////////////////////////////
                                 EVENTS
    //////////////////////////////////////////////////////////////*/

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

    /*//////////////////////////////////////////////////////////////
                            OWNERSHIP STORAGE
    //////////////////////////////////////////////////////////////*/

    address public owner;

    modifier onlyOwner() virtual {
        require(msg.sender == owner, "UNAUTHORIZED");

        _;
    }

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

    constructor(address _owner) {
        owner = _owner;

        emit OwnershipTransferred(address(0), _owner);
    }

    /*//////////////////////////////////////////////////////////////
                             OWNERSHIP LOGIC
    //////////////////////////////////////////////////////////////*/

    function transferOwnership(address newOwner) public virtual onlyOwner {
        owner = newOwner;

        emit OwnershipTransferred(msg.sender, newOwner);
    }
}

// File: lib/solmate/src/utils/ReentrancyGuard.sol



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

/// @notice Gas optimized reentrancy protection for smart contracts.
/// @author Solmate (https://github.com/transmissions11/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() virtual {
        require(locked == 1, "REENTRANCY");

        locked = 2;

        _;

        locked = 1;
    }
}

// File: lib/solmate/src/utils/SafeCastLib.sol



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

/// @notice Safe unsigned integer casting library that reverts on overflow.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/SafeCastLib.sol)
/// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeCast.sol)
library SafeCastLib {
    function safeCastTo248(uint256 x) internal pure returns (uint248 y) {
        require(x < 1 << 248);

        y = uint248(x);
    }

    function safeCastTo240(uint256 x) internal pure returns (uint240 y) {
        require(x < 1 << 240);

        y = uint240(x);
    }

    function safeCastTo232(uint256 x) internal pure returns (uint232 y) {
        require(x < 1 << 232);

        y = uint232(x);
    }

    function safeCastTo224(uint256 x) internal pure returns (uint224 y) {
        require(x < 1 << 224);

        y = uint224(x);
    }

    function safeCastTo216(uint256 x) internal pure returns (uint216 y) {
        require(x < 1 << 216);

        y = uint216(x);
    }

    function safeCastTo208(uint256 x) internal pure returns (uint208 y) {
        require(x < 1 << 208);

        y = uint208(x);
    }

    function safeCastTo200(uint256 x) internal pure returns (uint200 y) {
        require(x < 1 << 200);

        y = uint200(x);
    }

    function safeCastTo192(uint256 x) internal pure returns (uint192 y) {
        require(x < 1 << 192);

        y = uint192(x);
    }
    
    function safeCastTo184(uint256 x) internal pure returns (uint184 y) {
        require(x < 1 << 184);

        y = uint184(x);
    }

    function safeCastTo176(uint256 x) internal pure returns (uint176 y) {
        require(x < 1 << 176);

        y = uint176(x);
    }

    function safeCastTo168(uint256 x) internal pure returns (uint168 y) {
        require(x < 1 << 168);

        y = uint168(x);
    }

    function safeCastTo160(uint256 x) internal pure returns (uint160 y) {
        require(x < 1 << 160);

        y = uint160(x);
    }

    function safeCastTo152(uint256 x) internal pure returns (uint152 y) {
        require(x < 1 << 152);

        y = uint152(x);
    }

    function safeCastTo144(uint256 x) internal pure returns (uint144 y) {
        require(x < 1 << 144);

        y = uint144(x);
    }

    function safeCastTo136(uint256 x) internal pure returns (uint136 y) {
        require(x < 1 << 136);

        y = uint136(x);
    }

    function safeCastTo128(uint256 x) internal pure returns (uint128 y) {
        require(x < 1 << 128);

        y = uint128(x);
    }

    function safeCastTo120(uint256 x) internal pure returns (uint120 y) {
        require(x < 1 << 120);

        y = uint120(x);
    }

    function safeCastTo112(uint256 x) internal pure returns (uint112 y) {
        require(x < 1 << 112);

        y = uint112(x);
    }

    function safeCastTo104(uint256 x) internal pure returns (uint104 y) {
        require(x < 1 << 104);

        y = uint104(x);
    }

    function safeCastTo96(uint256 x) internal pure returns (uint96 y) {
        require(x < 1 << 96);

        y = uint96(x);
    }

    function safeCastTo88(uint256 x) internal pure returns (uint88 y) {
        require(x < 1 << 88);

        y = uint88(x);
    }

    function safeCastTo80(uint256 x) internal pure returns (uint80 y) {
        require(x < 1 << 80);

        y = uint80(x);
    }

    function safeCastTo72(uint256 x) internal pure returns (uint72 y) {
        require(x < 1 << 72);

        y = uint72(x);
    }

    function safeCastTo64(uint256 x) internal pure returns (uint64 y) {
        require(x < 1 << 64);

        y = uint64(x);
    }

    function safeCastTo56(uint256 x) internal pure returns (uint56 y) {
        require(x < 1 << 56);

        y = uint56(x);
    }

    function safeCastTo48(uint256 x) internal pure returns (uint48 y) {
        require(x < 1 << 48);

        y = uint48(x);
    }

    function safeCastTo40(uint256 x) internal pure returns (uint40 y) {
        require(x < 1 << 40);

        y = uint40(x);
    }

    function safeCastTo32(uint256 x) internal pure returns (uint32 y) {
        require(x < 1 << 32);

        y = uint32(x);
    }

    function safeCastTo24(uint256 x) internal pure returns (uint24 y) {
        require(x < 1 << 24);

        y = uint24(x);
    }

    function safeCastTo16(uint256 x) internal pure returns (uint16 y) {
        require(x < 1 << 16);

        y = uint16(x);
    }

    function safeCastTo8(uint256 x) internal pure returns (uint8 y) {
        require(x < 1 << 8);

        y = uint8(x);
    }
}

// File: src/interfaces/IVotingEscrow.sol



// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity 0.8.19;

import "forge-std/interfaces/IERC20.sol";

interface IVotingEscrow is IERC20 {

    /**
     * @notice Returns the vote power of `account`.
    */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @notice Returns the total vote power of all locked tokens.
    */
    function totalSupply() external view returns (uint256);
    /**
     * @dev Returns the current amount of votes that `account` has.
     * it is same with balanceOf(account)
     */
    function getVotes(address account) external view returns (uint256);

    /**
     * @notice Creates a new lock for `value` tokens that will unlock at `unlockTime`. 
     * @param value The number of tokens to be locked.
     * @param unlockTime The unix timestamp when the tokens will unlock.
     */
    function createLock(uint256 value, uint256 unlockTime) external;

    /**
     * @notice Creates a new lock for `value` tokens that will unlock at `unlockTime`,
     * and `account` can permit to spend.
     * @dev `deadline`, `v`, `r`, `s` are used for permit.
     * @param value The number of tokens to be locked.
     * @param unlockTime The unix timestamp when the tokens will unlock.
     * @param deadline The time at which to expire the signature.
     * @param v The recovery byte of the signature.
     * @param r Half of the ECDSA signature pair.
     * @param s Half of the ECDSA signature pair.
     */
    function createLockWithPermit(
        uint256 value,
        uint256 unlockTime,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external;

    /**
     * @notice Increase the amount of tokens that are locked.
     * @dev The lock must be exiting and not expired.
     * @param value The number of tokens to be increased.
     */
    function increaseAmount(uint256 value) external;

    /**
     * @notice Increase the unlock time of the lock.
     * @dev The lock must be exiting and not expired.
     * @param unlockTime The new unix timestamp when the tokens will unlock.
     */
    function increaseUnlockTime(uint256 unlockTime) external;

    /**
     * @notice Withdraws the tokens that have been unlocked.
     * @dev The lock must be expired.
     */
    function withdraw() external;

    /**
     * @notice Force withdraws the tokens that have been unlocked, and you will be charged a penalty.
     */
    function quitLock() external;

}

// File: src/interfaces/IBlocklist.sol



// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.19;

interface IBlocklist {
    function isBlocked(address addr) external view returns (bool);
}
// File: lib/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");
    }
}

// File: lib/forge-std/src/interfaces/IERC20.sol



// SPDX-License-Identifier: MIT
pragma solidity >=0.6.2;

/// @dev Interface of the ERC20 standard as defined in the EIP.
/// @dev This includes the optional name, symbol, and decimals metadata.
interface IERC20 {
    /// @dev Emitted when `value` tokens are moved from one account (`from`) to another (`to`).
    event Transfer(address indexed from, address indexed to, uint256 value);

    /// @dev Emitted when the allowance of a `spender` for an `owner` is set, where `value`
    /// is the new allowance.
    event Approval(address indexed owner, address indexed spender, uint256 value);

    /// @notice Returns the amount of tokens in existence.
    function totalSupply() external view returns (uint256);

    /// @notice Returns the amount of tokens owned by `account`.
    function balanceOf(address account) external view returns (uint256);

    /// @notice Moves `amount` tokens from the caller's account to `to`.
    function transfer(address to, uint256 amount) external returns (bool);

    /// @notice Returns the remaining number of tokens that `spender` is allowed
    /// to spend on behalf of `owner`
    function allowance(address owner, address spender) external view returns (uint256);

    /// @notice Sets `amount` as the allowance of `spender` over the caller's tokens.
    /// @dev Be aware of front-running risks: https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
    function approve(address spender, uint256 amount) external returns (bool);

    /// @notice Moves `amount` tokens from `from` to `to` using the allowance mechanism.
    /// `amount` is then deducted from the caller's allowance.
    function transferFrom(address from, address to, uint256 amount) external returns (bool);

    /// @notice Returns the name of the token.
    function name() external view returns (string memory);

    /// @notice Returns the symbol of the token.
    function symbol() external view returns (string memory);

    /// @notice Returns the decimals places of the token.
    function decimals() external view returns (uint8);
}

// File: lib/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);
    }
}