V3 Locker

Complete reference for the V3Locker contract, which handles locking of Uniswap V3 concentrated liquidity NFT positions.

Contract Address

  • Monad Testnet: 0x2D0dFc5a6731315D8f911E8534746D89B7472175

Overview

The V3 Locker enables users to lock their Uniswap V3 NFT positions (ERC-721 tokens) for a specified duration. While locked, users can:

  • Collect trading fees

  • Extend lock duration

  • Transfer ownership

  • View position details

Core Functions

lockPosition

Locks a V3 NFT position for a specified duration.

function lockPosition(
    uint256 nftId,
    uint256 unlockDate,
    address collectAddress,
    address condition
) external payable returns (uint256 lockId)

Parameters:

  • nftId: The ID of the V3 NFT position to lock

  • unlockDate: Unix timestamp when the position can be unlocked (max 10 years)

  • collectAddress: Address that will receive collected trading fees (use address(0) for msg.sender)

  • condition: Optional condition contract address (use address(0) for time-based only)

Returns:

  • lockId: Unique identifier for the created lock

Requirements:

  • Must send creationFee as msg.value

  • Must have approved the NFT to this contract

  • unlockDate must be in the future and within 10 years

  • NFT must have liquidity > 0

  • If condition is provided, it must be a valid contract

Events Emitted:

event Locked(
    address indexed owner,
    uint256 indexed lockId,
    address indexed nft,
    uint256 tokenId,
    uint256 unlockTime,
    bool permanent,
    address condition,
    address token0,
    address token1,
    address pool,
    uint128 liquidity,
    uint24 fee,
    int24 tickLower,
    int24 tickUpper,
    string token0Symbol,
    string token1Symbol,
    string token0Name,
    string token1Name,
    uint8 token0Decimals,
    uint8 token1Decimals
);

Example:

// Approve NFT
positionManager.approve(v3LockerAddress, myTokenId);

// Lock for 90 days
uint256 unlockDate = block.timestamp + 90 days;
uint256 lockId = v3Locker.lockPosition{value: 0.0001 ether}(
    myTokenId,
    unlockDate,
    msg.sender,  // I want to receive fees
    address(0)   // No custom condition
);

withdraw

Withdraws (unlocks) a locked position after the unlock date.

function withdraw(
    uint256 lockId,
    address receiver
) external

Parameters:

  • lockId: The ID of the lock to withdraw

  • receiver: Address to receive the unlocked NFT

Requirements:

  • Must be the lock owner (msg.sender == lock.owner)

  • Lock must be active

  • Current timestamp must be >= unlock date

  • If condition is set, condition must return true

  • Receiver cannot be address(0)

Events Emitted:

event Unlocked(
    address indexed owner,
    uint256 indexed lockId,
    address indexed nft,
    uint256 tokenId
);

Example:

// Withdraw to my address
v3Locker.withdraw(myLockId, msg.sender);

// Or withdraw to another address
v3Locker.withdraw(myLockId, myOtherWallet);

claimFees

Collects accumulated trading fees from the locked position.

function claimFees(
    uint256 lockId,
    uint128 amount0Max,
    uint128 amount1Max
) external

Parameters:

  • lockId: The ID of the lock

  • amount0Max: Maximum amount of token0 to collect (use type(uint128).max for all)

  • amount1Max: Maximum amount of token1 to collect (use type(uint128).max for all)

Requirements:

  • Must be the lock owner

  • Lock must be active

Fee Distribution:

  • Protocol takes claimFeeBps (default 1%)

  • Remaining fees sent to collectAddress

Events Emitted:

event FeesClaimed(
    address indexed owner,
    uint256 indexed lockId,
    uint256 amount0,
    uint256 amount1,
    uint256 feeTaken0,
    uint256 feeTaken1
);

Example:

// Claim all available fees
v3Locker.claimFees(
    myLockId,
    type(uint128).max,
    type(uint128).max
);

Fee Collection from Uniswap V3

One of the key benefits of locking V3 positions is that you can continue collecting trading fees while your liquidity is locked. This is unique to V3 and not available in V2.

How V3 Fee Collection Works

  1. Trading Fees Accrue: As traders swap tokens in the pool, fees accumulate in your locked position

  2. You Collect Fees: Call claimFees() to collect accumulated fees to your collectAddress

  3. Protocol Fee: A small percentage (default 1%) goes to the protocol treasury

  4. You Keep the Rest: The remaining fees are sent directly to your collectAddress

