S Price: $0.72464 (+7.68%)

Contract Diff Checker

Contract Name:
TokenLocker

Contract Source Code:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

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

    function _contextSuffixLength() internal view virtual returns (uint256) {
        return 0;
    }
}

abstract contract Ownable is Context {
    address private _owner;

    error OwnableUnauthorizedAccount(address account);
    error OwnableInvalidOwner(address owner);
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    constructor(address initialOwner) {
        if (initialOwner == address(0)) {
            revert OwnableInvalidOwner(address(0));
        }
        _transferOwnership(initialOwner);
    }

    modifier onlyOwner() {
        _checkOwner();
        _;
    }

    function owner() public view virtual returns (address) {
        return _owner;
    }

    function _checkOwner() internal view virtual {
        if (owner() != _msgSender()) {
            revert OwnableUnauthorizedAccount(_msgSender());
        }
    }

    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    function transferOwnership(address newOwner) public virtual onlyOwner {
        if (newOwner == address(0)) {
            revert OwnableInvalidOwner(address(0));
        }
        _transferOwnership(newOwner);
    }

    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}

abstract contract ReentrancyGuard {
    uint256 private constant NOT_ENTERED = 1;
    uint256 private constant ENTERED = 2;

    uint256 private _status;

    error ReentrancyGuardReentrantCall();

    constructor() {
        _status = NOT_ENTERED;
    }

    modifier nonReentrant() {
        _nonReentrantBefore();
        _;
        _nonReentrantAfter();
    }

    function _nonReentrantBefore() private {
        // On the first call to nonReentrant, _status will be NOT_ENTERED
        if (_status == ENTERED) {
            revert ReentrancyGuardReentrantCall();
        }

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

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

    function _reentrancyGuardEntered() internal view returns (bool) {
        return _status == ENTERED;
    }
}

interface IERC20 {
    function name() external view returns (string memory);
    function symbol() external view returns (string memory);
    function totalSupply() external view returns (uint256);
    function decimals() external view returns (uint8);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 value) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
}

interface IERC721Receiver {
    function onERC721Received(
        address operator,
        address from,
        uint256 tokenId,
        bytes calldata data
        ) external returns (bytes4);
}

interface IAlgebraFactory {
    function poolByPair(address tokenA, address tokenB) external view returns (address pool);
}

interface INonfungiblePositionManager {
    function safeTransferFrom(address from, address to, uint256 tokenId) external;
    function positions(uint256 tokenId)
        external
        view
        returns (
            uint88 nonce,
            address operator,
            address token0,
            address token1,
            int24 tickLower,
            int24 tickUpper,
            uint128 liquidity,
            uint256 feeGrowthInside0LastX128,
            uint256 feeGrowthInside1LastX128,
            uint128 tokensOwed0,
            uint128 tokensOwed1
        );
    struct CollectParams {
        uint256 tokenId;
        address recipient;
        uint128 amount0Max;
        uint128 amount1Max;
    }
    function collect(CollectParams calldata params) external payable returns (uint256 amount0, uint256 amount1);
    function factory() external view returns (address);
}

abstract contract UtilERC721 {
    /**
    * @dev this throws an error on false, instead of returning false,
    * but can still be used the same way on frontend.
    */
    function isAlgebraNFTPosition_(INonfungiblePositionManager nonfungiblepositionmanager_, uint256 tokenId_) internal view returns (bool) {
        // this will return true if NFT position is from Algebra
        try nonfungiblepositionmanager_.positions(tokenId_) // this will return potentially Algebra
            returns (
                uint88,
                address,
                address token0,
                address token1,
                int24,
                int24,
                uint128,
                uint256,
                uint256,
                uint128,
                uint128
            ) {
            // we now need to see if we can get the factory out of this
            try nonfungiblepositionmanager_.factory() returns (address factory) {
                IAlgebraFactory _factory = IAlgebraFactory(factory);
                // we now need to see if there is a pool
                try _factory.poolByPair(token0, token1) returns (address pool) {
                    // return true if there is a pool
                    return pool != address(0);                
                } catch (bytes memory /* lowLevelData */) {
                    return false;
                }

            } catch (bytes memory /* lowLevelData */) {
                return false;
            }
            
        } catch (bytes memory /* lowLevelData */) {
            return false;
        }
    }

    /**
    * @dev this function will revert the transaction if it's called
    * on a token that isn't an LP token. so, it's recommended to be
    * sure that it's being called on an LP token, or expect the error.
    */
    function getLpData_(INonfungiblePositionManager nonfungiblepositionmanager_, uint256 tokenId_)
        internal view returns (
            address token0,
            address token1,
            int24 tickLower,
            int24 tickUpper,
            uint128 liquidity
        ) {
            (,,token0, token1, tickLower, tickUpper, liquidity,,,,) =  nonfungiblepositionmanager_.positions(tokenId_);
    }
}

