2024-03-12-Lynex-VotingEscrowV2Upgradeable-Version2-diff
620 lines
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT
pragma solidity 0.8.13;
pragma solidity 0.8.13;
import {IERC721EnumerableUpgradeable, ERC721EnumerableUpgradeable, IERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol";
import {IERC721EnumerableUpgradeable, ERC721EnumerableUpgradeable, IERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol";
import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol";
import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import {SafeERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
import {SafeERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
import {ERC5725Upgradeable} from "./erc5725/ERC5725Upgradeable.sol";
import {ERC5725Upgradeable} from "./erc5725/ERC5725Upgradeable.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {IVotingEscrowV2Upgradeable, IVotes} from "./interfaces/IVotingEscrowV2Upgradeable.sol";
import {IVotingEscrowV2Upgradeable, IVotes} from "./interfaces/IVotingEscrowV2Upgradeable.sol";
import {IVeArtProxy} from "../../interfaces/IVeArtProxy.sol";
import {IVeArtProxy} from "../../interfaces/IVeArtProxy.sol";
import {SafeCastLibrary} from "./libraries/SafeCastLibrary.sol";
import {SafeCastLibrary} from "./libraries/SafeCastLibrary.sol";
import {EscrowDelegateCheckpoints, Checkpoints} from "./libraries/EscrowDelegateCheckpoints.sol";
import {EscrowDelegateCheckpoints, Checkpoints} from "./libraries/EscrowDelegateCheckpoints.sol";
import {EscrowDelegateStorage} from "./libraries/EscrowDelegateStorage.sol";
import {EscrowDelegateStorage} from "./libraries/EscrowDelegateStorage.sol";
/**
/**
 * @title VotingEscrow
 * @title VotingEscrow
 * @dev This contract is used for locking tokens and voting.
 * @dev This contract is used for locking tokens and voting.
 *
 *
 * - tokenIds always have a delegatee, with the owner being the default (see createLock)
 * - tokenIds always have a delegatee, with the owner being the default (see createLock)
 * - On transfers, delegation is reset. (See _update)
 * - On transfers, delegation is reset. (See _update)
 * -
 * -
 */
 */
contract VotingEscrowV2Upgradeable is 
contract VotingEscrowV2Upgradeable is 
    Initializable, 
    Initializable, 
    IVotingEscrowV2Upgradeable, 
    IVotingEscrowV2Upgradeable, 
    ERC5725Upgradeable, 
    ERC5725Upgradeable, 
    EscrowDelegateStorage, 
    EscrowDelegateStorage, 
    EIP712Upgradeable, 
    EIP712Upgradeable, 
    ReentrancyGuard
    ReentrancyGuard
{
{
    using SafeERC20Upgradeable for IERC20Upgradeable;
    using SafeERC20Upgradeable for IERC20Upgradeable;
    using SafeCastLibrary for uint256;
    using SafeCastLibrary for uint256;
    using EscrowDelegateCheckpoints for EscrowDelegateCheckpoints.EscrowDelegateStore;
    using EscrowDelegateCheckpoints for EscrowDelegateCheckpoints.EscrowDelegateStore;
    enum DepositType {
    enum DepositType {
        DEPOSIT_FOR_TYPE,
        DEPOSIT_FOR_TYPE,
        CREATE_LOCK_TYPE,
        CREATE_LOCK_TYPE,
        INCREASE_LOCK_AMOUNT,
        INCREASE_LOCK_AMOUNT,
        INCREASE_UNLOCK_TIME,
        INCREASE_UNLOCK_TIME,
        MERGE_TYPE,
        MERGE_TYPE,
        SPLIT_TYPE
        SPLIT_TYPE
    }
    }
    /// @notice The token being locked
    /// @notice The token being locked
    IERC20Upgradeable public _token;
    IERC20Upgradeable public _token;
    /// @notice Total locked supply
    /// @notice Total locked supply
    uint256 public supply;
    uint256 public supply;
    uint8 public constant decimals = 18;
    uint8 public constant decimals = 18;
    address public artProxy;
    address public artProxy;
    /// @notice The EIP-712 typehash for the delegation struct used by the contract
    /// @notice The EIP-712 typehash for the delegation struct used by the contract
    bytes32 public constant DELEGATION_TYPEHASH =
    bytes32 public constant DELEGATION_TYPEHASH =
        keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)");
        keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)");
    /// @notice A record of states for signing / validating signatures
    /// @notice A record of states for signing / validating signatures
    mapping(address => uint256) public nonces;
    mapping(address => uint256) public nonces;
    /// @dev OpenZeppelin v5 IVotes error
    /// @dev OpenZeppelin v5 IVotes error
    error VotesExpiredSignature(uint256 expiry);
    error VotesExpiredSignature(uint256 expiry);
    /**
    /**
     * @notice The constructor is disabled for this upgradeable contract.
     * @notice The constructor is disabled for this upgradeable contract.
     */
     */
    constructor() {
    constructor() {
        /// @dev Disable the initializers for implementation contracts to ensure that the contract is not left uninitialized.
        /// @dev Disable the initializers for implementation contracts to ensure that the contract is not left uninitialized.
        _disableInitializers();
        _disableInitializers();
    }
    }
    /**
    /**
     * @dev Initializes the contract with the given parameters.
     * @dev Initializes the contract with the given parameters.
     * @param _name The name to set for the token.
     * @param _name The name to set for the token.
     * @param _symbol The symbol to set for the token.
     * @param _symbol The symbol to set for the token.
     * @param version The version of the contract.
     * @param version The version of the contract.
     * @param mainToken The main token address that will be locked in the escrow.
     * @param mainToken The main token address that will be locked in the escrow.
     * @param _artProxy The address of the art proxy contract.
     * @param _artProxy The address of the art proxy contract.
     */
     */
    function initialize(
    function initialize(
        string memory _name,
        string memory _name,
        string memory _symbol,
        string memory _symbol,
        string memory version,
        string memory version,
        IERC20Upgradeable mainToken,
        IERC20Upgradeable mainToken,
        address _artProxy
        address _artProxy
    ) public initializer {
    ) public initializer {
        __ERC5725_init(_name, _symbol);
        __ERC5725_init(_name, _symbol);
        __EIP712_init(_name, version);
        __EIP712_init(_name, version);
        _token = mainToken;
        _token = mainToken;
        artProxy = _artProxy;
        artProxy = _artProxy;
        // Reset MAX_TIME in proxy storage
        // Reset MAX_TIME in proxy storage
        MAX_TIME = uint256(uint128(EscrowDelegateCheckpoints.MAX_TIME));
        MAX_TIME = uint256(uint128(EscrowDelegateCheckpoints.MAX_TIME));
    }
    }
    modifier checkAuthorized(uint256 _tokenId) {
    modifier checkAuthorized(uint256 _tokenId) {
        address owner = _ownerOf(_tokenId);
        address owner = _ownerOf(_tokenId);
        if (owner == address(0)) {
        if (owner == address(0)) {
            revert ERC721NonexistentToken(_tokenId);
            revert ERC721NonexistentToken(_tokenId);
        }
        }
        address sender = _msgSender();
        address sender = _msgSender();
        if (!_isAuthorized(owner, sender, _tokenId)) {
        if (!_isAuthorized(owner, sender, _tokenId)) {
            revert ERC721InsufficientApproval(sender, _tokenId);
            revert ERC721InsufficientApproval(sender, _tokenId);
        }
        }
        _;
        _;
    }
    }
    /// @dev Returns current token URI metadata
    /// @dev Returns current token URI metadata
    /// @param _tokenId Token ID to fetch URI for.
    /// @param _tokenId Token ID to fetch URI for.
    function tokenURI(uint _tokenId) public view override validToken(_tokenId) returns (string memory) {
    function tokenURI(uint _tokenId) public view override validToken(_tokenId) returns (string memory) {
        LockDetails memory _locked = _lockDetails[_tokenId];
        LockDetails memory _locked = _lockDetails[_tokenId];
        return
        return
            IVeArtProxy(artProxy)._tokenURI(
            IVeArtProxy(artProxy)._tokenURI(
                _tokenId,
                _tokenId,
                balanceOfNFT(_tokenId),
                balanceOfNFT(_tokenId),
                _locked.endTime,
                _locked.endTime,
                uint(int256(_locked.amount))
                uint(int256(_locked.amount))
            );
            );
    }
    }
    /**
    /**
     * @dev See {IERC165-supportsInterface}.
     * @dev See {IERC165-supportsInterface}.
     */
     */
    function supportsInterface(
    function supportsInterface(
        bytes4 interfaceId
        bytes4 interfaceId
    ) public view virtual override(ERC5725Upgradeable, IERC165Upgradeable) returns (bool supported) {
    ) public view virtual override(ERC5725Upgradeable, IERC165Upgradeable) returns (bool supported) {
        return interfaceId == type(IVotingEscrowV2Upgradeable).interfaceId || super.supportsInterface(interfaceId);
        return interfaceId == type(IVotingEscrowV2Upgradeable).interfaceId || super.supportsInterface(interfaceId);
    }
    }
    /**
    /**
     * @dev See {IERC721-_beforeTokenTransfer}.
     * @dev See {IERC721-_beforeTokenTransfer}.
     * Clears the approval of a given `tokenId` when the token is transferred or burned.
     * Clears the approval of a given `tokenId` when the token is transferred or burned.
     */
     */
    function _beforeTokenTransfer(
    function _beforeTokenTransfer(
        address from,
        address from,
        address to,
        address to,
        uint256 firstTokenId,
        uint256 firstTokenId,
        uint256 batchSize
        uint256 batchSize
    ) internal virtual override {
    ) internal virtual override {
        super._beforeTokenTransfer(from, to, firstTokenId, batchSize);
        super._beforeTokenTransfer(from, to, firstTokenId, batchSize);
        for (uint256 i = 0; i < batchSize; i++) {
        for (uint256 i = 0; i < batchSize; i++) {
            uint256 tokenId = firstTokenId + i;
            uint256 tokenId = firstTokenId + i;
            if (from != to) {
            if (from != to) {
                /// @dev Sets delegatee to new owner on transfers
                /// @dev Sets delegatee to new owner on transfers
                (address oldDelegatee, address newDelegatee) = edStore.delegate(
                (address oldDelegatee, address newDelegatee) = edStore.delegate(
                    tokenId,
                    tokenId,
                    to,
                    to,
                    _lockDetails[tokenId].endTime
                    _lockDetails[tokenId].endTime
                );
                );
                emit DelegateChanged(to, oldDelegatee, newDelegatee);
                emit DelegateChanged(to, oldDelegatee, newDelegatee);
                emit LockDelegateChanged(tokenId, to, oldDelegatee, newDelegatee);
                emit LockDelegateChanged(tokenId, to, oldDelegatee, newDelegatee);
            }
            }
        }
        }
    }
    }
    /**
    /**
     * ERC-5725 and token-locking logic
     * ERC-5725 and token-locking logic
     */
     */
    /// @notice maps the vesting data with tokenIds
    /// @notice maps the vesting data with tokenIds
    mapping(uint256 => LockDetails) public _lockDetails;
    mapping(uint256 => LockDetails) public _lockDetails;
    /// @notice tracker of current NFT id
    /// @notice tracker of current NFT id
    uint256 public totalNftsMinted = 0;
    uint256 public totalNftsMinted = 0;
    /**
    /**
     * @notice Creates a new vesting NFT and mints it
     * @notice Creates a new vesting NFT and mints it
     * @dev Token amount should be approved to be transferred by this contract before executing create
     * @dev Token amount should be approved to be transferred by this contract before executing create
     * @param value The total assets to be locked over time
     * @param value The total assets to be locked over time
     * @param duration Duration in seconds of the lock
     * @param duration Duration in seconds of the lock
     * @param to The receiver of the lock
     * @param to The receiver of the lock
     */
     */
    function _createLock(
    function _createLock(
        uint256 value,
        uint256 value,
        uint256 duration,
        uint256 duration,
        address to,
        address to,
        address delegatee,
        address delegatee,
        bool permanent,
        bool permanent,
        DepositType depositType
        DepositType depositType
    ) internal virtual returns (uint256) {
    ) internal virtual returns (uint256) {
        if (value == 0) revert ZeroAmount();
        if (value == 0) revert ZeroAmount();
        uint256 unlockTime;
        uint256 unlockTime;
        totalNftsMinted++;
        totalNftsMinted++;
        uint256 newTokenId = totalNftsMinted;
        uint256 newTokenId = totalNftsMinted;
        if (!permanent) {
        if (!permanent) {
            unlockTime = toGlobalClock(block.timestamp + duration); // Locktime is rounded down to global clock (days)
            unlockTime = toGlobalClock(block.timestamp + duration); // Locktime is rounded down to global clock (days)
            if (unlockTime <= block.timestamp) revert LockDurationNotInFuture();
            if (unlockTime <= block.timestamp) revert LockDurationNotInFuture();
            if (unlockTime > block.timestamp + MAX_TIME) revert LockDurationTooLong();
            if (unlockTime > block.timestamp + MAX_TIME) revert LockDurationTooLong();
        }
        }
        _safeMint(to, newTokenId);
        _safeMint(to, newTokenId);
        _lockDetails[newTokenId].startTime = block.timestamp;
        _lockDetails[newTokenId].startTime = block.timestamp;
        /// @dev Checkpoint created in _updateLock
        /// @dev Checkpoint created in _updateLock
        _updateLock(newTokenId, value, unlockTime, _lockDetails[newTokenId], permanent, depositType);
        _updateLock(newTokenId, value, unlockTime, _lockDetails[newTokenId], permanent, depositType);
        edStore.delegate(newTokenId, delegatee, unlockTime);
        edStore.delegate(newTokenId, delegatee, unlockTime);
        emit LockCreated(newTokenId, delegatee, value, unlockTime, permanent);
        emit LockCreated(newTokenId, delegatee, value, unlockTime, permanent);
        emit DelegateChanged(to, address(0), delegatee);
        emit DelegateChanged(to, address(0), delegatee);
        emit LockDelegateChanged(newTokenId, to, address(0), delegatee);
        emit LockDelegateChanged(newTokenId, to, address(0), delegatee);
        return newTokenId;
        return newTokenId;
    }
    }
    /**
    /**
     * @notice Creates a lock for the sender
     * @notice Creates a lock for the sender
     * @param _value The total assets to be locked over time
     * @param _value The total assets to be locked over time
     * @param _lockDuration Duration in seconds of the lock
     * @param _lockDuration Duration in seconds of the lock
     * @param _permanent Whether the lock is permanent or not
     * @param _permanent Whether the lock is permanent or not
     * @return The id of the newly created token
     * @return The id of the newly created token
     */
     */
    function createLock(
    function createLock(
        uint256 _value,
        uint256 _value,
        uint256 _lockDuration,
        uint256 _lockDuration,
        bool _permanent
        bool _permanent
    ) external nonReentrant returns (uint256) {
    ) external nonReentrant returns (uint256) {
        return _createLock(_value, _lockDuration, _msgSender(), _msgSender(), _permanent, DepositType.CREATE_LOCK_TYPE);
        return _createLock(_value, _lockDuration, _msgSender(), _msgSender(), _permanent, DepositType.CREATE_LOCK_TYPE);
    }
    }
    /**
    /**
     * @notice Creates a lock for a specified address
     * @notice Creates a lock for a specified address
     * @param _value The total assets to be locked over time
     * @param _value The total assets to be locked over time
     * @param _lockDuration Duration in seconds of the lock
     * @param _lockDuration Duration in seconds of the lock
     * @param _to The receiver of the lock
     * @param _to The receiver of the lock
     * @param _permanent Whether the lock is permanent or not
     * @param _permanent Whether the lock is permanent or not
     * @return The id of the newly created token
     * @return The id of the newly created token
     */
     */
    function createLockFor(
    function createLockFor(
        uint256 _value,
        uint256 _value,
        uint256 _lockDuration,
        uint256 _lockDuration,
        address _to,
        address _to,
        bool _permanent
        bool _permanent
    ) external nonReentrant returns (uint256) {
    ) external nonReentrant returns (uint256) {
        return _createLock(_value, _lockDuration, _to, _to, _permanent, DepositType.CREATE_LOCK_TYPE);
        return _createLock(_value, _lockDuration, _to, _to, _permanent, DepositType.CREATE_LOCK_TYPE);
    }
    }
    /**
    /**
     * @notice Creates a lock for a specified address
     * @notice Creates a lock for a specified address
     * @param _value The total assets to be locked over time
     * @param _value The total assets to be locked over time
     * @param _lockDuration Duration in seconds of the lock
     * @param _lockDuration Duration in seconds of the lock
     * @param _to The receiver of the lock
     * @param _to The receiver of the lock
     * @param _delegatee The receiver of the lock
     * @param _delegatee The receiver of the lock
     * @param _permanent Whether the lock is permanent or not
     * @param _permanent Whether the lock is permanent or not
     * @return The id of the newly created token
     * @return The id of the newly created token
     */
     */
    function createDelegatedLockFor(
    function createDelegatedLockFor(
        uint256 _value,
        uint256 _value,
        uint256 _lockDuration,
        uint256 _lockDuration,
        address _to,
        address _to,
        address _delegatee,
        address _delegatee,
        bool _permanent
        bool _permanent
    ) external nonReentrant returns (uint256) {
    ) external nonReentrant returns (uint256) {
        return _createLock(_value, _lockDuration, _to, _delegatee, _permanent, DepositType.CREATE_LOCK_TYPE);
        return _createLock(_value, _lockDuration, _to, _delegatee, _permanent, DepositType.CREATE_LOCK_TYPE);
    }
    }
    /**
    /**
     * @notice Updates the global checkpoint
     * @notice Updates the global checkpoint
     */
     */
    function globalCheckpoint() public nonReentrant {
    function globalCheckpoint() public nonReentrant {
        return edStore.globalCheckpoint();
        return edStore.globalCheckpoint();
    }
    }
    function checkpoint() external override {
    function checkpoint() external override {
        globalCheckpoint();
        globalCheckpoint();
    }
    }
    /**
    /**
     * @notice Updates the checkpoint for a delegatee
     * @notice Updates the checkpoint for a delegatee
     * @param _delegateeAddress The address of the delegatee
     * @param _delegateeAddress The address of the delegatee
     */
     */
    function checkpointDelegatee(address _delegateeAddress) external nonReentrant {
    function checkpointDelegatee(address _delegateeAddress) external nonReentrant {
        edStore.baseCheckpointDelegatee(_delegateeAddress);
        edStore.baseCheckpointDelegatee(_delegateeAddress);
    }
    }
    /// @notice Deposit & update lock tokens for a user
    /// @notice Deposit & update lock tokens for a user
    /// @dev The supply is increased by the _value amount
    /// @dev The supply is increased by the _value amount
    /// @param _tokenId NFT that holds lock
    /// @param _tokenId NFT that holds lock
    /// @param _increasedValue Amount to deposit
    /// @param _increasedValue Amount to deposit
    /// @param _unlockTime New time when to unlock the tokens, or 0 if unchanged
    /// @param _unlockTime New time when to unlock the tokens, or 0 if unchanged
    /// @param _oldLocked Previous locked amount / timestamp
    /// @param _oldLocked Previous locked amount / timestamp
    function _updateLock(
    function _updateLock(
        uint256 _tokenId,
        uint256 _tokenId,
        uint256 _increasedValue,
        uint256 _increasedValue,
        uint256 _unlockTime,
        uint256 _unlockTime,
        LockDetails memory _oldLocked,
        LockDetails memory _oldLocked,
        bool isPermanent,
        bool isPermanent,
        DepositType depositType
        DepositType depositType
    ) internal {
    ) internal {
        uint256 supplyBefore = supply;
        uint256 supplyBefore = supply;
        supply += _increasedValue;
        supply += _increasedValue;
        // Set newLocked to _oldLocked without mangling memory
        // Set newLocked to _oldLocked without mangling memory
        LockDetails memory newLocked;
        LockDetails memory newLocked;
        (newLocked.amount, newLocked.startTime, newLocked.endTime, newLocked.isPermanent) = (
        (newLocked.amount, newLocked.startTime, newLocked.endTime, newLocked.isPermanent) = (
            _oldLocked.amount,
            _oldLocked.amount,
            _oldLocked.startTime,
            _oldLocked.startTime,
            _oldLocked.endTime,
            _oldLocked.endTime,
            _oldLocked.isPermanent
            _oldLocked.isPermanent
        );
        );
        // Adding to existing lock, or if a lock is expired - creating a new one
        // Adding to existing lock, or if a lock is expired - creating a new one
        newLocked.amount += _increasedValue;
        newLocked.amount += _increasedValue;
        if (_unlockTime != 0 && !isPermanent) {
        if (_unlockTime != 0 && !isPermanent) {
            newLocked.endTime = _unlockTime;
            newLocked.endTime = _unlockTime;
        }
        }
        if (isPermanent) {
        if (isPermanent) {
            newLocked.endTime = 0;
            newLocked.endTime = 0;
            newLocked.isPermanent = true;
            newLocked.isPermanent = true;
        }
        }
        _lockDetails[_tokenId] = newLocked;
        _lockDetails[_tokenId] = newLocked;
        emit LockUpdated(_tokenId, _increasedValue, _unlockTime, isPermanent);
        emit LockUpdated(_tokenId, _increasedValue, _unlockTime, isPermanent);
        // Possibilities:
        // Possibilities:
        // Both _oldLocked.end could be current or expired (>/< block.timestamp)
        // Both _oldLocked.end could be current or expired (>/< block.timestamp)
        // or if the lock is a permanent lock, then _oldLocked.end == 0
        // or if the lock is a permanent lock, then _oldLocked.end == 0
        // value == 0 (extend lock) or value > 0 (add to lock or extend lock)
        // value == 0 (extend lock) or value > 0 (add to lock or extend lock)
        // newLocked.end > block.timestamp (always)
        // newLocked.end > block.timestamp (always)
        _checkpointLock(_tokenId, _oldLocked, newLocked);
        _checkpointLock(_tokenId, _oldLocked, newLocked);
        if (_increasedValue != 0 && depositType != DepositType.SPLIT_TYPE) {
        if (_increasedValue != 0 && depositType != DepositType.SPLIT_TYPE) {
            _token.safeTransferFrom(_msgSender(), address(this), _increasedValue);
            _token.safeTransferFrom(_msgSender(), address(this), _increasedValue);
        }
        }
        emit SupplyUpdated(supply, supplyBefore + _increasedValue);
        emit SupplyUpdated(supply, supplyBefore + _increasedValue);
    }
    }
    /// @notice Record global and per-user data to checkpoints. Used by VotingEscrow system.
    /// @notice Record global and per-user data to checkpoints. Used by VotingEscrow system.
    /// @param _tokenId NFT token ID. No user checkpoint if 0
    /// @param _tokenId NFT token ID. No user checkpoint if 0
    /// @param _oldLocked Previous locked amount / end lock time for the user
    /// @param _oldLocked Previous locked amount / end lock time for the user
    /// @param _newLocked New locked amount / end lock time for the user
    /// @param _newLocked New locked amount / end lock time for the user
    function _checkpointLock(
    function _checkpointLock(
        uint256 _tokenId,
        uint256 _tokenId,
        IVotingEscrowV2Upgradeable.LockDetails memory _oldLocked,
        IVotingEscrowV2Upgradeable.LockDetails memory _oldLocked,
        IVotingEscrowV2Upgradeable.LockDetails memory _newLocked
        IVotingEscrowV2Upgradeable.LockDetails memory _newLocked
    ) internal {
    ) internal {
        edStore.checkpoint(
        edStore.checkpoint(
            _tokenId,
            _tokenId,
            _oldLocked.amount.toInt128(),
            _oldLocked.amount.toInt128(),
            _newLocked.amount.toInt128(),
            _newLocked.amount.toInt128(),
            _oldLocked.endTime,
            _oldLocked.endTime,
            _newLocked.endTime
            _newLocked.endTime
        );
        );
    }
    }
    /// @notice Deposit `_value` tokens for `_tokenId` and add to the lock
    /// @notice Deposit `_value` tokens for `_tokenId` and add to the lock
    /// @dev Anyone (even a smart contract) can deposit for someone else, but
    /// @dev Anyone (even a smart contract) can deposit for someone else, but
    ///      cannot extend their locktime and deposit for a brand new user
    ///      cannot extend their locktime and deposit for a brand new user
    /// @param _tokenId lock NFT
    /// @param _tokenId lock NFT
    /// @param _value Amount to add to user's lock
    /// @param _value Amount to add to user's lock
    function increaseAmount(uint256 _tokenId, uint256 _value) external nonReentrant {
    function increaseAmount(uint256 _tokenId, uint256 _value) external nonReentrant {
        if (_value == 0) revert ZeroAmount();
        if (_value == 0) revert ZeroAmount();
        IVotingEscrowV2Upgradeable.LockDetails memory oldLocked = _lockDetails[_tokenId];
        IVotingEscrowV2Upgradeable.LockDetails memory oldLocked = _lockDetails[_tokenId];
        if (oldLocked.amount <= 0) revert NoLockFound();
        if (_ownerOf(_tokenId) == address(0)) revert NoLockFound();
        if (oldLocked.endTime <= block.timestamp && !oldLocked.isPermanent) revert LockExpired();
        if (oldLocked.endTime <= block.timestamp && !oldLocked.isPermanent) revert LockExpired();
        _updateLock(_tokenId, _value, 0, oldLocked, oldLocked.isPermanent, DepositType.INCREASE_LOCK_AMOUNT);
        _updateLock(_tokenId, _value, 0, oldLocked, oldLocked.isPermanent, DepositType.INCREASE_LOCK_AMOUNT);
    }
    }
    /**
    /**
     * @notice Increases the unlock time of a lock
     * @notice Increases the unlock time of a lock
     * @param _tokenId The id of the token to increase the unlock time for
     * @param _tokenId The id of the token to increase the unlock time for
     * @param _lockDuration The new duration of the lock
     * @param _lockDuration The new duration of the lock
     * @param _permanent Whether the lock is permanent or not
     * @param _permanent Whether the lock is permanent or not
     */
     */
    function increaseUnlockTime(
    function increaseUnlockTime(
        uint256 _tokenId,
        uint256 _tokenId,
        uint256 _lockDuration,
        uint256 _lockDuration,
        bool _permanent
        bool _permanent
    ) external nonReentrant checkAuthorized(_tokenId) {
    ) external nonReentrant checkAuthorized(_tokenId) {
        LockDetails memory oldLocked = _lockDetails[_tokenId];
        LockDetails memory oldLocked = _lockDetails[_tokenId];
        if (oldLocked.isPermanent) revert PermanentLock();
        if (oldLocked.isPermanent) revert PermanentLock();
        uint256 unlockTime;
        uint256 unlockTime;
        if (!_permanent) {
        if (!_permanent) {
            unlockTime = toGlobalClock(block.timestamp + _lockDuration);
            unlockTime = toGlobalClock(block.timestamp + _lockDuration);
            // Locktime is rounded down to global clock (days)
            // Locktime is rounded down to global clock (days)
            if (oldLocked.endTime <= block.timestamp) revert LockExpired();
            if (oldLocked.endTime <= block.timestamp) revert LockExpired();
            if (unlockTime <= oldLocked.endTime) revert LockDurationNotInFuture();
            if (unlockTime <= oldLocked.endTime) revert LockDurationNotInFuture();
            if (unlockTime > block.timestamp + MAX_TIME) revert LockDurationTooLong();
            if (unlockTime > block.timestamp + MAX_TIME) revert LockDurationTooLong();
        }
        }
        _updateLock(_tokenId, 0, unlockTime, oldLocked, _permanent, DepositType.INCREASE_UNLOCK_TIME);
        _updateLock(_tokenId, 0, unlockTime, oldLocked, _permanent, DepositType.INCREASE_UNLOCK_TIME);
        emit LockDurationExtended(_tokenId, unlockTime, _permanent);
        emit LockDurationExtended(_tokenId, unlockTime, _permanent);
    }
    }
    /**
    /**
     * @notice Unlocks a permanent lock
     * @notice Unlocks a permanent lock
     * @param _tokenId The id of the token to unlock
     * @param _tokenId The id of the token to unlock
     */
     */
    function unlockPermanent(uint256 _tokenId) external nonReentrant checkAuthorized(_tokenId) {
    function unlockPermanent(uint256 _tokenId) external nonReentrant checkAuthorized(_tokenId) {
        LockDetails memory newLocked = _lockDetails[_tokenId];
        LockDetails memory newLocked = _lockDetails[_tokenId];
        if (!newLocked.isPermanent) revert NotPermanentLock();
        if (!newLocked.isPermanent) revert NotPermanentLock();
        // Set the end time to the maximum possible time
        // Set the end time to the maximum possible time
        newLocked.endTime = toGlobalClock(block.timestamp + MAX_TIME);
        newLocked.endTime = toGlobalClock(block.timestamp + MAX_TIME);
        // Set the lock to not be permanent
        // Set the lock to not be permanent
        newLocked.isPermanent = false;
        newLocked.isPermanent = false;
        // Update the lock details
        // Update the lock details
        _checkpointLock(_tokenId, _lockDetails[_tokenId], newLocked);
        _checkpointLock(_tokenId, _lockDetails[_tokenId], newLocked);
        _lockDetails[_tokenId] = newLocked;
        _lockDetails[_tokenId] = newLocked;
        emit UnlockPermanent(_tokenId, _msgSender(), newLocked.endTime);
        emit UnlockPermanent(_tokenId, _msgSender(), newLocked.endTime);
    }
    }
    /**
    /**
     * @notice Claims the payout for a token
     * @notice Claims the payout for a token
     * @param _tokenId The id of the token to claim the payout for
     * @param _tokenId The id of the token to claim the payout for
     */
     */
    function _claim(uint256 _tokenId) internal validToken(_tokenId) nonReentrant checkAuthorized(_tokenId) {
    function _claim(uint256 _tokenId) internal validToken(_tokenId) nonReentrant checkAuthorized(_tokenId) {
        IVotingEscrowV2Upgradeable.LockDetails memory oldLocked = _lockDetails[_tokenId];
        IVotingEscrowV2Upgradeable.LockDetails memory oldLocked = _lockDetails[_tokenId];
        if (oldLocked.isPermanent) revert PermanentLock();
        if (oldLocked.isPermanent) revert PermanentLock();
        uint256 amountClaimed = claimablePayout(_tokenId);
        uint256 amountClaimed = claimablePayout(_tokenId);
        if (amountClaimed == 0) revert LockNotExpired();
        if (amountClaimed == 0) revert LockNotExpired();
        // Burn the NFT
        _burn(_tokenId);
        // Reset the lock details
        // Reset the lock details
        _lockDetails[_tokenId] = IVotingEscrowV2Upgradeable.LockDetails(0, 0, 0, false);
        _lockDetails[_tokenId] = IVotingEscrowV2Upgradeable.LockDetails(0, 0, 0, false);
        // Update the total supply
        // Update the total supply
        uint256 supplyBefore = supply;
        uint256 supplyBefore = supply;
        supply -= amountClaimed;
        supply -= amountClaimed;
        // Update the lock details
        // Update the lock details
        _checkpointLock(_tokenId, oldLocked, _lockDetails[_tokenId]);
        _checkpointLock(_tokenId, oldLocked, _lockDetails[_tokenId]);
        /// @notice ERC-5725 event
        /// @notice ERC-5725 event
        emit PayoutClaimed(_tokenId, msg.sender, amountClaimed);
        emit PayoutClaimed(_tokenId, msg.sender, amountClaimed);
        // IERC5725 - Update the total amount claimed
        // IERC5725 - Update the total amount claimed
        _payoutClaimed[_tokenId] += amountClaimed;
        _payoutClaimed[_tokenId] += amountClaimed;
        // Transfer the claimed amount to the sender
        // Transfer the claimed amount to the sender
        IERC20Upgradeable(_payoutToken(_tokenId)).safeTransfer(msg.sender, amountClaimed);
        IERC20Upgradeable(_payoutToken(_tokenId)).safeTransfer(msg.sender, amountClaimed);
        emit SupplyUpdated(supplyBefore, supply);
        emit SupplyUpdated(supplyBefore, supply);
    }
    }
    /**
    /**
     * @notice Claims the payout for a token
     * @notice Claims the payout for a token
     * @param _tokenId The id of the token to claim the payout for
     * @param _tokenId The id of the token to claim the payout for
     */
     */
    function claim(uint256 _tokenId) external override(ERC5725Upgradeable) {
    function claim(uint256 _tokenId) external override(ERC5725Upgradeable) {
        _claim(_tokenId);
        _claim(_tokenId);
    }
    }
    /**
    /**
     * @notice Merges two tokens together
     * @notice Merges two tokens together
     * @param _from The id of the token to merge from
     * @param _from The id of the token to merge from
     * @param _to The id of the token to merge to
     * @param _to The id of the token to merge to
     */
     */
    function merge(uint256 _from, uint256 _to) external nonReentrant checkAuthorized(_from) checkAuthorized(_to) {
    function merge(uint256 _from, uint256 _to) external nonReentrant checkAuthorized(_from) checkAuthorized(_to) {
        if (_from == _to) revert SameNFT();
        if (_from == _to) revert SameNFT();
        IVotingEscrowV2Upgradeable.LockDetails memory oldLockedTo = _lockDetails[_to];
        IVotingEscrowV2Upgradeable.LockDetails memory oldLockedTo = _lockDetails[_to];
        if (oldLockedTo.amount == 0) revert ZeroAmount();
        if (oldLockedTo.endTime <= block.timestamp && !oldLockedTo.isPermanent) revert LockExpired();
        if (oldLockedTo.endTime <= block.timestamp && !oldLockedTo.isPermanent) revert LockExpired();
        IVotingEscrowV2Upgradeable.LockDetails memory oldLockedFrom = _lockDetails[_from];
        IVotingEscrowV2Upgradeable.LockDetails memory oldLockedFrom = _lockDetails[_from];
        if (oldLockedFrom.isPermanent != oldLockedTo.isPermanent) revert PermanentLockMismatch();
        if (oldLockedFrom.amount == 0) revert ZeroAmount();
        if (oldLockedFrom.isPermanent == true && oldLockedFrom.isPermanent != oldLockedTo.isPermanent) revert PermanentLockMismatch();
        // Calculate the new end time
        // Calculate the new end time
        uint256 end = oldLockedFrom.endTime >= oldLockedTo.endTime ? oldLockedFrom.endTime : oldLockedTo.endTime;
        uint256 end = oldLockedFrom.endTime >= oldLockedTo.endTime ? oldLockedFrom.endTime : oldLockedTo.endTime;
        // Burn the token being merged from
        // Set lock amount to 0
        _burn(_from);
        _lockDetails[_from].amount = 0;
        // Reset the lock details
        _lockDetails[_from] = LockDetails(0, 0, 0, false);
        // Update the lock details
        // Update the lock details
        _checkpointLock(_from, oldLockedFrom, _lockDetails[_from]);
        _checkpointLock(_from, oldLockedFrom, _lockDetails[_from]);
        // Calculate the new lock details
        // Calculate the new lock details
        LockDetails memory newLockedTo;
        LockDetails memory newLockedTo;
        newLockedTo.amount = oldLockedTo.amount + oldLockedFrom.amount;
        newLockedTo.amount = oldLockedTo.amount + oldLockedFrom.amount;
        newLockedTo.isPermanent = oldLockedTo.isPermanent;
        newLockedTo.isPermanent = oldLockedTo.isPermanent;
        if (!newLockedTo.isPermanent) {
        if (!newLockedTo.isPermanent) {
            newLockedTo.endTime = end;
            newLockedTo.endTime = end;
        }
        }
        // Update the lock details
        // Update the lock details
        _checkpointLock(_to, oldLockedTo, newLockedTo);
        _checkpointLock(_to, oldLockedTo, newLockedTo);
        _lockDetails[_to] = newLockedTo;
        _lockDetails[_to] = newLockedTo;
        emit LockMerged(_from, _to, newLockedTo.amount, end, newLockedTo.isPermanent);
        emit LockMerged(_from, _to, newLockedTo.amount, end, newLockedTo.isPermanent);
    }
    }
    /**
    /**
     * @notice Splits a token into multiple tokens
     * @notice Splits a token into multiple tokens
     * @param _weights The percentages to split the token into
     * @param _weights The percentages to split the token into
     * @param _tokenId The id of the token to split
     * @param _tokenId The id of the token to split
     */
     */
    function split(uint256[] memory _weights, uint256 _tokenId) external nonReentrant checkAuthorized(_tokenId) {
    function split(uint256[] memory _weights, uint256 _tokenId) external nonReentrant checkAuthorized(_tokenId) {
        LockDetails memory locked = _lockDetails[_tokenId];
        LockDetails memory locked = _lockDetails[_tokenId];
        LockDetails storage lockedStorage = _lockDetails[_tokenId];
        uint256 currentTime = block.timestamp;
        uint256 currentTime = block.timestamp;
        /// @dev Pulling directly from locked struct to avoid stack-too-deep
        /// @dev Pulling directly from locked struct to avoid stack-too-deep
        if (locked.endTime <= currentTime && !locked.isPermanent) revert LockExpired();
        if (locked.endTime <= currentTime && !locked.isPermanent) revert LockExpired();
        if (locked.amount == 0 || _weights.length < 2) revert ZeroAmount();
        if (locked.amount == 0 || _weights.length < 2) revert ZeroAmount();
        // reset supply, _deposit_for increase it
        // reset supply, _deposit_for increase it
        supply -= uint256(int256(locked.amount));
        supply -= uint256(int256(locked.amount));
        // Capture owner for split
        // Capture owner for split
        address owner = _ownerOf(_tokenId);
        address owner = _ownerOf(_tokenId);
        uint256 totalWeight = 0;
        uint256 totalWeight = 0;
        for (uint256 i = 0; i < _weights.length; i++) {
        for (uint256 i = 0; i < _weights.length; i++) {
            totalWeight += _weights[i];
            totalWeight += _weights[i];
        }
        }
        // remove old data
        _lockDetails[_tokenId] = LockDetails(0, 0, 0, false);
        _checkpointLock(_tokenId, locked, _lockDetails[_tokenId]);
        _burn(_tokenId);
        uint256 duration = locked.isPermanent ? 0 : locked.endTime > currentTime ? locked.endTime - currentTime : 0;
        uint256 duration = locked.isPermanent ? 0 : locked.endTime > currentTime ? locked.endTime - currentTime : 0;
        uint256 amountLeftToSplit = locked.amount;
        uint256 amountLeftToSplit = locked.amount;
        for (uint256 i = 0; i < _weights.length; i++) {
        for (uint256 i = 0; i < _weights.length; i++) {
            uint256 value = (uint256(int256(locked.amount)) * _weights[i]) / totalWeight;
            uint256 value = (uint256(int256(locked.amount)) * _weights[i]) / totalWeight;
            if(i == _weights.length - 1) {
            if(i == _weights.length - 1) {
                /// @dev Ensure no rounding errors occur by passing the remainder to the last split
                /// @dev Ensure no rounding errors occur by passing the remainder to the last split
                value = amountLeftToSplit;
                value = amountLeftToSplit;
            } 
            } 
            amountLeftToSplit -= value;
            amountLeftToSplit -= value;
            _createLock(value, duration, owner, owner, locked.isPermanent, DepositType.SPLIT_TYPE);
            if (i == 0) {
                lockedStorage.amount = value;
                _checkpointLock(_tokenId, locked, lockedStorage);
            } else { 
                _createLock(value, duration, owner, owner, locked.isPermanent, DepositType.SPLIT_TYPE);
            }
        }
        }
        emit LockSplit(_weights, _tokenId);
        emit LockSplit(_weights, _tokenId);
    }
    }
    /**
     * @notice Burns a token
     * @param _tokenId The ids of the tokens to burn
     */
    function burn(uint256 _tokenId) external {
        if (_ownerOf(_tokenId) != _msgSender()) revert NotLockOwner();
        if(_lockDetails[_tokenId].amount > 0) revert LockHoldsValue();
        _burn(_tokenId);
    }
    /*///////////////////////////////////////////////////////////////
    /*///////////////////////////////////////////////////////////////
                           GAUGE REWARDS LOGIC
                           GAUGE REWARDS LOGIC
    //////////////////////////////////////////////////////////////*/
    //////////////////////////////////////////////////////////////*/
    function balanceOfNFT(uint256 _tokenId) public view returns (uint256) {
    function balanceOfNFT(uint256 _tokenId) public view returns (uint256) {
        return edStore.getAdjustedEscrowBias(_tokenId, block.timestamp);
        return edStore.getAdjustedEscrowBias(_tokenId, block.timestamp);
    }
    }
    function balanceOfNFTAt(uint256 _tokenId, uint256 _timestamp) external view returns (uint256) {
    function balanceOfNFTAt(uint256 _tokenId, uint256 _timestamp) external view returns (uint256) {
        return edStore.getAdjustedEscrowBias(_tokenId, _timestamp);
        return edStore.getAdjustedEscrowBias(_tokenId, _timestamp);
    }
    }
    function getPastEscrowPoint(
    function getPastEscrowPoint(
        uint256 _tokenId,
        uint256 _tokenId,
        uint256 _timestamp
        uint256 _timestamp
    ) external view override returns (Checkpoints.Point memory, uint48) {
    ) external view override returns (Checkpoints.Point memory, uint48) {
        return edStore.getAdjustedEscrow(_tokenId, _timestamp);
        return edStore.getAdjustedEscrow(_tokenId, _timestamp);
    }
    }
    function getFirstEscrowPoint(uint256 _tokenId) external view override returns (Checkpoints.Point memory, uint48) {
    function getFirstEscrowPoint(uint256 _tokenId) external view override returns (Checkpoints.Point memory, uint48) {
        return edStore.getFirstEscrowPoint(_tokenId);
        return edStore.getFirstEscrowPoint(_tokenId);
    }
    }
    function totalSupply() public view override(ERC721EnumerableUpgradeable, IERC721EnumerableUpgradeable) returns (uint256) {
    function totalSupply() public view override(ERC721EnumerableUpgradeable, IERC721EnumerableUpgradeable) returns (uint256) {
        return edStore.getAdjustedGlobalVotes(block.timestamp.toUint48());
        return edStore.getAdjustedGlobalVotes(block.timestamp.toUint48());
    }
    }
    /*///////////////////////////////////////////////////////////////
    /*///////////////////////////////////////////////////////////////
                           @dev See {IVotes}.
                           @dev See {IVotes}.
    //////////////////////////////////////////////////////////////*/
    //////////////////////////////////////////////////////////////*/
    /**
    /**
     * @notice Gets the votes for a delegatee
     * @notice Gets the votes for a delegatee
     * @param account The address of the delegatee
     * @param account The address of the delegatee
     * @return The number of votes the delegatee has
     * @return The number of votes the delegatee has
     */
     */
    function getVotes(address account) external view override(IVotes) returns (uint256) {
    function getVotes(address account) external view override(IVotes) returns (uint256) {
        return edStore.getAdjustedVotes(account, block.timestamp.toUint48());
        return edStore.getAdjustedVotes(account, block.timestamp.toUint48());
    }
    }
    /**
    /**
     * @notice Gets the past votes for a delegatee at a specific time point
     * @notice Gets the past votes for a delegatee at a specific time point
     * @param account The address of the delegatee
     * @param account The address of the delegatee
     * @param timepoint The time point to get the votes at
     * @param timepoint The time point to get the votes at
     * @return The number of votes the delegatee had at the time point
     * @return The number of votes the delegatee had at the time point
     */
     */
    function getPastVotes(address account, uint256 timepoint) external view override(IVotes) returns (uint256) {
    function getPastVotes(address account, uint256 timepoint) external view override(IVotes) returns (uint256) {
        return edStore.getAdjustedVotes(account, timepoint.toUint48());
        return edStore.getAdjustedVotes(account, timepoint.toUint48());
    }
    }
    /**
    /**
     * @notice Gets the total supply at a specific time point
     * @notice Gets the total supply at a specific time point
     * @param _timePoint The time point to get the total supply at
     * @param _timePoint The time point to get the total supply at
     * @return The total supply at the time point
     * @return The total supply at the time point
     */
     */
    function getPastTotalSupply(uint256 _timePoint) external view override(IVotes) returns (uint256) {
    function getPastTotalSupply(uint256 _timePoint) external view override(IVotes) returns (uint256) {
        return edStore.getAdjustedGlobalVotes(_timePoint.toUint48());
        return edStore.getAdjustedGlobalVotes(_timePoint.toUint48());
    }
    }
    /**
    /**
     * @notice Delegates votes to a delegatee
     * @notice Delegates votes to a delegatee
     * @param delegatee The account to delegate votes to
     * @param delegatee The account to delegate votes to
     */
     */
    function delegate(address delegatee) external override(IVotes) {
    function delegate(address delegatee) external override(IVotes) {
        _delegate(_msgSender(), delegatee);
        _delegate(_msgSender(), delegatee);
    }
    }
    /**
    /**
     * @notice Gets the delegate of a delegatee
     * @notice Gets the delegate of a delegatee
     * @dev This function implements IVotes interface.
     * @dev This function implements IVotes interface.
     *  An account can have multiple delegates in this contract. If multiple
     *  An account can have multiple delegates in this contract. If multiple
     *  different delegates are found, this function returns address(1) to
     *  different delegates are found, this function returns address(1) to
     *  indicate that there is not a single unique delegate.
     *  indicate that there is not a single unique delegate.
     * @param account The delegatee to get the delegate of
     * @param account The delegatee to get the delegate of
     * @return The delegate of the delegatee, or address(1) if multiple different delegates are found
     * @return The delegate of the delegatee, or address(1) if multiple different delegates are found
     */
     */
    function delegates(address account) external view override(IVotes) returns (address) {
    function delegates(address account) external view override(IVotes) returns (address) {
        address delegatee = address(0);
        address delegatee = address(0);
        uint256 balance = balanceOf(account);
        uint256 balance = balanceOf(account);
        /// @dev out-of-gas protection
        /// @dev out-of-gas protection
        uint256 runs = 50 > balance ? balance : 50;
        uint256 runs = 50 > balance ? balance : 50;
        for (uint256 i = 0; i < runs; i++) {
        for (uint256 i = 0; i < runs; i++) {
            uint256 tokenId = tokenOfOwnerByIndex(account, i);
            uint256 tokenId = tokenOfOwnerByIndex(account, i);
            address currentDelegatee = edStore.getEscrowDelegatee(tokenId);
            address currentDeleg
            /// @dev Hacky way to check if the delegatee is the same for all locks
            if (delegatee == address(0)) {
                delegatee = currentDelegatee;
            } else if (delegatee != currentDelegatee) {
                return address(1);
            }
        }
        return delegatee;
    }
    /**
     * @notice Delegates votes from a spe