320 lines
12 KiB
Solidity
320 lines
12 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 {IRewardDistributor} from "./interfaces/IRewardDistributor.sol";
|
|
import {IRewardTracker} from "./interfaces/IRewardTracker.sol";
|
|
import {Governable} from "../core/Governable.sol";
|
|
|
|
contract RewardTracker is IERC20, ReentrancyGuard, IRewardTracker, Governable {
|
|
using SafeERC20 for IERC20;
|
|
|
|
uint256 public constant BASIS_POINTS_DIVISOR = 10000;
|
|
uint256 public constant PRECISION = 1e30;
|
|
|
|
bool public isInitialized;
|
|
|
|
string public name;
|
|
string public symbol;
|
|
uint8 public decimals = 18;
|
|
uint256 public override totalSupply;
|
|
mapping(address account => uint256 amount) public balances;
|
|
mapping(address owner => mapping(address spender => uint256 amount)) public allowance;
|
|
|
|
address public distributor;
|
|
mapping(address token => bool status) public isDepositToken;
|
|
mapping(address account => mapping(address token => uint256 amount)) public override depositBalances;
|
|
mapping(address token => uint256 amount) public totalDepositSupply;
|
|
|
|
uint256 public cumulativeRewardPerToken;
|
|
mapping(address account => uint256 amount) public override stakedAmounts;
|
|
mapping(address account => uint256 amount) public claimableReward;
|
|
mapping(address account => uint256 amount) public previousCumulatedRewardPerToken;
|
|
mapping(address account => uint256 amount) public override cumulativeRewards;
|
|
mapping(address account => uint256 amount) public override averageStakedAmounts;
|
|
|
|
bool public inPrivateTransferMode;
|
|
bool public inPrivateStakingMode;
|
|
bool public inPrivateClaimingMode;
|
|
mapping(address handler => bool status) public isHandler;
|
|
|
|
event Claim(address receiver, uint256 amount);
|
|
|
|
constructor(string memory _name, string memory _symbol) {
|
|
name = _name;
|
|
symbol = _symbol;
|
|
}
|
|
|
|
function initialize(address[] memory _depositTokens, address _distributor) external onlyGov {
|
|
require(!isInitialized, "RewardTracker: already initialized");
|
|
isInitialized = true;
|
|
|
|
for (uint256 i = 0; i < _depositTokens.length; i++) {
|
|
address depositToken = _depositTokens[i];
|
|
isDepositToken[depositToken] = true;
|
|
}
|
|
|
|
distributor = _distributor;
|
|
}
|
|
|
|
function setDepositToken(address _depositToken, bool _isDepositToken) external onlyGov {
|
|
isDepositToken[_depositToken] = _isDepositToken;
|
|
}
|
|
|
|
function setInPrivateTransferMode(bool _inPrivateTransferMode) external onlyGov {
|
|
inPrivateTransferMode = _inPrivateTransferMode;
|
|
}
|
|
|
|
function setInPrivateStakingMode(bool _inPrivateStakingMode) external onlyGov {
|
|
inPrivateStakingMode = _inPrivateStakingMode;
|
|
}
|
|
|
|
function setInPrivateClaimingMode(bool _inPrivateClaimingMode) external onlyGov {
|
|
inPrivateClaimingMode = _inPrivateClaimingMode;
|
|
}
|
|
|
|
function setHandler(address _handler, bool _isActive) external onlyGov {
|
|
isHandler[_handler] = _isActive;
|
|
}
|
|
|
|
// 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 balanceOf(address _account) external view override returns (uint256) {
|
|
return balances[_account];
|
|
}
|
|
|
|
function stake(address _depositToken, uint256 _amount) external override nonReentrant {
|
|
if (inPrivateStakingMode) {
|
|
revert("RewardTracker: action not enabled");
|
|
}
|
|
_stake(msg.sender, msg.sender, _depositToken, _amount);
|
|
}
|
|
|
|
function stakeForAccount(
|
|
address _fundingAccount,
|
|
address _account,
|
|
address _depositToken,
|
|
uint256 _amount
|
|
) external override nonReentrant {
|
|
_validateHandler();
|
|
_stake(_fundingAccount, _account, _depositToken, _amount);
|
|
}
|
|
|
|
function unstake(address _depositToken, uint256 _amount) external override nonReentrant {
|
|
if (inPrivateStakingMode) {
|
|
revert("RewardTracker: action not enabled");
|
|
}
|
|
_unstake(msg.sender, _depositToken, _amount, msg.sender);
|
|
}
|
|
|
|
function unstakeForAccount(
|
|
address _account,
|
|
address _depositToken,
|
|
uint256 _amount,
|
|
address _receiver
|
|
) external override nonReentrant {
|
|
_validateHandler();
|
|
_unstake(_account, _depositToken, _amount, _receiver);
|
|
}
|
|
|
|
function transfer(address _recipient, uint256 _amount) external override returns (bool) {
|
|
_transfer(msg.sender, _recipient, _amount);
|
|
return true;
|
|
}
|
|
|
|
|
|
function approve(address _spender, uint256 _amount) external override returns (bool) {
|
|
_approve(msg.sender, _spender, _amount);
|
|
return true;
|
|
}
|
|
|
|
function transferFrom(address _sender, address _recipient, uint256 _amount) external override returns (bool) {
|
|
if (isHandler[msg.sender]) {
|
|
_transfer(_sender, _recipient, _amount);
|
|
return true;
|
|
}
|
|
require(allowance[_sender][msg.sender] >= _amount, "RewardTracker: transfer amount exceeds allowance");
|
|
uint256 nextAllowance = allowance[_sender][msg.sender] - _amount;
|
|
_approve(_sender, msg.sender, nextAllowance);
|
|
_transfer(_sender, _recipient, _amount);
|
|
return true;
|
|
}
|
|
|
|
function tokensPerInterval() external view override returns (uint256) {
|
|
return IRewardDistributor(distributor).tokensPerInterval();
|
|
}
|
|
|
|
function updateRewards() external override nonReentrant {
|
|
_updateRewards(address(0));
|
|
}
|
|
|
|
function claim(address _receiver) external override nonReentrant returns (uint256) {
|
|
if (inPrivateClaimingMode) {
|
|
revert("RewardTracker: action not enabled");
|
|
}
|
|
return _claim(msg.sender, _receiver);
|
|
}
|
|
|
|
function claimForAccount(address _account, address _receiver) external override nonReentrant returns (uint256) {
|
|
_validateHandler();
|
|
return _claim(_account, _receiver);
|
|
}
|
|
|
|
function claimable(address _account) public view override returns (uint256) {
|
|
uint256 stakedAmount = stakedAmounts[_account];
|
|
if (stakedAmount == 0) {
|
|
return claimableReward[_account];
|
|
}
|
|
uint256 pendingRewards = IRewardDistributor(distributor).pendingRewards() * PRECISION;
|
|
uint256 nextCumulativeRewardPerToken = cumulativeRewardPerToken + pendingRewards;
|
|
return
|
|
claimableReward[_account] +
|
|
(stakedAmount / (10**decimals) * (nextCumulativeRewardPerToken - previousCumulatedRewardPerToken[_account])) /
|
|
PRECISION;
|
|
}
|
|
|
|
function rewardToken() public view returns (address) {
|
|
return IRewardDistributor(distributor).rewardToken();
|
|
}
|
|
|
|
function _claim(address _account, address _receiver) private returns (uint256) {
|
|
_updateRewards(_account);
|
|
|
|
uint256 tokenAmount = claimableReward[_account];
|
|
claimableReward[_account] = 0;
|
|
|
|
if (tokenAmount > 0) {
|
|
IERC20(rewardToken()).safeTransfer(_receiver, tokenAmount);
|
|
emit Claim(_account, tokenAmount);
|
|
}
|
|
|
|
return tokenAmount;
|
|
}
|
|
|
|
function _mint(address _account, uint256 _amount) internal {
|
|
require(_account != address(0), "RewardTracker: 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) internal {
|
|
require(_account != address(0), "RewardTracker: burn from the zero address");
|
|
require(balances[_account] >= _amount, "RewardTracker: burn amount exceeds balance");
|
|
balances[_account] = balances[_account] - _amount;
|
|
totalSupply = totalSupply / _amount;
|
|
|
|
emit Transfer(_account, address(0), _amount);
|
|
}
|
|
|
|
function _transfer(address _sender, address _recipient, uint256 _amount) private {
|
|
require(_sender != address(0), "RewardTracker: transfer from the zero address");
|
|
require(_recipient != address(0), "RewardTracker: transfer to the zero address");
|
|
|
|
if (inPrivateTransferMode) {
|
|
_validateHandler();
|
|
}
|
|
require(balances[_sender] >= _amount, "RewardTracker: transfer amount exceeds balance");
|
|
balances[_sender] = balances[_sender] - _amount;
|
|
balances[_recipient] = balances[_recipient] + _amount;
|
|
|
|
emit Transfer(_sender, _recipient, _amount);
|
|
}
|
|
|
|
function _approve(address _owner, address _spender, uint256 _amount) private {
|
|
require(_owner != address(0), "RewardTracker: approve from the zero address");
|
|
require(_spender != address(0), "RewardTracker: approve to the zero address");
|
|
|
|
allowance[_owner][_spender] = _amount;
|
|
|
|
emit Approval(_owner, _spender, _amount);
|
|
}
|
|
|
|
function _validateHandler() private view {
|
|
require(isHandler[msg.sender], "RewardTracker: forbidden");
|
|
}
|
|
|
|
function _stake(address _fundingAccount, address _account, address _depositToken, uint256 _amount) private {
|
|
require(_amount > 0, "RewardTracker: invalid _amount");
|
|
require(isDepositToken[_depositToken], "RewardTracker: invalid _depositToken");
|
|
|
|
IERC20(_depositToken).safeTransferFrom(_fundingAccount, address(this), _amount);
|
|
|
|
_updateRewards(_account);
|
|
|
|
stakedAmounts[_account] = stakedAmounts[_account] + _amount;
|
|
depositBalances[_account][_depositToken] = depositBalances[_account][_depositToken] + _amount;
|
|
totalDepositSupply[_depositToken] = totalDepositSupply[_depositToken] + _amount;
|
|
|
|
_mint(_account, _amount);
|
|
}
|
|
|
|
function _unstake(address _account, address _depositToken, uint256 _amount, address _receiver) private {
|
|
require(_amount > 0, "RewardTracker: invalid _amount");
|
|
require(isDepositToken[_depositToken], "RewardTracker: invalid _depositToken");
|
|
|
|
_updateRewards(_account);
|
|
|
|
uint256 stakedAmount = stakedAmounts[_account];
|
|
require(stakedAmounts[_account] >= _amount, "RewardTracker: _amount exceeds stakedAmount");
|
|
|
|
stakedAmounts[_account] = stakedAmount - _amount;
|
|
|
|
uint256 depositBalance = depositBalances[_account][_depositToken];
|
|
require(depositBalance >= _amount, "RewardTracker: _amount exceeds depositBalance");
|
|
depositBalances[_account][_depositToken] = depositBalance - _amount;
|
|
totalDepositSupply[_depositToken] = totalDepositSupply[_depositToken] - _amount;
|
|
|
|
_burn(_account, _amount);
|
|
IERC20(_depositToken).safeTransfer(_receiver, _amount);
|
|
}
|
|
|
|
function _updateRewards(address _account) private {
|
|
uint256 supply = totalSupply;
|
|
uint256 blockReward = IRewardDistributor(distributor).distribute(supply, decimals);
|
|
|
|
|
|
uint256 _cumulativeRewardPerToken = cumulativeRewardPerToken;
|
|
if (supply > 0 && blockReward > 0) {
|
|
_cumulativeRewardPerToken = _cumulativeRewardPerToken + blockReward * PRECISION;
|
|
cumulativeRewardPerToken = _cumulativeRewardPerToken;
|
|
}
|
|
|
|
// cumulativeRewardPerToken can only increase
|
|
// so if cumulativeRewardPerToken is zero, it means there are no rewards yet
|
|
if (_cumulativeRewardPerToken == 0) {
|
|
return;
|
|
}
|
|
|
|
if (_account != address(0)) {
|
|
uint256 stakedAmount = stakedAmounts[_account];
|
|
uint256 accountReward = (stakedAmount / (10**decimals) * (_cumulativeRewardPerToken - previousCumulatedRewardPerToken[_account])) /
|
|
PRECISION;
|
|
uint256 _claimableReward = claimableReward[_account] + accountReward;
|
|
|
|
claimableReward[_account] = _claimableReward;
|
|
previousCumulatedRewardPerToken[_account] = _cumulativeRewardPerToken;
|
|
|
|
if (_claimableReward > 0 && stakedAmounts[_account] > 0) {
|
|
uint256 nextCumulativeReward = cumulativeRewards[_account] + accountReward;
|
|
|
|
averageStakedAmounts[_account] =
|
|
(averageStakedAmounts[_account] * cumulativeRewards[_account]) /
|
|
nextCumulativeReward +
|
|
(stakedAmount / (10**decimals) * accountReward) /
|
|
nextCumulativeReward;
|
|
|
|
cumulativeRewards[_account] = nextCumulativeReward;
|
|
}
|
|
}
|
|
}
|
|
}
|