Extending Locks

Comprehensive guide to extending lock durations, adding liquidity, and managing long-term locked positions.

Overview

RealSafe allows lock owners to extend their lock duration anytime, providing flexibility for long-term liquidity commitment strategies. This guide covers all aspects of lock extension and management.

Table of Contents


Basic Lock Extension

Extending V3 Locks

V3 locks can have their unlock date extended anytime before unlocking.

/**
 * @dev Extends a V3 lock duration
 */
function extendLock(
    uint256 lockId,
    uint256 newUnlockDate
) external

Requirements:

  • Must be lock owner

  • Lock must be active

  • newUnlockDate > current unlock date

  • newUnlockDate <= current time + 10 years

Example:

// Get current lock details
Lock memory lock = v3Locker.getLock(myLockId);

// Extend by 90 days
uint256 newUnlockDate = lock.unlockDate + 90 days;

// Execute extension
v3Locker.extendLock(myLockId, newUnlockDate);

Extending V2 Locks

V2 locks work the same way as V3 locks for extension.

// Get current unlock time
(, , , uint256 currentUnlockTime, , , ) = v2Locker.locks(myLockId);

// Extend by 6 months
uint256 newUnlockDate = currentUnlockTime + 180 days;

// Execute extension
v2Locker.extendLock(myLockId, newUnlockDate);

Events

Both V2 and V3 emit similar events:

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

Adding to V2 Locks

V2 Locker allows adding more LP tokens to existing locks without creating a new lock.

addToLock Function

function addToLock(
    uint256 lockId,
    uint256 amount
) external

Requirements:

  • Must be lock owner

  • Lock must be active

  • Amount must be > 0

  • Must have approved additional tokens

Example:

// Original lock: 1000 LP tokens
// Want to add: 500 more LP tokens

// 1. Approve additional tokens
IERC20(lpToken).approve(v2LockerAddress, 500 * 1e18);

// 2. Add to existing lock
v2Locker.addToLock(myLockId, 500 * 1e18);

// Now total: 1500 LP tokens locked

Why Add Instead of Creating New Lock?

  • Gas Savings: Cheaper than creating new lock

  • Simpler Management: One lock ID instead of multiple

  • Better UX: Easier to track and unlock

  • Same Unlock Date: All tokens unlock together

Events

event LockIncreased(
    address indexed owner,
    uint256 indexed lockId,
    uint256 addedAmount,
    uint256 newTotalAmount
);

Advanced Strategies

Strategy 1: Progressive Extension

Gradually extend lock duration over time to maintain commitment.

contract ProgressiveExtension {
    V3Locker public v3Locker;
    
    struct ExtensionSchedule {
        uint256 lockId;
        uint256[] extensionDates;
        uint256[] extensionDurations;
        uint256 nextIndex;
    }
    
    mapping(address => ExtensionSchedule) public schedules;
    
    /**
     * @dev Create an extension schedule
     * Example: Extend by 30 days every 3 months
     */
    function createSchedule(
        uint256 lockId,
        uint256[] calldata intervals,
        uint256[] calldata durations
    ) external {
        require(intervals.length == durations.length, "Length mismatch");
        
        schedules[msg.sender] = ExtensionSchedule({
            lockId: lockId,
            extensionDates: intervals,
            extensionDurations: durations,
            nextIndex: 0
        });
    }
    
    /**
     * @dev Execute next scheduled extension
     */
    function executeNextExtension() external {
        ExtensionSchedule storage schedule = schedules[msg.sender];
        require(schedule.nextIndex < schedule.extensionDates.length, "No more extensions");
        
        uint256 extensionDate = schedule.extensionDates[schedule.nextIndex];
        require(block.timestamp >= extensionDate, "Too early");
        
        Lock memory lock = v3Locker.getLock(schedule.lockId);
        uint256 duration = schedule.extensionDurations[schedule.nextIndex];
        uint256 newUnlockDate = lock.unlockDate + duration;
        
        v3Locker.extendLock(schedule.lockId, newUnlockDate);
        
        schedule.nextIndex++;
    }
}

Usage:

// Extend by 30 days every 90 days, 4 times
uint256[] memory intervals = new uint256[](4);
intervals[0] = block.timestamp + 90 days;
intervals[1] = block.timestamp + 180 days;
intervals[2] = block.timestamp + 270 days;
intervals[3] = block.timestamp + 360 days;

uint256[] memory durations = new uint256[](4);
durations[0] = 30 days;
durations[1] = 30 days;
durations[2] = 30 days;
durations[3] = 30 days;

progressiveExtension.createSchedule(myLockId, intervals, durations);

Strategy 2: Conditional Extension

Automatically extend based on conditions.

