// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol)
pragma solidity ^0.8.0;
/**
* @dev Interface of the ERC20 standard as defined in the EIP.
*/
interface IERC20 {
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
/**
* @dev Returns the amount of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the amount of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves `amount` tokens from the caller's account to `to`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address to, uint256 amount) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 amount) external returns (bool);
/**
* @dev Moves `amount` tokens from `from` to `to` using the
* allowance mechanism. `amount` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract TokenLock {
address public immutable owner;
IERC20 public immutable token;
uint256 public immutable lockDuration;
uint256 public immutable cutoffTime;
uint256 public totalLocked;
struct LockInfo {
uint256 amount;
uint256 releaseTime;
}
mapping(address => LockInfo) public locks;
event Locked(address indexed user, uint256 amount, uint256 releaseTime);
event Withdrawn(address indexed user, uint256 amount);
event WithdrawSurplusTo(address indexed to, uint256 amount);
constructor(address _owner, address _token, uint256 _lockDuration, uint256 _cutoffTime) {
owner = _owner;
token = IERC20(_token);
lockDuration = _lockDuration;
cutoffTime = _cutoffTime;
}
function lock(uint256 _amount) external {
require(block.timestamp <= cutoffTime, "LOCKING_PERIOD_OVER");
require(_amount > 0, "WRONG_AMOUNT");
LockInfo storage userLock = locks[msg.sender];
userLock.amount += _amount;
userLock.releaseTime = block.timestamp + lockDuration;
totalLocked += _amount;
emit Locked(msg.sender, userLock.amount, userLock.releaseTime);
bool success = _makeTransferFrom(msg.sender, address(this), _amount);
require(success, "TRANSFER_FAILED");
}
function withdraw() external {
LockInfo storage userLock = locks[msg.sender];
require(userLock.amount > 0, "NO_LOCKED_TOKENS");
require(block.timestamp >= userLock.releaseTime, "LOCK_NOT_EXPIRED");
uint256 amount = userLock.amount;
userLock.amount = 0;
totalLocked -= amount;
bool success = _makeTransfer(msg.sender, amount);
require(success, "TRANSFER_FAILED");
emit Withdrawn(msg.sender, amount);
}
/**
* @dev Withdraws surplus tokens to the specified address. Normally there
* won't be any surplus and this function will revert. It is only useful
* after an accidental transfer of tokens to this contract that bypassed
* the lock function.
*/
function withdrawSurplusTo(address to) external onlyOwner {
require(to != address(0), "ZERO_ADDRESS");
uint256 balance = token.balanceOf(address(this));
require (totalLocked < balance, "NO_SURPLUS");
uint256 surplus = balance - totalLocked;
bool success = _makeTransfer(to, surplus);
require(success, "TRANSFER_FAILED");
emit WithdrawSurplusTo(to, surplus);
}
modifier onlyOwner() {
require(msg.sender == owner, "ONLY_OWNER");
_;
}
function _makeTransfer(
address to,
uint256 amount
) private returns (bool success) {
return
_tokenCall(
abi.encodeWithSelector(
token.transfer.selector,
to,
amount
)
);
}
function _makeTransferFrom(
address from,
address to,
uint256 amount
) private returns (bool success) {
return
_tokenCall(
abi.encodeWithSelector(
token.transferFrom.selector,
from,
to,
amount
)
);
}
function _tokenCall(bytes memory data) private returns (bool) {
(bool success, bytes memory returndata) = address(token).call(data);
if (success) {
if (returndata.length > 0) {
success = abi.decode(returndata, (bool));
} else {
success = address(token).code.length > 0;
}
}
return success;
}
}
{
"compilationTarget": {
"contracts/TokenLock.sol": "TokenLock"
},
"evmVersion": "shanghai",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"details": {
"constantOptimizer": true,
"cse": true,
"deduplicate": true,
"inliner": true,
"jumpdestRemover": true,
"orderLiterals": true,
"peephole": true,
"simpleCounterForLoopUncheckedIncrement": true,
"yul": true,
"yulDetails": {
"optimizerSteps": "u:fDnTOcmu",
"stackAllocation": true
}
},
"runs": 200
},
"remappings": [],
"viaIR": true
}
[{"inputs":[{"internalType":"address","name":"_owner","type":"address"},{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_lockDuration","type":"uint256"},{"internalType":"uint256","name":"_cutoffTime","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"releaseTime","type":"uint256"}],"name":"Locked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"WithdrawSurplusTo","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Withdrawn","type":"event"},{"inputs":[],"name":"cutoffTime","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"lock","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"lockDuration","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"locks","outputs":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"releaseTime","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"token","outputs":[{"internalType":"contract IERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalLocked","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"}],"name":"withdrawSurplusTo","outputs":[],"stateMutability":"nonpayable","type":"function"}]