Skip to content

Commit

Permalink
TACoApplication: reset reward after penalty as soon as possible, prop…
Browse files Browse the repository at this point in the history
…er calculation authorizedOverall
  • Loading branch information
vzotova committed Apr 14, 2024
1 parent c47b4c6 commit c50a53a
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 16 deletions.
70 changes: 54 additions & 16 deletions contracts/contracts/TACoApplication.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1030,22 +1061,29 @@ 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);
}

/**
* @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);
}
}
4 changes: 4 additions & 0 deletions contracts/test/TACoApplicationTestSet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,8 @@ contract ChildApplicationForTACoApplicationMock {
function confirmOperatorAddress(address _operator) external {
rootApplication.confirmOperatorAddress(_operator);
}

function penalize(address _stakingProvider) external {
rootApplication.penalize(_stakingProvider);
}
}
2 changes: 2 additions & 0 deletions tests/application/test_authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
123 changes: 123 additions & 0 deletions tests/application/test_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)]
19 changes: 19 additions & 0 deletions tests/application/test_reward.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit c50a53a

Please sign in to comment.