contract ConditionalExtension {
    V3Locker public v3Locker;
    
    struct ConditionalRule {
        uint256 lockId;
        address priceOracle;
        int256 priceThreshold;
        uint256 extensionDuration;
    }
    
    mapping(address => ConditionalRule) public rules;
    
    /**
     * @dev Set rule: If ETH > $4000, extend by 90 days
     */
    function setRule(
        uint256 lockId,
        address priceOracle,
        int256 priceThreshold,
        uint256 extensionDuration
    ) external {
        rules[msg.sender] = ConditionalRule({
            lockId: lockId,
            priceOracle: priceOracle,
            priceThreshold: priceThreshold,
            extensionDuration: extensionDuration
        });
    }
    
    /**
     * @dev Check and extend if condition met
     */
    function checkAndExtend() external {
        ConditionalRule memory rule = rules[msg.sender];
        
        (, int256 price, , , ) = AggregatorV3Interface(rule.priceOracle)
            .latestRoundData();
        
        if (price >= rule.priceThreshold) {
            Lock memory lock = v3Locker.getLock(rule.lockId);
            uint256 newUnlockDate = lock.unlockDate + rule.extensionDuration;
            
            v3Locker.extendLock(rule.lockId, newUnlockDate);
        }
    }
}

Strategy 3: DAO-Controlled Extension

Let DAO vote on lock extensions.

contract DAOExtension {
    V3Locker public v3Locker;
    
    struct ExtensionProposal {
        uint256 lockId;
        uint256 newUnlockDate;
        uint256 votesFor;
        uint256 votesAgainst;
        bool executed;
        mapping(address => bool) hasVoted;
    }
    
    mapping(uint256 => ExtensionProposal) public proposals;
    uint256 public nextProposalId;
    
    uint256 public constant QUORUM = 100;  // 100 votes needed
    
    /**
     * @dev Create extension proposal
     */
    function propose(uint256 lockId, uint256 newUnlockDate) external returns (uint256) {
        uint256 proposalId = nextProposalId++;
        ExtensionProposal storage proposal = proposals[proposalId];
        
        proposal.lockId = lockId;
        proposal.newUnlockDate = newUnlockDate;
        
        return proposalId;
    }
    
    /**
     * @dev Vote on proposal
     */
    function vote(uint256 proposalId, bool support) external {
        ExtensionProposal storage proposal = proposals[proposalId];
        require(!proposal.executed, "Already executed");
        require(!proposal.hasVoted[msg.sender], "Already voted");
        
        if (support) {
            proposal.votesFor++;
        } else {
            proposal.votesAgainst++;
        }
        
        proposal.hasVoted[msg.sender] = true;
    }
    
    /**
     * @dev Execute passed proposal
     */
    function execute(uint256 proposalId) external {
        ExtensionProposal storage proposal = proposals[proposalId];
        require(!proposal.executed, "Already executed");
        
        uint256 totalVotes = proposal.votesFor + proposal.votesAgainst;
        require(totalVotes >= QUORUM, "Quorum not reached");
        require(proposal.votesFor > proposal.votesAgainst, "Proposal defeated");
        
        v3Locker.extendLock(proposal.lockId, proposal.newUnlockDate);
        proposal.executed = true;
    }
}

Strategy 4: Staggered Unlocks

Split liquidity into multiple locks with different unlock dates.

contract StaggeredLocks {
    V2Locker public v2Locker;
    
    /**
     * @dev Create multiple locks with staggered unlock dates
     */
    function createStaggeredLocks(
        address lpToken,
        uint256 totalAmount,
        uint256 numberOfLocks,
        uint256 intervalDays
    ) external payable {
        uint256 amountPerLock = totalAmount / numberOfLocks;
        uint256 creationFee = v2Locker.creationFee();
        
        require(msg.value >= creationFee * numberOfLocks, "Insufficient fee");
        
        IERC20(lpToken).approve(address(v2Locker), totalAmount);
        
        for (uint i = 0; i < numberOfLocks; i++) {
            uint256 unlockTime = block.timestamp + (intervalDays * (i + 1) * 1 days);
            
            v2Locker.lockLP{value: creationFee}(
                lpToken,
                amountPerLock,
                unlockTime,
                false,
                address(0)
            );
        }
    }
}

Example:

// Lock 10,000 LP tokens
// Split into 4 locks
// Unlock every 90 days

staggeredLocks.createStaggeredLocks{value: 0.0004 ether}(
    lpTokenAddress,
    10000 * 1e18,  // Total amount
    4,             // Number of locks
    90             // Days between unlocks
);

// Results in:
// Lock 1: 2,500 tokens, unlocks in 90 days
// Lock 2: 2,500 tokens, unlocks in 180 days
// Lock 3: 2,500 tokens, unlocks in 270 days
// Lock 4: 2,500 tokens, unlocks in 360 days

Use Cases

1. Long-Term Project Commitment

Scenario: Project wants to show long-term commitment but maintain flexibility.

Solution:

// Initial lock: 1 year
lockPosition(tokenId, block.timestamp + 365 days, ...);

// After 6 months, extend by another year
extendLock(lockId, block.timestamp + 365 days + 180 days);

// Keep extending to show continued commitment

2. Yield Farming Strategy

Scenario: Want to keep farming rewards while maintaining locked liquidity.

Solution:

// Lock V3 position for 90 days
uint256 lockId = v3Locker.lockPosition{value: fee}(...);

