BlackBoxTokenLocker

Description:

Decentralized Finance (DeFi) protocol contract providing Liquidity, Factory functionality.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

// SPDX-License-Identifier: GPL-3.0-or-later
// BlackBoxTokenLocker - v1.0
// admin@blackboxmint.com
/**
 * @title BlackBoxTokenLocker ("The Sacred Vault")
 * @notice Immutable, permissionless time-locker for standard ERC20 tokens. Once tokens are locked,
 *         they cannot be withdrawn before the unlock timestamp. Admin has no capability to access
 *         user-locked funds.
 * @dev Design highlights:
 *      - Standard ERC20 only: Fee-on-transfer/rebasing tokens are intentionally unsupported.
 *        Enforcement is via exact balance-delta verification in lockLiquidity.
 *      - Reentrancy-safe: Critical state-changing functions use a simple nonReentrant guard.
 *      - DoS-aware views: Convenience getters cap iteration to 500 for gas; predicates/pagination
 *        are provided unbounded or via explicit paging.
 *      - Admin scope: Admin can set a flat native fee, withdraw collected native fees, and withdraw
 *        non-locked ERC20 dust. Admin cannot modify, cancel, or seize any active user lock.
 *      - Transparency: Events are emitted for all state changes and admin actions.
 */

pragma solidity ^0.8.26;

interface ITOKEN {
    function decimals() external view returns (uint8);
    function totalSupply() external view returns (uint);
    function balanceOf(address owner) external view returns (uint);
    function transfer(address to, uint value) external returns (bool);
    function transferFrom(address from, address to, uint value) external returns (bool);
}

