Time-Based Condition

Documentation for the TimeBasedCondition contract, a simple example condition contract that allows custom unlock logic based on timestamps.

Contract Address

  • Monad Testnet: 0x3F400BaE5037474C3b8531CC61Cd177589093C9f

Overview

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
     * @param locker Address of the locker contract
     * @param lockId ID of the lock to check
     * @return bool True if the lock can be unlocked
     */
    function isUnlocked(
        address locker,
        uint256 lockId
    ) external view returns (bool);
}

Functions

setUnlockTimestamp

Sets the unlock timestamp for a specific lock (admin only).

function setUnlockTimestamp(
    address locker,
    uint256 lockId,
    uint256 unlockTimestamp
) external

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:

// Set unlock time to January 1, 2026
uint256 unlockTime = 1735689600;
timeCondition.setUnlockTimestamp(
    v3LockerAddress,
    myLockId,
    unlockTime
);

isUnlocked

Checks if a lock should be unlocked based on the condition (view function).

function isUnlocked(
    address locker,
    uint256 lockId
) external view returns (bool)

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:

bool canUnlock = timeCondition.isUnlocked(v3LockerAddress, myLockId);
if (canUnlock) {
    console.log("Lock can be unlocked!");
}

unlockTimestamps

Public mapping of unlock timestamps (view function).

mapping(address => mapping(uint256 => uint256)) public unlockTimestamps

Usage:

uint256 unlockTime = timeCondition.unlockTimestamps(v3LockerAddress, myLockId);
console.log("Unlock timestamp:", unlockTime);

admin

Returns the admin address (immutable).

address public immutable admin

Usage Examples

Creating a Lock with Time Condition

// 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)

Checking Unlock Status

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

Integration Example

Complete Workflow

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

Creating Custom Conditions

You can create your own condition contracts by implementing ILockCondition. Here are some examples:

Example 1: Price-Based Unlock

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {ILockCondition} from "../interfaces/ILockCondition.sol";
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

contract PriceBasedCondition is ILockCondition {
    AggregatorV3Interface public priceFeed;
    
    struct PriceCondition {
        uint256 targetPrice;  // Target price (in feed decimals)
        bool above;           // True = unlock when price > target
    }
    
    mapping(address => mapping(uint256 => PriceCondition)) public conditions;
    
    constructor(address _priceFeed) {
        priceFeed = AggregatorV3Interface(_priceFeed);
    }
    
    function setCondition(
        address locker,
        uint256 lockId,
        uint256 targetPrice,
        bool above
    ) external {
        conditions[locker][lockId] = PriceCondition({
            targetPrice: targetPrice,
            above: above
        });
    }
    
    function isUnlocked(
        address locker,
        uint256 lockId
    ) external view override returns (bool) {
        PriceCondition memory condition = conditions[locker][lockId];
        if (condition.targetPrice == 0) return false;
        
        (, int256 price, , , ) = priceFeed.latestRoundData();
        
        if (condition.above) {
            return uint256(price) >= condition.targetPrice;
        } else {
            return uint256(price) <= condition.targetPrice;
        }
    }
}

Example 2: Governance Vote Condition

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {ILockCondition} from "../interfaces/ILockCondition.sol";

contract GovernanceCondition is ILockCondition {
    struct Proposal {
        uint256 votesFor;
        uint256 votesAgainst;
        uint256 quorum;
        bool executed;
    }
    
    mapping(address => mapping(uint256 => Proposal)) public proposals;
    
    function createProposal(
        address locker,
        uint256 lockId,
        uint256 quorum
    ) external {
        proposals[locker][lockId] = Proposal({
            votesFor: 0,
            votesAgainst: 0,
            quorum: quorum,
            executed: false
        });
    }
    
    function vote(
        address locker,
        uint256 lockId,
        bool support
    ) external {
        Proposal storage proposal = proposals[locker][lockId];
        
        if (support) {
            proposal.votesFor++;
        } else {
            proposal.votesAgainst++;
        }
    }
    
    function isUnlocked(
        address locker,
        uint256 lockId
    ) external view override returns (bool) {
        Proposal memory proposal = proposals[locker][lockId];
        
        uint256 totalVotes = proposal.votesFor + proposal.votesAgainst;
        
        return totalVotes >= proposal.quorum && 
               proposal.votesFor > proposal.votesAgainst;
    }
}

