编译器
0.8.25+commit.b61c2a91
文件 1 的 9:Address.sol
pragma solidity ^0.8.1;
library Address {
function isContract(address account) internal view returns (bool) {
return account.code.length > 0;
}
function sendValue(address payable recipient, uint256 amount) internal {
require(address(this).balance >= amount, "Address: insufficient balance");
(bool success, ) = recipient.call{value: amount}("");
require(success, "Address: unable to send value, recipient may have reverted");
}
function functionCall(address target, bytes memory data) internal returns (bytes memory) {
return functionCallWithValue(target, data, 0, "Address: low-level call failed");
}
function functionCall(
address target,
bytes memory data,
string memory errorMessage
) internal returns (bytes memory) {
return functionCallWithValue(target, data, 0, errorMessage);
}
function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) {
return functionCallWithValue(target, data, value, "Address: low-level call with value failed");
}
function functionCallWithValue(
address target,
bytes memory data,
uint256 value,
string memory errorMessage
) internal returns (bytes memory) {
require(address(this).balance >= value, "Address: insufficient balance for call");
(bool success, bytes memory returndata) = target.call{value: value}(data);
return verifyCallResultFromTarget(target, success, returndata, errorMessage);
}
function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
return functionStaticCall(target, data, "Address: low-level static call failed");
}
function functionStaticCall(
address target,
bytes memory data,
string memory errorMessage
) internal view returns (bytes memory) {
(bool success, bytes memory returndata) = target.staticcall(data);
return verifyCallResultFromTarget(target, success, returndata, errorMessage);
}
function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
return functionDelegateCall(target, data, "Address: low-level delegate call failed");
}
function functionDelegateCall(
address target,
bytes memory data,
string memory errorMessage
) internal returns (bytes memory) {
(bool success, bytes memory returndata) = target.delegatecall(data);
return verifyCallResultFromTarget(target, success, returndata, errorMessage);
}
function verifyCallResultFromTarget(
address target,
bool success,
bytes memory returndata,
string memory errorMessage
) internal view returns (bytes memory) {
if (success) {
if (returndata.length == 0) {
require(isContract(target), "Address: call to non-contract");
}
return returndata;
} else {
_revert(returndata, errorMessage);
}
}
function verifyCallResult(
bool success,
bytes memory returndata,
string memory errorMessage
) internal pure returns (bytes memory) {
if (success) {
return returndata;
} else {
_revert(returndata, errorMessage);
}
}
function _revert(bytes memory returndata, string memory errorMessage) private pure {
if (returndata.length > 0) {
assembly {
let returndata_size := mload(returndata)
revert(add(32, returndata), returndata_size)
}
} else {
revert(errorMessage);
}
}
}
文件 2 的 9:Context.sol
pragma solidity ^0.8.0;
abstract contract Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}
}
文件 3 的 9:IERC20.sol
pragma solidity ^0.8.0;
interface IERC20 {
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
}
文件 4 的 9:IERC20Permit.sol
pragma solidity ^0.8.0;
interface IERC20Permit {
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external;
function nonces(address owner) external view returns (uint256);
function DOMAIN_SEPARATOR() external view returns (bytes32);
}
文件 5 的 9:IVotingEscrow.sol
pragma solidity 0.8.25;
interface IVotingEscrow {
struct Lockup {
uint128 amount;
uint128 duration;
uint128 end;
uint256 points;
bool isVesting;
}
function stakeVesting(uint256 amount, uint256 duration, address to, uint256 startTime) external returns (uint256 lockupId);
function unstakeVesting(address user, uint256 lockupId, bool force) external returns (uint256 amount);
function migrateVestingLock(address oldUser, address newUser, uint256 lockupId)
external
returns (uint256 newLockupId);
function extendVestingLock(address user, uint256 lockupId, uint256 amount, uint256 duration) external;
event Stake(
address indexed user, bool indexed isVesting, uint256 lockupId, uint256 amount, uint256 start, uint256 end, uint256 points
);
event Unstake(
address indexed user, bool indexed isVesting, uint256 lockupId, uint256 amount, uint256 end, uint256 points
);
event Migrated(address indexed oldUser, address indexed newUser, uint256 oldLockupId, uint256 newLockupId);
event Cancelled(address indexed user, uint256 lockupId, uint256 amount, uint256 points);
}
文件 6 的 9:Ownable.sol
pragma solidity ^0.8.0;
import "../utils/Context.sol";
abstract contract Ownable is Context {
address private _owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
constructor() {
_transferOwnership(_msgSender());
}
modifier onlyOwner() {
_checkOwner();
_;
}
function owner() public view virtual returns (address) {
return _owner;
}
function _checkOwner() internal view virtual {
require(owner() == _msgSender(), "Ownable: caller is not the owner");
}
function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}
function transferOwnership(address newOwner) public virtual onlyOwner {
require(newOwner != address(0), "Ownable: new owner is the zero address");
_transferOwnership(newOwner);
}
function _transferOwnership(address newOwner) internal virtual {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
}
文件 7 的 9:Ownable2Step.sol
pragma solidity ^0.8.0;
import "./Ownable.sol";
abstract contract Ownable2Step is Ownable {
address private _pendingOwner;
event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner);
function pendingOwner() public view virtual returns (address) {
return _pendingOwner;
}
function transferOwnership(address newOwner) public virtual override onlyOwner {
_pendingOwner = newOwner;
emit OwnershipTransferStarted(owner(), newOwner);
}
function _transferOwnership(address newOwner) internal virtual override {
delete _pendingOwner;
super._transferOwnership(newOwner);
}
function acceptOwnership() public virtual {
address sender = _msgSender();
require(pendingOwner() == sender, "Ownable2Step: caller is not the new owner");
_transferOwnership(sender);
}
}
文件 8 的 9:SafeERC20.sol
pragma solidity ^0.8.0;
import "../IERC20.sol";
import "../extensions/IERC20Permit.sol";
import "../../../utils/Address.sol";
library SafeERC20 {
using Address for address;
function safeTransfer(IERC20 token, address to, uint256 value) internal {
_callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
}
function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
_callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value));
}
function safeApprove(IERC20 token, address spender, uint256 value) internal {
require(
(value == 0) || (token.allowance(address(this), spender) == 0),
"SafeERC20: approve from non-zero to non-zero allowance"
);
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value));
}
function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {
uint256 oldAllowance = token.allowance(address(this), spender);
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, oldAllowance + value));
}
function safeDecreaseAllowance(IERC20 token, address spender, uint256 value) internal {
unchecked {
uint256 oldAllowance = token.allowance(address(this), spender);
require(oldAllowance >= value, "SafeERC20: decreased allowance below zero");
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, oldAllowance - value));
}
}
function forceApprove(IERC20 token, address spender, uint256 value) internal {
bytes memory approvalCall = abi.encodeWithSelector(token.approve.selector, spender, value);
if (!_callOptionalReturnBool(token, approvalCall)) {
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, 0));
_callOptionalReturn(token, approvalCall);
}
}
function safePermit(
IERC20Permit token,
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) internal {
uint256 nonceBefore = token.nonces(owner);
token.permit(owner, spender, value, deadline, v, r, s);
uint256 nonceAfter = token.nonces(owner);
require(nonceAfter == nonceBefore + 1, "SafeERC20: permit did not succeed");
}
function _callOptionalReturn(IERC20 token, bytes memory data) private {
bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed");
require(returndata.length == 0 || abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");
}
function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {
(bool success, bytes memory returndata) = address(token).call(data);
return
success && (returndata.length == 0 || abi.decode(returndata, (bool))) && Address.isContract(address(token));
}
}
文件 9 的 9:TrufVesting.sol
pragma solidity 0.8.25;
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IVotingEscrow} from "../interfaces/IVotingEscrow.sol";
contract TrufVesting is Ownable2Step {
using SafeERC20 for IERC20;
error ZeroAddress();
error ZeroAmount();
error Forbidden(address sender);
error InvalidTimestamp();
error InvalidAmount();
error InvalidVestingCategory(uint256 id);
error InvalidEmissions();
error InvalidVestingInfo(uint256 categoryIdx, uint256 id);
error InvalidUserVesting();
error ClaimAmountExceed();
error UserVestingAlreadySet(uint256 categoryIdx, uint256 vestingId, address user);
error UserVestingDoesNotExists(uint256 categoryIdx, uint256 vestingId, address user);
error MaxAllocationExceed();
error AlreadyVested(uint256 categoryIdx, uint256 vestingId, address user);
error LockExist();
error LockDoesNotExist();
error InvalidInitialReleasePct();
error InvalidInitialReleasePeriod();
error InvalidCliff();
error InvalidPeriod();
error InvalidUnit();
error Initialized();
event VestingCategorySet(uint256 indexed id, string category, uint256 maxAllocation, bool adminClaimable);
event EmissionScheduleSet(uint256 indexed categoryId, uint256[] emissions);
event VestingInfoSet(uint256 indexed categoryId, uint256 indexed id, VestingInfo info);
event UserVestingSet(
uint256 indexed categoryId, uint256 indexed vestingId, address indexed user, uint256 amount, uint64 startTime
);
event UserVestingMigrated(
uint256 indexed categoryId,
uint256 indexed vestingId,
address indexed user,
uint256 amount,
uint256 claimed,
uint256 locked,
uint64 startTime
);
event MigrateUser(
uint256 indexed categoryId, uint256 indexed vestingId, address prevUser, address newUser, uint256 newLockupId
);
event CancelVesting(
uint256 indexed categoryId, uint256 indexed vestingId, address indexed user, bool giveUnclaimed
);
event AdminSet(address indexed admin, bool indexed flag);
event Claimed(uint256 indexed categoryId, uint256 indexed vestingId, address indexed user, uint256 amount);
event VeTrufSet(address indexed veTRUF);
event Staked(
uint256 indexed categoryId,
uint256 indexed vestingId,
address indexed user,
uint256 amount,
uint256 start,
uint256 duration,
uint256 lockupId
);
event ExtendedStaking(
uint256 indexed categoryId, uint256 indexed vestingId, address indexed user, uint256 amount, uint256 duration
);
event Unstaked(uint256 indexed categoryId, uint256 indexed vestingId, address indexed user, uint256 amount);
struct VestingCategory {
string category;
uint256 maxAllocation;
uint256 allocated;
bool adminClaimable;
uint256 totalClaimed;
}
struct VestingInfo {
uint64 initialReleasePct;
uint64 initialReleasePeriod;
uint64 cliff;
uint64 period;
uint64 unit;
}
struct UserVesting {
uint256 amount;
uint256 claimed;
uint256 locked;
uint64 startTime;
}
uint256 public constant DENOMINATOR = 1e18;
uint64 public constant ONE_MONTH = 30 days;
mapping(uint256 => bool) public isInitialized;
IERC20 public immutable trufToken;
address public immutable trufMigrator;
IVotingEscrow public veTRUF;
uint64 public immutable tgeTime;
VestingCategory[] public categories;
mapping(uint256 => uint256[]) public emissionSchedule;
mapping(uint256 => VestingInfo[]) public vestingInfos;
mapping(uint256 => mapping(uint256 => mapping(address => UserVesting))) public userVestings;
mapping(uint256 => mapping(uint256 => mapping(address => uint256))) public lockupIds;
mapping(address => bool) public isAdmin;
modifier onlyAdmin() {
if (!isAdmin[msg.sender] && msg.sender != owner()) {
revert Forbidden(msg.sender);
}
_;
}
constructor(IERC20 _trufToken, address _trufMigrator, uint64 _tgeTime) {
if (address(_trufToken) == address(0)) revert ZeroAddress();
trufToken = _trufToken;
trufMigrator = _trufMigrator;
tgeTime = _tgeTime;
}
function claimable(uint256 categoryId, uint256 vestingId, address user)
public
view
returns (uint256 claimableAmount)
{
if (isInitialized[categoryId] == false) revert Initialized();
UserVesting memory userVesting = userVestings[categoryId][vestingId][user];
VestingInfo memory info = vestingInfos[categoryId][vestingId];
uint64 startTime = userVesting.startTime + info.initialReleasePeriod;
if (startTime > block.timestamp) {
return 0;
}
uint256 totalAmount = userVesting.amount;
uint256 initialRelease = (totalAmount * info.initialReleasePct) / DENOMINATOR;
startTime += info.cliff;
uint256 vestedAmount;
if (startTime > block.timestamp) {
vestedAmount = initialRelease;
} else {
uint64 timeElapsed = ((uint64(block.timestamp) - startTime) / info.unit) * info.unit;
vestedAmount = ((totalAmount - initialRelease) * timeElapsed) / info.period + initialRelease;
}
uint256 maxClaimable = userVesting.amount - userVesting.locked;
if (vestedAmount > maxClaimable) {
vestedAmount = maxClaimable;
}
if (vestedAmount <= userVesting.claimed) {
return 0;
}
claimableAmount = vestedAmount - userVesting.claimed;
uint256 emissionLeft = getEmission(categoryId) - categories[categoryId].totalClaimed;
if (claimableAmount > emissionLeft) {
claimableAmount = emissionLeft;
}
}
function claim(address user, uint256 categoryId, uint256 vestingId, uint256 claimAmount) public {
if (isInitialized[categoryId] == false) revert Initialized();
if (user != msg.sender && (!categories[categoryId].adminClaimable || !isAdmin[msg.sender])) {
revert Forbidden(msg.sender);
}
uint256 claimableAmount = claimable(categoryId, vestingId, user);
if (claimAmount == type(uint256).max) {
claimAmount = claimableAmount;
} else if (claimAmount > claimableAmount) {
revert ClaimAmountExceed();
}
if (claimAmount == 0) {
revert ZeroAmount();
}
categories[categoryId].totalClaimed += claimAmount;
userVestings[categoryId][vestingId][user].claimed += claimAmount;
trufToken.safeTransfer(user, claimAmount);
emit Claimed(categoryId, vestingId, user, claimAmount);
}
function stake(uint256 categoryId, uint256 vestingId, uint256 amount, uint256 duration) external {
_stake(msg.sender, categoryId, vestingId, amount, block.timestamp, duration);
}
function extendStaking(uint256 categoryId, uint256 vestingId, uint256 amount, uint256 duration) external {
if (isInitialized[categoryId] == false) revert Initialized();
uint256 lockupId = lockupIds[categoryId][vestingId][msg.sender];
if (lockupId == 0) {
revert LockDoesNotExist();
}
if (amount != 0) {
UserVesting storage userVesting = userVestings[categoryId][vestingId][msg.sender];
if (amount > userVesting.amount - userVesting.claimed - userVesting.locked) {
revert InvalidAmount();
}
userVesting.locked += amount;
trufToken.safeIncreaseAllowance(address(veTRUF), amount);
}
veTRUF.extendVestingLock(msg.sender, lockupId - 1, amount, duration);
emit ExtendedStaking(categoryId, vestingId, msg.sender, amount, duration);
}
function unstake(uint256 categoryId, uint256 vestingId) external {
if (isInitialized[categoryId] == false) revert Initialized();
uint256 lockupId = lockupIds[categoryId][vestingId][msg.sender];
if (lockupId == 0) {
revert LockDoesNotExist();
}
uint256 amount = veTRUF.unstakeVesting(msg.sender, lockupId - 1, false);
UserVesting storage userVesting = userVestings[categoryId][vestingId][msg.sender];
userVesting.locked -= amount;
delete lockupIds[categoryId][vestingId][msg.sender];
emit Unstaked(categoryId, vestingId, msg.sender, amount);
}
function migrateUser(uint256 categoryId, uint256 vestingId, address prevUser, address newUser) external onlyAdmin {
if (newUser == address(0)) {
revert ZeroAddress();
}
UserVesting storage prevVesting = userVestings[categoryId][vestingId][prevUser];
UserVesting storage newVesting = userVestings[categoryId][vestingId][newUser];
if (newVesting.amount != 0) {
revert UserVestingAlreadySet(categoryId, vestingId, newUser);
}
if (prevVesting.amount == 0) {
revert UserVestingDoesNotExists(categoryId, vestingId, prevUser);
}
newVesting.amount = prevVesting.amount;
newVesting.claimed = prevVesting.claimed;
newVesting.startTime = prevVesting.startTime;
uint256 lockupId = lockupIds[categoryId][vestingId][prevUser];
uint256 newLockupId;
if (lockupId != 0) {
newLockupId = veTRUF.migrateVestingLock(prevUser, newUser, lockupId - 1) + 1;
lockupIds[categoryId][vestingId][newUser] = newLockupId;
delete lockupIds[categoryId][vestingId][prevUser];
newVesting.locked = prevVesting.locked;
}
delete userVestings[categoryId][vestingId][prevUser];
emit MigrateUser(categoryId, vestingId, prevUser, newUser, newLockupId);
}
function cancelVesting(uint256 categoryId, uint256 vestingId, address user, bool giveUnclaimed)
external
onlyAdmin
{
UserVesting storage userVesting = userVestings[categoryId][vestingId][user];
if (userVesting.amount == 0) {
revert UserVestingDoesNotExists(categoryId, vestingId, user);
}
VestingInfo memory vestingInfo = vestingInfos[categoryId][vestingId];
if (
userVesting.startTime + vestingInfo.initialReleasePeriod + vestingInfo.cliff + vestingInfo.period
<= block.timestamp
) {
revert AlreadyVested(categoryId, vestingId, user);
}
uint256 lockupId = lockupIds[categoryId][vestingId][user];
if (lockupId != 0) {
veTRUF.unstakeVesting(user, lockupId - 1, true);
delete lockupIds[categoryId][vestingId][user];
userVesting.locked = 0;
}
VestingCategory storage category = categories[categoryId];
uint256 claimableAmount = claimable(categoryId, vestingId, user);
uint256 unvested = userVesting.amount - (userVesting.claimed + (giveUnclaimed ? claimableAmount : 0));
delete userVestings[categoryId][vestingId][user];
category.allocated -= unvested;
if (giveUnclaimed && claimableAmount != 0) {
trufToken.safeTransfer(user, claimableAmount);
category.totalClaimed += claimableAmount;
emit Claimed(categoryId, vestingId, user, claimableAmount);
}
emit CancelVesting(categoryId, vestingId, user, giveUnclaimed);
}
function setVestingCategory(string calldata category, uint256 maxAllocation, bool adminClaimable)
public
onlyOwner
{
if (maxAllocation == 0) {
revert ZeroAmount();
}
uint256 id = categories.length;
categories.push(VestingCategory(category, maxAllocation, 0, adminClaimable, 0));
emit VestingCategorySet(id, category, maxAllocation, adminClaimable);
}
function setEmissionSchedule(uint256 categoryId, uint256[] memory emissions) public onlyOwner {
if (isInitialized[categoryId]) {
revert Initialized();
}
uint256 maxAllocation = categories[categoryId].maxAllocation;
if (emissions.length == 0 || emissions[emissions.length - 1] != maxAllocation) {
revert InvalidEmissions();
}
delete emissionSchedule[categoryId];
emissionSchedule[categoryId] = emissions;
emit EmissionScheduleSet(categoryId, emissions);
}
function setVestingInfo(uint256 categoryIdx, uint256 id, VestingInfo calldata info) public onlyAdmin {
if (info.initialReleasePct > DENOMINATOR) {
revert InvalidInitialReleasePct();
} else if (info.initialReleasePeriod > info.period) {
revert InvalidInitialReleasePeriod();
} else if (info.cliff > 365 days) {
revert InvalidCliff();
} else if (info.period > 8 * 365 days) {
revert InvalidPeriod();
} else if (info.period % info.unit != 0) {
revert InvalidUnit();
}
if (id == type(uint256).max) {
id = vestingInfos[categoryIdx].length;
vestingInfos[categoryIdx].push(info);
} else {
vestingInfos[categoryIdx][id] = info;
}
emit VestingInfoSet(categoryIdx, id, info);
}
function migrate(
uint256 categoryId,
uint256 vestingId,
address user,
uint256 amount,
uint256 claimed,
uint256 locked,
uint64 vestingStartTime,
uint256 stakingStartTime,
uint256 stakingDuration
) public {
if (msg.sender != trufMigrator) {
revert();
}
if (user == address(0)) {
revert ZeroAddress();
}
if (amount == 0) {
revert ZeroAmount();
}
if (categoryId >= categories.length) {
revert InvalidVestingCategory(categoryId);
}
if (vestingId >= vestingInfos[categoryId].length) {
revert InvalidVestingInfo(categoryId, vestingId);
}
if (isInitialized[categoryId]) {
trufToken.safeTransferFrom(msg.sender, address(this), amount - claimed);
} else if (locked > 0) {
revert Initialized();
}
VestingCategory storage category = categories[categoryId];
UserVesting storage userVesting = userVestings[categoryId][vestingId][user];
if (amount < claimed + locked) {
revert InvalidUserVesting();
}
category.allocated += amount;
category.totalClaimed += claimed;
if (category.allocated > category.maxAllocation) {
revert MaxAllocationExceed();
}
if (vestingStartTime != 0 && vestingStartTime < tgeTime) revert InvalidTimestamp();
userVesting.amount += amount;
userVesting.claimed += claimed;
userVesting.startTime = vestingStartTime == 0 ? tgeTime : vestingStartTime;
emit UserVestingMigrated(categoryId, vestingId, user, amount, claimed, locked, userVesting.startTime);
if (locked > 0) {
_stake(user, categoryId, vestingId, locked, stakingStartTime, stakingDuration);
}
}
function setUserVesting(uint256 categoryId, uint256 vestingId, address user, uint64 startTime, uint256 amount)
public
onlyAdmin
{
if (user == address(0)) {
revert ZeroAddress();
}
if (amount == 0) {
revert ZeroAmount();
}
if (categoryId >= categories.length) {
revert InvalidVestingCategory(categoryId);
}
if (vestingId >= vestingInfos[categoryId].length) {
revert InvalidVestingInfo(categoryId, vestingId);
}
VestingCategory storage category = categories[categoryId];
UserVesting storage userVesting = userVestings[categoryId][vestingId][user];
category.allocated += amount;
category.allocated -= userVesting.amount;
if (category.allocated > category.maxAllocation) {
revert MaxAllocationExceed();
}
if (amount < userVesting.claimed + userVesting.locked) {
revert InvalidUserVesting();
}
if (startTime != 0 && startTime < tgeTime) revert InvalidTimestamp();
userVesting.amount = amount;
userVesting.startTime = startTime == 0 ? tgeTime : startTime;
emit UserVestingSet(categoryId, vestingId, user, amount, userVesting.startTime);
}
function setVeTruf(address _veTRUF) external onlyOwner {
if (_veTRUF == address(0)) {
revert ZeroAddress();
}
veTRUF = IVotingEscrow(_veTRUF);
emit VeTrufSet(_veTRUF);
}
function setAdmin(address _admin, bool _flag) external onlyOwner {
isAdmin[_admin] = _flag;
emit AdminSet(_admin, _flag);
}
function initialize(uint256 _categoryId) external {
if (isInitialized[_categoryId]) {
revert Initialized();
}
isInitialized[_categoryId] = true;
if (_categoryId != 0 && _categoryId != 7) {
trufToken.safeTransferFrom(msg.sender, address(this), categories[_categoryId].maxAllocation);
}
}
function multicall(bytes[] calldata payloads) external {
uint256 len = payloads.length;
for (uint256 i; i < len;) {
(bool success, bytes memory result) = address(this).delegatecall(payloads[i]);
if (!success) {
if (result.length < 68) revert();
assembly {
result := add(result, 0x04)
}
revert(abi.decode(result, (string)));
}
unchecked {
i += 1;
}
}
}
function getEmissionSchedule(uint256 categoryId) external view returns (uint256[] memory emissions) {
emissions = emissionSchedule[categoryId];
}
function getEmission(uint256 categoryId) public view returns (uint256 emissionLimit) {
uint64 _tgeTime = tgeTime;
if (block.timestamp >= _tgeTime) {
uint256 maxAllocation = categories[categoryId].maxAllocation;
if (emissionSchedule[categoryId].length == 0) {
return maxAllocation;
}
uint64 elapsedTime = uint64(block.timestamp) - _tgeTime + ONE_MONTH;
uint64 elapsedMonth = elapsedTime / ONE_MONTH;
if (elapsedMonth >= emissionSchedule[categoryId].length) {
return maxAllocation;
}
uint256 lastMonthEmission = elapsedMonth == 0 ? 0 : emissionSchedule[categoryId][elapsedMonth - 1];
uint256 thisMonthEmission = emissionSchedule[categoryId][elapsedMonth];
uint64 elapsedTimeOfLastMonth = elapsedTime % ONE_MONTH;
emissionLimit =
(thisMonthEmission - lastMonthEmission) * elapsedTimeOfLastMonth / ONE_MONTH + lastMonthEmission;
if (emissionLimit > maxAllocation) {
emissionLimit = maxAllocation;
}
}
}
function _stake(
address user,
uint256 categoryId,
uint256 vestingId,
uint256 amount,
uint256 start,
uint256 duration
) internal {
if (isInitialized[categoryId] == false) revert Initialized();
if (amount == 0) {
revert ZeroAmount();
}
if (lockupIds[categoryId][vestingId][user] != 0) {
revert LockExist();
}
UserVesting storage userVesting = userVestings[categoryId][vestingId][user];
if (amount > userVesting.amount - userVesting.claimed - userVesting.locked) {
revert InvalidAmount();
}
userVesting.locked += amount;
trufToken.safeIncreaseAllowance(address(veTRUF), amount);
uint256 lockupId = veTRUF.stakeVesting(amount, duration, user, start) + 1;
lockupIds[categoryId][vestingId][user] = lockupId;
emit Staked(categoryId, vestingId, user, amount, start, duration, lockupId);
}
}
{
"compilationTarget": {
"src/token/TrufVesting.sol": "TrufVesting"
},
"evmVersion": "paris",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": true,
"runs": 200
},
"remappings": [
":@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/",
":@uniswap/v2-periphery/=lib/uniswap-v2-periphery/contracts/",
":ds-test/=lib/forge-std/lib/ds-test/src/",
":erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/",
":forge-std/=lib/forge-std/src/",
":murky/src/=lib/murky/src/",
":openzeppelin-contracts/=lib/openzeppelin-contracts/",
":openzeppelin/=lib/openzeppelin-contracts/contracts/"
]
}
[{"inputs":[{"internalType":"contract IERC20","name":"_trufToken","type":"address"},{"internalType":"address","name":"_trufMigrator","type":"address"},{"internalType":"uint64","name":"_tgeTime","type":"uint64"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"uint256","name":"categoryIdx","type":"uint256"},{"internalType":"uint256","name":"vestingId","type":"uint256"},{"internalType":"address","name":"user","type":"address"}],"name":"AlreadyVested","type":"error"},{"inputs":[],"name":"ClaimAmountExceed","type":"error"},{"inputs":[{"internalType":"address","name":"sender","type":"address"}],"name":"Forbidden","type":"error"},{"inputs":[],"name":"Initialized","type":"error"},{"inputs":[],"name":"InvalidAmount","type":"error"},{"inputs":[],"name":"InvalidCliff","type":"error"},{"inputs":[],"name":"InvalidEmissions","type":"error"},{"inputs":[],"name":"InvalidInitialReleasePct","type":"error"},{"inputs":[],"name":"InvalidInitialReleasePeriod","type":"error"},{"inputs":[],"name":"InvalidPeriod","type":"error"},{"inputs":[],"name":"InvalidTimestamp","type":"error"},{"inputs":[],"name":"InvalidUnit","type":"error"},{"inputs":[],"name":"InvalidUserVesting","type":"error"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"InvalidVestingCategory","type":"error"},{"inputs":[{"internalType":"uint256","name":"categoryIdx","type":"uint256"},{"internalType":"uint256","name":"id","type":"uint256"}],"name":"InvalidVestingInfo","type":"error"},{"inputs":[],"name":"LockDoesNotExist","type":"error"},{"inputs":[],"name":"LockExist","type":"error"},{"inputs":[],"name":"MaxAllocationExceed","type":"error"},{"inputs":[{"internalType":"uint256","name":"categoryIdx","type":"uint256"},{"internalType":"uint256","name":"vestingId","type":"uint256"},{"internalType":"address","name":"user","type":"address"}],"name":"UserVestingAlreadySet","type":"error"},{"inputs":[{"internalType":"uint256","name":"categoryIdx","type":"uint256"},{"internalType":"uint256","name":"vestingId","type":"uint256"},{"internalType":"address","name":"user","type":"address"}],"name":"UserVestingDoesNotExists","type":"error"},{"inputs":[],"name":"ZeroAddress","type":"error"},{"inputs":[],"name":"ZeroAmount","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"admin","type":"address"},{"indexed":true,"internalType":"bool","name":"flag","type":"bool"}],"name":"AdminSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"categoryId","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"vestingId","type":"uint256"},{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"bool","name":"giveUnclaimed","type":"bool"}],"name":"CancelVesting","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"categoryId","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"vestingId","type":"uint256"},{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Claimed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"categoryId","type":"uint256"},{"indexed":false,"internalType":"uint256[]","name":"emissions","type":"uint256[]"}],"name":"EmissionScheduleSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"categoryId","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"vestingId","type":"uint256"},{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"duration","type":"uint256"}],"name":"ExtendedStaking","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"categoryId","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"vestingId","type":"uint256"},{"indexed":false,"internalType":"address","name":"prevUser","type":"address"},{"indexed":false,"internalType":"address","name":"newUser","type":"address"},{"indexed":false,"internalType":"uint256","name":"newLockupId","type":"uint256"}],"name":"MigrateUser","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferStarted","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":"uint256","name":"categoryId","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"vestingId","type":"uint256"},{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"start","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"duration","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"lockupId","type":"uint256"}],"name":"Staked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"categoryId","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"vestingId","type":"uint256"},{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Unstaked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"categoryId","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"vestingId","type":"uint256"},{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"claimed","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"locked","type":"uint256"},{"indexed":false,"internalType":"uint64","name":"startTime","type":"uint64"}],"name":"UserVestingMigrated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"categoryId","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"vestingId","type":"uint256"},{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"uint64","name":"startTime","type":"uint64"}],"name":"UserVestingSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"veTRUF","type":"address"}],"name":"VeTrufSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":false,"internalType":"string","name":"category","type":"string"},{"indexed":false,"internalType":"uint256","name":"maxAllocation","type":"uint256"},{"indexed":false,"internalType":"bool","name":"adminClaimable","type":"bool"}],"name":"VestingCategorySet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"categoryId","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"components":[{"internalType":"uint64","name":"initialReleasePct","type":"uint64"},{"internalType":"uint64","name":"initialReleasePeriod","type":"uint64"},{"internalType":"uint64","name":"cliff","type":"uint64"},{"internalType":"uint64","name":"period","type":"uint64"},{"internalType":"uint64","name":"unit","type":"uint64"}],"indexed":false,"internalType":"struct TrufVesting.VestingInfo","name":"info","type":"tuple"}],"name":"VestingInfoSet","type":"event"},{"inputs":[],"name":"DENOMINATOR","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"ONE_MONTH","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"acceptOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"categoryId","type":"uint256"},{"internalType":"uint256","name":"vestingId","type":"uint256"},{"internalType":"address","name":"user","type":"address"},{"internalType":"bool","name":"giveUnclaimed","type":"bool"}],"name":"cancelVesting","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"categories","outputs":[{"internalType":"string","name":"category","type":"string"},{"internalType":"uint256","name":"maxAllocation","type":"uint256"},{"internalType":"uint256","name":"allocated","type":"uint256"},{"internalType":"bool","name":"adminClaimable","type":"bool"},{"internalType":"uint256","name":"totalClaimed","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"user","type":"address"},{"internalType":"uint256","name":"categoryId","type":"uint256"},{"internalType":"uint256","name":"vestingId","type":"uint256"},{"internalType":"uint256","name":"claimAmount","type":"uint256"}],"name":"claim","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"categoryId","type":"uint256"},{"internalType":"uint256","name":"vestingId","type":"uint256"},{"internalType":"address","name":"user","type":"address"}],"name":"claimable","outputs":[{"internalType":"uint256","name":"claimableAmount","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"emissionSchedule","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"categoryId","type":"uint256"},{"internalType":"uint256","name":"vestingId","type":"uint256"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"duration","type":"uint256"}],"name":"extendStaking","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"categoryId","type":"uint256"}],"name":"getEmission","outputs":[{"internalType":"uint256","name":"emissionLimit","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"categoryId","type":"uint256"}],"name":"getEmissionSchedule","outputs":[{"internalType":"uint256[]","name":"emissions","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_categoryId","type":"uint256"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"isAdmin","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"isInitialized","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"lockupIds","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"categoryId","type":"uint256"},{"internalType":"uint256","name":"vestingId","type":"uint256"},{"internalType":"address","name":"user","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"claimed","type":"uint256"},{"internalType":"uint256","name":"locked","type":"uint256"},{"internalType":"uint64","name":"vestingStartTime","type":"uint64"},{"internalType":"uint256","name":"stakingStartTime","type":"uint256"},{"internalType":"uint256","name":"stakingDuration","type":"uint256"}],"name":"migrate","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"categoryId","type":"uint256"},{"internalType":"uint256","name":"vestingId","type":"uint256"},{"internalType":"address","name":"prevUser","type":"address"},{"internalType":"address","name":"newUser","type":"address"}],"name":"migrateUser","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes[]","name":"payloads","type":"bytes[]"}],"name":"multicall","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":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_admin","type":"address"},{"internalType":"bool","name":"_flag","type":"bool"}],"name":"setAdmin","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"categoryId","type":"uint256"},{"internalType":"uint256[]","name":"emissions","type":"uint256[]"}],"name":"setEmissionSchedule","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"categoryId","type":"uint256"},{"internalType":"uint256","name":"vestingId","type":"uint256"},{"internalType":"address","name":"user","type":"address"},{"internalType":"uint64","name":"startTime","type":"uint64"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"setUserVesting","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_veTRUF","type":"address"}],"name":"setVeTruf","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"category","type":"string"},{"internalType":"uint256","name":"maxAllocation","type":"uint256"},{"internalType":"bool","name":"adminClaimable","type":"bool"}],"name":"setVestingCategory","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"categoryIdx","type":"uint256"},{"internalType":"uint256","name":"id","type":"uint256"},{"components":[{"internalType":"uint64","name":"initialReleasePct","type":"uint64"},{"internalType":"uint64","name":"initialReleasePeriod","type":"uint64"},{"internalType":"uint64","name":"cliff","type":"uint64"},{"internalType":"uint64","name":"period","type":"uint64"},{"internalType":"uint64","name":"unit","type":"uint64"}],"internalType":"struct TrufVesting.VestingInfo","name":"info","type":"tuple"}],"name":"setVestingInfo","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"categoryId","type":"uint256"},{"internalType":"uint256","name":"vestingId","type":"uint256"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"duration","type":"uint256"}],"name":"stake","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"tgeTime","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"trufMigrator","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"trufToken","outputs":[{"internalType":"contract IERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"categoryId","type":"uint256"},{"internalType":"uint256","name":"vestingId","type":"uint256"}],"name":"unstake","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"userVestings","outputs":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"claimed","type":"uint256"},{"internalType":"uint256","name":"locked","type":"uint256"},{"internalType":"uint64","name":"startTime","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"veTRUF","outputs":[{"internalType":"contract IVotingEscrow","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"vestingInfos","outputs":[{"internalType":"uint64","name":"initialReleasePct","type":"uint64"},{"internalType":"uint64","name":"initialReleasePeriod","type":"uint64"},{"internalType":"uint64","name":"cliff","type":"uint64"},{"internalType":"uint64","name":"period","type":"uint64"},{"internalType":"uint64","name":"unit","type":"uint64"}],"stateMutability":"view","type":"function"}]