diff --git a/contracts/base/Staking20Base.sol b/contracts/base/Staking20Base.sol new file mode 100644 index 000000000..6661e411a --- /dev/null +++ b/contracts/base/Staking20Base.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../extension/ContractMetadata.sol"; +import "../extension/Multicall.sol"; +import "../extension/Ownable.sol"; +import "../extension/Staking20.sol"; + +import "../eip/interface/IERC20.sol"; + +/** + * note: This is a Beta release. + * + * EXTENSION: Staking20 + * + * The `Staking20Base` smart contract implements Token staking mechanism. + * Allows users to stake their ERC-20 Tokens and earn rewards in form of another ERC-20 tokens. + * + * Following features and implementation setup must be noted: + * + * - ERC-20 Tokens from only one contract can be staked. + * + * - Contract admin can choose to give out rewards by either transferring or minting the rewardToken, + * which is ideally a different ERC20 token. See {_mintRewards}. + * + * - To implement custom logic for staking, reward calculation, etc. corresponding functions can be + * overridden from the extension `Staking20`. + * + * - Ownership of the contract, with the ability to restrict certain functions to + * only be called by the contract's owner. + * + * - Multicall capability to perform multiple actions atomically. + * + */ +contract Staking20Base is ContractMetadata, Multicall, Ownable, Staking20 { + /// @dev ERC20 Reward Token address. See {_mintRewards} below. + address public rewardToken; + + constructor( + uint256 _timeUnit, + uint256 _rewardRatioNumerator, + uint256 _rewardRatioDenominator, + address _stakingToken, + address _rewardToken + ) Staking20(_stakingToken) { + _setupOwner(msg.sender); + _setTimeUnit(_timeUnit); + _setRewardRatio(_rewardRatioNumerator, _rewardRatioDenominator); + + require(_rewardToken != _stakingToken, "Reward Token and Staking Token can't be same."); + rewardToken = _rewardToken; + } + + /*////////////////////////////////////////////////////////////// + Minting logic + //////////////////////////////////////////////////////////////*/ + + /** + * @dev Mint ERC20 rewards to the staker. Must override. + * + * @param _staker Address for which to calculated rewards. + * @param _rewards Amount of tokens to be given out as reward. + * + */ + function _mintRewards(address _staker, uint256 _rewards) internal override { + // Mint or transfer reward-tokens here. + // e.g. + // + // IERC20(rewardToken).transfer(_staker, _rewards); + // + // OR + // + // Use a mintable ERC20, such as thirdweb's `TokenERC20.sol` + // + // TokenERC20(rewardToken).mintTo(_staker, _rewards); + // note: The staking contract should have minter role to mint tokens. + } + + /*////////////////////////////////////////////////////////////// + Other Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns whether staking restrictions can be set in given execution context. + function _canSetStakeConditions() internal view override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether owner can be set in the given execution context. + function _canSetOwner() internal view virtual override returns (bool) { + return msg.sender == owner(); + } +} diff --git a/contracts/extension/Staking20.sol b/contracts/extension/Staking20.sol new file mode 100644 index 000000000..b1ad45759 --- /dev/null +++ b/contracts/extension/Staking20.sol @@ -0,0 +1,281 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../openzeppelin-presets/security/ReentrancyGuard.sol"; +import "../eip/interface/IERC20.sol"; +import "../lib/CurrencyTransferLib.sol"; + +import "./interface/IStaking20.sol"; + +/** + * note: This is a Beta release. + */ + +abstract contract Staking20 is ReentrancyGuard, IStaking20 { + /*/////////////////////////////////////////////////////////////// + State variables / Mappings + //////////////////////////////////////////////////////////////*/ + + ///@dev Address of ERC20 contract -- staked tokens belong to this contract. + address public token; + + /// @dev Unit of time specified in number of seconds. Can be set as 1 seconds, 1 days, 1 hours, etc. + uint256 public timeUnit; + + ///@dev Rewards ratio is the number of reward tokens for a number of staked tokens, per unit of time. + uint256 public rewardRatioNumerator; + + ///@dev Rewards ratio is the number of reward tokens for a number of staked tokens, per unit of time. + uint256 public rewardRatioDenominator; + + ///@dev Mapping staker address to Staker struct. See {struct IStaking20.Staker}. + mapping(address => Staker) public stakers; + + /// @dev List of accounts that have staked that token-id. + address[] public stakersArray; + + constructor(address _token) ReentrancyGuard() { + require(address(_token) != address(0), "address 0"); + token = _token; + } + + /*/////////////////////////////////////////////////////////////// + External/Public Functions + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Stake ERC20 Tokens. + * + * @dev See {_stake}. Override that to implement custom logic. + * + * @param _amount Amount to stake. + */ + function stake(uint256 _amount) external nonReentrant { + _stake(_amount); + } + + /** + * @notice Withdraw staked ERC20 tokens. + * + * @dev See {_withdraw}. Override that to implement custom logic. + * + * @param _amount Amount to withdraw. + */ + function withdraw(uint256 _amount) external nonReentrant { + _withdraw(_amount); + } + + /** + * @notice Claim accumulated rewards. + * + * @dev See {_claimRewards}. Override that to implement custom logic. + * See {_calculateRewards} for reward-calculation logic. + */ + function claimRewards() external nonReentrant { + _claimRewards(); + } + + /** + * @notice Set time unit. Set as a number of seconds. + * Could be specified as -- x * 1 hours, x * 1 days, etc. + * + * @dev Only admin/authorized-account can call it. + * + * @param _timeUnit New time unit. + */ + function setTimeUnit(uint256 _timeUnit) external virtual { + if (!_canSetStakeConditions()) { + revert("Not authorized"); + } + + _updateUnclaimedRewardsForAll(); + + uint256 currentTimeUnit = timeUnit; + _setTimeUnit(_timeUnit); + + emit UpdatedTimeUnit(currentTimeUnit, _timeUnit); + } + + /** + * @notice Set rewards per unit of time. + * Interpreted as (numerator/denominator) rewards per second/per day/etc based on time-unit. + * + * For e.g., ratio of 1/20 would mean 1 reward token for every 20 tokens staked. + * + * @dev Only admin/authorized-account can call it. + * + * @param _numerator Reward ratio numerator. + * @param _denominator Reward ratio denominator. + */ + function setRewardRatio(uint256 _numerator, uint256 _denominator) external virtual { + if (!_canSetStakeConditions()) { + revert("Not authorized"); + } + + _updateUnclaimedRewardsForAll(); + + uint256 currentNumerator = rewardRatioNumerator; + uint256 currentDenominator = rewardRatioDenominator; + _setRewardRatio(_numerator, _denominator); + + emit UpdatedRewardRatio(currentNumerator, _numerator, currentDenominator, _denominator); + } + + /** + * @notice View amount staked and rewards for a user. + * + * @param _staker Address for which to calculated rewards. + * @return _tokensStaked Amount of tokens staked. + * @return _rewards Available reward amount. + */ + function getStakeInfo(address _staker) public view virtual returns (uint256 _tokensStaked, uint256 _rewards) { + _tokensStaked = stakers[_staker].amountStaked; + _rewards = _availableRewards(_staker); + } + + /*/////////////////////////////////////////////////////////////// + Internal Functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Staking logic. Override to add custom logic. + function _stake(uint256 _amount) internal virtual { + require(_amount != 0, "Staking 0 tokens"); + address _token = token; + + if (stakers[msg.sender].amountStaked > 0) { + _updateUnclaimedRewardsForStaker(msg.sender); + } else { + stakersArray.push(msg.sender); + stakers[msg.sender].timeOfLastUpdate = block.timestamp; + } + + CurrencyTransferLib.transferCurrency(_token, msg.sender, address(this), _amount); + + stakers[msg.sender].amountStaked += _amount; + + emit TokensStaked(msg.sender, _amount); + } + + /// @dev Withdraw logic. Override to add custom logic. + function _withdraw(uint256 _amount) internal virtual { + uint256 _amountStaked = stakers[msg.sender].amountStaked; + require(_amount != 0, "Withdrawing 0 tokens"); + require(_amountStaked >= _amount, "Withdrawing more than staked"); + + _updateUnclaimedRewardsForStaker(msg.sender); + + if (_amountStaked == _amount) { + address[] memory _stakersArray = stakersArray; + for (uint256 i = 0; i < _stakersArray.length; ++i) { + if (_stakersArray[i] == msg.sender) { + stakersArray[i] = stakersArray[_stakersArray.length - 1]; + stakersArray.pop(); + break; + } + } + } + stakers[msg.sender].amountStaked -= _amount; + + CurrencyTransferLib.transferCurrency(token, address(this), msg.sender, _amount); + + emit TokensWithdrawn(msg.sender, _amount); + } + + /// @dev Logic for claiming rewards. Override to add custom logic. + function _claimRewards() internal virtual { + uint256 rewards = stakers[msg.sender].unclaimedRewards + _calculateRewards(msg.sender); + + require(rewards != 0, "No rewards"); + + stakers[msg.sender].timeOfLastUpdate = block.timestamp; + stakers[msg.sender].unclaimedRewards = 0; + + _mintRewards(msg.sender, rewards); + + emit RewardsClaimed(msg.sender, rewards); + } + + /// @dev View available rewards for a user. + function _availableRewards(address _staker) internal view virtual returns (uint256 _rewards) { + if (stakers[_staker].amountStaked == 0) { + _rewards = stakers[_staker].unclaimedRewards; + } else { + _rewards = stakers[_staker].unclaimedRewards + _calculateRewards(_staker); + } + } + + /// @dev Update unclaimed rewards for all users. Called when setting timeUnit or rewardsPerUnitTime. + function _updateUnclaimedRewardsForAll() internal virtual { + address[] memory _stakers = stakersArray; + uint256 len = _stakers.length; + for (uint256 i = 0; i < len; ++i) { + address _staker = _stakers[i]; + + uint256 rewards = _calculateRewards(_staker); + stakers[_staker].unclaimedRewards += rewards; + stakers[_staker].timeOfLastUpdate = block.timestamp; + } + } + + /// @dev Update unclaimed rewards for a users. Called for every state change for a user. + function _updateUnclaimedRewardsForStaker(address _staker) internal virtual { + uint256 rewards = _calculateRewards(_staker); + stakers[_staker].unclaimedRewards += rewards; + stakers[_staker].timeOfLastUpdate = block.timestamp; + } + + /// @dev Set time unit in seconds. + function _setTimeUnit(uint256 _timeUnit) internal virtual { + timeUnit = _timeUnit; + } + + /// @dev Set reward ratio per unit time. + function _setRewardRatio(uint256 _numerator, uint256 _denominator) internal virtual { + require(_denominator != 0, "divide by 0"); + rewardRatioNumerator = _numerator; + rewardRatioDenominator = _denominator; + } + + /// @dev Reward calculation logic. Override to implement custom logic. + function _calculateRewards(address _staker) internal view virtual returns (uint256 _rewards) { + Staker memory staker = stakers[_staker]; + + _rewards = (((((block.timestamp - staker.timeOfLastUpdate) * staker.amountStaked) * rewardRatioNumerator) / + timeUnit) / rewardRatioDenominator); + } + + /** + * @dev Mint/Transfer ERC20 rewards to the staker. Must override. + * + * @param _staker Address for which to calculated rewards. + * @param _rewards Amount of tokens to be given out as reward. + * + * For example, override as below to mint ERC20 rewards: + * + * ``` + * function _mintRewards(address _staker, uint256 _rewards) internal override { + * + * TokenERC20(rewardTokenAddress).mintTo(_staker, _rewards); + * + * } + * ``` + */ + function _mintRewards(address _staker, uint256 _rewards) internal virtual; + + /** + * @dev Returns whether staking restrictions can be set in given execution context. + * Must override. + * + * + * For example, override as below to restrict access to admin: + * + * ``` + * function _canSetStakeConditions() internal override { + * + * return msg.sender == adminAddress; + * + * } + * ``` + */ + function _canSetStakeConditions() internal view virtual returns (bool); +} diff --git a/contracts/extension/Staking20Upgradeable.sol b/contracts/extension/Staking20Upgradeable.sol new file mode 100644 index 000000000..489bed518 --- /dev/null +++ b/contracts/extension/Staking20Upgradeable.sol @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "../eip/interface/IERC20.sol"; +import "../lib/CurrencyTransferLib.sol"; + +import "./interface/IStaking20.sol"; + +/** + * note: This is a Beta release. + */ + +abstract contract Staking20Upgradeable is ReentrancyGuardUpgradeable, IStaking20 { + /*/////////////////////////////////////////////////////////////// + State variables / Mappings + //////////////////////////////////////////////////////////////*/ + + ///@dev Address of ERC20 contract -- staked tokens belong to this contract. + address public token; + + /// @dev Unit of time specified in number of seconds. Can be set as 1 seconds, 1 days, 1 hours, etc. + uint256 public timeUnit; + + ///@dev Rewards ratio is the number of reward tokens for a number of staked tokens, per unit of time. + uint256 public rewardRatioNumerator; + + ///@dev Rewards ratio is the number of reward tokens for a number of staked tokens, per unit of time. + uint256 public rewardRatioDenominator; + + ///@dev Mapping staker address to Staker struct. See {struct IStaking20.Staker}. + mapping(address => Staker) public stakers; + + /// @dev List of accounts that have staked that token-id. + address[] public stakersArray; + + function __Staking20_init(address _token) internal onlyInitializing { + __ReentrancyGuard_init(); + + require(address(_token) != address(0), "token address 0"); + token = _token; + } + + /*/////////////////////////////////////////////////////////////// + External/Public Functions + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Stake ERC20 Tokens. + * + * @dev See {_stake}. Override that to implement custom logic. + * + * @param _amount Amount to stake. + */ + function stake(uint256 _amount) external nonReentrant { + _stake(_amount); + } + + /** + * @notice Withdraw staked ERC20 tokens. + * + * @dev See {_withdraw}. Override that to implement custom logic. + * + * @param _amount Amount to withdraw. + */ + function withdraw(uint256 _amount) external nonReentrant { + _withdraw(_amount); + } + + /** + * @notice Claim accumulated rewards. + * + * @dev See {_claimRewards}. Override that to implement custom logic. + * See {_calculateRewards} for reward-calculation logic. + */ + function claimRewards() external nonReentrant { + _claimRewards(); + } + + /** + * @notice Set time unit. Set as a number of seconds. + * Could be specified as -- x * 1 hours, x * 1 days, etc. + * + * @dev Only admin/authorized-account can call it. + * + * @param _timeUnit New time unit. + */ + function setTimeUnit(uint256 _timeUnit) external virtual { + if (!_canSetStakeConditions()) { + revert("Not authorized"); + } + + _updateUnclaimedRewardsForAll(); + + uint256 currentTimeUnit = timeUnit; + _setTimeUnit(_timeUnit); + + emit UpdatedTimeUnit(currentTimeUnit, _timeUnit); + } + + /** + * @notice Set rewards per unit of time. + * Interpreted as (numerator/denominator) rewards per second/per day/etc based on time-unit. + * + * For e.g., ratio of 1/20 would mean 1 reward token for every 20 tokens staked. + * + * @dev Only admin/authorized-account can call it. + * + * @param _numerator Reward ratio numerator. + * @param _denominator Reward ratio denominator. + */ + function setRewardRatio(uint256 _numerator, uint256 _denominator) external virtual { + if (!_canSetStakeConditions()) { + revert("Not authorized"); + } + + _updateUnclaimedRewardsForAll(); + + uint256 currentNumerator = rewardRatioNumerator; + uint256 currentDenominator = rewardRatioDenominator; + _setRewardRatio(_numerator, _denominator); + + emit UpdatedRewardRatio(currentNumerator, _numerator, currentDenominator, _denominator); + } + + /** + * @notice View amount staked and rewards for a user. + * + * @param _staker Address for which to calculated rewards. + * @return _tokensStaked Amount of tokens staked. + * @return _rewards Available reward amount. + */ + function getStakeInfo(address _staker) public view virtual returns (uint256 _tokensStaked, uint256 _rewards) { + _tokensStaked = stakers[_staker].amountStaked; + _rewards = _availableRewards(_staker); + } + + /*/////////////////////////////////////////////////////////////// + Internal Functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Staking logic. Override to add custom logic. + function _stake(uint256 _amount) internal virtual { + require(_amount != 0, "Staking 0 tokens"); + address _token = token; + + if (stakers[msg.sender].amountStaked > 0) { + _updateUnclaimedRewardsForStaker(msg.sender); + } else { + stakersArray.push(msg.sender); + stakers[msg.sender].timeOfLastUpdate = block.timestamp; + } + + CurrencyTransferLib.transferCurrency(_token, msg.sender, address(this), _amount); + + stakers[msg.sender].amountStaked += _amount; + + emit TokensStaked(msg.sender, _amount); + } + + /// @dev Withdraw logic. Override to add custom logic. + function _withdraw(uint256 _amount) internal virtual { + uint256 _amountStaked = stakers[msg.sender].amountStaked; + require(_amount != 0, "Withdrawing 0 tokens"); + require(_amountStaked >= _amount, "Withdrawing more than staked"); + + _updateUnclaimedRewardsForStaker(msg.sender); + + if (_amountStaked == _amount) { + address[] memory _stakersArray = stakersArray; + for (uint256 i = 0; i < _stakersArray.length; ++i) { + if (_stakersArray[i] == msg.sender) { + stakersArray[i] = stakersArray[_stakersArray.length - 1]; + stakersArray.pop(); + break; + } + } + } + stakers[msg.sender].amountStaked -= _amount; + + CurrencyTransferLib.transferCurrency(token, address(this), msg.sender, _amount); + + emit TokensWithdrawn(msg.sender, _amount); + } + + /// @dev Logic for claiming rewards. Override to add custom logic. + function _claimRewards() internal virtual { + uint256 rewards = stakers[msg.sender].unclaimedRewards + _calculateRewards(msg.sender); + + require(rewards != 0, "No rewards"); + + stakers[msg.sender].timeOfLastUpdate = block.timestamp; + stakers[msg.sender].unclaimedRewards = 0; + + _mintRewards(msg.sender, rewards); + + emit RewardsClaimed(msg.sender, rewards); + } + + /// @dev View available rewards for a user. + function _availableRewards(address _staker) internal view virtual returns (uint256 _rewards) { + if (stakers[_staker].amountStaked == 0) { + _rewards = stakers[_staker].unclaimedRewards; + } else { + _rewards = stakers[_staker].unclaimedRewards + _calculateRewards(_staker); + } + } + + /// @dev Update unclaimed rewards for all users. Called when setting timeUnit or rewardsPerUnitTime. + function _updateUnclaimedRewardsForAll() internal virtual { + address[] memory _stakers = stakersArray; + uint256 len = _stakers.length; + for (uint256 i = 0; i < len; ++i) { + address _staker = _stakers[i]; + + uint256 rewards = _calculateRewards(_staker); + stakers[_staker].unclaimedRewards += rewards; + stakers[_staker].timeOfLastUpdate = block.timestamp; + } + } + + /// @dev Update unclaimed rewards for a users. Called for every state change for a user. + function _updateUnclaimedRewardsForStaker(address _staker) internal virtual { + uint256 rewards = _calculateRewards(_staker); + stakers[_staker].unclaimedRewards += rewards; + stakers[_staker].timeOfLastUpdate = block.timestamp; + } + + /// @dev Set time unit in seconds. + function _setTimeUnit(uint256 _timeUnit) internal virtual { + timeUnit = _timeUnit; + } + + /// @dev Set reward ratio per unit time. + function _setRewardRatio(uint256 _numerator, uint256 _denominator) internal virtual { + require(_denominator != 0, "divide by 0"); + rewardRatioNumerator = _numerator; + rewardRatioDenominator = _denominator; + } + + /// @dev Reward calculation logic. Override to implement custom logic. + function _calculateRewards(address _staker) internal view virtual returns (uint256 _rewards) { + Staker memory staker = stakers[_staker]; + + _rewards = (((((block.timestamp - staker.timeOfLastUpdate) * staker.amountStaked) * rewardRatioNumerator) / + timeUnit) / rewardRatioDenominator); + } + + /** + * @dev Mint/Transfer ERC20 rewards to the staker. Must override. + * + * @param _staker Address for which to calculated rewards. + * @param _rewards Amount of tokens to be given out as reward. + * + * For example, override as below to mint ERC20 rewards: + * + * ``` + * function _mintRewards(address _staker, uint256 _rewards) internal override { + * + * TokenERC20(rewardTokenAddress).mintTo(_staker, _rewards); + * + * } + * ``` + */ + function _mintRewards(address _staker, uint256 _rewards) internal virtual; + + /** + * @dev Returns whether staking restrictions can be set in given execution context. + * Must override. + * + * + * For example, override as below to restrict access to admin: + * + * ``` + * function _canSetStakeConditions() internal override { + * + * return msg.sender == adminAddress; + * + * } + * ``` + */ + function _canSetStakeConditions() internal view virtual returns (bool); +} diff --git a/contracts/extension/Staking721.sol b/contracts/extension/Staking721.sol index d90a661f1..ed886a427 100644 --- a/contracts/extension/Staking721.sol +++ b/contracts/extension/Staking721.sol @@ -4,13 +4,13 @@ pragma solidity ^0.8.11; import "../openzeppelin-presets/security/ReentrancyGuard.sol"; import "../eip/interface/IERC721.sol"; -import "./interface/IStaking.sol"; +import "./interface/IStaking721.sol"; /** * note: This is a Beta release. */ -abstract contract Staking721 is ReentrancyGuard, IStaking { +abstract contract Staking721 is ReentrancyGuard, IStaking721 { /*/////////////////////////////////////////////////////////////// State variables / Mappings //////////////////////////////////////////////////////////////*/ diff --git a/contracts/extension/Staking721Upgradeable.sol b/contracts/extension/Staking721Upgradeable.sol index 2f7cb2adf..c23878a80 100644 --- a/contracts/extension/Staking721Upgradeable.sol +++ b/contracts/extension/Staking721Upgradeable.sol @@ -4,13 +4,13 @@ pragma solidity ^0.8.11; import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; -import "./interface/IStaking.sol"; +import "./interface/IStaking721.sol"; /** * note: This is a Beta release. */ -abstract contract Staking721Upgradeable is ReentrancyGuardUpgradeable, IStaking { +abstract contract Staking721Upgradeable is ReentrancyGuardUpgradeable, IStaking721 { /*/////////////////////////////////////////////////////////////// State variables / Mappings //////////////////////////////////////////////////////////////*/ diff --git a/contracts/extension/interface/IStaking20.sol b/contracts/extension/interface/IStaking20.sol new file mode 100644 index 000000000..ee6a4ff8e --- /dev/null +++ b/contracts/extension/interface/IStaking20.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +interface IStaking20 { + /// @dev Emitted when tokens are staked. + event TokensStaked(address indexed staker, uint256 amount); + + /// @dev Emitted when a tokens are withdrawn. + event TokensWithdrawn(address indexed staker, uint256 amount); + + /// @dev Emitted when a staker claims staking rewards. + event RewardsClaimed(address indexed staker, uint256 rewardAmount); + + /// @dev Emitted when contract admin updates timeUnit. + event UpdatedTimeUnit(uint256 oldTimeUnit, uint256 newTimeUnit); + + /// @dev Emitted when contract admin updates rewardsPerUnitTime. + event UpdatedRewardRatio( + uint256 oldNumerator, + uint256 newNumerator, + uint256 oldDenominator, + uint256 newDenominator + ); + + /// @dev Emitted when contract admin updates minimum staking amount. + event UpdatedMinStakeAmount(uint256 oldAmount, uint256 newAmount); + + /** + * @notice Staker Info. + * + * @param amountStaked Total number of tokens staked by the staker. + * + * @param timeOfLastUpdate Last reward-update timestamp. + * + * @param unclaimedRewards Rewards accumulated but not claimed by user yet. + */ + struct Staker { + uint256 amountStaked; + uint256 timeOfLastUpdate; + uint256 unclaimedRewards; + } + + /** + * @notice Stake ERC721 Tokens. + * + * @param amount Amount to stake. + */ + function stake(uint256 amount) external; + + /** + * @notice Withdraw staked tokens. + * + * @param amount Amount to withdraw. + */ + function withdraw(uint256 amount) external; + + /** + * @notice Claim accumulated rewards. + * + */ + function claimRewards() external; + + /** + * @notice View amount staked and total rewards for a user. + * + * @param staker Address for which to calculated rewards. + */ + function getStakeInfo(address staker) external view returns (uint256 _tokensStaked, uint256 _rewards); +} diff --git a/contracts/extension/interface/IStaking.sol b/contracts/extension/interface/IStaking721.sol similarity index 98% rename from contracts/extension/interface/IStaking.sol rename to contracts/extension/interface/IStaking721.sol index 94764ef7a..6b3de28ec 100644 --- a/contracts/extension/interface/IStaking.sol +++ b/contracts/extension/interface/IStaking721.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.11; -interface IStaking { +interface IStaking721 { /// @dev Emitted when a set of token-ids are staked. event TokensStaked(address indexed staker, uint256[] indexed tokenIds); diff --git a/contracts/staking/EditionStake.sol b/contracts/staking/EditionStake.sol index dd46e3396..43beb828f 100644 --- a/contracts/staking/EditionStake.sol +++ b/contracts/staking/EditionStake.sol @@ -5,17 +5,12 @@ pragma solidity ^0.8.11; import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155ReceiverUpgradeable.sol"; -// Signature utils -import "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol"; - // Meta transactions import "../openzeppelin-presets/metatx/ERC2771ContextUpgradeable.sol"; // Utils import "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; import "../lib/CurrencyTransferLib.sol"; -import "../lib/FeeType.sol"; // ========== Features ========== @@ -72,6 +67,13 @@ contract EditionStake is return uint8(VERSION); } + /// @dev Admin can withdraw excess reward tokens. + function withdrawRewardTokens(uint256 _amount) external { + require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Not authorized"); + + CurrencyTransferLib.transferCurrency(rewardToken, address(this), _msgSender(), _amount); + } + /*/////////////////////////////////////////////////////////////// ERC 165 / 721 logic //////////////////////////////////////////////////////////////*/ diff --git a/contracts/staking/NFTStake.sol b/contracts/staking/NFTStake.sol index 0a78b2500..5c18e3fc2 100644 --- a/contracts/staking/NFTStake.sol +++ b/contracts/staking/NFTStake.sol @@ -5,17 +5,12 @@ pragma solidity ^0.8.11; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721ReceiverUpgradeable.sol"; -// Signature utils -import "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol"; - // Meta transactions import "../openzeppelin-presets/metatx/ERC2771ContextUpgradeable.sol"; // Utils import "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; import "../lib/CurrencyTransferLib.sol"; -import "../lib/FeeType.sol"; // ========== Features ========== @@ -72,6 +67,13 @@ contract NFTStake is return uint8(VERSION); } + /// @dev Admin can withdraw excess reward tokens. + function withdrawRewardTokens(uint256 _amount) external { + require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Not authorized"); + + CurrencyTransferLib.transferCurrency(rewardToken, address(this), _msgSender(), _amount); + } + /*/////////////////////////////////////////////////////////////// ERC 165 / 721 logic //////////////////////////////////////////////////////////////*/ diff --git a/contracts/staking/TokenStake.sol b/contracts/staking/TokenStake.sol new file mode 100644 index 000000000..49fd07a11 --- /dev/null +++ b/contracts/staking/TokenStake.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +// Token +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +// Meta transactions +import "../openzeppelin-presets/metatx/ERC2771ContextUpgradeable.sol"; + +// Utils +import "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; +import { CurrencyTransferLib } from "../lib/CurrencyTransferLib.sol"; + +// ========== Features ========== + +import "../extension/ContractMetadata.sol"; +import "../extension/PermissionsEnumerable.sol"; +import { Staking20Upgradeable } from "../extension/Staking20Upgradeable.sol"; + +contract TokenStake is + Initializable, + ContractMetadata, + PermissionsEnumerable, + ERC2771ContextUpgradeable, + MulticallUpgradeable, + Staking20Upgradeable +{ + bytes32 private constant MODULE_TYPE = bytes32("TokenStake"); + uint256 private constant VERSION = 1; + + /// @dev ERC20 Reward Token address. See {_mintRewards} below. + address public rewardToken; + + constructor() initializer {} + + /// @dev Initiliazes the contract, like a constructor. + function initialize( + address _defaultAdmin, + string memory _contractURI, + address[] memory _trustedForwarders, + address _rewardToken, + address _stakingToken, + uint256 _timeUnit, + uint256 _rewardRatioNumerator, + uint256 _rewardRatioDenominator + ) external initializer { + __ReentrancyGuard_init(); + __ERC2771Context_init_unchained(_trustedForwarders); + + require(_rewardToken != _stakingToken, "Reward Token and Staking Token can't be same."); + rewardToken = _rewardToken; + __Staking20_init(_stakingToken); + _setTimeUnit(_timeUnit); + _setRewardRatio(_rewardRatioNumerator, _rewardRatioDenominator); + + _setupContractURI(_contractURI); + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + } + + /// @dev Returns the module type of the contract. + function contractType() external pure virtual returns (bytes32) { + return MODULE_TYPE; + } + + /// @dev Returns the version of the contract. + function contractVersion() external pure virtual returns (uint8) { + return uint8(VERSION); + } + + /// @dev Admin can withdraw excess reward tokens. + function withdrawRewardTokens(uint256 _amount) external { + require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Not authorized"); + + CurrencyTransferLib.transferCurrency(rewardToken, address(this), _msgSender(), _amount); + } + + /*/////////////////////////////////////////////////////////////// + Transfer Staking Rewards + //////////////////////////////////////////////////////////////*/ + + /// @dev Mint/Transfer ERC20 rewards to the staker. + function _mintRewards(address _staker, uint256 _rewards) internal override { + CurrencyTransferLib.transferCurrency(rewardToken, address(this), _staker, _rewards); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns whether staking related restrictions can be set in the given execution context. + function _canSetStakeConditions() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + function _msgSender() internal view virtual override returns (address sender) { + return ERC2771ContextUpgradeable._msgSender(); + } + + function _msgData() internal view virtual override returns (bytes calldata) { + return ERC2771ContextUpgradeable._msgData(); + } +} diff --git a/src/test/staking/TokenStake.t.sol b/src/test/staking/TokenStake.t.sol new file mode 100644 index 000000000..20441ef03 --- /dev/null +++ b/src/test/staking/TokenStake.t.sol @@ -0,0 +1,471 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { TokenStake } from "contracts/staking/TokenStake.sol"; + +// Test imports +import "contracts/lib/TWStrings.sol"; +import "../utils/BaseTest.sol"; + +contract TokenStakeTest is BaseTest { + TokenStake internal stakeContract; + + address internal stakerOne; + address internal stakerTwo; + + uint256 internal timeUnit; + uint256 internal rewardRatioNumerator; + uint256 internal rewardRatioDenominator; + + function setUp() public override { + super.setUp(); + + timeUnit = 60; + rewardRatioNumerator = 3; + rewardRatioDenominator = 50; + + stakerOne = address(0x345); + stakerTwo = address(0x567); + + erc20Aux.mint(stakerOne, 1000); // mint 1000 tokens to stakerOne + erc20Aux.mint(stakerTwo, 1000); // mint 1000 tokens to stakerTwo + + erc20.mint(deployer, 1000 ether); // mint reward tokens to contract admin + + stakeContract = TokenStake(getContract("TokenStake")); + + // set approvals + vm.prank(stakerOne); + erc20Aux.approve(address(stakeContract), type(uint256).max); + + vm.prank(stakerTwo); + erc20Aux.approve(address(stakeContract), type(uint256).max); + + vm.prank(deployer); + erc20.transfer(address(stakeContract), 100 ether); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: Stake + //////////////////////////////////////////////////////////////*/ + + function test_state_stake() public { + //================ first staker ====================== + vm.warp(1); + + // stake 400 tokens + vm.prank(stakerOne); + stakeContract.stake(400); + uint256 timeOfLastUpdate_one = block.timestamp; + + // check balances/ownership of staked tokens + assertEq(erc20Aux.balanceOf(address(stakeContract)), 400); + assertEq(erc20Aux.balanceOf(address(stakerOne)), 600); + + // check available rewards right after staking + (uint256 _amountStaked, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq(_amountStaked, 400); + assertEq(_availableRewards, 0); + + //=================== warp timestamp to calculate rewards + vm.roll(100); + vm.warp(1000); + + // check available rewards after warp + (, _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate_one) * 400) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + //================ second staker ====================== + vm.roll(200); + vm.warp(2000); + + // stake 20 tokens with token-id 0 + vm.prank(stakerTwo); + stakeContract.stake(200); + uint256 timeOfLastUpdate_two = block.timestamp; + + // check balances/ownership of staked tokens + assertEq(erc20Aux.balanceOf(address(stakeContract)), 200 + 400); // sum of staked tokens by both stakers + assertEq(erc20Aux.balanceOf(address(stakerTwo)), 800); + + // check available rewards right after staking + (_amountStaked, _availableRewards) = stakeContract.getStakeInfo(stakerTwo); + + assertEq(_amountStaked, 200); + assertEq(_availableRewards, 0); + + //=================== warp timestamp to calculate rewards + vm.roll(300); + vm.warp(3000); + + // check available rewards for stakerOne + (, _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate_one) * 400) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + // check available rewards for stakerTwo + (, _availableRewards) = stakeContract.getStakeInfo(stakerTwo); + + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate_two) * 200) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + } + + function test_revert_stake_stakingZeroTokens() public { + // stake 0 tokens + + vm.prank(stakerOne); + vm.expectRevert("Staking 0 tokens"); + stakeContract.stake(0); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: claimRewards + //////////////////////////////////////////////////////////////*/ + + function test_state_claimRewards() public { + //================ setup stakerOne ====================== + vm.warp(1); + + // stake 50 tokens with token-id 0 + vm.prank(stakerOne); + stakeContract.stake(400); + uint256 timeOfLastUpdate_one = block.timestamp; + + //=================== warp timestamp to claim rewards + vm.roll(100); + vm.warp(1000); + + vm.prank(stakerOne); + stakeContract.claimRewards(); + + // check reward balances + assertEq( + erc20.balanceOf(stakerOne), + (((((block.timestamp - timeOfLastUpdate_one) * 400) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + // check available rewards after claiming + (uint256 _amountStaked, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq(_amountStaked, 400); + assertEq(_availableRewards, 0); + + //================ setup stakerTwo ====================== + + // stake 20 tokens with token-id 1 + vm.prank(stakerTwo); + stakeContract.stake(200); + uint256 timeOfLastUpdate_two = block.timestamp; + + //=================== warp timestamp to claim rewards + vm.roll(200); + vm.warp(2000); + + vm.prank(stakerTwo); + stakeContract.claimRewards(); + + // check reward balances + assertEq( + erc20.balanceOf(stakerTwo), + (((((block.timestamp - timeOfLastUpdate_two) * 200) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + // check available rewards after claiming -- stakerTwo + (_amountStaked, _availableRewards) = stakeContract.getStakeInfo(stakerTwo); + assertEq(_amountStaked, 200); + assertEq(_availableRewards, 0); + + // check available rewards -- stakerOne + (_amountStaked, _availableRewards) = stakeContract.getStakeInfo(stakerOne); + assertEq(_amountStaked, 400); + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate_two) * 400) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + } + + function test_revert_claimRewards_noRewards() public { + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake(400); + + //=================== try to claim rewards in same block + + vm.prank(stakerOne); + vm.expectRevert("No rewards"); + stakeContract.claimRewards(); + + //======= withdraw tokens and claim rewards + vm.roll(100); + vm.warp(1000); + + vm.prank(stakerOne); + stakeContract.withdraw(400); + vm.prank(stakerOne); + stakeContract.claimRewards(); + + //===== try to claim rewards again + vm.roll(200); + vm.warp(2000); + vm.prank(stakerOne); + vm.expectRevert("No rewards"); + stakeContract.claimRewards(); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: stake conditions + //////////////////////////////////////////////////////////////*/ + + function test_state_setRewardRatio() public { + // set value and check + vm.prank(deployer); + stakeContract.setRewardRatio(3, 70); + assertEq(3, stakeContract.rewardRatioNumerator()); + assertEq(70, stakeContract.rewardRatioDenominator()); + + //================ stake tokens + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake(400); + uint256 timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and again set rewardsPerUnitTime + vm.roll(100); + vm.warp(1000); + + vm.prank(deployer); + stakeContract.setRewardRatio(3, 80); + assertEq(3, stakeContract.rewardRatioNumerator()); + assertEq(80, stakeContract.rewardRatioDenominator()); + uint256 newTimeOfLastUpdate = block.timestamp; + + // check available rewards -- should use previous value for rewardsPerUnitTime for calculation + (, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq(_availableRewards, (((((block.timestamp - timeOfLastUpdate) * 400) * 3) / timeUnit) / 70)); + + //====== check rewards after some time + vm.roll(300); + vm.warp(3000); + + (, uint256 _newRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _newRewards, + _availableRewards + (((((block.timestamp - newTimeOfLastUpdate) * 400) * 3) / timeUnit) / 80) + ); + } + + function test_state_setTimeUnit() public { + // set value and check + uint256 timeUnit = 100; + vm.prank(deployer); + stakeContract.setTimeUnit(timeUnit); + assertEq(timeUnit, stakeContract.timeUnit()); + + //================ stake tokens + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake(400); + uint256 timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and again set timeUnit + vm.roll(100); + vm.warp(1000); + + vm.prank(deployer); + stakeContract.setTimeUnit(200); + assertEq(200, stakeContract.timeUnit()); + uint256 newTimeOfLastUpdate = block.timestamp; + + // check available rewards -- should use previous value for timeUnit for calculation + (, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate) * 400) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + //====== check rewards after some time + vm.roll(300); + vm.warp(3000); + + (, uint256 _newRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _newRewards, + _availableRewards + + (((((block.timestamp - newTimeOfLastUpdate) * 400) * rewardRatioNumerator) / 200) / + rewardRatioDenominator) + ); + } + + function test_revert_setRewardRatio_notAuthorized() public { + vm.expectRevert("Not authorized"); + stakeContract.setRewardRatio(1, 2); + } + + function test_revert_setRewardRatio_divideByZero() public { + vm.prank(deployer); + vm.expectRevert("divide by 0"); + stakeContract.setRewardRatio(1, 0); + } + + function test_revert_setTimeUnit_notAuthorized() public { + vm.expectRevert("Not authorized"); + stakeContract.setTimeUnit(1); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: withdraw + //////////////////////////////////////////////////////////////*/ + + function test_state_withdraw() public { + //================ stake different tokens ====================== + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake(400); + + vm.prank(stakerTwo); + stakeContract.stake(200); + + uint256 timeOfLastUpdate = block.timestamp; + + //========== warp timestamp before withdraw + vm.roll(100); + vm.warp(1000); + + // withdraw partially for stakerOne + vm.prank(stakerOne); + stakeContract.withdraw(100); + uint256 timeOfLastUpdateLatest = block.timestamp; + + // check balances/ownership after withdraw + assertEq(erc20Aux.balanceOf(stakerOne), 700); + assertEq(erc20Aux.balanceOf(stakerTwo), 800); + assertEq(erc20Aux.balanceOf(address(stakeContract)), (400 - 100) + 200); + + // check available rewards after withdraw + (, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate) * 400) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + (, _availableRewards) = stakeContract.getStakeInfo(stakerTwo); + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate) * 200) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + // check available rewards some time after withdraw + vm.roll(200); + vm.warp(2000); + + // check rewards for stakerOne + (, _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + ((((((timeOfLastUpdateLatest - timeOfLastUpdate) * 400)) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + + ((((((block.timestamp - timeOfLastUpdateLatest) * 300)) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + // withdraw partially for stakerTwo + vm.prank(stakerTwo); + stakeContract.withdraw(100); + timeOfLastUpdateLatest = block.timestamp; + + // check balances/ownership after withdraw + assertEq(erc20Aux.balanceOf(stakerOne), 700); + assertEq(erc20Aux.balanceOf(stakerTwo), 900); + assertEq(erc20Aux.balanceOf(address(stakeContract)), (400 - 100) + (200 - 100)); + + // check rewards for stakerTwo + (, _availableRewards) = stakeContract.getStakeInfo(stakerTwo); + + assertEq( + _availableRewards, + ((((((block.timestamp - timeOfLastUpdate) * 200)) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + // check rewards for stakerTwo after some time + vm.roll(300); + vm.warp(3000); + (, _availableRewards) = stakeContract.getStakeInfo(stakerTwo); + + assertEq( + _availableRewards, + ((((((timeOfLastUpdateLatest - timeOfLastUpdate) * 200)) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + + ((((((block.timestamp - timeOfLastUpdateLatest) * 100)) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + } + + function test_revert_withdraw_withdrawingZeroTokens() public { + vm.expectRevert("Withdrawing 0 tokens"); + stakeContract.withdraw(0); + } + + function test_revert_withdraw_withdrawingMoreThanStaked() public { + // stake tokens + vm.prank(stakerOne); + stakeContract.stake(400); + + vm.prank(stakerTwo); + stakeContract.stake(200); + + // trying to withdraw more than staked + vm.roll(200); + vm.warp(2000); + + vm.prank(stakerOne); + vm.expectRevert("Withdrawing more than staked"); + stakeContract.withdraw(500); + + // withdraw partially + vm.prank(stakerOne); + stakeContract.withdraw(300); + + // trying to withdraw more than staked + vm.prank(stakerOne); + vm.expectRevert("Withdrawing more than staked"); + stakeContract.withdraw(500); + + // re-stake + vm.prank(stakerOne); + stakeContract.stake(300); + + // trying to withdraw more than staked + vm.prank(stakerOne); + vm.expectRevert("Withdrawing more than staked"); + stakeContract.withdraw(500); + } +} diff --git a/src/test/utils/BaseTest.sol b/src/test/utils/BaseTest.sol index d8eff28a1..96cf635a6 100644 --- a/src/test/utils/BaseTest.sol +++ b/src/test/utils/BaseTest.sol @@ -35,6 +35,7 @@ import "contracts/airdrop/AirdropERC1155.sol"; import "contracts/airdrop/AirdropERC1155Claimable.sol"; import { NFTStake } from "contracts/staking/NFTStake.sol"; import { EditionStake } from "contracts/staking/EditionStake.sol"; +import { TokenStake } from "contracts/staking/TokenStake.sol"; import "contracts/mock/Mock.sol"; import "contracts/mock/MockContractPublisher.sol"; @@ -45,6 +46,7 @@ abstract contract BaseTest is DSTest, Test { address public constant NATIVE_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; MockERC20 public erc20; + MockERC20 public erc20Aux; MockERC721 public erc721; MockERC1155 public erc1155; WETH9 public weth; @@ -91,6 +93,7 @@ abstract contract BaseTest is DSTest, Test { signer = vm.addr(privateKey); erc20 = new MockERC20(); + erc20Aux = new MockERC20(); erc721 = new MockERC721(); erc1155 = new MockERC1155(); weth = new WETH9(); @@ -135,6 +138,8 @@ abstract contract BaseTest is DSTest, Test { TWFactory(factory).addImplementation(address(new NFTStake())); TWFactory(factory).addImplementation(address(new MockContract(bytes32("EditionStake"), 1))); TWFactory(factory).addImplementation(address(new EditionStake())); + TWFactory(factory).addImplementation(address(new MockContract(bytes32("TokenStake"), 1))); + TWFactory(factory).addImplementation(address(new TokenStake())); vm.stopPrank(); // setup airdrop logic @@ -341,6 +346,13 @@ abstract contract BaseTest is DSTest, Test { (deployer, CONTRACT_URI, forwarders(), address(erc20), address(erc1155), 60, 1) ) ); + deployContractProxy( + "TokenStake", + abi.encodeCall( + TokenStake.initialize, + (deployer, CONTRACT_URI, forwarders(), address(erc20), address(erc20Aux), 60, 3, 50) + ) + ); } function deployContractProxy(string memory _contractType, bytes memory _initializer)