diff --git a/contracts/core/PoolIndexer.sol b/contracts/core/PoolIndexer.sol new file mode 100644 index 0000000..b359619 --- /dev/null +++ b/contracts/core/PoolIndexer.sol @@ -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(); + } +} diff --git a/contracts/farming/FarmRewardDistributorV2.sol b/contracts/farming/FarmRewardDistributorV2.sol new file mode 100644 index 0000000..9f49b96 --- /dev/null +++ b/contracts/farming/FarmRewardDistributorV2.sol @@ -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); + } +} diff --git a/contracts/plugins/PositionFarmRewardDistributor.sol b/contracts/farming/PositionFarmRewardDistributor.sol similarity index 95% rename from contracts/plugins/PositionFarmRewardDistributor.sol rename to contracts/farming/PositionFarmRewardDistributor.sol index 96ccb29..ce17d71 100644 --- a/contracts/plugins/PositionFarmRewardDistributor.sol +++ b/contracts/farming/PositionFarmRewardDistributor.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; +pragma solidity =0.8.21; import "../governance/Governable.sol"; import "@openzeppelin/contracts/utils/Address.sol"; @@ -71,9 +71,9 @@ contract PositionFarmRewardDistributor is Governable { token = _token; } - /// @notice Sets the address of the reward collector enabled or disabled - /// @param _collector The address of the reward collector - /// @param _enabled A boolean indicating whether the reward collector is enabled or disabled + /// @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 onlyGov { collectors[_collector] = _enabled; } diff --git a/contracts/plugins/RewardCollector.sol b/contracts/plugins/RewardCollector.sol index 816ed7b..f8b4b12 100644 --- a/contracts/plugins/RewardCollector.sol +++ b/contracts/plugins/RewardCollector.sol @@ -19,7 +19,11 @@ contract RewardCollector is Multicall { (router, EQU, EFC) = (_router, _EQU, _EFC); } - function sweepToken(IERC20 _token, uint256 _amountMinimum, address _receiver) external returns (uint256 amount) { + function sweepToken( + IERC20 _token, + uint256 _amountMinimum, + address _receiver + ) external virtual returns (uint256 amount) { amount = _token.balanceOf(address(this)); if (amount < _amountMinimum) revert InsufficientBalance(amount, _amountMinimum); @@ -29,7 +33,7 @@ contract RewardCollector is Multicall { function collectReferralFeeBatch( IPool[] calldata _pools, uint256[] calldata _referralTokens - ) external returns (uint256 amount) { + ) external virtual returns (uint256 amount) { _validateOwner(_referralTokens); IPool pool; @@ -42,36 +46,38 @@ contract RewardCollector is Multicall { } } - function collectFarmLiquidityRewardBatch(IPool[] calldata _pools) external returns (uint256 rewardDebt) { + function collectFarmLiquidityRewardBatch(IPool[] calldata _pools) external virtual returns (uint256 rewardDebt) { rewardDebt = router.pluginCollectFarmLiquidityRewardBatch(_pools, msg.sender, address(this)); } - function collectFarmRiskBufferFundRewardBatch(IPool[] calldata _pools) external returns (uint256 rewardDebt) { + function collectFarmRiskBufferFundRewardBatch( + IPool[] calldata _pools + ) external virtual returns (uint256 rewardDebt) { rewardDebt = router.pluginCollectFarmRiskBufferFundRewardBatch(_pools, msg.sender, address(this)); } function collectFarmReferralRewardBatch( IPool[] calldata _pools, uint256[] calldata _referralTokens - ) external returns (uint256 rewardDebt) { + ) external virtual returns (uint256 rewardDebt) { _validateOwner(_referralTokens); return router.pluginCollectFarmReferralRewardBatch(_pools, _referralTokens, address(this)); } - function collectStakingRewardBatch(uint256[] calldata _ids) external returns (uint256 rewardDebt) { + function collectStakingRewardBatch(uint256[] calldata _ids) external virtual returns (uint256 rewardDebt) { rewardDebt = router.pluginCollectStakingRewardBatch(msg.sender, address(this), _ids); } - function collectV3PosStakingRewardBatch(uint256[] calldata _ids) external returns (uint256 rewardDebt) { + function collectV3PosStakingRewardBatch(uint256[] calldata _ids) external virtual returns (uint256 rewardDebt) { rewardDebt = router.pluginCollectV3PosStakingRewardBatch(msg.sender, address(this), _ids); } - function collectArchitectRewardBatch(uint256[] calldata _tokenIDs) external returns (uint256 rewardDebt) { + function collectArchitectRewardBatch(uint256[] calldata _tokenIDs) external virtual returns (uint256 rewardDebt) { _validateOwner(_tokenIDs); rewardDebt = router.pluginCollectArchitectRewardBatch(address(this), _tokenIDs); } - function _validateOwner(uint256[] calldata _referralTokens) private view { + function _validateOwner(uint256[] calldata _referralTokens) internal view virtual { (address caller, uint256 tokensLen) = (msg.sender, _referralTokens.length); for (uint256 i; i < tokensLen; ++i) { if (EFC.ownerOf(_referralTokens[i]) != caller) diff --git a/contracts/plugins/RewardCollectorV2.sol b/contracts/plugins/RewardCollectorV2.sol index 259ca19..aa04667 100644 --- a/contracts/plugins/RewardCollectorV2.sol +++ b/contracts/plugins/RewardCollectorV2.sol @@ -2,7 +2,7 @@ pragma solidity =0.8.21; import "./RewardCollector.sol"; -import "./PositionFarmRewardDistributor.sol"; +import "../farming/PositionFarmRewardDistributor.sol"; /// @title RewardCollectorV2 /// @notice The contract extends the RewardCollector contract and implements additional functionality diff --git a/contracts/plugins/RewardCollectorV3.sol b/contracts/plugins/RewardCollectorV3.sol new file mode 100644 index 0000000..cc9393f --- /dev/null +++ b/contracts/plugins/RewardCollectorV3.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.21; + +import "./RewardCollector.sol"; +import "../farming/FarmRewardDistributorV2.sol"; + +/// @custom:since v0.0.3 +contract RewardCollectorV3 is RewardCollector { + FarmRewardDistributorV2 public immutable distributorV2; + + constructor( + Router _router, + IERC20 _EQU, + IEFC _EFC, + FarmRewardDistributorV2 _distributorV2 + ) RewardCollector(_router, _EQU, _EFC) { + distributorV2 = _distributorV2; + } + + function collectFarmRewardBatch( + PackedValue _nonceAndLockupPeriod, + PackedValue[] calldata _packedPoolRewardValues, + bytes calldata _signature, + address _receiver + ) external virtual { + distributorV2.collectBatch(msg.sender, _nonceAndLockupPeriod, _packedPoolRewardValues, _signature, _receiver); + } +} diff --git a/contracts/test/MockFeeDistributor.sol b/contracts/test/MockFeeDistributor.sol index 7bcea92..79aa088 100644 --- a/contracts/test/MockFeeDistributor.sol +++ b/contracts/test/MockFeeDistributor.sol @@ -1,15 +1,39 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity =0.8.21; +import "../libraries/SafeERC20.sol"; + contract MockFeeDistributor { uint256 public balance; uint256 public rewardAmountRes; uint256 public tokenIDRes; + mapping(uint16 => uint16) public multipliers; + + IERC20 public token; + + constructor() { + multipliers[30] = 1; + multipliers[60] = 2; + multipliers[90] = 3; + } + + function setToken(IERC20 _token) external { + token = _token; + } function depositFee(uint256 amount) external { balance += amount; } + function lockupRewardMultipliers(uint16 period) external view returns (uint16 multiplier) { + multiplier = multipliers[period]; + } + + function stake(uint256 amount, address /*account*/, uint16 period) external { + require(multipliers[period] > 0, "MockFeeDistributor: invalid period"); + SafeERC20.safeTransferFrom(token, msg.sender, address(this), amount); + } + function collectBatchByRouter( address /*_owner*/, address /*_receiver*/, diff --git a/contracts/types/PackedValue.sol b/contracts/types/PackedValue.sol new file mode 100644 index 0000000..1f21e80 --- /dev/null +++ b/contracts/types/PackedValue.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +type PackedValue is uint256; + +using { + packUint16, + unpackUint16, + packUint24, + unpackUint24, + packUint32, + unpackUint32, + packUint200, + unpackUint200, + packUint216, + unpackUint216, + packUint232, + unpackUint232 +} for PackedValue global; + +function packUint16(PackedValue self, uint16 value, uint8 position) pure returns (PackedValue) { + return PackedValue.wrap(PackedValue.unwrap(self) | (uint256(value) << position)); +} + +function unpackUint16(PackedValue self, uint8 position) pure returns (uint16) { + return uint16((PackedValue.unwrap(self) >> position) & 0xffff); +} + +function packUint24(PackedValue self, uint24 value, uint8 position) pure returns (PackedValue) { + return PackedValue.wrap(PackedValue.unwrap(self) | (uint256(value) << position)); +} + +function unpackUint24(PackedValue self, uint8 position) pure returns (uint24) { + return uint24((PackedValue.unwrap(self) >> position) & 0xffffff); +} + +function packUint32(PackedValue self, uint32 value, uint8 position) pure returns (PackedValue) { + return PackedValue.wrap(PackedValue.unwrap(self) | (uint256(value) << position)); +} + +function unpackUint32(PackedValue self, uint8 position) pure returns (uint32) { + return uint32((PackedValue.unwrap(self) >> position) & 0xffffffff); +} + +function packUint200(PackedValue self, uint200 value, uint8 position) pure returns (PackedValue) { + return PackedValue.wrap(PackedValue.unwrap(self) | (uint256(value) << position)); +} + +function unpackUint200(PackedValue self, uint8 position) pure returns (uint200) { + return uint200((PackedValue.unwrap(self) >> position) & 0xffffffffffffffffffffffffffffffffffffffffffffffffff); +} + +function packUint216(PackedValue self, uint216 value, uint8 position) pure returns (PackedValue) { + return PackedValue.wrap(PackedValue.unwrap(self) | (uint256(value) << position)); +} + +function unpackUint216(PackedValue self, uint8 position) pure returns (uint216) { + return uint216((PackedValue.unwrap(self) >> position) & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffff); +} + +function packUint232(PackedValue self, uint232 value, uint8 position) pure returns (PackedValue) { + return PackedValue.wrap(PackedValue.unwrap(self) | (uint256(value) << position)); +} + +function unpackUint232(PackedValue self, uint8 position) pure returns (uint232) { + return + uint232((PackedValue.unwrap(self) >> position) & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); +} diff --git a/hardhat.config.ts b/hardhat.config.ts index 7daf0da..c748f2b 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -72,6 +72,7 @@ const config: HardhatUserConfig = { }, etherscan: { apiKey: { + arbitrumGoerli: `${process.env.ARBISCAN_API_KEY}`, arbitrumOne: `${process.env.ARBISCAN_API_KEY}`, }, }, diff --git a/scripts/deployFarmRewardDistributorV2.ts b/scripts/deployFarmRewardDistributorV2.ts new file mode 100644 index 0000000..dcbf87b --- /dev/null +++ b/scripts/deployFarmRewardDistributorV2.ts @@ -0,0 +1,41 @@ +import {ethers, hardhatArguments} from "hardhat"; +import {networks} from "./networks"; + +async function main() { + const network = networks[hardhatArguments.network as keyof typeof networks]; + if (network == undefined) { + throw new Error(`network ${hardhatArguments.network} is not defined`); + } + if (network.distributorSigner == undefined) { + throw new Error(`network ${hardhatArguments.network} does not have a distributor signer`); + } + const chainId = (await ethers.provider.getNetwork()).chainId; + const document = require(`../deployments/${chainId}.json`); + + const Distributor = await ethers.getContractFactory("FarmRewardDistributorV2"); + const distributor = await Distributor.deploy( + network.distributorSigner, + document.deployments.EFC, + document.deployments.PositionFarmRewardDistributor, + document.deployments.FeeDistributor, + document.deployments.PoolIndexer + ); + await distributor.deployed(); + console.log(`FarmRewardDistributorV2 deployed to: ${distributor.address}`); + + document.deployments.FarmRewardDistributorV2 = distributor.address; + + const fs = require("fs"); + fs.writeFileSync(`deployments/${chainId}.json`, JSON.stringify(document)); + + // Set distributor as minter + const EQU = await ethers.getContractAt("MultiMinter", document.deployments.EQU); + await EQU.setMinter(distributor.address, true); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/deployPoolIndexer.ts b/scripts/deployPoolIndexer.ts new file mode 100644 index 0000000..e8ae71f --- /dev/null +++ b/scripts/deployPoolIndexer.ts @@ -0,0 +1,28 @@ +import {ethers, hardhatArguments} from "hardhat"; +import {networks} from "./networks"; + +async function main() { + const network = networks[hardhatArguments.network as keyof typeof networks]; + if (network == undefined) { + throw new Error(`network ${hardhatArguments.network} is not defined`); + } + const chainId = (await ethers.provider.getNetwork()).chainId; + const document = require(`../deployments/${chainId}.json`); + + const PoolIndexer = await ethers.getContractFactory("PoolIndexer"); + const poolIndexer = await PoolIndexer.deploy(document.deployments.PoolFactory); + await poolIndexer.deployed(); + console.log(`PoolIndexer deployed to: ${poolIndexer.address}`); + + document.deployments.PoolIndexer = poolIndexer.address; + + const fs = require("fs"); + fs.writeFileSync(`deployments/${chainId}.json`, JSON.stringify(document)); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/deployRewardCollectorV3.ts b/scripts/deployRewardCollectorV3.ts new file mode 100644 index 0000000..2ff8384 --- /dev/null +++ b/scripts/deployRewardCollectorV3.ts @@ -0,0 +1,44 @@ +import {ethers, hardhatArguments} from "hardhat"; +import {networks} from "./networks"; + +async function main() { + const network = networks[hardhatArguments.network as keyof typeof networks]; + if (network == undefined) { + throw new Error(`network ${hardhatArguments.network} is not defined`); + } + const chainId = (await ethers.provider.getNetwork()).chainId; + const document = require(`../deployments/${chainId}.json`); + + const RewardCollectorV3 = await ethers.getContractFactory("RewardCollectorV3"); + const rewardCollectorV3 = await RewardCollectorV3.deploy( + document.deployments.Router, + document.deployments.EQU, + document.deployments.EFC, + document.deployments.FarmRewardDistributorV2 + ); + await rewardCollectorV3.deployed(); + console.log(`RewardCollectorV3 deployed to: ${rewardCollectorV3.address}`); + + document.deployments.RewardCollectorV3 = rewardCollectorV3.address; + + const fs = require("fs"); + fs.writeFileSync(`deployments/${chainId}.json`, JSON.stringify(document)); + + // Register collector + const distributor = await ethers.getContractAt( + "FarmRewardDistributorV2", + document.deployments.FarmRewardDistributorV2 + ); + await distributor.setCollector(rewardCollectorV3.address, true); + + // Register plugin + const router = await ethers.getContractAt("Router", document.deployments.Router); + await router.registerPlugin(rewardCollectorV3.address); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/index.ts b/scripts/index.ts index 48d91ac..0f20538 100644 --- a/scripts/index.ts +++ b/scripts/index.ts @@ -284,6 +284,10 @@ async function main() { 2. Update Reward Farm - updateRewardFarm.ts 3. Deploy Position Farm Reward Distributor - deployPositionFarmRewardDistributor.ts 4. Deploy Reward Collector V2 - deployRewardCollectorV2.ts + 5. Deploy Pool Indexer - deployPoolIndexer.ts + 6. Assign Pool Index - registerPools.ts (incremental update) + 7. Deploy Farm Reward Distributor V2 - deployFarmRewardDistributorV2.ts + 8. Deploy Reward Collector V3 - deployRewardCollectorV3.ts `); } diff --git a/scripts/networks.ts b/scripts/networks.ts index 365e205..1d7c602 100644 --- a/scripts/networks.ts +++ b/scripts/networks.ts @@ -67,7 +67,7 @@ export const networks = { usd: "0x58e7F6b126eCC1A694B19062317b60Cf474E3D17", usdChainLinkPriceFeed: "0x0a023a3423D9b27A0BE48c768CCF2dD7877fEf5E", weth: "0xe39Ab88f8A4777030A534146A9Ca3B52bd5D43A3", - distributorSigner: undefined, + distributorSigner: "0x1696b05A21e0AF8379B5CEc3033bF764798E4168", minPositionRouterExecutionFee: ethers.utils.parseUnits("0.00021", "ether"), minOrderBookExecutionFee: ethers.utils.parseUnits("0.0003", "ether"), farmMintTime: Math.floor(new Date().getTime() / 1000) + 1 * 60 * 60, diff --git a/scripts/registerPools.ts b/scripts/registerPools.ts index 4380f48..25e4053 100644 --- a/scripts/registerPools.ts +++ b/scripts/registerPools.ts @@ -1,6 +1,7 @@ import {ethers, hardhatArguments} from "hardhat"; import {networks} from "./networks"; import {computePoolAddress, setBytecodeHash} from "../test/shared/address"; +import {Pool, PoolIndexer} from "../typechain-types"; export async function registerPools(chainId: number) { const network = networks[hardhatArguments.network as keyof typeof networks]; @@ -11,9 +12,16 @@ export async function registerPools(chainId: number) { setBytecodeHash(document.poolBytecodeHash); const poolFactory = await ethers.getContractAt("PoolFactory", document.deployments.PoolFactory); + let poolIndexer: undefined | PoolIndexer; + if (document.deployments.PoolIndexer != undefined) { + poolIndexer = await ethers.getContractAt("PoolIndexer", document.deployments.PoolIndexer); + } for (let item of network.tokens) { const enabled = await poolFactory.isEnabledToken(item.address); if (enabled) { + if ((await poolIndexer?.tokenIndexes(item.address)) === 0) { + await poolIndexer?.assignPoolIndex(poolFactory.pools(item.address)); + } continue; } @@ -21,6 +29,7 @@ export async function registerPools(chainId: number) { console.log(`registering ${item.name} (${item.address}) at ${poolAddr}`); await poolFactory.enableToken(item.address, item.tokenCfg, item.tokenFeeCfg, item.tokenPriceCfg); await poolFactory.createPool(item.address); + await poolIndexer?.assignPoolIndex(poolAddr); if (document.deployments.registerPools == undefined) { document.deployments.registerPools = []; } diff --git a/scripts/verify/verifyFarmRewardDistributorV2.ts b/scripts/verify/verifyFarmRewardDistributorV2.ts new file mode 100644 index 0000000..de58a95 --- /dev/null +++ b/scripts/verify/verifyFarmRewardDistributorV2.ts @@ -0,0 +1,11 @@ +import "dotenv/config"; + +const document = require(`../../deployments/${process.env.CHAIN_ID}.json`); + +module.exports = [ + `0x2288A79e5EFA061719EDaF8C69968c6e166ce322`, + `${document.deployments.EFC}`, + `${document.deployments.PositionFarmRewardDistributor}`, + `${document.deployments.FeeDistributor}`, + `${document.deployments.PoolIndexer}`, +]; diff --git a/scripts/verify/verifyPoolIndexer.ts b/scripts/verify/verifyPoolIndexer.ts new file mode 100644 index 0000000..957e606 --- /dev/null +++ b/scripts/verify/verifyPoolIndexer.ts @@ -0,0 +1,5 @@ +import "dotenv/config"; + +const document = require(`../../deployments/${process.env.CHAIN_ID}.json`); + +module.exports = [`${document.deployments.PoolFactory}`]; diff --git a/scripts/verify/verifyRewardCollectorV3.ts b/scripts/verify/verifyRewardCollectorV3.ts new file mode 100644 index 0000000..b07203d --- /dev/null +++ b/scripts/verify/verifyRewardCollectorV3.ts @@ -0,0 +1,10 @@ +import "dotenv/config"; + +const document = require(`../../deployments/${process.env.CHAIN_ID}.json`); + +module.exports = [ + `${document.deployments.Router}`, + `${document.deployments.EQU}`, + `${document.deployments.EFC}`, + `${document.deployments.FarmRewardDistributorV2}`, +]; diff --git a/test/foundry/FarmRewardDistributorV2.t.sol b/test/foundry/FarmRewardDistributorV2.t.sol new file mode 100644 index 0000000..3a0bf73 --- /dev/null +++ b/test/foundry/FarmRewardDistributorV2.t.sol @@ -0,0 +1,652 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.21; + +import "./Token.sol"; +import "forge-std/Test.sol"; +import "../../contracts/plugins/RewardCollectorV3.sol"; +import "../../contracts/farming/FarmRewardDistributorV2.sol"; +import "../../contracts/test/MockEFC.sol"; +import "../../contracts/test/MockPool.sol"; +import "../../contracts/test/MockPoolFactory.sol"; +import "../../contracts/test/MockFeeDistributor.sol"; + +contract FarmRewardDistributorV2Test is Test { + uint256 private constant SIGNER_PRIVATE_KEY = 0x12345; + uint256 private constant OTHER_PRIVATE_KEY = 0x54321; + address private constant ACCOUNT1 = address(0x201); + address private constant ACCOUNT2 = address(0x202); + + MockPool public pool1; + MockPool public pool2; + MockPoolFactory public poolFactory; + + PoolIndexer public poolIndexer; + + MockEFC public EFC; + RewardCollectorV3 public collector; + PositionFarmRewardDistributor public distributorV1; + FarmRewardDistributorV2 public distributorV2; + IERC20 public token; + MockFeeDistributor public feeDistributor; + + event RewardTypeDescriptionSet(uint16 indexed rewardType, string description); + event LockupFreeRateSet(uint16 indexed period, uint32 lockupFreeRate); + event RewardCollected( + IPool pool, + address indexed account, + uint16 indexed rewardType, + uint16 indexed referralToken, + uint32 nonce, + address receiver, + uint200 amount + ); + event RewardLockedAndBurned( + address indexed account, + uint16 indexed period, + address indexed receiver, + uint256 lockedOrUnlockedAmount, + uint256 burnedAmount + ); + + function setUp() public { + pool1 = new MockPool(IERC20(address(0)), IERC20(address(0x101))); + pool2 = new MockPool(IERC20(address(0)), IERC20(address(0x102))); + + poolFactory = new MockPoolFactory(); + poolFactory.createPool(address(pool1)); + poolFactory.createPool(address(pool2)); + + poolIndexer = new PoolIndexer(IPoolFactory(address(poolFactory))); + poolIndexer.assignPoolIndex(IPool(address(pool1))); + poolIndexer.assignPoolIndex(IPool(address(pool2))); + + address signer = vm.addr(SIGNER_PRIVATE_KEY); + token = new Token("T18", "T18"); + + distributorV1 = new PositionFarmRewardDistributor(signer, token); + Ownable(address(token)).transferOwnership(address(distributorV1)); + distributorV1.setCollector(address(this), true); + + PositionFarmRewardDistributor.PoolTotalReward[] + memory poolTotalRewards = new PositionFarmRewardDistributor.PoolTotalReward[](2); + poolTotalRewards[0] = PositionFarmRewardDistributor.PoolTotalReward(address(pool1), 1000); + poolTotalRewards[1] = PositionFarmRewardDistributor.PoolTotalReward(address(pool2), 2000); + distributorV1.collectPositionFarmRewardBatchByCollector( + ACCOUNT1, + 1, + poolTotalRewards, + signV1(SIGNER_PRIVATE_KEY, ACCOUNT1, 1, poolTotalRewards), + ACCOUNT1 + ); + + feeDistributor = new MockFeeDistributor(); + feeDistributor.setToken(token); + EFC = new MockEFC(); + EFC.setOwner(1, ACCOUNT1); + EFC.setOwner(1000, ACCOUNT1); + EFC.setOwner(10000, ACCOUNT1); + EFC.setOwner(19999, ACCOUNT1); + + distributorV2 = new FarmRewardDistributorV2( + signer, + IEFC(address(EFC)), + distributorV1, + IFeeDistributor(address(feeDistributor)), + poolIndexer + ); + distributorV2.setCollector(address(this), true); + vm.prank(address(distributorV1)); + Ownable(address(token)).transferOwnership(address(distributorV2)); + } + + function test_setRewardType_RevertIf_caller_is_not_gov() public { + vm.prank(address(0x1)); + vm.expectRevert(abi.encodeWithSelector(Governable.Forbidden.selector)); + distributorV2.setRewardType(4, "12345678901234567890123456789012"); + } + + function test_setRewardType_RevertIf_description_too_long() public { + vm.expectRevert(); + distributorV2.setRewardType(4, "1234567890123456789012345678901234567890123456789012345678901234567890"); + } + + function test_setRewardType() public { + vm.expectEmit(true, false, false, true); + emit RewardTypeDescriptionSet(4, "12345678901234567890123456789012"); + distributorV2.setRewardType(4, "12345678901234567890123456789012"); + assertEq(distributorV2.rewardTypesDescriptions(4), "12345678901234567890123456789012"); + + vm.expectEmit(true, false, false, true); + emit RewardTypeDescriptionSet(1, "12345678901234567890123456789012"); + distributorV2.setRewardType(1, "12345678901234567890123456789012"); + assertEq(distributorV2.rewardTypesDescriptions(1), "12345678901234567890123456789012"); + } + + function testFuzz_setRewardType(uint16 rewardType, string memory description) public { + if (bytes(description).length > 32) { + vm.expectRevert(); + distributorV2.setRewardType(rewardType, description); + } else { + vm.expectEmit(true, false, false, true); + emit RewardTypeDescriptionSet(rewardType, description); + distributorV2.setRewardType(rewardType, description); + assertEq(distributorV2.rewardTypesDescriptions(rewardType), description); + } + } + + function test_setLockupFreeRates_RevertIf_caller_is_not_gov() public { + vm.prank(address(0x1)); + vm.expectRevert(abi.encodeWithSelector(Governable.Forbidden.selector)); + FarmRewardDistributorV2.LockupFreeRateParameter[] + memory parameters = new FarmRewardDistributorV2.LockupFreeRateParameter[](1); + parameters[0] = FarmRewardDistributorV2.LockupFreeRateParameter(0, 1); + distributorV2.setLockupFreeRates(parameters); + } + + function test_setLockupFreeRates_RevertIf_lockup_free_rate_too_large() public { + vm.expectRevert(abi.encodeWithSelector(FarmRewardDistributorV2.InvalidLockupFreeRate.selector, 100000001)); + FarmRewardDistributorV2.LockupFreeRateParameter[] + memory parameters = new FarmRewardDistributorV2.LockupFreeRateParameter[](1); + parameters[0] = FarmRewardDistributorV2.LockupFreeRateParameter(0, 100000001); + distributorV2.setLockupFreeRates(parameters); + } + + function test_setLockupFreeRates_RevertIf_period_not_enabled() public { + vm.expectRevert(abi.encodeWithSelector(IFeeDistributor.InvalidLockupPeriod.selector, 31)); + FarmRewardDistributorV2.LockupFreeRateParameter[] + memory parameters = new FarmRewardDistributorV2.LockupFreeRateParameter[](1); + parameters[0] = FarmRewardDistributorV2.LockupFreeRateParameter(31, 100000000); + distributorV2.setLockupFreeRates(parameters); + } + + function test_setLockupFreeRates() public { + vm.expectEmit(true, false, false, true); + emit LockupFreeRateSet(0, 20_000_000); + vm.expectEmit(true, false, false, true); + emit LockupFreeRateSet(30, 30_000_000); + vm.expectEmit(true, false, false, true); + emit LockupFreeRateSet(60, 30_000_000); + vm.expectEmit(true, false, false, true); + emit LockupFreeRateSet(90, 0); + FarmRewardDistributorV2.LockupFreeRateParameter[] + memory parameters = new FarmRewardDistributorV2.LockupFreeRateParameter[](4); + parameters[0] = FarmRewardDistributorV2.LockupFreeRateParameter(0, 20_000_000); + parameters[1] = FarmRewardDistributorV2.LockupFreeRateParameter(30, 30_000_000); + parameters[2] = FarmRewardDistributorV2.LockupFreeRateParameter(60, 30_000_000); + parameters[3] = FarmRewardDistributorV2.LockupFreeRateParameter(90, 0); + distributorV2.setLockupFreeRates(parameters); + } + + function test_collectBatch_RevertIf_caller_is_not_collector() public { + vm.prank(address(0x1)); + vm.expectRevert(abi.encodeWithSelector(Governable.Forbidden.selector)); + PackedValue[] memory packedValues = new PackedValue[](1); + packedValues[0] = PackedValue.wrap(0); + distributorV2.collectBatch(ACCOUNT1, PackedValue.wrap(1), packedValues, bytes(""), ACCOUNT1); + } + + function test_collectBatch_RevertIf_nonce_is_invalid() public { + vm.expectRevert(abi.encodeWithSelector(PositionFarmRewardDistributor.InvalidNonce.selector, 1)); + PackedValue[] memory packedValues = new PackedValue[](1); + packedValues[0] = PackedValue.wrap(0); + distributorV2.collectBatch(ACCOUNT1, PackedValue.wrap(1), packedValues, bytes(""), ACCOUNT1); + } + + function test_collectBatch_RevertIf_period_is_invalid() public { + vm.expectRevert(abi.encodeWithSelector(IFeeDistributor.InvalidLockupPeriod.selector, 31)); + PackedValue[] memory packedValues = new PackedValue[](1); + packedValues[0] = PackedValue.wrap(0); + PackedValue nonceAndLockupPeriod = PackedValue.wrap(0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint32(1, 0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint16(31, 32); + distributorV2.collectBatch(ACCOUNT2, nonceAndLockupPeriod, packedValues, bytes(""), ACCOUNT2); + } + + function test_collectBatch_RevertIf_signature_is_invalid() public { + vm.expectRevert(abi.encodeWithSelector(PositionFarmRewardDistributor.InvalidSignature.selector)); + PackedValue[] memory packedValues = new PackedValue[](1); + packedValues[0] = PackedValue.wrap(0); + PackedValue nonceAndLockupPeriod = PackedValue.wrap(0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint32(1, 0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint16(30, 32); + distributorV2.collectBatch( + ACCOUNT2, + nonceAndLockupPeriod, + packedValues, + signV2(SIGNER_PRIVATE_KEY, ACCOUNT1, nonceAndLockupPeriod, packedValues), + ACCOUNT2 + ); + } + + function test_collectBatch_RevertIf_pool_is_invalid() public { + vm.expectRevert(abi.encodeWithSelector(PoolIndexer.InvalidPool.selector, address(0))); + PackedValue[] memory packedValues = new PackedValue[](1); + packedValues[0] = PackedValue.wrap(0); + PackedValue nonceAndLockupPeriod = PackedValue.wrap(0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint32(2, 0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint16(30, 32); + distributorV2.collectBatch( + ACCOUNT1, + nonceAndLockupPeriod, + packedValues, + signV2(SIGNER_PRIVATE_KEY, ACCOUNT1, nonceAndLockupPeriod, packedValues), + ACCOUNT1 + ); + } + + function test_collectBatch_RevertIf_reward_type_is_invalid() public { + vm.expectRevert(abi.encodeWithSelector(FarmRewardDistributorV2.InvalidRewardType.selector, type(uint16).max)); + PackedValue[] memory packedValues = new PackedValue[](1); + PackedValue packedPoolRewardValue = PackedValue.wrap(0); + packedPoolRewardValue = packedPoolRewardValue.packUint24(1, 0); + packedPoolRewardValue = packedPoolRewardValue.packUint16(type(uint16).max, 24); + packedValues[0] = packedPoolRewardValue; + PackedValue nonceAndLockupPeriod = PackedValue.wrap(0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint32(2, 0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint16(30, 32); + distributorV2.collectBatch( + ACCOUNT1, + nonceAndLockupPeriod, + packedValues, + signV2(SIGNER_PRIVATE_KEY, ACCOUNT1, nonceAndLockupPeriod, packedValues), + ACCOUNT1 + ); + } + + function test_collectBatch_RevertIf_amount_too_small() public { + vm.expectRevert(); + PackedValue[] memory packedValues = new PackedValue[](1); + PackedValue packedPoolRewardValue = PackedValue.wrap(0); + packedPoolRewardValue = packedPoolRewardValue.packUint24(1, 0); + packedPoolRewardValue = packedPoolRewardValue.packUint16(1, 24); + packedPoolRewardValue = packedPoolRewardValue.packUint16(0, 40); + packedPoolRewardValue = packedPoolRewardValue.packUint200(999, 56); + packedValues[0] = packedPoolRewardValue; + PackedValue nonceAndLockupPeriod = PackedValue.wrap(0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint32(2, 0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint16(30, 32); + distributorV2.collectBatch( + ACCOUNT1, + nonceAndLockupPeriod, + packedValues, + signV2(SIGNER_PRIVATE_KEY, ACCOUNT1, nonceAndLockupPeriod, packedValues), + ACCOUNT1 + ); + } + + function test_collectBatch_RevertIf_not_referral_token_owner() public { + vm.expectRevert(abi.encodeWithSelector(IFeeDistributor.InvalidNFTOwner.selector, ACCOUNT2, 1)); + PackedValue[] memory packedValues = new PackedValue[](1); + PackedValue packedPoolRewardValue = PackedValue.wrap(0); + packedPoolRewardValue = packedPoolRewardValue.packUint24(1, 0); + packedPoolRewardValue = packedPoolRewardValue.packUint16(1, 24); + packedPoolRewardValue = packedPoolRewardValue.packUint16(1, 40); + packedPoolRewardValue = packedPoolRewardValue.packUint216(999, 56); + packedValues[0] = packedPoolRewardValue; + PackedValue nonceAndLockupPeriod = PackedValue.wrap(0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint32(1, 0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint16(30, 32); + distributorV2.collectBatch( + ACCOUNT2, + nonceAndLockupPeriod, + packedValues, + signV2(SIGNER_PRIVATE_KEY, ACCOUNT2, nonceAndLockupPeriod, packedValues), + ACCOUNT1 + ); + } + + function test_collectBatch_read_value_once_from_distributor_v1_if_reward_type_is_1() public { + PackedValue[] memory packedValues = new PackedValue[](1); + PackedValue packedPoolRewardValue = PackedValue.wrap(0); + packedPoolRewardValue = packedPoolRewardValue.packUint24(1, 0); + packedPoolRewardValue = packedPoolRewardValue.packUint16(1, 24); + packedPoolRewardValue = packedPoolRewardValue.packUint16(0, 40); + packedPoolRewardValue = packedPoolRewardValue.packUint200(1003, 56); + packedValues[0] = packedPoolRewardValue; + PackedValue nonceAndLockupPeriod = PackedValue.wrap(0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint32(2, 0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint16(30, 32); + distributorV2.collectBatch( + ACCOUNT1, + nonceAndLockupPeriod, + packedValues, + signV2(SIGNER_PRIVATE_KEY, ACCOUNT1, nonceAndLockupPeriod, packedValues), + ACCOUNT1 + ); + + vm.expectEmit(true, true, true, true); + emit RewardCollected(IPool(address(pool1)), ACCOUNT1, 1, 0, 3, ACCOUNT1, 7); + packedPoolRewardValue = PackedValue.wrap(0); + packedPoolRewardValue = packedPoolRewardValue.packUint24(1, 0); + packedPoolRewardValue = packedPoolRewardValue.packUint16(1, 24); + packedPoolRewardValue = packedPoolRewardValue.packUint16(0, 40); + packedPoolRewardValue = packedPoolRewardValue.packUint200(1010, 56); + packedValues[0] = packedPoolRewardValue; + nonceAndLockupPeriod = PackedValue.wrap(0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint32(3, 0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint16(30, 32); + distributorV2.collectBatch( + ACCOUNT1, + nonceAndLockupPeriod, + packedValues, + signV2(SIGNER_PRIVATE_KEY, ACCOUNT1, nonceAndLockupPeriod, packedValues), + ACCOUNT1 + ); + } + + function test_collectBatch_read_value_from_distributor_v1_if_reward_type_is_1() public { + vm.expectEmit(true, true, true, true); + emit RewardCollected(IPool(address(pool1)), ACCOUNT1, 1, 0, 2, ACCOUNT1, 3); + PackedValue[] memory packedValues = new PackedValue[](1); + PackedValue packedPoolRewardValue = PackedValue.wrap(0); + packedPoolRewardValue = packedPoolRewardValue.packUint24(1, 0); + packedPoolRewardValue = packedPoolRewardValue.packUint16(1, 24); + packedPoolRewardValue = packedPoolRewardValue.packUint16(0, 40); + packedPoolRewardValue = packedPoolRewardValue.packUint200(1003, 56); + packedValues[0] = packedPoolRewardValue; + PackedValue nonceAndLockupPeriod = PackedValue.wrap(0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint32(2, 0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint16(30, 32); + distributorV2.collectBatch( + ACCOUNT1, + nonceAndLockupPeriod, + packedValues, + signV2(SIGNER_PRIVATE_KEY, ACCOUNT1, nonceAndLockupPeriod, packedValues), + ACCOUNT1 + ); + } + + function test_collectBatch_not_read_value_from_distributor_v1_if_reward_type_is_2() public { + vm.expectEmit(true, true, true, true); + emit RewardCollected(IPool(address(pool1)), ACCOUNT1, 2, 0, 2, ACCOUNT1, 1003); + PackedValue[] memory packedValues = new PackedValue[](1); + PackedValue packedPoolRewardValue = PackedValue.wrap(0); + packedPoolRewardValue = packedPoolRewardValue.packUint24(1, 0); + packedPoolRewardValue = packedPoolRewardValue.packUint16(2, 24); + packedPoolRewardValue = packedPoolRewardValue.packUint16(0, 40); + packedPoolRewardValue = packedPoolRewardValue.packUint200(1003, 56); + packedValues[0] = packedPoolRewardValue; + PackedValue nonceAndLockupPeriod = PackedValue.wrap(0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint32(2, 0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint16(30, 32); + distributorV2.collectBatch( + ACCOUNT1, + nonceAndLockupPeriod, + packedValues, + signV2(SIGNER_PRIVATE_KEY, ACCOUNT1, nonceAndLockupPeriod, packedValues), + ACCOUNT1 + ); + } + + function test_collectBatch_multiple_pools() public { + vm.expectEmit(true, true, true, true); + emit RewardCollected(IPool(address(pool1)), ACCOUNT1, 1, 0, 2, ACCOUNT1, 3); + PackedValue[] memory packedValues = new PackedValue[](8); + PackedValue packedPoolRewardValue = PackedValue.wrap(0); + packedPoolRewardValue = packedPoolRewardValue.packUint24(1, 0); + packedPoolRewardValue = packedPoolRewardValue.packUint16(1, 24); + packedPoolRewardValue = packedPoolRewardValue.packUint16(0, 40); + packedPoolRewardValue = packedPoolRewardValue.packUint200(1003, 56); + packedValues[0] = packedPoolRewardValue; + + vm.expectEmit(true, true, true, true); + emit RewardCollected(IPool(address(pool2)), ACCOUNT1, 1, 0, 2, ACCOUNT1, 5); + packedPoolRewardValue = PackedValue.wrap(0); + packedPoolRewardValue = packedPoolRewardValue.packUint24(2, 0); + packedPoolRewardValue = packedPoolRewardValue.packUint16(1, 24); + packedPoolRewardValue = packedPoolRewardValue.packUint16(0, 40); + packedPoolRewardValue = packedPoolRewardValue.packUint200(2005, 56); + packedValues[1] = packedPoolRewardValue; + + vm.expectEmit(true, true, true, true); + emit RewardCollected(IPool(address(pool1)), ACCOUNT1, 2, 0, 2, ACCOUNT1, 1003); + packedPoolRewardValue = PackedValue.wrap(0); + packedPoolRewardValue = packedPoolRewardValue.packUint24(1, 0); + packedPoolRewardValue = packedPoolRewardValue.packUint16(2, 24); + packedPoolRewardValue = packedPoolRewardValue.packUint16(0, 40); + packedPoolRewardValue = packedPoolRewardValue.packUint200(1003, 56); + packedValues[2] = packedPoolRewardValue; + + vm.expectEmit(true, true, true, true); + emit RewardCollected(IPool(address(pool2)), ACCOUNT1, 2, 0, 2, ACCOUNT1, 2005); + packedPoolRewardValue = PackedValue.wrap(0); + packedPoolRewardValue = packedPoolRewardValue.packUint24(2, 0); + packedPoolRewardValue = packedPoolRewardValue.packUint16(2, 24); + packedPoolRewardValue = packedPoolRewardValue.packUint16(0, 40); + packedPoolRewardValue = packedPoolRewardValue.packUint200(2005, 56); + packedValues[3] = packedPoolRewardValue; + + vm.expectEmit(true, true, true, true); + emit RewardCollected(IPool(address(pool1)), ACCOUNT1, 3, 0, 2, ACCOUNT1, 1003); + packedPoolRewardValue = PackedValue.wrap(0); + packedPoolRewardValue = packedPoolRewardValue.packUint24(1, 0); + packedPoolRewardValue = packedPoolRewardValue.packUint16(3, 24); + packedPoolRewardValue = packedPoolRewardValue.packUint16(0, 40); + packedPoolRewardValue = packedPoolRewardValue.packUint200(1003, 56); + packedValues[4] = packedPoolRewardValue; + + vm.expectEmit(true, true, true, true); + emit RewardCollected(IPool(address(pool2)), ACCOUNT1, 3, 0, 2, ACCOUNT1, 2005); + packedPoolRewardValue = PackedValue.wrap(0); + packedPoolRewardValue = packedPoolRewardValue.packUint24(2, 0); + packedPoolRewardValue = packedPoolRewardValue.packUint16(3, 24); + packedPoolRewardValue = packedPoolRewardValue.packUint16(0, 40); + packedPoolRewardValue = packedPoolRewardValue.packUint200(2005, 56); + packedValues[5] = packedPoolRewardValue; + + vm.expectEmit(true, true, true, true); + emit RewardCollected(IPool(address(pool1)), ACCOUNT1, 3, 1, 2, ACCOUNT1, 1003); + packedPoolRewardValue = PackedValue.wrap(0); + packedPoolRewardValue = packedPoolRewardValue.packUint24(1, 0); + packedPoolRewardValue = packedPoolRewardValue.packUint16(3, 24); + packedPoolRewardValue = packedPoolRewardValue.packUint16(1, 40); + packedPoolRewardValue = packedPoolRewardValue.packUint200(1003, 56); + packedValues[6] = packedPoolRewardValue; + + vm.expectEmit(true, true, true, true); + emit RewardCollected(IPool(address(pool2)), ACCOUNT1, 3, 19999, 2, ACCOUNT1, 2005); + packedPoolRewardValue = PackedValue.wrap(0); + packedPoolRewardValue = packedPoolRewardValue.packUint24(2, 0); + packedPoolRewardValue = packedPoolRewardValue.packUint16(3, 24); + packedPoolRewardValue = packedPoolRewardValue.packUint16(19999, 40); + packedPoolRewardValue = packedPoolRewardValue.packUint200(2005, 56); + packedValues[7] = packedPoolRewardValue; + + vm.expectEmit(true, true, true, true); + emit RewardLockedAndBurned(ACCOUNT1, 30, ACCOUNT1, 4516, 4516); + PackedValue nonceAndLockupPeriod = PackedValue.wrap(0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint32(2, 0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint16(30, 32); + distributorV2.collectBatch( + ACCOUNT1, + nonceAndLockupPeriod, + packedValues, + signV2(SIGNER_PRIVATE_KEY, ACCOUNT1, nonceAndLockupPeriod, packedValues), + ACCOUNT1 + ); + + assertEq(1003, distributorV2.collectedRewards(ACCOUNT1, IPool(address(pool1)), 1)); + assertEq(2005, distributorV2.collectedRewards(ACCOUNT1, IPool(address(pool2)), 1)); + + assertEq(1003, distributorV2.collectedRewards(ACCOUNT1, IPool(address(pool1)), 2)); + assertEq(2005, distributorV2.collectedRewards(ACCOUNT1, IPool(address(pool2)), 2)); + + assertEq(1003, distributorV2.collectedRewards(ACCOUNT1, IPool(address(pool1)), 3)); + assertEq(2005, distributorV2.collectedRewards(ACCOUNT1, IPool(address(pool2)), 3)); + + assertEq(1003, distributorV2.collectedReferralRewards(1, IPool(address(pool1)), 3)); + assertEq(0, distributorV2.collectedReferralRewards(1, IPool(address(pool2)), 3)); + + assertEq(0, distributorV2.collectedReferralRewards(19999, IPool(address(pool1)), 3)); + assertEq(2005, distributorV2.collectedReferralRewards(19999, IPool(address(pool2)), 3)); + } + + function test_collectBatch_receiver_is_zero_address() public { + vm.expectEmit(true, true, true, true); + emit RewardCollected(IPool(address(pool1)), ACCOUNT2, 1, 0, 1, address(this), 1003); + PackedValue[] memory packedValues = new PackedValue[](1); + PackedValue packedPoolRewardValue = PackedValue.wrap(0); + packedPoolRewardValue = packedPoolRewardValue.packUint24(1, 0); + packedPoolRewardValue = packedPoolRewardValue.packUint16(1, 24); + packedPoolRewardValue = packedPoolRewardValue.packUint16(0, 40); + packedPoolRewardValue = packedPoolRewardValue.packUint200(1003, 56); + packedValues[0] = packedPoolRewardValue; + + vm.expectEmit(true, true, true, true); + emit RewardLockedAndBurned(ACCOUNT2, 30, address(this), 501, 502); + PackedValue nonceAndLockupPeriod = PackedValue.wrap(0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint32(1, 0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint16(30, 32); + distributorV2.collectBatch( + ACCOUNT2, + nonceAndLockupPeriod, + packedValues, + signV2(SIGNER_PRIVATE_KEY, ACCOUNT2, nonceAndLockupPeriod, packedValues), + address(0) + ); + } + + function test_collectBatch_period_is_0() public { + vm.expectEmit(true, true, true, true); + emit RewardCollected(IPool(address(pool1)), ACCOUNT2, 1, 0, 1, address(this), 1003); + PackedValue[] memory packedValues = new PackedValue[](1); + PackedValue packedPoolRewardValue = PackedValue.wrap(0); + packedPoolRewardValue = packedPoolRewardValue.packUint24(1, 0); + packedPoolRewardValue = packedPoolRewardValue.packUint16(1, 24); + packedPoolRewardValue = packedPoolRewardValue.packUint16(0, 40); + packedPoolRewardValue = packedPoolRewardValue.packUint200(1003, 56); + packedValues[0] = packedPoolRewardValue; + + vm.expectEmit(true, true, true, true); + emit RewardLockedAndBurned(ACCOUNT2, 0, address(this), 250, 1003 - 250); + PackedValue nonceAndLockupPeriod = PackedValue.wrap(0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint32(1, 0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint16(0, 32); + distributorV2.collectBatch( + ACCOUNT2, + nonceAndLockupPeriod, + packedValues, + signV2(SIGNER_PRIVATE_KEY, ACCOUNT2, nonceAndLockupPeriod, packedValues), + address(0) + ); + + assertEq(250, token.balanceOf(address(this))); + assertEq(1003 - 250, token.balanceOf(address(0x1))); + } + + function test_collectBatch_period_is_30() public { + vm.expectEmit(true, true, true, true); + emit RewardCollected(IPool(address(pool1)), ACCOUNT2, 1, 0, 1, address(this), 1003); + PackedValue[] memory packedValues = new PackedValue[](1); + PackedValue packedPoolRewardValue = PackedValue.wrap(0); + packedPoolRewardValue = packedPoolRewardValue.packUint24(1, 0); + packedPoolRewardValue = packedPoolRewardValue.packUint16(1, 24); + packedPoolRewardValue = packedPoolRewardValue.packUint16(0, 40); + packedPoolRewardValue = packedPoolRewardValue.packUint200(1003, 56); + packedValues[0] = packedPoolRewardValue; + + vm.expectEmit(true, true, true, true); + emit RewardLockedAndBurned(ACCOUNT2, 30, address(this), 501, 502); + PackedValue nonceAndLockupPeriod = PackedValue.wrap(0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint32(1, 0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint16(30, 32); + distributorV2.collectBatch( + ACCOUNT2, + nonceAndLockupPeriod, + packedValues, + signV2(SIGNER_PRIVATE_KEY, ACCOUNT2, nonceAndLockupPeriod, packedValues), + address(0) + ); + + assertEq(501, token.balanceOf(address(feeDistributor))); + assertEq(502, token.balanceOf(address(0x1))); + } + + function test_collectBatch_period_is_60() public { + vm.expectEmit(true, true, true, true); + emit RewardCollected(IPool(address(pool1)), ACCOUNT2, 1, 0, 1, address(this), 1003); + PackedValue[] memory packedValues = new PackedValue[](1); + PackedValue packedPoolRewardValue = PackedValue.wrap(0); + packedPoolRewardValue = packedPoolRewardValue.packUint24(1, 0); + packedPoolRewardValue = packedPoolRewardValue.packUint16(1, 24); + packedPoolRewardValue = packedPoolRewardValue.packUint16(0, 40); + packedPoolRewardValue = packedPoolRewardValue.packUint200(1003, 56); + packedValues[0] = packedPoolRewardValue; + + vm.expectEmit(true, true, true, true); + emit RewardLockedAndBurned(ACCOUNT2, 60, address(this), 752, 1003 - 752); + PackedValue nonceAndLockupPeriod = PackedValue.wrap(0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint32(1, 0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint16(60, 32); + distributorV2.collectBatch( + ACCOUNT2, + nonceAndLockupPeriod, + packedValues, + signV2(SIGNER_PRIVATE_KEY, ACCOUNT2, nonceAndLockupPeriod, packedValues), + address(0) + ); + + assertEq(752, token.balanceOf(address(feeDistributor))); + assertEq(1003 - 752, token.balanceOf(address(0x1))); + } + + function test_collectBatch_period_is_90() public { + vm.expectEmit(true, true, true, true); + emit RewardCollected(IPool(address(pool1)), ACCOUNT2, 1, 0, 1, address(this), 1003); + PackedValue[] memory packedValues = new PackedValue[](1); + PackedValue packedPoolRewardValue = PackedValue.wrap(0); + packedPoolRewardValue = packedPoolRewardValue.packUint24(1, 0); + packedPoolRewardValue = packedPoolRewardValue.packUint16(1, 24); + packedPoolRewardValue = packedPoolRewardValue.packUint16(0, 40); + packedPoolRewardValue = packedPoolRewardValue.packUint200(1003, 56); + packedValues[0] = packedPoolRewardValue; + + vm.expectEmit(true, true, true, true); + emit RewardLockedAndBurned(ACCOUNT2, 90, address(this), 1003, 0); + PackedValue nonceAndLockupPeriod = PackedValue.wrap(0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint32(1, 0); + nonceAndLockupPeriod = nonceAndLockupPeriod.packUint16(90, 32); + distributorV2.collectBatch( + ACCOUNT2, + nonceAndLockupPeriod, + packedValues, + signV2(SIGNER_PRIVATE_KEY, ACCOUNT2, nonceAndLockupPeriod, packedValues), + address(0) + ); + + assertEq(1003, token.balanceOf(address(feeDistributor))); + assertEq(0, token.balanceOf(address(0x1))); + } + + function signV1( + uint256 _privateKey, + address _account, + uint32 _nonce, + PositionFarmRewardDistributor.PoolTotalReward[] memory _poolTotalRewards + ) private pure returns (bytes memory) { + bytes32 hash = keccak256( + abi.encodePacked( + "\x19Ethereum Signed Message:\n32", + keccak256(abi.encode(_account, _nonce, _poolTotalRewards)) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, hash); + return abi.encodePacked(r, s, v); + } + + function signV2( + uint256 _privateKey, + address _account, + PackedValue _nonceAndLockupPeriod, + PackedValue[] memory _packedPoolRewardValues + ) private pure returns (bytes memory) { + bytes32 hash = keccak256( + abi.encodePacked( + "\x19Ethereum Signed Message:\n32", + keccak256(abi.encode(_account, _nonceAndLockupPeriod, _packedPoolRewardValues)) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, hash); + return abi.encodePacked(r, s, v); + } +} diff --git a/test/foundry/PackedValue.t.sol b/test/foundry/PackedValue.t.sol new file mode 100644 index 0000000..fee7a10 --- /dev/null +++ b/test/foundry/PackedValue.t.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.21; + +import "forge-std/Test.sol"; +import "../../contracts/types/PackedValue.sol"; + +contract PackedValueTest is Test { + function setUp() public {} + + function test_pack_1_value(uint16 value) public { + PackedValue packed = PackedValue.wrap(0); + packed = packed.packUint16(value, 0); + + assertEq(packed.unpackUint16(0), value); + assertEq(PackedValue.unwrap(packed), uint256(value)); + } + + function test_pack_2_value(uint16 value, uint24 value2) public { + PackedValue packed = PackedValue.wrap(0); + packed = packed.packUint16(value, 0); + packed = packed.packUint24(value2, 16); + + assertEq(packed.unpackUint16(0), value); + assertEq(packed.unpackUint24(16), value2); + assertEq(PackedValue.unwrap(packed), uint256(value) | (uint256(value2) << 16)); + } + + function test_pack_2_value(uint16 value, uint32 value2) public { + PackedValue packed = PackedValue.wrap(0); + packed = packed.packUint16(value, 0); + packed = packed.packUint32(value2, 16); + + assertEq(packed.unpackUint16(0), value); + assertEq(packed.unpackUint32(16), value2); + assertEq(PackedValue.unwrap(packed), uint256(value) | (uint256(value2) << 16)); + } + + function test_pack_2_value(uint32 value, uint32 value2) public { + PackedValue packed = PackedValue.wrap(0); + packed = packed.packUint32(value, 0); + packed = packed.packUint32(value2, 32); + + assertEq(packed.unpackUint32(0), value); + assertEq(packed.unpackUint32(32), value2); + assertEq(PackedValue.unwrap(packed), uint256(value) | (uint256(value2) << 32)); + } + + function test_pack_2_value(uint24 value, uint232 value2) public { + PackedValue packed = PackedValue.wrap(0); + packed = packed.packUint24(value, 0); + packed = packed.packUint232(value2, 24); + + assertEq(packed.unpackUint24(0), value); + assertEq(packed.unpackUint232(24), value2); + assertEq(PackedValue.unwrap(packed), uint256(value) | (uint256(value2) << 24)); + } + + function test_pack_3_value(uint24 value, uint32 value2, uint16 value3) public { + PackedValue packed = PackedValue.wrap(0); + packed = packed.packUint24(value, 0); + packed = packed.packUint32(value2, 24); + packed = packed.packUint16(value3, 56); + + assertEq(packed.unpackUint24(0), value); + assertEq(packed.unpackUint32(24), value2); + assertEq(packed.unpackUint16(56), value3); + assertEq(PackedValue.unwrap(packed), uint256(value) | (uint256(value2) << 24) | (uint256(value3) << 56)); + } + + function test_pack_3_value(uint24 value, uint16 value2, uint216 value3) public { + PackedValue packed = PackedValue.wrap(0); + packed = packed.packUint24(value, 0); + packed = packed.packUint16(value2, 24); + packed = packed.packUint216(value3, 40); + + assertEq(packed.unpackUint24(0), value); + assertEq(packed.unpackUint16(24), value2); + assertEq(packed.unpackUint216(40), value3); + assertEq(PackedValue.unwrap(packed), uint256(value) | (uint256(value2) << 24) | (uint256(value3) << 40)); + } + + function test_pack_4_value(uint24 value, uint16 value2, uint16 value3, uint200 value4) public { + PackedValue packed = PackedValue.wrap(0); + packed = packed.packUint24(value, 0); + packed = packed.packUint16(value2, 24); + packed = packed.packUint16(value3, 40); + packed = packed.packUint200(value4, 56); + + assertEq(packed.unpackUint24(0), value); + assertEq(packed.unpackUint16(24), value2); + assertEq(packed.unpackUint16(40), value3); + assertEq(packed.unpackUint200(56), value4); + } +} diff --git a/test/foundry/PositionFarmRewardDistributor.t.sol b/test/foundry/PositionFarmRewardDistributor.t.sol index 3cafdb1..09ebc6e 100644 --- a/test/foundry/PositionFarmRewardDistributor.t.sol +++ b/test/foundry/PositionFarmRewardDistributor.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.21; import "./Token.sol"; import "forge-std/Test.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; -import "../../contracts/plugins/PositionFarmRewardDistributor.sol"; +import "../../contracts/farming/PositionFarmRewardDistributor.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract PositionFarmRewardDistributorTest is Test {