// Collect trading fees monthly while locked
for (uint i = 0; i < 3; i++) {
    // Wait 30 days
    vm.warp(block.timestamp + 30 days);
    
    // Claim fees
    v3Locker.claimFees(lockId, type(uint128).max, type(uint128).max);
}

// Decide to extend based on yields
if (yieldsAreGood) {
    extendLock(lockId, block.timestamp + 90 days);
}

3. Vesting Schedule

Scenario: Team wants progressive liquidity unlocking.

Solution:

// Create 4 locks for team allocation
// 25% unlock every 6 months
uint256 teamAllocation = 1000000 * 1e18;

for (uint i = 0; i < 4; i++) {
    uint256 amount = teamAllocation / 4;
    uint256 unlockTime = block.timestamp + ((i + 1) * 180 days);
    
    v2Locker.lockLP{value: fee}(lpToken, amount, unlockTime, false, address(0));
}

4. Building Trust with Community

Scenario: New project wants to build trust by demonstrating long-term liquidity commitment.

Timeline:

  1. Launch: Lock 100% of initial liquidity for 30 days

  2. After 2 weeks: Extend to 90 days (shows commitment)

  3. After 1 month: Extend to 6 months (building confidence)

  4. After 3 months: Extend to 1 year (long-term commitment proven)

// Launch
uint256 lockId = v3Locker.lockPosition{value: fee}(
    tokenId,
    block.timestamp + 30 days,
    treasury,
    address(0)
);

// Week 2: Extend to 90 days
v3Locker.extendLock(lockId, block.timestamp + 90 days);

// Month 1: Extend to 6 months
v3Locker.extendLock(lockId, block.timestamp + 180 days);

// Month 3: Extend to 1 year
v3Locker.extendLock(lockId, block.timestamp + 365 days);

Best Practices

1. Plan Extensions in Advance

// Document your extension strategy
struct ExtensionPlan {
    uint256 lockId;
    uint256[] milestones;    // When to extend
    uint256[] durations;     // How much to extend
    string reasoning;        // Why this schedule
}

// Review quarterly
function reviewExtensionPlan() external onlyOwner {
    // Check market conditions
    // Check project performance
    // Decide on next extension
}

2. Communicate Extensions to Community

event LockExtensionPlanned(
    uint256 indexed lockId,
    uint256 currentUnlockTime,
    uint256 newUnlockTime,
    string reason
);

function announceExtension(
    uint256 lockId,
    uint256 newUnlockTime,
    string calldata reason
) external {
    emit LockExtensionPlanned(lockId, lock.unlockDate, newUnlockTime, reason);
    
    // Then execute
    v3Locker.extendLock(lockId, newUnlockTime);
}

3. Monitor Gas Costs

// Batch multiple operations to save gas
function batchExtend(uint256[] calldata lockIds, uint256[] calldata newDates) external {
    require(lockIds.length == newDates.length, "Length mismatch");
    
    for (uint i = 0; i < lockIds.length; i++) {
        v3Locker.extendLock(lockIds[i], newDates[i]);
    }
}

4. Set Maximum Extension Limits

// For governance/safety
uint256 public constant MAX_EXTENSION = 365 days;

function safeExtend(uint256 lockId) external {
    Lock memory lock = v3Locker.getLock(lockId);
    uint256 newUnlockDate = lock.unlockDate + MAX_EXTENSION;
    
    require(newUnlockDate <= block.timestamp + 10 years, "Too far");
    
    v3Locker.extendLock(lockId, newUnlockDate);
}

Gas Optimization

Estimated Gas Costs

Operation
V2 Locker
V3 Locker

extendLock()

~50,000 gas

~50,000 gas

addToLock()

~100,000 gas

N/A

Optimization Tips

  1. Batch Operations: Extend multiple locks in one transaction

  2. Optimal Timing: Extend during low gas periods

  3. Combine Operations: Add to lock + extend together (V2)

// Optimized: Add and extend in one transaction
function addAndExtend(
    uint256 lockId,
    uint256 additionalAmount,
    uint256 newUnlockDate
) external {
    // Add tokens
    IERC20(lpToken).approve(address(v2Locker), additionalAmount);
    v2Locker.addToLock(lockId, additionalAmount);
    
    // Extend lock
    v2Locker.extendLock(lockId, newUnlockDate);
}

Monitoring Extensions

Track Extension History

contract ExtensionTracker {
    struct ExtensionRecord {
        uint256 timestamp;
        uint256 oldUnlockDate;
        uint256 newUnlockDate;
        address extender;
    }
    
    mapping(uint256 => ExtensionRecord[]) public history;
    
    function recordExtension(
        uint256 lockId,
        uint256 oldDate,
        uint256 newDate
    ) external {
        history[lockId].push(ExtensionRecord({
            timestamp: block.timestamp,
            oldUnlockDate: oldDate,
            newUnlockDate: newDate,
            extender: msg.sender
        }));
    }
    
    function getExtensionHistory(uint256 lockId) 
        external 
        view 
        returns (ExtensionRecord[] memory) 
    {
        return history[lockId];
    }
}

Last updated