From a9d3baef03ba81a9e583cef38803df4606032697 Mon Sep 17 00:00:00 2001 From: Viktoriia Zotova Date: Wed, 5 Feb 2025 18:58:33 -0500 Subject: [PATCH 01/10] Block new stakes, top-ups, authorization increases, slashing --- contracts/staking/IStaking.sol | 81 - contracts/staking/TokenStaking.sol | 468 +---- contracts/test/TokenStakingTestSet.sol | 134 +- test/staking/TokenStaking.test.js | 2539 +++--------------------- 4 files changed, 389 insertions(+), 2833 deletions(-) diff --git a/contracts/staking/IStaking.sol b/contracts/staking/IStaking.sol index e15f1126..e1b36c40 100644 --- a/contracts/staking/IStaking.sol +++ b/contracts/staking/IStaking.sol @@ -33,18 +33,6 @@ interface IStaking { // // - /// @notice Creates a delegation with `msg.sender` owner with the given - /// staking provider, beneficiary, and authorizer. Transfers the - /// given amount of T to the staking contract. - /// @dev The owner of the delegation needs to have the amount approved to - /// transfer to the staking contract. - function stake( - address stakingProvider, - address payable beneficiary, - address authorizer, - uint96 amount - ) external; - /// @notice Allows the Governance to set the minimum required stake amount. /// This amount is required to protect against griefing the staking /// contract and individual applications are allowed to require @@ -61,18 +49,6 @@ interface IStaking { /// before individual stake authorizers are able to authorize it. function approveApplication(address application) external; - /// @notice Increases the authorization of the given staking provider for - /// the given application by the given amount. Can only be called by - /// the authorizer for that staking provider. - /// @dev Calls `authorizationIncreased(address stakingProvider, uint256 amount)` - /// on the given application to notify the application about - /// authorization change. See `IApplication`. - function increaseAuthorization( - address stakingProvider, - address application, - uint96 amount - ) external; - /// @notice Requests decrease of the authorization for the given staking /// provider on the given application by the provided amount. /// It may not change the authorized amount immediatelly. When @@ -145,23 +121,6 @@ interface IStaking { /// Can only be called by the Governance. function setAuthorizationCeiling(uint256 ceiling) external; - // - // - // Stake top-up - // - // - - /// @notice Increases the amount of the stake for the given staking provider. - /// If `autoIncrease` flag is true then the amount will be added for - /// all authorized applications. - /// @dev The sender of this transaction needs to have the amount approved to - /// transfer to the staking contract. - function topUp(address stakingProvider, uint96 amount) external; - - /// @notice Toggle `autoIncrease` flag. If true then the complete amount - /// in top-up will be added to already authorized applications. - function toggleAutoAuthorizationIncrease(address stakingProvider) external; - // // // Undelegating a stake (unstaking) @@ -182,42 +141,11 @@ interface IStaking { // // - /// @notice Sets reward in T tokens for notification of misbehaviour - /// of one staking provider. Can only be called by the governance. - function setNotificationReward(uint96 reward) external; - - /// @notice Transfer some amount of T tokens as reward for notifications - /// of misbehaviour - function pushNotificationReward(uint96 reward) external; - /// @notice Withdraw some amount of T tokens from notifiers treasury. /// Can only be called by the governance. function withdrawNotificationReward(address recipient, uint96 amount) external; - /// @notice Adds staking providers to the slashing queue along with the - /// amount that should be slashed from each one of them. Can only be - /// called by application authorized for all staking providers in - /// the array. - function slash(uint96 amount, address[] memory stakingProviders) external; - - /// @notice Adds staking providers to the slashing queue along with the - /// amount. The notifier will receive reward per each staking - /// provider from notifiers treasury. Can only be called by - /// application authorized for all staking providers in the array. - function seize( - uint96 amount, - uint256 rewardMultipier, - address notifier, - address[] memory stakingProviders - ) external; - - /// @notice Takes the given number of queued slashing operations and - /// processes them. Receives 5% of the slashed amount. - /// Executes `involuntaryAllocationDecrease` function on each - /// affected application. - function processSlashing(uint256 count) external; - // // // Auxiliary functions @@ -244,12 +172,6 @@ interface IStaking { view returns (uint256); - /// @notice Returns auto-increase flag. - function getAutoIncreaseFlag(address stakingProvider) - external - view - returns (bool); - /// @notice Gets the stake owner, the beneficiary and the authorizer /// for the specified staking provider address. /// @return owner Stake owner address. @@ -267,9 +189,6 @@ interface IStaking { /// @notice Returns length of application array function getApplicationsLength() external view returns (uint256); - /// @notice Returns length of slashing queue - function getSlashingQueueLength() external view returns (uint256); - /// @notice Returns the maximum application authorization function getMaxAuthorization(address stakingProvider) external diff --git a/contracts/staking/TokenStaking.sol b/contracts/staking/TokenStaking.sol index 8ab19625..d05665db 100644 --- a/contracts/staking/TokenStaking.sol +++ b/contracts/staking/TokenStaking.sol @@ -98,34 +98,24 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { uint256 private legacyStakeDiscrepancyRewardMultiplier; uint256 public notifiersTreasury; - uint256 public notificationReward; + // slither-disable-next-line constable-states + uint256 private legacyNotificationReward; mapping(address => StakingProviderInfo) internal stakingProviders; mapping(address => ApplicationInfo) public applicationInfo; address[] public applications; - SlashingEvent[] public slashingQueue; - uint256 public slashingQueueIndex; + // slither-disable-next-line constable-states + SlashingEvent[] private legacySlashingQueue; + // slither-disable-next-line constable-states + uint256 private legacySlashingQueueIndex; + - event Staked( - StakeType indexed stakeType, - address indexed owner, - address indexed stakingProvider, - address beneficiary, - address authorizer, - uint96 amount - ); event MinimumStakeAmountSet(uint96 amount); event ApplicationStatusChanged( address indexed application, ApplicationStatus indexed newStatus ); - event AuthorizationIncreased( - address indexed stakingProvider, - address indexed application, - uint96 fromAmount, - uint96 toAmount - ); event AuthorizationDecreaseRequested( address indexed stakingProvider, address indexed application, @@ -150,26 +140,8 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { address indexed panicButton ); event AuthorizationCeilingSet(uint256 ceiling); - event ToppedUp(address indexed stakingProvider, uint96 amount); - event AutoIncreaseToggled( - address indexed stakingProvider, - bool autoIncrease - ); event Unstaked(address indexed stakingProvider, uint96 amount); - event TokensSeized( - address indexed stakingProvider, - uint96 amount, - bool indexed discrepancy - ); - event NotificationRewardSet(uint96 reward); - event NotificationRewardPushed(uint96 reward); event NotificationRewardWithdrawn(address recipient, uint96 amount); - event NotifierRewarded(address indexed notifier, uint256 amount); - event SlashingProcessed( - address indexed caller, - uint256 count, - uint256 tAmount - ); event GovernanceTransferred(address oldGovernance, address newGovernance); modifier onlyGovernance() { @@ -232,55 +204,6 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { // // - /// @notice Creates a delegation with `msg.sender` owner with the given - /// staking provider, beneficiary, and authorizer. Transfers the - /// given amount of T to the staking contract. - /// @dev The owner of the delegation needs to have the amount approved to - /// transfer to the staking contract. - function stake( - address stakingProvider, - address payable beneficiary, - address authorizer, - uint96 amount - ) external override { - require( - stakingProvider != address(0) && - beneficiary != address(0) && - authorizer != address(0), - "Parameters must be specified" - ); - StakingProviderInfo storage stakingProviderStruct = stakingProviders[ - stakingProvider - ]; - require( - stakingProviderStruct.owner == address(0), - "Provider is already in use" - ); - require( - amount > 0 && amount >= minTStakeAmount, - "Amount is less than minimum" - ); - stakingProviderStruct.owner = msg.sender; - stakingProviderStruct.authorizer = authorizer; - stakingProviderStruct.beneficiary = beneficiary; - - stakingProviderStruct.tStake = amount; - /* solhint-disable-next-line not-rely-on-time */ - stakingProviderStruct.startStakingTimestamp = block.timestamp; - - increaseStakeCheckpoint(stakingProvider, amount); - - emit Staked( - StakeType.T, - msg.sender, - stakingProvider, - beneficiary, - authorizer, - amount - ); - token.safeTransferFrom(msg.sender, address(this), amount); - } - /// @notice Allows the Governance to set the minimum required stake amount. /// This amount is required to protect against griefing the staking /// contract and individual applications are allowed to require @@ -328,61 +251,6 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { emit ApplicationStatusChanged(application, ApplicationStatus.APPROVED); } - /// @notice Increases the authorization of the given staking provider for - /// the given application by the given amount. Can only be called by - /// the given staking provider’s authorizer. - /// @dev Calls `authorizationIncreased` callback on the given application to - /// notify the application about authorization change. - /// See `IApplication`. - function increaseAuthorization( - address stakingProvider, - address application, - uint96 amount - ) external override onlyAuthorizerOf(stakingProvider) { - require(amount > 0, "Parameters must be specified"); - ApplicationInfo storage applicationStruct = applicationInfo[ - application - ]; - require( - applicationStruct.status == ApplicationStatus.APPROVED, - "Application is not approved" - ); - - StakingProviderInfo storage stakingProviderStruct = stakingProviders[ - stakingProvider - ]; - AppAuthorization storage authorization = stakingProviderStruct - .authorizations[application]; - uint96 fromAmount = authorization.authorized; - if (fromAmount == 0) { - require( - authorizationCeiling == 0 || - stakingProviderStruct.authorizedApplications.length < - authorizationCeiling, - "Too many applications" - ); - stakingProviderStruct.authorizedApplications.push(application); - } - - uint96 availableTValue = getAvailableToAuthorize( - stakingProvider, - application - ); - require(availableTValue >= amount, "Not enough stake to authorize"); - authorization.authorized += amount; - emit AuthorizationIncreased( - stakingProvider, - application, - fromAmount, - authorization.authorized - ); - IApplication(application).authorizationIncreased( - stakingProvider, - fromAmount, - authorization.authorized - ); - } - /// @notice Requests decrease of all authorizations for the given staking /// provider on all applications by all authorized amount. /// It may not change the authorized amount immediatelly. When @@ -570,79 +438,6 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { emit AuthorizationCeilingSet(ceiling); } - // - // - // Stake top-up - // - // - - /// @notice Increases the amount of the stake for the given staking provider. - /// If `autoIncrease` flag is true then the amount will be added for - /// all authorized applications. - /// @dev The sender of this transaction needs to have the amount approved to - /// transfer to the staking contract. - function topUp(address stakingProvider, uint96 amount) external override { - require( - stakingProviders[stakingProvider].owner != address(0), - "Nothing to top-up" - ); - require(amount > 0, "Parameters must be specified"); - StakingProviderInfo storage stakingProviderStruct = stakingProviders[ - stakingProvider - ]; - stakingProviderStruct.tStake += amount; - emit ToppedUp(stakingProvider, amount); - increaseStakeCheckpoint(stakingProvider, amount); - token.safeTransferFrom(msg.sender, address(this), amount); - - if (!stakingProviderStruct.autoIncrease) { - return; - } - - // increase authorization for all authorized app - for ( - uint256 i = 0; - i < stakingProviderStruct.authorizedApplications.length; - i++ - ) { - address application = stakingProviderStruct.authorizedApplications[ - i - ]; - AppAuthorization storage authorization = stakingProviderStruct - .authorizations[application]; - uint96 fromAmount = authorization.authorized; - authorization.authorized += amount; - emit AuthorizationIncreased( - stakingProvider, - application, - fromAmount, - authorization.authorized - ); - IApplication(application).authorizationIncreased( - stakingProvider, - fromAmount, - authorization.authorized - ); - } - } - - /// @notice Toggle `autoIncrease` flag. If true then the complete amount - /// in top-up will be added to already authorized applications. - function toggleAutoAuthorizationIncrease(address stakingProvider) - external - override - onlyAuthorizerOf(stakingProvider) - { - StakingProviderInfo storage stakingProviderStruct = stakingProviders[ - stakingProvider - ]; - stakingProviderStruct.autoIncrease = !stakingProviderStruct - .autoIncrease; - emit AutoIncreaseToggled( - stakingProvider, - stakingProviderStruct.autoIncrease - ); - } // // @@ -689,26 +484,6 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { // // - /// @notice Sets reward in T tokens for notification of misbehaviour - /// of one staking provider. Can only be called by the governance. - function setNotificationReward(uint96 reward) - external - override - onlyGovernance - { - notificationReward = reward; - emit NotificationRewardSet(reward); - } - - /// @notice Transfer some amount of T tokens as reward for notifications - /// of misbehaviour - function pushNotificationReward(uint96 reward) external override { - require(reward > 0, "Parameters must be specified"); - notifiersTreasury += reward; - emit NotificationRewardPushed(reward); - token.safeTransferFrom(msg.sender, address(this), reward); - } - /// @notice Withdraw some amount of T tokens from notifiers treasury. /// Can only be called by the governance. function withdrawNotificationReward(address recipient, uint96 amount) @@ -722,67 +497,6 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { token.safeTransfer(recipient, amount); } - /// @notice Adds staking providers to the slashing queue along with the - /// amount that should be slashed from each one of them. Can only be - /// called by application authorized for all staking providers in - /// the array. - /// @dev This method doesn't emit events for providers that are added to - /// the queue. If necessary events can be added to the application - /// level. - function slash(uint96 amount, address[] memory _stakingProviders) - external - override - { - notify(amount, 0, address(0), _stakingProviders); - } - - /// @notice Adds staking providers to the slashing queue along with the - /// amount. The notifier will receive reward per each provider from - /// notifiers treasury. Can only be called by application - /// authorized for all staking providers in the array. - /// @dev This method doesn't emit events for staking providers that are - /// added to the queue. If necessary events can be added to the - /// application level. - function seize( - uint96 amount, - uint256 rewardMultiplier, - address notifier, - address[] memory _stakingProviders - ) external override { - notify(amount, rewardMultiplier, notifier, _stakingProviders); - } - - /// @notice Takes the given number of queued slashing operations and - /// processes them. Receives 5% of the slashed amount. - /// Executes `involuntaryAuthorizationDecrease` function on each - /// affected application. - function processSlashing(uint256 count) external virtual override { - require( - slashingQueueIndex < slashingQueue.length && count > 0, - "Nothing to process" - ); - - uint256 maxIndex = slashingQueueIndex + count; - maxIndex = MathUpgradeable.min(maxIndex, slashingQueue.length); - count = maxIndex - slashingQueueIndex; - uint96 tAmountToBurn = 0; - - uint256 index = slashingQueueIndex; - for (; index < maxIndex; index++) { - SlashingEvent storage slashing = slashingQueue[index]; - tAmountToBurn += processSlashing(slashing); - } - slashingQueueIndex = index; - - uint256 tProcessorReward = uint256(tAmountToBurn).percent( - SLASHING_REWARD_PERCENT - ); - notifiersTreasury += tAmountToBurn - tProcessorReward.toUint96(); - emit SlashingProcessed(msg.sender, count, tProcessorReward); - if (tProcessorReward > 0) { - token.safeTransfer(msg.sender, tProcessorReward); - } - } /// @notice Delegate voting power from the stake associated to the /// `stakingProvider` to a `delegatee` address. Caller must be the @@ -859,16 +573,6 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { return stakingProviders[stakingProvider].startStakingTimestamp; } - /// @notice Returns auto-increase flag. - function getAutoIncreaseFlag(address stakingProvider) - external - view - override - returns (bool) - { - return stakingProviders[stakingProvider].autoIncrease; - } - /// @notice Gets the stake owner, the beneficiary and the authorizer /// for the specified staking provider address. /// @return owner Stake owner address. @@ -897,11 +601,6 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { return applications.length; } - /// @notice Returns length of slashing queue - function getSlashingQueueLength() external view override returns (uint256) { - return slashingQueue.length; - } - /// @notice Requests decrease of the authorization for the given staking /// provider on the given application by the provided amount. /// It may not change the authorized amount immediatelly. When @@ -1021,150 +720,6 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { moveVotingPower(oldDelegatee, delegatee, stakingProviderBalance); } - /// @notice Adds staking providers to the slashing queue along with the - /// amount. The notifier will receive reward per each staking - /// provider from notifiers treasury. Can only be called by - /// application authorized for all staking providers in the array. - function notify( - uint96 amount, - uint256 rewardMultiplier, - address notifier, - address[] memory _stakingProviders - ) internal { - require( - amount > 0 && _stakingProviders.length > 0, - "Parameters must be specified" - ); - - ApplicationInfo storage applicationStruct = applicationInfo[msg.sender]; - require( - applicationStruct.status == ApplicationStatus.APPROVED, - "Application is not approved" - ); - - uint256 queueLength = slashingQueue.length; - for (uint256 i = 0; i < _stakingProviders.length; i++) { - address stakingProvider = _stakingProviders[i]; - uint256 amountToSlash = MathUpgradeable.min( - stakingProviders[stakingProvider] - .authorizations[msg.sender] - .authorized, - amount - ); - if ( - //slither-disable-next-line incorrect-equality - amountToSlash == 0 - ) { - continue; - } - slashingQueue.push( - SlashingEvent(stakingProvider, amountToSlash.toUint96()) - ); - } - - if (notifier != address(0)) { - uint256 reward = ((slashingQueue.length - queueLength) * - notificationReward).percent(rewardMultiplier); - reward = MathUpgradeable.min(reward, notifiersTreasury); - emit NotifierRewarded(notifier, reward); - if (reward != 0) { - notifiersTreasury -= reward; - token.safeTransfer(notifier, reward); - } - } - } - - /// @notice Processes one specified slashing event. - /// Executes `involuntaryAuthorizationDecrease` function on each - /// affected application. - //slither-disable-next-line dead-code - function processSlashing(SlashingEvent storage slashing) - internal - returns (uint96 tAmountToBurn) - { - StakingProviderInfo storage stakingProviderStruct = stakingProviders[ - slashing.stakingProvider - ]; - uint96 tAmountToSlash = slashing.amount; - uint96 oldStake = stakingProviderStruct.tStake; - // slash T - tAmountToBurn = MathUpgradeable - .min(tAmountToSlash, stakingProviderStruct.tStake) - .toUint96(); - stakingProviderStruct.tStake -= tAmountToBurn; - tAmountToSlash -= tAmountToBurn; - - uint96 slashedAmount = slashing.amount - tAmountToSlash; - emit TokensSeized(slashing.stakingProvider, slashedAmount, false); - authorizationDecrease( - slashing.stakingProvider, - stakingProviderStruct, - slashedAmount - ); - uint96 newStake = stakingProviderStruct.tStake; - decreaseStakeCheckpoint(slashing.stakingProvider, oldStake - newStake); - } - - /// @notice Synchronize authorizations (if needed) after slashing stake - //slither-disable-next-line dead-code - function authorizationDecrease( - address stakingProvider, - StakingProviderInfo storage stakingProviderStruct, - uint96 slashedAmount - ) internal { - uint96 totalStake = stakingProviderStruct.tStake; - uint256 applicationsToDelete = 0; - for ( - uint256 i = 0; - i < stakingProviderStruct.authorizedApplications.length; - i++ - ) { - address authorizedApplication = stakingProviderStruct - .authorizedApplications[i]; - AppAuthorization storage authorization = stakingProviderStruct - .authorizations[authorizedApplication]; - uint96 fromAmount = authorization.authorized; - - authorization.authorized -= MathUpgradeable - .min(fromAmount, slashedAmount) - .toUint96(); - - if (authorization.authorized > totalStake) { - authorization.authorized = totalStake; - } - - bool successful = true; - //slither-disable-next-line calls-loop - try - IApplication(authorizedApplication) - .involuntaryAuthorizationDecrease{ - gas: GAS_LIMIT_AUTHORIZATION_DECREASE - }(stakingProvider, fromAmount, authorization.authorized) - {} catch { - successful = false; - } - if (authorization.deauthorizing > authorization.authorized) { - authorization.deauthorizing = authorization.authorized; - } - emit AuthorizationInvoluntaryDecreased( - stakingProvider, - authorizedApplication, - fromAmount, - authorization.authorized, - successful - ); - if (authorization.authorized == 0) { - applicationsToDelete++; - } - } - if (applicationsToDelete > 0) { - cleanAuthorizedApplications( - stakingProviderStruct, - applicationsToDelete - ); - } - } - /// @notice Removes application with zero authorization from authorized /// applications array function cleanAuthorizedApplications( @@ -1232,15 +787,6 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { } } - /// @notice Creates new checkpoints due to an increment of a stakers' stake - /// @param _delegator Address of the staking provider acting as delegator - /// @param _amount Amount of T to increment - function increaseStakeCheckpoint(address _delegator, uint96 _amount) - internal - { - newStakeCheckpoint(_delegator, _amount, true); - } - /// @notice Creates new checkpoints due to a decrease of a stakers' stake /// @param _delegator Address of the stake owner acting as delegator /// @param _amount Amount of T to decrease diff --git a/contracts/test/TokenStakingTestSet.sol b/contracts/test/TokenStakingTestSet.sol index eb54e0b9..fbee0175 100644 --- a/contracts/test/TokenStakingTestSet.sol +++ b/contracts/test/TokenStakingTestSet.sol @@ -58,24 +58,6 @@ contract ApplicationMock is IApplication { .approveAuthorizationDecrease(stakingProvider); } - function slash(uint96 amount, address[] memory _stakingProviders) external { - tokenStaking.slash(amount, _stakingProviders); - } - - function seize( - uint96 amount, - uint256 rewardMultiplier, - address notifier, - address[] memory _stakingProviders - ) external { - tokenStaking.seize( - amount, - rewardMultiplier, - notifier, - _stakingProviders - ); - } - function availableRewards(address) external pure returns (uint96) { return 0; } @@ -149,6 +131,9 @@ contract ManagedGrantMock { } contract ExtendedTokenStaking is TokenStaking { + using SafeTUpgradeable for T; + + /// @custom:oz-upgrades-unsafe-allow constructor constructor(T _token) TokenStaking(_token) {} function cleanAuthorizedApplications( @@ -179,9 +164,6 @@ contract ExtendedTokenStaking is TokenStaking { .authorizedApplications = _applications; } - // to decrease size of test contract - function processSlashing(uint256 count) external override {} - function getAuthorizedApplications(address stakingProvider) external view @@ -189,4 +171,114 @@ contract ExtendedTokenStaking is TokenStaking { { return stakingProviders[stakingProvider].authorizedApplications; } + + /// @notice Creates a delegation with `msg.sender` owner with the given + /// staking provider, beneficiary, and authorizer. Transfers the + /// given amount of T to the staking contract. + /// @dev The owner of the delegation needs to have the amount approved to + /// transfer to the staking contract. + function stake( + address stakingProvider, + address payable beneficiary, + address authorizer, + uint96 amount + ) external { + require( + stakingProvider != address(0) && + beneficiary != address(0) && + authorizer != address(0), + "Parameters must be specified" + ); + StakingProviderInfo storage stakingProviderStruct = stakingProviders[ + stakingProvider + ]; + require( + stakingProviderStruct.owner == address(0), + "Provider is already in use" + ); + require( + amount > 0 && amount >= minTStakeAmount, + "Amount is less than minimum" + ); + stakingProviderStruct.owner = msg.sender; + stakingProviderStruct.authorizer = authorizer; + stakingProviderStruct.beneficiary = beneficiary; + + stakingProviderStruct.tStake = amount; + /* solhint-disable-next-line not-rely-on-time */ + stakingProviderStruct.startStakingTimestamp = block.timestamp; + + increaseStakeCheckpoint(stakingProvider, amount); + + token.safeTransferFrom(msg.sender, address(this), amount); + } + + + /// @notice Increases the authorization of the given staking provider for + /// the given application by the given amount. Can only be called by + /// the given staking provider’s authorizer. + /// @dev Calls `authorizationIncreased` callback on the given application to + /// notify the application about authorization change. + /// See `IApplication`. + function increaseAuthorization( + address stakingProvider, + address application, + uint96 amount + ) external onlyAuthorizerOf(stakingProvider) { + require(amount > 0, "Parameters must be specified"); + ApplicationInfo storage applicationStruct = applicationInfo[ + application + ]; + require( + applicationStruct.status == ApplicationStatus.APPROVED, + "Application is not approved" + ); + + StakingProviderInfo storage stakingProviderStruct = stakingProviders[ + stakingProvider + ]; + AppAuthorization storage authorization = stakingProviderStruct + .authorizations[application]; + uint96 fromAmount = authorization.authorized; + if (fromAmount == 0) { + require( + authorizationCeiling == 0 || + stakingProviderStruct.authorizedApplications.length < + authorizationCeiling, + "Too many applications" + ); + stakingProviderStruct.authorizedApplications.push(application); + } + + uint96 availableTValue = getAvailableToAuthorize( + stakingProvider, + application + ); + require(availableTValue >= amount, "Not enough stake to authorize"); + authorization.authorized += amount; + IApplication(application).authorizationIncreased( + stakingProvider, + fromAmount, + authorization.authorized + ); + } + + /// @notice Transfer some amount of T tokens as reward for notifications + /// of misbehaviour + function pushNotificationReward(uint96 reward) external { + require(reward > 0, "Parameters must be specified"); + notifiersTreasury += reward; + token.safeTransferFrom(msg.sender, address(this), reward); + } + + /// @notice Creates new checkpoints due to an increment of a stakers' stake + /// @param _delegator Address of the staking provider acting as delegator + /// @param _amount Amount of T to increment + function increaseStakeCheckpoint(address _delegator, uint96 _amount) + internal + { + newStakeCheckpoint(_delegator, _amount, true); + } + + } diff --git a/test/staking/TokenStaking.test.js b/test/staking/TokenStaking.test.js index bf91e4ee..f04fd3e5 100644 --- a/test/staking/TokenStaking.test.js +++ b/test/staking/TokenStaking.test.js @@ -71,10 +71,10 @@ describe("TokenStaking", () => { .connect(deployer) .transfer(otherStaker.address, initialStakerBalance) - const TokenStaking = await ethers.getContractFactory("TokenStaking") + const ExtendedTokenStaking = await ethers.getContractFactory("ExtendedTokenStaking") const tokenStakingInitializerArgs = [] tokenStaking = await upgrades.deployProxy( - TokenStaking, + ExtendedTokenStaking, tokenStakingInitializerArgs, { constructorArgs: [tToken.address], @@ -119,238 +119,6 @@ describe("TokenStaking", () => { }) }) - describe("stake", () => { - context("when caller did not provide staking provider", () => { - it("should revert", async () => { - amount = 0 - await expect( - tokenStaking - .connect(staker) - .stake(AddressZero, beneficiary.address, authorizer.address, amount) - ).to.be.revertedWith("Parameters must be specified") - }) - }) - - context("when caller did not provide beneficiary", () => { - it("should revert", async () => { - amount = 0 - await expect( - tokenStaking - .connect(staker) - .stake( - stakingProvider.address, - AddressZero, - authorizer.address, - amount - ) - ).to.be.revertedWith("Parameters must be specified") - }) - }) - - context("when caller did not provide authorizer", () => { - it("should revert", async () => { - amount = 0 - await expect( - tokenStaking - .connect(staker) - .stake( - stakingProvider.address, - beneficiary.address, - AddressZero, - amount - ) - ).to.be.revertedWith("Parameters must be specified") - }) - }) - - context("when staking provider is in use", () => { - context( - "when other stake delegated to the specified staking provider", - () => { - it("should revert", async () => { - const amount = initialStakerBalance - await tToken - .connect(otherStaker) - .approve(tokenStaking.address, amount) - await tokenStaking - .connect(otherStaker) - .stake( - stakingProvider.address, - beneficiary.address, - authorizer.address, - amount - ) - await tToken.connect(staker).approve(tokenStaking.address, amount) - await expect( - tokenStaking - .connect(staker) - .stake( - stakingProvider.address, - beneficiary.address, - authorizer.address, - amount - ) - ).to.be.revertedWith("Provider is already in use") - }) - } - ) - }) - - context("when staker delegates too small amount", () => { - context("when amount is zero and minimum amount was not set", () => { - it("should revert", async () => { - amount = 0 - await expect( - tokenStaking - .connect(staker) - .stake( - stakingProvider.address, - beneficiary.address, - authorizer.address, - amount - ) - ).to.be.revertedWith("Amount is less than minimum") - }) - }) - - context("when amount is less than minimum", () => { - it("should revert", async () => { - amount = initialStakerBalance - await tokenStaking - .connect(deployer) - .setMinimumStakeAmount(amount.add(1)) - await tToken.connect(staker).approve(tokenStaking.address, amount) - await expect( - tokenStaking - .connect(staker) - .stake( - stakingProvider.address, - beneficiary.address, - authorizer.address, - amount - ) - ).to.be.revertedWith("Amount is less than minimum") - }) - }) - }) - - context( - "when stake delegates enough tokens to free staking provider", - () => { - const amount = initialStakerBalance - let tx - let blockTimestamp - - beforeEach(async () => { - await tokenStaking - .connect(deployer) - .setMinimumStakeAmount(initialStakerBalance) - await tToken.connect(staker).approve(tokenStaking.address, amount) - tx = await tokenStaking - .connect(staker) - .stake( - stakingProvider.address, - beneficiary.address, - authorizer.address, - amount - ) - blockTimestamp = await lastBlockTime() - }) - - it("should set roles equal to the provided values", async () => { - expect( - await tokenStaking.rolesOf(stakingProvider.address) - ).to.deep.equal([ - staker.address, - beneficiary.address, - authorizer.address, - ]) - }) - - it("should set value of stakes", async () => { - await assertStake(stakingProvider.address, amount) - }) - - it("should start staking timestamp", async () => { - expect( - await tokenStaking.getStartStakingTimestamp(stakingProvider.address) - ).to.equal(blockTimestamp) - }) - - it("should transfer tokens to the staking contract", async () => { - expect(await tToken.balanceOf(tokenStaking.address)).to.equal(amount) - }) - - it("should increase available amount to authorize", async () => { - expect( - await tokenStaking.getAvailableToAuthorize( - stakingProvider.address, - application1Mock.address - ) - ).to.equal(amount) - }) - - it("should emit Staked event", async () => { - await expect(tx) - .to.emit(tokenStaking, "Staked") - .withArgs( - StakeTypes.T, - staker.address, - stakingProvider.address, - beneficiary.address, - authorizer.address, - amount - ) - }) - - it("should create a new checkpoint for staked total supply", async () => { - const lastBlock = await mineBlocks(1) - expect(await tokenStaking.getPastTotalSupply(lastBlock - 1)).to.equal( - amount - ) - }) - it("shouldn't create a new checkpoint for any stake role", async () => { - expect(await tokenStaking.getVotes(staker.address)).to.equal(0) - expect(await tokenStaking.getVotes(stakingProvider.address)).to.equal( - 0 - ) - expect(await tokenStaking.getVotes(beneficiary.address)).to.equal(0) - expect(await tokenStaking.getVotes(authorizer.address)).to.equal(0) - }) - - context("after vote delegation", () => { - beforeEach(async () => { - tx = await tokenStaking - .connect(staker) - .delegateVoting(stakingProvider.address, delegatee.address) - }) - - it("checkpoint for staked total supply should remain constant", async () => { - const lastBlock = await mineBlocks(1) - expect( - await tokenStaking.getPastTotalSupply(lastBlock - 1) - ).to.equal(amount) - }) - - it("should create a new checkpoint for staker's delegatee", async () => { - expect(await tokenStaking.getVotes(delegatee.address)).to.equal( - amount - ) - }) - - it("shouldn't create a new checkpoint for any stake role", async () => { - expect(await tokenStaking.getVotes(staker.address)).to.equal(0) - expect( - await tokenStaking.getVotes(stakingProvider.address) - ).to.equal(0) - expect(await tokenStaking.getVotes(beneficiary.address)).to.equal(0) - expect(await tokenStaking.getVotes(authorizer.address)).to.equal(0) - }) - }) - } - ) - }) - describe("approveApplication", () => { context("when caller is not the governance", () => { it("should revert", async () => { @@ -465,14 +233,14 @@ describe("TokenStaking", () => { }) }) - describe("increaseAuthorization", () => { + describe("requestAuthorizationDecrease", () => { context("when caller is not authorizer", () => { it("should revert", async () => { const amount = initialStakerBalance await expect( tokenStaking .connect(staker) - .increaseAuthorization( + ["requestAuthorizationDecrease(address,address,uint96)"]( stakingProvider.address, application1Mock.address, amount @@ -496,14 +264,31 @@ describe("TokenStaking", () => { authorizer.address, amount ) + await tokenStaking + .connect(deployer) + .approveApplication(application1Mock.address) + await tokenStaking + .connect(authorizer) + .increaseAuthorization( + stakingProvider.address, + application1Mock.address, + amount + ) }) - context("when application was not approved", () => { + context("when application was paused", () => { it("should revert", async () => { + const amount = initialStakerBalance + await tokenStaking + .connect(deployer) + .setPanicButton(application1Mock.address, panicButton.address) + await tokenStaking + .connect(panicButton) + .pauseApplication(application1Mock.address) await expect( tokenStaking .connect(authorizer) - .increaseAuthorization( + ["requestAuthorizationDecrease(address,address,uint96)"]( stakingProvider.address, application1Mock.address, amount @@ -512,426 +297,64 @@ describe("TokenStaking", () => { }) }) - context("when application was approved", () => { - beforeEach(async () => { + context("when application is disabled", () => { + it("should revert", async () => { await tokenStaking .connect(deployer) - .approveApplication(application1Mock.address) - }) - - context("when application was paused", () => { - it("should revert", async () => { - await tokenStaking - .connect(deployer) - .setPanicButton(application1Mock.address, panicButton.address) - await tokenStaking - .connect(panicButton) - .pauseApplication(application1Mock.address) - await expect( - tokenStaking - .connect(authorizer) - .increaseAuthorization( - stakingProvider.address, - application1Mock.address, - amount - ) - ).to.be.revertedWith("Application is not approved") - }) - }) - - context("when application is disabled", () => { - it("should revert", async () => { - await tokenStaking - .connect(deployer) - .disableApplication(application1Mock.address) - await expect( - tokenStaking - .connect(authorizer) - .increaseAuthorization( - stakingProvider.address, - application1Mock.address, - amount - ) - ).to.be.revertedWith("Application is not approved") - }) + .disableApplication(application1Mock.address) + await expect( + tokenStaking + .connect(authorizer) + ["requestAuthorizationDecrease(address)"]( + stakingProvider.address + ) + ).to.be.revertedWith("Application is not approved") }) + }) - context("when already authorized maximum applications", () => { - it("should revert", async () => { - await tokenStaking.connect(deployer).setAuthorizationCeiling(1) - await tokenStaking + context("when amount to decrease is zero", () => { + it("should revert", async () => { + await expect( + tokenStaking .connect(authorizer) - .increaseAuthorization( + ["requestAuthorizationDecrease(address,address,uint96)"]( stakingProvider.address, application1Mock.address, - amount + 0 ) - await tokenStaking - .connect(deployer) - .approveApplication(application2Mock.address) - await expect( - tokenStaking - .connect(authorizer) - .increaseAuthorization( - stakingProvider.address, - application2Mock.address, - amount - ) - ).to.be.revertedWith("Too many applications") - }) - }) - - context("when authorize more than staked amount", () => { - it("should revert", async () => { - await expect( - tokenStaking - .connect(authorizer) - .increaseAuthorization( - stakingProvider.address, - application1Mock.address, - amount.add(1) - ) - ).to.be.revertedWith("Not enough stake to authorize") - }) + ).to.be.revertedWith("Parameters must be specified") }) + }) - context("when authorize staked tokens in one tx", () => { - let tx - const authorizedAmount = amount.div(3) - - beforeEach(async () => { - tx = await tokenStaking + context("when amount to decrease is more than authorized", () => { + it("should revert", async () => { + await expect( + tokenStaking .connect(authorizer) - .increaseAuthorization( + ["requestAuthorizationDecrease(address,address,uint96)"]( stakingProvider.address, application1Mock.address, - authorizedAmount - ) - }) - - it("should increase authorization", async () => { - expect( - await tokenStaking.authorizedStake( - stakingProvider.address, - application1Mock.address + amount.add(1) ) - ).to.equal(authorizedAmount) - expect( - await tokenStaking.getMaxAuthorization(stakingProvider.address) - ).to.equal(authorizedAmount) - }) + ).to.be.revertedWith("Amount exceeds authorized") + }) + }) - it("should decrease available amount to authorize for one application", async () => { - expect( - await tokenStaking.getAvailableToAuthorize( - stakingProvider.address, - application1Mock.address - ) - ).to.equal(amount.sub(authorizedAmount)) - expect( - await tokenStaking.getAvailableToAuthorize( - stakingProvider.address, - application2Mock.address - ) - ).to.equal(amount) - }) + context("when amount to decrease is less than authorized", () => { + const amountToDecrease = amount.div(3) + const expectedFromAmount = amount + const expectedToAmount = amount.sub(amountToDecrease) + let tx - it("should inform application", async () => { - await assertApplicationStakingProviders( - application1Mock, + beforeEach(async () => { + tx = await tokenStaking + .connect(authorizer) + ["requestAuthorizationDecrease(address,address,uint96)"]( stakingProvider.address, - authorizedAmount, - Zero + application1Mock.address, + amountToDecrease ) - }) - - it("should emit AuthorizationIncreased", async () => { - await expect(tx) - .to.emit(tokenStaking, "AuthorizationIncreased") - .withArgs( - stakingProvider.address, - application1Mock.address, - 0, - authorizedAmount - ) - }) - }) - - context( - "when authorize more than staked amount in several txs", - () => { - it("should revert", async () => { - await tokenStaking - .connect(authorizer) - .increaseAuthorization( - stakingProvider.address, - application1Mock.address, - amount.sub(1) - ) - await expect( - tokenStaking - .connect(authorizer) - .increaseAuthorization( - stakingProvider.address, - application1Mock.address, - 2 - ) - ).to.be.revertedWith("Not enough stake to authorize") - }) - } - ) - - context("when authorize staked tokens in several txs", () => { - let tx1 - let tx2 - const authorizedAmount1 = amount.sub(1) - const authorizedAmount2 = 1 - - beforeEach(async () => { - tx1 = await tokenStaking - .connect(authorizer) - .increaseAuthorization( - stakingProvider.address, - application1Mock.address, - authorizedAmount1 - ) - tx2 = await tokenStaking - .connect(authorizer) - .increaseAuthorization( - stakingProvider.address, - application1Mock.address, - authorizedAmount2 - ) - }) - - it("should increase authorization", async () => { - expect( - await tokenStaking.authorizedStake( - stakingProvider.address, - application1Mock.address - ) - ).to.equal(amount) - expect( - await tokenStaking.getMaxAuthorization(stakingProvider.address) - ).to.equal(amount) - }) - - it("should decrease available amount to authorize for one application", async () => { - expect( - await tokenStaking.getAvailableToAuthorize( - stakingProvider.address, - application1Mock.address - ) - ).to.equal(0) - expect( - await tokenStaking.getAvailableToAuthorize( - stakingProvider.address, - application2Mock.address - ) - ).to.equal(amount) - }) - - it("should inform application", async () => { - await assertApplicationStakingProviders( - application1Mock, - stakingProvider.address, - amount, - Zero - ) - }) - - it("should emit two AuthorizationIncreased", async () => { - await expect(tx1) - .to.emit(tokenStaking, "AuthorizationIncreased") - .withArgs( - stakingProvider.address, - application1Mock.address, - 0, - authorizedAmount1 - ) - await expect(tx2) - .to.emit(tokenStaking, "AuthorizationIncreased") - .withArgs( - stakingProvider.address, - application1Mock.address, - authorizedAmount1, - authorizedAmount1.add(authorizedAmount2) - ) - }) - }) - - context("when authorize after full deauthorization", () => { - beforeEach(async () => { - await tokenStaking.connect(deployer).setAuthorizationCeiling(1) - await tokenStaking - .connect(authorizer) - .increaseAuthorization( - stakingProvider.address, - application1Mock.address, - amount - ) - await tokenStaking - .connect(authorizer) - ["requestAuthorizationDecrease(address)"]( - stakingProvider.address - ) - await application1Mock.approveAuthorizationDecrease( - stakingProvider.address - ) - await tokenStaking - .connect(deployer) - .approveApplication(application2Mock.address) - await tokenStaking - .connect(authorizer) - .increaseAuthorization( - stakingProvider.address, - application2Mock.address, - amount - ) - }) - - it("should increase authorization", async () => { - expect( - await tokenStaking.authorizedStake( - stakingProvider.address, - application1Mock.address - ) - ).to.equal(0) - expect( - await tokenStaking.authorizedStake( - stakingProvider.address, - application2Mock.address - ) - ).to.equal(amount) - }) - }) - }) - } - ) - }) - - describe("requestAuthorizationDecrease", () => { - context("when caller is not authorizer", () => { - it("should revert", async () => { - const amount = initialStakerBalance - await expect( - tokenStaking - .connect(staker) - ["requestAuthorizationDecrease(address,address,uint96)"]( - stakingProvider.address, - application1Mock.address, - amount - ) - ).to.be.revertedWith("Not authorizer") - }) - }) - - context( - "when caller is authorizer of staking provider with T stake", - () => { - const amount = initialStakerBalance - - beforeEach(async () => { - await tToken.connect(staker).approve(tokenStaking.address, amount) - await tokenStaking - .connect(staker) - .stake( - stakingProvider.address, - beneficiary.address, - authorizer.address, - amount - ) - await tokenStaking - .connect(deployer) - .approveApplication(application1Mock.address) - await tokenStaking - .connect(authorizer) - .increaseAuthorization( - stakingProvider.address, - application1Mock.address, - amount - ) - }) - - context("when application was paused", () => { - it("should revert", async () => { - const amount = initialStakerBalance - await tokenStaking - .connect(deployer) - .setPanicButton(application1Mock.address, panicButton.address) - await tokenStaking - .connect(panicButton) - .pauseApplication(application1Mock.address) - await expect( - tokenStaking - .connect(authorizer) - ["requestAuthorizationDecrease(address,address,uint96)"]( - stakingProvider.address, - application1Mock.address, - amount - ) - ).to.be.revertedWith("Application is not approved") - }) - }) - - context("when application is disabled", () => { - it("should revert", async () => { - await tokenStaking - .connect(deployer) - .disableApplication(application1Mock.address) - await expect( - tokenStaking - .connect(authorizer) - ["requestAuthorizationDecrease(address)"]( - stakingProvider.address - ) - ).to.be.revertedWith("Application is not approved") - }) - }) - - context("when amount to decrease is zero", () => { - it("should revert", async () => { - await expect( - tokenStaking - .connect(authorizer) - ["requestAuthorizationDecrease(address,address,uint96)"]( - stakingProvider.address, - application1Mock.address, - 0 - ) - ).to.be.revertedWith("Parameters must be specified") - }) - }) - - context("when amount to decrease is more than authorized", () => { - it("should revert", async () => { - await expect( - tokenStaking - .connect(authorizer) - ["requestAuthorizationDecrease(address,address,uint96)"]( - stakingProvider.address, - application1Mock.address, - amount.add(1) - ) - ).to.be.revertedWith("Amount exceeds authorized") - }) - }) - - context("when amount to decrease is less than authorized", () => { - const amountToDecrease = amount.div(3) - const expectedFromAmount = amount - const expectedToAmount = amount.sub(amountToDecrease) - let tx - - beforeEach(async () => { - tx = await tokenStaking - .connect(authorizer) - ["requestAuthorizationDecrease(address,address,uint96)"]( - stakingProvider.address, - application1Mock.address, - amountToDecrease - ) - }) + }) it("should keep authorized amount unchanged", async () => { expect( @@ -1755,8 +1178,16 @@ describe("TokenStaking", () => { }) }) - describe("topUp", () => { - context("when amount is zero", () => { + describe("unstakeT", () => { + context("when staking provider has no stake", () => { + it("should revert", async () => { + await expect( + tokenStaking.unstakeT(deployer.address, 0) + ).to.be.revertedWith("Not owner or provider") + }) + }) + + context("when caller is not owner or staking provider", () => { it("should revert", async () => { await tToken .connect(staker) @@ -1765,1661 +1196,244 @@ describe("TokenStaking", () => { .connect(staker) .stake( stakingProvider.address, - staker.address, - staker.address, + beneficiary.address, + authorizer.address, initialStakerBalance ) await expect( - tokenStaking - .connect(stakingProvider) - .topUp(stakingProvider.address, 0) - ).to.be.revertedWith("Parameters must be specified") + tokenStaking.connect(authorizer).unstakeT(stakingProvider.address, 0) + ).to.be.revertedWith("Not owner or provider") }) }) - context("when staking provider has no delegated stake", () => { + context("when amount to unstake is zero", () => { it("should revert", async () => { - await expect( - tokenStaking - .connect(stakingProvider) - .topUp(stakingProvider.address, initialStakerBalance) - ).to.be.revertedWith("Nothing to top-up") - }) - }) - - context("when staking provider has T stake", () => { - const amount = initialStakerBalance.div(3) - const topUpAmount = initialStakerBalance.mul(2) - const expectedAmount = amount.add(topUpAmount) - let tx - let blockTimestamp - - beforeEach(async () => { - await tToken.connect(staker).approve(tokenStaking.address, amount) - await tokenStaking - .connect(staker) - .stake( - stakingProvider.address, - staker.address, - staker.address, - amount - ) - blockTimestamp = await lastBlockTime() - await tokenStaking - .connect(deployer) - .approveApplication(application1Mock.address) - await tokenStaking - .connect(staker) - .increaseAuthorization( - stakingProvider.address, - application1Mock.address, - amount - ) - - await tokenStaking - .connect(staker) - .delegateVoting(stakingProvider.address, delegatee.address) - await tToken - .connect(deployer) - .transfer(stakingProvider.address, topUpAmount) - await tToken - .connect(stakingProvider) - .approve(tokenStaking.address, topUpAmount) - tx = await tokenStaking - .connect(stakingProvider) - .topUp(stakingProvider.address, topUpAmount) - }) - - it("should update T staked amount", async () => { - await assertStake(stakingProvider.address, expectedAmount) - }) - - it("should not update roles", async () => { - expect( - await tokenStaking.rolesOf(stakingProvider.address) - ).to.deep.equal([staker.address, staker.address, staker.address]) - }) - - it("should not update start staking timestamp", async () => { - expect( - await tokenStaking.getStartStakingTimestamp(stakingProvider.address) - ).to.equal(blockTimestamp) - }) - - it("should transfer tokens to the staking contract", async () => { - expect(await tToken.balanceOf(tokenStaking.address)).to.equal( - expectedAmount - ) - }) - - it("should increase available amount to authorize", async () => { - expect( - await tokenStaking.getAvailableToAuthorize( - stakingProvider.address, - application1Mock.address - ) - ).to.equal(topUpAmount) - }) - - it("should not increase authorized amount", async () => { - expect( - await tokenStaking.getMaxAuthorization(stakingProvider.address) - ).to.equal(amount) - }) - - it("should emit ToppedUp event", async () => { - await expect(tx) - .to.emit(tokenStaking, "ToppedUp") - .withArgs(stakingProvider.address, topUpAmount) - }) - - it("should increase the delegatee voting power", async () => { - expect(await tokenStaking.getVotes(delegatee.address)).to.equal( - expectedAmount - ) - }) - - it("should increase the total voting power", async () => { - const lastBlock = await mineBlocks(1) - expect(await tokenStaking.getPastTotalSupply(lastBlock - 1)).to.equal( - expectedAmount - ) - }) - }) - - context("when staking provider unstaked T previously", () => { - const amount = initialStakerBalance - let tx - let blockTimestamp - - beforeEach(async () => { - await tToken - .connect(staker) - .approve(tokenStaking.address, amount.mul(2)) - await tokenStaking - .connect(staker) - .stake( - stakingProvider.address, - staker.address, - staker.address, - amount - ) - blockTimestamp = await lastBlockTime() - await tokenStaking - .connect(staker) - .toggleAutoAuthorizationIncrease(stakingProvider.address) - - await increaseTime(86400) // +24h - - await tokenStaking - .connect(staker) - .unstakeT(stakingProvider.address, amount) - tx = await tokenStaking - .connect(staker) - .topUp(stakingProvider.address, amount) - }) - - it("should update T staked amount", async () => { - await assertStake(stakingProvider.address, amount) - }) - - it("should not update start staking timestamp", async () => { - expect( - await tokenStaking.getStartStakingTimestamp(stakingProvider.address) - ).to.equal(blockTimestamp) - }) - - it("should emit ToppedUp event", async () => { - await expect(tx) - .to.emit(tokenStaking, "ToppedUp") - .withArgs(stakingProvider.address, amount) - }) - }) - - context("when auto increase flag is enabled", () => { - const amount = initialStakerBalance.div(2) - const topUpAmount = initialStakerBalance - const expectedAmount = amount.add(topUpAmount) - const authorized1 = amount - const authorized2 = amount.div(2) - let tx - - beforeEach(async () => { - await tToken.connect(staker).approve(tokenStaking.address, amount) - await tokenStaking - .connect(staker) - .stake( - stakingProvider.address, - beneficiary.address, - authorizer.address, - amount - ) - await tokenStaking - .connect(deployer) - .approveApplication(application1Mock.address) - await tokenStaking - .connect(deployer) - .approveApplication(application2Mock.address) - await tokenStaking - .connect(authorizer) - .increaseAuthorization( - stakingProvider.address, - application1Mock.address, - authorized1 - ) - await tokenStaking - .connect(authorizer) - .increaseAuthorization( - stakingProvider.address, - application2Mock.address, - authorized2 - ) - await tokenStaking - .connect(authorizer) - .toggleAutoAuthorizationIncrease(stakingProvider.address) - - await tToken - .connect(deployer) - .transfer(stakingProvider.address, topUpAmount) - await tToken - .connect(stakingProvider) - .approve(tokenStaking.address, topUpAmount) - tx = await tokenStaking - .connect(stakingProvider) - .topUp(stakingProvider.address, topUpAmount) - }) - - it("should update T staked amount", async () => { - await assertStake(stakingProvider.address, expectedAmount) - }) - - it("should not increase available amount to authorize", async () => { - expect( - await tokenStaking.getAvailableToAuthorize( - stakingProvider.address, - application1Mock.address - ) - ).to.equal(0) - expect( - await tokenStaking.getAvailableToAuthorize( - stakingProvider.address, - application2Mock.address - ) - ).to.equal(amount.sub(authorized2)) - }) - - it("should increase authorized amount", async () => { - expect( - await tokenStaking.getMaxAuthorization(stakingProvider.address) - ).to.equal(expectedAmount) - }) - - it("should emit ToppedUp event", async () => { - await expect(tx) - .to.emit(tokenStaking, "ToppedUp") - .withArgs(stakingProvider.address, topUpAmount) - }) - - it("should increase authorized amounts", async () => { - expect( - await tokenStaking.authorizedStake( - stakingProvider.address, - application1Mock.address - ) - ).to.equal(expectedAmount) - expect( - await tokenStaking.authorizedStake( - stakingProvider.address, - application2Mock.address - ) - ).to.equal(authorized2.add(topUpAmount)) - }) - - it("should inform application", async () => { - await assertApplicationStakingProviders( - application1Mock, - stakingProvider.address, - expectedAmount, - Zero - ) - await assertApplicationStakingProviders( - application2Mock, - stakingProvider.address, - authorized2.add(topUpAmount), - Zero - ) - }) - - it("should emit AuthorizationIncreased", async () => { - await expect(tx) - .to.emit(tokenStaking, "AuthorizationIncreased") - .withArgs( - stakingProvider.address, - application1Mock.address, - authorized1, - expectedAmount - ) - await expect(tx) - .to.emit(tokenStaking, "AuthorizationIncreased") - .withArgs( - stakingProvider.address, - application2Mock.address, - authorized2, - authorized2.add(topUpAmount) - ) - }) - }) - }) - - describe("toggleAutoAuthorizationIncrease", () => { - const amount = initialStakerBalance - - beforeEach(async () => { - await tToken.connect(staker).approve(tokenStaking.address, amount) - await tokenStaking - .connect(staker) - .stake( - stakingProvider.address, - beneficiary.address, - authorizer.address, - amount - ) - }) - - context("when caller is not authorizer", () => { - it("should revert", async () => { - await expect( - tokenStaking - .connect(stakingProvider) - .toggleAutoAuthorizationIncrease(stakingProvider.address) - ).to.be.revertedWith("Not authorizer") - }) - }) - - context("when method called first time", () => { - let tx - - beforeEach(async () => { - tx = await tokenStaking - .connect(authorizer) - .toggleAutoAuthorizationIncrease(stakingProvider.address) - }) - - it("should enable auto increase flag", async () => { - expect( - await tokenStaking.getAutoIncreaseFlag(stakingProvider.address) - ).to.equal(true) - }) - - it("should emit AutoIncreaseToggled", async () => { - await expect(tx) - .to.emit(tokenStaking, "AutoIncreaseToggled") - .withArgs(stakingProvider.address, true) - }) - }) - - context("when method called second time", () => { - let tx - - beforeEach(async () => { - await tokenStaking - .connect(authorizer) - .toggleAutoAuthorizationIncrease(stakingProvider.address) - tx = await tokenStaking - .connect(authorizer) - .toggleAutoAuthorizationIncrease(stakingProvider.address) - }) - - it("should enable auto increase flag", async () => { - expect( - await tokenStaking.getAutoIncreaseFlag(stakingProvider.address) - ).to.equal(false) - }) - - it("should emit AutoIncreaseToggled", async () => { - await expect(tx) - .to.emit(tokenStaking, "AutoIncreaseToggled") - .withArgs(stakingProvider.address, false) - }) - }) - }) - - describe("unstakeT", () => { - context("when staking provider has no stake", () => { - it("should revert", async () => { - await expect( - tokenStaking.unstakeT(deployer.address, 0) - ).to.be.revertedWith("Not owner or provider") - }) - }) - - context("when caller is not owner or staking provider", () => { - it("should revert", async () => { - await tToken - .connect(staker) - .approve(tokenStaking.address, initialStakerBalance) - await tokenStaking - .connect(staker) - .stake( - stakingProvider.address, - beneficiary.address, - authorizer.address, - initialStakerBalance - ) - await expect( - tokenStaking.connect(authorizer).unstakeT(stakingProvider.address, 0) - ).to.be.revertedWith("Not owner or provider") - }) - }) - - context("when amount to unstake is zero", () => { - it("should revert", async () => { - await tToken - .connect(staker) - .approve(tokenStaking.address, initialStakerBalance) - await tokenStaking - .connect(staker) - .stake( - stakingProvider.address, - beneficiary.address, - authorizer.address, - initialStakerBalance - ) - await expect( - tokenStaking.connect(staker).unstakeT(stakingProvider.address, 0) - ).to.be.revertedWith("Too much to unstake") - }) - }) - - context("when amount to unstake is more than not authorized", () => { - it("should revert", async () => { - const amount = initialStakerBalance - await tokenStaking - .connect(deployer) - .approveApplication(application1Mock.address) - await tToken.connect(staker).approve(tokenStaking.address, amount) - await tokenStaking - .connect(staker) - .stake( - stakingProvider.address, - beneficiary.address, - authorizer.address, - amount - ) - const authorized = amount.div(3) - await tokenStaking - .connect(authorizer) - .increaseAuthorization( - stakingProvider.address, - application1Mock.address, - authorized - ) - - const amountToUnstake = amount.sub(authorized).add(1) - await expect( - tokenStaking - .connect(stakingProvider) - .unstakeT(stakingProvider.address, amountToUnstake) - ).to.be.revertedWith("Too much to unstake") - }) - }) - - context("when unstake before minimum staking time passes", () => { - const amount = initialStakerBalance - const minAmount = initialStakerBalance.div(3) - - beforeEach(async () => { - await tToken.connect(staker).approve(tokenStaking.address, amount) - await tokenStaking - .connect(staker) - .stake( - stakingProvider.address, - beneficiary.address, - authorizer.address, - amount - ) - await tokenStaking.connect(deployer).setMinimumStakeAmount(minAmount) - }) - - context("when the stake left would be above the minimum", () => { - it("should revert", async () => { - const amountToUnstake = amount.sub(minAmount).sub(1) - await expect( - tokenStaking - .connect(staker) - .unstakeT(stakingProvider.address, amountToUnstake) - ).to.be.revertedWith("Can't unstake earlier than 24h") - }) - }) - - context("when the stake left would be the minimum", () => { - it("should revert", async () => { - const amountToUnstake = amount.sub(minAmount) - await expect( - tokenStaking - .connect(staker) - .unstakeT(stakingProvider.address, amountToUnstake) - ).to.be.revertedWith("Can't unstake earlier than 24h") - }) - }) - - context("when the stake left would be below the minimum", () => { - it("should revert", async () => { - const amountToUnstake = amount.sub(minAmount).add(1) - await expect( - tokenStaking - .connect(staker) - .unstakeT(stakingProvider.address, amountToUnstake) - ).to.be.revertedWith("Can't unstake earlier than 24h") - }) - }) - }) - - context("when unstake after minimum staking time passes", () => { - const amount = initialStakerBalance - const minAmount = initialStakerBalance.div(3) - let tx - let blockTimestamp - - beforeEach(async () => { - await tokenStaking.connect(deployer).setMinimumStakeAmount(minAmount) - await tToken.connect(staker).approve(tokenStaking.address, amount) - await tokenStaking - .connect(staker) - .stake( - stakingProvider.address, - beneficiary.address, - authorizer.address, - amount - ) - blockTimestamp = await lastBlockTime() - - await increaseTime(86400) // +24h - - tx = await tokenStaking - .connect(stakingProvider) - .unstakeT(stakingProvider.address, amount) - }) - - it("should update T staked amount", async () => { - await assertStake(stakingProvider.address, Zero) - }) - - it("should not update roles", async () => { - expect( - await tokenStaking.rolesOf(stakingProvider.address) - ).to.deep.equal([ - staker.address, - beneficiary.address, - authorizer.address, - ]) - }) - - it("should not update start staking timestamp", async () => { - expect( - await tokenStaking.getStartStakingTimestamp(stakingProvider.address) - ).to.equal(blockTimestamp) - }) - - it("should transfer tokens to the staker address", async () => { - expect(await tToken.balanceOf(tokenStaking.address)).to.equal(0) - expect(await tToken.balanceOf(staker.address)).to.equal(amount) - }) - - it("should emit Unstaked", async () => { - await expect(tx) - .to.emit(tokenStaking, "Unstaked") - .withArgs(stakingProvider.address, amount) - }) - }) - }) - - describe("setNotificationReward", () => { - const amount = initialStakerBalance - - context("when caller is not the governance", () => { - it("should revert", async () => { - await expect( - tokenStaking.connect(staker).setNotificationReward(amount) - ).to.be.revertedWith("Caller is not the governance") - }) - }) - - context("when caller is the governance", () => { - let tx - - beforeEach(async () => { - tx = await tokenStaking.connect(deployer).setNotificationReward(amount) - }) - - it("should set values", async () => { - expect(await tokenStaking.notificationReward()).to.equal(amount) - }) - - it("should emit NotificationRewardSet event", async () => { - await expect(tx) - .to.emit(tokenStaking, "NotificationRewardSet") - .withArgs(amount) - }) - }) - }) - - describe("pushNotificationReward", () => { - context("when reward is zero", () => { - it("should revert", async () => { - await expect(tokenStaking.pushNotificationReward(0)).to.be.revertedWith( - "Parameters must be specified" - ) - }) - }) - - context("when reward is not zero", () => { - const reward = initialStakerBalance - let tx - - beforeEach(async () => { - await tToken.connect(staker).approve(tokenStaking.address, reward) - tx = await tokenStaking.connect(staker).pushNotificationReward(reward) - }) - - it("should increase treasury amount", async () => { - expect(await tokenStaking.notifiersTreasury()).to.equal(reward) - }) - - it("should transfer tokens to the staking contract", async () => { - expect(await tToken.balanceOf(tokenStaking.address)).to.equal(reward) - }) - - it("should emit NotificationRewardPushed event", async () => { - await expect(tx) - .to.emit(tokenStaking, "NotificationRewardPushed") - .withArgs(reward) - }) - }) - }) - - describe("withdrawNotificationReward", () => { - context("when caller is not the governance", () => { - it("should revert", async () => { - await expect( - tokenStaking - .connect(staker) - .withdrawNotificationReward(deployer.address, 1) - ).to.be.revertedWith("Caller is not the governance") - }) - }) - - context("when amount is more than in treasury", () => { - it("should revert", async () => { - await expect( - tokenStaking - .connect(deployer) - .withdrawNotificationReward(deployer.address, 1) - ).to.be.revertedWith("Not enough tokens") - }) - }) - - context("when amount is less than in treasury", () => { - const reward = initialStakerBalance - const amount = reward.div(3) - const expectedReward = reward.sub(amount) - let tx - - beforeEach(async () => { - await tToken.connect(staker).approve(tokenStaking.address, reward) - await tokenStaking.connect(staker).pushNotificationReward(reward) - tx = await tokenStaking - .connect(deployer) - .withdrawNotificationReward(auxiliaryAccount.address, amount) - }) - - it("should decrease treasury amount", async () => { - expect(await tokenStaking.notifiersTreasury()).to.equal(expectedReward) - }) - - it("should transfer tokens to the recipient", async () => { - expect(await tToken.balanceOf(tokenStaking.address)).to.equal( - expectedReward - ) - expect(await tToken.balanceOf(auxiliaryAccount.address)).to.equal( - amount - ) - }) - - it("should emit NotificationRewardWithdrawn event", async () => { - await expect(tx) - .to.emit(tokenStaking, "NotificationRewardWithdrawn") - .withArgs(auxiliaryAccount.address, amount) - }) - }) - }) - - describe("slash", () => { - context("when amount is zero", () => { - it("should revert", async () => { - await expect( - tokenStaking.slash(0, [stakingProvider.address]) - ).to.be.revertedWith("Parameters must be specified") - }) - }) - - context("when staking providers were not provided", () => { - it("should revert", async () => { - await expect( - tokenStaking.slash(initialStakerBalance, []) - ).to.be.revertedWith("Parameters must be specified") - }) - }) - - context("when application was not approved", () => { - it("should revert", async () => { - await expect( - tokenStaking.slash(initialStakerBalance, [stakingProvider.address]) - ).to.be.revertedWith("Application is not approved") - }) - }) - - context("when application was paused", () => { - it("should revert", async () => { - await tokenStaking - .connect(deployer) - .approveApplication(application1Mock.address) - await tokenStaking - .connect(deployer) - .setPanicButton(application1Mock.address, panicButton.address) - await tokenStaking - .connect(panicButton) - .pauseApplication(application1Mock.address) - await expect( - application1Mock.slash(initialStakerBalance, [ - stakingProvider.address, - ]) - ).to.be.revertedWith("Application is not approved") - }) - }) - - context("when application is disabled", () => { - it("should revert", async () => { - await tokenStaking - .connect(deployer) - .approveApplication(application1Mock.address) - await tokenStaking - .connect(deployer) - .disableApplication(application1Mock.address) - await expect( - application1Mock.slash(initialStakerBalance, [ - stakingProvider.address, - ]) - ).to.be.revertedWith("Application is not approved") - }) - }) - - context( - "when application was not authorized by one staking provider", - () => { - beforeEach(async () => { - await tokenStaking - .connect(deployer) - .approveApplication(application1Mock.address) - await application1Mock.slash(initialStakerBalance, [ - stakingProvider.address, - ]) - }) - - it("should skip this event", async () => { - expect(await tokenStaking.getSlashingQueueLength()).to.equal(0) - }) - } - ) - - context("when authorized amount is less than amount to slash", () => { - const amount = initialStakerBalance.div(2) - const amountToSlash = initialStakerBalance // amountToSlash > amount - - beforeEach(async () => { - await tokenStaking - .connect(deployer) - .approveApplication(application1Mock.address) - - await tToken.connect(staker).approve(tokenStaking.address, amount) - await tokenStaking - .connect(staker) - .stake( - stakingProvider.address, - staker.address, - staker.address, - amount - ) - await tokenStaking - .connect(staker) - .increaseAuthorization( - stakingProvider.address, - application1Mock.address, - amount - ) - - await tToken - .connect(otherStaker) - .approve(tokenStaking.address, amountToSlash) - await tokenStaking - .connect(otherStaker) - .stake( - otherStaker.address, - otherStaker.address, - otherStaker.address, - amountToSlash - ) - await tokenStaking - .connect(otherStaker) - .increaseAuthorization( - otherStaker.address, - application1Mock.address, - amountToSlash - ) - - await application1Mock.slash(amountToSlash, [ - stakingProvider.address, - otherStaker.address, - ]) - }) - - it("should add two slashing events", async () => { - await assertSlashingQueue(0, stakingProvider.address, amount) - await assertSlashingQueue(1, otherStaker.address, amountToSlash) - expect(await tokenStaking.getSlashingQueueLength()).to.equal(2) - }) - }) - - context("when authorized amount is more than amount to slash", () => { - const amount = initialStakerBalance.div(2) - const authorized = amount.div(2) - const amountToSlash = authorized.div(2) - - beforeEach(async () => { - await tokenStaking - .connect(deployer) - .approveApplication(application1Mock.address) - - await tToken - .connect(staker) - .approve(tokenStaking.address, initialStakerBalance) - await tokenStaking - .connect(staker) - .stake( - stakingProvider.address, - staker.address, - staker.address, - amount - ) - await tokenStaking - .connect(staker) - .increaseAuthorization( - stakingProvider.address, - application1Mock.address, - authorized - ) - - await tokenStaking - .connect(staker) - .stake( - otherStaker.address, - otherStaker.address, - otherStaker.address, - amount - ) - await tokenStaking - .connect(otherStaker) - .increaseAuthorization( - otherStaker.address, - application1Mock.address, - amount - ) - - await application1Mock.slash(amountToSlash, [ - stakingProvider.address, - otherStaker.address, - ]) - }) - - it("should add two slashing events", async () => { - await assertSlashingQueue(0, stakingProvider.address, amountToSlash) - await assertSlashingQueue(1, otherStaker.address, amountToSlash) - expect(await tokenStaking.getSlashingQueueLength()).to.equal(2) - }) - - it("should keep index of queue unchanged", async () => { - expect(await tokenStaking.slashingQueueIndex()).to.equal(0) - }) - }) - }) - - describe("seize", () => { - const amount = initialStakerBalance.div(2) - const authorized = amount.div(2) - const amountToSlash = authorized.div(2) - const rewardMultiplier = 75 - const rewardPerProvider = amount.div(10) - const reward = rewardPerProvider.mul(10) - let tx - let notifier - - beforeEach(async () => { - notifier = await ethers.Wallet.createRandom() - await tokenStaking - .connect(deployer) - .approveApplication(application1Mock.address) - - await tToken - .connect(staker) - .approve(tokenStaking.address, initialStakerBalance) - await tokenStaking - .connect(staker) - .stake(stakingProvider.address, staker.address, staker.address, amount) - await tokenStaking - .connect(staker) - .increaseAuthorization( - stakingProvider.address, - application1Mock.address, - authorized - ) - - await tokenStaking - .connect(staker) - .stake( - otherStaker.address, - otherStaker.address, - otherStaker.address, - amount - ) - await tokenStaking - .connect(otherStaker) - .increaseAuthorization( - otherStaker.address, - application1Mock.address, - amount - ) - }) - - context("when notifier was not specified", () => { - beforeEach(async () => { - await tToken.connect(deployer).approve(tokenStaking.address, reward) - await tokenStaking.connect(deployer).pushNotificationReward(reward) - await tokenStaking - .connect(deployer) - .setNotificationReward(rewardPerProvider) - - tx = await application1Mock.seize( - amountToSlash, - rewardMultiplier, - AddressZero, - [otherStaker.address, stakingProvider.address] - ) - }) - - it("should add two slashing events", async () => { - await assertSlashingQueue(0, otherStaker.address, amountToSlash) - await assertSlashingQueue(1, stakingProvider.address, amountToSlash) - expect(await tokenStaking.getSlashingQueueLength()).to.equal(2) - }) - - it("should keep index of queue unchanged", async () => { - expect(await tokenStaking.slashingQueueIndex()).to.equal(0) - }) - - it("should not transfer any tokens", async () => { - expect(await tokenStaking.notifiersTreasury()).to.equal(reward) - const expectedBalance = reward.add(amount.mul(2)) - expect(await tToken.balanceOf(tokenStaking.address)).to.equal( - expectedBalance - ) - expect(await tToken.balanceOf(notifier.address)).to.equal(0) - }) - }) - - context("when reward per staking provider was not set", () => { - beforeEach(async () => { - await tToken.connect(deployer).approve(tokenStaking.address, reward) - await tokenStaking.connect(deployer).pushNotificationReward(reward) - - tx = await application1Mock.seize( - amountToSlash, - rewardMultiplier, - notifier.address, - [stakingProvider.address] - ) - }) - - it("should add one slashing event", async () => { - await assertSlashingQueue(0, stakingProvider.address, amountToSlash) - expect(await tokenStaking.getSlashingQueueLength()).to.equal(1) - }) - - it("should keep index of queue unchanged", async () => { - expect(await tokenStaking.slashingQueueIndex()).to.equal(0) - }) - - it("should not transfer any tokens", async () => { - expect(await tokenStaking.notifiersTreasury()).to.equal(reward) - const expectedBalance = reward.add(amount.mul(2)) - expect(await tToken.balanceOf(tokenStaking.address)).to.equal( - expectedBalance - ) - expect(await tToken.balanceOf(notifier.address)).to.equal(0) - }) - - it("should emit NotifierRewarded event", async () => { - await expect(tx) - .to.emit(tokenStaking, "NotifierRewarded") - .withArgs(notifier.address, 0) - }) - }) - - context("when no more reward for notifier", () => { - beforeEach(async () => { - await tokenStaking - .connect(deployer) - .setNotificationReward(rewardPerProvider) - - tx = await application1Mock.seize( - amountToSlash, - rewardMultiplier, - notifier.address, - [otherStaker.address] - ) - }) - - it("should add one slashing event", async () => { - await assertSlashingQueue(0, otherStaker.address, amountToSlash) - }) - - it("should keep index of queue unchanged", async () => { - expect(await tokenStaking.slashingQueueIndex()).to.equal(0) - }) - - it("should not transfer any tokens", async () => { - expect(await tokenStaking.notifiersTreasury()).to.equal(0) - const expectedBalance = amount.mul(2) - expect(await tToken.balanceOf(tokenStaking.address)).to.equal( - expectedBalance - ) - expect(await tToken.balanceOf(notifier.address)).to.equal(0) - }) - - it("should emit NotifierRewarded event", async () => { - await expect(tx) - .to.emit(tokenStaking, "NotifierRewarded") - .withArgs(notifier.address, 0) - }) - }) - - context("when reward multiplier is zero", () => { - beforeEach(async () => { - await tToken.connect(deployer).approve(tokenStaking.address, reward) - await tokenStaking.connect(deployer).pushNotificationReward(reward) - await tokenStaking - .connect(deployer) - .setNotificationReward(rewardPerProvider) - - tx = await application1Mock.seize(amountToSlash, 0, notifier.address, [ - stakingProvider.address, - ]) - }) - - it("should not transfer any tokens", async () => { - expect(await tokenStaking.notifiersTreasury()).to.equal(reward) - const expectedBalance = reward.add(amount.mul(2)) - expect(await tToken.balanceOf(tokenStaking.address)).to.equal( - expectedBalance - ) - expect(await tToken.balanceOf(notifier.address)).to.equal(0) - }) - - it("should emit NotifierRewarded event", async () => { - await expect(tx) - .to.emit(tokenStaking, "NotifierRewarded") - .withArgs(notifier.address, 0) - }) - }) - - context("when reward is less than amount of tokens in treasury", () => { - beforeEach(async () => { - await tToken.connect(deployer).approve(tokenStaking.address, reward) - await tokenStaking.connect(deployer).pushNotificationReward(reward) - await tokenStaking - .connect(deployer) - .setNotificationReward(reward.sub(1)) - - tx = await application1Mock.seize( - amountToSlash, - rewardMultiplier, - notifier.address, - [stakingProvider.address, otherStaker.address] - ) - }) - - it("should transfer all tokens", async () => { - expect(await tokenStaking.notifiersTreasury()).to.equal(0) - const expectedBalance = amount.mul(2) - expect(await tToken.balanceOf(tokenStaking.address)).to.equal( - expectedBalance - ) - expect(await tToken.balanceOf(notifier.address)).to.equal(reward) - }) - - it("should emit NotifierRewarded event", async () => { - await expect(tx) - .to.emit(tokenStaking, "NotifierRewarded") - .withArgs(notifier.address, reward) - }) - }) - - context("when reward is greater than amount of tokens in treasury", () => { - // 2 providers - const expectedReward = rewardPerProvider - .mul(2) - .mul(rewardMultiplier) - .div(100) - - beforeEach(async () => { - await tToken.connect(deployer).approve(tokenStaking.address, reward) - await tokenStaking.connect(deployer).pushNotificationReward(reward) - await tokenStaking - .connect(deployer) - .setNotificationReward(rewardPerProvider) - - tx = await application1Mock.seize( - amountToSlash, - rewardMultiplier, - notifier.address, - [stakingProvider.address, otherStaker.address, authorizer.address] - ) - }) - - it("should transfer all tokens", async () => { - const expectedTreasuryBalance = reward.sub(expectedReward) - expect(await tokenStaking.notifiersTreasury()).to.equal( - expectedTreasuryBalance - ) - const expectedBalance = expectedTreasuryBalance.add(amount.mul(2)) - expect(await tToken.balanceOf(tokenStaking.address)).to.equal( - expectedBalance - ) - expect(await tToken.balanceOf(notifier.address)).to.equal( - expectedReward - ) - }) - - it("should emit NotifierRewarded event", async () => { - await expect(tx) - .to.emit(tokenStaking, "NotifierRewarded") - .withArgs(notifier.address, expectedReward) - }) - }) - }) - - describe("processSlashing", () => { - context("when queue is empty", () => { - it("should revert", async () => { - await expect(tokenStaking.processSlashing(1)).to.be.revertedWith( - "Nothing to process" - ) - }) - }) - - context("when queue is not empty", () => { - const tAmount = initialStakerBalance.div(2) - const tStake2 = tAmount.mul(2) - - const provider1Authorized1 = tAmount.div(2) - const amountToSlash = provider1Authorized1.div(2) - const provider1Authorized2 = provider1Authorized1 - const provider2Authorized1 = tStake2 - const provider2Authorized2 = tAmount.div(100) - - const expectedTReward1 = rewardFromPenalty(amountToSlash, 100) - const expectedTReward2 = rewardFromPenalty(tStake2, 100) - - let tx - - beforeEach(async () => { - await tokenStaking - .connect(deployer) - .approveApplication(application1Mock.address) - await tokenStaking - .connect(deployer) - .approveApplication(application2Mock.address) - - await tToken - .connect(staker) - .approve(tokenStaking.address, initialStakerBalance) - await tokenStaking - .connect(staker) - .stake( - stakingProvider.address, - staker.address, - staker.address, - tAmount - ) - await tokenStaking - .connect(staker) - .delegateVoting(stakingProvider.address, delegatee.address) - await tokenStaking - .connect(staker) - .increaseAuthorization( - stakingProvider.address, - application1Mock.address, - provider1Authorized1 - ) - await tokenStaking - .connect(staker) - .increaseAuthorization( - stakingProvider.address, - application2Mock.address, - provider1Authorized2 - ) - - await tToken.connect(deployer).transfer(otherStaker.address, tStake2) - await tToken.connect(otherStaker).approve(tokenStaking.address, tStake2) - await tokenStaking - .connect(otherStaker) - .stake( - otherStaker.address, - otherStaker.address, - otherStaker.address, - tStake2 - ) - - await tokenStaking - .connect(otherStaker) - .increaseAuthorization( - otherStaker.address, - application1Mock.address, - provider2Authorized1 - ) - await tokenStaking - .connect(otherStaker) - .increaseAuthorization( - otherStaker.address, - application2Mock.address, - provider2Authorized2 - ) - - await application1Mock.slash(amountToSlash, [ - stakingProvider.address, - otherStaker.address, - ]) - await application1Mock.slash(tStake2, [otherStaker.address]) - }) - - context("when provided number is zero", () => { - it("should revert", async () => { - await expect(tokenStaking.processSlashing(0)).to.be.revertedWith( - "Nothing to process" - ) - }) - }) - - context("when slash only one staking provider with T stake", () => { - const expectedAmount = tAmount.sub(amountToSlash) - - beforeEach(async () => { - tx = await tokenStaking.connect(auxiliaryAccount).processSlashing(1) - }) - - it("should update staked amount", async () => { - await assertStake(stakingProvider.address, expectedAmount) - }) - - it("should decrease the delegatee voting power", async () => { - expect(await tokenStaking.getVotes(delegatee.address)).to.equal( - expectedAmount - ) - }) - - it("should update index of queue", async () => { - expect(await tokenStaking.slashingQueueIndex()).to.equal(1) - expect(await tokenStaking.getSlashingQueueLength()).to.equal(3) - }) - - it("should transfer reward to processor", async () => { - const expectedBalance = tAmount.add(tStake2).sub(expectedTReward1) - expect(await tToken.balanceOf(tokenStaking.address)).to.equal( - expectedBalance - ) - expect(await tToken.balanceOf(auxiliaryAccount.address)).to.equal( - expectedTReward1 - ) - }) - - it("should increase amount in notifiers treasury ", async () => { - const expectedTreasuryBalance = amountToSlash.sub(expectedTReward1) - expect(await tokenStaking.notifiersTreasury()).to.equal( - expectedTreasuryBalance - ) - }) - - it("should decrease authorized amounts only for one provider", async () => { - expect( - await tokenStaking.authorizedStake( - stakingProvider.address, - application1Mock.address - ) - ).to.equal(provider1Authorized1.sub(amountToSlash)) - expect( - await tokenStaking.authorizedStake( - stakingProvider.address, - application2Mock.address - ) - ).to.equal(provider1Authorized2.sub(amountToSlash)) - expect( - await tokenStaking.authorizedStake( - otherStaker.address, - application1Mock.address - ) - ).to.equal(provider2Authorized1) - expect( - await tokenStaking.authorizedStake( - otherStaker.address, - application2Mock.address - ) - ).to.equal(provider2Authorized2) - }) - - it("should not allow to authorize more applications", async () => { - await tokenStaking - .connect(deployer) - .approveApplication(auxiliaryAccount.address) - await tokenStaking.connect(deployer).setAuthorizationCeiling(2) - - await expect( - tokenStaking - .connect(staker) - .increaseAuthorization( - stakingProvider.address, - auxiliaryAccount.address, - 1 - ) - ).to.be.revertedWith("Too many applications") - }) - - it("should inform all applications", async () => { - await assertApplicationStakingProviders( - application1Mock, - stakingProvider.address, - provider1Authorized1.sub(amountToSlash), - Zero - ) - await assertApplicationStakingProviders( - application2Mock, - stakingProvider.address, - provider1Authorized2.sub(amountToSlash), - Zero - ) - await assertApplicationStakingProviders( - application1Mock, - otherStaker.address, - provider2Authorized1, - Zero - ) - await assertApplicationStakingProviders( - application2Mock, - otherStaker.address, - provider2Authorized2, - Zero - ) - }) - - it("should emit TokensSeized and SlashingProcessed events", async () => { - await expect(tx) - .to.emit(tokenStaking, "TokensSeized") - .withArgs(stakingProvider.address, amountToSlash, false) - await expect(tx) - .to.emit(tokenStaking, "SlashingProcessed") - .withArgs(auxiliaryAccount.address, 1, expectedTReward1) - }) - }) - - context("when process everything in the queue", () => { - const expectedReward = expectedTReward1.add(expectedTReward2) - - beforeEach(async () => { - await tokenStaking.connect(auxiliaryAccount).processSlashing(1) - tx = await tokenStaking.connect(auxiliaryAccount).processSlashing(10) - }) - - it("should update staked amount", async () => { - await assertStake(otherStaker.address, Zero) - }) - - it("should update index of queue", async () => { - expect(await tokenStaking.slashingQueueIndex()).to.equal(3) - expect(await tokenStaking.getSlashingQueueLength()).to.equal(3) - }) - - it("should transfer reward to processor", async () => { - const expectedBalance = tAmount.add(tStake2).sub(expectedReward) - expect(await tToken.balanceOf(tokenStaking.address)).to.equal( - expectedBalance - ) - expect(await tToken.balanceOf(auxiliaryAccount.address)).to.equal( - expectedReward - ) - }) - - it("should increase amount in notifiers treasury ", async () => { - const expectedTreasuryBalance = amountToSlash - .add(tStake2) - .sub(expectedReward) - expect(await tokenStaking.notifiersTreasury()).to.equal( - expectedTreasuryBalance - ) - }) - - it("should decrease authorized amount and inform applications", async () => { - expect( - await tokenStaking.authorizedStake( - otherStaker.address, - application1Mock.address - ) - ).to.equal(0) - expect( - await tokenStaking.authorizedStake( - otherStaker.address, - application2Mock.address - ) - ).to.equal(0) - await assertApplicationStakingProviders( - application1Mock, - otherStaker.address, - Zero, - Zero - ) - await assertApplicationStakingProviders( - application2Mock, - otherStaker.address, - Zero, - Zero - ) - }) - - it("should allow to authorize more applications", async () => { - await tokenStaking.connect(deployer).setAuthorizationCeiling(2) - await tToken.connect(deployer).transfer(otherStaker.address, tAmount) - await tToken - .connect(otherStaker) - .approve(tokenStaking.address, tAmount) - await tokenStaking - .connect(otherStaker) - .topUp(otherStaker.address, tAmount) - - await tokenStaking - .connect(otherStaker) - .increaseAuthorization( - otherStaker.address, - application1Mock.address, - tAmount - ) - await tokenStaking - .connect(otherStaker) - .increaseAuthorization( - otherStaker.address, - application2Mock.address, - tAmount - ) - }) - - it("should emit TokensSeized, SlashingProcessed and AuthorizationInvoluntaryDecreased", async () => { - await expect(tx) - .to.emit(tokenStaking, "TokensSeized") - .withArgs(otherStaker.address, amountToSlash, false) - await expect(tx) - .to.emit(tokenStaking, "TokensSeized") - .withArgs(otherStaker.address, tStake2.sub(amountToSlash), false) - await expect(tx) - .to.emit(tokenStaking, "SlashingProcessed") - .withArgs(auxiliaryAccount.address, 2, expectedTReward2) - await expect(tx) - .to.emit(tokenStaking, "AuthorizationInvoluntaryDecreased") - .withArgs( - otherStaker.address, - application1Mock.address, - provider2Authorized1, - provider2Authorized1.sub(amountToSlash), - true - ) - await expect(tx) - .to.emit(tokenStaking, "AuthorizationInvoluntaryDecreased") - .withArgs( - otherStaker.address, - application1Mock.address, - provider2Authorized1.sub(amountToSlash), - Zero, - true - ) - }) - }) - - context("when staking provider has no stake anymore", () => { - beforeEach(async () => { - await tokenStaking - .connect(staker) - ["requestAuthorizationDecrease(address)"](stakingProvider.address) - await application1Mock.approveAuthorizationDecrease( - stakingProvider.address - ) - await application2Mock.approveAuthorizationDecrease( - stakingProvider.address - ) - await increaseTime(86400) // +24h - await tokenStaking - .connect(stakingProvider) - .unstakeT(stakingProvider.address, tAmount) - tx = await tokenStaking.connect(auxiliaryAccount).processSlashing(1) - }) - - it("should not update staked amount", async () => { - await assertStake(stakingProvider.address, Zero) - }) - - it("should update index of queue", async () => { - expect(await tokenStaking.slashingQueueIndex()).to.equal(1) - expect(await tokenStaking.getSlashingQueueLength()).to.equal(3) - }) - - it("should not transfer reward to processor", async () => { - const expectedBalance = tStake2 - expect(await tToken.balanceOf(tokenStaking.address)).to.equal( - expectedBalance + await tToken + .connect(staker) + .approve(tokenStaking.address, initialStakerBalance) + await tokenStaking + .connect(staker) + .stake( + stakingProvider.address, + beneficiary.address, + authorizer.address, + initialStakerBalance ) - expect(await tToken.balanceOf(auxiliaryAccount.address)).to.equal(0) - }) - - it("should not increase amount in notifiers treasury ", async () => { - expect(await tokenStaking.notifiersTreasury()).to.equal(0) - }) - - it("should emit TokensSeized and SlashingProcessed events", async () => { - await expect(tx) - .to.emit(tokenStaking, "TokensSeized") - .withArgs(stakingProvider.address, 0, false) - await expect(tx) - .to.emit(tokenStaking, "SlashingProcessed") - .withArgs(auxiliaryAccount.address, 1, 0) - }) + await expect( + tokenStaking.connect(staker).unstakeT(stakingProvider.address, 0) + ).to.be.revertedWith("Too much to unstake") }) }) - context("when decrease authorized amount to zero", () => { - const tAmount = initialStakerBalance - - const amountToSlash = tAmount.div(3) - const authorized = amountToSlash - - beforeEach(async () => { - await tokenStaking.connect(deployer).setAuthorizationCeiling(2) + context("when amount to unstake is more than not authorized", () => { + it("should revert", async () => { + const amount = initialStakerBalance await tokenStaking .connect(deployer) .approveApplication(application1Mock.address) - await tokenStaking - .connect(deployer) - .approveApplication(application2Mock.address) - - await tToken.connect(staker).approve(tokenStaking.address, tAmount) + await tToken.connect(staker).approve(tokenStaking.address, amount) await tokenStaking .connect(staker) .stake( stakingProvider.address, - staker.address, - staker.address, - tAmount + beneficiary.address, + authorizer.address, + amount ) + const authorized = amount.div(3) await tokenStaking - .connect(staker) + .connect(authorizer) .increaseAuthorization( stakingProvider.address, - application2Mock.address, + application1Mock.address, authorized ) + + const amountToUnstake = amount.sub(authorized).add(1) + await expect( + tokenStaking + .connect(stakingProvider) + .unstakeT(stakingProvider.address, amountToUnstake) + ).to.be.revertedWith("Too much to unstake") + }) + }) + + context("when unstake before minimum staking time passes", () => { + const amount = initialStakerBalance + const minAmount = initialStakerBalance.div(3) + + beforeEach(async () => { + await tToken.connect(staker).approve(tokenStaking.address, amount) await tokenStaking .connect(staker) - .increaseAuthorization( + .stake( stakingProvider.address, - application1Mock.address, - authorized + beneficiary.address, + authorizer.address, + amount ) + await tokenStaking.connect(deployer).setMinimumStakeAmount(minAmount) + }) - await application1Mock.slash(amountToSlash, [stakingProvider.address]) + context("when the stake left would be above the minimum", () => { + it("should revert", async () => { + const amountToUnstake = amount.sub(minAmount).sub(1) + await expect( + tokenStaking + .connect(staker) + .unstakeT(stakingProvider.address, amountToUnstake) + ).to.be.revertedWith("Can't unstake earlier than 24h") + }) + }) - await tokenStaking.processSlashing(1) + context("when the stake left would be the minimum", () => { + it("should revert", async () => { + const amountToUnstake = amount.sub(minAmount) + await expect( + tokenStaking + .connect(staker) + .unstakeT(stakingProvider.address, amountToUnstake) + ).to.be.revertedWith("Can't unstake earlier than 24h") + }) }) - it("should decrease authorized amount", async () => { - expect( - await tokenStaking.authorizedStake( - stakingProvider.address, - application1Mock.address - ) - ).to.equal(0) - expect( - await tokenStaking.authorizedStake( - stakingProvider.address, - application2Mock.address - ) - ).to.equal(0) + context("when the stake left would be below the minimum", () => { + it("should revert", async () => { + const amountToUnstake = amount.sub(minAmount).add(1) + await expect( + tokenStaking + .connect(staker) + .unstakeT(stakingProvider.address, amountToUnstake) + ).to.be.revertedWith("Can't unstake earlier than 24h") + }) }) + }) - it("should allow to authorize one more application", async () => { - await tokenStaking - .connect(staker) - .increaseAuthorization( - stakingProvider.address, - application1Mock.address, - authorized - ) + context("when unstake after minimum staking time passes", () => { + const amount = initialStakerBalance + const minAmount = initialStakerBalance.div(3) + let tx + let blockTimestamp + beforeEach(async () => { + await tokenStaking.connect(deployer).setMinimumStakeAmount(minAmount) + await tToken.connect(staker).approve(tokenStaking.address, amount) await tokenStaking .connect(staker) - .increaseAuthorization( + .stake( stakingProvider.address, - application2Mock.address, - authorized + beneficiary.address, + authorizer.address, + amount ) + blockTimestamp = await lastBlockTime() - await tokenStaking - .connect(deployer) - .approveApplication(auxiliaryAccount.address) + await increaseTime(86400) // +24h + + tx = await tokenStaking + .connect(stakingProvider) + .unstakeT(stakingProvider.address, amount) + }) + + it("should update T staked amount", async () => { + await assertStake(stakingProvider.address, Zero) + }) + + it("should not update roles", async () => { + expect( + await tokenStaking.rolesOf(stakingProvider.address) + ).to.deep.equal([ + staker.address, + beneficiary.address, + authorizer.address, + ]) + }) + + it("should not update start staking timestamp", async () => { + expect( + await tokenStaking.getStartStakingTimestamp(stakingProvider.address) + ).to.equal(blockTimestamp) + }) + + it("should transfer tokens to the staker address", async () => { + expect(await tToken.balanceOf(tokenStaking.address)).to.equal(0) + expect(await tToken.balanceOf(staker.address)).to.equal(amount) + }) + + it("should emit Unstaked", async () => { + await expect(tx) + .to.emit(tokenStaking, "Unstaked") + .withArgs(stakingProvider.address, amount) + }) + }) + }) + + describe("withdrawNotificationReward", () => { + context("when caller is not the governance", () => { + it("should revert", async () => { await expect( tokenStaking .connect(staker) - .increaseAuthorization( - stakingProvider.address, - auxiliaryAccount.address, - authorized - ) - ).to.be.revertedWith("Too many applications") + .withdrawNotificationReward(deployer.address, 1) + ).to.be.revertedWith("Caller is not the governance") + }) + }) + + context("when amount is more than in treasury", () => { + it("should revert", async () => { + await expect( + tokenStaking + .connect(deployer) + .withdrawNotificationReward(deployer.address, 1) + ).to.be.revertedWith("Not enough tokens") + }) + }) + + context("when amount is less than in treasury", () => { + const reward = initialStakerBalance + const amount = reward.div(3) + const expectedReward = reward.sub(amount) + let tx + + beforeEach(async () => { + await tToken.connect(staker).approve(tokenStaking.address, reward) + await tokenStaking.connect(staker).pushNotificationReward(reward) + tx = await tokenStaking + .connect(deployer) + .withdrawNotificationReward(auxiliaryAccount.address, amount) + }) + + it("should decrease treasury amount", async () => { + expect(await tokenStaking.notifiersTreasury()).to.equal(expectedReward) + }) + + it("should transfer tokens to the recipient", async () => { + expect(await tToken.balanceOf(tokenStaking.address)).to.equal( + expectedReward + ) + expect(await tToken.balanceOf(auxiliaryAccount.address)).to.equal( + amount + ) + }) + + it("should emit NotificationRewardWithdrawn event", async () => { + await expect(tx) + .to.emit(tokenStaking, "NotificationRewardWithdrawn") + .withArgs(auxiliaryAccount.address, amount) }) }) }) describe("cleanAuthorizedApplications", () => { const amount = initialStakerBalance - let extendedTokenStaking - - beforeEach(async () => { - const ExtendedTokenStaking = await ethers.getContractFactory( - "ExtendedTokenStaking" - ) - extendedTokenStaking = await ExtendedTokenStaking.deploy(tToken.address) - await extendedTokenStaking.deployed() - }) context("when all authorized applications with 0 authorization", () => { beforeEach(async () => { - await extendedTokenStaking.setAuthorizedApplications( + await tokenStaking.setAuthorizedApplications( stakingProvider.address, [application1Mock.address, application2Mock.address] ) - await extendedTokenStaking.cleanAuthorizedApplications( + await tokenStaking.cleanAuthorizedApplications( stakingProvider.address, 2 ) @@ -3427,7 +1441,7 @@ describe("TokenStaking", () => { it("should remove all applications", async () => { expect( - await extendedTokenStaking.getAuthorizedApplications( + await tokenStaking.getAuthorizedApplications( stakingProvider.address ) ).to.deep.equal([]) @@ -3438,16 +1452,16 @@ describe("TokenStaking", () => { "when one application in the end of the array with non-zero authorization", () => { beforeEach(async () => { - await extendedTokenStaking.setAuthorizedApplications( + await tokenStaking.setAuthorizedApplications( stakingProvider.address, [application1Mock.address, application2Mock.address] ) - await extendedTokenStaking.setAuthorization( + await tokenStaking.setAuthorization( stakingProvider.address, application2Mock.address, amount ) - await extendedTokenStaking.cleanAuthorizedApplications( + await tokenStaking.cleanAuthorizedApplications( stakingProvider.address, 1 ) @@ -3455,7 +1469,7 @@ describe("TokenStaking", () => { it("should remove only first application", async () => { expect( - await extendedTokenStaking.getAuthorizedApplications( + await tokenStaking.getAuthorizedApplications( stakingProvider.address ) ).to.deep.equal([application2Mock.address]) @@ -3467,16 +1481,16 @@ describe("TokenStaking", () => { "when one application in the beggining of the array with non-zero authorization", () => { beforeEach(async () => { - await extendedTokenStaking.setAuthorizedApplications( + await tokenStaking.setAuthorizedApplications( stakingProvider.address, [application1Mock.address, application2Mock.address] ) - await extendedTokenStaking.setAuthorization( + await tokenStaking.setAuthorization( stakingProvider.address, application1Mock.address, amount ) - await extendedTokenStaking.cleanAuthorizedApplications( + await tokenStaking.cleanAuthorizedApplications( stakingProvider.address, 1 ) @@ -3484,7 +1498,7 @@ describe("TokenStaking", () => { it("should remove only first application", async () => { expect( - await extendedTokenStaking.getAuthorizedApplications( + await tokenStaking.getAuthorizedApplications( stakingProvider.address ) ).to.deep.equal([application1Mock.address]) @@ -3496,7 +1510,7 @@ describe("TokenStaking", () => { "when one application in the middle of the array with non-zero authorization", () => { beforeEach(async () => { - await extendedTokenStaking.setAuthorizedApplications( + await tokenStaking.setAuthorizedApplications( stakingProvider.address, [ application1Mock.address, @@ -3504,12 +1518,12 @@ describe("TokenStaking", () => { auxiliaryAccount.address, ] ) - await extendedTokenStaking.setAuthorization( + await tokenStaking.setAuthorization( stakingProvider.address, application2Mock.address, amount ) - await extendedTokenStaking.cleanAuthorizedApplications( + await tokenStaking.cleanAuthorizedApplications( stakingProvider.address, 2 ) @@ -3517,7 +1531,7 @@ describe("TokenStaking", () => { it("should remove first and last applications", async () => { expect( - await extendedTokenStaking.getAuthorizedApplications( + await tokenStaking.getAuthorizedApplications( stakingProvider.address ) ).to.deep.equal([application2Mock.address]) @@ -3550,19 +1564,4 @@ describe("TokenStaking", () => { "invalid deauthorizingTo" ).to.equal(expectedDeauthorizingTo) } - - async function assertSlashingQueue( - index, - expectedStakingProviderAddress, - expectedAmount - ) { - expect( - (await tokenStaking.slashingQueue(index)).stakingProvider, - "invalid stakingProvider" - ).to.equal(expectedStakingProviderAddress) - expect( - (await tokenStaking.slashingQueue(index)).amount, - "invalid amount" - ).to.equal(expectedAmount) - } }) From 6ff4985464e5f18141faeb9d736cc8a1fdf42f96 Mon Sep 17 00:00:00 2001 From: Viktoriia Zotova Date: Thu, 6 Feb 2025 16:38:58 -0500 Subject: [PATCH 02/10] Adding method to deauth stakes that more than 15m T --- contracts/staking/TokenStaking.sol | 59 ++++-- contracts/test/TokenStakingTestSet.sol | 12 +- test/staking/TokenStaking.test.js | 250 +++++++++++++++++++++++-- 3 files changed, 281 insertions(+), 40 deletions(-) diff --git a/contracts/staking/TokenStaking.sol b/contracts/staking/TokenStaking.sol index d05665db..ebe20402 100644 --- a/contracts/staking/TokenStaking.sol +++ b/contracts/staking/TokenStaking.sol @@ -39,13 +39,6 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { using PercentUtils for uint256; using SafeCastUpgradeable for uint256; - // enum is used for Staked event to have backward compatibility - enum StakeType { - NU, - KEEP, - T - } - enum ApplicationStatus { NOT_APPROVED, APPROVED, @@ -81,10 +74,8 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { uint96 amount; } - uint256 internal constant SLASHING_REWARD_PERCENT = 5; uint256 internal constant MIN_STAKE_TIME = 24 hours; - uint256 internal constant GAS_LIMIT_AUTHORIZATION_DECREASE = 250000; - uint256 internal constant CONVERSION_DIVISOR = 10**(18 - 3); + uint96 internal constant MAX_STAKE = 15 * 10**(18 + 6); // 15m T /// @custom:oz-upgrades-unsafe-allow state-variable-immutable T internal immutable token; @@ -110,7 +101,6 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { // slither-disable-next-line constable-states uint256 private legacySlashingQueueIndex; - event MinimumStakeAmountSet(uint96 amount); event ApplicationStatusChanged( address indexed application, @@ -361,6 +351,51 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { cleanAuthorizedApplications(stakingProviderStruct, 1); } + // TODO consider rename + // @dev decreases to 15m T + function forceCapDecreaseAuthorization(address stakingProvider) external { + //override { + StakingProviderInfo storage stakingProviderStruct = stakingProviders[ + stakingProvider + ]; + uint96 deauthorized = 0; + for ( + uint256 i = 0; + i < stakingProviderStruct.authorizedApplications.length; + i++ + ) { + address application = stakingProviderStruct.authorizedApplications[ + i + ]; + AppAuthorization storage authorization = stakingProviders[ + stakingProvider + ].authorizations[application]; + uint96 authorized = authorization.authorized; + if (authorized > MAX_STAKE) { + IApplication(application).involuntaryAuthorizationDecrease( + stakingProvider, + authorized, + MAX_STAKE + ); + authorization.authorized = MAX_STAKE; + if (authorization.authorized < authorization.deauthorizing) { + authorization.deauthorizing = authorization.authorized; + } + + deauthorized += authorized - MAX_STAKE; + + emit AuthorizationDecreaseApproved( + stakingProvider, + application, + authorized, + MAX_STAKE + ); + } + } + + require(deauthorized > 0, "Nothing was deauthorized"); + } + /// @notice Pauses the given application’s eligibility to slash stakes. /// Besides that stakers can't change authorization to the application. /// Can be called only by the Panic Button of the particular @@ -438,7 +473,6 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { emit AuthorizationCeilingSet(ceiling); } - // // // Undelegating a stake (unstaking) @@ -497,7 +531,6 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { token.safeTransfer(recipient, amount); } - /// @notice Delegate voting power from the stake associated to the /// `stakingProvider` to a `delegatee` address. Caller must be the /// owner of this stake. diff --git a/contracts/test/TokenStakingTestSet.sol b/contracts/test/TokenStakingTestSet.sol index fbee0175..bc86ee0e 100644 --- a/contracts/test/TokenStakingTestSet.sol +++ b/contracts/test/TokenStakingTestSet.sol @@ -78,13 +78,10 @@ contract ApplicationMock is IApplication { toAmount != stakingProviderStruct.authorized, "Nothing to decrease" ); - uint96 decrease = stakingProviderStruct.authorized - toAmount; - if (stakingProviderStruct.deauthorizingTo > decrease) { - stakingProviderStruct.deauthorizingTo -= decrease; - } else { - stakingProviderStruct.deauthorizingTo = 0; - } stakingProviderStruct.authorized = toAmount; + if (stakingProviderStruct.deauthorizingTo > toAmount) { + stakingProviderStruct.deauthorizingTo = toAmount; + } } } @@ -213,7 +210,6 @@ contract ExtendedTokenStaking is TokenStaking { token.safeTransferFrom(msg.sender, address(this), amount); } - /// @notice Increases the authorization of the given staking provider for /// the given application by the given amount. Can only be called by /// the given staking provider’s authorizer. @@ -279,6 +275,4 @@ contract ExtendedTokenStaking is TokenStaking { { newStakeCheckpoint(_delegator, _amount, true); } - - } diff --git a/test/staking/TokenStaking.test.js b/test/staking/TokenStaking.test.js index f04fd3e5..5ecc3a0a 100644 --- a/test/staking/TokenStaking.test.js +++ b/test/staking/TokenStaking.test.js @@ -6,12 +6,6 @@ const { to1e18 } = helpers.number const { AddressZero, Zero } = ethers.constants -const StakeTypes = { - NU: 0, - KEEP: 1, - T: 2, -} - const ApplicationStatus = { NOT_APPROVED: 0, APPROVED: 1, @@ -27,10 +21,6 @@ describe("TokenStaking", () => { const tAllocation = to1e18("4500000000") // 4.5 Billion - function rewardFromPenalty(penalty, rewardMultiplier) { - return penalty.mul(5).div(100).mul(rewardMultiplier).div(100) - } - let tokenStaking let deployer @@ -71,7 +61,9 @@ describe("TokenStaking", () => { .connect(deployer) .transfer(otherStaker.address, initialStakerBalance) - const ExtendedTokenStaking = await ethers.getContractFactory("ExtendedTokenStaking") + const ExtendedTokenStaking = await ethers.getContractFactory( + "ExtendedTokenStaking" + ) const tokenStakingInitializerArgs = [] tokenStaking = await upgrades.deployProxy( ExtendedTokenStaking, @@ -921,6 +913,190 @@ describe("TokenStaking", () => { }) }) + describe("forceCapDecreaseAuthorization", () => { + const maxStake = to1e18("15000000") // 15m + const amount = maxStake.mul(2) + + beforeEach(async () => { + await tToken.connect(deployer).transfer(staker.address, amount) + + await tokenStaking + .connect(deployer) + .approveApplication(application1Mock.address) + await tToken.connect(staker).approve(tokenStaking.address, amount) + await tokenStaking + .connect(staker) + .stake( + stakingProvider.address, + beneficiary.address, + authorizer.address, + amount + ) + }) + + context("when stake is less than 15m", () => { + it("should revert", async () => { + await tokenStaking + .connect(authorizer) + .increaseAuthorization( + stakingProvider.address, + application1Mock.address, + maxStake + ) + await expect( + tokenStaking + .connect(authorizer) + .forceCapDecreaseAuthorization(stakingProvider.address) + ).to.be.revertedWith("Nothing was deauthorized") + }) + }) + + context("when authorization is more than 15m for one application", () => { + let tx + const amount2 = maxStake.div(3) + + beforeEach(async () => { + await tokenStaking + .connect(deployer) + .approveApplication(application2Mock.address) + + await tokenStaking + .connect(authorizer) + .increaseAuthorization( + stakingProvider.address, + application1Mock.address, + amount + ) + + await tokenStaking + .connect(authorizer) + .increaseAuthorization( + stakingProvider.address, + application2Mock.address, + amount2 + ) + + tx = await tokenStaking + .connect(deployer) + .forceCapDecreaseAuthorization(stakingProvider.address) + }) + + it("should set authorized amount to max", async () => { + expect( + await tokenStaking.authorizedStake( + stakingProvider.address, + application1Mock.address + ) + ).to.equal(maxStake) + expect( + await tokenStaking.authorizedStake( + stakingProvider.address, + application2Mock.address + ) + ).to.equal(amount2) + }) + + it("should emit AuthorizationDecreaseApproved", async () => { + await expect(tx) + .to.emit(tokenStaking, "AuthorizationDecreaseApproved") + .withArgs( + stakingProvider.address, + application1Mock.address, + amount, + maxStake + ) + }) + }) + + context("when previous deathorization is in process", () => { + let tx + const amount2 = maxStake.div(3) + const amountToDecrease = amount.div(20) + + beforeEach(async () => { + await tokenStaking + .connect(deployer) + .approveApplication(application2Mock.address) + + await tokenStaking + .connect(authorizer) + .increaseAuthorization( + stakingProvider.address, + application1Mock.address, + amount + ) + + await tokenStaking + .connect(authorizer) + .increaseAuthorization( + stakingProvider.address, + application2Mock.address, + amount2 + ) + + await tokenStaking + .connect(authorizer) + ["requestAuthorizationDecrease(address,address,uint96)"]( + stakingProvider.address, + application1Mock.address, + amountToDecrease + ) + await tokenStaking + .connect(authorizer) + ["requestAuthorizationDecrease(address,address,uint96)"]( + stakingProvider.address, + application2Mock.address, + amountToDecrease + ) + + tx = await tokenStaking + .connect(deployer) + .forceCapDecreaseAuthorization(stakingProvider.address) + }) + + it("should set authorized amount to max", async () => { + expect( + await tokenStaking.authorizedStake( + stakingProvider.address, + application1Mock.address + ) + ).to.equal(maxStake) + expect( + await tokenStaking.authorizedStake( + stakingProvider.address, + application2Mock.address + ) + ).to.equal(amount2) + }) + + it("should send request to application with last amount", async () => { + await assertApplicationStakingProviders( + application1Mock, + stakingProvider.address, + maxStake, + maxStake + ) + await assertApplicationStakingProviders( + application2Mock, + stakingProvider.address, + amount2, + amount2.sub(amountToDecrease) + ) + }) + + it("should emit AuthorizationDecreaseApproved", async () => { + await expect(tx) + .to.emit(tokenStaking, "AuthorizationDecreaseApproved") + .withArgs( + stakingProvider.address, + application1Mock.address, + amount, + maxStake + ) + }) + }) + }) + describe("pauseApplication", () => { beforeEach(async () => { await tokenStaking @@ -1429,10 +1605,10 @@ describe("TokenStaking", () => { context("when all authorized applications with 0 authorization", () => { beforeEach(async () => { - await tokenStaking.setAuthorizedApplications( - stakingProvider.address, - [application1Mock.address, application2Mock.address] - ) + await tokenStaking.setAuthorizedApplications(stakingProvider.address, [ + application1Mock.address, + application2Mock.address, + ]) await tokenStaking.cleanAuthorizedApplications( stakingProvider.address, 2 @@ -1441,9 +1617,7 @@ describe("TokenStaking", () => { it("should remove all applications", async () => { expect( - await tokenStaking.getAuthorizedApplications( - stakingProvider.address - ) + await tokenStaking.getAuthorizedApplications(stakingProvider.address) ).to.deep.equal([]) }) }) @@ -1540,6 +1714,46 @@ describe("TokenStaking", () => { ) }) + describe("delegateVoting", () => { + const amount = initialStakerBalance + + context("after vote delegation", () => { + beforeEach(async () => { + await tToken.connect(staker).approve(tokenStaking.address, amount) + await tokenStaking + .connect(staker) + .stake( + stakingProvider.address, + beneficiary.address, + authorizer.address, + amount + ) + + tx = await tokenStaking + .connect(staker) + .delegateVoting(stakingProvider.address, delegatee.address) + }) + + it("checkpoint for staked total supply should remain constant", async () => { + const lastBlock = await mineBlocks(1) + expect(await tokenStaking.getPastTotalSupply(lastBlock - 1)).to.equal( + amount + ) + }) + + it("should create a new checkpoint for staker's delegatee", async () => { + expect(await tokenStaking.getVotes(delegatee.address)).to.equal(amount) + }) + + it("shouldn't create a new checkpoint for any stake role", async () => { + expect(await tokenStaking.getVotes(staker.address)).to.equal(0) + expect(await tokenStaking.getVotes(stakingProvider.address)).to.equal(0) + expect(await tokenStaking.getVotes(beneficiary.address)).to.equal(0) + expect(await tokenStaking.getVotes(authorizer.address)).to.equal(0) + }) + }) + }) + async function assertStake(address, expectedTStake) { expect(await tokenStaking.stakeAmount(address), "invalid tStake").to.equal( expectedTStake From 1b5160c893ca045f31fbfaa38b4ae3e25a200904 Mon Sep 17 00:00:00 2001 From: Viktoriia Zotova Date: Fri, 7 Feb 2025 16:26:04 -0500 Subject: [PATCH 03/10] Method to force deauth multiple stakes and different calculation of existing deauthorization --- contracts/staking/TokenStaking.sol | 26 ++++-- contracts/test/TokenStakingTestSet.sol | 10 +++ test/staking/TokenStaking.test.js | 115 ++++++++++++++++++++----- 3 files changed, 126 insertions(+), 25 deletions(-) diff --git a/contracts/staking/TokenStaking.sol b/contracts/staking/TokenStaking.sol index ebe20402..0ebabf84 100644 --- a/contracts/staking/TokenStaking.sol +++ b/contracts/staking/TokenStaking.sol @@ -353,7 +353,18 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { // TODO consider rename // @dev decreases to 15m T - function forceCapDecreaseAuthorization(address stakingProvider) external { + function forceCapDecreaseAuthorization(address[] memory _stakingProviders) + external + { + require(_stakingProviders.length > 0, "Wrong input parameters"); + for (uint256 i = 0; i < _stakingProviders.length; i++) { + forceCapDecreaseAuthorization(_stakingProviders[i]); + } + } + + // TODO consider rename + // @dev decreases to 15m T + function forceCapDecreaseAuthorization(address stakingProvider) public { //override { StakingProviderInfo storage stakingProviderStruct = stakingProviders[ stakingProvider @@ -377,12 +388,17 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { authorized, MAX_STAKE ); - authorization.authorized = MAX_STAKE; - if (authorization.authorized < authorization.deauthorizing) { - authorization.deauthorizing = authorization.authorized; + uint96 decrease = authorized - MAX_STAKE; + + if (authorization.deauthorizing >= decrease) { + authorization.deauthorizing -= decrease; + } else { + authorization.deauthorizing = 0; + // TODO cancel deauth? how? check tBTC } - deauthorized += authorized - MAX_STAKE; + authorization.authorized = MAX_STAKE; + deauthorized += decrease; emit AuthorizationDecreaseApproved( stakingProvider, diff --git a/contracts/test/TokenStakingTestSet.sol b/contracts/test/TokenStakingTestSet.sol index bc86ee0e..0d2f056d 100644 --- a/contracts/test/TokenStakingTestSet.sol +++ b/contracts/test/TokenStakingTestSet.sol @@ -275,4 +275,14 @@ contract ExtendedTokenStaking is TokenStaking { { newStakeCheckpoint(_delegator, _amount, true); } + + function getDeauthorizingAmount( + address stakingProvider, + address application + ) external view returns (uint96) { + return + stakingProviders[stakingProvider] + .authorizations[application] + .deauthorizing; + } } diff --git a/test/staking/TokenStaking.test.js b/test/staking/TokenStaking.test.js index 5ecc3a0a..ef5dc722 100644 --- a/test/staking/TokenStaking.test.js +++ b/test/staking/TokenStaking.test.js @@ -946,7 +946,7 @@ describe("TokenStaking", () => { await expect( tokenStaking .connect(authorizer) - .forceCapDecreaseAuthorization(stakingProvider.address) + ["forceCapDecreaseAuthorization(address)"](stakingProvider.address) ).to.be.revertedWith("Nothing was deauthorized") }) }) @@ -978,7 +978,7 @@ describe("TokenStaking", () => { tx = await tokenStaking .connect(deployer) - .forceCapDecreaseAuthorization(stakingProvider.address) + ["forceCapDecreaseAuthorization(address)"](stakingProvider.address) }) it("should set authorized amount to max", async () => { @@ -1008,10 +1008,10 @@ describe("TokenStaking", () => { }) }) - context("when previous deathorization is in process", () => { + context("when previous deauthorization is in process", () => { let tx - const amount2 = maxStake.div(3) const amountToDecrease = amount.div(20) + const amountToDecrease2 = maxStake.add(amountToDecrease) beforeEach(async () => { await tokenStaking @@ -1031,7 +1031,7 @@ describe("TokenStaking", () => { .increaseAuthorization( stakingProvider.address, application2Mock.address, - amount2 + amount ) await tokenStaking @@ -1046,12 +1046,12 @@ describe("TokenStaking", () => { ["requestAuthorizationDecrease(address,address,uint96)"]( stakingProvider.address, application2Mock.address, - amountToDecrease + amountToDecrease2 ) tx = await tokenStaking .connect(deployer) - .forceCapDecreaseAuthorization(stakingProvider.address) + ["forceCapDecreaseAuthorization(address)"](stakingProvider.address) }) it("should set authorized amount to max", async () => { @@ -1066,22 +1066,89 @@ describe("TokenStaking", () => { stakingProvider.address, application2Mock.address ) - ).to.equal(amount2) + ).to.equal(maxStake) }) it("should send request to application with last amount", async () => { - await assertApplicationStakingProviders( - application1Mock, - stakingProvider.address, - maxStake, - maxStake - ) - await assertApplicationStakingProviders( - application2Mock, - stakingProvider.address, - amount2, - amount2.sub(amountToDecrease) - ) + expect( + await tokenStaking.getDeauthorizingAmount( + stakingProvider.address, + application1Mock.address + ) + ).to.equal(0) + expect( + await tokenStaking.getDeauthorizingAmount( + stakingProvider.address, + application2Mock.address + ) + ).to.equal(amountToDecrease) + }) + + it("should emit AuthorizationDecreaseApproved", async () => { + await expect(tx) + .to.emit(tokenStaking, "AuthorizationDecreaseApproved") + .withArgs( + stakingProvider.address, + application1Mock.address, + amount, + maxStake + ) + }) + }) + + context("when sending transaction for multiple staking providers", () => { + let tx + + beforeEach(async () => { + await tToken.connect(deployer).transfer(otherStaker.address, amount) + + await tToken.connect(otherStaker).approve(tokenStaking.address, amount) + await tokenStaking + .connect(otherStaker) + .stake( + otherStaker.address, + otherStaker.address, + otherStaker.address, + amount + ) + + await tokenStaking + .connect(authorizer) + .increaseAuthorization( + stakingProvider.address, + application1Mock.address, + amount + ) + + await tokenStaking + .connect(otherStaker) + .increaseAuthorization( + otherStaker.address, + application1Mock.address, + amount + ) + + tx = await tokenStaking + .connect(deployer) + ["forceCapDecreaseAuthorization(address[])"]([ + stakingProvider.address, + otherStaker.address, + ]) + }) + + it("should set authorized amount to max", async () => { + expect( + await tokenStaking.authorizedStake( + stakingProvider.address, + application1Mock.address + ) + ).to.equal(maxStake) + expect( + await tokenStaking.authorizedStake( + otherStaker.address, + application1Mock.address + ) + ).to.equal(maxStake) }) it("should emit AuthorizationDecreaseApproved", async () => { @@ -1093,6 +1160,14 @@ describe("TokenStaking", () => { amount, maxStake ) + await expect(tx) + .to.emit(tokenStaking, "AuthorizationDecreaseApproved") + .withArgs( + otherStaker.address, + application1Mock.address, + amount, + maxStake + ) }) }) }) From cb4b2281a27de1c827422286679145c70778da24 Mon Sep 17 00:00:00 2001 From: Viktoriia Zotova Date: Mon, 10 Feb 2025 11:12:21 -0500 Subject: [PATCH 04/10] Adds method to force deauthorization of Beta stakers --- contracts/staking/TokenStaking.sol | 85 ++++++-- test/staking/TokenStaking.test.js | 325 +++++++++++++++++++++++++++++ 2 files changed, 395 insertions(+), 15 deletions(-) diff --git a/contracts/staking/TokenStaking.sol b/contracts/staking/TokenStaking.sol index 0ebabf84..4a0a1f9c 100644 --- a/contracts/staking/TokenStaking.sol +++ b/contracts/staking/TokenStaking.sol @@ -335,18 +335,10 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { StakingProviderInfo storage stakingProviderStruct = stakingProviders[ stakingProvider ]; - AppAuthorization storage authorization = stakingProviderStruct - .authorizations[application]; - uint96 fromAmount = authorization.authorized; - require(fromAmount > 0, "Application is not authorized"); - authorization.authorized = 0; - authorization.deauthorizing = 0; - - emit AuthorizationDecreaseApproved( + forceDecreaseAuthorization( stakingProvider, - application, - fromAmount, - 0 + stakingProviderStruct, + application ); cleanAuthorizedApplications(stakingProviderStruct, 1); } @@ -362,8 +354,9 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { } } + /// @notice Forced deauthorization of stake above 15m T. + /// Can be called by anyone. // TODO consider rename - // @dev decreases to 15m T function forceCapDecreaseAuthorization(address stakingProvider) public { //override { StakingProviderInfo storage stakingProviderStruct = stakingProviders[ @@ -378,9 +371,8 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { address application = stakingProviderStruct.authorizedApplications[ i ]; - AppAuthorization storage authorization = stakingProviders[ - stakingProvider - ].authorizations[application]; + AppAuthorization storage authorization = stakingProviderStruct + .authorizations[application]; uint96 authorized = authorization.authorized; if (authorized > MAX_STAKE) { IApplication(application).involuntaryAuthorizationDecrease( @@ -412,6 +404,47 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { require(deauthorized > 0, "Nothing was deauthorized"); } + /// @notice Forced deauthorization of Beta staker. + /// Can be called only by the governance. + function forceBetaStakersCapDecreaseAuthorization(address betaStaker) + public + onlyGovernance + { + StakingProviderInfo storage stakingProviderStruct = stakingProviders[ + betaStaker + ]; + uint256 authorizedApplications = stakingProviderStruct + .authorizedApplications + .length; + + require(authorizedApplications > 0, "Nothing was authorized"); + for (uint256 i = 0; i < authorizedApplications; i++) { + address application = stakingProviderStruct.authorizedApplications[ + i + ]; + forceDecreaseAuthorization( + betaStaker, + stakingProviderStruct, + application + ); + } + cleanAuthorizedApplications( + stakingProviderStruct, + authorizedApplications + ); + } + + /// @notice Forced deauthorization of Beta stakers. + /// Can be called only by the governance. + function forceBetaStakersCapDecreaseAuthorization( + address[] memory betaStakers + ) external { + require(betaStakers.length > 0, "Wrong input parameters"); + for (uint256 i = 0; i < betaStakers.length; i++) { + forceBetaStakersCapDecreaseAuthorization(betaStakers[i]); + } + } + /// @notice Pauses the given application’s eligibility to slash stakes. /// Besides that stakers can't change authorization to the application. /// Can be called only by the Panic Button of the particular @@ -808,6 +841,28 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { } } + /// @notice Decreases the authorization for the given `stakingProvider` on + /// the given `application`, for all authorized amount. + function forceDecreaseAuthorization( + address stakingProvider, + StakingProviderInfo storage stakingProviderStruct, + address application + ) internal { + AppAuthorization storage authorization = stakingProviderStruct + .authorizations[application]; + uint96 fromAmount = authorization.authorized; + require(fromAmount > 0, "Application is not authorized"); + authorization.authorized = 0; + authorization.deauthorizing = 0; + + emit AuthorizationDecreaseApproved( + stakingProvider, + application, + fromAmount, + 0 + ); + } + /// @notice Creates new checkpoints due to a change of stake amount /// @param _delegator Address of the staking provider acting as delegator /// @param _amount Amount of T to increment diff --git a/test/staking/TokenStaking.test.js b/test/staking/TokenStaking.test.js index ef5dc722..7ca3a8d4 100644 --- a/test/staking/TokenStaking.test.js +++ b/test/staking/TokenStaking.test.js @@ -1172,6 +1172,331 @@ describe("TokenStaking", () => { }) }) + describe("forceBetaStakersCapDecreaseAuthorization", () => { + const amount = initialStakerBalance + + beforeEach(async () => { + await tToken.connect(deployer).transfer(staker.address, amount.mul(10)) + + await tokenStaking + .connect(deployer) + .approveApplication(application1Mock.address) + await tToken.connect(staker).approve(tokenStaking.address, amount.mul(10)) + await tokenStaking + .connect(staker) + .stake( + stakingProvider.address, + beneficiary.address, + authorizer.address, + amount + ) + }) + + context("when caller is not the governance", () => { + it("should revert", async () => { + await expect( + tokenStaking + .connect(stakingProvider) + ["forceBetaStakersCapDecreaseAuthorization(address)"]( + stakingProvider.address + ) + ).to.be.revertedWith("Caller is not the governance") + }) + }) + + context("when nothing was authorized", () => { + it("should revert", async () => { + await expect( + tokenStaking + .connect(deployer) + ["forceBetaStakersCapDecreaseAuthorization(address)"]( + stakingProvider.address + ) + ).to.be.revertedWith("Nothing was authorized") + }) + }) + + context("when deauthorizing one beta staker", () => { + let tx + const amount2 = amount.div(3) + + beforeEach(async () => { + await tokenStaking + .connect(deployer) + .approveApplication(application2Mock.address) + + await tokenStaking + .connect(authorizer) + .increaseAuthorization( + stakingProvider.address, + application1Mock.address, + amount + ) + + await tokenStaking + .connect(authorizer) + .increaseAuthorization( + stakingProvider.address, + application2Mock.address, + amount2 + ) + + tx = await tokenStaking + .connect(deployer) + ["forceBetaStakersCapDecreaseAuthorization(address)"]( + stakingProvider.address + ) + }) + + it("should set authorized amount to 0", async () => { + expect( + await tokenStaking.authorizedStake( + stakingProvider.address, + application1Mock.address + ) + ).to.equal(0) + expect( + await tokenStaking.authorizedStake( + stakingProvider.address, + application2Mock.address + ) + ).to.equal(0) + }) + + it("should not send request to the applications", async () => { + await assertApplicationStakingProviders( + application1Mock, + stakingProvider.address, + amount, + 0 + ) + await assertApplicationStakingProviders( + application2Mock, + stakingProvider.address, + amount2, + 0 + ) + }) + + it("should emit AuthorizationDecreaseApproved", async () => { + await expect(tx) + .to.emit(tokenStaking, "AuthorizationDecreaseApproved") + .withArgs( + stakingProvider.address, + application1Mock.address, + amount, + 0 + ) + await expect(tx) + .to.emit(tokenStaking, "AuthorizationDecreaseApproved") + .withArgs( + stakingProvider.address, + application2Mock.address, + amount2, + 0 + ) + }) + }) + + context("when previous deauthorization is in process", () => { + let tx + const amountToDecrease = amount.div(20) + const amountToDecrease2 = amountToDecrease.mul(2) + + beforeEach(async () => { + await tokenStaking + .connect(deployer) + .approveApplication(application2Mock.address) + + await tokenStaking + .connect(authorizer) + .increaseAuthorization( + stakingProvider.address, + application1Mock.address, + amount + ) + + await tokenStaking + .connect(authorizer) + .increaseAuthorization( + stakingProvider.address, + application2Mock.address, + amount + ) + + await tokenStaking + .connect(authorizer) + ["requestAuthorizationDecrease(address,address,uint96)"]( + stakingProvider.address, + application1Mock.address, + amountToDecrease + ) + await tokenStaking + .connect(authorizer) + ["requestAuthorizationDecrease(address,address,uint96)"]( + stakingProvider.address, + application2Mock.address, + amountToDecrease2 + ) + + tx = await tokenStaking + .connect(deployer) + ["forceBetaStakersCapDecreaseAuthorization(address)"]( + stakingProvider.address + ) + }) + + it("should set authorized amount to 0", async () => { + expect( + await tokenStaking.authorizedStake( + stakingProvider.address, + application1Mock.address + ) + ).to.equal(0) + expect( + await tokenStaking.authorizedStake( + stakingProvider.address, + application2Mock.address + ) + ).to.equal(0) + }) + + it("should not send request to the applications", async () => { + await assertApplicationStakingProviders( + application1Mock, + stakingProvider.address, + amount, + amount.sub(amountToDecrease) + ) + await assertApplicationStakingProviders( + application2Mock, + stakingProvider.address, + amount, + amount.sub(amountToDecrease2) + ) + }) + + it("should set deauthorizing amount to 0", async () => { + expect( + await tokenStaking.getDeauthorizingAmount( + stakingProvider.address, + application1Mock.address + ) + ).to.equal(0) + expect( + await tokenStaking.getDeauthorizingAmount( + stakingProvider.address, + application2Mock.address + ) + ).to.equal(0) + }) + + it("should emit AuthorizationDecreaseApproved", async () => { + await expect(tx) + .to.emit(tokenStaking, "AuthorizationDecreaseApproved") + .withArgs( + stakingProvider.address, + application1Mock.address, + amount, + 0 + ) + await expect(tx) + .to.emit(tokenStaking, "AuthorizationDecreaseApproved") + .withArgs( + stakingProvider.address, + application2Mock.address, + amount, + 0 + ) + }) + }) + + context("when sending transaction for multiple staking providers", () => { + let tx + + beforeEach(async () => { + await tToken.connect(deployer).transfer(otherStaker.address, amount) + + await tToken.connect(otherStaker).approve(tokenStaking.address, amount) + await tokenStaking + .connect(otherStaker) + .stake( + otherStaker.address, + otherStaker.address, + otherStaker.address, + amount + ) + + await tokenStaking + .connect(authorizer) + .increaseAuthorization( + stakingProvider.address, + application1Mock.address, + amount + ) + + await tokenStaking + .connect(otherStaker) + .increaseAuthorization( + otherStaker.address, + application1Mock.address, + amount + ) + + tx = await tokenStaking + .connect(deployer) + ["forceBetaStakersCapDecreaseAuthorization(address[])"]([ + stakingProvider.address, + otherStaker.address, + ]) + }) + + it("should set authorized amount to 0", async () => { + expect( + await tokenStaking.authorizedStake( + stakingProvider.address, + application1Mock.address + ) + ).to.equal(0) + expect( + await tokenStaking.authorizedStake( + otherStaker.address, + application1Mock.address + ) + ).to.equal(0) + }) + + it("should not send request to the application", async () => { + await assertApplicationStakingProviders( + application1Mock, + stakingProvider.address, + amount, + 0 + ) + await assertApplicationStakingProviders( + application1Mock, + otherStaker.address, + amount, + 0 + ) + }) + + it("should emit AuthorizationDecreaseApproved", async () => { + await expect(tx) + .to.emit(tokenStaking, "AuthorizationDecreaseApproved") + .withArgs( + stakingProvider.address, + application1Mock.address, + amount, + 0 + ) + await expect(tx) + .to.emit(tokenStaking, "AuthorizationDecreaseApproved") + .withArgs(otherStaker.address, application1Mock.address, amount, 0) + }) + }) + }) + describe("pauseApplication", () => { beforeEach(async () => { await tokenStaking From 50df19c9463e532cde39630d36f3a404aa936dbd Mon Sep 17 00:00:00 2001 From: Viktoriia Zotova Date: Wed, 12 Feb 2025 19:55:45 -0500 Subject: [PATCH 05/10] Method to opt-out 50% of authorization amount --- contracts/staking/TokenStaking.sol | 224 ++++++++++++++-------- test/staking/TokenStaking.test.js | 293 ++++++++++++++++++++++++++++- 2 files changed, 431 insertions(+), 86 deletions(-) diff --git a/contracts/staking/TokenStaking.sol b/contracts/staking/TokenStaking.sol index 4a0a1f9c..5b019bf5 100644 --- a/contracts/staking/TokenStaking.sol +++ b/contracts/staking/TokenStaking.sol @@ -57,6 +57,7 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { address[] authorizedApplications; uint256 startStakingTimestamp; bool autoIncrease; + uint256 optOutAmount; } struct AppAuthorization { @@ -76,6 +77,7 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { uint256 internal constant MIN_STAKE_TIME = 24 hours; uint96 internal constant MAX_STAKE = 15 * 10**(18 + 6); // 15m T + uint96 internal constant HALF_MAX_STAKE = MAX_STAKE / 2; // 7.5m T /// @custom:oz-upgrades-unsafe-allow state-variable-immutable T internal immutable token; @@ -344,7 +346,8 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { } // TODO consider rename - // @dev decreases to 15m T + /// @notice Forced deauthorization of stake above 15m T. + /// Can be called by anyone. function forceCapDecreaseAuthorization(address[] memory _stakingProviders) external { @@ -354,94 +357,39 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { } } - /// @notice Forced deauthorization of stake above 15m T. - /// Can be called by anyone. - // TODO consider rename - function forceCapDecreaseAuthorization(address stakingProvider) public { - //override { - StakingProviderInfo storage stakingProviderStruct = stakingProviders[ - stakingProvider - ]; - uint96 deauthorized = 0; - for ( - uint256 i = 0; - i < stakingProviderStruct.authorizedApplications.length; - i++ - ) { - address application = stakingProviderStruct.authorizedApplications[ - i - ]; - AppAuthorization storage authorization = stakingProviderStruct - .authorizations[application]; - uint96 authorized = authorization.authorized; - if (authorized > MAX_STAKE) { - IApplication(application).involuntaryAuthorizationDecrease( - stakingProvider, - authorized, - MAX_STAKE - ); - uint96 decrease = authorized - MAX_STAKE; - - if (authorization.deauthorizing >= decrease) { - authorization.deauthorizing -= decrease; - } else { - authorization.deauthorizing = 0; - // TODO cancel deauth? how? check tBTC - } - - authorization.authorized = MAX_STAKE; - deauthorized += decrease; - - emit AuthorizationDecreaseApproved( - stakingProvider, - application, - authorized, - MAX_STAKE - ); - } - } - - require(deauthorized > 0, "Nothing was deauthorized"); - } - - /// @notice Forced deauthorization of Beta staker. - /// Can be called only by the governance. - function forceBetaStakersCapDecreaseAuthorization(address betaStaker) - public - onlyGovernance + /// @notice Allows to instantly deauthorize up to 50% of max authorization. + /// Can be called only by the delegation owner or the staking + /// provider. + function optOutDecreaseAuthorization(address stakingProvider, uint96 amount) + external + onlyOwnerOrStakingProvider(stakingProvider) { + require(amount > 0, "Parameters must be specified"); StakingProviderInfo storage stakingProviderStruct = stakingProviders[ - betaStaker + stakingProvider ]; - uint256 authorizedApplications = stakingProviderStruct - .authorizedApplications - .length; - - require(authorizedApplications > 0, "Nothing was authorized"); - for (uint256 i = 0; i < authorizedApplications; i++) { - address application = stakingProviderStruct.authorizedApplications[ - i - ]; - forceDecreaseAuthorization( - betaStaker, - stakingProviderStruct, - application - ); + ( + uint96 availableToOptOut, + uint96 maxAuthorization + ) = getAvailableOptOutAmount(stakingProvider, stakingProviderStruct); + if (maxAuthorization > MAX_STAKE) { + forceDecreaseAuthorization(stakingProvider, MAX_STAKE); + maxAuthorization = MAX_STAKE; + availableToOptOut = HALF_MAX_STAKE; } - cleanAuthorizedApplications( - stakingProviderStruct, - authorizedApplications - ); + require(availableToOptOut >= amount, "Opt-out is not available"); // TODO rephrase + forceDecreaseAuthorization(stakingProvider, maxAuthorization - amount); + stakingProviderStruct.optOutAmount += amount; } /// @notice Forced deauthorization of Beta stakers. /// Can be called only by the governance. - function forceBetaStakersCapDecreaseAuthorization( - address[] memory betaStakers - ) external { + function forceBetaStakerDecreaseAuthorization(address[] memory betaStakers) + external + { require(betaStakers.length > 0, "Wrong input parameters"); for (uint256 i = 0; i < betaStakers.length; i++) { - forceBetaStakersCapDecreaseAuthorization(betaStakers[i]); + forceBetaStakerDecreaseAuthorization(betaStakers[i]); } } @@ -732,6 +680,44 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { ); } + /// @notice Forced deauthorization of Beta staker. + /// Can be called only by the governance. + function forceBetaStakerDecreaseAuthorization(address betaStaker) + public + onlyGovernance + { + StakingProviderInfo storage stakingProviderStruct = stakingProviders[ + betaStaker + ]; + uint256 authorizedApplications = stakingProviderStruct + .authorizedApplications + .length; + + require(authorizedApplications > 0, "Nothing was authorized"); + for (uint256 i = 0; i < authorizedApplications; i++) { + address application = stakingProviderStruct.authorizedApplications[ + i + ]; + forceDecreaseAuthorization( + betaStaker, + stakingProviderStruct, + application + ); + } + cleanAuthorizedApplications( + stakingProviderStruct, + authorizedApplications + ); + } + + /// @notice Forced deauthorization of stake above 15m T. + /// Can be called by anyone. + // TODO consider rename + function forceCapDecreaseAuthorization(address stakingProvider) public { + //override { + forceDecreaseAuthorization(stakingProvider, MAX_STAKE); + } + /// @notice Returns the maximum application authorization function getMaxAuthorization(address stakingProvider) public @@ -779,6 +765,21 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { } } + /// @notice Returns available amount to instantly deauthorize. + function getAvailableOptOutAmount(address stakingProvider) + public + view + returns (uint96 availableToOptOut) + { + StakingProviderInfo storage stakingProviderStruct = stakingProviders[ + stakingProvider + ]; + (availableToOptOut, ) = getAvailableOptOutAmount( + stakingProvider, + stakingProviderStruct + ); + } + /// @notice Delegate voting power from the stake associated to the /// `stakingProvider` to a `delegatee` address. Caller must be the owner /// of this stake. @@ -905,4 +906,69 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { governance = newGuvnor; emit GovernanceTransferred(oldGuvnor, newGuvnor); } + + function forceDecreaseAuthorization( + address stakingProvider, + uint96 amountTo + ) internal { + StakingProviderInfo storage stakingProviderStruct = stakingProviders[ + stakingProvider + ]; + uint96 deauthorized = 0; + for ( + uint256 i = 0; + i < stakingProviderStruct.authorizedApplications.length; + i++ + ) { + address application = stakingProviderStruct.authorizedApplications[ + i + ]; + AppAuthorization storage authorization = stakingProviderStruct + .authorizations[application]; + uint96 authorized = authorization.authorized; + if (authorized > amountTo) { + IApplication(application).involuntaryAuthorizationDecrease( + stakingProvider, + authorized, + amountTo + ); + uint96 decrease = authorized - amountTo; + + if (authorization.deauthorizing >= decrease) { + authorization.deauthorizing -= decrease; + } else { + authorization.deauthorizing = 0; + } + + authorization.authorized = amountTo; + deauthorized += decrease; + + emit AuthorizationDecreaseApproved( + stakingProvider, + application, + authorized, + amountTo + ); + } + } + + require(deauthorized > 0, "Nothing was deauthorized"); + } + + function getAvailableOptOutAmount( + address stakingProvider, + StakingProviderInfo storage stakingProviderStruct + ) + internal + view + returns (uint96 availableToOptOut, uint96 maxAuthorization) + { + maxAuthorization = getMaxAuthorization(stakingProvider); + uint96 optOutAmount = stakingProviderStruct.optOutAmount.toUint96(); + if (maxAuthorization < optOutAmount) { + availableToOptOut = 0; + } else { + availableToOptOut = (maxAuthorization - optOutAmount) / 2; + } + } } diff --git a/test/staking/TokenStaking.test.js b/test/staking/TokenStaking.test.js index 7ca3a8d4..5e207f2d 100644 --- a/test/staking/TokenStaking.test.js +++ b/test/staking/TokenStaking.test.js @@ -600,7 +600,9 @@ describe("TokenStaking", () => { await tokenStaking .connect(authorizer) ["requestAuthorizationDecrease(address)"](stakingProvider.address) - application1Mock.approveAuthorizationDecrease(stakingProvider.address) + await application1Mock.approveAuthorizationDecrease( + stakingProvider.address + ) await expect( application1Mock.approveAuthorizationDecrease(stakingProvider.address) ).to.be.revertedWith("No deauthorizing in process") @@ -1172,7 +1174,284 @@ describe("TokenStaking", () => { }) }) - describe("forceBetaStakersCapDecreaseAuthorization", () => { + describe("optOutDecreaseAuthorization", () => { + const maxStake = to1e18("15000000") // 15m + const amount = maxStake.mul(2) + + beforeEach(async () => { + await tToken.connect(deployer).transfer(staker.address, amount) + + await tokenStaking + .connect(deployer) + .approveApplication(application1Mock.address) + await tToken.connect(staker).approve(tokenStaking.address, amount) + await tokenStaking + .connect(staker) + .stake( + stakingProvider.address, + beneficiary.address, + authorizer.address, + amount + ) + }) + + context("when staking provider has no stake", () => { + it("should revert", async () => { + await expect( + tokenStaking.optOutDecreaseAuthorization(stakingProvider.address, 0) + ).to.be.revertedWith("Not owner or provider") + }) + }) + + context("when amount to decrease is zero", () => { + it("should revert", async () => { + await expect( + tokenStaking + .connect(stakingProvider) + .optOutDecreaseAuthorization(stakingProvider.address, 0) + ).to.be.revertedWith("Parameters must be specified") + }) + }) + + context("when request too big amount for opt-out", () => { + const amount2 = maxStake.div(2) + const amountToOptOut = amount2.div(2) + + it("should revert", async () => { + await expect( + tokenStaking + .connect(stakingProvider) + .optOutDecreaseAuthorization(stakingProvider.address, 1) + ).to.be.revertedWith("Opt-out is not available") + + await tokenStaking + .connect(authorizer) + .increaseAuthorization( + stakingProvider.address, + application1Mock.address, + amount2 + ) + await expect( + tokenStaking + .connect(stakingProvider) + .optOutDecreaseAuthorization( + stakingProvider.address, + amountToOptOut.add(1) + ) + ).to.be.revertedWith("Opt-out is not available") + + await tokenStaking + .connect(stakingProvider) + .optOutDecreaseAuthorization(stakingProvider.address, amountToOptOut) + await expect( + tokenStaking + .connect(stakingProvider) + .optOutDecreaseAuthorization(stakingProvider.address, 1) + ).to.be.revertedWith("Opt-out is not available") + }) + }) + + context("when authorization is more than 15m for one application", () => { + let tx + const amount2 = maxStake.sub(1) + const amountToOptOut = maxStake.div(4) + const expectedAmount = maxStake.sub(amountToOptOut) + + beforeEach(async () => { + await tokenStaking + .connect(deployer) + .approveApplication(application2Mock.address) + + await tokenStaking + .connect(authorizer) + .increaseAuthorization( + stakingProvider.address, + application1Mock.address, + amount + ) + + await tokenStaking + .connect(authorizer) + .increaseAuthorization( + stakingProvider.address, + application2Mock.address, + amount2 + ) + + tx = await tokenStaking + .connect(stakingProvider) + .optOutDecreaseAuthorization(stakingProvider.address, amountToOptOut) + }) + + it("should update authorized amount", async () => { + expect( + await tokenStaking.authorizedStake( + stakingProvider.address, + application1Mock.address + ) + ).to.equal(expectedAmount) + expect( + await tokenStaking.authorizedStake( + stakingProvider.address, + application2Mock.address + ) + ).to.equal(expectedAmount) + }) + + it("should update available opt-out amount", async () => { + expect( + await tokenStaking.getAvailableOptOutAmount(stakingProvider.address) + ).to.equal(maxStake.div(2).sub(amountToOptOut)) + }) + + it("should emit AuthorizationDecreaseApproved", async () => { + await expect(tx) + .to.emit(tokenStaking, "AuthorizationDecreaseApproved") + .withArgs( + stakingProvider.address, + application1Mock.address, + amount, + maxStake + ) + await expect(tx) + .to.emit(tokenStaking, "AuthorizationDecreaseApproved") + .withArgs( + stakingProvider.address, + application1Mock.address, + maxStake, + expectedAmount + ) + await expect(tx) + .to.emit(tokenStaking, "AuthorizationDecreaseApproved") + .withArgs( + stakingProvider.address, + application2Mock.address, + amount2, + expectedAmount + ) + }) + + context("when use all opt-out amount and decrease after", () => { + beforeEach(async () => { + await tokenStaking + .connect(stakingProvider) + .optOutDecreaseAuthorization( + stakingProvider.address, + maxStake.div(2).sub(amountToOptOut) + ) + + await tokenStaking + .connect(authorizer) + ["requestAuthorizationDecrease(address,address,uint96)"]( + stakingProvider.address, + application1Mock.address, + maxStake.div(4) + ) + await tokenStaking + .connect(authorizer) + ["requestAuthorizationDecrease(address,address,uint96)"]( + stakingProvider.address, + application2Mock.address, + maxStake.div(4) + ) + await application1Mock.approveAuthorizationDecrease( + stakingProvider.address + ) + await application2Mock.approveAuthorizationDecrease( + stakingProvider.address + ) + }) + + it("should update available opt-out amount", async () => { + expect( + await tokenStaking.getAvailableOptOutAmount(stakingProvider.address) + ).to.equal(0) + }) + + it("should revert when calling for more opt-out", async () => { + await expect( + tokenStaking + .connect(stakingProvider) + .optOutDecreaseAuthorization(stakingProvider.address, 1) + ).to.be.revertedWith("Opt-out is not available") + }) + }) + }) + + context("when calling transaction multiple times", () => { + let tx + const amountToDecrease = amount.div(20) + const amountToOptOut1 = maxStake.div(3) + const amountToOptOut2 = maxStake.div(6) + const expectedAmount = maxStake.sub(amountToOptOut1).sub(amountToOptOut2) + + beforeEach(async () => { + await tokenStaking + .connect(authorizer) + .increaseAuthorization( + stakingProvider.address, + application1Mock.address, + amount + ) + + await tokenStaking + .connect(authorizer) + ["requestAuthorizationDecrease(address,address,uint96)"]( + stakingProvider.address, + application1Mock.address, + amountToDecrease + ) + + await tokenStaking + .connect(deployer) + ["forceCapDecreaseAuthorization(address)"](stakingProvider.address) + + await tokenStaking + .connect(stakingProvider) + .optOutDecreaseAuthorization(stakingProvider.address, amountToOptOut1) + tx = await tokenStaking + .connect(stakingProvider) + .optOutDecreaseAuthorization(stakingProvider.address, amountToOptOut2) + }) + + it("should update authorized amount", async () => { + expect( + await tokenStaking.authorizedStake( + stakingProvider.address, + application1Mock.address + ) + ).to.equal(expectedAmount) + }) + + it("should update available opt-out amount", async () => { + expect( + await tokenStaking.getAvailableOptOutAmount(stakingProvider.address) + ).to.equal(maxStake.div(2).sub(amountToOptOut1).sub(amountToOptOut2)) + }) + + it("should cancel deauthorization amount", async () => { + expect( + await tokenStaking.getDeauthorizingAmount( + stakingProvider.address, + application2Mock.address + ) + ).to.equal(0) + }) + + it("should emit AuthorizationDecreaseApproved", async () => { + await expect(tx) + .to.emit(tokenStaking, "AuthorizationDecreaseApproved") + .withArgs( + stakingProvider.address, + application1Mock.address, + maxStake.sub(amountToOptOut1), + maxStake.sub(amountToOptOut1).sub(amountToOptOut2) + ) + }) + }) + }) + + describe("forceBetaStakerDecreaseAuthorization", () => { const amount = initialStakerBalance beforeEach(async () => { @@ -1197,7 +1476,7 @@ describe("TokenStaking", () => { await expect( tokenStaking .connect(stakingProvider) - ["forceBetaStakersCapDecreaseAuthorization(address)"]( + ["forceBetaStakerDecreaseAuthorization(address)"]( stakingProvider.address ) ).to.be.revertedWith("Caller is not the governance") @@ -1209,7 +1488,7 @@ describe("TokenStaking", () => { await expect( tokenStaking .connect(deployer) - ["forceBetaStakersCapDecreaseAuthorization(address)"]( + ["forceBetaStakerDecreaseAuthorization(address)"]( stakingProvider.address ) ).to.be.revertedWith("Nothing was authorized") @@ -1243,7 +1522,7 @@ describe("TokenStaking", () => { tx = await tokenStaking .connect(deployer) - ["forceBetaStakersCapDecreaseAuthorization(address)"]( + ["forceBetaStakerDecreaseAuthorization(address)"]( stakingProvider.address ) }) @@ -1341,7 +1620,7 @@ describe("TokenStaking", () => { tx = await tokenStaking .connect(deployer) - ["forceBetaStakersCapDecreaseAuthorization(address)"]( + ["forceBetaStakerDecreaseAuthorization(address)"]( stakingProvider.address ) }) @@ -1445,7 +1724,7 @@ describe("TokenStaking", () => { tx = await tokenStaking .connect(deployer) - ["forceBetaStakersCapDecreaseAuthorization(address[])"]([ + ["forceBetaStakerDecreaseAuthorization(address[])"]([ stakingProvider.address, otherStaker.address, ]) From 6c00479f519c582ac52ab4099b1f4e15e46df753 Mon Sep 17 00:00:00 2001 From: Viktoriia Zotova Date: Thu, 13 Feb 2025 11:06:23 -0500 Subject: [PATCH 06/10] Removes PercentUtils --- contracts/staking/TokenStaking.sol | 2 -- contracts/utils/PercentUtils.sol | 32 ------------------------------ 2 files changed, 34 deletions(-) delete mode 100644 contracts/utils/PercentUtils.sol diff --git a/contracts/staking/TokenStaking.sol b/contracts/staking/TokenStaking.sol index 5b019bf5..da9ea5c3 100644 --- a/contracts/staking/TokenStaking.sol +++ b/contracts/staking/TokenStaking.sol @@ -19,7 +19,6 @@ import "./IApplication.sol"; import "./IStaking.sol"; import "../governance/Checkpoints.sol"; import "../token/T.sol"; -import "../utils/PercentUtils.sol"; import "../utils/SafeTUpgradeable.sol"; import "../vending/VendingMachine.sol"; import "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol"; @@ -36,7 +35,6 @@ import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; /// libraries. See https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable contract TokenStaking is Initializable, IStaking, Checkpoints { using SafeTUpgradeable for T; - using PercentUtils for uint256; using SafeCastUpgradeable for uint256; enum ApplicationStatus { diff --git a/contracts/utils/PercentUtils.sol b/contracts/utils/PercentUtils.sol deleted file mode 100644 index 5f154304..00000000 --- a/contracts/utils/PercentUtils.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -// ██████████████ ▐████▌ ██████████████ -// ██████████████ ▐████▌ ██████████████ -// ▐████▌ ▐████▌ -// ▐████▌ ▐████▌ -// ██████████████ ▐████▌ ██████████████ -// ██████████████ ▐████▌ ██████████████ -// ▐████▌ ▐████▌ -// ▐████▌ ▐████▌ -// ▐████▌ ▐████▌ -// ▐████▌ ▐████▌ -// ▐████▌ ▐████▌ -// ▐████▌ ▐████▌ - -pragma solidity 0.8.9; - -library PercentUtils { - // Return `b`% of `a` - // 200.percent(40) == 80 - // Commutative, works both ways - function percent(uint256 a, uint256 b) internal pure returns (uint256) { - return (a * b) / 100; - } - - // Return `a` as percentage of `b`: - // 80.asPercentOf(200) == 40 - //slither-disable-next-line dead-code - function asPercentOf(uint256 a, uint256 b) internal pure returns (uint256) { - return (a * 100) / b; - } -} From 74035ef9c48c67c6b87efe5cf425e355734dd0f3 Mon Sep 17 00:00:00 2001 From: Viktoriia Zotova Date: Tue, 25 Feb 2025 18:59:58 -0500 Subject: [PATCH 07/10] Apply suggestions from code review --- .../staking/IApplicationWithDecreaseDelay.sol | 12 ++--- .../staking/IApplicationWithOperator.sol | 20 +++---- contracts/staking/TokenStaking.sol | 52 ++++++++++++++++--- contracts/test/TokenStakingTestSet.sol | 28 +++++----- test/staking/TokenStaking.test.js | 22 ++++---- 5 files changed, 85 insertions(+), 49 deletions(-) diff --git a/contracts/staking/IApplicationWithDecreaseDelay.sol b/contracts/staking/IApplicationWithDecreaseDelay.sol index 03ecc370..e92adf60 100644 --- a/contracts/staking/IApplicationWithDecreaseDelay.sol +++ b/contracts/staking/IApplicationWithDecreaseDelay.sol @@ -19,6 +19,12 @@ import "./IApplication.sol"; /// @title Interface for Threshold Network applications with delay after decrease request interface IApplicationWithDecreaseDelay is IApplication { + /// @notice Approves the previously registered authorization decrease + /// request. Reverts if authorization decrease delay has not passed + /// yet or if the authorization decrease was not requested for the + /// given staking provider. + function approveAuthorizationDecrease(address stakingProvider) external; + /// @notice Returns authorization-related parameters of the application. /// @dev The minimum authorization is also returned by `minimumAuthorization()` /// function, as a requirement of `IApplication` interface. @@ -59,10 +65,4 @@ interface IApplicationWithDecreaseDelay is IApplication { external view returns (uint64); - - /// @notice Approves the previously registered authorization decrease - /// request. Reverts if authorization decrease delay has not passed - /// yet or if the authorization decrease was not requested for the - /// given staking provider. - function approveAuthorizationDecrease(address stakingProvider) external; } diff --git a/contracts/staking/IApplicationWithOperator.sol b/contracts/staking/IApplicationWithOperator.sol index 05879d21..8d2f4e9f 100644 --- a/contracts/staking/IApplicationWithOperator.sol +++ b/contracts/staking/IApplicationWithOperator.sol @@ -19,6 +19,16 @@ import "./IApplication.sol"; /// @title Interface for Threshold Network applications with operator role interface IApplicationWithOperator is IApplication { + /// @notice Used by staking provider to set operator address that will + /// operate a node. The operator address must be unique. + /// Reverts if the operator is already set for the staking provider + /// or if the operator address is already in use. + /// @dev Depending on application the given staking provider can set operator + /// address only once or multiple times. Besides that, application can decide + /// if function reverts if there is a pending authorization decrease for + /// the staking provider. + function registerOperator(address operator) external; + /// @notice Returns operator registered for the given staking provider. function stakingProviderToOperator(address stakingProvider) external @@ -30,14 +40,4 @@ interface IApplicationWithOperator is IApplication { external view returns (address); - - /// @notice Used by staking provider to set operator address that will - /// operate a node. The operator address must be unique. - /// Reverts if the operator is already set for the staking provider - /// or if the operator address is already in use. - /// @dev Depending on application the given staking provider can set operator - /// address only once or multiple times. Besides that, application can decide - /// if function reverts if there is a pending authorization decrease for - /// the staking provider. - function registerOperator(address operator) external; } diff --git a/contracts/staking/TokenStaking.sol b/contracts/staking/TokenStaking.sol index da9ea5c3..f2e5e39d 100644 --- a/contracts/staking/TokenStaking.sol +++ b/contracts/staking/TokenStaking.sol @@ -37,6 +37,13 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { using SafeTUpgradeable for T; using SafeCastUpgradeable for uint256; + // enum is used for Staked event to have backward compatibility + enum StakeType { + NU, + KEEP, + T + } + enum ApplicationStatus { NOT_APPROVED, APPROVED, @@ -101,11 +108,25 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { // slither-disable-next-line constable-states uint256 private legacySlashingQueueIndex; + event Staked( + StakeType indexed stakeType, + address indexed owner, + address indexed stakingProvider, + address beneficiary, + address authorizer, + uint96 amount + ); event MinimumStakeAmountSet(uint96 amount); event ApplicationStatusChanged( address indexed application, ApplicationStatus indexed newStatus ); + event AuthorizationIncreased( + address indexed stakingProvider, + address indexed application, + uint96 fromAmount, + uint96 toAmount + ); event AuthorizationDecreaseRequested( address indexed stakingProvider, address indexed application, @@ -130,8 +151,26 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { address indexed panicButton ); event AuthorizationCeilingSet(uint256 ceiling); + event ToppedUp(address indexed stakingProvider, uint96 amount); + event AutoIncreaseToggled( + address indexed stakingProvider, + bool autoIncrease + ); event Unstaked(address indexed stakingProvider, uint96 amount); + event TokensSeized( + address indexed stakingProvider, + uint96 amount, + bool indexed discrepancy + ); + event NotificationRewardSet(uint96 reward); + event NotificationRewardPushed(uint96 reward); event NotificationRewardWithdrawn(address recipient, uint96 amount); + event NotifierRewarded(address indexed notifier, uint256 amount); + event SlashingProcessed( + address indexed caller, + uint256 count, + uint256 tAmount + ); event GovernanceTransferred(address oldGovernance, address newGovernance); modifier onlyGovernance() { @@ -343,15 +382,14 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { cleanAuthorizedApplications(stakingProviderStruct, 1); } - // TODO consider rename /// @notice Forced deauthorization of stake above 15m T. /// Can be called by anyone. - function forceCapDecreaseAuthorization(address[] memory _stakingProviders) + function forceAuthorizationCap(address[] memory _stakingProviders) external { require(_stakingProviders.length > 0, "Wrong input parameters"); for (uint256 i = 0; i < _stakingProviders.length; i++) { - forceCapDecreaseAuthorization(_stakingProviders[i]); + forceAuthorizationCap(_stakingProviders[i]); } } @@ -375,7 +413,7 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { maxAuthorization = MAX_STAKE; availableToOptOut = HALF_MAX_STAKE; } - require(availableToOptOut >= amount, "Opt-out is not available"); // TODO rephrase + require(availableToOptOut >= amount, "Opt-out amount too high"); forceDecreaseAuthorization(stakingProvider, maxAuthorization - amount); stakingProviderStruct.optOutAmount += amount; } @@ -710,9 +748,7 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { /// @notice Forced deauthorization of stake above 15m T. /// Can be called by anyone. - // TODO consider rename - function forceCapDecreaseAuthorization(address stakingProvider) public { - //override { + function forceAuthorizationCap(address stakingProvider) public { forceDecreaseAuthorization(stakingProvider, MAX_STAKE); } @@ -950,7 +986,7 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { } } - require(deauthorized > 0, "Nothing was deauthorized"); + require(deauthorized > 0, "Nothing to deauthorize"); } function getAvailableOptOutAmount( diff --git a/contracts/test/TokenStakingTestSet.sol b/contracts/test/TokenStakingTestSet.sol index 0d2f056d..5e5992fe 100644 --- a/contracts/test/TokenStakingTestSet.sol +++ b/contracts/test/TokenStakingTestSet.sol @@ -161,14 +161,6 @@ contract ExtendedTokenStaking is TokenStaking { .authorizedApplications = _applications; } - function getAuthorizedApplications(address stakingProvider) - external - view - returns (address[] memory) - { - return stakingProviders[stakingProvider].authorizedApplications; - } - /// @notice Creates a delegation with `msg.sender` owner with the given /// staking provider, beneficiary, and authorizer. Transfers the /// given amount of T to the staking contract. @@ -267,13 +259,12 @@ contract ExtendedTokenStaking is TokenStaking { token.safeTransferFrom(msg.sender, address(this), reward); } - /// @notice Creates new checkpoints due to an increment of a stakers' stake - /// @param _delegator Address of the staking provider acting as delegator - /// @param _amount Amount of T to increment - function increaseStakeCheckpoint(address _delegator, uint96 _amount) - internal + function getAuthorizedApplications(address stakingProvider) + external + view + returns (address[] memory) { - newStakeCheckpoint(_delegator, _amount, true); + return stakingProviders[stakingProvider].authorizedApplications; } function getDeauthorizingAmount( @@ -285,4 +276,13 @@ contract ExtendedTokenStaking is TokenStaking { .authorizations[application] .deauthorizing; } + + /// @notice Creates new checkpoints due to an increment of a stakers' stake + /// @param _delegator Address of the staking provider acting as delegator + /// @param _amount Amount of T to increment + function increaseStakeCheckpoint(address _delegator, uint96 _amount) + internal + { + newStakeCheckpoint(_delegator, _amount, true); + } } diff --git a/test/staking/TokenStaking.test.js b/test/staking/TokenStaking.test.js index 5e207f2d..d41fe160 100644 --- a/test/staking/TokenStaking.test.js +++ b/test/staking/TokenStaking.test.js @@ -915,7 +915,7 @@ describe("TokenStaking", () => { }) }) - describe("forceCapDecreaseAuthorization", () => { + describe("forceAuthorizationCap", () => { const maxStake = to1e18("15000000") // 15m const amount = maxStake.mul(2) @@ -948,8 +948,8 @@ describe("TokenStaking", () => { await expect( tokenStaking .connect(authorizer) - ["forceCapDecreaseAuthorization(address)"](stakingProvider.address) - ).to.be.revertedWith("Nothing was deauthorized") + ["forceAuthorizationCap(address)"](stakingProvider.address) + ).to.be.revertedWith("Nothing to deauthorize") }) }) @@ -980,7 +980,7 @@ describe("TokenStaking", () => { tx = await tokenStaking .connect(deployer) - ["forceCapDecreaseAuthorization(address)"](stakingProvider.address) + ["forceAuthorizationCap(address)"](stakingProvider.address) }) it("should set authorized amount to max", async () => { @@ -1053,7 +1053,7 @@ describe("TokenStaking", () => { tx = await tokenStaking .connect(deployer) - ["forceCapDecreaseAuthorization(address)"](stakingProvider.address) + ["forceAuthorizationCap(address)"](stakingProvider.address) }) it("should set authorized amount to max", async () => { @@ -1132,7 +1132,7 @@ describe("TokenStaking", () => { tx = await tokenStaking .connect(deployer) - ["forceCapDecreaseAuthorization(address[])"]([ + ["forceAuthorizationCap(address[])"]([ stakingProvider.address, otherStaker.address, ]) @@ -1222,7 +1222,7 @@ describe("TokenStaking", () => { tokenStaking .connect(stakingProvider) .optOutDecreaseAuthorization(stakingProvider.address, 1) - ).to.be.revertedWith("Opt-out is not available") + ).to.be.revertedWith("Opt-out amount too high") await tokenStaking .connect(authorizer) @@ -1238,7 +1238,7 @@ describe("TokenStaking", () => { stakingProvider.address, amountToOptOut.add(1) ) - ).to.be.revertedWith("Opt-out is not available") + ).to.be.revertedWith("Opt-out amount too high") await tokenStaking .connect(stakingProvider) @@ -1247,7 +1247,7 @@ describe("TokenStaking", () => { tokenStaking .connect(stakingProvider) .optOutDecreaseAuthorization(stakingProvider.address, 1) - ).to.be.revertedWith("Opt-out is not available") + ).to.be.revertedWith("Opt-out amount too high") }) }) @@ -1373,7 +1373,7 @@ describe("TokenStaking", () => { tokenStaking .connect(stakingProvider) .optOutDecreaseAuthorization(stakingProvider.address, 1) - ).to.be.revertedWith("Opt-out is not available") + ).to.be.revertedWith("Opt-out amount too high") }) }) }) @@ -1404,7 +1404,7 @@ describe("TokenStaking", () => { await tokenStaking .connect(deployer) - ["forceCapDecreaseAuthorization(address)"](stakingProvider.address) + ["forceAuthorizationCap(address)"](stakingProvider.address) await tokenStaking .connect(stakingProvider) From d78cbdaaf87e30fffc9b572822a43600334ddc36 Mon Sep 17 00:00:00 2001 From: Viktoriia Zotova Date: Wed, 26 Feb 2025 13:13:16 -0500 Subject: [PATCH 08/10] Freeze all application except TACo --- contracts/staking/TokenStaking.sol | 27 ++++++++++++++++++++++---- contracts/test/TokenStakingTestSet.sol | 4 ++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/contracts/staking/TokenStaking.sol b/contracts/staking/TokenStaking.sol index f2e5e39d..7c7d3287 100644 --- a/contracts/staking/TokenStaking.sol +++ b/contracts/staking/TokenStaking.sol @@ -83,6 +83,8 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { uint256 internal constant MIN_STAKE_TIME = 24 hours; uint96 internal constant MAX_STAKE = 15 * 10**(18 + 6); // 15m T uint96 internal constant HALF_MAX_STAKE = MAX_STAKE / 2; // 7.5m T + address internal constant TACO_APPLICATION = + 0x347CC7ede7e5517bD47D20620B2CF1b406edcF07; /// @custom:oz-upgrades-unsafe-allow state-variable-immutable T internal immutable token; @@ -730,20 +732,22 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { .length; require(authorizedApplications > 0, "Nothing was authorized"); + uint256 temp = 0; for (uint256 i = 0; i < authorizedApplications; i++) { address application = stakingProviderStruct.authorizedApplications[ i ]; + if (skipApplication(application)) { + continue; + } forceDecreaseAuthorization( betaStaker, stakingProviderStruct, application ); + temp++; } - cleanAuthorizedApplications( - stakingProviderStruct, - authorizedApplications - ); + cleanAuthorizedApplications(stakingProviderStruct, temp); } /// @notice Forced deauthorization of stake above 15m T. @@ -771,6 +775,9 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { address application = stakingProviderStruct.authorizedApplications[ i ]; + if (skipApplication(application)) { + continue; + } maxAuthorization = MathUpgradeable.max( maxAuthorization, stakingProviderStruct.authorizations[application].authorized @@ -957,6 +964,9 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { address application = stakingProviderStruct.authorizedApplications[ i ]; + if (skipApplication(application)) { + continue; + } AppAuthorization storage authorization = stakingProviderStruct .authorizations[application]; uint96 authorized = authorization.authorized; @@ -1005,4 +1015,13 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { availableToOptOut = (maxAuthorization - optOutAmount) / 2; } } + + function skipApplication(address application) + internal + pure + virtual + returns (bool) + { + return application != TACO_APPLICATION; + } } diff --git a/contracts/test/TokenStakingTestSet.sol b/contracts/test/TokenStakingTestSet.sol index 5e5992fe..18554eed 100644 --- a/contracts/test/TokenStakingTestSet.sol +++ b/contracts/test/TokenStakingTestSet.sol @@ -285,4 +285,8 @@ contract ExtendedTokenStaking is TokenStaking { { newStakeCheckpoint(_delegator, _amount, true); } + + function skipApplication(address) internal pure override returns (bool) { + return false; + } } From 417942f6cc84e9b084dfa84c63fff96fbc712f35 Mon Sep 17 00:00:00 2001 From: Viktoriia Zotova Date: Wed, 26 Feb 2025 21:50:27 -0500 Subject: [PATCH 09/10] Removes ability to create new requests for decrease authorization --- contracts/staking/IStaking.sol | 15 - contracts/staking/TokenStaking.sol | 99 +----- contracts/test/TokenStakingTestSet.sol | 87 +++++ test/staking/TokenStaking.test.js | 462 ++----------------------- 4 files changed, 122 insertions(+), 541 deletions(-) diff --git a/contracts/staking/IStaking.sol b/contracts/staking/IStaking.sol index e1b36c40..353948b9 100644 --- a/contracts/staking/IStaking.sol +++ b/contracts/staking/IStaking.sol @@ -45,10 +45,6 @@ interface IStaking { // // - /// @notice Allows the Governance to approve the particular application - /// before individual stake authorizers are able to authorize it. - function approveApplication(address application) external; - /// @notice Requests decrease of the authorization for the given staking /// provider on the given application by the provided amount. /// It may not change the authorized amount immediatelly. When @@ -66,17 +62,6 @@ interface IStaking { uint96 amount ) external; - /// @notice Requests decrease of all authorizations for the given staking - /// provider on all applications by all authorized amount. - /// It may not change the authorized amount immediatelly. When - /// it happens depends on the application. Can only be called by the - /// given staking provider’s authorizer. Overwrites pending - /// authorization decrease for the given staking provider and - /// application. - /// @dev Calls `authorizationDecreaseRequested(address stakingProvider, uint256 amount)` - /// for each authorized application. See `IApplication`. - function requestAuthorizationDecrease(address stakingProvider) external; - /// @notice Called by the application at its discretion to approve the /// previously requested authorization decrease request. Can only be /// called by the application that was previously requested to diff --git a/contracts/staking/TokenStaking.sol b/contracts/staking/TokenStaking.sol index 7c7d3287..37c39c14 100644 --- a/contracts/staking/TokenStaking.sol +++ b/contracts/staking/TokenStaking.sol @@ -260,66 +260,6 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { // // - /// @notice Allows the Governance to approve the particular application - /// before individual stake authorizers are able to authorize it. - function approveApplication(address application) - external - override - onlyGovernance - { - require(application != address(0), "Parameters must be specified"); - ApplicationInfo storage info = applicationInfo[application]; - require( - info.status == ApplicationStatus.NOT_APPROVED || - info.status == ApplicationStatus.PAUSED, - "Can't approve application" - ); - - if (info.status == ApplicationStatus.NOT_APPROVED) { - applications.push(application); - } - info.status = ApplicationStatus.APPROVED; - emit ApplicationStatusChanged(application, ApplicationStatus.APPROVED); - } - - /// @notice Requests decrease of all authorizations for the given staking - /// provider on all applications by all authorized amount. - /// It may not change the authorized amount immediatelly. When - /// it happens depends on the application. Can only be called by the - /// given staking provider’s authorizer. Overwrites pending - /// authorization decrease for the given staking provider and - /// application. - /// @dev Calls `authorizationDecreaseRequested` callback - /// for each authorized application. See `IApplication`. - function requestAuthorizationDecrease(address stakingProvider) external { - StakingProviderInfo storage stakingProviderStruct = stakingProviders[ - stakingProvider - ]; - uint96 deauthorizing = 0; - for ( - uint256 i = 0; - i < stakingProviderStruct.authorizedApplications.length; - i++ - ) { - address application = stakingProviderStruct.authorizedApplications[ - i - ]; - uint96 authorized = stakingProviderStruct - .authorizations[application] - .authorized; - if (authorized > 0) { - requestAuthorizationDecrease( - stakingProvider, - application, - authorized - ); - deauthorizing += authorized; - } - } - - require(deauthorizing > 0, "Nothing was authorized"); - } - /// @notice Called by the application at its discretion to approve the /// previously requested authorization decrease request. Can only be /// called by the application that was previously requested to @@ -399,8 +339,8 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { /// Can be called only by the delegation owner or the staking /// provider. function optOutDecreaseAuthorization(address stakingProvider, uint96 amount) - external - onlyOwnerOrStakingProvider(stakingProvider) + public + onlyAuthorizerOf(stakingProvider) { require(amount > 0, "Parameters must be specified"); StakingProviderInfo storage stakingProviderStruct = stakingProviders[ @@ -682,40 +622,10 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { /// application. See `IApplication`. function requestAuthorizationDecrease( address stakingProvider, - address application, + address, uint96 amount ) public override onlyAuthorizerOf(stakingProvider) { - ApplicationInfo storage applicationStruct = applicationInfo[ - application - ]; - require( - applicationStruct.status == ApplicationStatus.APPROVED, - "Application is not approved" - ); - - require(amount > 0, "Parameters must be specified"); - - AppAuthorization storage authorization = stakingProviders[ - stakingProvider - ].authorizations[application]; - require( - authorization.authorized >= amount, - "Amount exceeds authorized" - ); - - authorization.deauthorizing = amount; - uint96 deauthorizingTo = authorization.authorized - amount; - emit AuthorizationDecreaseRequested( - stakingProvider, - application, - authorization.authorized, - deauthorizingTo - ); - IApplication(application).authorizationDecreaseRequested( - stakingProvider, - authorization.authorized, - deauthorizingTo - ); + optOutDecreaseAuthorization(stakingProvider, amount); } /// @notice Forced deauthorization of Beta staker. @@ -1016,6 +926,7 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { } } + // slither-disable-next-line dead-code function skipApplication(address application) internal pure diff --git a/contracts/test/TokenStakingTestSet.sol b/contracts/test/TokenStakingTestSet.sol index 18554eed..091536e4 100644 --- a/contracts/test/TokenStakingTestSet.sol +++ b/contracts/test/TokenStakingTestSet.sol @@ -259,6 +259,55 @@ contract ExtendedTokenStaking is TokenStaking { token.safeTransferFrom(msg.sender, address(this), reward); } + /// @notice Allows the Governance to approve the particular application + /// before individual stake authorizers are able to authorize it. + function approveApplication(address application) external { + require(application != address(0), "Parameters must be specified"); + ApplicationInfo storage info = applicationInfo[application]; + require( + info.status == ApplicationStatus.NOT_APPROVED || + info.status == ApplicationStatus.PAUSED, + "Can't approve application" + ); + + if (info.status == ApplicationStatus.NOT_APPROVED) { + applications.push(application); + } + info.status = ApplicationStatus.APPROVED; + emit ApplicationStatusChanged(application, ApplicationStatus.APPROVED); + } + + function legacyRequestAuthorizationDecrease(address stakingProvider) + external + { + StakingProviderInfo storage stakingProviderStruct = stakingProviders[ + stakingProvider + ]; + uint96 deauthorizing = 0; + for ( + uint256 i = 0; + i < stakingProviderStruct.authorizedApplications.length; + i++ + ) { + address application = stakingProviderStruct.authorizedApplications[ + i + ]; + uint96 authorized = stakingProviderStruct + .authorizations[application] + .authorized; + if (authorized > 0) { + legacyRequestAuthorizationDecrease( + stakingProvider, + application, + authorized + ); + deauthorizing += authorized; + } + } + + require(deauthorizing > 0, "Nothing was authorized"); + } + function getAuthorizedApplications(address stakingProvider) external view @@ -277,6 +326,44 @@ contract ExtendedTokenStaking is TokenStaking { .deauthorizing; } + function legacyRequestAuthorizationDecrease( + address stakingProvider, + address application, + uint96 amount + ) public { + ApplicationInfo storage applicationStruct = applicationInfo[ + application + ]; + require( + applicationStruct.status == ApplicationStatus.APPROVED, + "Application is not approved" + ); + + require(amount > 0, "Parameters must be specified"); + + AppAuthorization storage authorization = stakingProviders[ + stakingProvider + ].authorizations[application]; + require( + authorization.authorized >= amount, + "Amount exceeds authorized" + ); + + authorization.deauthorizing = amount; + uint96 deauthorizingTo = authorization.authorized - amount; + emit AuthorizationDecreaseRequested( + stakingProvider, + application, + authorization.authorized, + deauthorizingTo + ); + IApplication(application).authorizationDecreaseRequested( + stakingProvider, + authorization.authorized, + deauthorizingTo + ); + } + /// @notice Creates new checkpoints due to an increment of a stakers' stake /// @param _delegator Address of the staking provider acting as delegator /// @param _amount Amount of T to increment diff --git a/test/staking/TokenStaking.test.js b/test/staking/TokenStaking.test.js index d41fe160..fe0f2b6d 100644 --- a/test/staking/TokenStaking.test.js +++ b/test/staking/TokenStaking.test.js @@ -4,7 +4,7 @@ const { helpers } = require("hardhat") const { lastBlockTime, mineBlocks, increaseTime } = helpers.time const { to1e18 } = helpers.number -const { AddressZero, Zero } = ethers.constants +const { Zero } = ethers.constants const ApplicationStatus = { NOT_APPROVED: 0, @@ -111,414 +111,6 @@ describe("TokenStaking", () => { }) }) - describe("approveApplication", () => { - context("when caller is not the governance", () => { - it("should revert", async () => { - await expect( - tokenStaking.connect(staker).approveApplication(AddressZero) - ).to.be.revertedWith("Caller is not the governance") - }) - }) - - context("when caller did not provide application", () => { - it("should revert", async () => { - await expect( - tokenStaking.connect(deployer).approveApplication(AddressZero) - ).to.be.revertedWith("Parameters must be specified") - }) - }) - - context("when application has already been approved", () => { - it("should revert", async () => { - await tokenStaking - .connect(deployer) - .approveApplication(application1Mock.address) - await expect( - tokenStaking - .connect(deployer) - .approveApplication(application1Mock.address) - ).to.be.revertedWith("Can't approve application") - }) - }) - - context("when application is disabled", () => { - it("should revert", async () => { - await tokenStaking - .connect(deployer) - .approveApplication(application1Mock.address) - await tokenStaking - .connect(deployer) - .disableApplication(application1Mock.address) - await expect( - tokenStaking - .connect(deployer) - .approveApplication(application1Mock.address) - ).to.be.revertedWith("Can't approve application") - }) - }) - - context("when approving new application", () => { - let tx - - beforeEach(async () => { - tx = await tokenStaking - .connect(deployer) - .approveApplication(application1Mock.address) - }) - - it("should approve application", async () => { - expect( - await tokenStaking.applicationInfo(application1Mock.address) - ).to.deep.equal([ApplicationStatus.APPROVED, AddressZero]) - }) - - it("should add application to the list of all applications", async () => { - expect(await tokenStaking.getApplicationsLength()).to.equal(1) - expect(await tokenStaking.applications(0)).to.equal( - application1Mock.address - ) - }) - - it("should emit ApplicationStatusChanged", async () => { - await expect(tx) - .to.emit(tokenStaking, "ApplicationStatusChanged") - .withArgs(application1Mock.address, ApplicationStatus.APPROVED) - }) - }) - - context("when approving paused application", () => { - let tx - - beforeEach(async () => { - await tokenStaking - .connect(deployer) - .approveApplication(application1Mock.address) - await tokenStaking - .connect(deployer) - .setPanicButton(application1Mock.address, panicButton.address) - await tokenStaking - .connect(panicButton) - .pauseApplication(application1Mock.address) - tx = await tokenStaking - .connect(deployer) - .approveApplication(application1Mock.address) - }) - - it("should enable application", async () => { - expect( - await tokenStaking.applicationInfo(application1Mock.address) - ).to.deep.equal([ApplicationStatus.APPROVED, panicButton.address]) - }) - - it("should keep list of all applications unchanged", async () => { - expect(await tokenStaking.getApplicationsLength()).to.equal(1) - expect(await tokenStaking.applications(0)).to.equal( - application1Mock.address - ) - }) - - it("should emit ApplicationStatusChanged", async () => { - await expect(tx) - .to.emit(tokenStaking, "ApplicationStatusChanged") - .withArgs(application1Mock.address, ApplicationStatus.APPROVED) - }) - }) - }) - - describe("requestAuthorizationDecrease", () => { - context("when caller is not authorizer", () => { - it("should revert", async () => { - const amount = initialStakerBalance - await expect( - tokenStaking - .connect(staker) - ["requestAuthorizationDecrease(address,address,uint96)"]( - stakingProvider.address, - application1Mock.address, - amount - ) - ).to.be.revertedWith("Not authorizer") - }) - }) - - context( - "when caller is authorizer of staking provider with T stake", - () => { - const amount = initialStakerBalance - - beforeEach(async () => { - await tToken.connect(staker).approve(tokenStaking.address, amount) - await tokenStaking - .connect(staker) - .stake( - stakingProvider.address, - beneficiary.address, - authorizer.address, - amount - ) - await tokenStaking - .connect(deployer) - .approveApplication(application1Mock.address) - await tokenStaking - .connect(authorizer) - .increaseAuthorization( - stakingProvider.address, - application1Mock.address, - amount - ) - }) - - context("when application was paused", () => { - it("should revert", async () => { - const amount = initialStakerBalance - await tokenStaking - .connect(deployer) - .setPanicButton(application1Mock.address, panicButton.address) - await tokenStaking - .connect(panicButton) - .pauseApplication(application1Mock.address) - await expect( - tokenStaking - .connect(authorizer) - ["requestAuthorizationDecrease(address,address,uint96)"]( - stakingProvider.address, - application1Mock.address, - amount - ) - ).to.be.revertedWith("Application is not approved") - }) - }) - - context("when application is disabled", () => { - it("should revert", async () => { - await tokenStaking - .connect(deployer) - .disableApplication(application1Mock.address) - await expect( - tokenStaking - .connect(authorizer) - ["requestAuthorizationDecrease(address)"]( - stakingProvider.address - ) - ).to.be.revertedWith("Application is not approved") - }) - }) - - context("when amount to decrease is zero", () => { - it("should revert", async () => { - await expect( - tokenStaking - .connect(authorizer) - ["requestAuthorizationDecrease(address,address,uint96)"]( - stakingProvider.address, - application1Mock.address, - 0 - ) - ).to.be.revertedWith("Parameters must be specified") - }) - }) - - context("when amount to decrease is more than authorized", () => { - it("should revert", async () => { - await expect( - tokenStaking - .connect(authorizer) - ["requestAuthorizationDecrease(address,address,uint96)"]( - stakingProvider.address, - application1Mock.address, - amount.add(1) - ) - ).to.be.revertedWith("Amount exceeds authorized") - }) - }) - - context("when amount to decrease is less than authorized", () => { - const amountToDecrease = amount.div(3) - const expectedFromAmount = amount - const expectedToAmount = amount.sub(amountToDecrease) - let tx - - beforeEach(async () => { - tx = await tokenStaking - .connect(authorizer) - ["requestAuthorizationDecrease(address,address,uint96)"]( - stakingProvider.address, - application1Mock.address, - amountToDecrease - ) - }) - - it("should keep authorized amount unchanged", async () => { - expect( - await tokenStaking.authorizedStake( - stakingProvider.address, - application1Mock.address - ) - ).to.equal(amount) - }) - - it("should send request to application", async () => { - await assertApplicationStakingProviders( - application1Mock, - stakingProvider.address, - amount, - expectedToAmount - ) - }) - - it("should emit AuthorizationDecreaseRequested", async () => { - await expect(tx) - .to.emit(tokenStaking, "AuthorizationDecreaseRequested") - .withArgs( - stakingProvider.address, - application1Mock.address, - expectedFromAmount, - expectedToAmount - ) - }) - }) - - context( - "when request to decrease all authorized amount for several applications", - () => { - let tx - - beforeEach(async () => { - await tokenStaking - .connect(deployer) - .approveApplication(application2Mock.address) - await tokenStaking - .connect(authorizer) - .increaseAuthorization( - stakingProvider.address, - application2Mock.address, - amount - ) - tx = await tokenStaking - .connect(authorizer) - ["requestAuthorizationDecrease(address)"]( - stakingProvider.address - ) - }) - - it("should keep authorized amount unchanged", async () => { - expect( - await tokenStaking.authorizedStake( - stakingProvider.address, - application1Mock.address - ) - ).to.equal(amount) - expect( - await tokenStaking.authorizedStake( - stakingProvider.address, - application2Mock.address - ) - ).to.equal(amount) - }) - - it("should send request to application", async () => { - await assertApplicationStakingProviders( - application1Mock, - stakingProvider.address, - amount, - Zero - ) - await assertApplicationStakingProviders( - application2Mock, - stakingProvider.address, - amount, - Zero - ) - }) - - it("should emit AuthorizationDecreaseRequested", async () => { - await expect(tx) - .to.emit(tokenStaking, "AuthorizationDecreaseRequested") - .withArgs( - stakingProvider.address, - application1Mock.address, - amount, - Zero - ) - await expect(tx) - .to.emit(tokenStaking, "AuthorizationDecreaseRequested") - .withArgs( - stakingProvider.address, - application2Mock.address, - amount, - Zero - ) - }) - } - ) - - context("when decrease requested twice", () => { - const expectedFromAmount = amount - const amountToDecrease1 = amount.div(3) - const expectedToAmount1 = amount.sub(amountToDecrease1) - const amountToDecrease2 = amount.div(5) - const expectedToAmount2 = amount.sub(amountToDecrease2) - let tx1 - let tx2 - - beforeEach(async () => { - tx1 = await tokenStaking - .connect(authorizer) - ["requestAuthorizationDecrease(address,address,uint96)"]( - stakingProvider.address, - application1Mock.address, - amountToDecrease1 - ) - tx2 = await tokenStaking - .connect(authorizer) - ["requestAuthorizationDecrease(address,address,uint96)"]( - stakingProvider.address, - application1Mock.address, - amountToDecrease2 - ) - }) - - it("should keep authorized amount unchanged", async () => { - expect( - await tokenStaking.authorizedStake( - stakingProvider.address, - application1Mock.address - ) - ).to.equal(amount) - }) - - it("should send request to application with last amount", async () => { - await assertApplicationStakingProviders( - application1Mock, - stakingProvider.address, - amount, - expectedToAmount2 - ) - }) - - it("should emit AuthorizationDecreaseRequested twice", async () => { - await expect(tx1) - .to.emit(tokenStaking, "AuthorizationDecreaseRequested") - .withArgs( - stakingProvider.address, - application1Mock.address, - expectedFromAmount, - expectedToAmount1 - ) - await expect(tx2) - .to.emit(tokenStaking, "AuthorizationDecreaseRequested") - .withArgs( - stakingProvider.address, - application1Mock.address, - expectedFromAmount, - expectedToAmount2 - ) - }) - }) - } - ) - }) - describe("approveAuthorizationDecrease", () => { const amount = initialStakerBalance @@ -599,7 +191,9 @@ describe("TokenStaking", () => { ) await tokenStaking .connect(authorizer) - ["requestAuthorizationDecrease(address)"](stakingProvider.address) + ["legacyRequestAuthorizationDecrease(address)"]( + stakingProvider.address + ) await application1Mock.approveAuthorizationDecrease( stakingProvider.address ) @@ -618,7 +212,7 @@ describe("TokenStaking", () => { beforeEach(async () => { await tokenStaking .connect(authorizer) - ["requestAuthorizationDecrease(address,address,uint96)"]( + ["legacyRequestAuthorizationDecrease(address,address,uint96)"]( stakingProvider.address, application1Mock.address, amountToDecrease @@ -671,7 +265,7 @@ describe("TokenStaking", () => { ) await tokenStaking .connect(authorizer) - ["requestAuthorizationDecrease(address,address,uint96)"]( + ["legacyRequestAuthorizationDecrease(address,address,uint96)"]( stakingProvider.address, application1Mock.address, amount @@ -730,7 +324,9 @@ describe("TokenStaking", () => { ) await tokenStaking .connect(authorizer) - ["requestAuthorizationDecrease(address)"](stakingProvider.address) + ["legacyRequestAuthorizationDecrease(address)"]( + stakingProvider.address + ) await application1Mock.approveAuthorizationDecrease( stakingProvider.address ) @@ -866,7 +462,9 @@ describe("TokenStaking", () => { ) await tokenStaking .connect(authorizer) - ["requestAuthorizationDecrease(address)"](stakingProvider.address) + ["legacyRequestAuthorizationDecrease(address)"]( + stakingProvider.address + ) await tokenStaking .connect(deployer) @@ -1038,14 +636,14 @@ describe("TokenStaking", () => { await tokenStaking .connect(authorizer) - ["requestAuthorizationDecrease(address,address,uint96)"]( + ["legacyRequestAuthorizationDecrease(address,address,uint96)"]( stakingProvider.address, application1Mock.address, amountToDecrease ) await tokenStaking .connect(authorizer) - ["requestAuthorizationDecrease(address,address,uint96)"]( + ["legacyRequestAuthorizationDecrease(address,address,uint96)"]( stakingProvider.address, application2Mock.address, amountToDecrease2 @@ -1199,7 +797,7 @@ describe("TokenStaking", () => { it("should revert", async () => { await expect( tokenStaking.optOutDecreaseAuthorization(stakingProvider.address, 0) - ).to.be.revertedWith("Not owner or provider") + ).to.be.revertedWith("Not authorizer") }) }) @@ -1207,7 +805,7 @@ describe("TokenStaking", () => { it("should revert", async () => { await expect( tokenStaking - .connect(stakingProvider) + .connect(authorizer) .optOutDecreaseAuthorization(stakingProvider.address, 0) ).to.be.revertedWith("Parameters must be specified") }) @@ -1220,7 +818,7 @@ describe("TokenStaking", () => { it("should revert", async () => { await expect( tokenStaking - .connect(stakingProvider) + .connect(authorizer) .optOutDecreaseAuthorization(stakingProvider.address, 1) ).to.be.revertedWith("Opt-out amount too high") @@ -1233,7 +831,7 @@ describe("TokenStaking", () => { ) await expect( tokenStaking - .connect(stakingProvider) + .connect(authorizer) .optOutDecreaseAuthorization( stakingProvider.address, amountToOptOut.add(1) @@ -1241,11 +839,11 @@ describe("TokenStaking", () => { ).to.be.revertedWith("Opt-out amount too high") await tokenStaking - .connect(stakingProvider) + .connect(authorizer) .optOutDecreaseAuthorization(stakingProvider.address, amountToOptOut) await expect( tokenStaking - .connect(stakingProvider) + .connect(authorizer) .optOutDecreaseAuthorization(stakingProvider.address, 1) ).to.be.revertedWith("Opt-out amount too high") }) @@ -1279,7 +877,7 @@ describe("TokenStaking", () => { ) tx = await tokenStaking - .connect(stakingProvider) + .connect(authorizer) .optOutDecreaseAuthorization(stakingProvider.address, amountToOptOut) }) @@ -1334,7 +932,7 @@ describe("TokenStaking", () => { context("when use all opt-out amount and decrease after", () => { beforeEach(async () => { await tokenStaking - .connect(stakingProvider) + .connect(authorizer) .optOutDecreaseAuthorization( stakingProvider.address, maxStake.div(2).sub(amountToOptOut) @@ -1342,14 +940,14 @@ describe("TokenStaking", () => { await tokenStaking .connect(authorizer) - ["requestAuthorizationDecrease(address,address,uint96)"]( + ["legacyRequestAuthorizationDecrease(address,address,uint96)"]( stakingProvider.address, application1Mock.address, maxStake.div(4) ) await tokenStaking .connect(authorizer) - ["requestAuthorizationDecrease(address,address,uint96)"]( + ["legacyRequestAuthorizationDecrease(address,address,uint96)"]( stakingProvider.address, application2Mock.address, maxStake.div(4) @@ -1371,7 +969,7 @@ describe("TokenStaking", () => { it("should revert when calling for more opt-out", async () => { await expect( tokenStaking - .connect(stakingProvider) + .connect(authorizer) .optOutDecreaseAuthorization(stakingProvider.address, 1) ).to.be.revertedWith("Opt-out amount too high") }) @@ -1396,7 +994,7 @@ describe("TokenStaking", () => { await tokenStaking .connect(authorizer) - ["requestAuthorizationDecrease(address,address,uint96)"]( + ["legacyRequestAuthorizationDecrease(address,address,uint96)"]( stakingProvider.address, application1Mock.address, amountToDecrease @@ -1407,10 +1005,10 @@ describe("TokenStaking", () => { ["forceAuthorizationCap(address)"](stakingProvider.address) await tokenStaking - .connect(stakingProvider) + .connect(authorizer) .optOutDecreaseAuthorization(stakingProvider.address, amountToOptOut1) tx = await tokenStaking - .connect(stakingProvider) + .connect(authorizer) .optOutDecreaseAuthorization(stakingProvider.address, amountToOptOut2) }) @@ -1605,14 +1203,14 @@ describe("TokenStaking", () => { await tokenStaking .connect(authorizer) - ["requestAuthorizationDecrease(address,address,uint96)"]( + ["legacyRequestAuthorizationDecrease(address,address,uint96)"]( stakingProvider.address, application1Mock.address, amountToDecrease ) await tokenStaking .connect(authorizer) - ["requestAuthorizationDecrease(address,address,uint96)"]( + ["legacyRequestAuthorizationDecrease(address,address,uint96)"]( stakingProvider.address, application2Mock.address, amountToDecrease2 From 748f2648f2afbeb7f0e8fed11c1396180d6f6491 Mon Sep 17 00:00:00 2001 From: Viktoriia Zotova Date: Tue, 4 Mar 2025 15:57:23 -0500 Subject: [PATCH 10/10] Adds stubs for "seize" and "slash" --- contracts/staking/IStaking.sol | 17 +++++++++++++++++ contracts/staking/TokenStaking.sol | 29 +++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/contracts/staking/IStaking.sol b/contracts/staking/IStaking.sol index 353948b9..67680582 100644 --- a/contracts/staking/IStaking.sol +++ b/contracts/staking/IStaking.sol @@ -131,6 +131,23 @@ interface IStaking { function withdrawNotificationReward(address recipient, uint96 amount) external; + /// @notice Adds staking providers to the slashing queue along with the + /// amount that should be slashed from each one of them. Can only be + /// called by application authorized for all staking providers in + /// the array. + function slash(uint96 amount, address[] memory stakingProviders) external; + + /// @notice Adds staking providers to the slashing queue along with the + /// amount. The notifier will receive reward per each staking + /// provider from notifiers treasury. Can only be called by + /// application authorized for all staking providers in the array. + function seize( + uint96 amount, + uint256 rewardMultipier, + address notifier, + address[] memory stakingProviders + ) external; + // // // Auxiliary functions diff --git a/contracts/staking/TokenStaking.sol b/contracts/staking/TokenStaking.sol index 37c39c14..1347cbe0 100644 --- a/contracts/staking/TokenStaking.sol +++ b/contracts/staking/TokenStaking.sol @@ -174,6 +174,12 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { uint256 tAmount ); event GovernanceTransferred(address oldGovernance, address newGovernance); + event NotificationReceived( + uint96 amount, + uint256 rewardMultipier, + address notifier, + address[] stakingProviders + ); modifier onlyGovernance() { require(governance == msg.sender, "Caller is not the governance"); @@ -506,6 +512,29 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { token.safeTransfer(recipient, amount); } + /// @notice Stub for legacy "slash" method + function slash(uint96 amount, address[] memory _stakingProviders) + external + override + { + emit NotificationReceived(amount, 0, address(0), _stakingProviders); + } + + /// @notice Stub for legacy "seize" method + function seize( + uint96 amount, + uint256 rewardMultiplier, + address notifier, + address[] memory _stakingProviders + ) external override { + emit NotificationReceived( + amount, + rewardMultiplier, + notifier, + _stakingProviders + ); + } + /// @notice Delegate voting power from the stake associated to the /// `stakingProvider` to a `delegatee` address. Caller must be the /// owner of this stake.