Example 3: Multi-Signature Condition

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {ILockCondition} from "../interfaces/ILockCondition.sol";

contract MultiSigCondition is ILockCondition {
    struct MultiSig {
        address[] signers;
        uint256 threshold;
        mapping(address => bool) signed;
        uint256 signCount;
    }
    
    mapping(address => mapping(uint256 => MultiSig)) public multiSigs;
    
    function createMultiSig(
        address locker,
        uint256 lockId,
        address[] memory signers,
        uint256 threshold
    ) external {
        require(threshold <= signers.length, "Invalid threshold");
        
        MultiSig storage ms = multiSigs[locker][lockId];
        ms.signers = signers;
        ms.threshold = threshold;
    }
    
    function sign(address locker, uint256 lockId) external {
        MultiSig storage ms = multiSigs[locker][lockId];
        require(!ms.signed[msg.sender], "Already signed");
        
        bool isSigner = false;
        for (uint i = 0; i < ms.signers.length; i++) {
            if (ms.signers[i] == msg.sender) {
                isSigner = true;
                break;
            }
        }
        require(isSigner, "Not a signer");
        
        ms.signed[msg.sender] = true;
        ms.signCount++;
    }
    
    function isUnlocked(
        address locker,
        uint256 lockId
    ) external view override returns (bool) {
        MultiSig storage ms = multiSigs[locker][lockId];
        return ms.signCount >= ms.threshold;
    }
}

Best Practices

For Condition Developers

  1. Fail Safely: If your condition reverts, the locker will fall back to time-based unlock

  2. Gas Efficiency: Keep isUnlocked() view function gas-efficient

  3. State Changes: Only modify state in setter functions, not in isUnlocked()

  4. Access Control: Implement proper access control for setter functions

  5. Testing: Thoroughly test all edge cases

For Users

  1. Verify Condition: Always verify the condition contract before using it

  2. Understand Logic: Make sure you understand how the condition works

  3. Fallback: Remember that time-based unlock still applies if condition fails

  4. Admin Trust: Be aware of who controls the condition contract

  5. Use Known Conditions: Prefer using well-tested, verified condition contracts


Security Considerations

Risks

  1. Malicious Conditions: A malicious condition could prevent unlocking

  2. Admin Control: Condition admin has significant power

  3. Condition Bugs: Bugs in condition logic could lock funds

  4. Gas Griefing: Expensive condition checks could make unlocking costly

Mitigations

Try/Catch Protection: Locker contracts wrap condition calls in try/catch blocks

if (lock.condition != address(0)) {
    try ILockCondition(lock.condition).isUnlocked(address(this), lockId) 
        returns (bool unlocked) {
        return unlocked;
    } catch {
        // Fall back to time-based unlock
        return false;
    }
}

Time-Based Fallback: Standard unlock time still applies

Optional Usage: Conditions are completely optional


Error Reference

error Unauthorized();        // Not authorized to set conditions
error InvalidTimestamp();    // Timestamp is invalid

Events

The TimeBasedCondition contract does not emit custom events, but you can add them in your own condition contracts:

event ConditionSet(address indexed locker, uint256 indexed lockId, uint256 timestamp);
event ConditionMet(address indexed locker, uint256 indexed lockId);
event AdminChanged(address indexed oldAdmin, address indexed newAdmin);

Gas Estimates

Function
Gas Cost

setUnlockTimestamp()

~45,000

isUnlocked() (view)

~5,000


Last updated