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/IStaking.sol b/contracts/staking/IStaking.sol index e15f1126..67680582 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 @@ -57,22 +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 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 @@ -90,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 @@ -145,23 +106,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,14 +126,6 @@ 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) @@ -212,12 +148,6 @@ interface IStaking { 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 +174,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 +191,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..1347cbe0 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 is used for Staked event to have backward compatibility @@ -64,6 +62,7 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { address[] authorizedApplications; uint256 startStakingTimestamp; bool autoIncrease; + uint256 optOutAmount; } struct AppAuthorization { @@ -81,10 +80,11 @@ 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 + 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; @@ -98,14 +98,17 @@ 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, @@ -171,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"); @@ -232,55 +241,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 @@ -306,121 +266,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 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 - /// 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 @@ -477,22 +322,61 @@ 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); } + /// @notice Forced deauthorization of stake above 15m T. + /// Can be called by anyone. + function forceAuthorizationCap(address[] memory _stakingProviders) + external + { + require(_stakingProviders.length > 0, "Wrong input parameters"); + for (uint256 i = 0; i < _stakingProviders.length; i++) { + forceAuthorizationCap(_stakingProviders[i]); + } + } + + /// @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) + public + onlyAuthorizerOf(stakingProvider) + { + require(amount > 0, "Parameters must be specified"); + StakingProviderInfo storage stakingProviderStruct = stakingProviders[ + stakingProvider + ]; + ( + uint96 availableToOptOut, + uint96 maxAuthorization + ) = getAvailableOptOutAmount(stakingProvider, stakingProviderStruct); + if (maxAuthorization > MAX_STAKE) { + forceDecreaseAuthorization(stakingProvider, MAX_STAKE); + maxAuthorization = MAX_STAKE; + availableToOptOut = HALF_MAX_STAKE; + } + require(availableToOptOut >= amount, "Opt-out amount too high"); + forceDecreaseAuthorization(stakingProvider, maxAuthorization - amount); + stakingProviderStruct.optOutAmount += amount; + } + + /// @notice Forced deauthorization of Beta stakers. + /// Can be called only by the governance. + function forceBetaStakerDecreaseAuthorization(address[] memory betaStakers) + external + { + require(betaStakers.length > 0, "Wrong input parameters"); + for (uint256 i = 0; i < betaStakers.length; i++) { + forceBetaStakerDecreaseAuthorization(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 @@ -570,80 +454,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 - ); - } - // // // Undelegating a stake (unstaking) @@ -689,26 +499,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,66 +512,27 @@ 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. + /// @notice Stub for legacy "slash" method function slash(uint96 amount, address[] memory _stakingProviders) external override { - notify(amount, 0, address(0), _stakingProviders); + emit NotificationReceived(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. + /// @notice Stub for legacy "seize" method 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" + emit NotificationReceived( + amount, + rewardMultiplier, + notifier, + _stakingProviders ); - - 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 @@ -859,16 +610,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 +638,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 @@ -915,40 +651,48 @@ 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" - ); + optOutDecreaseAuthorization(stakingProvider, amount); + } - require(amount > 0, "Parameters must be specified"); + /// @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; - AppAuthorization storage authorization = stakingProviders[ - stakingProvider - ].authorizations[application]; - require( - authorization.authorized >= amount, - "Amount exceeds authorized" - ); + 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, temp); + } - authorization.deauthorizing = amount; - uint96 deauthorizingTo = authorization.authorized - amount; - emit AuthorizationDecreaseRequested( - stakingProvider, - application, - authorization.authorized, - deauthorizingTo - ); - IApplication(application).authorizationDecreaseRequested( - stakingProvider, - authorization.authorized, - deauthorizingTo - ); + /// @notice Forced deauthorization of stake above 15m T. + /// Can be called by anyone. + function forceAuthorizationCap(address stakingProvider) public { + forceDecreaseAuthorization(stakingProvider, MAX_STAKE); } /// @notice Returns the maximum application authorization @@ -970,6 +714,9 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { address application = stakingProviderStruct.authorizedApplications[ i ]; + if (skipApplication(application)) { + continue; + } maxAuthorization = MathUpgradeable.max( maxAuthorization, stakingProviderStruct.authorizations[application].authorized @@ -998,6 +745,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. @@ -1021,150 +783,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( @@ -1204,6 +822,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 @@ -1232,15 +872,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 @@ -1255,4 +886,82 @@ 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 + ]; + if (skipApplication(application)) { + continue; + } + 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 to deauthorize"); + } + + 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; + } + } + + // slither-disable-next-line dead-code + 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 eb54e0b9..091536e4 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; } @@ -96,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; + } } } @@ -149,6 +128,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,8 +161,152 @@ contract ExtendedTokenStaking is TokenStaking { .authorizedApplications = _applications; } - // to decrease size of test contract - function processSlashing(uint256 count) external override {} + /// @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 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 @@ -189,4 +315,65 @@ contract ExtendedTokenStaking is TokenStaking { { return stakingProviders[stakingProvider].authorizedApplications; } + + function getDeauthorizingAmount( + address stakingProvider, + address application + ) external view returns (uint96) { + return + stakingProviders[stakingProvider] + .authorizations[application] + .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 + function increaseStakeCheckpoint(address _delegator, uint96 _amount) + internal + { + newStakeCheckpoint(_delegator, _amount, true); + } + + function skipApplication(address) internal pure override returns (bool) { + return false; + } } 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; - } -} diff --git a/test/staking/TokenStaking.test.js b/test/staking/TokenStaking.test.js index bf91e4ee..fe0f2b6d 100644 --- a/test/staking/TokenStaking.test.js +++ b/test/staking/TokenStaking.test.js @@ -4,13 +4,7 @@ const { helpers } = require("hardhat") const { lastBlockTime, mineBlocks, increaseTime } = helpers.time const { to1e18 } = helpers.number -const { AddressZero, Zero } = ethers.constants - -const StakeTypes = { - NU: 0, - KEEP: 1, - T: 2, -} +const { Zero } = ethers.constants const ApplicationStatus = { NOT_APPROVED: 0, @@ -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,10 +61,12 @@ 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,256 +111,274 @@ describe("TokenStaking", () => { }) }) - describe("stake", () => { - context("when caller did not provide staking provider", () => { + describe("approveAuthorizationDecrease", () => { + 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 not approved", () => { 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") + application2Mock.approveAuthorizationDecrease(stakingProvider.address) + ).to.be.revertedWith("Application is not approved") }) }) - context("when caller did not provide beneficiary", () => { + context("when application was paused", () => { it("should revert", async () => { - amount = 0 + await tokenStaking + .connect(deployer) + .setPanicButton(application1Mock.address, panicButton.address) + await tokenStaking + .connect(panicButton) + .pauseApplication(application1Mock.address) await expect( - tokenStaking - .connect(staker) - .stake( - stakingProvider.address, - AddressZero, - authorizer.address, - amount - ) - ).to.be.revertedWith("Parameters must be specified") + application1Mock.approveAuthorizationDecrease(stakingProvider.address) + ).to.be.revertedWith("Application is not approved") }) }) - context("when caller did not provide authorizer", () => { + context("when application is disabled", () => { it("should revert", async () => { - amount = 0 + await tokenStaking + .connect(deployer) + .disableApplication(application1Mock.address) await expect( - tokenStaking - .connect(staker) - .stake( - stakingProvider.address, - beneficiary.address, - AddressZero, - amount - ) - ).to.be.revertedWith("Parameters must be specified") + application1Mock.approveAuthorizationDecrease(stakingProvider.address) + ).to.be.revertedWith("Application is not approved") + }) + }) + + context("when approve without request", () => { + it("should revert", async () => { + await expect( + application1Mock.approveAuthorizationDecrease(stakingProvider.address) + ).to.be.revertedWith("No deauthorizing in process") }) }) - 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 approve twice", () => { + it("should revert", async () => { + await tokenStaking + .connect(deployer) + .approveApplication(application2Mock.address) + await tokenStaking + .connect(authorizer) + .increaseAuthorization( + stakingProvider.address, + application2Mock.address, + amount + ) + await tokenStaking + .connect(authorizer) + ["legacyRequestAuthorizationDecrease(address)"]( + stakingProvider.address + ) + await application1Mock.approveAuthorizationDecrease( + stakingProvider.address + ) + await expect( + application1Mock.approveAuthorizationDecrease(stakingProvider.address) + ).to.be.revertedWith("No deauthorizing in process") + }) }) - 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 approve after request of partial deauthorization", () => { + const amountToDecrease = amount.div(3) + const expectedFromAmount = amount + const expectedToAmount = amount.sub(amountToDecrease) + let tx + + beforeEach(async () => { + await tokenStaking + .connect(authorizer) + ["legacyRequestAuthorizationDecrease(address,address,uint96)"]( + stakingProvider.address, + application1Mock.address, + amountToDecrease + ) + tx = await application1Mock.approveAuthorizationDecrease( + stakingProvider.address + ) }) - 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") - }) + it("should decrease authorized amount", async () => { + expect( + await tokenStaking.authorizedStake( + stakingProvider.address, + application1Mock.address + ) + ).to.equal(expectedToAmount) + expect( + await tokenStaking.getMaxAuthorization(stakingProvider.address) + ).to.equal(expectedToAmount) + }) + + it("should emit AuthorizationDecreaseApproved", async () => { + await expect(tx) + .to.emit(tokenStaking, "AuthorizationDecreaseApproved") + .withArgs( + stakingProvider.address, + application1Mock.address, + expectedFromAmount, + expectedToAmount + ) }) }) context( - "when stake delegates enough tokens to free staking provider", + "when approve after request of full deauthorization for one app", () => { - const amount = initialStakerBalance + const otherAmount = amount.div(3) 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( + .approveApplication(application2Mock.address) + await tokenStaking + .connect(authorizer) + .increaseAuthorization( + stakingProvider.address, + application2Mock.address, + otherAmount + ) + await tokenStaking + .connect(authorizer) + ["legacyRequestAuthorizationDecrease(address,address,uint96)"]( stakingProvider.address, - beneficiary.address, - authorizer.address, + application1Mock.address, amount ) - blockTimestamp = await lastBlockTime() + tx = await application1Mock.approveAuthorizationDecrease( + stakingProvider.address + ) }) - it("should set roles equal to the provided values", async () => { + it("should decrease authorized amount", async () => { expect( - await tokenStaking.rolesOf(stakingProvider.address) - ).to.deep.equal([ - staker.address, - beneficiary.address, - authorizer.address, - ]) + await tokenStaking.authorizedStake( + stakingProvider.address, + application1Mock.address + ) + ).to.equal(0) + expect( + await tokenStaking.authorizedStake( + stakingProvider.address, + application2Mock.address + ) + ).to.equal(otherAmount) + expect( + await tokenStaking.getMaxAuthorization(stakingProvider.address) + ).to.equal(otherAmount) }) - it("should set value of stakes", async () => { - await assertStake(stakingProvider.address, amount) + it("should emit AuthorizationDecreaseApproved", async () => { + await expect(tx) + .to.emit(tokenStaking, "AuthorizationDecreaseApproved") + .withArgs( + stakingProvider.address, + application1Mock.address, + amount, + Zero + ) }) + } + ) - it("should start staking timestamp", async () => { - expect( - await tokenStaking.getStartStakingTimestamp(stakingProvider.address) - ).to.equal(blockTimestamp) - }) + context( + "when approve after request of full deauthorization for last app", + () => { + let tx - it("should transfer tokens to the staking contract", async () => { - expect(await tToken.balanceOf(tokenStaking.address)).to.equal(amount) + beforeEach(async () => { + await tokenStaking + .connect(deployer) + .approveApplication(application2Mock.address) + await tokenStaking + .connect(authorizer) + .increaseAuthorization( + stakingProvider.address, + application2Mock.address, + amount + ) + await tokenStaking + .connect(authorizer) + ["legacyRequestAuthorizationDecrease(address)"]( + stakingProvider.address + ) + await application1Mock.approveAuthorizationDecrease( + stakingProvider.address + ) + tx = await application2Mock.approveAuthorizationDecrease( + stakingProvider.address + ) }) - it("should increase available amount to authorize", async () => { + it("should decrease authorized amount", async () => { expect( - await tokenStaking.getAvailableToAuthorize( + await tokenStaking.authorizedStake( stakingProvider.address, application1Mock.address ) - ).to.equal(amount) + ).to.equal(0) + expect( + await tokenStaking.authorizedStake( + stakingProvider.address, + application2Mock.address + ) + ).to.equal(0) }) - it("should emit Staked event", async () => { + it("should emit AuthorizationDecreaseApproved", async () => { await expect(tx) - .to.emit(tokenStaking, "Staked") + .to.emit(tokenStaking, "AuthorizationDecreaseApproved") .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 + application2Mock.address, + amount, + Zero ) - }) - - 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", () => { + describe("forceDecreaseAuthorization", () => { + context("when application is not approved", () => { it("should revert", async () => { await expect( - tokenStaking.connect(staker).approveApplication(AddressZero) - ).to.be.revertedWith("Caller is not the governance") + tokenStaking + .connect(auxiliaryAccount) + .forceDecreaseAuthorization( + stakingProvider.address, + application1Mock.address + ) + ).to.be.revertedWith("Application is not disabled") }) }) - 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", () => { + context("when application is approved", () => { it("should revert", async () => { await tokenStaking .connect(deployer) @@ -376,738 +386,143 @@ describe("TokenStaking", () => { await expect( tokenStaking .connect(deployer) - .approveApplication(application1Mock.address) - ).to.be.revertedWith("Can't approve application") + .forceDecreaseAuthorization( + stakingProvider.address, + application1Mock.address + ) + ).to.be.revertedWith("Application is not disabled") }) }) - context("when application is disabled", () => { + context("when application is paused", () => { it("should revert", async () => { await tokenStaking .connect(deployer) .approveApplication(application1Mock.address) await tokenStaking .connect(deployer) - .disableApplication(application1Mock.address) + .setPanicButton(application1Mock.address, panicButton.address) + await tokenStaking + .connect(panicButton) + .pauseApplication(application1Mock.address) await expect( tokenStaking - .connect(deployer) - .approveApplication(application1Mock.address) - ).to.be.revertedWith("Can't approve application") + .connect(staker) + .forceDecreaseAuthorization( + stakingProvider.address, + application1Mock.address + ) + ).to.be.revertedWith("Application is not disabled") }) }) - context("when approving new application", () => { - let tx - - beforeEach(async () => { - tx = await tokenStaking + context("when application was not authorized and got disabled", () => { + it("should revert", async () => { + 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) + await tokenStaking + .connect(deployer) + .disableApplication(application1Mock.address) + await expect( + tokenStaking + .connect(deployer) + .forceDecreaseAuthorization( + stakingProvider.address, + application1Mock.address + ) + ).to.be.revertedWith("Application is not authorized") }) }) - context("when approving paused application", () => { + context("when application was authorized and got disabled", () => { + const amount = initialStakerBalance let tx beforeEach(async () => { + await tokenStaking.connect(deployer).setAuthorizationCeiling(1) await tokenStaking .connect(deployer) .approveApplication(application1Mock.address) + await tToken.connect(staker).approve(tokenStaking.address, amount) await tokenStaking - .connect(deployer) - .setPanicButton(application1Mock.address, panicButton.address) + .connect(staker) + .stake( + stakingProvider.address, + beneficiary.address, + authorizer.address, + amount + ) await tokenStaking - .connect(panicButton) - .pauseApplication(application1Mock.address) + .connect(authorizer) + .increaseAuthorization( + stakingProvider.address, + application1Mock.address, + amount + ) + await tokenStaking + .connect(authorizer) + ["legacyRequestAuthorizationDecrease(address)"]( + stakingProvider.address + ) + + await tokenStaking + .connect(deployer) + .disableApplication(application1Mock.address) + tx = await tokenStaking .connect(deployer) - .approveApplication(application1Mock.address) + .forceDecreaseAuthorization( + stakingProvider.address, + application1Mock.address + ) }) - it("should enable application", async () => { + it("should set authorized amount to 0", async () => { expect( - await tokenStaking.applicationInfo(application1Mock.address) - ).to.deep.equal([ApplicationStatus.APPROVED, panicButton.address]) + await tokenStaking.authorizedStake( + stakingProvider.address, + application1Mock.address + ) + ).to.equal(0) }) - 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 allow to authorize more applications", async () => { + await tokenStaking + .connect(deployer) + .approveApplication(application2Mock.address) + await tokenStaking + .connect(authorizer) + .increaseAuthorization( + stakingProvider.address, + application2Mock.address, + amount + ) }) - it("should emit ApplicationStatusChanged", async () => { + it("should emit AuthorizationDecreaseApproved", async () => { await expect(tx) - .to.emit(tokenStaking, "ApplicationStatusChanged") - .withArgs(application1Mock.address, ApplicationStatus.APPROVED) - }) - }) - }) - - describe("increaseAuthorization", () => { - context("when caller is not authorizer", () => { - it("should revert", async () => { - const amount = initialStakerBalance - await expect( - tokenStaking - .connect(staker) - .increaseAuthorization( - 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 - ) - }) - - context("when application was not approved", () => { - it("should revert", async () => { - await expect( - tokenStaking - .connect(authorizer) - .increaseAuthorization( - stakingProvider.address, - application1Mock.address, - amount - ) - ).to.be.revertedWith("Application is not approved") - }) - }) - - context("when application was approved", () => { - beforeEach(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") - }) - }) - - context("when already authorized maximum applications", () => { - it("should revert", async () => { - await tokenStaking.connect(deployer).setAuthorizationCeiling(1) - await tokenStaking - .connect(authorizer) - .increaseAuthorization( - stakingProvider.address, - application1Mock.address, - amount - ) - 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") - }) - }) - - context("when authorize staked tokens in one tx", () => { - let tx - const authorizedAmount = amount.div(3) - - beforeEach(async () => { - tx = await tokenStaking - .connect(authorizer) - .increaseAuthorization( - stakingProvider.address, - application1Mock.address, - authorizedAmount - ) - }) - - it("should increase authorization", async () => { - expect( - await tokenStaking.authorizedStake( - stakingProvider.address, - application1Mock.address - ) - ).to.equal(authorizedAmount) - expect( - await tokenStaking.getMaxAuthorization(stakingProvider.address) - ).to.equal(authorizedAmount) - }) - - 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) - }) - - it("should inform application", async () => { - await assertApplicationStakingProviders( - application1Mock, - stakingProvider.address, - authorizedAmount, - Zero - ) - }) - - 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") + .to.emit(tokenStaking, "AuthorizationDecreaseApproved") + .withArgs( + stakingProvider.address, + application1Mock.address, + amount, + 0 + ) }) }) - - 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 + describe("forceAuthorizationCap", () => { + 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) @@ -1117,64 +532,100 @@ 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 stake is less than 15m", () => { it("should revert", async () => { + await tokenStaking + .connect(authorizer) + .increaseAuthorization( + stakingProvider.address, + application1Mock.address, + maxStake + ) await expect( - application2Mock.approveAuthorizationDecrease(stakingProvider.address) - ).to.be.revertedWith("Application is not approved") + tokenStaking + .connect(authorizer) + ["forceAuthorizationCap(address)"](stakingProvider.address) + ).to.be.revertedWith("Nothing to deauthorize") }) }) - context("when application was paused", () => { - it("should revert", async () => { + context("when authorization is more than 15m for one application", () => { + let tx + const amount2 = maxStake.div(3) + + beforeEach(async () => { await tokenStaking .connect(deployer) - .setPanicButton(application1Mock.address, panicButton.address) + .approveApplication(application2Mock.address) + await tokenStaking - .connect(panicButton) - .pauseApplication(application1Mock.address) - await expect( - application1Mock.approveAuthorizationDecrease(stakingProvider.address) - ).to.be.revertedWith("Application is not approved") - }) - }) + .connect(authorizer) + .increaseAuthorization( + stakingProvider.address, + application1Mock.address, + amount + ) - context("when application is disabled", () => { - it("should revert", async () => { await tokenStaking + .connect(authorizer) + .increaseAuthorization( + stakingProvider.address, + application2Mock.address, + amount2 + ) + + tx = await tokenStaking .connect(deployer) - .disableApplication(application1Mock.address) - await expect( - application1Mock.approveAuthorizationDecrease(stakingProvider.address) - ).to.be.revertedWith("Application is not approved") + ["forceAuthorizationCap(address)"](stakingProvider.address) }) - }) - context("when approve without request", () => { - it("should revert", async () => { - await expect( - application1Mock.approveAuthorizationDecrease(stakingProvider.address) - ).to.be.revertedWith("No deauthorizing in process") + 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 approve twice", () => { - it("should revert", async () => { + context("when previous deauthorization is in process", () => { + let tx + const amountToDecrease = amount.div(20) + const amountToDecrease2 = maxStake.add(amountToDecrease) + + beforeEach(async () => { await tokenStaking .connect(deployer) .approveApplication(application2Mock.address) + + await tokenStaking + .connect(authorizer) + .increaseAuthorization( + stakingProvider.address, + application1Mock.address, + amount + ) + await tokenStaking .connect(authorizer) .increaseAuthorization( @@ -1182,45 +633,122 @@ describe("TokenStaking", () => { application2Mock.address, amount ) + await tokenStaking .connect(authorizer) - ["requestAuthorizationDecrease(address)"](stakingProvider.address) - application1Mock.approveAuthorizationDecrease(stakingProvider.address) - await expect( - application1Mock.approveAuthorizationDecrease(stakingProvider.address) - ).to.be.revertedWith("No deauthorizing in process") + ["legacyRequestAuthorizationDecrease(address,address,uint96)"]( + stakingProvider.address, + application1Mock.address, + amountToDecrease + ) + await tokenStaking + .connect(authorizer) + ["legacyRequestAuthorizationDecrease(address,address,uint96)"]( + stakingProvider.address, + application2Mock.address, + amountToDecrease2 + ) + + tx = await tokenStaking + .connect(deployer) + ["forceAuthorizationCap(address)"](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(maxStake) + }) + + it("should send request to application with last amount", async () => { + 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 approve after request of partial deauthorization", () => { - const amountToDecrease = amount.div(3) - const expectedFromAmount = amount - const expectedToAmount = amount.sub(amountToDecrease) + 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) - ["requestAuthorizationDecrease(address,address,uint96)"]( + .increaseAuthorization( stakingProvider.address, application1Mock.address, - amountToDecrease + amount ) - tx = await application1Mock.approveAuthorizationDecrease( - stakingProvider.address - ) + + await tokenStaking + .connect(otherStaker) + .increaseAuthorization( + otherStaker.address, + application1Mock.address, + amount + ) + + tx = await tokenStaking + .connect(deployer) + ["forceAuthorizationCap(address[])"]([ + stakingProvider.address, + otherStaker.address, + ]) }) - it("should decrease authorized amount", async () => { + it("should set authorized amount to max", async () => { expect( await tokenStaking.authorizedStake( stakingProvider.address, application1Mock.address ) - ).to.equal(expectedToAmount) + ).to.equal(maxStake) expect( - await tokenStaking.getMaxAuthorization(stakingProvider.address) - ).to.equal(expectedToAmount) + await tokenStaking.authorizedStake( + otherStaker.address, + application1Mock.address + ) + ).to.equal(maxStake) }) it("should emit AuthorizationDecreaseApproved", async () => { @@ -1229,217 +757,233 @@ describe("TokenStaking", () => { .withArgs( stakingProvider.address, application1Mock.address, - expectedFromAmount, - expectedToAmount + amount, + maxStake + ) + await expect(tx) + .to.emit(tokenStaking, "AuthorizationDecreaseApproved") + .withArgs( + otherStaker.address, + application1Mock.address, + amount, + maxStake ) }) }) + }) - context( - "when approve after request of full deauthorization for one app", - () => { - const otherAmount = amount.div(3) - let tx + describe("optOutDecreaseAuthorization", () => { + const maxStake = to1e18("15000000") // 15m + const amount = maxStake.mul(2) - beforeEach(async () => { - await tokenStaking - .connect(deployer) - .approveApplication(application2Mock.address) - await tokenStaking + 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 authorizer") + }) + }) + + context("when amount to decrease is zero", () => { + it("should revert", async () => { + await expect( + tokenStaking .connect(authorizer) - .increaseAuthorization( - stakingProvider.address, - application2Mock.address, - otherAmount - ) - await tokenStaking + .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(authorizer) - ["requestAuthorizationDecrease(address,address,uint96)"]( - stakingProvider.address, - application1Mock.address, - amount - ) - tx = await application1Mock.approveAuthorizationDecrease( - stakingProvider.address - ) - }) + .optOutDecreaseAuthorization(stakingProvider.address, 1) + ).to.be.revertedWith("Opt-out amount too high") - it("should decrease authorized amount", async () => { - expect( - await tokenStaking.authorizedStake( - stakingProvider.address, - application1Mock.address - ) - ).to.equal(0) - expect( - await tokenStaking.authorizedStake( + await tokenStaking + .connect(authorizer) + .increaseAuthorization( + stakingProvider.address, + application1Mock.address, + amount2 + ) + await expect( + tokenStaking + .connect(authorizer) + .optOutDecreaseAuthorization( stakingProvider.address, - application2Mock.address + amountToOptOut.add(1) ) - ).to.equal(otherAmount) - expect( - await tokenStaking.getMaxAuthorization(stakingProvider.address) - ).to.equal(otherAmount) - }) + ).to.be.revertedWith("Opt-out amount too high") - it("should emit AuthorizationDecreaseApproved", async () => { - await expect(tx) - .to.emit(tokenStaking, "AuthorizationDecreaseApproved") - .withArgs( - stakingProvider.address, - application1Mock.address, - amount, - Zero - ) - }) - } - ) + await tokenStaking + .connect(authorizer) + .optOutDecreaseAuthorization(stakingProvider.address, amountToOptOut) + await expect( + tokenStaking + .connect(authorizer) + .optOutDecreaseAuthorization(stakingProvider.address, 1) + ).to.be.revertedWith("Opt-out amount too high") + }) + }) - context( - "when approve after request of full deauthorization for last app", - () => { - let tx + 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(authorizer) + .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(deployer) - .approveApplication(application2Mock.address) + .connect(authorizer) + .optOutDecreaseAuthorization( + stakingProvider.address, + maxStake.div(2).sub(amountToOptOut) + ) + await tokenStaking .connect(authorizer) - .increaseAuthorization( + ["legacyRequestAuthorizationDecrease(address,address,uint96)"]( stakingProvider.address, - application2Mock.address, - amount + application1Mock.address, + maxStake.div(4) ) await tokenStaking .connect(authorizer) - ["requestAuthorizationDecrease(address)"](stakingProvider.address) + ["legacyRequestAuthorizationDecrease(address,address,uint96)"]( + stakingProvider.address, + application2Mock.address, + maxStake.div(4) + ) await application1Mock.approveAuthorizationDecrease( stakingProvider.address ) - tx = await application2Mock.approveAuthorizationDecrease( + await application2Mock.approveAuthorizationDecrease( stakingProvider.address ) }) - it("should decrease authorized amount", async () => { - expect( - await tokenStaking.authorizedStake( - stakingProvider.address, - application1Mock.address - ) - ).to.equal(0) + it("should update available opt-out amount", async () => { expect( - await tokenStaking.authorizedStake( - stakingProvider.address, - application2Mock.address - ) + await tokenStaking.getAvailableOptOutAmount(stakingProvider.address) ).to.equal(0) }) - it("should emit AuthorizationDecreaseApproved", async () => { - await expect(tx) - .to.emit(tokenStaking, "AuthorizationDecreaseApproved") - .withArgs( - stakingProvider.address, - application2Mock.address, - amount, - Zero - ) + it("should revert when calling for more opt-out", async () => { + await expect( + tokenStaking + .connect(authorizer) + .optOutDecreaseAuthorization(stakingProvider.address, 1) + ).to.be.revertedWith("Opt-out amount too high") }) - } - ) - }) - - describe("forceDecreaseAuthorization", () => { - context("when application is not approved", () => { - it("should revert", async () => { - await expect( - tokenStaking - .connect(auxiliaryAccount) - .forceDecreaseAuthorization( - stakingProvider.address, - application1Mock.address - ) - ).to.be.revertedWith("Application is not disabled") - }) - }) - - context("when application is approved", () => { - it("should revert", async () => { - await tokenStaking - .connect(deployer) - .approveApplication(application1Mock.address) - await expect( - tokenStaking - .connect(deployer) - .forceDecreaseAuthorization( - stakingProvider.address, - application1Mock.address - ) - ).to.be.revertedWith("Application is not disabled") - }) - }) - - context("when application is 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( - tokenStaking - .connect(staker) - .forceDecreaseAuthorization( - stakingProvider.address, - application1Mock.address - ) - ).to.be.revertedWith("Application is not disabled") - }) - }) - - context("when application was not authorized and got disabled", () => { - it("should revert", async () => { - await tokenStaking - .connect(deployer) - .approveApplication(application1Mock.address) - await tokenStaking - .connect(deployer) - .disableApplication(application1Mock.address) - await expect( - tokenStaking - .connect(deployer) - .forceDecreaseAuthorization( - stakingProvider.address, - application1Mock.address - ) - ).to.be.revertedWith("Application is not authorized") }) }) - context("when application was authorized and got disabled", () => { - const amount = initialStakerBalance + 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(deployer).setAuthorizationCeiling(1) - 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 - ) await tokenStaking .connect(authorizer) .increaseAuthorization( @@ -1447,42 +991,49 @@ describe("TokenStaking", () => { application1Mock.address, amount ) + await tokenStaking .connect(authorizer) - ["requestAuthorizationDecrease(address)"](stakingProvider.address) + ["legacyRequestAuthorizationDecrease(address,address,uint96)"]( + stakingProvider.address, + application1Mock.address, + amountToDecrease + ) await tokenStaking .connect(deployer) - .disableApplication(application1Mock.address) + ["forceAuthorizationCap(address)"](stakingProvider.address) + await tokenStaking + .connect(authorizer) + .optOutDecreaseAuthorization(stakingProvider.address, amountToOptOut1) tx = await tokenStaking - .connect(deployer) - .forceDecreaseAuthorization( - stakingProvider.address, - application1Mock.address - ) + .connect(authorizer) + .optOutDecreaseAuthorization(stakingProvider.address, amountToOptOut2) }) - it("should set authorized amount to 0", async () => { + it("should update authorized amount", async () => { expect( await tokenStaking.authorizedStake( stakingProvider.address, application1Mock.address ) - ).to.equal(0) + ).to.equal(expectedAmount) }) - it("should allow to authorize more applications", async () => { - await tokenStaking - .connect(deployer) - .approveApplication(application2Mock.address) - await tokenStaking - .connect(authorizer) - .increaseAuthorization( + 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, - amount + application2Mock.address ) + ).to.equal(0) }) it("should emit AuthorizationDecreaseApproved", async () => { @@ -1491,189 +1042,514 @@ describe("TokenStaking", () => { .withArgs( stakingProvider.address, application1Mock.address, - amount, - 0 + maxStake.sub(amountToOptOut1), + maxStake.sub(amountToOptOut1).sub(amountToOptOut2) ) }) }) }) - describe("pauseApplication", () => { + describe("forceBetaStakerDecreaseAuthorization", () => { + 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(deployer) - .setPanicButton(application1Mock.address, panicButton.address) + .connect(staker) + .stake( + stakingProvider.address, + beneficiary.address, + authorizer.address, + amount + ) }) - context("when caller is not the panic button address", () => { + context("when caller is not the governance", () => { it("should revert", async () => { await expect( tokenStaking - .connect(deployer) - .pauseApplication(application1Mock.address) - ).to.be.revertedWith("Caller is not the panic button") - }) - }) - - context("when application is disabled", () => { - it("should revert", async () => { - await tokenStaking - .connect(deployer) - .disableApplication(application1Mock.address) - await expect( - tokenStaking - .connect(panicButton) - .pauseApplication(application1Mock.address) - ).to.be.revertedWith("Can't pause application") + .connect(stakingProvider) + ["forceBetaStakerDecreaseAuthorization(address)"]( + stakingProvider.address + ) + ).to.be.revertedWith("Caller is not the governance") }) }) - context("when application was paused", () => { + context("when nothing was authorized", () => { it("should revert", async () => { - await tokenStaking - .connect(panicButton) - .pauseApplication(application1Mock.address) await expect( tokenStaking - .connect(panicButton) - .pauseApplication(application1Mock.address) - ).to.be.revertedWith("Can't pause application") + .connect(deployer) + ["forceBetaStakerDecreaseAuthorization(address)"]( + stakingProvider.address + ) + ).to.be.revertedWith("Nothing was authorized") }) }) - context("when pause active application", () => { + 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(panicButton) - .pauseApplication(application1Mock.address) + .connect(deployer) + ["forceBetaStakerDecreaseAuthorization(address)"]( + stakingProvider.address + ) }) - it("should pause application", async () => { + it("should set authorized amount to 0", async () => { expect( - await tokenStaking.applicationInfo(application1Mock.address) - ).to.deep.equal([ApplicationStatus.PAUSED, panicButton.address]) + await tokenStaking.authorizedStake( + stakingProvider.address, + application1Mock.address + ) + ).to.equal(0) + expect( + await tokenStaking.authorizedStake( + stakingProvider.address, + application2Mock.address + ) + ).to.equal(0) }) - 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 not send request to the applications", async () => { + await assertApplicationStakingProviders( + application1Mock, + stakingProvider.address, + amount, + 0 + ) + await assertApplicationStakingProviders( + application2Mock, + stakingProvider.address, + amount2, + 0 ) }) - it("should emit ApplicationStatusChanged", async () => { + it("should emit AuthorizationDecreaseApproved", async () => { await expect(tx) - .to.emit(tokenStaking, "ApplicationStatusChanged") - .withArgs(application1Mock.address, ApplicationStatus.PAUSED) - }) - }) - }) - - describe("disableApplication", () => { - beforeEach(async () => { - await tokenStaking - .connect(deployer) - .approveApplication(application1Mock.address) - await tokenStaking - .connect(deployer) - .setPanicButton(application1Mock.address, panicButton.address) - }) - - context("when caller is not the governance", () => { - it("should revert", async () => { - await expect( - tokenStaking - .connect(panicButton) - .disableApplication(application1Mock.address) - ).to.be.revertedWith("Caller is not the governance") + .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 application is not approved", () => { - it("should revert", async () => { - await expect( - tokenStaking - .connect(deployer) - .disableApplication(application2Mock.address) - ).to.be.revertedWith("Can't disable application") - }) - }) + context("when previous deauthorization is in process", () => { + let tx + const amountToDecrease = amount.div(20) + const amountToDecrease2 = amountToDecrease.mul(2) - context("when application is disabled", () => { - it("should revert", async () => { + beforeEach(async () => { await tokenStaking .connect(deployer) - .disableApplication(application1Mock.address) - await expect( - tokenStaking - .connect(deployer) - .disableApplication(application1Mock.address) - ).to.be.revertedWith("Can't disable application") - }) - }) + .approveApplication(application2Mock.address) - const contextDisable = (preparation) => { - let tx + await tokenStaking + .connect(authorizer) + .increaseAuthorization( + stakingProvider.address, + application1Mock.address, + amount + ) - beforeEach(async () => { - await preparation() + await tokenStaking + .connect(authorizer) + .increaseAuthorization( + stakingProvider.address, + application2Mock.address, + amount + ) + + await tokenStaking + .connect(authorizer) + ["legacyRequestAuthorizationDecrease(address,address,uint96)"]( + stakingProvider.address, + application1Mock.address, + amountToDecrease + ) + await tokenStaking + .connect(authorizer) + ["legacyRequestAuthorizationDecrease(address,address,uint96)"]( + stakingProvider.address, + application2Mock.address, + amountToDecrease2 + ) tx = await tokenStaking .connect(deployer) - .disableApplication(application1Mock.address) + ["forceBetaStakerDecreaseAuthorization(address)"]( + stakingProvider.address + ) }) - it("should disable application", async () => { + it("should set authorized amount to 0", async () => { expect( - await tokenStaking.applicationInfo(application1Mock.address) - ).to.deep.equal([ApplicationStatus.DISABLED, panicButton.address]) + await tokenStaking.authorizedStake( + stakingProvider.address, + application1Mock.address + ) + ).to.equal(0) + expect( + await tokenStaking.authorizedStake( + stakingProvider.address, + application2Mock.address + ) + ).to.equal(0) }) - 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 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 emit ApplicationStatusChanged", async () => { - await expect(tx) - .to.emit(tokenStaking, "ApplicationStatusChanged") - .withArgs(application1Mock.address, ApplicationStatus.DISABLED) + 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) }) - } - context("when disable approved application", () => { - contextDisable(() => {}) - }) - - context("when disable paused application", () => { - contextDisable(async () => { - await tokenStaking - .connect(panicButton) - .pauseApplication(application1Mock.address) + 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 + ) }) }) - }) - describe("setPanicButton", () => { - beforeEach(async () => { - await tokenStaking - .connect(deployer) - .approveApplication(application1Mock.address) - }) + context("when sending transaction for multiple staking providers", () => { + let tx - context("when caller is not the governance", () => { - it("should revert", async () => { - await expect( - tokenStaking - .connect(staker) - .setPanicButton(application1Mock.address, panicButton.address) + 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) + ["forceBetaStakerDecreaseAuthorization(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 + .connect(deployer) + .approveApplication(application1Mock.address) + await tokenStaking + .connect(deployer) + .setPanicButton(application1Mock.address, panicButton.address) + }) + + context("when caller is not the panic button address", () => { + it("should revert", async () => { + await expect( + tokenStaking + .connect(deployer) + .pauseApplication(application1Mock.address) + ).to.be.revertedWith("Caller is not the panic button") + }) + }) + + context("when application is disabled", () => { + it("should revert", async () => { + await tokenStaking + .connect(deployer) + .disableApplication(application1Mock.address) + await expect( + tokenStaking + .connect(panicButton) + .pauseApplication(application1Mock.address) + ).to.be.revertedWith("Can't pause application") + }) + }) + + context("when application was paused", () => { + it("should revert", async () => { + await tokenStaking + .connect(panicButton) + .pauseApplication(application1Mock.address) + await expect( + tokenStaking + .connect(panicButton) + .pauseApplication(application1Mock.address) + ).to.be.revertedWith("Can't pause application") + }) + }) + + context("when pause active application", () => { + let tx + + beforeEach(async () => { + tx = await tokenStaking + .connect(panicButton) + .pauseApplication(application1Mock.address) + }) + + it("should pause application", async () => { + expect( + await tokenStaking.applicationInfo(application1Mock.address) + ).to.deep.equal([ApplicationStatus.PAUSED, 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.PAUSED) + }) + }) + }) + + describe("disableApplication", () => { + beforeEach(async () => { + await tokenStaking + .connect(deployer) + .approveApplication(application1Mock.address) + await tokenStaking + .connect(deployer) + .setPanicButton(application1Mock.address, panicButton.address) + }) + + context("when caller is not the governance", () => { + it("should revert", async () => { + await expect( + tokenStaking + .connect(panicButton) + .disableApplication(application1Mock.address) + ).to.be.revertedWith("Caller is not the governance") + }) + }) + + context("when application is not approved", () => { + it("should revert", async () => { + await expect( + tokenStaking + .connect(deployer) + .disableApplication(application2Mock.address) + ).to.be.revertedWith("Can't disable application") + }) + }) + + context("when application is disabled", () => { + it("should revert", async () => { + await tokenStaking + .connect(deployer) + .disableApplication(application1Mock.address) + await expect( + tokenStaking + .connect(deployer) + .disableApplication(application1Mock.address) + ).to.be.revertedWith("Can't disable application") + }) + }) + + const contextDisable = (preparation) => { + let tx + + beforeEach(async () => { + await preparation() + + tx = await tokenStaking + .connect(deployer) + .disableApplication(application1Mock.address) + }) + + it("should disable application", async () => { + expect( + await tokenStaking.applicationInfo(application1Mock.address) + ).to.deep.equal([ApplicationStatus.DISABLED, 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.DISABLED) + }) + } + + context("when disable approved application", () => { + contextDisable(() => {}) + }) + + context("when disable paused application", () => { + contextDisable(async () => { + await tokenStaking + .connect(panicButton) + .pauseApplication(application1Mock.address) + }) + }) + }) + + describe("setPanicButton", () => { + beforeEach(async () => { + await tokenStaking + .connect(deployer) + .approveApplication(application1Mock.address) + }) + + context("when caller is not the governance", () => { + it("should revert", async () => { + await expect( + tokenStaking + .connect(staker) + .setPanicButton(application1Mock.address, panicButton.address) ).to.be.revertedWith("Caller is not the governance") }) }) @@ -1755,420 +1631,31 @@ describe("TokenStaking", () => { }) }) - describe("topUp", () => { - context("when amount is zero", () => { + describe("unstakeT", () => { + context("when staking provider has no stake", () => { it("should revert", async () => { - await tToken - .connect(staker) - .approve(tokenStaking.address, initialStakerBalance) - await tokenStaking - .connect(staker) - .stake( - stakingProvider.address, - staker.address, - staker.address, - initialStakerBalance - ) await expect( - tokenStaking - .connect(stakingProvider) - .topUp(stakingProvider.address, 0) - ).to.be.revertedWith("Parameters must be specified") + tokenStaking.unstakeT(deployer.address, 0) + ).to.be.revertedWith("Not owner or provider") }) }) - context("when staking provider has no delegated stake", () => { + context("when caller is not owner or staking provider", () => { 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) + .approve(tokenStaking.address, initialStakerBalance) 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) + initialStakerBalance ) - }) - }) - }) - - 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") + tokenStaking.connect(authorizer).unstakeT(stakingProvider.address, 0) + ).to.be.revertedWith("Not owner or provider") }) }) @@ -2180,1246 +1667,226 @@ describe("TokenStaking", () => { 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 - ) - 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) - }) + 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 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( - stakingProvider.address, - [application1Mock.address, application2Mock.address] - ) - await extendedTokenStaking.cleanAuthorizedApplications( + await tokenStaking.setAuthorizedApplications(stakingProvider.address, [ + application1Mock.address, + application2Mock.address, + ]) + await tokenStaking.cleanAuthorizedApplications( stakingProvider.address, 2 ) @@ -3427,9 +1894,7 @@ describe("TokenStaking", () => { it("should remove all applications", async () => { expect( - await extendedTokenStaking.getAuthorizedApplications( - stakingProvider.address - ) + await tokenStaking.getAuthorizedApplications(stakingProvider.address) ).to.deep.equal([]) }) }) @@ -3438,16 +1903,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 +1920,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 +1932,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 +1949,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 +1961,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 +1969,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 +1982,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]) @@ -3526,6 +1991,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 @@ -3550,19 +2055,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) - } })