diff --git a/contracts/contracts/TACoApplication.sol b/contracts/contracts/TACoApplication.sol index 2fa50c45..2fb18187 100644 --- a/contracts/contracts/TACoApplication.sol +++ b/contracts/contracts/TACoApplication.sol @@ -411,15 +411,36 @@ contract TACoApplication is * @param _stakingProvider Staking provider address */ function updateRewardInternal(address _stakingProvider) internal { + StakingProviderInfo storage info = stakingProviderInfo[_stakingProvider]; + if ( + _stakingProvider != address(0) && + info.endPenalty != 0 && + info.endPenalty <= block.timestamp + ) { + resetReward(_stakingProvider, info); + } + rewardPerTokenStored = rewardPerToken(); lastUpdateTime = lastTimeRewardApplicable(); if (_stakingProvider != address(0)) { - StakingProviderInfo storage info = stakingProviderInfo[_stakingProvider]; info.tReward = availableRewards(_stakingProvider); info.rewardPerTokenPaid = rewardPerTokenStored; } } + /** + * @notice Resets reward after penalty + */ + function resetReward(address _stakingProvider, StakingProviderInfo storage _info) internal { + uint96 before = effectiveAuthorized(_info.authorized, _info.penaltyPercent); + _info.endPenalty = 0; + _info.penaltyPercent = 0; + if (_info.operatorConfirmed) { + authorizedOverall += _info.authorized - before; + } + emit RewardReset(_stakingProvider); + } + /** * @notice Returns last time when reward was applicable */ @@ -456,37 +477,36 @@ contract TACoApplication is return result.toUint96(); } - // FIXME function effectiveAuthorized( uint96 _authorized, uint192 _penaltyPercent - ) internal view returns (uint96) { + ) internal pure returns (uint96) { return uint96((_authorized * (PENALTY_BASE - _penaltyPercent)) / PENALTY_BASE); } + /// @dev This view should be called after updateReward modifier function effectiveAuthorized( uint96 _authorized, StakingProviderInfo storage _info ) internal view returns (uint96) { - if (_info.endPenalty != 0 && _info.endPenalty > block.timestamp) { - return effectiveAuthorized(_authorized, _info.penaltyPercent); - } else { + if (_info.endPenalty == 0) { return _info.authorized; } + return effectiveAuthorized(_authorized, _info.penaltyPercent); } + /// @dev This view should be called after updateReward modifier function effectiveDifference( uint96 _from, uint96 _to, StakingProviderInfo storage _info ) internal view returns (uint96) { - if (_info.endPenalty != 0 && _info.endPenalty > block.timestamp) { - uint96 effectiveFrom = effectiveAuthorized(_from, _info.penaltyPercent); - uint96 effectiveTo = effectiveAuthorized(_to, _info.penaltyPercent); - return effectiveFrom - effectiveTo; - } else { + if (_info.endPenalty == 0) { return _from - _to; } + uint96 effectiveFrom = effectiveAuthorized(_from, _info.penaltyPercent); + uint96 effectiveTo = effectiveAuthorized(_to, _info.penaltyPercent); + return effectiveFrom - effectiveTo; } /** @@ -789,6 +809,17 @@ contract TACoApplication is return uint64(endDeauthorization - block.timestamp); } + /** + * @notice Returns information about reward penalty. + */ + function getPenalty( + address _stakingProvider + ) external view returns (uint192 penalty, uint64 endPenalty) { + StakingProviderInfo storage info = stakingProviderInfo[_stakingProvider]; + penalty = info.penaltyPercent; + endPenalty = info.endPenalty; + } + /** * @notice Get the value of authorized tokens for active providers as well as providers and their authorized tokens * @param _startIndex Start index for looking in providers array @@ -1030,9 +1061,18 @@ contract TACoApplication is msg.sender == address(childApplication), "Only child application allowed to penalize" ); + + if (_stakingProvider == address(0)) { + return; + } + StakingProviderInfo storage info = stakingProviderInfo[_stakingProvider]; - info.penaltyPercent = penaltyDefault; + uint96 before = effectiveAuthorized(info.authorized, info.penaltyPercent); info.endPenalty = uint64(block.timestamp + penaltyDuration); + info.penaltyPercent = penaltyDefault; + if (info.operatorConfirmed) { + authorizedOverall -= before - effectiveAuthorized(info.authorized, info.penaltyPercent); + } emit Penalized(_stakingProvider, info.penaltyPercent, info.endPenalty); } @@ -1040,12 +1080,10 @@ contract TACoApplication is * @notice Resets future reward back to 100% * @param _stakingProvider Staking provider address */ - function resetReward(address _stakingProvider) external updateReward(_stakingProvider) { + function resetReward(address _stakingProvider) external { StakingProviderInfo storage info = stakingProviderInfo[_stakingProvider]; require(info.endPenalty != 0, "There are no any penalties"); require(info.endPenalty <= block.timestamp, "Penalty is still ongoing"); - info.endPenalty = 0; - info.penaltyPercent = 0; - emit RewardReset(_stakingProvider); + updateRewardInternal(_stakingProvider); } } diff --git a/contracts/test/TACoApplicationTestSet.sol b/contracts/test/TACoApplicationTestSet.sol index 33e6dff5..017d10d3 100644 --- a/contracts/test/TACoApplicationTestSet.sol +++ b/contracts/test/TACoApplicationTestSet.sol @@ -172,4 +172,8 @@ contract ChildApplicationForTACoApplicationMock { function confirmOperatorAddress(address _operator) external { rootApplication.confirmOperatorAddress(_operator); } + + function penalize(address _stakingProvider) external { + rootApplication.penalize(_stakingProvider); + } } diff --git a/tests/application/test_authorization.py b/tests/application/test_authorization.py index 90922970..4f78e907 100644 --- a/tests/application/test_authorization.py +++ b/tests/application/test_authorization.py @@ -25,6 +25,8 @@ END_COMMITMENT_SLOT = 8 MIN_AUTHORIZATION = Web3.to_wei(40_000, "ether") DEAUTHORIZATION_DURATION = 60 * 60 * 24 * 60 # 60 days in seconds +PENALTY_DEFAULT = 1000 # 10% penalty +PENALTY_DURATION = 60 * 60 * 24 # 1 day in seconds def test_authorization_parameters(taco_application): diff --git a/tests/application/test_operator.py b/tests/application/test_operator.py index eb0b15bf..af9aee3a 100644 --- a/tests/application/test_operator.py +++ b/tests/application/test_operator.py @@ -22,6 +22,8 @@ CONFIRMATION_SLOT = 1 MIN_AUTHORIZATION = Web3.to_wei(40_000, "ether") MIN_OPERATOR_SECONDS = 24 * 60 * 60 +PENALTY_DEFAULT = 1000 # 10% penalty +PENALTY_DURATION = 60 * 60 * 24 # 1 day in seconds def test_bond_operator(accounts, threshold_staking, taco_application, child_application, chain): @@ -356,6 +358,10 @@ def test_confirm_address( min_authorization = MIN_AUTHORIZATION min_operator_seconds = MIN_OPERATOR_SECONDS + # Only child app can penalize + with ape.reverts("Only child application allowed to confirm operator"): + taco_application.confirmOperatorAddress(staking_provider, sender=staking_provider) + # Skips confirmation if operator is not associated with staking provider child_application.confirmOperatorAddress(staking_provider, sender=staking_provider) assert not taco_application.isOperatorConfirmed(staking_provider) @@ -398,3 +404,120 @@ def test_slash(accounts, threshold_staking, taco_application): assert threshold_staking.notifier() == investigator assert threshold_staking.stakingProvidersToSeize(0) == staking_provider assert threshold_staking.getLengthOfStakingProvidersToSeize() == 1 + + +def test_penalize(accounts, threshold_staking, taco_application, child_application, chain): + creator, staking_provider, *everyone_else = accounts[0:] + min_authorization = MIN_AUTHORIZATION + + # Only child app can penalize + with ape.reverts("Only child application allowed to penalize"): + taco_application.penalize(staking_provider, sender=creator) + + # Skips penalty if staking provider was not specified + child_application.penalize(ZERO_ADDRESS, sender=staking_provider) + assert taco_application.getPenalty(staking_provider) == [0, 0] + + # Penalize staking provider with 0 authorization + tx = child_application.penalize(staking_provider, sender=staking_provider) + timestamp = tx.timestamp + end_of_penalty = timestamp + PENALTY_DURATION + assert taco_application.getPenalty(staking_provider) == [PENALTY_DEFAULT, end_of_penalty] + assert tx.events == [ + taco_application.Penalized( + stakingProvider=staking_provider, + penaltyPercent=PENALTY_DEFAULT, + endPenalty=end_of_penalty, + ) + ] + assert taco_application.authorizedOverall() == 0 + + # Increase authorization with no confirmation and check penalty + chain.pending_timestamp += PENALTY_DURATION + threshold_staking.authorizationIncreased(staking_provider, 0, min_authorization, sender=creator) + tx = child_application.penalize(staking_provider, sender=staking_provider) + timestamp = tx.timestamp + end_of_penalty = timestamp + PENALTY_DURATION + assert taco_application.getPenalty(staking_provider) == [PENALTY_DEFAULT, end_of_penalty] + assert taco_application.authorizedOverall() == 0 + assert tx.events == [ + taco_application.Penalized( + stakingProvider=staking_provider, + penaltyPercent=PENALTY_DEFAULT, + endPenalty=end_of_penalty, + ) + ] + + # Increase authorization with confirmation and check penalty + chain.pending_timestamp += PENALTY_DURATION + taco_application.bondOperator(staking_provider, staking_provider, sender=staking_provider) + child_application.confirmOperatorAddress(staking_provider, sender=staking_provider) + assert taco_application.authorizedOverall() == min_authorization + tx = child_application.penalize(staking_provider, sender=staking_provider) + timestamp = tx.timestamp + end_of_penalty = timestamp + PENALTY_DURATION + assert taco_application.getPenalty(staking_provider) == [PENALTY_DEFAULT, end_of_penalty] + assert taco_application.authorizedOverall() == min_authorization * 9 / 10 + assert tx.events == [ + taco_application.Penalized( + stakingProvider=staking_provider, + penaltyPercent=PENALTY_DEFAULT, + endPenalty=end_of_penalty, + ) + ] + + # Penalize again + tx = child_application.penalize(staking_provider, sender=staking_provider) + timestamp = tx.timestamp + end_of_penalty = timestamp + PENALTY_DURATION + assert taco_application.getPenalty(staking_provider) == [PENALTY_DEFAULT, end_of_penalty] + assert taco_application.authorizedOverall() == min_authorization * 9 / 10 + assert tx.events == [ + taco_application.Penalized( + stakingProvider=staking_provider, + penaltyPercent=PENALTY_DEFAULT, + endPenalty=end_of_penalty, + ) + ] + + +def test_reset_reward(accounts, threshold_staking, taco_application, child_application, chain): + creator, staking_provider, *everyone_else = accounts[0:] + min_authorization = MIN_AUTHORIZATION + + # This method only for penalized staking providers + with ape.reverts("There are no any penalties"): + taco_application.resetReward(staking_provider, sender=creator) + + # Penalize staking provider + child_application.penalize(staking_provider, sender=staking_provider) + + # Not enough time passed + with ape.reverts("Penalty is still ongoing"): + taco_application.resetReward(staking_provider, sender=creator) + + chain.pending_timestamp += PENALTY_DURATION + tx = taco_application.resetReward(staking_provider, sender=creator) + assert taco_application.getPenalty(staking_provider) == [0, 0] + assert taco_application.authorizedOverall() == 0 + assert tx.events == [taco_application.RewardReset(stakingProvider=staking_provider)] + + # Increase authorization with no confirmation and reset reward + threshold_staking.authorizationIncreased(staking_provider, 0, min_authorization, sender=creator) + child_application.penalize(staking_provider, sender=staking_provider) + chain.pending_timestamp += PENALTY_DURATION + tx = taco_application.resetReward(staking_provider, sender=creator) + assert taco_application.getPenalty(staking_provider) == [0, 0] + assert taco_application.authorizedOverall() == 0 + assert tx.events == [taco_application.RewardReset(stakingProvider=staking_provider)] + + # Increase authorization with confirmation and reset reward + taco_application.bondOperator(staking_provider, staking_provider, sender=staking_provider) + child_application.confirmOperatorAddress(staking_provider, sender=staking_provider) + child_application.penalize(staking_provider, sender=staking_provider) + chain.pending_timestamp += PENALTY_DURATION + assert taco_application.authorizedOverall() == min_authorization * 9 / 10 + tx = taco_application.resetReward(staking_provider, sender=creator) + assert taco_application.getPenalty(staking_provider) == [0, 0] + assert taco_application.authorizedOverall() == min_authorization + assert tx.events == [taco_application.RewardReset(stakingProvider=staking_provider)] diff --git a/tests/application/test_reward.py b/tests/application/test_reward.py index 69c3630a..1dc1b370 100644 --- a/tests/application/test_reward.py +++ b/tests/application/test_reward.py @@ -28,6 +28,7 @@ DEAUTHORIZATION_DURATION = 60 * 60 * 24 * 60 # 60 days in seconds FLOATING_POINT_DIVISOR = 10**21 REWARD_PORTION = MIN_AUTHORIZATION * 10**3 +PENALTY_DURATION = 60 * 60 * 24 # 1 day in seconds def test_push_reward( @@ -248,6 +249,15 @@ def check_reward_with_confirmation(): taco_application.resynchronizeAuthorization(staking_provider_2, sender=creator) check_reward_no_confirmation() + # Penalize staking provider, no confirmed operator + child_application.penalize(staking_provider_2, sender=creator) + check_reward_no_confirmation() + + # Reset reward after penalty, no confirmed operator + chain.pending_timestamp += PENALTY_DURATION + taco_application.resetReward(staking_provider_2, sender=creator) + check_reward_no_confirmation() + # Wait and confirm operator taco_application.pushReward(reward_portion, sender=distributor) chain.pending_timestamp += reward_duration // 2 @@ -289,6 +299,15 @@ def check_reward_with_confirmation(): taco_application.resynchronizeAuthorization(staking_provider_2, sender=creator) check_reward_with_confirmation() + # Penalize staking provider with confirmation + child_application.penalize(staking_provider_2, sender=creator) + check_reward_with_confirmation() + + # Reset reward after penalty, with confirmation + chain.pending_timestamp += PENALTY_DURATION + taco_application.resetReward(staking_provider_2, sender=creator) + check_reward_with_confirmation() + # Bond operator with confirmation (confirmation will be dropped) taco_application.pushReward(reward_portion, sender=distributor) chain.pending_timestamp += min_operator_seconds