HookSwapV3PositionLocker
0x86426094d82bc1fd40f0901965b23d30837dc66b
Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
5,779 bytes
ABI entries
25
7 read · 8 write
License
none
Contract information
- Address
- 0x86426094d82bc1fd40f0901965b23d30837dc66b
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.24+commit.e11b9ed9
- Optimization
- Enabled
- Creator
- 0xc14C897c6b…9230205125
- Creation tx
- 0x7cabc00320…50beb46159
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (7)
feeReceiver() → address
getLockData(uint256) → uint256, address, address, uint256, address, uint40, uint40, bool
getLocksForAddress(address) → uint256[]
lockCount() → uint256
lockFee() → uint256
onERC721Received(address, address, uint256, bytes) → bytes4
owner() → address
Events (9)
FeeReceiverUpdatedFeesCollectedLockFeePaidLockFeeUpdatedLockOwnershipTransferredOwnershipTransferredPositionExtendedPositionLockedPositionWithdrawn
ABI
[
{
"inputs": [
{
"internalType": "uint256",
"name": "lockFee_",
"type": "uint256"
},
{
"internalType": "address",
"name": "feeReceiver_",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "oldReceiver",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "newReceiver",
"type": "address"
}
],
"name": "FeeReceiverUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "id",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount0",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount1",
"type": "uint256"
}
],
"name": "FeesCollected",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "payer",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "LockFeePaid",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "oldFee",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "newFee",
"type": "uint256"
}
],
"name": "LockFeeUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "id",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "LockOwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "oldOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "id",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint40",
"name": "newUnlockTime",
"type": "uint40"
}
],
"name": "PositionExtended",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "id",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "nftManager",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": false,
"internalType": "uint40",
"name": "unlockTime",
"type": "uint40"
}
],
"name": "PositionLocked",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "id",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "PositionWithdrawn",
"type": "event"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "lockId_",
"type": "uint256"
}
],
"name": "collectFees",
"outputs": [
{
"internalType": "uint256",
"name": "amount0",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount1",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "lockId_",
"type": "uint256"
},
{
"internalType": "uint40",
"name": "newUnlockTime_",
"type": "uint40"
}
],
"name": "extend",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "feeReceiver",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "lockId_",
"type": "uint256"
}
],
"name": "getLockData",
"outputs": [
{
"internalType": "uint256",
"name": "id",
"type": "uint256"
},
{
"internalType": "address",
"name": "lockOwner",
"type": "address"
},
{
"internalType": "address",
"name": "nftManager",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "address",
"name": "createdBy",
"type": "address"
},
{
"internalType": "uint40",
"name": "createdAt",
"type": "uint40"
},
{
"internalType": "uint40",
"name": "unlockTime",
"type": "uint40"
},
{
"internalType": "bool",
"name": "withdrawn",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "address_",
"type": "address"
}
],
"name": "getLocksForAddress",
"outputs": [
{
"internalType": "uint256[]",
"name": "",
"type": "uint256[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "nftManager_",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId_",
"type": "uint256"
},
{
"internalType": "uint40",
"name": "unlockTime_",
"type": "uint40"
}
],
"name": "lock",
"outputs": [
{
"internalType": "uint256",
"name": "id",
"type": "uint256"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "lockCount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "lockFee",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"name": "onERC721Received",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "feeReceiver_",
"type": "address"
}
],
"name": "setFeeReceiver",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "lockFee_",
"type": "uint256"
}
],
"name": "setLockFee",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "lockId_",
"type": "uint256"
},
{
"internalType": "address",
"name": "newOwner_",
"type": "address"
}
],
"name": "transferLockOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner_",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "lockId_",
"type": "uint256"
}
],
"name": "withdraw",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]Source code
// SPDX-License-Identifier: GPL-3.0
// Derived from OnlyMoons (github.com/onlymoons-io/onlymoons), GPL-3.0.
//
// HookSwap V3 Position Locker — locks Uniswap-V3 concentrated-liquidity
// positions (ERC-721 NFTs from the NonfungiblePositionManager).
//
// The OnlyMoons v2 locker custodies fungible ERC-20 / v2-LP tokens and cannot
// hold a v3 position NFT. This locker fills that gap while reusing the same
// native-token fee model and owner-search-index conventions as
// HookSwapTokenLockerManager. Its key feature: while the position principal
// stays locked, the lock owner can still claim the position's accrued trading
// fees via `collectFees`.
//
// NOTE: this is NEW code (not part of the audited OnlyMoons base). Get a fresh
// security review before using it with real value.
pragma solidity ^0.8.0;
import { Ownable } from "./Ownable.sol";
import { ReentrancyGuard } from "./library/ReentrancyGuard.sol";
import { IERC721Receiver } from "./library/IERC721Receiver.sol";
/**
* @dev Minimal subset of the Uniswap-V3 NonfungiblePositionManager interface
* used by this locker.
*/
interface INonfungiblePositionManager {
struct CollectParams {
uint256 tokenId;
address recipient;
uint128 amount0Max;
uint128 amount1Max;
}
function positions(uint256 tokenId) external view returns (
uint96 nonce,
address operator,
address token0,
address token1,
uint24 fee,
int24 tickLower,
int24 tickUpper,
uint128 liquidity,
uint256 feeGrowthInside0LastX128,
uint256 feeGrowthInside1LastX128,
uint128 tokensOwed0,
uint128 tokensOwed1
);
function collect(CollectParams calldata params) external payable returns (uint256 amount0, uint256 amount1);
function ownerOf(uint256 tokenId) external view returns (address);
function safeTransferFrom(address from, address to, uint256 tokenId) external;
function transferFrom(address from, address to, uint256 tokenId) external;
}
/**
* @title HookSwapV3PositionLocker
*/
contract HookSwapV3PositionLocker is Ownable, ReentrancyGuard, IERC721Receiver {
struct V3Lock {
uint256 id;
address owner;
address nftManager;
uint256 tokenId;
address createdBy;
uint40 createdAt;
uint40 unlockTime;
bool withdrawn;
}
event PositionLocked(
uint256 indexed id,
address indexed nftManager,
uint256 indexed tokenId,
address owner,
uint40 unlockTime
);
event PositionExtended(uint256 indexed id, uint40 newUnlockTime);
event PositionWithdrawn(uint256 indexed id, address indexed to, uint256 tokenId);
event FeesCollected(uint256 indexed id, address indexed recipient, uint256 amount0, uint256 amount1);
event LockOwnershipTransferred(uint256 indexed id, address indexed previousOwner, address indexed newOwner);
event LockFeeUpdated(uint256 oldFee, uint256 newFee);
event FeeReceiverUpdated(address oldReceiver, address newReceiver);
event LockFeePaid(address indexed payer, uint256 amount);
constructor(uint256 lockFee_, address feeReceiver_) Ownable(_msgSender()) {
_lockFee = lockFee_;
// default the fee receiver to the deployer when not provided
_feeReceiver = feeReceiver_ == address(0) ? _msgSender() : feeReceiver_;
}
uint256 private _lockCount;
/** @dev native-token fee required to create a lock (0 = free). */
uint256 private _lockFee;
/** @dev where collected fees are sent. */
address private _feeReceiver;
/** @dev main mapping for lock data */
mapping(uint256 => V3Lock) private _locks;
/** @dev owner search index (same convention as the v2 manager) */
mapping(address => uint256[]) private _locksForAddress;
modifier onlyLockOwner(uint256 lockId_) {
require(_locks[lockId_].owner == _msgSender(), "Only the lock owner can execute this function");
_;
}
/* --------------------------------------------------------------- fees */
function lockFee() external view returns (uint256) {
return _lockFee;
}
function feeReceiver() external view returns (address) {
return _feeReceiver;
}
function setLockFee(uint256 lockFee_) external onlyOwner {
emit LockFeeUpdated(_lockFee, lockFee_);
_lockFee = lockFee_;
}
function setFeeReceiver(address feeReceiver_) external onlyOwner {
require(feeReceiver_ != address(0), "Fee receiver cannot be the zero address");
emit FeeReceiverUpdated(_feeReceiver, feeReceiver_);
_feeReceiver = feeReceiver_;
}
/** @dev collects exactly `_lockFee` in native token and forwards it to the receiver. */
function _collectFee() private {
uint256 fee = _lockFee;
require(msg.value >= fee, "Insufficient lock fee");
if (fee > 0) {
(bool sent, ) = payable(_feeReceiver).call{ value: fee }("");
require(sent, "Fee transfer failed");
emit LockFeePaid(_msgSender(), fee);
}
// refund any overpayment
uint256 refund = msg.value - fee;
if (refund > 0) {
(bool refunded, ) = payable(_msgSender()).call{ value: refund }("");
require(refunded, "Refund failed");
}
}
/* --------------------------------------------------------------- lock */
/**
* @dev lock a v3 position NFT until `unlockTime_`. Charges the native-token
* lock fee (overpayment refunded) and pulls the NFT in via safeTransferFrom.
*/
function lock(
address nftManager_,
uint256 tokenId_,
uint40 unlockTime_
) external payable nonReentrant returns (uint256 id) {
require(nftManager_ != address(0), "NFT manager cannot be the zero address");
require(unlockTime_ > uint40(block.timestamp), "Unlock time must be in the future");
_collectFee();
id = _lockCount++;
_locks[id] = V3Lock({
id: id,
owner: _msgSender(),
nftManager: nftManager_,
tokenId: tokenId_,
createdBy: _msgSender(),
createdAt: uint40(block.timestamp),
unlockTime: unlockTime_,
withdrawn: false
});
// add to the owner search index
_locksForAddress[_msgSender()].push(id);
// pull the NFT in. this triggers onERC721Received on this contract.
INonfungiblePositionManager(nftManager_).safeTransferFrom(_msgSender(), address(this), tokenId_);
emit PositionLocked(id, nftManager_, tokenId_, _msgSender(), unlockTime_);
}
/**
* @dev ERC-721 receiver hook — required to accept safe transfers.
*/
function onERC721Received(
address /* operator */,
address /* from */,
uint256 /* tokenId */,
bytes calldata /* data */
) external pure override returns (bytes4) {
return IERC721Receiver.onERC721Received.selector;
}
/**
* @dev while locked, the lock owner collects the position's accrued trading
* fees. Principal (liquidity) stays locked; only fees are sent to the owner.
*/
function collectFees(uint256 lockId_)
external
nonReentrant
onlyLockOwner(lockId_)
returns (uint256 amount0, uint256 amount1)
{
V3Lock storage lock_ = _locks[lockId_];
require(!lock_.withdrawn, "Position already withdrawn");
(amount0, amount1) = INonfungiblePositionManager(lock_.nftManager).collect(
INonfungiblePositionManager.CollectParams({
tokenId: lock_.tokenId,
recipient: lock_.owner,
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
})
);
emit FeesCollected(lockId_, lock_.owner, amount0, amount1);
}
/**
* @dev extend the lock. New time must be >= current unlock time and in the future.
*/
function extend(uint256 lockId_, uint40 newUnlockTime_) external onlyLockOwner(lockId_) {
V3Lock storage lock_ = _locks[lockId_];
require(!lock_.withdrawn, "Position already withdrawn");
require(
newUnlockTime_ >= lock_.unlockTime && newUnlockTime_ >= uint40(block.timestamp),
"New unlock time must be a future time beyond the previous value"
);
lock_.unlockTime = newUnlockTime_;
emit PositionExtended(lockId_, newUnlockTime_);
}
/**
* @dev after unlockTime, transfer the position NFT back to the lock owner.
*/
function withdraw(uint256 lockId_) external nonReentrant onlyLockOwner(lockId_) {
V3Lock storage lock_ = _locks[lockId_];
require(!lock_.withdrawn, "Position already withdrawn");
require(uint40(block.timestamp) >= lock_.unlockTime, "Wait until unlockTime to withdraw");
lock_.withdrawn = true;
INonfungiblePositionManager(lock_.nftManager).safeTransferFrom(address(this), lock_.owner, lock_.tokenId);
emit PositionWithdrawn(lockId_, lock_.owner, lock_.tokenId);
}
/**
* @dev transfer ownership of a lock to a new address, keeping the owner
* search index in sync (same replace-and-pop pattern as the v2 manager).
*/
function transferLockOwnership(uint256 lockId_, address newOwner_) external onlyLockOwner(lockId_) {
require(newOwner_ != address(0), "New owner cannot be the zero address");
V3Lock storage lock_ = _locks[lockId_];
address previousOwner = lock_.owner;
// remove from previous owner's index, unless it's the original creator
// (the creator entry is kept so history stays searchable, mirroring the v2 manager).
if (previousOwner != lock_.createdBy) {
uint256[] storage prev = _locksForAddress[previousOwner];
for (uint256 i = 0; i < prev.length; i++) {
if (prev[i] != lockId_) continue;
prev[i] = prev[prev.length - 1];
prev.pop();
break;
}
}
lock_.owner = newOwner_;
// add to new owner's index only if not already present
bool hasId = false;
uint256[] storage next = _locksForAddress[newOwner_];
for (uint256 i = 0; i < next.length; i++) {
if (next[i] == lockId_) {
hasId = true;
break;
}
}
if (!hasId) {
next.push(lockId_);
}
emit LockOwnershipTransferred(lockId_, previousOwner, newOwner_);
}
/* --------------------------------------------------------------- views */
function lockCount() external view returns (uint256) {
return _lockCount;
}
function getLocksForAddress(address address_) external view returns (uint256[] memory) {
return _locksForAddress[address_];
}
function getLockData(uint256 lockId_) external view returns (
uint256 id,
address lockOwner,
address nftManager,
uint256 tokenId,
address createdBy,
uint40 createdAt,
uint40 unlockTime,
bool withdrawn
) {
V3Lock storage lock_ = _locks[lockId_];
id = lock_.id;
lockOwner = lock_.owner;
nftManager = lock_.nftManager;
tokenId = lock_.tokenId;
createdBy = lock_.createdBy;
createdAt = lock_.createdAt;
unlockTime = lock_.unlockTime;
withdrawn = lock_.withdrawn;
}
}
Chain explorer4806msChain node80ms