The TimeBasedCondition contract demonstrates how custom unlock conditions can be implemented. It allows an admin to set specific unlock timestamps for individual locks, providing more granular control than the standard time-based unlocking in the locker contracts.
Key Features
Admin-controlled unlock timestamps
Per-lock granular control
Safe fallback behavior
Simple example for custom conditions
ILockCondition Interface
All condition contracts must implement the ILockCondition interface:
interface ILockCondition{/** * @dev Checks if a lock should be unlocked * @paramlocker Address of the locker contract * @paramlockId ID of the lock to check * @returnbool True if the lock can be unlocked */functionisUnlocked(addresslocker,uint256lockId)externalviewreturns(bool);}
Functions
setUnlockTimestamp
Sets the unlock timestamp for a specific lock (admin only).
Parameters:
locker: Address of the locker contract (V2 or V3)
lockId: ID of the lock
unlockTimestamp: Unix timestamp when the lock should be unlocked
Requirements:
Only callable by admin (deployer)
Unlock timestamp must be in the future
Example:
isUnlocked
Checks if a lock should be unlocked based on the condition (view function).
Parameters:
locker: Address of the locker contract
lockId: ID of the lock to check
Returns:
true if current timestamp >= unlock timestamp
false otherwise (or if no timestamp set)
Example:
unlockTimestamps
Public mapping of unlock timestamps (view function).
Usage:
admin
Returns the admin address (immutable).
Usage Examples
Creating a Lock with Time Condition
Checking Unlock Status
Integration Example
Complete Workflow
Creating Custom Conditions
You can create your own condition contracts by implementing ILockCondition. Here are some examples:
Example 1: Price-Based Unlock
Example 2: Governance Vote Condition
Example 3: Multi-Signature Condition
Best Practices
For Condition Developers
Fail Safely: If your condition reverts, the locker will fall back to time-based unlock
Gas Efficiency: Keep isUnlocked() view function gas-efficient
State Changes: Only modify state in setter functions, not in isUnlocked()
Access Control: Implement proper access control for setter functions
Testing: Thoroughly test all edge cases
For Users
Verify Condition: Always verify the condition contract before using it
Understand Logic: Make sure you understand how the condition works
Fallback: Remember that time-based unlock still applies if condition fails
Admin Trust: Be aware of who controls the condition contract
Use Known Conditions: Prefer using well-tested, verified condition contracts
Security Considerations
Risks
Malicious Conditions: A malicious condition could prevent unlocking
Admin Control: Condition admin has significant power
Condition Bugs: Bugs in condition logic could lock funds
Gas Griefing: Expensive condition checks could make unlocking costly
Mitigations
Try/Catch Protection: Locker contracts wrap condition calls in try/catch blocks
Time-Based Fallback: Standard unlock time still applies
Optional Usage: Conditions are completely optional
Error Reference
Events
The TimeBasedCondition contract does not emit custom events, but you can add them in your own condition contracts:
// Deploy or get TimeBasedCondition
address timeCondition = 0xae61e21eA7461cf49f0f358de944C5448bD430aa;
// Lock position with condition
uint256 lockId = v3Locker.lockPosition{value: creationFee}(
tokenId,
block.timestamp + 90 days, // Standard unlock time
msg.sender,
timeCondition // Use time condition
);
// Later, admin can set a different unlock time
// (must be done by condition admin)
// Get lock details
Lock memory lock = v3Locker.getLock(myLockId);
if (lock.condition != address(0)) {
// Check condition
bool conditionMet = ILockCondition(lock.condition).isUnlocked(
address(v3Locker),
myLockId
);
if (conditionMet) {
console.log("Condition met! Can unlock.");
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {V3Locker} from "./V3Locker.sol";
import {TimeBasedCondition} from "./conditions/TimeBasedCondition.sol";
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
contract TimeConditionExample {
V3Locker public v3Locker;
TimeBasedCondition public timeCondition;
address public positionManager;
constructor(
address _v3Locker,
address _timeCondition,
address _positionManager
) {
v3Locker = V3Locker(_v3Locker);
timeCondition = TimeBasedCondition(_timeCondition);
positionManager = _positionManager;
}
/**
* @dev Lock a position with time condition
*/
function lockWithTimeCondition(
uint256 tokenId,
uint256 standardUnlockTime
) external payable returns (uint256) {
// Approve NFT
IERC721(positionManager).approve(address(v3Locker), tokenId);
// Lock with time condition
uint256 lockId = v3Locker.lockPosition{value: msg.value}(
tokenId,
standardUnlockTime,
msg.sender,
address(timeCondition)
);
return lockId;
}
/**
* @dev Check if lock can be unlocked via condition
*/
function canUnlockViaCondition(uint256 lockId) external view returns (bool) {
return timeCondition.isUnlocked(address(v3Locker), lockId);
}
/**
* @dev Unlock position (checks both time and condition)
*/
function unlockPosition(uint256 lockId) external {
// This will check:
// 1. Standard unlock time
// 2. Condition (if set)
v3Locker.withdraw(lockId, msg.sender);
}
}