diff --git a/README.md b/README.md index b63893e..84a97b0 100644 --- a/README.md +++ b/README.md @@ -113,22 +113,28 @@ contract AMMPoolToken is ERC20Plugins { Storage access: -- [1 storage slot](https://github.com/1inch/farming/blob/master/contracts/accounting/FarmAccounting.sol#L14-L16) for farming params, updated only on farming restarting: +- [1 storage slot](https://github.com/1inch/farming/blob/master/contracts/accounting/Farming.sol#L9-L11) for farming params, updated when farming is restarted or stopped: ```solidity uint40 public finished; uint32 public duration; uint184 public reward; ``` + +- [1 storage slot](https://github.com/1inch/farming/blob/master/contracts/accounting/Farming.sol#L9-L12) for total amount of rewards that haven't been claimed yet, updated when farming is restarted, stopped or when the user claims rewards: + + ```solidity + uint256 balance; + ``` -- [1 storage slot](https://github.com/1inch/farming/blob/master/contracts/accounting/UserAccounting.sol#L7-L8) for farming state, updated only on changing number of farming tokens: +- [1 storage slot](https://github.com/1inch/farming/blob/master/contracts/accounting/Rewards.sol#L9-L10) for farming state, updated only on changing number of farming tokens: ```solidity uint40 public checkpoint; uint216 public farmedPerTokenStored; ``` -- [1 storage slot](https://github.com/1inch/farming/blob/master/contracts/accounting/UserAccounting.sol#L9) per each farmer, updated on deposits/withdrawals (kudos to [@snjax](https://github.com/snjax)): +- [1 storage slot](https://github.com/1inch/farming/blob/master/contracts/accounting/Rewards.sol#L11) per each farmer, updated on deposits/withdrawals (kudos to [@snjax](https://github.com/snjax)): ```solidity mapping(address => int256) public corrections; diff --git a/contracts/Distributor.sol b/contracts/Distributor.sol new file mode 100644 index 0000000..d2ba71d --- /dev/null +++ b/contracts/Distributor.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; + +import { IDistributor } from "./interfaces/IDistributor.sol"; + +abstract contract Distributor is IDistributor, Ownable { + address internal _distributor; + + event DistributorChanged(address oldDistributor, address newDistributor); + + error NotDistributor(); + error SameDistributor(); + error ZeroDistributorAddress(); + + modifier onlyDistributor { + if (msg.sender != _distributor) revert NotDistributor(); + _; + } + + function setDistributor(address distributor_) public virtual onlyOwner { + if (distributor_ == address(0)) revert ZeroDistributorAddress(); + address oldDistributor = _distributor; + if (distributor_ == oldDistributor) revert SameDistributor(); + emit DistributorChanged(oldDistributor, distributor_); + _distributor = distributor_; + } + + function distributor() public view virtual returns (address) { + return _distributor; + } +} diff --git a/contracts/FarmingLib.sol b/contracts/FarmingLib.sol index 16ecc6b..e352a13 100644 --- a/contracts/FarmingLib.sol +++ b/contracts/FarmingLib.sol @@ -2,20 +2,20 @@ pragma solidity ^0.8.0; -import { FarmAccounting } from "./accounting/FarmAccounting.sol"; -import { UserAccounting } from "./accounting/UserAccounting.sol"; +import { Farming } from "./accounting/Farming.sol"; +import { Rewards } from "./accounting/Rewards.sol"; /// @title FarmingLib -/// @dev A library for farming logic, using FarmAccounting and UserAccounting. +/// @dev A library for farming logic, using Farming and Rewards. library FarmingLib { - using FarmAccounting for FarmAccounting.Info; - using UserAccounting for UserAccounting.Info; + using Farming for Farming.Info; + using Rewards for Rewards.Info; using FarmingLib for FarmingLib.Info; - /// @dev Struct containing farm and user detailed info for farming operations. See {FarmAccounting.Info} and {UserAccounting.Info} for. + /// @dev Struct containing farming and rewards detailed info for farming operations. See {Farming.Info} and {Rewards.Info}. struct Data { - FarmAccounting.Info farmInfo; - UserAccounting.Info userInfo; + Farming.Info farmingInfo; + Rewards.Info rewardsInfo; } /// @dev Struct containing the total supply function and a data slot for EVM storage. @@ -25,7 +25,7 @@ library FarmingLib { } /** - * @notice Creates a new Info struct. + * @notice Creates a new FarmingLib.Info struct. * @param getTotalSupply The function to get the total supply. * @param data The data struct for storage. * @return info The created Info struct. @@ -40,8 +40,8 @@ library FarmingLib { } /** - * @notice Retrieves the Data struct from an Info struct. - * @param self The Info struct. + * @notice Retrieves the FarmingLib.Data struct from an FarmingLib.Info struct. + * @param self The Info struct to retrieve data from storage. * @return data The retrieved Data struct. */ function getData(Info memory self) internal pure returns(Data storage data) { @@ -52,16 +52,16 @@ library FarmingLib { } /** - * @notice Begins farming for a specified period. - * @param self The Info struct. - * @param amount The amount to farm. - * @param period The farming period. - * @return reward The farming reward. + * @notice Updates farming info with new amount and specified period. + * @param self The FarmingLib.Info struct to retrieve data from storage. + * @param amount A new amount to farm. + * @param period A new farming period. + * @return reward Updated farming reward. */ - function startFarming(Info memory self, uint256 amount, uint256 period) internal returns(uint256 reward) { + function updateFarmData(Info memory self, uint256 amount, uint256 period) internal returns(uint256 reward) { Data storage data = self.getData(); - data.userInfo.updateFarmedPerToken(_farmedPerToken(self)); - reward = data.farmInfo.startFarming(amount, period); + data.rewardsInfo.updateFarmedPerToken(_farmedPerToken(self)); + reward = data.farmingInfo.update(amount, period); } /** @@ -69,67 +69,56 @@ library FarmingLib { * @param self The FarmingLib.Info struct to retrieve data from storage. * @return leftover Amount of reward tokens remaining after farming. */ - function stopFarming(Info memory self) internal returns(uint256 leftover) { + function cancelFarming(Info memory self) internal returns(uint256 leftover) { Data storage data = self.getData(); - data.userInfo.updateFarmedPerToken(_farmedPerToken(self)); - leftover = data.farmInfo.stopFarming(); - } - - /** - * @notice Gets the farmed amount for an account. - * @param self The Info struct. - * @param account The account to check. - * @param balance The account balance. - * @return result The farmed amount. - */ - function farmed(Info memory self, address account, uint256 balance) internal view returns(uint256) { - return self.getData().userInfo.farmed(account, balance, _farmedPerToken(self)); + data.rewardsInfo.updateFarmedPerToken(_farmedPerToken(self)); + leftover = data.farmingInfo.cancel(); } /** - * @notice Claims the farmed amount for an account. - * @param self The Info struct. - * @param account The account to claim for. - * @param balance The account balance. - * @return amount The claimed amount. + * @notice Claims the farmed reward tokens for an account. + * @param self The FarmingLib.Info struct to retrieve data from storage. + * @param account The address of the account to claim for. + * @param balance The account balance of farmable tokens. + * @return amount The claimed amount of reward tokens. */ function claim(Info memory self, address account, uint256 balance) internal returns(uint256 amount) { Data storage data = self.getData(); uint256 fpt = _farmedPerToken(self); - amount = data.userInfo.farmed(account, balance, fpt); + amount = data.rewardsInfo.farmed(account, balance, fpt); if (amount > 0) { - data.userInfo.eraseFarmed(account, balance, fpt); - data.farmInfo.claim(amount); + data.rewardsInfo.eraseFarmed(account, balance, fpt); + data.farmingInfo.claim(amount); } } /** - * @notice Updates the balances of two accounts. - * @param self The Info struct. - * @param from The account to transfer from. - * @param to The account to transfer to. + * @notice Updates the farmable token balances of two accounts. + * @param self The FarmingLib.Info struct to retrieve data from storage. + * @param from The address of the account to transfer from. + * @param to The address of the account to transfer to. * @param amount The amount to transfer. */ function updateBalances(Info memory self, address from, address to, uint256 amount) internal { - self.getData().userInfo.updateBalances(from, to, amount, _farmedPerToken(self)); + self.getData().rewardsInfo.updateBalances(from, to, amount, _farmedPerToken(self)); } - function _farmedPerToken(Info memory self) private view returns (uint256) { - return self.getData().userInfo.farmedPerToken(_infoToContext(self), _lazyGetSupply, _lazyGetFarmed); - } - - // UserAccounting bindings - - function _lazyGetSupply(bytes32 context) private view returns(uint256) { - Info memory self = _contextToInfo(context); - return self.getTotalSupply(); + /** + * @notice Gets the amount of farmed reward tokens for an account. + * @param self The FarmingLib.Info struct to retrieve data from storage. + * @param account The address of the account to check. + * @param balance The farmable token balance of the account. + * @return result The number of tokens farmed. + */ + function farmed(Info memory self, address account, uint256 balance) internal view returns(uint256) { + return self.getData().rewardsInfo.farmed(account, balance, _farmedPerToken(self)); } - function _lazyGetFarmed(bytes32 context, uint256 checkpoint) private view returns(uint256) { - Info memory self = _contextToInfo(context); - return self.getData().farmInfo.farmedSinceCheckpointScaled(checkpoint); + function _farmedPerToken(Info memory self) private view returns (uint256) { + return self.getData().rewardsInfo.farmedPerToken(_infoToContext(self), _lazyGetSupply, _lazyGetFarmed); } + // --- Rewards bindings section start --- function _contextToInfo(bytes32 context) private pure returns(Info memory self) { assembly ("memory-safe") { // solhint-disable-line no-inline-assembly self := context @@ -141,4 +130,15 @@ library FarmingLib { context := self } } + + function _lazyGetSupply(bytes32 context) private view returns(uint256) { + Info memory self = _contextToInfo(context); + return self.getTotalSupply(); + } + + function _lazyGetFarmed(bytes32 context, uint256 checkpoint) private view returns(uint256) { + Info memory self = _contextToInfo(context); + return self.getData().farmingInfo.farmedSinceCheckpointScaled(checkpoint); + } + // --- Rewards bindings section end --- } diff --git a/contracts/FarmingPlugin.sol b/contracts/FarmingPlugin.sol index fc3cbe9..8e9f8f5 100644 --- a/contracts/FarmingPlugin.sol +++ b/contracts/FarmingPlugin.sol @@ -3,37 +3,36 @@ pragma solidity ^0.8.0; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; -import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; + import { SafeERC20 } from "@1inch/solidity-utils/contracts/libraries/SafeERC20.sol"; -import { Plugin } from "@1inch/token-plugins/contracts/Plugin.sol"; import { IERC20Plugins } from "@1inch/token-plugins/contracts/interfaces/IERC20Plugins.sol"; +import { Plugin } from "@1inch/token-plugins/contracts/Plugin.sol"; +import { Distributor } from "./Distributor.sol"; +import { FarmingLib, Farming } from "./FarmingLib.sol"; import { IFarmingPlugin } from "./interfaces/IFarmingPlugin.sol"; -import { FarmingLib, FarmAccounting } from "./FarmingLib.sol"; -contract FarmingPlugin is Plugin, IFarmingPlugin, Ownable { - using SafeERC20 for IERC20; - using FarmingLib for FarmingLib.Info; - using FarmAccounting for FarmAccounting.Info; +/** + * @title Plugin for farming reward tokens. + * @notice This contract only accounts for the balances of users + * who added it as a plugin to the farmable token. + */ +contract FarmingPlugin is Plugin, IFarmingPlugin, Distributor { using Address for address payable; - - error ZeroFarmableTokenAddress(); - error ZeroRewardsTokenAddress(); - error ZeroDistributorAddress(); - error SameDistributor(); - error InsufficientFunds(); + using Farming for Farming.Info; + using FarmingLib for FarmingLib.Info; + using SafeERC20 for IERC20; IERC20 public immutable rewardsToken; - address private _distributor; - uint256 private _totalSupply; FarmingLib.Data private _farm; - - modifier onlyDistributor { - if (msg.sender != _distributor) revert AccessDenied(); - _; - } + uint256 private _totalSupply; + + error InsufficientFunds(); + error ZeroFarmableTokenAddress(); + error ZeroRewardsTokenAddress(); constructor(IERC20Plugins farmableToken_, IERC20 rewardsToken_) Plugin(farmableToken_) @@ -44,45 +43,29 @@ contract FarmingPlugin is Plugin, IFarmingPlugin, Ownable { emit FarmCreated(address(farmableToken_), address(rewardsToken_)); } - function farmInfo() public view returns(FarmAccounting.Info memory) { - return _farm.farmInfo; - } - - function totalSupply() public view returns(uint256) { - return _totalSupply; - } - - function distributor() public view returns(address) { - return _distributor; - } - - function setDistributor(address distributor_) public virtual onlyOwner { - if (distributor_ == address(0)) revert ZeroDistributorAddress(); - address oldDistributor = _distributor; - if (distributor_ == oldDistributor) revert SameDistributor(); - emit DistributorChanged(oldDistributor, distributor_); - _distributor = distributor_; - } - + /** + * @notice See {IFarmingPlugin-startFarming} + */ function startFarming(uint256 amount, uint256 period) public virtual onlyDistributor { - uint256 reward = _makeInfo().startFarming(amount, period); + uint256 reward = _makeInfo().updateFarmData(amount, period); emit RewardUpdated(reward, period); rewardsToken.safeTransferFrom(msg.sender, address(this), amount); } + /** + * @notice See {IFarmingPlugin-stopFarming} + */ function stopFarming() public virtual onlyDistributor { - uint256 leftover = _makeInfo().stopFarming(); + uint256 leftover = _makeInfo().cancelFarming(); emit RewardUpdated(0, 0); if (leftover > 0) { rewardsToken.safeTransfer(msg.sender, leftover); } } - function farmed(address account) public view virtual returns(uint256) { - uint256 balance = IERC20Plugins(token).pluginBalanceOf(address(this), account); - return _makeInfo().farmed(account, balance); - } - + /** + * @notice See {IFarmingPlugin-claim} + */ function claim() public virtual { uint256 pluginBalance = IERC20Plugins(token).pluginBalanceOf(address(this), msg.sender); uint256 amount = _makeInfo().claim(msg.sender, pluginBalance); @@ -91,6 +74,42 @@ contract FarmingPlugin is Plugin, IFarmingPlugin, Ownable { } } + /** + * @notice See {IFarmingPlugin-rescueFunds} + */ + function rescueFunds(IERC20 token_, uint256 amount) public virtual onlyDistributor { + if(token_ == IERC20(address(0))) { + payable(_distributor).sendValue(amount); + } else { + if (token_ == rewardsToken) { + if (rewardsToken.balanceOf(address(this)) < _farm.farmingInfo.balance + amount) revert InsufficientFunds(); + } + token_.safeTransfer(_distributor, amount); + } + } + + /** + * @notice See {IFarmingPlugin-farmInfo} + */ + function farmInfo() public view returns(Farming.Info memory) { + return _farm.farmingInfo; + } + + /** + * @notice See {IFarmingPlugin-totalSupply} + */ + function totalSupply() public view returns(uint256) { + return _totalSupply; + } + + /** + * @notice See {IFarmingPlugin-farmed} + */ + function farmed(address account) public view virtual returns(uint256) { + uint256 balance = IERC20Plugins(token).pluginBalanceOf(address(this), account); + return _makeInfo().farmed(account, balance); + } + function _transferReward(IERC20 reward, address to, uint256 amount) internal virtual { reward.safeTransfer(to, amount); } @@ -105,17 +124,6 @@ contract FarmingPlugin is Plugin, IFarmingPlugin, Ownable { } } - function rescueFunds(IERC20 token_, uint256 amount) public virtual onlyDistributor { - if(token_ == IERC20(address(0))) { - payable(_distributor).sendValue(amount); - } else { - if (token_ == rewardsToken) { - if (rewardsToken.balanceOf(address(this)) < _farm.farmInfo.balance + amount) revert InsufficientFunds(); - } - token_.safeTransfer(_distributor, amount); - } - } - function _makeInfo() private view returns(FarmingLib.Info memory) { return FarmingLib.makeInfo(totalSupply, _farm); } diff --git a/contracts/FarmingPool.sol b/contracts/FarmingPool.sol index 23299d5..e864767 100644 --- a/contracts/FarmingPool.sol +++ b/contracts/FarmingPool.sol @@ -2,42 +2,39 @@ pragma solidity ^0.8.0; -import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { IERC20, ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import { Address } from "@openzeppelin/contracts/utils/Address.sol"; + import { SafeERC20 } from "@1inch/solidity-utils/contracts/libraries/SafeERC20.sol"; +import { Distributor } from "./Distributor.sol"; +import { Farming, FarmingLib } from "./FarmingLib.sol"; import { IFarmingPool } from "./interfaces/IFarmingPool.sol"; -import { FarmAccounting, FarmingLib } from "./FarmingLib.sol"; -contract FarmingPool is IFarmingPool, Ownable, ERC20 { - using SafeERC20 for IERC20; +/** + * @title Contract for farming reward tokens, required for farming tokens that don't support plugins. + * @notice This contract accounts for the balance of the farmable token's deposits through + * its own balance as it is inherited from ERC20. + */ +contract FarmingPool is IFarmingPool, Distributor, ERC20 { using Address for address payable; using FarmingLib for FarmingLib.Info; - - error SameStakingAndRewardsTokens(); - error ZeroStakingTokenAddress(); - error ZeroRewardsTokenAddress(); - error ZeroDistributorAddress(); - error SameDistributor(); - error AccessDenied(); - error InsufficientFunds(); - error MaxBalanceExceeded(); + using SafeERC20 for IERC20; uint256 internal constant _MAX_BALANCE = 1e32; - IERC20 public immutable stakingToken; IERC20 public immutable rewardsToken; + IERC20 public immutable stakingToken; - address private _distributor; FarmingLib.Data private _farm; - modifier onlyDistributor { - if (msg.sender != _distributor) revert AccessDenied(); - _; - } - + error InsufficientFunds(); + error MaxBalanceExceeded(); + error SameStakingAndRewardsTokens(); + error ZeroStakingTokenAddress(); + error ZeroRewardsTokenAddress(); + constructor(IERC20Metadata stakingToken_, IERC20 rewardsToken_) ERC20( string(abi.encodePacked("Farming of ", stakingToken_.name())), @@ -51,55 +48,46 @@ contract FarmingPool is IFarmingPool, Ownable, ERC20 { rewardsToken = rewardsToken_; } - function decimals() public view virtual override returns (uint8) { - return IERC20Metadata(address(stakingToken)).decimals(); - } - - function farmInfo() public view returns(FarmAccounting.Info memory) { - return _farm.farmInfo; - } - - function distributor() public view virtual returns (address) { - return _distributor; - } - - function setDistributor(address distributor_) public virtual onlyOwner { - if (distributor_ == address(0)) revert ZeroDistributorAddress(); - address oldDistributor = _distributor; - if (distributor_ == oldDistributor) revert SameDistributor(); - emit DistributorChanged(oldDistributor, distributor_); - _distributor = distributor_; - } - + /** + * @notice See {IFarmingPool-startFarming} + */ function startFarming(uint256 amount, uint256 period) public virtual onlyDistributor { - uint256 reward = _makeInfo().startFarming(amount, period); + uint256 reward = _makeInfo().updateFarmData(amount, period); emit RewardUpdated(reward, period); rewardsToken.safeTransferFrom(msg.sender, address(this), amount); } + /** + * @notice See {IFarmingPool-stopFarming} + */ function stopFarming() public virtual onlyDistributor { - uint256 leftover = _makeInfo().stopFarming(); + uint256 leftover = _makeInfo().cancelFarming(); emit RewardUpdated(0, 0); if (leftover > 0) { rewardsToken.safeTransfer(msg.sender, leftover); } } - function farmed(address account) public view virtual returns (uint256) { - return _makeInfo().farmed(account, balanceOf(account)); - } - + /** + * @notice See {IFarmingPool-deposit} + */ function deposit(uint256 amount) public virtual { _mint(msg.sender, amount); if (balanceOf(msg.sender) > _MAX_BALANCE) revert MaxBalanceExceeded(); stakingToken.safeTransferFrom(msg.sender, address(this), amount); } + /** + * @notice See {IFarmingPool-withdraw} + */ function withdraw(uint256 amount) public virtual { _burn(msg.sender, amount); stakingToken.safeTransfer(msg.sender, amount); } + /** + * @notice See {IFarmingPool-claim} + */ function claim() public virtual { uint256 amount = _makeInfo().claim(msg.sender, balanceOf(msg.sender)); if (amount > 0) { @@ -107,15 +95,17 @@ contract FarmingPool is IFarmingPool, Ownable, ERC20 { } } - function _transferReward(IERC20 reward, address to, uint256 amount) internal virtual { - reward.safeTransfer(to, amount); - } - + /** + * @notice See {IFarmingPool-exit} + */ function exit() public virtual { withdraw(balanceOf(msg.sender)); claim(); } + /** + * @notice See {IFarmingPool-rescueFunds} + */ function rescueFunds(IERC20 token, uint256 amount) public virtual onlyDistributor { if (token == IERC20(address(0))) { payable(_distributor).sendValue(amount); @@ -123,19 +113,39 @@ contract FarmingPool is IFarmingPool, Ownable, ERC20 { if (token == stakingToken) { if (stakingToken.balanceOf(address(this)) < totalSupply() + amount) revert InsufficientFunds(); } else if (token == rewardsToken) { - if (rewardsToken.balanceOf(address(this)) < _farm.farmInfo.balance + amount) revert InsufficientFunds(); + if (rewardsToken.balanceOf(address(this)) < _farm.farmingInfo.balance + amount) revert InsufficientFunds(); } token.safeTransfer(_distributor, amount); } } - function _makeInfo() private view returns(FarmingLib.Info memory) { - return FarmingLib.makeInfo(totalSupply, _farm); + /** + * @notice See {IERC20Metadata-decimals} + */ + function decimals() public view virtual override returns (uint8) { + return IERC20Metadata(address(stakingToken)).decimals(); } - // ERC20 overrides + /** + * @notice See {IFarmingPool-farmInfo} + */ + function farmInfo() public view returns(Farming.Info memory) { + return _farm.farmingInfo; + } + /** + * @notice See {IFarmingPool-farmed} + */ + function farmed(address account) public view virtual returns (uint256) { + return _makeInfo().farmed(account, balanceOf(account)); + } + + function _transferReward(IERC20 reward, address to, uint256 amount) internal virtual { + reward.safeTransfer(to, amount); + } + + // --- ERC20 overrides section start --- function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual override { super._beforeTokenTransfer(from, to, amount); @@ -143,4 +153,9 @@ contract FarmingPool is IFarmingPool, Ownable, ERC20 { _makeInfo().updateBalances(from, to, amount); } } + // --- ERC20 overrides section end --- + + function _makeInfo() private view returns(FarmingLib.Info memory) { + return FarmingLib.makeInfo(totalSupply, _farm); + } } diff --git a/contracts/MultiFarmingPlugin.sol b/contracts/MultiFarmingPlugin.sol index bbcab6f..9ec61f1 100644 --- a/contracts/MultiFarmingPlugin.sol +++ b/contracts/MultiFarmingPlugin.sol @@ -2,45 +2,44 @@ pragma solidity ^0.8.0; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { Address } from "@openzeppelin/contracts/utils/Address.sol"; -import { Plugin } from "@1inch/token-plugins/contracts/Plugin.sol"; -import { SafeERC20 } from "@1inch/solidity-utils/contracts/libraries/SafeERC20.sol"; + import { AddressArray, AddressSet } from "@1inch/solidity-utils/contracts/libraries/AddressSet.sol"; +import { SafeERC20 } from "@1inch/solidity-utils/contracts/libraries/SafeERC20.sol"; import { IERC20Plugins } from "@1inch/token-plugins/contracts/interfaces/IERC20Plugins.sol"; +import { Plugin } from "@1inch/token-plugins/contracts/Plugin.sol"; +import { Distributor } from "./Distributor.sol"; +import { Farming, FarmingLib } from "./FarmingLib.sol"; import { IMultiFarmingPlugin } from "./interfaces/IMultiFarmingPlugin.sol"; -import { FarmAccounting, FarmingLib } from "./FarmingLib.sol"; -contract MultiFarmingPlugin is Plugin, IMultiFarmingPlugin, Ownable { - using SafeERC20 for IERC20; - using FarmingLib for FarmingLib.Info; +/** + * @title Plugin for farming multiple reward tokens. + * @notice This contract only accounts for the balances of users + * who added it as a plugin to the farmable token. + */ +contract MultiFarmingPlugin is Plugin, IMultiFarmingPlugin, Distributor { using Address for address payable; - using AddressSet for AddressSet.Data; using AddressArray for AddressArray.Data; - - error ZeroFarmableTokenAddress(); - error ZeroRewardsTokenAddress(); - error ZeroDistributorAddress(); - error SameDistributor(); - error RewardsTokenAlreadyAdded(); - error RewardsTokensLimitTooHigh(uint256); - error RewardsTokensLimitReached(); - error RewardsTokenNotFound(); - error InsufficientFunds(); - + using AddressSet for AddressSet.Data; + using FarmingLib for FarmingLib.Info; + using SafeERC20 for IERC20; + uint256 public immutable rewardsTokensLimit; - address private _distributor; - uint256 private _totalSupply; mapping(IERC20 => FarmingLib.Data) private _farms; AddressSet.Data private _rewardsTokens; + uint256 private _totalSupply; - modifier onlyDistributor { - if (msg.sender != _distributor) revert AccessDenied(); - _; - } + error InsufficientFunds(); + error RewardsTokenAlreadyAdded(); + error RewardsTokenNotFound(); + error RewardsTokensLimitTooHigh(uint256); + error RewardsTokensLimitReached(); + error ZeroFarmableTokenAddress(); + error ZeroRewardsTokenAddress(); constructor(IERC20Plugins farmableToken_, uint256 rewardsTokensLimit_) Plugin(farmableToken_) { if (rewardsTokensLimit_ > 5) revert RewardsTokensLimitTooHigh(rewardsTokensLimit_); @@ -49,30 +48,16 @@ contract MultiFarmingPlugin is Plugin, IMultiFarmingPlugin, Ownable { rewardsTokensLimit = rewardsTokensLimit_; } + /** + * @notice See {IMultiFarmingPlugin-rewardsTokens} + */ function rewardsTokens() external view returns(address[] memory) { return _rewardsTokens.items.get(); } - function farmInfo(IERC20 rewardsToken) public view returns(FarmAccounting.Info memory) { - return _farms[rewardsToken].farmInfo; - } - - function totalSupply() public view returns(uint256) { - return _totalSupply; - } - - function distributor() public view returns(address) { - return _distributor; - } - - function setDistributor(address distributor_) public virtual onlyOwner { - if (distributor_ == address(0)) revert ZeroDistributorAddress(); - address oldDistributor = _distributor; - if (distributor_ == oldDistributor) revert SameDistributor(); - emit DistributorChanged(oldDistributor, distributor_); - _distributor = distributor_; - } - + /** + * @notice See {IMultiFarmingPlugin-addRewardsToken} + */ function addRewardsToken(address rewardsToken) public virtual onlyOwner { if (rewardsToken == address(0)) revert ZeroRewardsTokenAddress(); if (_rewardsTokens.length() == rewardsTokensLimit) revert RewardsTokensLimitReached(); @@ -80,34 +65,41 @@ contract MultiFarmingPlugin is Plugin, IMultiFarmingPlugin, Ownable { emit FarmCreated(address(token), rewardsToken); } + /** + * @notice See {IMultiFarmingPlugin-startFarming} + */ function startFarming(IERC20 rewardsToken, uint256 amount, uint256 period) public virtual onlyDistributor { if (!_rewardsTokens.contains(address(rewardsToken))) revert RewardsTokenNotFound(); - uint256 reward = _makeInfo(rewardsToken).startFarming(amount, period); + uint256 reward = _makeInfo(rewardsToken).updateFarmData(amount, period); emit RewardUpdated(address(rewardsToken), reward, period); rewardsToken.safeTransferFrom(msg.sender, address(this), amount); } + /** + * @notice See {IMultiFarmingPlugin-stopFarming} + */ function stopFarming(IERC20 rewardsToken) public virtual onlyDistributor { if (!_rewardsTokens.contains(address(rewardsToken))) revert RewardsTokenNotFound(); - uint256 leftover = _makeInfo(rewardsToken).stopFarming(); + uint256 leftover = _makeInfo(rewardsToken).cancelFarming(); emit RewardUpdated(address(rewardsToken), 0, 0); if (leftover > 0) { rewardsToken.safeTransfer(msg.sender, leftover); } } - function farmed(IERC20 rewardsToken, address account) public view virtual returns(uint256) { - uint256 balance = IERC20Plugins(token).pluginBalanceOf(address(this), account); - return _makeInfo(rewardsToken).farmed(account, balance); - } - + /** + * @notice See {IMultiFarmingPlugin-claim} + */ function claim(IERC20 rewardsToken) public virtual { uint256 pluginBalance = IERC20Plugins(token).pluginBalanceOf(address(this), msg.sender); _claim(rewardsToken, msg.sender, pluginBalance); } + /** + * @notice See {IMultiFarmingPlugin-claim} + */ function claim() public virtual { uint256 pluginBalance = IERC20Plugins(token).pluginBalanceOf(address(this), msg.sender); address[] memory tokens = _rewardsTokens.items.get(); @@ -119,13 +111,42 @@ contract MultiFarmingPlugin is Plugin, IMultiFarmingPlugin, Ownable { } } - function _claim(IERC20 rewardsToken, address account, uint256 pluginBalance) private { - uint256 amount = _makeInfo(rewardsToken).claim(account, pluginBalance); - if (amount > 0) { - _transferReward(rewardsToken, account, amount); + /** + * @notice See {IMultiFarmingPlugin-rescueFunds} + */ + function rescueFunds(IERC20 token_, uint256 amount) public virtual onlyDistributor { + if(token_ == IERC20(address(0))) { + payable(_distributor).sendValue(amount); + } else { + if (_rewardsTokens.contains(address(token_))) { + if (token_.balanceOf(address(this)) < _farms[token_].farmingInfo.balance + amount) revert InsufficientFunds(); + } + token_.safeTransfer(_distributor, amount); } } + /** + * @notice See {IMultiFarmingPlugin-farmInfo} + */ + function farmInfo(IERC20 rewardsToken) public view returns(Farming.Info memory) { + return _farms[rewardsToken].farmingInfo; + } + + /** + * @notice See {IMultiFarmingPlugin-totalSupply} + */ + function totalSupply() public view returns(uint256) { + return _totalSupply; + } + + /** + * @notice See {IMultiFarmingPlugin-farmed} + */ + function farmed(IERC20 rewardsToken, address account) public view virtual returns(uint256) { + uint256 balance = IERC20Plugins(token).pluginBalanceOf(address(this), account); + return _makeInfo(rewardsToken).farmed(account, balance); + } + function _transferReward(IERC20 reward, address to, uint256 amount) internal virtual { reward.safeTransfer(to, amount); } @@ -146,14 +167,10 @@ contract MultiFarmingPlugin is Plugin, IMultiFarmingPlugin, Ownable { } } - function rescueFunds(IERC20 token_, uint256 amount) public virtual onlyDistributor { - if(token_ == IERC20(address(0))) { - payable(_distributor).sendValue(amount); - } else { - if (_rewardsTokens.contains(address(token_))) { - if (token_.balanceOf(address(this)) < _farms[token_].farmInfo.balance + amount) revert InsufficientFunds(); - } - token_.safeTransfer(_distributor, amount); + function _claim(IERC20 rewardsToken, address account, uint256 pluginBalance) private { + uint256 amount = _makeInfo(rewardsToken).claim(account, pluginBalance); + if (amount > 0) { + _transferReward(rewardsToken, account, amount); } } diff --git a/contracts/accounting/FarmAccounting.sol b/contracts/accounting/Farming.sol similarity index 91% rename from contracts/accounting/FarmAccounting.sol rename to contracts/accounting/Farming.sol index 78e2a6e..026cbd7 100644 --- a/contracts/accounting/FarmAccounting.sol +++ b/contracts/accounting/Farming.sol @@ -4,11 +4,7 @@ pragma solidity ^0.8.0; import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; -library FarmAccounting { - error ZeroDuration(); - error DurationTooLarge(); - error AmountTooLarge(); - +library Farming { struct Info { uint40 finished; uint32 duration; @@ -19,19 +15,11 @@ library FarmAccounting { uint256 internal constant _MAX_REWARD_AMOUNT = 1e32; // 108 bits uint256 internal constant _SCALE = 1e18; // 60 bits - /// @dev Requires extra 18 decimals for precision, result fits in 168 bits - function farmedSinceCheckpointScaled(Info storage info, uint256 checkpoint) internal view returns(uint256 amount) { - unchecked { - (uint40 finished, uint32 duration, uint184 reward) = (info.finished, info.duration, info.reward); - if (duration > 0) { - uint256 elapsed = Math.min(block.timestamp, finished) - Math.min(checkpoint, finished); - // size of (type(uint32).max * _MAX_REWARD_AMOUNT * _SCALE) is less than 200 bits, so there is no overflow - return elapsed * reward * _SCALE / duration; - } - } - } + error ZeroDuration(); + error DurationTooLarge(); + error AmountTooLarge(); - function startFarming(Info storage info, uint256 amount, uint256 period) internal returns(uint256) { + function update(Info storage info, uint256 amount, uint256 period) internal returns(uint256) { if (period == 0) revert ZeroDuration(); if (period > type(uint32).max) revert DurationTooLarge(); @@ -52,7 +40,7 @@ library FarmAccounting { return amount; } - function stopFarming(Info storage info) internal returns(uint256 leftover) { + function cancel(Info storage info) internal returns(uint256 leftover) { leftover = info.reward - farmedSinceCheckpointScaled(info, info.finished - info.duration) / _SCALE; (info.finished, info.duration, info.reward, info.balance) = ( uint40(block.timestamp), @@ -65,4 +53,16 @@ library FarmAccounting { function claim(Info storage info, uint256 amount) internal { info.balance -= amount; } + + /// @dev Requires extra 18 decimals for precision, result fits in 168 bits + function farmedSinceCheckpointScaled(Info storage info, uint256 checkpoint) internal view returns(uint256 amount) { + unchecked { + (uint40 finished, uint32 duration, uint184 reward) = (info.finished, info.duration, info.reward); + if (duration > 0) { + uint256 elapsed = Math.min(block.timestamp, finished) - Math.min(checkpoint, finished); + // size of (type(uint32).max * _MAX_REWARD_AMOUNT * _SCALE) is less than 200 bits, so there is no overflow + return elapsed * reward * _SCALE / duration; + } + } + } } diff --git a/contracts/accounting/UserAccounting.sol b/contracts/accounting/Rewards.sol similarity index 94% rename from contracts/accounting/UserAccounting.sol rename to contracts/accounting/Rewards.sol index 735596a..627ccf4 100644 --- a/contracts/accounting/UserAccounting.sol +++ b/contracts/accounting/Rewards.sol @@ -2,37 +2,15 @@ pragma solidity ^0.8.0; -import { FarmAccounting } from "./FarmAccounting.sol"; +import { Farming } from "./Farming.sol"; -library UserAccounting { +library Rewards { struct Info { uint40 checkpoint; uint216 farmedPerTokenStored; mapping(address => int256) corrections; } - function farmedPerToken( - Info storage info, - bytes32 context, - function(bytes32) internal view returns(uint256) lazyGetSupply, - function(bytes32, uint256) internal view returns(uint256) lazyGetFarmed - ) internal view returns(uint256) { - (uint256 checkpoint, uint256 fpt) = (info.checkpoint, info.farmedPerTokenStored); - if (block.timestamp != checkpoint) { - uint256 supply = lazyGetSupply(context); - if (supply > 0) { - // fpt increases by 168 bit / supply - unchecked { fpt += lazyGetFarmed(context, checkpoint) / supply; } - } - } - return fpt; - } - - function farmed(Info storage info, address account, uint256 balance, uint256 fpt) internal view returns(uint256) { - // balance * fpt is less than 168 bit - return uint256(int256(balance * fpt) - info.corrections[account]) / FarmAccounting._SCALE; - } - function eraseFarmed(Info storage info, address account, uint256 balance, uint256 fpt) internal { // balance * fpt is less than 168 bit info.corrections[account] = int256(balance * fpt); @@ -60,4 +38,26 @@ library UserAccounting { } } } + + function farmedPerToken( + Info storage info, + bytes32 context, + function(bytes32) internal view returns(uint256) lazyGetSupply, + function(bytes32, uint256) internal view returns(uint256) lazyGetFarmed + ) internal view returns(uint256) { + (uint256 checkpoint, uint256 fpt) = (info.checkpoint, info.farmedPerTokenStored); + if (block.timestamp != checkpoint) { + uint256 supply = lazyGetSupply(context); + if (supply > 0) { + // fpt increases by 168 bit / supply + unchecked { fpt += lazyGetFarmed(context, checkpoint) / supply; } + } + } + return fpt; + } + + function farmed(Info storage info, address account, uint256 balance, uint256 fpt) internal view returns(uint256) { + // balance * fpt is less than 168 bit + return uint256(int256(balance * fpt) - info.corrections[account]) / Farming._SCALE; + } } diff --git a/contracts/interfaces/IDistributor.sol b/contracts/interfaces/IDistributor.sol new file mode 100644 index 0000000..9fa825c --- /dev/null +++ b/contracts/interfaces/IDistributor.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +interface IDistributor { + function setDistributor(address distributor_) external; + + function distributor() external view returns(address); +} diff --git a/contracts/interfaces/IFarmingPlugin.sol b/contracts/interfaces/IFarmingPlugin.sol index a02ba45..624560d 100644 --- a/contracts/interfaces/IFarmingPlugin.sol +++ b/contracts/interfaces/IFarmingPlugin.sol @@ -4,26 +4,50 @@ pragma solidity ^0.8.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IPlugin } from "@1inch/token-plugins/contracts/interfaces/IPlugin.sol"; -import { FarmAccounting } from "../accounting/FarmAccounting.sol"; +import { Farming } from "../accounting/Farming.sol"; interface IFarmingPlugin is IPlugin { + // Emitted in constructor when the plugin is set up. event FarmCreated(address token, address reward); - event DistributorChanged(address oldDistributor, address newDistributor); + // Emitted when farming parameters are updated. event RewardUpdated(uint256 reward, uint256 duration); - // View functions - function totalSupply() external view returns(uint256); - function distributor() external view returns(address); - function farmInfo() external view returns(FarmAccounting.Info memory); - function farmed(address account) external view returns(uint256); - // User functions + /** + * @notice Claims the farmed reward tokens for the caller. + */ function claim() external; - // Owner functions - function setDistributor(address distributor_) external; - // Distributor functions + /** + * @notice Begins farming for the specified period. + * @param amount The amount to farm. + * @param period The farming period. + */ function startFarming(uint256 amount, uint256 period) external; - function rescueFunds(IERC20 token, uint256 amount) external; + /** + * @notice Stops farming immediately and refunds unspent rewards. + */ + function stopFarming() external; + /** + * @notice Retrieves tokens that accidentally appeared on the contract. + * @param token_ The address of the token to be rescued. + * @param amount The number of tokens to rescue. + */ + function rescueFunds(IERC20 token_, uint256 amount) external; + + // View functions + /** + * Returns the number of farmable tokens counted by this plugin. + */ + function totalSupply() external view returns(uint256); + /** + * @notice Gets information about the current farm. + */ + function farmInfo() external view returns(Farming.Info memory); + /** + * @notice Gets the amount of farmed reward tokens for the account. + * @param account The address of the account to check. + */ + function farmed(address account) external view returns(uint256); } diff --git a/contracts/interfaces/IFarmingPool.sol b/contracts/interfaces/IFarmingPool.sol index 180649c..174fcd6 100644 --- a/contracts/interfaces/IFarmingPool.sol +++ b/contracts/interfaces/IFarmingPool.sol @@ -3,27 +3,59 @@ pragma solidity ^0.8.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { FarmAccounting } from "../accounting/FarmAccounting.sol"; +import { Farming } from "../accounting/Farming.sol"; interface IFarmingPool is IERC20 { - event DistributorChanged(address oldDistributor, address newDistributor); + // Emitted when farming parameters are updated. event RewardUpdated(uint256 reward, uint256 duration); - // View functions - function distributor() external view returns(address); - function farmInfo() external view returns(FarmAccounting.Info memory); - function farmed(address account) external view returns(uint256); - // User functions + /** + * @notice Stakes the farmable tokens and mints its own tokens in return. + * @param amount The amount of tokens to stake. + */ function deposit(uint256 amount) external; + /** + * @notice Burns the contract tokens and returns the farmable tokens. + * @param amount The amount of tokens to withdraw. + */ function withdraw(uint256 amount) external; + /** + * @notice Claims the farmed reward tokens for the caller. + */ function claim() external; + /** + * @notice Claims the farmed reward tokens for the caller + * and withdraws the staked tokens. + */ function exit() external; - // Owner functions - function setDistributor(address distributor_) external; - // Distributor functions + /** + * @notice Begins farming for the specified period. + * @param amount The amount to farm. + * @param period The farming period. + */ function startFarming(uint256 amount, uint256 period) external; + /** + * @notice Stops farming immediately and refunds unspent rewards. + */ + function stopFarming() external; + /** + * @notice Retrieves tokens that accidentally appeared on the contract. + * @param token The address of the token to be rescued. + * @param amount The number of tokens to rescue. + */ function rescueFunds(IERC20 token, uint256 amount) external; + + // View functions + /** + * @notice Gets information about the current farm. + */ + function farmInfo() external view returns(Farming.Info memory); + /** + * @notice Gets the amount of farmed reward tokens for the account. + * @param account The address of the account to check. + */ + function farmed(address account) external view returns(uint256); } diff --git a/contracts/interfaces/IMultiFarmingPlugin.sol b/contracts/interfaces/IMultiFarmingPlugin.sol index b718b75..b19c768 100644 --- a/contracts/interfaces/IMultiFarmingPlugin.sol +++ b/contracts/interfaces/IMultiFarmingPlugin.sol @@ -4,27 +4,63 @@ pragma solidity ^0.8.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IPlugin } from "@1inch/token-plugins/contracts/interfaces/IPlugin.sol"; -import { FarmAccounting } from "../accounting/FarmAccounting.sol"; +import { Farming } from "../accounting/Farming.sol"; interface IMultiFarmingPlugin is IPlugin { + // Emitted when a new reward token is added. event FarmCreated(address token, address reward); - event DistributorChanged(address oldDistributor, address newDistributor); + // Emitted when farming parameters are updated. event RewardUpdated(address token, uint256 reward, uint256 duration); - // View functions - function totalSupply() external view returns(uint256); - function distributor() external view returns(address); - function farmInfo(IERC20 rewardsToken) external view returns(FarmAccounting.Info memory); - function farmed(IERC20 rewardsToken, address account) external view returns(uint256); - // User functions + /** + * @notice Claims the selected farmed reward tokens for the caller. + * @param rewardsToken The address of the reward token. + */ function claim(IERC20 rewardsToken) external; + /** + * @notice Claims for the caller all farmed reward tokens supported by the plugin. + */ function claim() external; - // Owner functions - function setDistributor(address distributor_) external; - // Distributor functions + /** + * @notice Begins farming for the selected reward token for the specified period. + * @param rewardsToken The address of the reward token. + * @param amount The amount to farm. + * @param period The farming period. + */ function startFarming(IERC20 rewardsToken, uint256 amount, uint256 period) external; - function rescueFunds(IERC20 token, uint256 amount) external; + /** + * @notice Stops farming for the selected reward token immediately and refunds unspent rewards. + * @param rewardsToken The address of the reward token. + */ + function stopFarming(IERC20 rewardsToken) external; + /** + * @notice Retrieves tokens that accidentally appeared on the contract. + * @param token_ The address of the token to be rescued. + * @param amount The number of tokens to rescue. + */ + function rescueFunds(IERC20 token_, uint256 amount) external; + + // View functions + /** + * Returns the number of farmable tokens counted by this plugin. + */ + function totalSupply() external view returns(uint256); + /** + * @notice Gets information about the current farm for the selected reward token. + * @param rewardsToken The address of the reward token. + */ + function farmInfo(IERC20 rewardsToken) external view returns(Farming.Info memory); + /** + * @notice Gets the amount of selected reward tokens farmed for the account. + * @param rewardsToken The address of the reward token. + * @param account The address of the account to check. + */ + function farmed(IERC20 rewardsToken, address account) external view returns(uint256); + /** + * Gets all reward tokens that are supported by this plugin. + */ + function rewardsTokens() external view returns(address[] memory); } diff --git a/test/FarmingPlugin.js b/test/FarmingPlugin.js index 9606ea0..a6b2dd6 100644 --- a/test/FarmingPlugin.js +++ b/test/FarmingPlugin.js @@ -62,7 +62,7 @@ describe('FarmingPlugin', function () { const { farm } = await loadFixture(initContracts); await expect( farm.connect(wallet2).startFarming(1000, 60 * 60 * 24), - ).to.be.revertedWithCustomError(farm, 'AccessDenied'); + ).to.be.revertedWithCustomError(farm, 'NotDistributor'); }); /* @@ -243,7 +243,7 @@ describe('FarmingPlugin', function () { expect(wallet2).to.not.equal(distributor); await expect( farm.connect(wallet2).stopFarming(), - ).to.be.revertedWithCustomError(farm, 'AccessDenied'); + ).to.be.revertedWithCustomError(farm, 'NotDistributor'); }); /* @@ -371,7 +371,7 @@ describe('FarmingPlugin', function () { expect(wallet2).to.not.equal(distributor); await expect( farm.connect(wallet2).rescueFunds(gift, '1000'), - ).to.be.revertedWithCustomError(farm, 'AccessDenied'); + ).to.be.revertedWithCustomError(farm, 'NotDistributor'); }); /* diff --git a/test/FarmingPool.js b/test/FarmingPool.js index 064bdef..051718d 100644 --- a/test/FarmingPool.js +++ b/test/FarmingPool.js @@ -52,7 +52,7 @@ describe('FarmingPool', function () { const { farm } = await loadFixture(initContracts); await expect( farm.connect(wallet2).startFarming(1000, 60 * 60 * 24), - ).to.be.revertedWithCustomError(farm, 'AccessDenied'); + ).to.be.revertedWithCustomError(farm, 'NotDistributor'); }); }); @@ -515,7 +515,7 @@ describe('FarmingPool', function () { expect(wallet2.address).to.not.equal(distributor); await expect( farm.connect(wallet2).stopFarming(), - ).to.be.revertedWithCustomError(farm, 'AccessDenied'); + ).to.be.revertedWithCustomError(farm, 'NotDistributor'); }); it('should transfer tokens from farm to wallet', async function () { @@ -582,7 +582,7 @@ describe('FarmingPool', function () { expect(wallet2.address).to.not.equal(distributor); await expect( farm.connect(wallet2).rescueFunds(gift, '1000'), - ).to.be.revertedWithCustomError(farm, 'AccessDenied'); + ).to.be.revertedWithCustomError(farm, 'NotDistributor'); }); it('should thrown with not enough balance for staking token', async function () { diff --git a/test/MultiFarmingPlugin.js b/test/MultiFarmingPlugin.js index 77d188e..0c0761a 100644 --- a/test/MultiFarmingPlugin.js +++ b/test/MultiFarmingPlugin.js @@ -104,7 +104,7 @@ describe('MultiFarmingPlugin', function () { expect(wallet2).to.not.equal(distributor); await expect( multiFarm.connect(wallet2).stopFarming(gift), - ).to.be.revertedWithCustomError(multiFarm, 'AccessDenied'); + ).to.be.revertedWithCustomError(multiFarm, 'NotDistributor'); }); it('should be reverted because of an invalid reward token', async function () { @@ -168,7 +168,7 @@ describe('MultiFarmingPlugin', function () { expect(wallet2).to.not.equal(distributor); await expect( multiFarm.connect(wallet2).rescueFunds(gift, '1000'), - ).to.be.revertedWithCustomError(multiFarm, 'AccessDenied'); + ).to.be.revertedWithCustomError(multiFarm, 'NotDistributor'); }); it('should thrown with insufficient funds', async function () {