文件 1 的 4:ITokenVesting.sol
pragma solidity ^0.8.7;
interface ITokenVesting {
event OwnershipTransferPending(address indexed owner, address indexed pendingOwner);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
function owner() external view returns (address owner_);
function pendingOwner() external view returns (address pendingOwner_);
function renounceOwnership() external;
function transferOwnership(address newOwner_) external;
function acceptOwnership() external;
event VestingScheduleSet(address indexed receiver_);
event VestingFunded(uint256 totalTokens_);
event ReceiverChanged(address indexed oldReceiver, address indexed newReceiver);
event VestingKilled(address indexed receiver_, uint256 tokensClaimed_, address indexed destination_);
event TokensClaimed(address indexed receiver_, uint256 tokensClaimed_, address indexed destination_);
struct VestingSchedule {
uint256 startTime;
uint256 cliff;
uint256 totalPeriods;
uint256 timePerPeriod;
uint256 totalTokens;
uint256 tokensClaimed;
}
function token() external returns (address token_);
function totalVestingsTokens() external returns (uint256 totalVestingsTokens_);
function vestingScheduleOf(address receiver_) external returns (
uint256 startTime_,
uint256 cliff_,
uint256 totalPeriods_,
uint256 timePerPeriod_,
uint256 totalTokens_,
uint256 tokensClaimed_
);
function setVestingSchedules(address[] calldata receivers_, VestingSchedule[] calldata vestings_) external;
function fundVesting(uint256 totalTokens_) external;
function changeReceiver(address oldReceiver_, address newReceiver_) external;
function claimableTokens(address receiver_) external view returns (uint256 claimableTokens_);
function claimTokens(address destination_) external;
function killVesting(address receiver_, address destination_) external;
event RecoveredToken(address indexed token, uint256 amount, address indexed destination);
function recoverToken(address token_, address destination_) external;
}
文件 2 的 4:Interfaces.sol
pragma solidity ^0.8.7;
interface IERC20Like {
function balanceOf(address account_) external view returns (uint256 balance_);
function transfer(address recipient_, uint256 amount_) external returns (bool success_);
function allowance(address owner_, address spender_) external view returns (uint256 allowance_);
function approve(address spender_, uint256 amount_) external returns (bool success_);
function transferFrom(address sender_, address recipient_, uint256 amount_) external returns (bool success_);
}
文件 3 的 4:MockERC20.sol
pragma solidity ^0.8.7;
contract MockERC20 {
string public name;
string public symbol;
uint8 public immutable decimals;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
constructor(string memory name_, string memory symbol_, uint8 decimals_) {
name = name_;
symbol = symbol_;
decimals = decimals_;
}
function approve(address spender_, uint256 amount_) external returns (bool success_) {
_approve(msg.sender, spender_, amount_);
return true;
}
function transfer(address to_, uint256 amount_) external returns (bool success_) {
_transfer(msg.sender, to_, amount_);
return true;
}
function transferFrom(address owner_, address recipient_, uint256 amount_) external returns (bool success_) {
_approve(owner_, msg.sender, allowance[owner_][msg.sender] - amount_);
_transfer(owner_, recipient_, amount_);
return true;
}
function mint(address recipient_, uint256 amount_) external returns (bool success_) {
_mint(recipient_, amount_);
return true;
}
function burn(address owner_, uint256 amount_) internal returns (bool success_) {
_burn(owner_, amount_);
return true;
}
function _approve(address owner_, address spender_, uint256 amount_) internal {
allowance[owner_][spender_] = amount_;
}
function _transfer(address owner_, address recipient_, uint256 amount_) internal {
balanceOf[owner_] -= amount_;
balanceOf[recipient_] += amount_;
}
function _mint(address recipient_, uint256 amount_) internal {
totalSupply += amount_;
balanceOf[recipient_] += amount_;
}
function _burn(address owner_, uint256 amount_) internal {
balanceOf[owner_] -= amount_;
totalSupply -= amount_;
}
}
文件 4 的 4:TokenVesting.sol
pragma solidity ^0.8.7;
import { IERC20Like } from "./Interfaces.sol";
import { ITokenVesting } from "./ITokenVesting.sol";
contract TokenVesting is ITokenVesting {
address public override owner;
address public override pendingOwner;
address public override token;
uint256 public override totalVestingsTokens;
mapping(address => VestingSchedule) public override vestingScheduleOf;
constructor(address token_) {
owner = msg.sender;
token = token_;
}
modifier onlyOwner() {
require(owner == msg.sender, "TV:NOT_OWNER");
_;
}
function renounceOwnership() external override onlyOwner {
pendingOwner = owner = address(0);
emit OwnershipTransferred(msg.sender, address(0));
}
function transferOwnership(address newOwner_) external override onlyOwner {
pendingOwner = newOwner_;
emit OwnershipTransferPending(msg.sender, newOwner_);
}
function acceptOwnership() external override {
require(pendingOwner == msg.sender, "TV:NOT_PENDING_OWNER");
emit OwnershipTransferred(owner, msg.sender);
owner = msg.sender;
pendingOwner = address(0);
}
function setVestingSchedules(address[] calldata receivers_, VestingSchedule[] calldata vestingSchedules_) external override onlyOwner {
for (uint256 i; i < vestingSchedules_.length; ++i) {
address receiver = receivers_[i];
vestingScheduleOf[receiver] = vestingSchedules_[i];
emit VestingScheduleSet(receiver);
}
}
function fundVesting(uint256 totalTokens_) external override onlyOwner {
require(totalVestingsTokens == uint256(0), "TV:ALREADY_FUNDED");
_safeTransferFrom(token, msg.sender, address(this), totalTokens_);
totalVestingsTokens = totalTokens_;
emit VestingFunded(totalTokens_);
}
function changeReceiver(address oldReceiver_, address newReceiver_) external override onlyOwner {
vestingScheduleOf[address(0)] = vestingScheduleOf[oldReceiver_];
vestingScheduleOf[oldReceiver_] = vestingScheduleOf[newReceiver_];
vestingScheduleOf[newReceiver_] = vestingScheduleOf[address(0)];
delete vestingScheduleOf[address(0)];
emit ReceiverChanged(oldReceiver_, newReceiver_);
}
function claimableTokens(address receiver_) public view override returns (uint256 claimableTokens_) {
VestingSchedule storage vestingSchedule = vestingScheduleOf[receiver_];
uint256 totalPeriods = vestingSchedule.totalPeriods;
if (totalPeriods == uint256(0)) return uint256(0);
uint256 timePassed = block.timestamp - vestingSchedule.startTime;
uint256 cliff = vestingSchedule.cliff;
if (timePassed <= cliff) return uint256(0);
uint256 multiplier = (timePassed - cliff) / vestingSchedule.timePerPeriod;
return
(
(
(
multiplier > totalPeriods ? totalPeriods : multiplier
)
* vestingSchedule.totalTokens
)
/ totalPeriods
)
- vestingSchedule.tokensClaimed;
}
function claimTokens(address destination_) external override {
require(totalVestingsTokens > uint256(0), "TV:NOT_FUNDED");
VestingSchedule storage vestingSchedule = vestingScheduleOf[msg.sender];
uint256 tokensToClaim = claimableTokens(msg.sender);
require(tokensToClaim > uint256(0), "TV:NO_CLAIMABLE");
vestingSchedule.tokensClaimed += tokensToClaim;
_safeTransfer(token, destination_, tokensToClaim);
emit TokensClaimed(msg.sender, tokensToClaim, destination_);
}
function killVesting(address receiver_, address destination_) external override onlyOwner {
VestingSchedule storage vestingSchedule = vestingScheduleOf[receiver_];
uint256 totalTokens = vestingSchedule.totalTokens;
uint256 tokensToClaim = totalTokens - vestingSchedule.tokensClaimed;
vestingScheduleOf[receiver_].tokensClaimed = totalTokens;
_safeTransfer(token, destination_, tokensToClaim);
emit VestingKilled(receiver_, tokensToClaim, destination_);
}
function recoverToken(address token_, address destination_) external override onlyOwner {
require(token_ != token, "TV:CANNOT_RECOVER_VESTING_TOKEN");
uint256 amount = IERC20Like(token_).balanceOf(address(this));
require(amount > uint256(0), "TV:NO_TOKEN");
_safeTransfer(token_, destination_, amount);
emit RecoveredToken(token_, amount, destination_);
}
function _safeTransfer(address token_, address to_, uint256 amount_) internal {
( bool success, bytes memory data ) = token_.call(abi.encodeWithSelector(IERC20Like.transfer.selector, to_, amount_));
require(success && (data.length == uint256(0) || abi.decode(data, (bool))), 'TV:SAFE_TRANSFER_FAILED');
}
function _safeTransferFrom(address token_, address from_, address to_, uint256 amount_) internal {
( bool success, bytes memory data ) = token_.call(abi.encodeWithSelector(IERC20Like.transferFrom.selector, from_, to_, amount_));
require(success && (data.length == uint256(0) || abi.decode(data, (bool))), 'TV:SAFE_TRANSFER_FROM_FAILED');
}
}
{
"compilationTarget": {
"TokenVesting.sol": "TokenVesting"
},
"evmVersion": "london",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": true,
"runs": 200
},
"remappings": []
}
[{"inputs":[{"internalType":"address","name":"token_","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"pendingOwner","type":"address"}],"name":"OwnershipTransferPending","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"oldReceiver","type":"address"},{"indexed":true,"internalType":"address","name":"newReceiver","type":"address"}],"name":"ReceiverChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":true,"internalType":"address","name":"destination","type":"address"}],"name":"RecoveredToken","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"receiver_","type":"address"},{"indexed":false,"internalType":"uint256","name":"tokensClaimed_","type":"uint256"},{"indexed":true,"internalType":"address","name":"destination_","type":"address"}],"name":"TokensClaimed","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"totalTokens_","type":"uint256"}],"name":"VestingFunded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"receiver_","type":"address"},{"indexed":false,"internalType":"uint256","name":"tokensClaimed_","type":"uint256"},{"indexed":true,"internalType":"address","name":"destination_","type":"address"}],"name":"VestingKilled","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"receiver_","type":"address"}],"name":"VestingScheduleSet","type":"event"},{"inputs":[],"name":"acceptOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"oldReceiver_","type":"address"},{"internalType":"address","name":"newReceiver_","type":"address"}],"name":"changeReceiver","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"destination_","type":"address"}],"name":"claimTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"receiver_","type":"address"}],"name":"claimableTokens","outputs":[{"internalType":"uint256","name":"claimableTokens_","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"totalTokens_","type":"uint256"}],"name":"fundVesting","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"receiver_","type":"address"},{"internalType":"address","name":"destination_","type":"address"}],"name":"killVesting","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pendingOwner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"token_","type":"address"},{"internalType":"address","name":"destination_","type":"address"}],"name":"recoverToken","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"receivers_","type":"address[]"},{"components":[{"internalType":"uint256","name":"startTime","type":"uint256"},{"internalType":"uint256","name":"cliff","type":"uint256"},{"internalType":"uint256","name":"totalPeriods","type":"uint256"},{"internalType":"uint256","name":"timePerPeriod","type":"uint256"},{"internalType":"uint256","name":"totalTokens","type":"uint256"},{"internalType":"uint256","name":"tokensClaimed","type":"uint256"}],"internalType":"struct ITokenVesting.VestingSchedule[]","name":"vestingSchedules_","type":"tuple[]"}],"name":"setVestingSchedules","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"token","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalVestingsTokens","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner_","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"vestingScheduleOf","outputs":[{"internalType":"uint256","name":"startTime","type":"uint256"},{"internalType":"uint256","name":"cliff","type":"uint256"},{"internalType":"uint256","name":"totalPeriods","type":"uint256"},{"internalType":"uint256","name":"timePerPeriod","type":"uint256"},{"internalType":"uint256","name":"totalTokens","type":"uint256"},{"internalType":"uint256","name":"tokensClaimed","type":"uint256"}],"stateMutability":"view","type":"function"}]