-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* add pool indexer * add packed value * add distributor v2 to collect farm reward (#75) * only compatible with `distributor v1` when the reward type is `1` * fixed an issue with incorrect retrieval of rewards already claimed in `distributor v1` * support setting lockup period and lockup-free rate * support for lockup and burn token * add comments to `FarmRewardDistributorV2` * add `RewardCollectorV3` plugin * move `distributor` to `farming` folder * Lockup and burn token (#76) * support setting lockup period and lockup-free rate * support for lockup and burn token * add comments to `FarmRewardDistributorV2` * `RewardCollectorV3` now supports the functions of `RewardCollector` * add test case for `FarmRewardDistributorV2` * support collect referral reward * add deploy scripts for `FarmRewardDistributorV2` and `RewardCollectorV3` --------- Co-authored-by: waldoclayton <[email protected]>
- Loading branch information
1 parent
5b42a3b
commit c53801d
Showing
21 changed files
with
1,404 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
pragma solidity =0.8.21; | ||
|
||
import "./interfaces/IPoolFactory.sol"; | ||
|
||
/// @notice The contract is used for assigning pool and token indexes. | ||
/// Using an index instead of an address can effectively reduce the gas cost of the transaction. | ||
/// @custom:since v0.0.3 | ||
contract PoolIndexer { | ||
IPoolFactory public immutable poolFactory; | ||
|
||
uint24 public poolIndex; | ||
/// @notice Mapping of pools to their indexes | ||
mapping(IPool => uint24) public poolIndexes; | ||
/// @notice Mapping of indexes to their pools | ||
mapping(uint24 => IPool) public indexPools; | ||
|
||
/// @notice Emitted when a index is assigned to a pool and token | ||
/// @param pool The address of the pool | ||
/// @param token The ERC20 token used in the pool | ||
/// @param index The index assigned to the pool and token | ||
event PoolIndexAssigned(IPool indexed pool, IERC20 indexed token, uint24 indexed index); | ||
|
||
/// @notice Error thrown when the pool index is already assigned | ||
error PoolIndexAlreadyAssigned(IPool pool); | ||
/// @notice Error thrown when the pool is invalid | ||
error InvalidPool(IPool pool); | ||
|
||
constructor(IPoolFactory _poolFactory) { | ||
poolFactory = _poolFactory; | ||
} | ||
|
||
/// @notice Assign a pool index to a pool | ||
function assignPoolIndex(IPool _pool) external returns (uint24 index) { | ||
if (poolIndexes[_pool] != 0) revert PoolIndexAlreadyAssigned(_pool); | ||
|
||
if (!poolFactory.isPool(address(_pool))) revert InvalidPool(_pool); | ||
|
||
index = ++poolIndex; | ||
poolIndexes[_pool] = index; | ||
indexPools[index] = _pool; | ||
|
||
emit PoolIndexAssigned(_pool, _pool.token(), index); | ||
} | ||
|
||
/// @notice Get the index of a token | ||
/// @param _token The ERC20 token used in the pool | ||
/// @return index The index assigned to the token, 0 if not exists | ||
function tokenIndexes(IERC20 _token) external view returns (uint24 index) { | ||
index = poolIndexes[poolFactory.pools(_token)]; | ||
} | ||
|
||
/// @notice Get the token of an index | ||
/// @param _index The index assigned to the token | ||
/// @return token The ERC20 token used in the pool, address(0) if not exists | ||
function indexToken(uint24 _index) external view returns (IERC20 token) { | ||
IPool pool = indexPools[_index]; | ||
token = address(pool) == address(0) ? IERC20(address(0)) : pool.token(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,303 @@ | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
pragma solidity =0.8.21; | ||
|
||
import "../core/PoolIndexer.sol"; | ||
import "../core/interfaces/IPool.sol"; | ||
import "../governance/Governable.sol"; | ||
import "../types/PackedValue.sol"; | ||
import "../libraries/SafeCast.sol"; | ||
import "../libraries/SafeERC20.sol"; | ||
import "../libraries/Constants.sol"; | ||
import "../libraries/ReentrancyGuard.sol"; | ||
import "./PositionFarmRewardDistributor.sol"; | ||
import "@openzeppelin/contracts/utils/Address.sol"; | ||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; | ||
|
||
/// @notice The contract allows users to collect farm rewards and lockup and | ||
/// burn the rewards based on the lockup period | ||
/// @custom:since v0.0.3 | ||
contract FarmRewardDistributorV2 is Governable, ReentrancyGuard { | ||
using SafeCast for *; | ||
using SafeERC20 for IERC20; | ||
using ECDSA for bytes32; | ||
|
||
struct LockupFreeRateParameter { | ||
/// @notice The lockup period, 0 means no lockup | ||
uint16 period; | ||
/// @notice The lockup free rate, denominated in ten thousandths of a bip (i.e. 1e-8) | ||
uint32 lockupFreeRate; | ||
} | ||
|
||
uint16 public constant REWARD_TYPE_POSITION = 1; | ||
uint16 public constant REWARD_TYPE_LIQUIDITY = 2; | ||
uint16 public constant REWARD_TYPE_RISK_BUFFER_FUND = 3; | ||
uint16 public constant REWARD_TYPE_REFERRAL_LIQUIDITY = 4; | ||
uint16 public constant REWARD_TYPE_REFERRAL_POSITION = 5; | ||
|
||
/// @notice The address of the signer | ||
address public immutable signer; | ||
/// @notice The address of the token to be distributed | ||
IERC20 public immutable token; | ||
/// @notice The address of the EFC token | ||
IEFC public immutable EFC; | ||
/// @notice The address of the distributor v1 | ||
PositionFarmRewardDistributor public immutable distributorV1; | ||
/// @notice The address of the fee distributor | ||
IFeeDistributor public immutable feeDistributor; | ||
/// @notice The address of the pool indexer | ||
PoolIndexer public immutable poolIndexer; | ||
|
||
/// @notice The collectors | ||
mapping(address => bool) public collectors; | ||
|
||
/// @notice Mapping of reward types to their description. | ||
/// e.g. 1 => "Position", 2 => "Liquidity", 3 => "RiskBufferFund" | ||
mapping(uint16 => string) public rewardTypesDescriptions; | ||
/// @notice Mapping of lockup period to their lockup free rate | ||
mapping(uint16 => uint32) public lockupFreeRates; | ||
|
||
/// @notice The nonces for each account | ||
mapping(address => uint32) public nonces; | ||
/// @notice Mapping of accounts to their collected rewards for corresponding pools and reward types | ||
mapping(address => mapping(IPool => mapping(uint16 => uint200))) public collectedRewards; | ||
/// @notice Mapping of referral tokens to their collected rewards for corresponding pools and reward types | ||
mapping(uint16 => mapping(IPool => mapping(uint16 => uint200))) public collectedReferralRewards; | ||
|
||
/// @notice Event emitted when the reward type description is set | ||
event RewardTypeDescriptionSet(uint16 indexed rewardType, string description); | ||
/// @notice Event emitted when the collector is enabled or disabled | ||
/// @param collector The address of the collector | ||
/// @param enabled Whether the collector is enabled or disabled | ||
event CollectorUpdated(address indexed collector, bool enabled); | ||
/// @notice Event emitted when the lockup free rate is set | ||
/// @param period The lockup period, 0 means no lockup | ||
/// @param lockupFreeRate The lockup free rate, denominated in ten thousandths of a bip (i.e. 1e-8) | ||
event LockupFreeRateSet(uint16 indexed period, uint32 lockupFreeRate); | ||
/// @notice Event emitted when the reward is collected | ||
/// @param pool The pool from which to collect the reward | ||
/// @param account The account that collect the reward for | ||
/// @param rewardType The reward type | ||
/// @param nonce The nonce of the account | ||
/// @param receiver The address that received the reward | ||
/// @param amount The amount of the reward collected | ||
event RewardCollected( | ||
IPool pool, | ||
address indexed account, | ||
uint16 indexed rewardType, | ||
uint16 indexed referralToken, | ||
uint32 nonce, | ||
address receiver, | ||
uint200 amount | ||
); | ||
/// @notice Event emitted when the reward is locked and burned | ||
/// @param account The account that collect the reward for | ||
/// @param period The lockup period, 0 means no lockup | ||
/// @param receiver The address that received the unlocked reward or the locked reward | ||
/// @param lockedOrUnlockedAmount The amount of the unlocked reward or the locked reward | ||
/// @param burnedAmount The amount of the burned reward | ||
event RewardLockedAndBurned( | ||
address indexed account, | ||
uint16 indexed period, | ||
address indexed receiver, | ||
uint256 lockedOrUnlockedAmount, | ||
uint256 burnedAmount | ||
); | ||
|
||
/// @notice Error thrown when the reward type is invalid | ||
error InvalidRewardType(uint16 rewardType); | ||
/// @notice Error thrown when the lockup free rate is invalid | ||
error InvalidLockupFreeRate(uint32 lockupFreeRate); | ||
|
||
modifier onlyCollector() { | ||
if (!collectors[msg.sender]) revert Forbidden(); | ||
_; | ||
} | ||
|
||
constructor( | ||
address _signer, | ||
IEFC _EFC, | ||
PositionFarmRewardDistributor _distributorV1, | ||
IFeeDistributor _feeDistributor, | ||
PoolIndexer _poolIndexer | ||
) { | ||
signer = _signer; | ||
EFC = _EFC; | ||
distributorV1 = _distributorV1; | ||
token = _distributorV1.token(); | ||
feeDistributor = _feeDistributor; | ||
poolIndexer = _poolIndexer; | ||
|
||
token.approve(address(_feeDistributor), type(uint256).max); // approve unlimited | ||
|
||
_setRewardType(REWARD_TYPE_POSITION, "Position"); | ||
_setRewardType(REWARD_TYPE_LIQUIDITY, "Liquidity"); | ||
_setRewardType(REWARD_TYPE_RISK_BUFFER_FUND, "RiskBufferFund"); | ||
_setRewardType(REWARD_TYPE_REFERRAL_LIQUIDITY, "ReferralLiquidity"); | ||
_setRewardType(REWARD_TYPE_REFERRAL_POSITION, "ReferralPosition"); | ||
|
||
_setLockupFreeRate(LockupFreeRateParameter({period: 0, lockupFreeRate: 25_000_000})); // 25% | ||
_setLockupFreeRate(LockupFreeRateParameter({period: 30, lockupFreeRate: 50_000_000})); // 50% | ||
_setLockupFreeRate(LockupFreeRateParameter({period: 60, lockupFreeRate: 75_000_000})); // 75% | ||
_setLockupFreeRate(LockupFreeRateParameter({period: 90, lockupFreeRate: 100_000_000})); // 100% | ||
} | ||
|
||
/// @notice Set whether the address of the reward collector is enabled or disabled | ||
/// @param _collector Address to set | ||
/// @param _enabled Whether the address is enabled or disabled | ||
function setCollector(address _collector, bool _enabled) external virtual onlyGov { | ||
collectors[_collector] = _enabled; | ||
emit CollectorUpdated(_collector, _enabled); | ||
} | ||
|
||
/// @notice Set the reward type description | ||
/// @param _rewardType The reward type to set | ||
/// @param _description The description to set | ||
function setRewardType(uint16 _rewardType, string calldata _description) external virtual onlyGov { | ||
_setRewardType(_rewardType, _description); | ||
} | ||
|
||
/// @notice Set lockup free rates for multiple periods | ||
/// @param _parameters The parameters to set | ||
function setLockupFreeRates(LockupFreeRateParameter[] calldata _parameters) external virtual onlyGov { | ||
uint256 len = _parameters.length; | ||
for (uint256 i; i < len; ) { | ||
_setLockupFreeRate(_parameters[i]); | ||
// prettier-ignore | ||
unchecked { ++i; } | ||
} | ||
} | ||
|
||
/// @notice Collect the farm reward by the collector | ||
/// @param _account The account that collect the reward for | ||
/// @param _nonceAndLockupPeriod The packed values of the nonce and lockup period: bit 0-31 represent the nonce, | ||
/// bit 32-47 represent the lockup period | ||
/// @param _packedPoolRewardValues The packed values of the pool index, reward type, and amount: bit 0-23 represent | ||
/// the pool index, bit 24-39 represent the reward type, bit 40-55 represent the referral token, and bit 56-255 | ||
/// represent the amount. If the referral token is non-zero, the account MUST be the owner of the referral token | ||
/// @param _signature The signature of the parameters to verify | ||
/// @param _receiver The address that received the reward | ||
function collectBatch( | ||
address _account, | ||
PackedValue _nonceAndLockupPeriod, | ||
PackedValue[] calldata _packedPoolRewardValues, | ||
bytes calldata _signature, | ||
address _receiver | ||
) external virtual nonReentrant onlyCollector { | ||
if (_receiver == address(0)) _receiver = msg.sender; | ||
|
||
// check nonce | ||
uint32 nonce = _nonceAndLockupPeriod.unpackUint32(0); | ||
if (nonce != _nonceFor(_account) + 1) revert PositionFarmRewardDistributor.InvalidNonce(nonce); | ||
|
||
// check lockup period | ||
uint16 lockupPeriod = _nonceAndLockupPeriod.unpackUint16(32); | ||
uint32 lockupFreeRate = lockupFreeRates[lockupPeriod]; | ||
if (lockupFreeRate == 0) revert IFeeDistributor.InvalidLockupPeriod(lockupPeriod); | ||
|
||
// check signature | ||
address _signer = keccak256(abi.encode(_account, _nonceAndLockupPeriod, _packedPoolRewardValues)) | ||
.toEthSignedMessageHash() | ||
.recover(_signature); | ||
if (_signer != signer) revert PositionFarmRewardDistributor.InvalidSignature(); | ||
|
||
uint256 totalCollectableReward; | ||
IPool pool; | ||
PackedValue packedPoolRewardValue; | ||
uint256 len = _packedPoolRewardValues.length; | ||
for (uint256 i; i < len; ) { | ||
packedPoolRewardValue = _packedPoolRewardValues[i]; | ||
pool = poolIndexer.indexPools(packedPoolRewardValue.unpackUint24(0)); | ||
if (address(pool) == address(0)) revert PoolIndexer.InvalidPool(pool); | ||
|
||
uint16 rewardType = packedPoolRewardValue.unpackUint16(24); | ||
if (bytes(rewardTypesDescriptions[rewardType]).length == 0) revert InvalidRewardType(rewardType); | ||
|
||
uint16 referralToken = packedPoolRewardValue.unpackUint16(40); | ||
uint200 amount = packedPoolRewardValue.unpackUint200(56); | ||
uint200 collectableReward = amount - _collectedRewardFor(_account, pool, rewardType, referralToken); | ||
if (referralToken > 0) { | ||
if (EFC.ownerOf(referralToken) != _account) | ||
revert IFeeDistributor.InvalidNFTOwner(_account, referralToken); | ||
|
||
collectedReferralRewards[referralToken][pool][rewardType] = amount; | ||
} else { | ||
collectedRewards[_account][pool][rewardType] = amount; | ||
} | ||
|
||
totalCollectableReward += collectableReward; | ||
emit RewardCollected(pool, _account, rewardType, referralToken, nonce, _receiver, collectableReward); | ||
|
||
// prettier-ignore | ||
unchecked { ++i; } | ||
} | ||
|
||
nonces[_account] = nonce; | ||
|
||
_lockupAndBurnToken(_account, lockupPeriod, lockupFreeRate, totalCollectableReward, _receiver); | ||
} | ||
|
||
function _nonceFor(address _account) internal view virtual returns (uint32 nonce) { | ||
nonce = nonces[_account]; | ||
if (nonce == 0) nonce = distributorV1.nonces(_account); | ||
} | ||
|
||
function _collectedRewardFor( | ||
address _account, | ||
IPool _pool, | ||
uint16 _rewardType, | ||
uint16 _referralToken | ||
) internal view virtual returns (uint200 collectedReward) { | ||
if (_referralToken > 0) { | ||
collectedReward = collectedReferralRewards[_referralToken][_pool][_rewardType]; | ||
} else { | ||
collectedReward = collectedRewards[_account][_pool][_rewardType]; | ||
if (collectedReward == 0 && _rewardType == REWARD_TYPE_POSITION) | ||
collectedReward = distributorV1.collectedRewards(address(_pool), _account).toUint200(); | ||
} | ||
} | ||
|
||
function _setRewardType(uint16 _rewardType, string memory _description) internal virtual { | ||
require(bytes(_description).length <= 32); | ||
|
||
rewardTypesDescriptions[_rewardType] = _description; | ||
emit RewardTypeDescriptionSet(_rewardType, _description); | ||
} | ||
|
||
function _setLockupFreeRate(LockupFreeRateParameter memory _parameter) internal virtual { | ||
if (_parameter.lockupFreeRate > Constants.BASIS_POINTS_DIVISOR) | ||
revert InvalidLockupFreeRate(_parameter.lockupFreeRate); | ||
|
||
if (_parameter.period > 0) | ||
if (feeDistributor.lockupRewardMultipliers(_parameter.period) == 0) | ||
revert IFeeDistributor.InvalidLockupPeriod(_parameter.period); | ||
|
||
lockupFreeRates[_parameter.period] = _parameter.lockupFreeRate; | ||
emit LockupFreeRateSet(_parameter.period, _parameter.lockupFreeRate); | ||
} | ||
|
||
function _lockupAndBurnToken( | ||
address _account, | ||
uint16 _lockupPeriod, | ||
uint32 _lockupFreeRate, | ||
uint256 _totalCollectableReward, | ||
address _receiver | ||
) internal virtual { | ||
Address.functionCall( | ||
address(token), | ||
abi.encodeWithSignature("mint(address,uint256)", address(this), _totalCollectableReward) | ||
); | ||
|
||
uint256 lockedOrUnlockedAmount = (_totalCollectableReward * _lockupFreeRate) / Constants.BASIS_POINTS_DIVISOR; | ||
uint256 burnedAmount = _totalCollectableReward - lockedOrUnlockedAmount; | ||
|
||
// first burn the token | ||
if (burnedAmount > 0) token.safeTransfer(address(0x1), burnedAmount); | ||
|
||
// then lockup or transfer the token | ||
if (_lockupPeriod == 0) token.safeTransfer(_receiver, lockedOrUnlockedAmount); | ||
else feeDistributor.stake(lockedOrUnlockedAmount, _receiver, _lockupPeriod); | ||
|
||
emit RewardLockedAndBurned(_account, _lockupPeriod, _receiver, lockedOrUnlockedAmount, burnedAmount); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.