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");
}
}