// SPDX-License-Identifier: MIT pragma solidity 0.8.19; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IVester} from "./interfaces/IVester.sol"; import {IRewardTracker} from "./interfaces/IRewardTracker.sol"; import {Governable} from "../core/Governable.sol"; import {IMintable} from "../interfaces/IMintable.sol"; contract Vester is IVester, IERC20, ReentrancyGuard, Governable { using SafeERC20 for IERC20; string public name; string public symbol; uint8 public decimals = 18; uint256 public vestingDuration; address public esToken; address public claimableToken; address public override rewardTrackerCEC; address public override rewardTrackerEsCEC; uint256 public override totalSupply; bool public needCheckStake; mapping(address account => uint256 amount) public balances; mapping(address account => uint256 amount) public override cumulativeClaimAmounts; mapping(address account => uint256 amount) public override claimedAmounts; mapping(address account => uint256 time) public lastVestingTimes; mapping(address account => uint256 amount) public override cumulativeRewardDeductions; mapping(address account => uint256 amount) public override bonusRewards; mapping(address handler => bool status) public isHandler; event Claim(address indexed receiver, uint256 amount); event Deposit(address indexed account, uint256 amount); event Withdraw(address indexed account, uint256 claimedAmount, uint256 balance); event DurationUpdated(uint256 duration); constructor( string memory _name, string memory _symbol, uint256 _vestingDuration, address _esToken, address _claimableToken, address _rewardTrackerCEC, address _rewardTrackerEsCEC, bool _needCheckStake ) { name = _name; symbol = _symbol; vestingDuration = _vestingDuration; esToken = _esToken; claimableToken = _claimableToken; rewardTrackerCEC = _rewardTrackerCEC; rewardTrackerEsCEC = _rewardTrackerEsCEC; needCheckStake = _needCheckStake; } function setHandler(address _handler, bool _isActive) external onlyGov { isHandler[_handler] = _isActive; } function deposit(uint256 _amount) external nonReentrant { _deposit(msg.sender, _amount); } function depositForAccount(address _account, uint256 _amount) external nonReentrant { _validateHandler(); _deposit(_account, _amount); } function claim() external nonReentrant returns (uint256) { return _claim(msg.sender, msg.sender); } function claimForAccount(address _account, address _receiver) external override nonReentrant returns (uint256) { _validateHandler(); return _claim(_account, _receiver); } // to help users who accidentally send their tokens to this contract function withdrawToken(address _token, address _account, uint256 _amount) external onlyGov { IERC20(_token).safeTransfer(_account, _amount); } function withdraw() external nonReentrant { address account = msg.sender; address _receiver = account; _claim(account, _receiver); uint256 claimedAmount = cumulativeClaimAmounts[account]; uint256 balance = balances[account]; uint256 totalVested = balance + claimedAmount; require(totalVested > 0, "Vester: vested amount is zero"); IERC20(esToken).safeTransfer(_receiver, balance); _burn(account, balance); delete cumulativeClaimAmounts[account]; delete claimedAmounts[account]; delete lastVestingTimes[account]; emit Withdraw(account, claimedAmount, balance); } function setRewardTrackerCEC(address _rewardTracker) external onlyGov { rewardTrackerCEC = _rewardTracker; } function setRewardTrackerEsCEC(address _rewardTracker) external onlyGov { rewardTrackerEsCEC = _rewardTracker; } function updateDuration(uint256 _vestingDuration) external onlyGov { vestingDuration = _vestingDuration; emit DurationUpdated(_vestingDuration); } function setCumulativeRewardDeductions(address _account, uint256 _amount) external override nonReentrant { _validateHandler(); cumulativeRewardDeductions[_account] = _amount; } function setBonusRewards(address _account, uint256 _amount) external override nonReentrant { _validateHandler(); bonusRewards[_account] = _amount; } function getMaxVestableAmount(address _account) public view override returns (uint256) { uint256 maxVestableAmount = bonusRewards[_account]; if (hasRewardTrackerCEC()) { uint256 cumulativeRewardCEC = IRewardTracker(rewardTrackerCEC).cumulativeRewards(_account); maxVestableAmount = maxVestableAmount + cumulativeRewardCEC ; } if (hasRewardTrackerEsCEC()) { uint256 cumulativeRewardEsCEC = IRewardTracker(rewardTrackerEsCEC).cumulativeRewards(_account); maxVestableAmount = maxVestableAmount + cumulativeRewardEsCEC; } uint256 cumulativeRewardDeduction = cumulativeRewardDeductions[_account]; if (maxVestableAmount < cumulativeRewardDeduction) { return 0; } return maxVestableAmount - cumulativeRewardDeduction; } function hasRewardTrackerCEC() public view returns (bool) { return rewardTrackerCEC != address(0); } function hasRewardTrackerEsCEC() public view returns (bool) { return rewardTrackerEsCEC != address(0); } function getTotalVested(address _account) public view returns (uint256) { return balances[_account] + cumulativeClaimAmounts[_account]; } function balanceOf(address _account) public view override returns (uint256) { return balances[_account]; } // empty implementation, tokens are non-transferrable function transfer(address /* recipient */, uint256 /* amount */) public virtual override returns (bool) { revert("Vester: non-transferrable"); } // empty implementation, tokens are non-transferrable function allowance(address /* owner */, address /* spender */) public view virtual override returns (uint256) { return 0; } // empty implementation, tokens are non-transferrable function approve(address /* spender */, uint256 /* amount */) public virtual override returns (bool) { revert("Vester: non-transferrable"); } // empty implementation, tokens are non-transferrable function transferFrom( address /* sender */, address /* recipient */, uint256 /* amount */ ) public virtual override returns (bool) { revert("Vester: non-transferrable"); } function getVestedAmount(address _account) public view override returns (uint256) { uint256 balance = balances[_account]; uint256 cumulativeClaimAmount = cumulativeClaimAmounts[_account]; return balance + cumulativeClaimAmount; } function _mint(address _account, uint256 _amount) private { require(_account != address(0), "Vester: mint to the zero address"); totalSupply = totalSupply + _amount; balances[_account] = balances[_account] + _amount; emit Transfer(address(0), _account, _amount); } function _burn(address _account, uint256 _amount) private { require(_account != address(0), "Vester: burn from the zero address"); require(balances[_account] >= _amount, "Vester: balance is not enough"); balances[_account] = balances[_account] - _amount; totalSupply = totalSupply - _amount; emit Transfer(_account, address(0), _amount); } /** * @dev Deposit ES tokens to the contract */ function _deposit(address _account, uint256 _amount) private { require(_amount > 0, "Vester: invalid _amount"); _updateVesting(_account); IERC20(esToken).safeTransferFrom(_account, address(this), _amount); _mint(_account, _amount); if (needCheckStake && hasRewardTrackerCEC()) { // if u want to transfer 100 esCec to cec, u need to have 100 cec in stake uint256 cecAmount = IRewardTracker(rewardTrackerCEC).stakedAmounts(_account); require(balances[_account] <= cecAmount, "Vester: insufficient cec balance"); } uint256 maxAmount = getMaxVestableAmount(_account); require(getTotalVested(_account) <= maxAmount, "Vester: max vestable amount exceeded"); emit Deposit(_account, _amount); } function updateVesting(address _account) public { _updateVesting(_account); } function _updateVesting(address _account) public { uint256 amount = _getNextClaimableAmount(_account); lastVestingTimes[_account] = block.timestamp; if (amount == 0) { return; } // transfer claimableAmount from balances to cumulativeClaimAmounts _burn(_account, amount); cumulativeClaimAmounts[_account] = cumulativeClaimAmounts[_account] + amount; IMintable(esToken).burn(address(this), amount); } function _getNextClaimableAmount(address _account) private view returns (uint256) { uint256 timeDiff = block.timestamp - lastVestingTimes[_account]; uint256 balance = balances[_account]; if (balance == 0) { return 0; } uint256 vestedAmount = getVestedAmount(_account); uint256 claimableAmount = (vestedAmount * timeDiff) / vestingDuration; if (claimableAmount < balance) { return claimableAmount; } return balance; } function claimable(address _account) public view override returns (uint256) { uint256 amount = cumulativeClaimAmounts[_account] - claimedAmounts[_account]; uint256 nextClaimable = _getNextClaimableAmount(_account); return amount + nextClaimable; } function _claim(address _account, address _receiver) private returns (uint256) { _updateVesting(_account); uint256 amount = claimable(_account); claimedAmounts[_account] = claimedAmounts[_account] + amount; IERC20(claimableToken).safeTransfer(_receiver, amount); emit Claim(_account, amount); return amount; } function _validateHandler() private view { require(isHandler[msg.sender], "Vester: forbidden"); } }