From 2ec403f1df8971c571d951f596d64706fc531ffb Mon Sep 17 00:00:00 2001 From: ququzone Date: Wed, 27 Nov 2024 22:12:47 +0800 Subject: [PATCH] feat: add FixedRewadPoolV2 --- contracts/gauges/FixedRewardPooV2.sol | 248 ++++++++++++++++++++++++++ contracts/utils/Multicall.sol | 24 +++ 2 files changed, 272 insertions(+) create mode 100644 contracts/gauges/FixedRewardPooV2.sol create mode 100644 contracts/utils/Multicall.sol diff --git a/contracts/gauges/FixedRewardPooV2.sol b/contracts/gauges/FixedRewardPooV2.sol new file mode 100644 index 0000000..5df4afa --- /dev/null +++ b/contracts/gauges/FixedRewardPooV2.sol @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; + +import {Multicall} from "../utils/Multicall.sol"; +import {IWeightedNFT} from "../interfaces/IWeightedNFT.sol"; + +contract FixedRewardPoolV2 is Multicall, OwnableUpgradeable, ReentrancyGuardUpgradeable, ERC721Holder { + string public constant GAUGE_TYPE = "gauge_fixed_reward_pool"; + string public constant GAUGE_VERSION = "0.2.0"; + + event Deposit(address indexed user, uint256 tokenId, uint256 rewards, uint256 weight); + event Withdraw(address indexed user, uint256 tokenId, uint256 rewards, uint256 weight); + event EmergencyWithdraw(address indexed user, uint256 tokenId, uint256 weight); + event Poke(address indexed user, uint256 tokenId, uint256 rewards, uint256 originalWeight, uint256 newWeight); + event ClaimRewards(address indexed user, uint256 rewards); + event StopRewards(uint256 stopHeight); + event NewRewardPerBlock(uint256 rewardPerBlock); + event NewBonusEndBlock(uint256 bonusEndBlock); + + struct UserInfo { + uint256 amount; // How many staked tokens the user has provided + uint256 rewardDebt; // Reward debt + } + + // The precision factor + uint256 constant PRECISION_FACTOR = 1e12; + + // The weighted NFT contract + IWeightedNFT public weightNFT; + // Accrued token per share + uint256 public accTokenPerShare; + // The block number of the last pool update + uint256 public lastRewardBlock; + // The block number when mining ends + uint256 public bonusEndBlock; + // Reward tokens created per block + uint256 public rewardPerBlock; + // Total staked weight + uint256 public totalStakedWeight; + // Info of each user that stakes tokens (stakedToken) + mapping(address => UserInfo) public userInfo; + + // Mapping from tokenId to staker + mapping(uint256 => address) public tokenStaker; + // Mapping from tokenId to weight + mapping(uint256 => uint256) public tokenWeight; + + function initialize( + address _nft, + uint256 _startBlock, + uint256 _rewardPerBlock, + uint256 _totalBlocks + ) external initializer { + require(_startBlock >= block.number, "invalid start block"); + require(_rewardPerBlock > 0, "invalid reward per block"); + + __Ownable_init(); + __ReentrancyGuard_init(); + weightNFT = IWeightedNFT(_nft); + lastRewardBlock = _startBlock; + rewardPerBlock = _rewardPerBlock; + bonusEndBlock = _startBlock + _totalBlocks; + } + + function updateRewardPerBlock(uint256 _rewardPerBlock) external onlyOwner { + _updatePool(); + rewardPerBlock = _rewardPerBlock; + emit NewRewardPerBlock(_rewardPerBlock); + } + + function updateBonusEndBlock(uint256 _bonusEndBlock) external onlyOwner { + require(_bonusEndBlock > block.number, "Invalid end block"); + + _updatePool(); + bonusEndBlock = _bonusEndBlock; + emit NewBonusEndBlock(_bonusEndBlock); + } + + function stopReward() external onlyOwner { + require(bonusEndBlock < block.number, "Cannot stop ended cycle"); + bonusEndBlock = block.number; + + emit StopRewards(block.number); + } + + function deposit(uint256 _tokenId, address _recipient) public nonReentrant { + UserInfo storage user = userInfo[_recipient]; + _updatePool(); + + uint256 _pending = 0; + if (user.amount > 0) { + _pending = (user.amount * accTokenPerShare) / PRECISION_FACTOR - user.rewardDebt; + if (_pending > 0) { + (bool success, ) = _recipient.call{value: _pending}(""); + require(success, "Failed to send reward"); + } + } + + uint256 _amount = weightNFT.weight(_tokenId); + if (_amount > 0) { + user.amount = user.amount + _amount; + IERC721(weightNFT.nft()).safeTransferFrom(msg.sender, address(this), _tokenId); + tokenStaker[_tokenId] = _recipient; + tokenWeight[_tokenId] = _amount; + totalStakedWeight = totalStakedWeight + _amount; + } + + user.rewardDebt = (user.amount * accTokenPerShare) / PRECISION_FACTOR; + + emit Deposit(_recipient, _tokenId, _pending, _amount); + } + + function deposit(uint256 _tokenId) external { + deposit(_tokenId, msg.sender); + } + + function withdraw(uint256 _tokenId) external nonReentrant { + require(tokenStaker[_tokenId] == msg.sender, "Invalid staker"); + UserInfo storage user = userInfo[msg.sender]; + + _updatePool(); + + uint256 _pending = (user.amount * accTokenPerShare) / PRECISION_FACTOR - user.rewardDebt; + + uint256 _amount = tokenWeight[_tokenId]; + user.amount = user.amount - _amount; + IERC721(weightNFT.nft()).safeTransferFrom(address(this), msg.sender, _tokenId); + tokenStaker[_tokenId] = address(0); + tokenWeight[_tokenId] = 0; + totalStakedWeight = totalStakedWeight - _amount; + + if (_pending > 0) { + (bool success, ) = msg.sender.call{value: _pending}(""); + require(success, "Failed to send reward"); + } + + user.rewardDebt = (user.amount * accTokenPerShare) / PRECISION_FACTOR; + + emit Withdraw(msg.sender, _tokenId, _pending, _amount); + } + + function emergencyWithdraw(uint256 _tokenId) external nonReentrant { + require(tokenStaker[_tokenId] == msg.sender, "Invalid staker"); + UserInfo storage user = userInfo[msg.sender]; + + uint256 _amount = tokenWeight[_tokenId]; + + user.amount = user.amount - _amount; + IERC721(weightNFT.nft()).safeTransferFrom(address(this), msg.sender, _tokenId); + tokenStaker[_tokenId] = address(0); + tokenWeight[_tokenId] = 0; + totalStakedWeight = totalStakedWeight - _amount; + + emit EmergencyWithdraw(msg.sender, _tokenId, user.amount); + } + + function poke(uint256 _tokenId) external nonReentrant { + address _staker = tokenStaker[_tokenId]; + require(_staker != address(0), "Invalid token"); + + UserInfo storage user = userInfo[_staker]; + + _updatePool(); + + uint256 _pending = (user.amount * accTokenPerShare) / PRECISION_FACTOR - user.rewardDebt; + if (_pending > 0) { + (bool success, ) = _staker.call{value: _pending}(""); + require(success, "Failed to send reward"); + } + + uint256 _originalWeight = tokenWeight[_tokenId]; + uint256 _newWeight = weightNFT.weight(_tokenId); + if (_originalWeight != _newWeight) { + user.amount = user.amount - _originalWeight + _newWeight; + tokenWeight[_tokenId] = _newWeight; + totalStakedWeight = totalStakedWeight - _originalWeight + _newWeight; + } + + user.rewardDebt = (user.amount * accTokenPerShare) / PRECISION_FACTOR; + + emit Poke(_staker, _tokenId, _pending, _originalWeight, _newWeight); + } + + function claimRewards(address _user) external nonReentrant { + UserInfo storage user = userInfo[_user]; + + _updatePool(); + + uint256 _pending = (user.amount * accTokenPerShare) / PRECISION_FACTOR - user.rewardDebt; + if (_pending > 0) { + (bool success, ) = _user.call{value: _pending}(""); + require(success, "Failed to send reward"); + } + + user.rewardDebt = (user.amount * accTokenPerShare) / PRECISION_FACTOR; + emit ClaimRewards(_user, _pending); + } + + function pendingReward(address _user) external view returns (uint256) { + UserInfo storage user = userInfo[_user]; + uint256 _totalStakedWeight = totalStakedWeight; + if (block.number > lastRewardBlock && _totalStakedWeight != 0) { + uint256 multiplier = _getMultiplier(lastRewardBlock, block.number); + uint256 rewards = multiplier * rewardPerBlock; + uint256 adjustedTokenPerShare = accTokenPerShare + (rewards * PRECISION_FACTOR) / _totalStakedWeight; + return (user.amount * adjustedTokenPerShare) / PRECISION_FACTOR - user.rewardDebt; + } else { + return (user.amount * accTokenPerShare) / PRECISION_FACTOR - user.rewardDebt; + } + } + + function _updatePool() internal { + if (block.number <= lastRewardBlock) { + return; + } + + if (totalStakedWeight == 0) { + lastRewardBlock = block.number; + return; + } + + uint256 multiplier = _getMultiplier(lastRewardBlock, block.number); + uint256 rewards = multiplier * rewardPerBlock; + accTokenPerShare = accTokenPerShare + (rewards * PRECISION_FACTOR) / totalStakedWeight; + lastRewardBlock = block.number; + } + + function _getMultiplier(uint256 _from, uint256 _to) internal view returns (uint256) { + if (_to <= bonusEndBlock) { + return _to - _from; + } else if (_from >= bonusEndBlock) { + return 0; + } else { + return bonusEndBlock - _from; + } + } + + function stakingToken() external view returns (address) { + return weightNFT.nft(); + } + + receive() external payable {} +} diff --git a/contracts/utils/Multicall.sol b/contracts/utils/Multicall.sol new file mode 100644 index 0000000..df63f6f --- /dev/null +++ b/contracts/utils/Multicall.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @title Multicall +/// @notice Enables calling multiple methods in a single call to the contract +abstract contract Multicall { + function multicall(bytes[] calldata data) public payable returns (bytes[] memory results) { + results = new bytes[](data.length); + for (uint256 i = 0; i < data.length; i++) { + (bool success, bytes memory result) = address(this).delegatecall(data[i]); + + if (!success) { + // Next 5 lines from https://ethereum.stackexchange.com/a/83577 + if (result.length < 68) revert(); + assembly { + result := add(result, 0x04) + } + revert(abi.decode(result, (string))); + } + + results[i] = result; + } + } +}