contract BlackBoxTokenLocker {
    /**
     * @dev A lock record describing an immutable token time-lock created by a user.
     * - token: ERC20 token being locked
     * - locker: creator/owner of the lock
     * - amount: exact token amount locked
     * - unlockTime: UNIX timestamp after which withdrawal is allowed
     * - description: optional short description (UI/metadata only)
     * - withdrawn: true once withdrawn
     * - lockId: unique sequential identifier
     */
    struct Lock {
        address token;
        address locker;
        uint256 amount;
        uint256 unlockTime;
        string description;
        bool withdrawn;
        uint256 lockId;
    }

    mapping(uint256 => Lock) public locks;
    mapping(address => uint256[]) public userLocks;
    mapping(address => uint256[]) public tokenLocks;
    mapping(address => uint256) public tokenBalanceLocked;

    // Sequential identifier for newly created locks (starts at 1 for clarity)
    uint256 public nextLockId = 1;
    // Total locks ever created (monotonic)
    uint256 public totalLocksCreated;
    // Aggregate amount currently locked across all tokens
    uint256 public totalValueLocked;
    // Flat native fee (in wei) required to create a lock
    uint256 public LOCK_FEE = 1000000000000000;
    // Upper bound to prevent unbounded per-user growth
    uint256 public constant MAX_LOCKS_PER_USER = 1000;
    // Maximum allowed lock duration (100 years)
    uint256 public constant MAX_LOCK_DURATION = 100 * 365 days;
    // Minimum lock duration buffer (1 hour)
    uint256 public constant MIN_LOCK_DURATION = 1 hours;
    // Max description length (bytes)
    uint256 public constant MAX_DESCRIPTION_LENGTH = 128;
    address private blackBoxAdmin;
    bool private _locked;

    // Core lifecycle events
    event LiquidityLocked(uint256 indexed lockId, address indexed token, address indexed locker, uint256 amount, uint256 unlockTime, string description);
    event LiquidityWithdrawn(uint256 indexed lockId, address indexed token, address indexed locker, uint256 amount);
    // Admin transparency events
    event FeeUpdated(uint256 oldFee, uint256 newFee);
    event FeeRefunded(address indexed user, uint256 refundAmount);
    event NativeWithdrawn(address indexed admin, uint256 amount);
    event EmergencyTokenWithdrawn(address indexed admin, address indexed token, uint256 amount);

    /// @dev Restricts a function to the contract admin.
    modifier blackBoxAdminOnly() {
        require(msg.sender == blackBoxAdmin, "Administration only");
        _;
    }
    /// @dev Simple non-reentrancy guard.
    modifier nonReentrant() {
        require(!_locked, "Reentrant call");
        _locked = true;
        _;
        _locked = false;
    }

    /// @notice Deploys the locker and sets the deploying address as admin.
    /// @dev Admin is immutable for user locks; it cannot seize or modify user funds.
    constructor() payable {
        blackBoxAdmin = msg.sender;
    }
    /// @notice Accepts direct native payments (e.g., fee top-ups). Zero-value transfers are rejected.
    receive() external payable {
        require(msg.value > 0, "no zeros");
    }
    /// @dev Reject all unknown function calls.
    fallback() external {
        revert("Invalid function call");
    }

    /**
     * @notice Lock tokens until a future timestamp.
     * @dev Standard ERC20 only. Fee-on-transfer/rebasing tokens are rejected by exact balance-delta check.
     *      Excess native payment above LOCK_FEE is refunded automatically.
     * @param _token ERC20 token address
     * @param _amount Amount of tokens to lock
     * @param _unlockTime UNIX timestamp strictly greater than now + MIN_LOCK_DURATION and within MAX_LOCK_DURATION
     * @param _description Optional short description (<= MAX_DESCRIPTION_LENGTH)
     * @custom:reverts Invalid token/address/amount/duration; insufficient fee; too many user locks; transfer failures
     * @custom:events LiquidityLocked, FeeRefunded(optional)
     */
    function lockLiquidity(address _token, uint256 _amount, uint256 _unlockTime, string memory _description) external payable nonReentrant {
        require(_token != address(0), "Invalid token address");
        require(_amount > 0, "Amount must be greater than 0");
        require(_unlockTime > block.timestamp + MIN_LOCK_DURATION, "Minimum lock duration not met");
        require(_unlockTime <= block.timestamp + MAX_LOCK_DURATION, "Maximum lock duration exceeded");
        require(msg.value >= LOCK_FEE, "Insufficient fee");
        require(bytes(_description).length <= MAX_DESCRIPTION_LENGTH, "Description too long");
        require(userLocks[msg.sender].length < MAX_LOCKS_PER_USER, "Too many locks per user");

        // Refund excess payment (if any)
        if (msg.value > LOCK_FEE) {
            uint256 refundAmount = msg.value - LOCK_FEE;
            (bool success, ) = msg.sender.call{value: refundAmount}("");
            require(success, "Refund failed");
            emit FeeRefunded(msg.sender, refundAmount);
        }
        // Exact-amount enforcement: rejects fee-on-transfer/rebasing tokens
        uint256 balanceBefore = ITOKEN(_token).balanceOf(address(this));
        require(ITOKEN(_token).transferFrom(msg.sender, address(this), _amount), "Token transfer failed");
        uint256 balanceAfter = ITOKEN(_token).balanceOf(address(this));
        uint256 actualAmount = balanceAfter - balanceBefore;
        require(actualAmount == _amount, "Transfer amount mismatch");
        locks[nextLockId] = Lock({token: _token, locker: msg.sender, amount: _amount, unlockTime: _unlockTime, description: _description, withdrawn: false, lockId: nextLockId});
        userLocks[msg.sender].push(nextLockId);
        tokenLocks[_token].push(nextLockId);
        totalLocksCreated++;
        totalValueLocked += _amount;
        tokenBalanceLocked[_token] += _amount;
        emit LiquidityLocked(nextLockId, _token, msg.sender, _amount, _unlockTime, _description);
        nextLockId++;
    }

    /**
     * @notice Withdraw tokens from a matured lock you own.
     * @param _lockId ID of the lock to withdraw
     * @custom:reverts If caller is not the locker, already withdrawn, or unlockTime not yet reached
     * @custom:events LiquidityWithdrawn
     */
    function withdrawLiquidity(uint256 _lockId) external nonReentrant {
        require(_lockId < nextLockId, "Invalid lock ID");
        Lock storage lock = locks[_lockId];
        require(lock.locker == msg.sender, "Not the locker");
        require(!lock.withdrawn, "Already withdrawn");
        require(block.timestamp >= lock.unlockTime, "Lock period not expired");
        lock.withdrawn = true;
        totalValueLocked -= lock.amount;
        tokenBalanceLocked[lock.token] -= lock.amount;
        require(ITOKEN(lock.token).transfer(msg.sender, lock.amount), "Token transfer failed");
        emit LiquidityWithdrawn(_lockId, lock.token, msg.sender, lock.amount);
    }

    /// @notice Fetch full details of a lock plus a convenience canWithdraw flag.
    /// @param _lockId Lock identifier
    /// @return token The token address
    /// @return locker The locker address
    /// @return amount The locked token amount
    /// @return unlockTime The unlock timestamp
    /// @return description Optional description
    /// @return withdrawn Whether already withdrawn
    /// @return canWithdraw Convenience predicate: now >= unlockTime and not withdrawn
    function getLockInfo(uint256 _lockId) external view returns (address token, address locker, uint256 amount, uint256 unlockTime, string memory description, bool withdrawn, bool canWithdraw) {
        require(_lockId < nextLockId, "Invalid lock ID");
        Lock memory lock = locks[_lockId];
        return (lock.token, lock.locker, lock.amount, lock.unlockTime, lock.description, lock.withdrawn, block.timestamp >= lock.unlockTime && !lock.withdrawn);
    }

    /// @notice Get all lock IDs created by a user.
    /// @param _user Address to query
    /// @return lockIds Array of lock IDs
    function getUserLocks(address _user) external view returns (uint256[] memory lockIds) {
        return userLocks[_user];
    }

    /// @notice Get all lock IDs associated with a token.
    /// @param _token Token to query
    /// @return lockIds Array of lock IDs
    function getTokenLocks(address _token) external view returns (uint256[] memory lockIds) {
        return tokenLocks[_token];
    }

    /// @notice Convenience: up to 500 active (not withdrawn) lock IDs for a user.
    /// @dev This function caps iteration to 500 for gas safety on-chain.
    /// @param _user Address to query
    /// @return lockIds Up to 500 active lock IDs
    function getUserActiveLocks(address _user) external view returns (uint256[] memory lockIds) {
        uint256[] memory allLocks = userLocks[_user];
        uint256 length = allLocks.length;
        if (length > 500) {
            length = 500;
        }
        uint256 activeCount = 0;
        for (uint256 i = 0; i < length; i++) {
            if (!locks[allLocks[i]].withdrawn) {
                activeCount++;
            }
        }
        uint256[] memory activeLocks = new uint256[](activeCount);
        uint256 index = 0;
        for (uint256 i = 0; i < length; i++) {
            if (!locks[allLocks[i]].withdrawn) {
                activeLocks[index] = allLocks[i];
                index++;
            }
        }
        return activeLocks;
    }

    /// @notice Convenience: up to 500 active (not withdrawn) lock IDs for a token.
    /// @dev This function caps iteration to 500 for gas safety on-chain.
    /// @param _token Token to query
    /// @return lockIds Up to 500 active lock IDs
    function getTokenActiveLocks(address _token) external view returns (uint256[] memory lockIds) {
        uint256[] memory allLocks = tokenLocks[_token];
        uint256 length = allLocks.length;
        if (length > 500) {
            length = 500;
        }
        uint256 activeCount = 0;
        for (uint256 i = 0; i < length; i++) {
            if (!locks[allLocks[i]].withdrawn) {
                activeCount++;
            }
        }
        uint256[] memory activeLocks = new uint256[](activeCount);
        uint256 index = 0;
        for (uint256 i = 0; i < length; i++) {
            if (!locks[allLocks[i]].withdrawn) {
                activeLocks[index] = allLocks[i];
                index++;
            }
        }
        return activeLocks;
    }

    /// @notice Predicate: returns true if there exists at least one active (not withdrawn) lock for the token whose unlockTime is in the future.
    /// @dev Unbounded scan to guarantee correctness.
    /// @param _token Token to query
    /// @return locked True if any active lock exists
    function isTokenLocked(address _token) external view returns (bool locked) {
        uint256[] memory tokenLockIds = tokenLocks[_token];

        for (uint256 i = 0; i < tokenLockIds.length; i++) {
            Lock memory lock = locks[tokenLockIds[i]];
            if (!lock.withdrawn && block.timestamp < lock.unlockTime) {
                return true;
            }
        }
        return false;
    }

    /// @notice Total amount of a token locked across all active locks.
    /// @param _token Token to query
    /// @return amount Total locked amount
    function getTokenTotalLocked(address _token) external view returns (uint256 amount) {
        return tokenBalanceLocked[_token];
    }

    /// @notice Admin: set the flat native fee required to create a lock.
    /// @param _fee New fee in wei
    function setLockFee(uint256 _fee) external blackBoxAdminOnly {
        uint256 oldFee = LOCK_FEE;
        LOCK_FEE = _fee;
        emit FeeUpdated(oldFee, _fee);
    }

    /// @notice Admin: withdraw all native fees collected by the contract.
    function withdrawNative() external blackBoxAdminOnly {
        require(address(this).balance > 0, "Balance is zero");
        uint256 amount = address(this).balance;
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Withdrawal failed");
        emit NativeWithdrawn(msg.sender, amount);
    }

    /// @notice Admin: withdraw a specific amount of native fees.
    /// @param _amount Amount in wei to withdraw
    function withdrawNativeAmount(uint256 _amount) external blackBoxAdminOnly {
        require(_amount > 0, "Cannot withdraw zero");
        require(address(this).balance >= _amount, "Amount exceeds balance");
        (bool success, ) = msg.sender.call{value: _amount}("");
        require(success, "Withdrawal failed");
        emit NativeWithdrawn(msg.sender, _amount);
    }

    /// @notice Current native balance (fees) held by the contract.
    /// @return amount Native balance in wei
    function nativeBalance() external view returns (uint256 amount) {
        return address(this).balance;
    }

    /**
     * @notice Admin: withdraw non-locked ERC20 tokens (dust) from the contract.
     * @dev Cannot withdraw any portion of user-locked balances. Uses balance - totalLocked to compute availability.
     * @param _token ERC20 token to withdraw
     * @param _amount Amount to transfer out (must be <= available)
     * @custom:reverts If requested amount exceeds available (non-locked) balance
     * @custom:events EmergencyTokenWithdrawn
     */
    function emergencyWithdrawToken(address _token, uint256 _amount) external blackBoxAdminOnly nonReentrant {
        uint256 contractBalance = ITOKEN(_token).balanceOf(address(this));
        uint256 totalLocked = tokenBalanceLocked[_token];
        require(contractBalance > totalLocked, "No available balance");
        uint256 availableBalance = contractBalance - totalLocked;
        require(_amount > 0, "Amount must be greater than 0");
        require(_amount <= availableBalance, "Cannot withdraw locked tokens");
        require(ITOKEN(_token).transfer(msg.sender, _amount), "Token transfer failed");
        emit EmergencyTokenWithdrawn(msg.sender, _token, _amount);
    }

    /// @notice View helper: amount of non-locked ERC20 that can be withdrawn by admin via emergencyWithdrawToken.
    /// @param _token Token to query
    /// @return amount Available (non-locked) token balance
    function getAvailableBalance(address _token) external view returns (uint256 amount) {
        uint256 contractBalance = ITOKEN(_token).balanceOf(address(this));
        uint256 totalLocked = tokenBalanceLocked[_token];
        return contractBalance > totalLocked ? contractBalance - totalLocked : 0;
    }

    // Paginated getters for on-chain usage
    /// @notice Page through a user's lock IDs.
    /// @param _user Address to query
    /// @param _offset Start index (0-based)
    /// @param _limit Max items to return
    /// @return lockIds Slice of lock IDs in range [_offset, min(_offset+_limit, total))
    function getUserLocksPaginated(address _user, uint256 _offset, uint256 _limit) external view returns (uint256[] memory lockIds) {
        uint256[] memory allLocks = userLocks[_user];
        if (_offset >= allLocks.length) return new uint256[](0);

        uint256 end = _offset + _limit;
        if (end > allLocks.length) end = allLocks.length;

        uint256[] memory result = new uint256[](end - _offset);
        for (uint256 i = _offset; i < end; i++) {
            result[i - _offset] = allLocks[i];
        }
        return result;
    }

    /// @notice Page through a token's lock IDs.
    /// @param _token Token to query
    /// @param _offset Start index (0-based)
    /// @param _limit Max items to return
    /// @return lockIds Slice of lock IDs in range [_offset, min(_offset+_limit, total))
    function getTokenLocksPaginated(address _token, uint256 _offset, uint256 _limit) external view returns (uint256[] memory lockIds) {
        uint256[] memory allLocks = tokenLocks[_token];
        if (_offset >= allLocks.length) return new uint256[](0);

        uint256 end = _offset + _limit;
        if (end > allLocks.length) end = allLocks.length;

        uint256[] memory result = new uint256[](end - _offset);
        for (uint256 i = _offset; i < end; i++) {
            result[i - _offset] = allLocks[i];
        }
        return result;
    }
}

Tags:
DeFi, Liquidity, Factory|addr:0x5e4585cbd6e839181e87eba24d3a0d9d8678ec95|verified:true|block:23391680|tx:0xbf4a486c4422c8c0dc3b87af9165c779c7166b6dd95f18c0114d95da2da3eca7|first_check:1758272541

Submitted on: 2025-09-19 11:02:22

Comments

Log in to comment.

No comments yet.