// SPDX-License-Identifier: MPL-2.0 pragma solidity ^0.8.17; import "@openzeppelin/contracts/access/Ownable2Step.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import {IRewarder} from "./interfaces/IRewarder.sol"; /** * @dev Distribute stacking rewards according to an NFT level, claimable at some later point. */ contract FixedVirtualDistributor is Ownable2Step, ReentrancyGuard { IRewarder public immutable REWARDER; IERC721 public immutable NFT_CONTRACT; uint256[] internal participants; mapping(uint256 => uint256) internal levels; mapping(uint256 => bool) internal penalties; mapping(uint256 => uint256) internal allocated; mapping(uint256 => uint256) internal claimed; uint256 internal totalLevels; uint256 internal totalAllocated; uint256 internal lastAllocation; uint256 public immutable vestingStarts; uint256 public immutable vestingEnds; uint256 public immutable totalReward; event PenaltyApplied(uint256 indexed nft_id); event PenaltyRemoved(uint256 indexed nft_id); event DropIn(uint256 indexed nft_id, uint256 level); event Allocated(uint256 indexed nft_id, uint256 amount); event AllocationFinished(uint256 totalAmount); event Claimed(uint256 indexed nft_id, address indexed owner, uint256 amount); event TotalLevelsChanged(uint256 levels); error InvalidVestingConfig(); error InvalidReward(); error InvalidNFTConfig(); error InvalidNFT(); error NothingToAllocate(); error Unauthorized(); error NothingToClaim(); error NotVested(); /** * @dev Initialise virtual distributor. * * @param _vestingStarts UNIX timestamp in seconds when tokens start to vest. * @param _vestingEnds UNIX timestamp in seconds when tokens fully vest. * @param _rewardContract Contract which distribute rewards. * @param _totalReward total amount of tokens to be distributed. * @param _nftContract NFT contract which is rewarded. * @param _nftIds array of NFT ids which get reward. * @param _nftLevels array of NFT level which get reward, should correspond to _nftIds. */ constructor( uint256 _vestingStarts, uint256 _vestingEnds, IRewarder _rewardContract, uint256 _totalReward, IERC721 _nftContract, uint256[] memory _nftIds, uint256[] memory _nftLevels ) { vestingStarts = _vestingStarts; if (_vestingEnds <= _vestingStarts) revert InvalidVestingConfig(); vestingEnds = _vestingEnds; if (_totalReward <= 0) revert InvalidReward(); totalReward = _totalReward; NFT_CONTRACT = _nftContract; REWARDER = _rewardContract; lastAllocation = _vestingStarts; if (_nftIds.length != _nftLevels.length) revert InvalidNFTConfig(); participants = _nftIds; uint256 _totalLevels = 0; for (uint256 _i = 0; _i < _nftIds.length; _i++) { uint256 _id = _nftIds[_i]; if (_nftContract.ownerOf(_id) == address(0)) revert InvalidNFT(); uint256 _level = _nftLevels[_i]; if (_level <= 0) revert InvalidNFTConfig(); levels[_id] = _level; _totalLevels += _level; emit DropIn(_id, _level); } totalLevels = _totalLevels; emit TotalLevelsChanged(_totalLevels); } /** * @dev Check accumulated rewards for specific NFT. * * @param _nftId NFT id which earns reward. * @return Total amount of tokens allocated to and not claimed by specific participant. */ function pendingRewards(uint256 _nftId) public view returns(uint256) { if (vestingStarts >= block.timestamp) return 0; uint256 _le7el = levels[_nftId]; if (_le7el <= 0) return 0; uint256 _pendingTotal = _vestedAmount(totalReward - totalAllocated, block.timestamp, lastAllocation, vestingEnds); uint256 _pending = _pendingTotal * _le7el / totalLevels; return _pending + allocated[_nftId] - claimed[_nftId]; } /** * @dev Claim no more than currently allocated tokens. * * @param _nftId NFT id which earns reward. * @param _amount Desired amount to claim, pass 0 to claim all available. */ function claim(uint256 _nftId, uint256 _amount) external nonReentrant { address _nftOwner = NFT_CONTRACT.ownerOf(_nftId); if (msg.sender != _nftOwner) revert Unauthorized(); uint256 _pending = pendingRewards(_nftId); if (_pending <= 0) revert NothingToClaim(); if (_amount > _pending) revert NotVested(); if (_amount == 0) _amount = _pending; claimed[_nftId] += _amount; REWARDER.onReward(address(NFT_CONTRACT), _nftId, _nftOwner, _nftOwner, _amount, levels[_nftId]); emit Claimed(_nftId, _nftOwner, _amount); } /** * @dev Admin can cut user's rewards 2 times. * * @param _nftId NFT id which gets the penalty. */ function adminPenaltyAdd(uint256 _nftId) external onlyOwner nonReentrant { uint256 _level = levels[_nftId]; if (_level == 0 || penalties[_nftId]) revert InvalidNFT(); _allocate(); penalties[_nftId] = true; emit PenaltyApplied(_nftId); levels[_nftId] /= 2; uint256 _totalLevels = totalLevels - (_level / 2); totalLevels = _totalLevels; emit TotalLevelsChanged(_totalLevels); } /** * @dev Admin can remove previously given penalty. * * @param _nftId NFT id which no longer penalised. */ function adminPenaltyRemove(uint256 _nftId) external onlyOwner nonReentrant { if (!penalties[_nftId]) revert InvalidNFT(); _allocate(); penalties[_nftId] = false; emit PenaltyRemoved(_nftId); uint256 _level = levels[_nftId]; levels[_nftId] *= 2; uint256 _totalLevels = totalLevels + _level; totalLevels = _totalLevels; emit TotalLevelsChanged(_totalLevels); } /** * @dev Helper to calculate vested amounts according to timeline. * * @param _reward Total tokens remained to vest. * @param _currentTimestamp Vesting point in time. * @param _vestingStarts Last vesting point. * @param _vestingEnds When reward is fully vested. * @return Total amount of tokens vested so far. */ function _vestedAmount( uint256 _reward, uint256 _currentTimestamp, uint256 _vestingStarts, uint256 _vestingEnds ) internal pure returns(uint256) { if (_currentTimestamp <= _vestingStarts) return 0; if (_currentTimestamp > _vestingEnds) _currentTimestamp = _vestingEnds; return _reward * (_currentTimestamp - _vestingStarts) / (_vestingEnds - _vestingStarts); } /** * @dev Save previous allocations before changes in levels. */ function _allocate() internal { uint256 _lastAllocation = lastAllocation; if (block.timestamp <= _lastAllocation) revert NothingToAllocate(); uint256 _vestingEnds = vestingEnds; uint256 _totalReward = totalReward; uint256 _totalAllocated = totalAllocated; uint256 _newTotalAllocation = _vestedAmount(_totalReward - _totalAllocated, block.timestamp, _lastAllocation, _vestingEnds); lastAllocation = block.timestamp; uint256 _totalLevels = totalLevels; uint256[] memory _participants = participants; for (uint256 _i = 0; _i < _participants.length; _i++) { uint256 _nftId = _participants[_i]; uint256 _level = levels[_nftId]; uint256 _alloc = _newTotalAllocation * _level / _totalLevels; allocated[_nftId] += _alloc; _totalAllocated += _alloc; emit Allocated(_nftId, _alloc); } totalAllocated = _totalAllocated; emit AllocationFinished(_totalAllocated); } }