// SPDX-License-Identifier: UNLICENSED pragma solidity =0.8.18; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; import "../SomaGuard/utils/GuardableUpgradeable.sol"; import "../SomaAccessControl/utils/AccessibleUpgradeable.sol"; import "../Lockdrop/extensions/TokenRecoveryUpgradeable.sol"; import "./ITokenRewards.sol"; /** * @notice Implementation of the {ITokenRewards} interface. */ contract TokenRewards is ITokenRewards, TokenRecoveryUpgradeable, GuardableUpgradeable { using SafeERC20 for IERC20; /** * @inheritdoc ITokenRewards */ bytes32 public constant GLOBAL_ADMIN_ROLE = keccak256("TokenRewards.ADMIN_ROLE"); /** * @inheritdoc ITokenRewards */ bytes32 public constant GLOBAL_SEIZE_ROLE = keccak256("TokenRewards.SEIZE_ROLE"); /** * @inheritdoc ITokenRewards */ bytes32 public LOCAL_ADMIN_ROLE; /** * @inheritdoc ITokenRewards */ bytes32 public LOCAL_SEIZE_ROLE; /** * @inheritdoc ITokenRewards */ uint256 public startTime; /** * @inheritdoc ITokenRewards */ uint256 public endTime; /** * @notice The root of the merkle tree. */ bytes32 public merkleRoot; /** * @inheritdoc ITokenRewards */ IERC20 public rewardToken; /** * @inheritdoc ITokenRewards */ mapping(address => uint256) public claimedAmount; /** * @notice Initializes the contract. * @param _rewardToken The address of the reward token. * @param _startTime The timestamp marking the start of the vesting period. * @param _endTime The timestamp marking the end of the vesting period. * @param _merkleRoot The root of the merkle tree. * @custom:requirement The timestamp of the function call must be less than `_startTime`. */ function initialize(address _rewardToken, uint256 _startTime, uint256 _endTime, bytes32 _merkleRoot) external initializer { require(block.timestamp < _startTime, "TokenRewards: START MUST BE AFTER CURRENT TIME"); require(_rewardToken != address(0), "TokenRewards: REWARD TOKEN MUST NOT BE ADDRESS ZERO"); require(_merkleRoot != bytes32(0), "TokenRewards: MERKLE ROOT MUST NOT BE BYTES ZERO"); _updateTime(_startTime, _endTime); rewardToken = IERC20(_rewardToken); merkleRoot = _merkleRoot; LOCAL_ADMIN_ROLE = keccak256(abi.encodePacked(address(this), GLOBAL_ADMIN_ROLE)); LOCAL_SEIZE_ROLE = keccak256(abi.encodePacked(address(this), GLOBAL_SEIZE_ROLE)); __Accessible_init(); __TokenRecovery__init(); _disableTokenRecovery(_rewardToken); } /** * @notice Modifier to ensure function callers have the GLOBAL_SEIZE_ROLE or LOCAL_SEIZE_ROLE. */ modifier onlySeizeRole() { address sender = _msgSender(); require( hasRole(LOCAL_SEIZE_ROLE, sender) || hasRole(GLOBAL_SEIZE_ROLE, sender), "TokenRewards: UNAUTHORIZED SEIZE" ); _; } /** * @notice Modifier to ensure function callers have the GLOBAL_ADMIN_ROLE or LOCAL_ADMIN_ROLE. */ modifier onlyAdminRole() { address sender = _msgSender(); require( hasRole(LOCAL_ADMIN_ROLE, sender) || hasRole(GLOBAL_ADMIN_ROLE, sender), "TokenRewards: UNAUTHORIZED ADMIN" ); _; } /** * @notice Checks if TokenRewards inherits a given contract interface. * @dev See {IERC165-supportsInterface}. */ function supportsInterface(bytes4 interfaceId) public view virtual override(TokenRecoveryUpgradeable, GuardableUpgradeable) returns (bool) { return interfaceId == type(ITokenRewards).interfaceId || super.supportsInterface(interfaceId); } /** * @inheritdoc ITokenRewards */ function updateTime(uint256 _startTime, uint256 _endTime) external onlyAdminRole { _updateTime(_startTime, _endTime); } function _updateTime(uint256 _startTime, uint256 _endTime) internal { require(_startTime < _endTime, "TokenRewards: END TIME LESS THAN START"); if (startTime != _startTime) { startTime = _startTime; } if (endTime != _endTime) { endTime = _endTime; } emit UpdatedTimestamps(startTime, endTime, _msgSender()); } /** * @inheritdoc ITokenRewards */ function rewardTokenBalance() external view returns (uint256) { return rewardToken.balanceOf(address(this)); } /** * @inheritdoc ITokenRewards */ function duration() public view returns (uint256) { return (endTime - startTime); } /** * @notice Deposits reward tokens into the contract. * @param amount The amount of reward tokens to deposit. * @custom:emits RewardsDeposited */ function depositRewards(uint256 amount) external { address ownContractAddress = address(this); address caller = _msgSender(); rewardToken.safeTransferFrom(caller, ownContractAddress, amount); emit RewardsDeposited(amount, caller); } /** * @inheritdoc ITokenRewards */ function seize() external onlySeizeRole { address _receiveAddress = SOMA.seizeTo(); uint256 amount = rewardToken.balanceOf(address(this)); rewardToken.safeTransfer(_receiveAddress, amount); emit Seized(amount, _receiveAddress, _msgSender()); } /** * @inheritdoc ITokenRewards */ function claim(bytes32[] calldata _proof, uint256 _amount) external whenNotPaused returns (bool) { address beneficiary = _msgSender(); require(_amount > 0, "TokenRewards: INVALID AMOUNT"); require(_checkProof(_proof, _leafFromAddressAndAmount(beneficiary, _amount)), "TokenRewards: INVALID PROOF"); uint256 _claimable = claimableRewards(_amount, beneficiary); require(_claimable > 0, "TokenRewards: NOTHING TO CLAIM"); require(hasPrivileges(beneficiary), "TokenRewards: NO PRIVILEGES"); claimedAmount[beneficiary] += _claimable; rewardToken.safeTransfer(beneficiary, _claimable); emit Claimed(_claimable, beneficiary); return true; } /** * @inheritdoc ITokenRewards */ function claimableRewards(uint256 amount, address user) public view returns (uint256) { uint256 vested = _vestedAmount(amount); uint256 claimed = claimedAmount[user]; unchecked { return vested <= claimed ? 0 : (vested - claimed); } } function _checkProof(bytes32[] calldata proof, bytes32 hash) internal view returns (bool) { return MerkleProof.verify(proof, merkleRoot, hash); } function _leafFromAddressAndAmount(address _a, uint256 _n) internal pure returns (bytes32) { return keccak256(abi.encodePacked(_a, _n)); } function _vestedAmount(uint256 totalAllocation) internal view returns (uint256) { uint256 _timestamp = block.timestamp; uint256 _startTime = startTime; uint256 _duration = duration(); if (_timestamp <= _startTime) { return 0; } else if (_timestamp > _startTime + _duration) { return totalAllocation; } else { return (totalAllocation * (_timestamp - _startTime)) / _duration; } } }