Fee Collection Example

// Check position details first
Lock memory lock = v3Locker.getLock(myLockId);
console.log("Collect address:", lock.collectAddress);

// Claim all available fees
v3Locker.claimFees(
    myLockId,
    type(uint128).max,  // Collect all token0
    type(uint128).max   // Collect all token1
);

// Fees are sent to lock.collectAddress
// Protocol fee (1%) sent to treasury

Updating Your Collect Address

You can change where fees are sent at any time:

// Change fee recipient to another address
v3Locker.setCollectAddress(myLockId, myTreasuryAddress);

// Now all future fee claims go to myTreasuryAddress

V3 vs V2 Fee Collection

Feature
V3 Locker
V2 Locker

Fee Claiming

Yes - call claimFees()

No separate claim function

How Fees Accrue

Separate from liquidity

Increases LP token value

When You Get Fees

Anytime via claimFees()

When you unlock

Protocol Fee

1% on claimed fees

None

Important: V2 LP tokens work differently - trading fees automatically accrue to the LP token's value. You don't claim fees separately; they're reflected in the increasing value of your LP tokens. When you unlock your V2 LP tokens, you receive the full value including all accrued fees.


extendLock

Extends the unlock date of an existing lock.

function extendLock(
    uint256 lockId,
    uint256 newUnlockDate
) external

Parameters:

  • lockId: The ID of the lock to extend

  • newUnlockDate: New unlock timestamp (must be later than current, max 10 years from now)

Requirements:

  • Must be the lock owner

  • Lock must be active

  • New unlock date must be > current unlock date

  • New unlock date must be <= current timestamp + 10 years

Events Emitted:

event LockExtended(
    address indexed owner,
    uint256 indexed lockId,
    uint256 oldUnlockTime,
    uint256 newUnlockTime
);

Example:

// Extend lock by 30 more days
uint256 newUnlockDate = block.timestamp + 120 days; // 90 + 30
v3Locker.extendLock(myLockId, newUnlockDate);

transferLockOwnership

Initiates a two-step ownership transfer (step 1 of 2).

function transferLockOwnership(
    uint256 lockId,
    address newOwner
) external

Parameters:

  • lockId: The ID of the lock

  • newOwner: Address of the new owner

Requirements:

  • Must be the current lock owner

  • Lock must be active

  • New owner cannot be address(0)

Next Step: New owner must call acceptLockOwnership()

Example:

// Initiate transfer
v3Locker.transferLockOwnership(myLockId, buyerAddress);

// Buyer must accept...

acceptLockOwnership

Accepts ownership transfer (step 2 of 2).

function acceptLockOwnership(uint256 lockId) external

Parameters:

  • lockId: The ID of the lock

Requirements:

  • Must be the pending owner

  • Lock must be active

Events Emitted:

event LockOwnershipTransferred(
    uint256 indexed lockId,
    address indexed previousOwner,
    address indexed newOwner
);

Example:

// Accept ownership transfer
v3Locker.acceptLockOwnership(lockId);

setCollectAddress

Updates the address that receives collected fees.

function setCollectAddress(
    uint256 lockId,
    address newCollectAddress
) external

Parameters:

  • lockId: The ID of the lock

  • newCollectAddress: New address for fee collection

Requirements:

  • Must be the lock owner

  • Lock must be active

  • New address cannot be address(0)

Example:

// Change fee recipient
v3Locker.setCollectAddress(myLockId, myTreasuryAddress);

View Functions

getOwnerLockIds

Returns all lock IDs owned by an address.

function getOwnerLockIds(address owner) external view returns (uint256[] memory)

Example:

uint256[] memory myLocks = v3Locker.getOwnerLockIds(msg.sender);

getOwnerLockCount

Returns the number of locks owned by an address.

function getOwnerLockCount(address owner) external view returns (uint256)

getLock

Returns complete lock details.

function getLock(uint256 lockId) external view returns (Lock memory)

Returns:

struct Lock {
    uint256 lockId;         // Lock ID
    uint256 nftId;          // NFT token ID
    address owner;          // Current owner
    address pendingOwner;   // Pending owner (for transfers)
    address pool;           // Pool address
    address token0;         // First token
    address token1;         // Second token
    uint128 liquidity;      // Liquidity amount
    uint256 unlockDate;     // Unlock timestamp
    address collectAddress; // Fee recipient
    bool isActive;          // Is lock active
    address condition;      // Condition contract (if any)
}

