From eb2163ab7de71c1b4bc8c882cc931f4d74242b46 Mon Sep 17 00:00:00 2001 From: Dmitry Redkin Date: Tue, 13 Aug 2024 10:24:04 +0300 Subject: [PATCH 1/5] logic --- contracts/gov/GovPool.sol | 8 ++ contracts/gov/settings/GovSettings.sol | 52 ++++++++- contracts/gov/user-keeper/GovUserKeeper.sol | 101 ++++++++++++++++-- .../interfaces/gov/settings/IGovSettings.sol | 34 ++++++ .../gov/user-keeper/IGovUserKeeper.sol | 14 +++ test/gov/GovPool.test.js | 36 +++++++ 6 files changed, 231 insertions(+), 14 deletions(-) diff --git a/contracts/gov/GovPool.sol b/contracts/gov/GovPool.sol index 62518381..aa5cfabc 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 { + _checkBlock(DEPOSIT_WITHDRAW, msg.sender); + + _unlock(msg.sender); + + _govUserKeeper.redeemTokens(msg.sender, receiver, amount); + } + function delegate( address delegatee, uint256 amount, diff --git a/contracts/gov/settings/GovSettings.sol b/contracts/gov/settings/GovSettings.sol index af3bbfad..04d6c400 100644 --- a/contracts/gov/settings/GovSettings.sol +++ b/contracts/gov/settings/GovSettings.sol @@ -13,6 +13,10 @@ 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); @@ -91,6 +95,29 @@ contract GovSettings is IGovSettings, OwnableUpgradeable { } } + function createNewStaking( + uint64 lockTime, + uint256 rewardMultiplier, + uint256 redeemPenalty + ) 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, true); + } + + function closeStaking(uint256 id) external onlyOwner { + require(id > 0 && id <= totalStakes, "GovSettings: invalid staking 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 +132,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 +146,23 @@ contract GovSettings is IGovSettings, OwnableUpgradeable { return settings[executorToSettings[executor]]; } + function getStakingSettings( + uint256 id + ) public view override returns (StakingInfo memory stakingInfo) { + require(id > 0 && id <= totalStakes, "GovSettings: invalid id"); + 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 +174,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..5f2e9bec 100644 --- a/contracts/gov/user-keeper/GovUserKeeper.sol +++ b/contracts/gov/user-keeper/GovUserKeeper.sol @@ -19,6 +19,7 @@ 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"; @@ -47,6 +48,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 +63,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 +116,32 @@ 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 ) 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; + (address settings, , , , ) = IGovPool(owner()).getHelperContracts(); + IGovSettings.StakingInfo memory stakeInfo = IGovSettings(settings).getStakingSettings(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); + _sendNativeOrToken(owner(), redeemToGovpoolAmount); + _sendNativeOrToken(receiver, amount - redeemToGovpoolAmount); } function delegateTokens( @@ -483,6 +502,28 @@ contract GovUserKeeper is IGovUserKeeper, OwnableUpgradeable, ERC721HolderUpgrad IERC721Power(_nftInfo.nftAddress).recalculateNftPowers(nftIds); } + function stake(uint256 id) external { + (address settings, , , , ) = IGovPool(owner()).getHelperContracts(); + uint256[] memory ids = new uint256[](2); + + Stake storage currentStake = _stakes[msg.sender]; + ids[0] = currentStake.stakeId; + ids[1] = id; + + IGovSettings.StakingInfo[] memory stakeInfos = IGovSettings(settings) + .getStakingSettingsList(ids); + + require(!stakeInfos[1].disabled, "GovUK: staking tier is disabled"); + require( + ids[0] == 0 || + currentStake.startedAt + stakeInfos[0].lockTime <= block.timestamp || + stakeInfos[0].lockTime < stakeInfos[1].lockTime, + "GovUK: Already staked" + ); + + _stake(currentStake, ids[1]); + } + function setERC20Address(address _tokenAddress) external override onlyOwner { _setERC20Address(_tokenAddress); } @@ -759,6 +800,22 @@ contract GovUserKeeper is IGovUserKeeper, OwnableUpgradeable, ERC721HolderUpgrad nativeAmount -= value; } + function getStakingMultiplier(address user) public view 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 +896,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 +929,13 @@ 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; + } } diff --git a/contracts/interfaces/gov/settings/IGovSettings.sol b/contracts/interfaces/gov/settings/IGovSettings.sol index da47cb6f..4ee667f5 100644 --- a/contracts/interfaces/gov/settings/IGovSettings.sol +++ b/contracts/interfaces/gov/settings/IGovSettings.sol @@ -51,6 +51,18 @@ 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 disabled the state of staking + struct StakingInfo { + uint64 lockTime; + uint256 rewardMultiplier; + uint256 redeemPenalty; + bool disabled; + } + /// @notice The function to get settings of this executor /// @param executor the executor /// @return setting id of the executor @@ -76,6 +88,16 @@ 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 + ) external; + /// @notice The function to get default settings /// @return default setting function getDefaultSettings() external view returns (ProposalSettings memory); @@ -88,4 +110,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..c0945d9e 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,12 @@ 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 + function redeemTokens(address payer, address receiver, uint256 amount) external; + /// @notice The function for delegating tokens /// @param delegator the address of delegator /// @param delegatee the address of delegatee diff --git a/test/gov/GovPool.test.js b/test/gov/GovPool.test.js index 59cd1210..9da16603 100644 --- a/test/gov/GovPool.test.js +++ b/test/gov/GovPool.test.js @@ -803,6 +803,42 @@ describe("GovPool", () => { }); }); + describe.only("staking", () => { + beforeEach(async () => { + await impersonate(govPool.address); + }); + + it("must be govPool to create staking", async () => { + await truffleAssert.reverts(settings.createNewStaking(1, 1, 0), "Ownable: caller is not the owner"); + }); + + it("staking must have correct parameters", async () => { + await truffleAssert.reverts( + settings.createNewStaking(0, 1, 0, { from: govPool.address }), + "GovSettings: wrong staking info", + ); + + await truffleAssert.reverts( + settings.createNewStaking(1, 0, 0, { from: govPool.address }), + "GovSettings: wrong staking info", + ); + + await truffleAssert.reverts( + settings.createNewStaking(1, 1, wei("1000000001"), { 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 id"); + + await settings.createNewStaking(1, 1, 0, { from: govPool.address }); + assert.equal(await settings.totalStakes(), 1); + assert.deepEqual(await settings.getStakingSettings(1), ["1", "1", "0", true]); + }); + }); + describe("unlock()", () => { beforeEach("setup", async () => { await token.mint(SECOND, wei("100000000000000000000")); From 27e7c9ce41b91377905cb368ec32c03c89aff06e Mon Sep 17 00:00:00 2001 From: Dmitry Redkin Date: Wed, 14 Aug 2024 15:05:48 +0300 Subject: [PATCH 2/5] tests --- contracts/gov/settings/GovSettings.sol | 6 +- contracts/gov/user-keeper/GovUserKeeper.sol | 3 +- contracts/interfaces/gov/IGovPool.sol | 5 + .../interfaces/gov/settings/IGovSettings.sol | 4 + .../gov/user-keeper/IGovUserKeeper.sol | 4 + scripts/utils/constants.js | 4 +- test/gov/GovPool.test.js | 159 +++++++++++++++++- test/gov/GovUserKeeper.test.js | 2 + 8 files changed, 180 insertions(+), 7 deletions(-) diff --git a/contracts/gov/settings/GovSettings.sol b/contracts/gov/settings/GovSettings.sol index 04d6c400..f8a65454 100644 --- a/contracts/gov/settings/GovSettings.sol +++ b/contracts/gov/settings/GovSettings.sol @@ -108,10 +108,10 @@ contract GovSettings is IGovSettings, OwnableUpgradeable { ); uint256 id = ++totalStakes; - stakingList[id] = StakingInfo(lockTime, rewardMultiplier, redeemPenalty, true); + stakingList[id] = StakingInfo(lockTime, rewardMultiplier, redeemPenalty, false); } - function closeStaking(uint256 id) external onlyOwner { + function closeStaking(uint256 id) external override onlyOwner { require(id > 0 && id <= totalStakes, "GovSettings: invalid staking id"); StakingInfo storage stake = stakingList[id]; @@ -149,7 +149,7 @@ contract GovSettings is IGovSettings, OwnableUpgradeable { function getStakingSettings( uint256 id ) public view override returns (StakingInfo memory stakingInfo) { - require(id > 0 && id <= totalStakes, "GovSettings: invalid id"); + require(id <= totalStakes, "GovSettings: invalid id"); stakingInfo = stakingList[id]; } diff --git a/contracts/gov/user-keeper/GovUserKeeper.sol b/contracts/gov/user-keeper/GovUserKeeper.sol index 5f2e9bec..024383d0 100644 --- a/contracts/gov/user-keeper/GovUserKeeper.sol +++ b/contracts/gov/user-keeper/GovUserKeeper.sol @@ -502,7 +502,7 @@ contract GovUserKeeper is IGovUserKeeper, OwnableUpgradeable, ERC721HolderUpgrad IERC721Power(_nftInfo.nftAddress).recalculateNftPowers(nftIds); } - function stake(uint256 id) external { + function stake(uint256 id) external override { (address settings, , , , ) = IGovPool(owner()).getHelperContracts(); uint256[] memory ids = new uint256[](2); @@ -931,6 +931,7 @@ contract GovUserKeeper is IGovUserKeeper, OwnableUpgradeable, ERC721HolderUpgrad } function _stake(Stake storage userStake, uint256 newId) internal { + require(newId != 0, "GovUK: zero staking id"); userStake.startedAt = uint64(block.timestamp); userStake.stakeId = newId; } 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 4ee667f5..6a381dd0 100644 --- a/contracts/interfaces/gov/settings/IGovSettings.sol +++ b/contracts/interfaces/gov/settings/IGovSettings.sol @@ -98,6 +98,10 @@ interface IGovSettings { uint256 redeemPenalty ) 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); diff --git a/contracts/interfaces/gov/user-keeper/IGovUserKeeper.sol b/contracts/interfaces/gov/user-keeper/IGovUserKeeper.sol index c0945d9e..05ef9406 100644 --- a/contracts/interfaces/gov/user-keeper/IGovUserKeeper.sol +++ b/contracts/interfaces/gov/user-keeper/IGovUserKeeper.sol @@ -217,6 +217,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; 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 9da16603..c0cbe46d 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,7 +810,7 @@ describe("GovPool", () => { }); }); - describe.only("staking", () => { + describe("staking", () => { beforeEach(async () => { await impersonate(govPool.address); }); @@ -835,8 +842,156 @@ describe("GovPool", () => { await settings.createNewStaking(1, 1, 0, { from: govPool.address }); assert.equal(await settings.totalStakes(), 1); + assert.deepEqual(await settings.getStakingSettings(1), ["1", "1", "0", false]); + }); + + it("could disable staking", async () => { + await settings.createNewStaking(1, 1, 0, { from: govPool.address }); + assert.deepEqual(await settings.getStakingSettings(1), ["1", "1", "0", 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", 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), "GovUK: zero staking id"); + }); + + it("cant stake on non existent tier", async () => { + await truffleAssert.reverts(userKeeper.stake(2), "GovSettings: invalid id"); + }); + + it("cant stake on disabled tier", async () => { + await settings.createNewStaking(5, 1, 0, { 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, { from: govPool.address }); + await userKeeper.stake(1); + await settings.createNewStaking(5, 1, 0, { 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, { 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, { 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, { 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, { 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, { 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, { from: govPool.address }); + await userKeeper.stake(1); + + await truffleAssert.reverts(govPool.withdraw(OWNER, wei("100"), []), "GovUK: staked"); + + await settings.createNewStaking(200, 2, 0, { from: govPool.address }); + + await userKeeper.stake(2); + }); + + it("cant redeem from user keeper", async () => { + await truffleAssert.reverts( + userKeeper.redeemTokens(OWNER, OWNER, wei("100")), + "Ownable: caller is not the owner", + ); + }); + + it("cant redeem zero tokens", async () => { + await govPool.deposit(wei("100"), []); + await settings.createNewStaking(100, 1, 50, { 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, { 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, { 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("50")); + 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()); + }); }); describe("unlock()", () => { diff --git a/test/gov/GovUserKeeper.test.js b/test/gov/GovUserKeeper.test.js index 3648a6ab..de43ef2c 100644 --- a/test/gov/GovUserKeeper.test.js +++ b/test/gov/GovUserKeeper.test.js @@ -1053,6 +1053,8 @@ 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")), "GovUK: token is not supported"); + await truffleAssert.reverts( userKeeper.delegateTokensTreasury(OWNER, wei("100")), "GovUK: token is not supported", From 788352099b1b01fb2969d3b2d2b7eacb36699953 Mon Sep 17 00:00:00 2001 From: Dmitry Redkin Date: Wed, 14 Aug 2024 21:02:26 +0300 Subject: [PATCH 3/5] refactor --- contracts/gov/settings/GovSettings.sol | 2 +- contracts/gov/user-keeper/GovUserKeeper.sol | 42 ++++++++++++--------- test/gov/GovPool.test.js | 8 +++- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/contracts/gov/settings/GovSettings.sol b/contracts/gov/settings/GovSettings.sol index f8a65454..50cc0853 100644 --- a/contracts/gov/settings/GovSettings.sol +++ b/contracts/gov/settings/GovSettings.sol @@ -149,7 +149,7 @@ contract GovSettings is IGovSettings, OwnableUpgradeable { function getStakingSettings( uint256 id ) public view override returns (StakingInfo memory stakingInfo) { - require(id <= totalStakes, "GovSettings: invalid id"); + require(id != 0 && id <= totalStakes, "GovSettings: invalid id"); stakingInfo = stakingList[id]; } diff --git a/contracts/gov/user-keeper/GovUserKeeper.sol b/contracts/gov/user-keeper/GovUserKeeper.sol index 024383d0..155db83d 100644 --- a/contracts/gov/user-keeper/GovUserKeeper.sol +++ b/contracts/gov/user-keeper/GovUserKeeper.sol @@ -133,8 +133,7 @@ contract GovUserKeeper is IGovUserKeeper, OwnableUpgradeable, ERC721HolderUpgrad _prepareTokenWithdraw(payer, amount); uint256 id = _stakes[payer].stakeId; - (address settings, , , , ) = IGovPool(owner()).getHelperContracts(); - IGovSettings.StakingInfo memory stakeInfo = IGovSettings(settings).getStakingSettings(id); + IGovSettings.StakingInfo memory stakeInfo = _getStakeInfo(id); uint256 redeemPenalty = stakeInfo.redeemPenalty; require(redeemPenalty != type(uint256).max, "GovUK: redeem forbidden"); @@ -503,25 +502,26 @@ contract GovUserKeeper is IGovUserKeeper, OwnableUpgradeable, ERC721HolderUpgrad } function stake(uint256 id) external override { - (address settings, , , , ) = IGovPool(owner()).getHelperContracts(); - uint256[] memory ids = new uint256[](2); + address user = msg.sender; + Stake storage userStake = _stakes[user]; - Stake storage currentStake = _stakes[msg.sender]; - ids[0] = currentStake.stakeId; - ids[1] = id; + // inderect check for correct id is here + IGovSettings.StakingInfo memory newStakeInfo = _getStakeInfo(id); + require(!newStakeInfo.disabled, "GovUK: staking tier is disabled"); - IGovSettings.StakingInfo[] memory stakeInfos = IGovSettings(settings) - .getStakingSettingsList(ids); + if (!_isStaked(user)) { + _stake(userStake, id); + return; + } - require(!stakeInfos[1].disabled, "GovUK: staking tier is disabled"); - require( - ids[0] == 0 || - currentStake.startedAt + stakeInfos[0].lockTime <= block.timestamp || - stakeInfos[0].lockTime < stakeInfos[1].lockTime, - "GovUK: Already staked" - ); + IGovSettings.StakingInfo memory oldStakeInfo = _getStakeInfo(userStake.stakeId); - _stake(currentStake, ids[1]); + if (oldStakeInfo.lockTime < newStakeInfo.lockTime) { + _stake(userStake, id); + return; + } + + revert("GovUK: Already staked"); } function setERC20Address(address _tokenAddress) external override onlyOwner { @@ -931,7 +931,6 @@ contract GovUserKeeper is IGovUserKeeper, OwnableUpgradeable, ERC721HolderUpgrad } function _stake(Stake storage userStake, uint256 newId) internal { - require(newId != 0, "GovUK: zero staking id"); userStake.startedAt = uint64(block.timestamp); userStake.stakeId = newId; } @@ -939,4 +938,11 @@ contract GovUserKeeper is IGovUserKeeper, OwnableUpgradeable, ERC721HolderUpgrad 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/test/gov/GovPool.test.js b/test/gov/GovPool.test.js index c0cbe46d..3e7d6867 100644 --- a/test/gov/GovPool.test.js +++ b/test/gov/GovPool.test.js @@ -842,7 +842,11 @@ describe("GovPool", () => { await settings.createNewStaking(1, 1, 0, { from: govPool.address }); assert.equal(await settings.totalStakes(), 1); - assert.deepEqual(await settings.getStakingSettings(1), ["1", "1", "0", false]); + await settings.createNewStaking(2, 2, 1, { from: govPool.address }); + assert.equal(await settings.totalStakes(), 2); + const list = await settings.getStakingSettingsList([1, 2]); + assert.deepEqual(list[0], ["1", "1", "0", false]); + assert.deepEqual(list[1], ["2", "2", "1", false]); }); it("could disable staking", async () => { @@ -867,7 +871,7 @@ describe("GovPool", () => { }); it("cant stake on zero tier", async () => { - await truffleAssert.reverts(userKeeper.stake(0), "GovUK: zero staking id"); + await truffleAssert.reverts(userKeeper.stake(0), "GovSettings: invalid id"); }); it("cant stake on non existent tier", async () => { From 91f48357dec181a6ab1d5f2cee50844bf71a2b38 Mon Sep 17 00:00:00 2001 From: Dmitry Redkin Date: Thu, 15 Aug 2024 10:59:07 +0300 Subject: [PATCH 4/5] transfer to treasury --- contracts/gov/GovPool.sol | 2 +- contracts/gov/settings/GovSettings.sol | 11 +++- contracts/gov/user-keeper/GovUserKeeper.sol | 18 ++++--- .../interfaces/gov/settings/IGovSettings.sol | 5 +- .../gov/user-keeper/IGovUserKeeper.sol | 13 ++++- .../libs/gov/gov-pool/GovPoolRewards.sol | 17 +++++- test/gov/GovPool.test.js | 54 ++++++++++--------- test/gov/GovUserKeeper.test.js | 5 +- 8 files changed, 85 insertions(+), 40 deletions(-) diff --git a/contracts/gov/GovPool.sol b/contracts/gov/GovPool.sol index aa5cfabc..b5262ffb 100644 --- a/contracts/gov/GovPool.sol +++ b/contracts/gov/GovPool.sol @@ -254,7 +254,7 @@ contract GovPool is _unlock(msg.sender); - _govUserKeeper.redeemTokens(msg.sender, receiver, amount); + _govUserKeeper.redeemTokens(msg.sender, receiver, amount, coreProperties); } function delegate( diff --git a/contracts/gov/settings/GovSettings.sol b/contracts/gov/settings/GovSettings.sol index 50cc0853..45ec410e 100644 --- a/contracts/gov/settings/GovSettings.sol +++ b/contracts/gov/settings/GovSettings.sol @@ -98,7 +98,8 @@ contract GovSettings is IGovSettings, OwnableUpgradeable { function createNewStaking( uint64 lockTime, uint256 rewardMultiplier, - uint256 redeemPenalty + uint256 redeemPenalty, + bool allowStakingUpgrade ) external override onlyOwner { require( lockTime > 0 && @@ -108,7 +109,13 @@ contract GovSettings is IGovSettings, OwnableUpgradeable { ); uint256 id = ++totalStakes; - stakingList[id] = StakingInfo(lockTime, rewardMultiplier, redeemPenalty, false); + stakingList[id] = StakingInfo( + lockTime, + rewardMultiplier, + redeemPenalty, + allowStakingUpgrade, + false + ); } function closeStaking(uint256 id) external override onlyOwner { diff --git a/contracts/gov/user-keeper/GovUserKeeper.sol b/contracts/gov/user-keeper/GovUserKeeper.sol index 155db83d..26072e58 100644 --- a/contracts/gov/user-keeper/GovUserKeeper.sol +++ b/contracts/gov/user-keeper/GovUserKeeper.sol @@ -22,6 +22,7 @@ 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"; @@ -125,7 +126,8 @@ contract GovUserKeeper is IGovUserKeeper, OwnableUpgradeable, ERC721HolderUpgrad function redeemTokens( address payer, address receiver, - uint256 amount + uint256 amount, + ICoreProperties coreProperties ) external override onlyOwner withSupportedToken { require(amount > 0, "GovUK: empty redeem"); require(_isStaked(payer), "GovUK: not staked"); @@ -137,10 +139,14 @@ contract GovUserKeeper is IGovUserKeeper, OwnableUpgradeable, ERC721HolderUpgrad uint256 redeemPenalty = stakeInfo.redeemPenalty; require(redeemPenalty != type(uint256).max, "GovUK: redeem forbidden"); - uint256 redeemToGovpoolAmount = amount.percentage(redeemPenalty); + uint256 redeemToGovPoolAmount = amount.percentage(redeemPenalty); + + (uint256 commission, address dexeTreasury) = coreProperties.getDEXECommissionPercentages(); + uint256 redeemToDexeAmount = redeemToGovPoolAmount.percentage(commission); - _sendNativeOrToken(owner(), redeemToGovpoolAmount); - _sendNativeOrToken(receiver, amount - redeemToGovpoolAmount); + _sendNativeOrToken(dexeTreasury, redeemToDexeAmount); + _sendNativeOrToken(owner(), redeemToGovPoolAmount - redeemToDexeAmount); + _sendNativeOrToken(receiver, amount - redeemToGovPoolAmount); } function delegateTokens( @@ -516,7 +522,7 @@ contract GovUserKeeper is IGovUserKeeper, OwnableUpgradeable, ERC721HolderUpgrad IGovSettings.StakingInfo memory oldStakeInfo = _getStakeInfo(userStake.stakeId); - if (oldStakeInfo.lockTime < newStakeInfo.lockTime) { + if (oldStakeInfo.lockTime < newStakeInfo.lockTime && oldStakeInfo.allowStakingUpgrade) { _stake(userStake, id); return; } @@ -800,7 +806,7 @@ contract GovUserKeeper is IGovUserKeeper, OwnableUpgradeable, ERC721HolderUpgrad nativeAmount -= value; } - function getStakingMultiplier(address user) public view returns (uint256) { + function getStakingMultiplier(address user) public view override returns (uint256) { Stake storage currentStake = _stakes[user]; uint256 id = currentStake.stakeId; diff --git a/contracts/interfaces/gov/settings/IGovSettings.sol b/contracts/interfaces/gov/settings/IGovSettings.sol index 6a381dd0..da3d2b43 100644 --- a/contracts/interfaces/gov/settings/IGovSettings.sol +++ b/contracts/interfaces/gov/settings/IGovSettings.sol @@ -55,11 +55,13 @@ interface IGovSettings { /// @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; } @@ -95,7 +97,8 @@ interface IGovSettings { function createNewStaking( uint64 lockTime, uint256 rewardMultiplier, - uint256 redeemPenalty + uint256 redeemPenalty, + bool allowStakingUpgrade ) external; /// @notice Disables active staking diff --git a/contracts/interfaces/gov/user-keeper/IGovUserKeeper.sol b/contracts/interfaces/gov/user-keeper/IGovUserKeeper.sol index 05ef9406..287995e6 100644 --- a/contracts/interfaces/gov/user-keeper/IGovUserKeeper.sol +++ b/contracts/interfaces/gov/user-keeper/IGovUserKeeper.sol @@ -114,7 +114,13 @@ interface IGovUserKeeper { /// @param payer the address from whom to redeem the tokens /// @param receiver the redeem receiver address /// @param amount the erc20 redeeming amount - function redeemTokens(address payer, address receiver, uint256 amount) external; + /// @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 @@ -378,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/test/gov/GovPool.test.js b/test/gov/GovPool.test.js index 3e7d6867..0fa38312 100644 --- a/test/gov/GovPool.test.js +++ b/test/gov/GovPool.test.js @@ -816,22 +816,22 @@ describe("GovPool", () => { }); it("must be govPool to create staking", async () => { - await truffleAssert.reverts(settings.createNewStaking(1, 1, 0), "Ownable: caller is not the owner"); + 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, { from: govPool.address }), + settings.createNewStaking(0, 1, 0, false, { from: govPool.address }), "GovSettings: wrong staking info", ); await truffleAssert.reverts( - settings.createNewStaking(1, 0, 0, { from: govPool.address }), + settings.createNewStaking(1, 0, 0, false, { from: govPool.address }), "GovSettings: wrong staking info", ); await truffleAssert.reverts( - settings.createNewStaking(1, 1, wei("1000000001"), { from: govPool.address }), + settings.createNewStaking(1, 1, wei("1000000001"), false, { from: govPool.address }), "GovSettings: wrong staking info", ); }); @@ -840,23 +840,23 @@ describe("GovPool", () => { assert.equal(await settings.totalStakes(), 0); await truffleAssert.reverts(settings.getStakingSettings(1), "GovSettings: invalid id"); - await settings.createNewStaking(1, 1, 0, { from: govPool.address }); + await settings.createNewStaking(1, 1, 0, false, { from: govPool.address }); assert.equal(await settings.totalStakes(), 1); - await settings.createNewStaking(2, 2, 1, { from: govPool.address }); + 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]); - assert.deepEqual(list[1], ["2", "2", "1", false]); + 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, { from: govPool.address }); - assert.deepEqual(await settings.getStakingSettings(1), ["1", "1", "0", false]); + 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", true]); + assert.deepEqual(await settings.getStakingSettings(1), ["1", "1", "0", false, true]); }); it("can't disable invalid staking", async () => { @@ -879,15 +879,15 @@ describe("GovPool", () => { }); it("cant stake on disabled tier", async () => { - await settings.createNewStaking(5, 1, 0, { from: govPool.address }); + 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, { from: govPool.address }); + await settings.createNewStaking(10, 1, 0, false, { from: govPool.address }); await userKeeper.stake(1); - await settings.createNewStaking(5, 1, 0, { from: govPool.address }); + await settings.createNewStaking(5, 1, 0, false, { from: govPool.address }); await truffleAssert.reverts(userKeeper.stake(2), "GovUK: Already staked"); }); @@ -897,7 +897,7 @@ describe("GovPool", () => { await setTime((await getCurrentBlockTime()) + 1); await govPool.withdraw.call(OWNER, wei("100"), [1, 2, 3]); - await settings.createNewStaking(5, 1, 0, { from: govPool.address }); + 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"); @@ -906,7 +906,7 @@ describe("GovPool", () => { it("could withdraw from disabled tier", async () => { await govPool.deposit(wei("100"), []); - await settings.createNewStaking(100, 1, 0, { from: govPool.address }); + await settings.createNewStaking(100, 1, 0, false, { from: govPool.address }); await userKeeper.stake(1); await truffleAssert.reverts(govPool.withdraw(OWNER, wei("100"), []), "GovUK: staked"); @@ -918,7 +918,7 @@ describe("GovPool", () => { it("could withdraw after staking end", async () => { await govPool.deposit(wei("100"), []); - await settings.createNewStaking(100, 1, 0, { from: govPool.address }); + await settings.createNewStaking(100, 1, 0, false, { from: govPool.address }); await userKeeper.stake(1); await truffleAssert.reverts(govPool.withdraw(OWNER, wei("100"), []), "GovUK: staked"); @@ -930,40 +930,40 @@ describe("GovPool", () => { it("could restake after staking end", async () => { await govPool.deposit(wei("100"), []); - await settings.createNewStaking(100, 1, 0, { from: govPool.address }); + 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, { from: govPool.address }); + 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, { from: govPool.address }); + 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, { from: govPool.address }); + 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")), + 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, { from: govPool.address }); + await settings.createNewStaking(100, 1, 50, false, { from: govPool.address }); await userKeeper.stake(1); await truffleAssert.reverts(govPool.withdraw(OWNER, wei("100"), []), "GovUK: staked"); @@ -978,7 +978,7 @@ describe("GovPool", () => { it("cant redeem when redeem forbidden", async () => { await govPool.deposit(wei("100"), []); - await settings.createNewStaking(100, 1, MAX_UINT, { from: govPool.address }); + 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"); @@ -986,15 +986,17 @@ describe("GovPool", () => { it("could redeem", async () => { await govPool.deposit(wei("100"), []); - await settings.createNewStaking(100, 1, PERCENTAGE_50, { from: govPool.address }); + 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("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()); }); }); diff --git a/test/gov/GovUserKeeper.test.js b/test/gov/GovUserKeeper.test.js index de43ef2c..db9233d0 100644 --- a/test/gov/GovUserKeeper.test.js +++ b/test/gov/GovUserKeeper.test.js @@ -1053,7 +1053,10 @@ 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")), "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")), From 8bee86d24860c021fa1c4c9416e18434d6ee1a25 Mon Sep 17 00:00:00 2001 From: Dmitry Redkin Date: Thu, 15 Aug 2024 13:22:54 +0300 Subject: [PATCH 5/5] babt --- contracts/gov/GovPool.sol | 2 +- contracts/gov/settings/GovSettings.sol | 12 +++++++----- test/gov/GovPool.test.js | 10 +++++++--- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/contracts/gov/GovPool.sol b/contracts/gov/GovPool.sol index b5262ffb..48c5a7ed 100644 --- a/contracts/gov/GovPool.sol +++ b/contracts/gov/GovPool.sol @@ -249,7 +249,7 @@ contract GovPool is emit Withdrawn(amount, nftIds, receiver); } - function redeemTokens(address receiver, uint256 amount) external { + function redeemTokens(address receiver, uint256 amount) external override onlyBABTHolder { _checkBlock(DEPOSIT_WITHDRAW, msg.sender); _unlock(msg.sender); diff --git a/contracts/gov/settings/GovSettings.sol b/contracts/gov/settings/GovSettings.sol index 45ec410e..a7124ae6 100644 --- a/contracts/gov/settings/GovSettings.sol +++ b/contracts/gov/settings/GovSettings.sol @@ -20,6 +20,11 @@ contract GovSettings is IGovSettings, OwnableUpgradeable { 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, @@ -118,9 +123,7 @@ contract GovSettings is IGovSettings, OwnableUpgradeable { ); } - function closeStaking(uint256 id) external override onlyOwner { - require(id > 0 && id <= totalStakes, "GovSettings: invalid staking id"); - + function closeStaking(uint256 id) external override onlyOwner withCorrectStakingId(id) { StakingInfo storage stake = stakingList[id]; stake.disabled = true; } @@ -155,8 +158,7 @@ contract GovSettings is IGovSettings, OwnableUpgradeable { function getStakingSettings( uint256 id - ) public view override returns (StakingInfo memory stakingInfo) { - require(id != 0 && id <= totalStakes, "GovSettings: invalid id"); + ) public view override withCorrectStakingId(id) returns (StakingInfo memory stakingInfo) { stakingInfo = stakingList[id]; } diff --git a/test/gov/GovPool.test.js b/test/gov/GovPool.test.js index 0fa38312..24b8044c 100644 --- a/test/gov/GovPool.test.js +++ b/test/gov/GovPool.test.js @@ -838,7 +838,7 @@ describe("GovPool", () => { it("could create staking", async () => { assert.equal(await settings.totalStakes(), 0); - await truffleAssert.reverts(settings.getStakingSettings(1), "GovSettings: invalid id"); + 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); @@ -871,11 +871,11 @@ describe("GovPool", () => { }); it("cant stake on zero tier", async () => { - await truffleAssert.reverts(userKeeper.stake(0), "GovSettings: invalid id"); + 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 id"); + await truffleAssert.reverts(userKeeper.stake(2), "GovSettings: invalid staking id"); }); it("cant stake on disabled tier", async () => { @@ -6104,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); });