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;
}
}
Submitted on: 2025-09-19 11:02:22
Comments
Log in to comment.
No comments yet.