Example:

Lock memory lock = v3Locker.getLock(myLockId);
console.log("Unlock date:", lock.unlockDate);
console.log("Liquidity:", lock.liquidity);

canUnlock

Checks if a lock can be unlocked now.

function canUnlock(uint256 lockId) external view returns (bool)

Example:

if (v3Locker.canUnlock(myLockId)) {
    // Can withdraw now!
    v3Locker.withdraw(myLockId, msg.sender);
}

isLocked

Checks if an NFT is currently locked.

function isLocked(uint256 nftId) external view returns (bool)

Example:

bool locked = v3Locker.isLocked(tokenId);
require(!locked, "NFT is locked");

getPositionInfo

Returns position information from the Position Manager.

function getPositionInfo(uint256 lockId) 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
)

State Variables

// Immutable
address public immutable positionManager;  // V3 Position Manager address

// Configuration
uint256 public creationFee;                // Fee to create lock
uint256 public claimFeeBps;                // Fee on claimed rewards (bps)
address public treasury;                   // Treasury address

// State
uint256 public nextLockId;                 // Next available lock ID

Events

event Locked(...);                         // Position locked
event Unlocked(...);                       // Position unlocked
event FeesClaimed(...);                    // Fees collected
event LockExtended(...);                   // Lock duration extended
event LockOwnershipTransferred(...);       // Ownership transferred

Integration Examples

Web3.js

const Web3 = require('web3');
const web3 = new Web3('https://testnet-rpc.monad.xyz');

const V3_LOCKER_ABI = [...]; // Import ABI
const v3Locker = new web3.eth.Contract(V3_LOCKER_ABI, '0x0b4619Ed28429a392C79aed87E6572F34ab6199e');

// Lock a position
async function lockPosition(tokenId, days) {
    const unlockDate = Math.floor(Date.now() / 1000) + (days * 86400);
    const creationFee = await v3Locker.methods.creationFee().call();
    
    await v3Locker.methods.lockPosition(
        tokenId,
        unlockDate,
        '0x0000000000000000000000000000000000000000',
        '0x0000000000000000000000000000000000000000'
    ).send({
        from: userAddress,
        value: creationFee
    });
}

Ethers.js

const { ethers } = require('ethers');

const provider = new ethers.providers.JsonRpcProvider('https://testnet-rpc.monad.xyz');
const wallet = new ethers.Wallet(privateKey, provider);

const v3Locker = new ethers.Contract(
    '0x0b4619Ed28429a392C79aed87E6572F34ab6199e',
    V3_LOCKER_ABI,
    wallet
);

// Get all locks
async function getMyLocks() {
    const lockIds = await v3Locker.getOwnerLockIds(wallet.address);
    
    const locks = await Promise.all(
        lockIds.map(id => v3Locker.getLock(id))
    );
    
    return locks;
}

// Claim fees
async function claimFees(lockId) {
    const tx = await v3Locker.claimFees(
        lockId,
        ethers.constants.MaxUint256,
        ethers.constants.MaxUint256
    );
    await tx.wait();
}

Error Reference

error InvalidLock();                  // Lock ID doesn't exist or invalid
error LockNotActive();               // Lock has been withdrawn
error LockNotUnlocked();             // Unlock conditions not met
error NotLockOwner();                // Caller is not the lock owner
error InsufficientCreationFee();     // Didn't send enough creation fee
error InvalidFeeBps();               // Fee BPS > 10000
error InvalidTreasury();             // Treasury address is zero
error TransferFailed();              // Native token transfer failed
error InvalidCondition();            // Condition address is not a contract
error InvalidPositionManager();      // Position manager is zero address
error InvalidUnlockTime();           // Unlock time is invalid
error EmergencyWithdrawalFailed();   // Emergency withdrawal failed

Gas Estimates

Approximate gas costs on Monad Testnet:

Function
Gas Cost
USD (at $3000 ETH, 0.001 gwei)

lockPosition()

~250,000

~$0.75

claimFees()

~150,000

~$0.45

withdraw()

~180,000

~$0.54

extendLock()

~50,000

~$0.15

transferLockOwnership()

~45,000

~$0.14

Note: Actual costs may vary based on network congestion and position complexity


Last updated