346 lines
11 KiB
Solidity
346 lines
11 KiB
Solidity
// 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 pairToken;
|
|
address public claimableToken;
|
|
|
|
address public override rewardTracker;
|
|
|
|
uint256 public override totalSupply;
|
|
uint256 public pairSupply;
|
|
bool public needCheckStake;
|
|
|
|
mapping(address account => uint256 amount) public balances;
|
|
mapping(address account => uint256 amount) public override pairAmounts;
|
|
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 receiver, uint256 amount);
|
|
event Deposit(address account, uint256 amount);
|
|
event Withdraw(address account, uint256 claimedAmount, uint256 balance);
|
|
event PairTransfer(address indexed from, address indexed to, uint256 value);
|
|
|
|
constructor(
|
|
string memory _name,
|
|
string memory _symbol,
|
|
uint256 _vestingDuration,
|
|
address _esToken,
|
|
address _pairToken,
|
|
address _claimableToken,
|
|
address _rewardTracker,
|
|
bool _needCheckStake
|
|
) {
|
|
name = _name;
|
|
symbol = _symbol;
|
|
vestingDuration = _vestingDuration;
|
|
esToken = _esToken;
|
|
pairToken = _pairToken;
|
|
claimableToken = _claimableToken;
|
|
rewardTracker = _rewardTracker;
|
|
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");
|
|
|
|
if (hasPairToken()) {
|
|
uint256 pairAmount = pairAmounts[account];
|
|
_burnPair(account, pairAmount);
|
|
IERC20(pairToken).safeTransfer(_receiver, pairAmount);
|
|
}
|
|
|
|
IERC20(esToken).safeTransfer(_receiver, balance);
|
|
_burn(account, balance);
|
|
|
|
delete cumulativeClaimAmounts[account];
|
|
delete claimedAmounts[account];
|
|
delete lastVestingTimes[account];
|
|
|
|
emit Withdraw(account, claimedAmount, balance);
|
|
}
|
|
|
|
function setRewardTracker(address _rewardTracker) external onlyGov {
|
|
rewardTracker = _rewardTracker;
|
|
}
|
|
|
|
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 (hasRewardTracker()) {
|
|
uint256 cumulativeReward = IRewardTracker(rewardTracker).cumulativeRewards(_account);
|
|
maxVestableAmount = maxVestableAmount + cumulativeReward;
|
|
}
|
|
|
|
uint256 cumulativeRewardDeduction = cumulativeRewardDeductions[_account];
|
|
|
|
if (maxVestableAmount < cumulativeRewardDeduction) {
|
|
return 0;
|
|
}
|
|
|
|
return maxVestableAmount - cumulativeRewardDeduction;
|
|
}
|
|
|
|
function getCombinedAverageStakedAmount(address _account) public view override returns (uint256) {
|
|
if (!hasRewardTracker()) {
|
|
return 0;
|
|
}
|
|
|
|
uint256 cumulativeReward = IRewardTracker(rewardTracker).cumulativeRewards(_account);
|
|
if (cumulativeReward == 0) {
|
|
return 0;
|
|
}
|
|
|
|
return IRewardTracker(rewardTracker).averageStakedAmounts(_account);
|
|
}
|
|
|
|
function getPairAmount(address _account, uint256 _esAmount) public view returns (uint256) {
|
|
if (!hasRewardTracker()) {
|
|
return 0;
|
|
}
|
|
|
|
uint256 combinedAverageStakedAmount = getCombinedAverageStakedAmount(_account);
|
|
if (combinedAverageStakedAmount == 0) {
|
|
return 0;
|
|
}
|
|
|
|
uint256 maxVestableAmount = getMaxVestableAmount(_account);
|
|
if (maxVestableAmount == 0) {
|
|
return 0;
|
|
}
|
|
|
|
return (_esAmount * combinedAverageStakedAmount) / maxVestableAmount;
|
|
}
|
|
|
|
function hasRewardTracker() public view returns (bool) {
|
|
return rewardTracker != address(0);
|
|
}
|
|
|
|
function hasPairToken() public view returns (bool) {
|
|
return pairToken != 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 _mintPair(address _account, uint256 _amount) private {
|
|
require(_account != address(0), "Vester: mint to the zero address");
|
|
|
|
pairSupply = pairSupply + _amount;
|
|
pairAmounts[_account] = pairAmounts[_account] + _amount;
|
|
|
|
emit PairTransfer(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);
|
|
}
|
|
|
|
function _burnPair(address _account, uint256 _amount) private {
|
|
require(_account != address(0), "Vester: burn from the zero address");
|
|
require(pairAmounts[_account] >= _amount, "Vester: balance is not enough");
|
|
pairAmounts[_account] = pairAmounts[_account] - _amount;
|
|
pairSupply = pairSupply - _amount;
|
|
|
|
emit PairTransfer(_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 (hasPairToken()) {
|
|
uint256 pairAmount = pairAmounts[_account];
|
|
uint256 nextPairAmount = getPairAmount(_account, balances[_account]);
|
|
if (nextPairAmount > pairAmount) {
|
|
uint256 pairAmountDiff = nextPairAmount - pairAmount;
|
|
IERC20(pairToken).safeTransferFrom(_account, address(this), pairAmountDiff);
|
|
_mintPair(_account, pairAmountDiff);
|
|
}
|
|
}
|
|
if (needCheckStake && hasRewardTracker()) {
|
|
// if u want to transfer 100 esCec to cec, u need to have 100 cec in stake
|
|
uint256 cecAmount = IRewardTracker(rewardTracker).depositBalances(_account, claimableToken);
|
|
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");
|
|
}
|
|
}
|