# Conditional Unlocks

Advanced guide to implementing and using conditional unlock mechanisms in RealSafe.

## Overview

Conditional unlocks allow you to create custom logic for when positions can be unlocked, beyond simple time-based locks. This opens up powerful use cases for DeFi protocols, DAOs, and advanced liquidity management strategies.

## Table of Contents

1. [Understanding Conditions](#understanding-conditions)
2. [Built-in Conditions](#built-in-conditions)
3. [Custom Condition Examples](#custom-condition-examples)
4. [Advanced Patterns](#advanced-patterns)
5. [Security Considerations](#security-considerations)
6. [Best Practices](#best-practices)

***

## Understanding Conditions

### What is a Condition?

A condition is a smart contract that implements the `ILockCondition` interface:

```solidity
interface ILockCondition {
    function isUnlocked(address locker, uint256 lockId) external view returns (bool);
}
```

When a lock has a condition set:

1. **Time Check**: Standard unlock time is checked first
2. **Condition Check**: If time hasn't passed, condition is checked
3. **Either/Or**: Lock can be unlocked if EITHER condition is true

### How Conditions Are Used

```solidity
// When creating a lock with condition
lockPosition(
    tokenId,
    unlockDate,         // Standard time-based unlock
    collectAddress,
    conditionAddress    // Custom condition contract
);

// When unlocking
function _canUnlock(Lock storage lock) internal view returns (bool) {
    // Check 1: Time-based
    if (block.timestamp >= lock.unlockDate) {
        return true;
    }
    
    // Check 2: Condition-based
    if (lock.condition != address(0)) {
        try ILockCondition(lock.condition).isUnlocked(address(this), lockId) {
            returns (bool unlocked) {
            return unlocked;
        } catch {
            return false;  // Safe fallback
        }
    }
    
    return false;
}
```

### Key Benefits

* **Flexibility**: Unlock based on any on-chain logic
* **Composability**: Combine multiple conditions
* **Safety**: Falls back to time-based if condition fails
* **Transparency**: All logic is on-chain and verifiable

***

## Built-in Conditions

### TimeBasedCondition

Simple admin-controlled unlock timestamps.

**Deployment Addresses:**

* Monad Testnet: `0x3F400BaE5037474C3b8531CC61Cd177589093C9f`
* Monad Testnet: `0x3F400BaE5037474C3b8531CC61Cd177589093C9f`

**Use Case**: Admin wants fine-grained control over individual lock unlock times.

```solidity
// Lock with time condition
uint256 lockId = v3Locker.lockPosition{value: fee}(
    tokenId,
    block.timestamp + 365 days,  // Max time
    msg.sender,
    timeConditionAddress
);

// Admin can set earlier unlock time
timeCondition.setUnlockTimestamp(
    v3LockerAddress,
    lockId,
    block.timestamp + 30 days    // Unlock in 30 days instead
);
```

***

## Custom Condition Examples

### 1. Price-Based Unlock

Unlock when an asset reaches a target price.

```solidity
// 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 PriceCondition is ILockCondition {
    AggregatorV3Interface public immutable priceFeed;
    address public immutable admin;
    
    struct PriceTarget {
        int256 targetPrice;
        bool above;  // true = unlock when price above, false = below
    }
    
    mapping(address => mapping(uint256 => PriceTarget)) public targets;
    
    constructor(address _priceFeed) {
        priceFeed = AggregatorV3Interface(_priceFeed);
        admin = msg.sender;
    }
    
    function setTarget(
        address locker,
        uint256 lockId,
        int256 targetPrice,
        bool above
    ) external {
        require(msg.sender == admin, "Not admin");
        targets[locker][lockId] = PriceTarget(targetPrice, above);
    }
    
    function isUnlocked(address locker, uint256 lockId) 
        external 
        view 
        override 
        returns (bool) 
    {
        PriceTarget memory target = targets[locker][lockId];
        if (target.targetPrice == 0) return false;
        
        (, int256 price, , , ) = priceFeed.latestRoundData();
        
        return target.above ? 
            price >= target.targetPrice : 
            price <= target.targetPrice;
    }
}
```

**Usage:**

```solidity
// Deploy price condition
PriceCondition priceCondition = new PriceCondition(ethUsdPriceFeed);

// Lock: unlock when ETH > $5000
priceCondition.setTarget(
    v3LockerAddress,
    lockId,
    5000 * 1e8,  // $5000 (8 decimals)
    true         // above
);
```

***

### 2. TVL-Based Unlock

Unlock when protocol TVL reaches a threshold.

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

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

interface ITVLOracle {
    function getTVL() external view returns (uint256);
}

contract TVLCondition is ILockCondition {
    ITVLOracle public immutable tvlOracle;
    
    mapping(address => mapping(uint256 => uint256)) public tvlThresholds;
    
    constructor(address _tvlOracle) {
        tvlOracle = ITVLOracle(_tvlOracle);
    }
    
    function setThreshold(
        address locker,
        uint256 lockId,
        uint256 tvlThreshold
    ) external {
        tvlThresholds[locker][lockId] = tvlThreshold;
    }
    
    function isUnlocked(address locker, uint256 lockId) 
        external 
        view 
        override 
        returns (bool) 
    {
        uint256 threshold = tvlThresholds[locker][lockId];
        if (threshold == 0) return false;
        
        uint256 currentTVL = tvlOracle.getTVL();
        return currentTVL >= threshold;
    }
}
```

***

### 3. Multi-Signature Unlock

Require multiple signatures to unlock.

```solidity
// 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;
        uint256 signCount;
        mapping(address => bool) hasSigned;
    }
    
    mapping(address => mapping(uint256 => MultiSig)) private multiSigs;
    
    event MultiSigCreated(address indexed locker, uint256 indexed lockId);
    event Signed(address indexed locker, uint256 indexed lockId, address indexed signer);
    
    function createMultiSig(
        address locker,
        uint256 lockId,
        address[] calldata signers,
        uint256 threshold
    ) external {
        require(signers.length > 0, "No signers");
        require(threshold > 0 && threshold <= signers.length, "Invalid threshold");
        
        MultiSig storage ms = multiSigs[locker][lockId];
        ms.signers = signers;
        ms.threshold = threshold;
        
        emit MultiSigCreated(locker, lockId);
    }
    
    function sign(address locker, uint256 lockId) external {
        MultiSig storage ms = multiSigs[locker][lockId];
        require(!ms.hasSigned[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.hasSigned[msg.sender] = true;
        ms.signCount++;
        
        emit Signed(locker, lockId, msg.sender);
    }
    
    function isUnlocked(address locker, uint256 lockId) 
        external 
        view 
        override 
        returns (bool) 
    {
        MultiSig storage ms = multiSigs[locker][lockId];
        return ms.signCount >= ms.threshold;
    }
    
    function getSignCount(address locker, uint256 lockId) 
        external 
        view 
        returns (uint256) 
    {
        return multiSigs[locker][lockId].signCount;
    }
}
```

**Usage:**

```solidity
// Create 3-of-5 multisig
address[] memory signers = new address[](5);
signers[0] = address1;
signers[1] = address2;
signers[2] = address3;
signers[3] = address4;
signers[4] = address5;

multiSigCondition.createMultiSig(
    v3LockerAddress,
    lockId,
    signers,
    3  // 3 signatures required
);

// Each signer calls
multiSigCondition.sign(v3LockerAddress, lockId);
```

***

### 4. Governance Vote Condition

Unlock based on DAO vote outcome.

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

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

interface IGovernor {
    enum ProposalState { Pending, Active, Canceled, Defeated, Succeeded, Queued, Expired, Executed }
    function state(uint256 proposalId) external view returns (ProposalState);
}

contract GovernanceCondition is ILockCondition {
    IGovernor public immutable governor;
    
    mapping(address => mapping(uint256 => uint256)) public proposalIds;
    
    constructor(address _governor) {
        governor = IGovernor(_governor);
    }
    
    function setProposal(
        address locker,
        uint256 lockId,
        uint256 proposalId
    ) external {
        proposalIds[locker][lockId] = proposalId;
    }
    
    function isUnlocked(address locker, uint256 lockId) 
        external 
        view 
        override 
        returns (bool) 
    {
        uint256 proposalId = proposalIds[locker][lockId];
        if (proposalId == 0) return false;
        
        IGovernor.ProposalState state = governor.state(proposalId);
        return state == IGovernor.ProposalState.Executed;
    }
}
```

***

### 5. Milestone-Based Unlock

Unlock when project milestones are achieved.

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

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

contract MilestoneCondition is ILockCondition {
    address public immutable admin;
    
    struct Milestone {
        string description;
        bool achieved;
        uint256 achievedAt;
    }
    
    mapping(address => mapping(uint256 => Milestone[])) public milestones;
    mapping(address => mapping(uint256 => uint256)) public requiredMilestones;
    
    event MilestoneAdded(address indexed locker, uint256 indexed lockId, string description);
    event MilestoneAchieved(address indexed locker, uint256 indexed lockId, uint256 milestoneIndex);
    
    constructor() {
        admin = msg.sender;
    }
    
    function addMilestone(
        address locker,
        uint256 lockId,
        string calldata description
    ) external {
        require(msg.sender == admin, "Not admin");
        
        milestones[locker][lockId].push(Milestone({
            description: description,
            achieved: false,
            achievedAt: 0
        }));
        
        emit MilestoneAdded(locker, lockId, description);
    }
    
    function setRequiredMilestones(
        address locker,
        uint256 lockId,
        uint256 required
    ) external {
        require(msg.sender == admin, "Not admin");
        requiredMilestones[locker][lockId] = required;
    }
    
    function achieveMilestone(
        address locker,
        uint256 lockId,
        uint256 milestoneIndex
    ) external {
        require(msg.sender == admin, "Not admin");
        
        Milestone storage milestone = milestones[locker][lockId][milestoneIndex];
        require(!milestone.achieved, "Already achieved");
        
        milestone.achieved = true;
        milestone.achievedAt = block.timestamp;
        
        emit MilestoneAchieved(locker, lockId, milestoneIndex);
    }
    
    function isUnlocked(address locker, uint256 lockId) 
        external 
        view 
        override 
        returns (bool) 
    {
        uint256 required = requiredMilestones[locker][lockId];
        if (required == 0) return false;
        
        Milestone[] storage lockMilestones = milestones[locker][lockId];
        uint256 achieved = 0;
        
        for (uint i = 0; i < lockMilestones.length; i++) {
            if (lockMilestones[i].achieved) {
                achieved++;
            }
        }
        
        return achieved >= required;
    }
    
    function getAchievedCount(address locker, uint256 lockId) 
        external 
        view 
        returns (uint256) 
    {
        Milestone[] storage lockMilestones = milestones[locker][lockId];
        uint256 achieved = 0;
        
        for (uint i = 0; i < lockMilestones.length; i++) {
            if (lockMilestones[i].achieved) {
                achieved++;
            }
        }
        
        return achieved;
    }
}
```

***

## Advanced Patterns

### Composite Conditions

Combine multiple conditions with AND/OR logic.

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

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

contract CompositeCondition is ILockCondition {
    enum LogicOperator { AND, OR }
    
    struct Composite {
        address[] conditions;
        LogicOperator operator;
    }
    
    mapping(address => mapping(uint256 => Composite)) public composites;
    
    function setComposite(
        address locker,
        uint256 lockId,
        address[] calldata conditions,
        LogicOperator operator
    ) external {
        composites[locker][lockId] = Composite(conditions, operator);
    }
    
    function isUnlocked(address locker, uint256 lockId) 
        external 
        view 
        override 
        returns (bool) 
    {
        Composite storage composite = composites[locker][lockId];
        if (composite.conditions.length == 0) return false;
        
        if (composite.operator == LogicOperator.AND) {
            // All conditions must be true
            for (uint i = 0; i < composite.conditions.length; i++) {
                try ILockCondition(composite.conditions[i]).isUnlocked(locker, lockId) 
                    returns (bool unlocked) {
                    if (!unlocked) return false;
                } catch {
                    return false;
                }
            }
            return true;
        } else {
            // Any condition can be true
            for (uint i = 0; i < composite.conditions.length; i++) {
                try ILockCondition(composite.conditions[i]).isUnlocked(locker, lockId) 
                    returns (bool unlocked) {
                    if (unlocked) return true;
                } catch {
                    continue;
                }
            }
            return false;
        }
    }
}
```

***

## Security Considerations

### Risks

1. **Malicious Conditions**: Could permanently lock funds
2. **Admin Control**: Centralization risk
3. **Gas Griefing**: Expensive condition checks
4. **Oracle Manipulation**: Price/data manipulation
5. **Reentrancy**: If condition calls external contracts

### Mitigations

* **Try/Catch Wrapper**: Locker wraps condition calls
* **Time-Based Fallback**: Standard unlock always available
* **View Functions Only**: `isUnlocked()` should be view
* **Gas Limits**: Keep checks simple
* **Audited Conditions**: Use verified contracts only

***

## Best Practices

### For Condition Developers

1. Keep `isUnlocked()` simple and gas-efficient
2. Use `view` modifier (no state changes in unlock check)
3. Handle edge cases gracefully
4. Implement proper access control
5. Add comprehensive events
6. Document all functions
7. Test thoroughly

### For Users

1. Verify condition contract code
2. Understand the unlock logic
3. Check admin addresses
4. Test with small amounts first
5. Use time-based fallback as safety net
6. Monitor condition state

***

## Testing Conditions

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

import {Test} from "forge-std/Test.sol";
import {PriceCondition} from "../src/conditions/PriceCondition.sol";

contract PriceConditionTest is Test {
    PriceCondition condition;
    address mockPriceFeed;
    
    function setUp() public {
        // Deploy mock price feed
        mockPriceFeed = address(new MockPriceFeed());
        condition = new PriceCondition(mockPriceFeed);
    }
    
    function testUnlockWhenPriceAboveTarget() public {
        // Set target price
        condition.setTarget(
            address(this),
            1,
            3000 * 1e8,  // $3000
            true          // above
        );
        
        // Set current price below target
        MockPriceFeed(mockPriceFeed).setPrice(2900 * 1e8);
        assertFalse(condition.isUnlocked(address(this), 1));
        
        // Set price above target
        MockPriceFeed(mockPriceFeed).setPrice(3100 * 1e8);
        assertTrue(condition.isUnlocked(address(this), 1));
    }
}
```

***

## Related Documentation

* [Time-Based Condition](https://docs.realsafe.app/interfaces/timebased)
* [V3 Locker Interface](https://docs.realsafe.app/interfaces/v3)
* [V2 Locker Interface](https://docs.realsafe.app/interfaces/v2)
* [Security Guide](https://docs.realsafe.app/reference/security)
