diff --git a/contracts/gov/GovPool.sol b/contracts/gov/GovPool.sol index 62518381..48c5a7ed 100644 --- a/contracts/gov/GovPool.sol +++ b/contracts/gov/GovPool.sol @@ -249,6 +249,14 @@ contract GovPool is emit Withdrawn(amount, nftIds, receiver); } + function redeemTokens(address receiver, uint256 amount) external override onlyBABTHolder { + _checkBlock(DEPOSIT_WITHDRAW, msg.sender); + + _unlock(msg.sender); + + _govUserKeeper.redeemTokens(msg.sender, receiver, amount, coreProperties); + } + function delegate( address delegatee, uint256 amount, diff --git a/contracts/gov/settings/GovSettings.sol b/contracts/gov/settings/GovSettings.sol index af3bbfad..a7124ae6 100644 --- a/contracts/gov/settings/GovSettings.sol +++ b/contracts/gov/settings/GovSettings.sol @@ -13,9 +13,18 @@ contract GovSettings is IGovSettings, OwnableUpgradeable { mapping(uint256 => ProposalSettings) public settings; // settingsId => info mapping(address => uint256) public executorToSettings; // executor => settingsId + uint256 public totalStakes; + + mapping(uint256 => StakingInfo) public stakingList; // stakingId => info + event SettingsChanged(uint256 settingsId, string description); event ExecutorChanged(uint256 settingsId, address executor); + modifier withCorrectStakingId(uint256 id) { + require(id > 0 && id <= totalStakes, "GovSettings: invalid staking id"); + _; + } + function __GovSettings_init( address govPoolAddress, address validatorsAddress, @@ -91,6 +100,34 @@ contract GovSettings is IGovSettings, OwnableUpgradeable { } } + function createNewStaking( + uint64 lockTime, + uint256 rewardMultiplier, + uint256 redeemPenalty, + bool allowStakingUpgrade + ) external override onlyOwner { + require( + lockTime > 0 && + rewardMultiplier > 0 && + (redeemPenalty == type(uint256).max || redeemPenalty <= PERCENTAGE_100), + "GovSettings: wrong staking info" + ); + + uint256 id = ++totalStakes; + stakingList[id] = StakingInfo( + lockTime, + rewardMultiplier, + redeemPenalty, + allowStakingUpgrade, + false + ); + } + + function closeStaking(uint256 id) external override onlyOwner withCorrectStakingId(id) { + StakingInfo storage stake = stakingList[id]; + stake.disabled = true; + } + function _validateProposalSettings(ProposalSettings calldata _settings) internal pure { require(_settings.duration > 0, "GovSettings: invalid vote duration value"); require(_settings.quorum <= PERCENTAGE_100, "GovSettings: invalid quorum value"); @@ -105,10 +142,6 @@ contract GovSettings is IGovSettings, OwnableUpgradeable { ); } - function _settingsExist(uint256 settingsId) internal view returns (bool) { - return settings[settingsId].duration > 0; - } - function getDefaultSettings() external view override returns (ProposalSettings memory) { return settings[uint256(ExecutorType.DEFAULT)]; } @@ -123,6 +156,22 @@ contract GovSettings is IGovSettings, OwnableUpgradeable { return settings[executorToSettings[executor]]; } + function getStakingSettings( + uint256 id + ) public view override withCorrectStakingId(id) returns (StakingInfo memory stakingInfo) { + stakingInfo = stakingList[id]; + } + + function getStakingSettingsList( + uint256[] calldata ids + ) external view override returns (StakingInfo[] memory stakingInfos) { + stakingInfos = new StakingInfo[](ids.length); + + for (uint256 i = 0; i < ids.length; i++) { + stakingInfos[i] = getStakingSettings(ids[i]); + } + } + function _setExecutor(address executor, uint256 settingsId) internal { executorToSettings[executor] = settingsId; @@ -134,4 +183,8 @@ contract GovSettings is IGovSettings, OwnableUpgradeable { emit SettingsChanged(settingsId, _settings.executorDescription); } + + function _settingsExist(uint256 settingsId) internal view returns (bool) { + return settings[settingsId].duration > 0; + } } diff --git a/contracts/gov/user-keeper/GovUserKeeper.sol b/contracts/gov/user-keeper/GovUserKeeper.sol index b6e40f6b..26072e58 100644 --- a/contracts/gov/user-keeper/GovUserKeeper.sol +++ b/contracts/gov/user-keeper/GovUserKeeper.sol @@ -19,8 +19,10 @@ import "@uniswap/v2-periphery/contracts/interfaces/IWETH.sol"; import "../../interfaces/core/IContractsRegistry.sol"; import "../../interfaces/core/INetworkProperties.sol"; import "../../interfaces/gov/user-keeper/IGovUserKeeper.sol"; +import "../../interfaces/gov/settings/IGovSettings.sol"; import "../../interfaces/gov/IGovPool.sol"; import "../../interfaces/gov/ERC721/powers/IERC721Power.sol"; +import "../../interfaces/core/ICoreProperties.sol"; import "../../libs/math/MathHelper.sol"; import "../../libs/gov/gov-user-keeper/GovUserKeeperView.sol"; @@ -47,6 +49,8 @@ contract GovUserKeeper is IGovUserKeeper, OwnableUpgradeable, ERC721HolderUpgrad address public wethAddress; address public networkPropertiesAddress; + mapping(address => Stake) internal _stakes; + event SetERC20(address token); event SetERC721(address token); @@ -60,6 +64,11 @@ contract GovUserKeeper is IGovUserKeeper, OwnableUpgradeable, ERC721HolderUpgrad _; } + modifier whenNotStaked(address user) { + require(!_isStaked(user), "GovUK: staked"); + _; + } + function __GovUserKeeper_init( address _tokenAddress, address _nftAddress, @@ -108,21 +117,36 @@ contract GovUserKeeper is IGovUserKeeper, OwnableUpgradeable, ERC721HolderUpgrad address payer, address receiver, uint256 amount + ) external override onlyOwner withSupportedToken whenNotStaked(payer) { + _prepareTokenWithdraw(payer, amount); + + _sendNativeOrToken(receiver, amount); + } + + function redeemTokens( + address payer, + address receiver, + uint256 amount, + ICoreProperties coreProperties ) external override onlyOwner withSupportedToken { - UserInfo storage payerInfo = _usersInfo[payer]; - BalanceInfo storage payerBalanceInfo = payerInfo.balances[IGovPool.VoteType.PersonalVote]; + require(amount > 0, "GovUK: empty redeem"); + require(_isStaked(payer), "GovUK: not staked"); - uint256 balance = payerBalanceInfo.tokens; - uint256 maxTokensLocked = payerInfo.maxTokensLocked; + _prepareTokenWithdraw(payer, amount); - require( - amount <= balance.max(maxTokensLocked) - maxTokensLocked, - "GovUK: can't withdraw this" - ); + uint256 id = _stakes[payer].stakeId; + IGovSettings.StakingInfo memory stakeInfo = _getStakeInfo(id); - payerBalanceInfo.tokens = balance - amount; + uint256 redeemPenalty = stakeInfo.redeemPenalty; + require(redeemPenalty != type(uint256).max, "GovUK: redeem forbidden"); + uint256 redeemToGovPoolAmount = amount.percentage(redeemPenalty); - _sendNativeOrToken(receiver, amount); + (uint256 commission, address dexeTreasury) = coreProperties.getDEXECommissionPercentages(); + uint256 redeemToDexeAmount = redeemToGovPoolAmount.percentage(commission); + + _sendNativeOrToken(dexeTreasury, redeemToDexeAmount); + _sendNativeOrToken(owner(), redeemToGovPoolAmount - redeemToDexeAmount); + _sendNativeOrToken(receiver, amount - redeemToGovPoolAmount); } function delegateTokens( @@ -483,6 +507,29 @@ contract GovUserKeeper is IGovUserKeeper, OwnableUpgradeable, ERC721HolderUpgrad IERC721Power(_nftInfo.nftAddress).recalculateNftPowers(nftIds); } + function stake(uint256 id) external override { + address user = msg.sender; + Stake storage userStake = _stakes[user]; + + // inderect check for correct id is here + IGovSettings.StakingInfo memory newStakeInfo = _getStakeInfo(id); + require(!newStakeInfo.disabled, "GovUK: staking tier is disabled"); + + if (!_isStaked(user)) { + _stake(userStake, id); + return; + } + + IGovSettings.StakingInfo memory oldStakeInfo = _getStakeInfo(userStake.stakeId); + + if (oldStakeInfo.lockTime < newStakeInfo.lockTime && oldStakeInfo.allowStakingUpgrade) { + _stake(userStake, id); + return; + } + + revert("GovUK: Already staked"); + } + function setERC20Address(address _tokenAddress) external override onlyOwner { _setERC20Address(_tokenAddress); } @@ -759,6 +806,22 @@ contract GovUserKeeper is IGovUserKeeper, OwnableUpgradeable, ERC721HolderUpgrad nativeAmount -= value; } + function getStakingMultiplier(address user) public view override returns (uint256) { + Stake storage currentStake = _stakes[user]; + uint256 id = currentStake.stakeId; + + if (id == 0) return 0; + + (address settings, , , , ) = IGovPool(owner()).getHelperContracts(); + IGovSettings.StakingInfo memory stakeInfo = IGovSettings(settings).getStakingSettings(id); + + if (stakeInfo.disabled) return 0; + + if (currentStake.startedAt + stakeInfo.lockTime <= block.timestamp) return 0; + + return stakeInfo.rewardMultiplier; + } + function _sendNativeOrToken(address receiver, uint256 amount) internal { address token = tokenAddress; amount = amount.from18Safe(token); @@ -839,6 +902,21 @@ contract GovUserKeeper is IGovUserKeeper, OwnableUpgradeable, ERC721HolderUpgrad require(_nftInfo.nftAddress != address(0), "GovUK: nft is not supported"); } + function _prepareTokenWithdraw(address payer, uint256 amount) internal { + UserInfo storage payerInfo = _usersInfo[payer]; + BalanceInfo storage payerBalanceInfo = payerInfo.balances[IGovPool.VoteType.PersonalVote]; + + uint256 balance = payerBalanceInfo.tokens; + uint256 maxTokensLocked = payerInfo.maxTokensLocked; + + require( + amount <= balance.max(maxTokensLocked) - maxTokensLocked, + "GovUK: can't withdraw this" + ); + + payerBalanceInfo.tokens = balance - amount; + } + function _handleNative(uint256 value, bool wrapping) internal { if (value == 0) { return; @@ -857,4 +935,20 @@ contract GovUserKeeper is IGovUserKeeper, OwnableUpgradeable, ERC721HolderUpgrad return _wethAddress != address(0) && wethAddress == tokenAddress; } + + function _stake(Stake storage userStake, uint256 newId) internal { + userStake.startedAt = uint64(block.timestamp); + userStake.stakeId = newId; + } + + function _isStaked(address user) internal view returns (bool) { + return getStakingMultiplier(user) != 0; + } + + function _getStakeInfo( + uint256 id + ) internal view returns (IGovSettings.StakingInfo memory stakeInfo) { + (address settings, , , , ) = IGovPool(owner()).getHelperContracts(); + stakeInfo = IGovSettings(settings).getStakingSettings(id); + } } diff --git a/contracts/interfaces/gov/IGovPool.sol b/contracts/interfaces/gov/IGovPool.sol index 4683a60c..1da5b578 100644 --- a/contracts/interfaces/gov/IGovPool.sol +++ b/contracts/interfaces/gov/IGovPool.sol @@ -369,6 +369,11 @@ interface IGovPool { /// @param nftIds the array of nft ids to withdraw function withdraw(address receiver, uint256 amount, uint256[] calldata nftIds) external; + /// @notice The function for redeemings staked tokens + /// @param receiver the redeeming receiver address + /// @param amount the erc20 redeeming amount + function redeemTokens(address receiver, uint256 amount) external; + /// @notice The function for delegating tokens /// @param delegatee the target address for delegation (person who will receive the delegation) /// @param amount the erc20 delegation amount diff --git a/contracts/interfaces/gov/settings/IGovSettings.sol b/contracts/interfaces/gov/settings/IGovSettings.sol index da47cb6f..da3d2b43 100644 --- a/contracts/interfaces/gov/settings/IGovSettings.sol +++ b/contracts/interfaces/gov/settings/IGovSettings.sol @@ -51,6 +51,20 @@ interface IGovSettings { uint256 voteRewardsCoefficient; } + /// @notice The struct holds information about staking + /// @param lockTime the lock time of the stake + /// @param rewardMultiplier the reward bonus for the staker + /// @param redeemPenalty the percent substracted for early unstake, 0-100*10**25, uint.max if not allowed + /// @param allowStakingUpgrade the possibility to switch to long-term staking without penalties + /// @param disabled the state of staking + struct StakingInfo { + uint64 lockTime; + uint256 rewardMultiplier; + uint256 redeemPenalty; + bool allowStakingUpgrade; + bool disabled; + } + /// @notice The function to get settings of this executor /// @param executor the executor /// @return setting id of the executor @@ -76,6 +90,21 @@ interface IGovSettings { uint256[] calldata settingsIds ) external; + /// @notice Create new staking + /// @param lockTime Time to lock assets + /// @param rewardMultiplier The reward multiplier with precision + /// @param redeemPenalty The penalty for early unstake 0-100% precision or uint.max + function createNewStaking( + uint64 lockTime, + uint256 rewardMultiplier, + uint256 redeemPenalty, + bool allowStakingUpgrade + ) external; + + /// @notice Disables active staking + /// @param id The staking id + function closeStaking(uint256 id) external; + /// @notice The function to get default settings /// @return default setting function getDefaultSettings() external view returns (ProposalSettings memory); @@ -88,4 +117,16 @@ interface IGovSettings { /// @param executor Executor address /// @return `ProposalSettings` by `executor` address function getExecutorSettings(address executor) external view returns (ProposalSettings memory); + + /// @notice The function the get the staking settings + /// @param id Staking id + /// @return `StakingInfo` by staking `id` + function getStakingSettings(uint256 id) external view returns (StakingInfo memory); + + /// @notice The function the get the staking settings list + /// @param ids Staking ids list + /// @return `StakingInfo` list by staking `ids` list + function getStakingSettingsList( + uint256[] calldata ids + ) external view returns (StakingInfo[] memory); } diff --git a/contracts/interfaces/gov/user-keeper/IGovUserKeeper.sol b/contracts/interfaces/gov/user-keeper/IGovUserKeeper.sol index aa9c474e..287995e6 100644 --- a/contracts/interfaces/gov/user-keeper/IGovUserKeeper.sol +++ b/contracts/interfaces/gov/user-keeper/IGovUserKeeper.sol @@ -86,6 +86,14 @@ interface IGovUserKeeper { uint256[] perNftPower; } + /// @notice The struct that hold information about user's stake + /// @param stakeId the if of staking parameters + /// @param startedAt the timestamp of staking start + struct Stake { + uint256 stakeId; + uint64 startedAt; + } + /// @notice The function for injecting dependencies from the GovPool /// @param contractsRegistry the address of Contracts Registry function setDependencies(address contractsRegistry, bytes memory) external; @@ -102,6 +110,18 @@ interface IGovUserKeeper { /// @param amount the erc20 withdrawal amount function withdrawTokens(address payer, address receiver, uint256 amount) external; + /// @notice The function for redeeming staked tokens + /// @param payer the address from whom to redeem the tokens + /// @param receiver the redeem receiver address + /// @param amount the erc20 redeeming amount + /// @param coreProperties the CoreProperties contract address + function redeemTokens( + address payer, + address receiver, + uint256 amount, + ICoreProperties coreProperties + ) external; + /// @notice The function for delegating tokens /// @param delegator the address of delegator /// @param delegatee the address of delegatee @@ -203,6 +223,10 @@ interface IGovUserKeeper { /// @param nftIds the array of nft ids to recalculate the power for function updateNftPowers(uint256[] calldata nftIds) external; + /// @notice The function for staking tokens + /// @param id the id of the staking tier + function stake(uint256 id) external; + /// @notice The function for setting erc20 address /// @param _tokenAddress the erc20 address function setERC20Address(address _tokenAddress) external; @@ -360,4 +384,9 @@ interface IGovUserKeeper { uint256 value, uint256 amount ) external view returns (uint256 nativeAmount); + + /// @notice The function for getting the staking multiplier for a user + /// @param user the address of the user + /// @return the multiplier amount with 25 digits precision + function getStakingMultiplier(address user) external view returns (uint256); } diff --git a/contracts/libs/gov/gov-pool/GovPoolRewards.sol b/contracts/libs/gov/gov-pool/GovPoolRewards.sol index 54e572e4..26bee84a 100644 --- a/contracts/libs/gov/gov-pool/GovPoolRewards.sol +++ b/contracts/libs/gov/gov-pool/GovPoolRewards.sol @@ -7,6 +7,7 @@ import "@openzeppelin/contracts/utils/math/Math.sol"; import "../../../interfaces/core/ICoreProperties.sol"; import "../../../interfaces/gov/IGovPool.sol"; +import "../../../interfaces/gov/user-keeper/IGovUserKeeper.sol"; import "../../../interfaces/gov/ERC721/multipliers/IAbstractERC721Multiplier.sol"; import "../../utils/TokenBalance.sol"; @@ -224,12 +225,24 @@ library GovPoolRewards { function _getMultipliedRewards(address user, uint256 amount) internal view returns (uint256) { (address nftMultiplier, , , ) = IGovPool(address(this)).getNftContracts(); + (, address userKeeper, , , ) = IGovPool(address(this)).getHelperContracts(); + + uint256 multiplierReward; + uint256 stakingReward; if (nftMultiplier != address(0)) { - amount += IAbstractERC721Multiplier(nftMultiplier).getExtraRewards(user, amount); + multiplierReward = IAbstractERC721Multiplier(nftMultiplier).getExtraRewards( + user, + amount + ); } - return amount; + stakingReward = amount.ratio( + IGovUserKeeper(userKeeper).getStakingMultiplier(user), + PRECISION + ); + + return amount + multiplierReward + stakingReward; } function _getVotingRewards( diff --git a/scripts/utils/constants.js b/scripts/utils/constants.js index 10a00378..681dae49 100644 --- a/scripts/utils/constants.js +++ b/scripts/utils/constants.js @@ -8,9 +8,10 @@ const SECONDS_IN_MONTH = SECONDS_IN_DAY * 30; const PRECISION = toBN(10).pow(25); const PERCENTAGE_100 = PRECISION.times(100); +const PERCENTAGE_50 = PRECISION.times(50); const DECIMAL = toBN(10).pow(18); -const MAX_UINT = toBN(2).pow(256) - 1; +const MAX_UINT = toBN(2).pow(256).minus(1); module.exports = { ZERO_ADDR, @@ -19,6 +20,7 @@ module.exports = { SECONDS_IN_MONTH, PRECISION, PERCENTAGE_100, + PERCENTAGE_50, DECIMAL, MAX_UINT, }; diff --git a/test/gov/GovPool.test.js b/test/gov/GovPool.test.js index 59cd1210..24b8044c 100644 --- a/test/gov/GovPool.test.js +++ b/test/gov/GovPool.test.js @@ -35,7 +35,14 @@ const { getBytesChangeValidatorSettings, getBytesMonthlyWithdraw, } = require("../utils/gov-validators-utils"); -const { ZERO_ADDR, ETHER_ADDR, PRECISION, PERCENTAGE_100 } = require("../../scripts/utils/constants"); +const { + ZERO_ADDR, + ETHER_ADDR, + PRECISION, + PERCENTAGE_100, + PERCENTAGE_50, + MAX_UINT, +} = require("../../scripts/utils/constants"); const { ProposalState, DEFAULT_CORE_PROPERTIES, @@ -803,6 +810,196 @@ describe("GovPool", () => { }); }); + describe("staking", () => { + beforeEach(async () => { + await impersonate(govPool.address); + }); + + it("must be govPool to create staking", async () => { + await truffleAssert.reverts(settings.createNewStaking(1, 1, 0, false), "Ownable: caller is not the owner"); + }); + + it("staking must have correct parameters", async () => { + await truffleAssert.reverts( + settings.createNewStaking(0, 1, 0, false, { from: govPool.address }), + "GovSettings: wrong staking info", + ); + + await truffleAssert.reverts( + settings.createNewStaking(1, 0, 0, false, { from: govPool.address }), + "GovSettings: wrong staking info", + ); + + await truffleAssert.reverts( + settings.createNewStaking(1, 1, wei("1000000001"), false, { from: govPool.address }), + "GovSettings: wrong staking info", + ); + }); + + it("could create staking", async () => { + assert.equal(await settings.totalStakes(), 0); + await truffleAssert.reverts(settings.getStakingSettings(1), "GovSettings: invalid staking id"); + + await settings.createNewStaking(1, 1, 0, false, { from: govPool.address }); + assert.equal(await settings.totalStakes(), 1); + await settings.createNewStaking(2, 2, 1, false, { from: govPool.address }); + assert.equal(await settings.totalStakes(), 2); + const list = await settings.getStakingSettingsList([1, 2]); + assert.deepEqual(list[0], ["1", "1", "0", false, false]); + assert.deepEqual(list[1], ["2", "2", "1", false, false]); + }); + + it("could disable staking", async () => { + await settings.createNewStaking(1, 1, 0, false, { from: govPool.address }); + assert.deepEqual(await settings.getStakingSettings(1), ["1", "1", "0", false, false]); + + await truffleAssert.reverts(settings.closeStaking(1), "Ownable: caller is not the owner"); + + await settings.closeStaking(1, { from: govPool.address }); + assert.deepEqual(await settings.getStakingSettings(1), ["1", "1", "0", false, true]); + }); + + it("can't disable invalid staking", async () => { + await truffleAssert.reverts( + settings.closeStaking(0, { from: govPool.address }), + "GovSettings: invalid staking id", + ); + await truffleAssert.reverts( + settings.closeStaking(1, { from: govPool.address }), + "GovSettings: invalid staking id", + ); + }); + + it("cant stake on zero tier", async () => { + await truffleAssert.reverts(userKeeper.stake(0), "GovSettings: invalid staking id"); + }); + + it("cant stake on non existent tier", async () => { + await truffleAssert.reverts(userKeeper.stake(2), "GovSettings: invalid staking id"); + }); + + it("cant stake on disabled tier", async () => { + await settings.createNewStaking(5, 1, 0, false, { from: govPool.address }); + await settings.closeStaking(1, { from: govPool.address }); + await truffleAssert.reverts(userKeeper.stake(1), "GovUK: staking tier is disabled"); + }); + + it("cant stake if already staked", async () => { + await settings.createNewStaking(10, 1, 0, false, { from: govPool.address }); + await userKeeper.stake(1); + await settings.createNewStaking(5, 1, 0, false, { from: govPool.address }); + await truffleAssert.reverts(userKeeper.stake(2), "GovUK: Already staked"); + }); + + it("could stake anything and withdraw nfts", async () => { + await govPool.deposit(wei("100"), [1, 2, 3]); + + await setTime((await getCurrentBlockTime()) + 1); + await govPool.withdraw.call(OWNER, wei("100"), [1, 2, 3]); + + await settings.createNewStaking(5, 1, 0, false, { from: govPool.address }); + await userKeeper.stake(1); + await truffleAssert.reverts(govPool.withdraw(OWNER, wei("100"), [1, 2, 3]), "GovUK: staked"); + + await govPool.withdraw(OWNER, 0, [1, 2, 3]); + }); + + it("could withdraw from disabled tier", async () => { + await govPool.deposit(wei("100"), []); + await settings.createNewStaking(100, 1, 0, false, { from: govPool.address }); + await userKeeper.stake(1); + + await truffleAssert.reverts(govPool.withdraw(OWNER, wei("100"), []), "GovUK: staked"); + + await settings.closeStaking(1, { from: govPool.address }); + + await govPool.withdraw(OWNER, wei("100"), []); + }); + + it("could withdraw after staking end", async () => { + await govPool.deposit(wei("100"), []); + await settings.createNewStaking(100, 1, 0, false, { from: govPool.address }); + await userKeeper.stake(1); + + await truffleAssert.reverts(govPool.withdraw(OWNER, wei("100"), []), "GovUK: staked"); + + await setTime((await getCurrentBlockTime()) + 100); + + await govPool.withdraw(OWNER, wei("100"), []); + }); + + it("could restake after staking end", async () => { + await govPool.deposit(wei("100"), []); + await settings.createNewStaking(100, 1, 0, false, { from: govPool.address }); + await userKeeper.stake(1); + + await truffleAssert.reverts(govPool.withdraw(OWNER, wei("100"), []), "GovUK: staked"); + + await setTime((await getCurrentBlockTime()) + 100); + + await settings.createNewStaking(100, 2, 0, false, { from: govPool.address }); + + await userKeeper.stake(2); + }); + + it("could restake to a longer tier", async () => { + await govPool.deposit(wei("100"), []); + await settings.createNewStaking(100, 1, 0, true, { from: govPool.address }); + await userKeeper.stake(1); + + await truffleAssert.reverts(govPool.withdraw(OWNER, wei("100"), []), "GovUK: staked"); + + await settings.createNewStaking(200, 2, 0, false, { from: govPool.address }); + + await userKeeper.stake(2); + }); + + it("cant redeem from user keeper", async () => { + await truffleAssert.reverts( + userKeeper.redeemTokens(OWNER, OWNER, wei("100"), OWNER), + "Ownable: caller is not the owner", + ); + }); + + it("cant redeem zero tokens", async () => { + await govPool.deposit(wei("100"), []); + await settings.createNewStaking(100, 1, 50, false, { from: govPool.address }); + await userKeeper.stake(1); + + await truffleAssert.reverts(govPool.withdraw(OWNER, wei("100"), []), "GovUK: staked"); + + await truffleAssert.reverts(govPool.redeemTokens(OWNER, 0), "GovUK: empty redeem"); + }); + + it("cant redeem if not staked", async () => { + await govPool.deposit(wei("100"), []); + await truffleAssert.reverts(govPool.redeemTokens(OWNER, wei("100")), "GovUK: not staked"); + }); + + it("cant redeem when redeem forbidden", async () => { + await govPool.deposit(wei("100"), []); + await settings.createNewStaking(100, 1, MAX_UINT, false, { from: govPool.address }); + await userKeeper.stake(1); + + await truffleAssert.reverts(govPool.redeemTokens(OWNER, wei("100")), "GovUK: redeem forbidden"); + }); + + it("could redeem", async () => { + await govPool.deposit(wei("100"), []); + await settings.createNewStaking(100, 1, PERCENTAGE_50, false, { from: govPool.address }); + await userKeeper.stake(1); + + const ownerBalance = (await token.balanceOf(OWNER)).plus(wei("50")); + const govPoolBalance = (await token.balanceOf(govPool.address)).plus(wei("40")); + const treasuryBalance = (await token.balanceOf(ETHER_ADDR)).plus(wei("10")); + await govPool.redeemTokens(OWNER, wei("100")); + + assert.equal((await token.balanceOf(OWNER)).toFixed(), ownerBalance.toFixed()); + assert.equal((await token.balanceOf(govPool.address)).toFixed(), govPoolBalance.toFixed()); + assert.equal((await token.balanceOf(ETHER_ADDR)).toFixed(), treasuryBalance.toFixed()); + }); + }); + describe("unlock()", () => { beforeEach("setup", async () => { await token.mint(SECOND, wei("100000000000000000000")); @@ -5907,6 +6104,10 @@ describe("GovPool", () => { await truffleAssert.reverts(govPool.withdraw(SECOND, wei("1000"), []), REVERT_STRING); }); + it("redeem()", async () => { + await truffleAssert.reverts(govPool.redeemTokens(SECOND, wei("1000")), REVERT_STRING); + }); + it("delegate()", async () => { await truffleAssert.reverts(govPool.delegate(OWNER, wei("500"), []), REVERT_STRING); }); diff --git a/test/gov/GovUserKeeper.test.js b/test/gov/GovUserKeeper.test.js index 3648a6ab..db9233d0 100644 --- a/test/gov/GovUserKeeper.test.js +++ b/test/gov/GovUserKeeper.test.js @@ -1053,6 +1053,11 @@ describe("GovUserKeeper", () => { await truffleAssert.reverts(userKeeper.delegateTokens(OWNER, OWNER, wei("100")), "GovUK: token is not supported"); + await truffleAssert.reverts( + userKeeper.redeemTokens(OWNER, OWNER, wei("100"), OWNER), + "GovUK: token is not supported", + ); + await truffleAssert.reverts( userKeeper.delegateTokensTreasury(OWNER, wei("100")), "GovUK: token is not supported",