contract TokenLocker is Ownable, UtilERC721, IERC721Receiver, ReentrancyGuard {
    
    INonfungiblePositionManager public constant NONFUNGIBLE_POSITION_MANAGER = INonfungiblePositionManager(0xd82Fe82244ad01AaD671576202F9b46b76fAdFE2); // SwapX NonfungiblePositionManager
    uint40 public constant MAX_LOCK_DURATION = 2 * 52 weeks; // max set for 2 years in case the owner makes a mistake and put an infinite timestamp instead

    struct NFTLock {
        uint256 _tokenId;
        address _createdBy;
        uint40 _createdAt;
        uint40 _unlockTime;
        bool _isDeposited;
    }
    mapping(uint256 => NFTLock) public nftLocks;
    uint256[] public nftLockers;

    event TokenLockerCreated(
        uint40 id,
        uint256 indexed tokenId,
        address token0,
        address token1,
        address createdBy,
        uint40 unlockTime
    );
    event Extended(uint256 indexed tokenId, uint40 newUnlockTime);
    event Deposited(uint256 indexed tokenId);
    event Withdrew(uint256 indexed tokenId);
    event FeesCollected(
        address indexed recipient,
        uint256 indexed tokenId,
        address token0,
        uint256 amount0,
        address token1,
        uint256 amount1
    );
    

    constructor() Ownable(msg.sender) {}

    function createTokenLocker(uint256 tokenId_, uint40 unlockTime_) external onlyOwner nonReentrant {
        // first let's check if the tokenId_ is from SwapX v3 pool
        require(isAlgebraNFTPosition_(NONFUNGIBLE_POSITION_MANAGER, tokenId_), "Token is not from SwapX v3 protocol");

        // now check if the tokenId already exists
        require(nftLocks[tokenId_]._createdBy == address(0), "NFT position already exists. Please use the deposit function instead");

        // now make sure the unlock time is appropriate
        require(unlockTime_ > uint40(block.timestamp), "Unlock time must be in the future");
        require(unlockTime_ <= uint40(block.timestamp) + MAX_LOCK_DURATION, "Unlock time cannot exceed 2 years");

        // transfer the NFT position to the contract
        NONFUNGIBLE_POSITION_MANAGER.safeTransferFrom(_msgSender(), address(this), tokenId_);

        nftLocks[tokenId_] = NFTLock({
            _tokenId: tokenId_,
            _createdBy: _msgSender(),
            _createdAt: uint40(block.timestamp),
            _unlockTime: unlockTime_,
            _isDeposited: true
        });
        nftLockers.push(tokenId_);

        (address token0, address token1,,,) = getLpData_(NONFUNGIBLE_POSITION_MANAGER, tokenId_);

        emit TokenLockerCreated(
            uint40(nftLockers.length - 1),
            tokenId_,
            token0,
            token1,
            _msgSender(),
            unlockTime_
        );
    }

    function getLpData(uint256 tokenId_) external view returns (
        address token0,
        address token1,
        int24 tickLower,
        int24 tickUpper,
        uint128 liquidity
        ) {
        (token0, token1, tickLower, tickUpper, liquidity) = getLpData_(NONFUNGIBLE_POSITION_MANAGER, tokenId_);
    }

    /**
    * @dev extend duration
    */
    function extendTime(uint256 tokenId_, uint40 newUnlockTime_) external onlyOwner nonReentrant {
        _extendTime(tokenId_, newUnlockTime_);
    }

    function _extendTime(uint256 tokenId_, uint40 newUnlockTime_) internal {
        NFTLock storage nftLock = nftLocks[tokenId_];

        require(nftLock._isDeposited, "The token ID is no longer active");
        require(
            newUnlockTime_ != 0 && newUnlockTime_ >= nftLock._unlockTime && newUnlockTime_ >= uint40(block.timestamp),
            "New unlock time must be a future time beyond the previous value"
        );
        require(newUnlockTime_ <= uint40(block.timestamp) + MAX_LOCK_DURATION, "New unlock time cannot exceed 2 years");
        
        nftLock._unlockTime = newUnlockTime_;
        emit Extended(tokenId_, newUnlockTime_);
    }

    /**
    * @dev deposit the NFT position
    */
    function deposit(uint256 tokenId_, uint40 newUnlockTime_) external onlyOwner nonReentrant {
        NFTLock storage nftLock = nftLocks[tokenId_];

        require(nftLock._createdBy != address(0), "NFT position doesn't exist. Please use the createTokenLocker function instead");
        require(!nftLock._isDeposited, "NFT position is deposited already");
        
        nftLock._isDeposited = true;
        
        _extendTime(tokenId_, newUnlockTime_);

        NONFUNGIBLE_POSITION_MANAGER.safeTransferFrom(_msgSender(), address(this), tokenId_);

        emit Deposited(tokenId_);
    }

    /**
    * @dev collect fees from the deposited NFT position
    */
    function collectFees(uint256 tokenId_, address recipient_) external onlyOwner nonReentrant {
        require(nftLocks[tokenId_]._isDeposited, "NFT position not yet deposited");
        // if sent to address(0), it will be transferred in the owner as a safety measure
        if (recipient_ == address(0)) {
            recipient_ = owner();
        }

        (address token0, address token1,,,) = getLpData_(NONFUNGIBLE_POSITION_MANAGER, tokenId_);
        
        INonfungiblePositionManager.CollectParams memory params;
        params = INonfungiblePositionManager.CollectParams({
            tokenId: tokenId_,
            recipient: recipient_,
            amount0Max: type(uint128).max,
            amount1Max: type(uint128).max
        });
        
        (uint256 amount0, uint256 amount1) = NONFUNGIBLE_POSITION_MANAGER.collect(params);

        emit FeesCollected(recipient_, tokenId_, token0, amount0, token1, amount1);
    }

    /**
    * @dev withdraw all of the deposited token
    */
    function withdraw(uint256 tokenId_) external onlyOwner nonReentrant {
        require(nftLocks[tokenId_]._isDeposited, "NFT position not yet deposited");
        require(uint40(block.timestamp) >= nftLocks[tokenId_]._unlockTime, "Wait until unlockTime to withdraw");

        NONFUNGIBLE_POSITION_MANAGER.safeTransferFrom(address(this), owner(), tokenId_);        
        nftLocks[tokenId_]._isDeposited = false;
        
        emit Withdrew(tokenId_);
    }

    /**
    * @dev recovery function -
    * just in case this contract winds up with additional tokens (from dividends, etc).
    * attempting to withdraw the locked token will revert.
    */
    function withdrawToken(address address_) external onlyOwner nonReentrant {
        IERC20 token = IERC20(address_);
        uint256 amount = token.balanceOf(address(this));

        require(amount > 0, "No token amount to withdraw");

        token.transfer(owner(), amount);
    }

    /**
    * @dev recovery function -
    * just in case this contract winds up with S in it (from dividends etc)
    */
    function withdrawS() external onlyOwner nonReentrant {
        address payable receiver = payable(owner());
        uint256 amount = address(this).balance;

        require(amount > 0, "No S to withdraw");
        
        (bool success,) = receiver.call{value: amount}("");
        require(success, "Withdraw S failed");
    }

    receive() external payable {
        // we need this function to receive S,
        // which might happen from dividend tokens.
        // use `withdrawS` to remove S from the contract.
    }

     /**
     * @dev Implementation of the {IERC721Receiver} interface.
     * This function is called by the NFT contract when a token is transferred to this contract.
     */
    function onERC721Received(
        address,
        address,
        uint256,
        bytes calldata
        ) external override pure returns (bytes4) {
        return this.onERC721Received.selector;
    }

    function renounceOwnership() public view override onlyOwner {
        revert("Token Locker must always have an owner");
    }

}

Please enter a contract address above to load the contract details and source code.

Context size (optional):