diff --git a/ape-config.yaml b/ape-config.yaml index d0ddbfd9..63ff262e 100644 --- a/ape-config.yaml +++ b/ape-config.yaml @@ -62,6 +62,8 @@ deployments: pre_percentage_penalty_coefficient: 100000 reward_duration: 604800 # one week in seconds deauthorization_duration: 5184000 # 60 days in seconds + commitment_duration_1: 15552000 # 180 days in seconds + commitment_duration_2: 31104000 # 360 days in seconds verify: False rinkeby: - nu_token: "0x78D591D90a4a768B9D2790deA465D472b6Fe0f18" diff --git a/contracts/contracts/TACoApplication.sol b/contracts/contracts/TACoApplication.sol index c1805f1f..2c600ca9 100644 --- a/contracts/contracts/TACoApplication.sol +++ b/contracts/contracts/TACoApplication.sol @@ -128,6 +128,13 @@ contract TACoApplication is IApplication, ITACoChildToRoot, OwnableUpgradeable { uint256 startTimestamp ); + /** + * @notice Signals that a staking provider made a commitment + * @param stakingProvider Staking provider address + * @param endCommitment End of commitment + */ + event CommitmentMade(address indexed stakingProvider, uint256 endCommitment); + struct StakingProviderInfo { address operator; bool operatorConfirmed; @@ -137,6 +144,7 @@ contract TACoApplication is IApplication, ITACoChildToRoot, OwnableUpgradeable { uint64 endDeauthorization; uint96 tReward; uint96 rewardPerTokenPaid; + uint64 endCommitment; } uint96 public immutable minimumAuthorization; @@ -144,6 +152,11 @@ contract TACoApplication is IApplication, ITACoChildToRoot, OwnableUpgradeable { uint256 public immutable rewardDuration; uint256 public immutable deauthorizationDuration; + uint64 public immutable commitmentDurationOption1; + uint64 public immutable commitmentDurationOption2; + uint64 public immutable commitmentDurationOption3; + uint64 public immutable commitmentDurationOption4; + IStaking public immutable tStaking; IERC20 public immutable token; @@ -169,6 +182,7 @@ contract TACoApplication is IApplication, ITACoChildToRoot, OwnableUpgradeable { * @param _minOperatorSeconds Min amount of seconds while an operator can't be changed * @param _rewardDuration Duration of one reward cycle in seconds * @param _deauthorizationDuration Duration of decreasing authorization in seconds + * @param _commitmentDurationOptions Options for commitment duration */ constructor( IERC20 _token, @@ -176,12 +190,15 @@ contract TACoApplication is IApplication, ITACoChildToRoot, OwnableUpgradeable { uint96 _minimumAuthorization, uint256 _minOperatorSeconds, uint256 _rewardDuration, - uint256 _deauthorizationDuration + uint256 _deauthorizationDuration, + uint64[] memory _commitmentDurationOptions ) { require( _rewardDuration != 0 && _tStaking.authorizedStake(address(this), address(this)) == 0 && - _token.totalSupply() > 0, + _token.totalSupply() > 0 && + _commitmentDurationOptions.length >= 1 && + _commitmentDurationOptions.length <= 4, "Wrong input parameters" ); rewardDuration = _rewardDuration; @@ -190,6 +207,16 @@ contract TACoApplication is IApplication, ITACoChildToRoot, OwnableUpgradeable { token = _token; tStaking = _tStaking; minOperatorSeconds = _minOperatorSeconds; + commitmentDurationOption1 = _commitmentDurationOptions[0]; + commitmentDurationOption2 = _commitmentDurationOptions.length >= 2 + ? _commitmentDurationOptions[1] + : 0; + commitmentDurationOption3 = _commitmentDurationOptions.length >= 3 + ? _commitmentDurationOptions[2] + : 0; + commitmentDurationOption4 = _commitmentDurationOptions.length >= 4 + ? _commitmentDurationOptions[3] + : 0; _disableInitializers(); } @@ -440,6 +467,10 @@ contract TACoApplication is IApplication, ITACoChildToRoot, OwnableUpgradeable { _toAmount == 0 || _toAmount >= minimumAuthorization, "Resulting authorization will be less than minimum" ); + require( + info.endCommitment <= block.timestamp, + "Can't request deauthorization before end of commitment" + ); if (info.operatorConfirmed) { resynchronizeAuthorizedOverall(info, _fromAmount); } @@ -475,6 +506,7 @@ contract TACoApplication is IApplication, ITACoChildToRoot, OwnableUpgradeable { info.authorized = toAmount; info.deauthorizing = 0; info.endDeauthorization = 0; + info.endCommitment = 0; if (info.authorized == 0) { _stakingProviderFromOperator[info.operator] = address(0); @@ -513,6 +545,30 @@ contract TACoApplication is IApplication, ITACoChildToRoot, OwnableUpgradeable { _updateAuthorization(_stakingProvider, info); } + /** + * @notice Make a commitment to not request authorization decrease for specified duration + * @param _stakingProvider Staking provider address + * @param _commitmentDuration Duration of commitment + */ + function makeCommitment( + address _stakingProvider, + uint64 _commitmentDuration + ) external onlyOwnerOrStakingProvider(_stakingProvider) { + require( + _commitmentDuration > 0 && + (_commitmentDuration == commitmentDurationOption1 || + _commitmentDuration == commitmentDurationOption2 || + _commitmentDuration == commitmentDurationOption3 || + _commitmentDuration == commitmentDurationOption4), + "Commitment duration must be equal to one of options" + ); + StakingProviderInfo storage info = stakingProviderInfo[_stakingProvider]; + require(info.endDeauthorization == 0, "Commitment can't be made during deauthorization"); + require(info.endCommitment == 0, "Commitment already made"); + info.endCommitment = uint64(block.timestamp) + _commitmentDuration; + emit CommitmentMade(_stakingProvider, info.endCommitment); + } + //-------------------------Main------------------------- /** * @notice Returns staking provider for specified operator diff --git a/scripts/deploy_taco_application.py b/scripts/deploy_taco_application.py index b21d911d..2e88816b 100644 --- a/scripts/deploy_taco_application.py +++ b/scripts/deploy_taco_application.py @@ -27,6 +27,10 @@ def main(account_id=None): deployments_config.get("pre_min_operator_seconds"), deployments_config.get("reward_duration"), deployments_config.get("deauthorization_duration"), + [ + deployments_config.get("commitment_duration_1"), + deployments_config.get("commitment_duration_2"), + ], sender=deployer, publish=deployments_config.get("verify"), ) diff --git a/tests/application/conftest.py b/tests/application/conftest.py index e2fee3c1..4e8e6d6b 100644 --- a/tests/application/conftest.py +++ b/tests/application/conftest.py @@ -28,6 +28,9 @@ REWARD_DURATION = 60 * 60 * 24 * 7 # one week in seconds DEAUTHORIZATION_DURATION = 60 * 60 * 24 * 60 # 60 days in seconds TOTAL_SUPPLY = Web3.to_wei(1_000_000_000, "ether") # TODO NU(1_000_000_000, 'NU').to_units() +COMMITMENT_DURATION_1 = 182 * 60 * 24 * 60 # 182 days in seconds +COMMITMENT_DURATION_2 = 2 * COMMITMENT_DURATION_1 # 365 days in seconds +COMMITMENT_DURATION_3 = 3 * COMMITMENT_DURATION_1 # 365 days in seconds DEPENDENCY = project.dependencies["openzeppelin"]["4.9.1"] @@ -75,6 +78,7 @@ def taco_application(project, creator, token, threshold_staking): MIN_OPERATOR_SECONDS, REWARD_DURATION, DEAUTHORIZATION_DURATION, + [COMMITMENT_DURATION_1, COMMITMENT_DURATION_2, COMMITMENT_DURATION_3], ) proxy_admin = DEPENDENCY.ProxyAdmin.deploy(sender=creator) diff --git a/tests/application/test_authorization.py b/tests/application/test_authorization.py index 26e5ef4f..359279f8 100644 --- a/tests/application/test_authorization.py +++ b/tests/application/test_authorization.py @@ -23,6 +23,7 @@ AUTHORIZATION_SLOT = 3 DEAUTHORIZING_SLOT = 4 END_DEAUTHORIZATION_SLOT = 5 +END_COMMITMENT_SLOT = 8 MIN_AUTHORIZATION = Web3.to_wei(40_000, "ether") DEAUTHORIZATION_DURATION = 60 * 60 * 24 * 60 # 60 days in seconds @@ -403,10 +404,29 @@ def test_authorization_decrease_request( # Request decrease without syncing with child app taco_application.setChildApplication(ZERO_ADDRESS, sender=creator) threshold_staking.authorizationDecreaseRequested( - staking_provider, value // 2, value // 2, sender=creator + staking_provider, value // 2, 0, sender=creator ) assert child_application.authorizedStake(staking_provider) == 0 + # Try to request decrease before ending of commitment + chain.pending_timestamp += deauthorization_duration + taco_application.finishAuthorizationDecrease(staking_provider, sender=creator) + threshold_staking.authorizationIncreased(staking_provider, 0, value, sender=creator) + commitment_duration = taco_application.commitmentDurationOption1() + taco_application.makeCommitment(staking_provider, commitment_duration, sender=staking_provider) + + # Commitment is still active + with ape.reverts("Can't request deauthorization before end of commitment"): + threshold_staking.authorizationDecreaseRequested( + staking_provider, value, value // 2, sender=creator + ) + chain.pending_timestamp += commitment_duration + + # Now decrease can be requested + threshold_staking.authorizationDecreaseRequested( + staking_provider, value, value // 2, sender=creator + ) + def test_finish_authorization_decrease( accounts, threshold_staking, taco_application, child_application, chain @@ -663,3 +683,94 @@ def test_resync(accounts, threshold_staking, taco_application, child_application assert child_application.authorizedStake(staking_provider) == value assert taco_application.getOperatorFromStakingProvider(staking_provider) == ZERO_ADDRESS assert child_application.operatorFromStakingProvider(staking_provider) == staking_provider + + +def test_commitment(accounts, threshold_staking, taco_application, chain): + """ + Tests for authorization method: makeCommitment + """ + + creator, staking_provider, another_staking_provider = accounts[0:3] + deauthorization_duration = DEAUTHORIZATION_DURATION + minimum_authorization = MIN_AUTHORIZATION + value = 2 * minimum_authorization + commitment_duration_1 = taco_application.commitmentDurationOption1() + commitment_duration_2 = taco_application.commitmentDurationOption2() + commitment_duration_3 = taco_application.commitmentDurationOption3() + + # Commitment can be made only for authorized staking provider + with ape.reverts("Not owner or provider"): + taco_application.makeCommitment( + staking_provider, commitment_duration_1, sender=staking_provider + ) + + # Prepare staking provider + threshold_staking.authorizationIncreased(staking_provider, 0, value, sender=creator) + + # Begin deauthorization + threshold_staking.authorizationDecreaseRequested( + staking_provider, value, minimum_authorization, sender=creator + ) + + # Commitment can't be made during deauthorization + with ape.reverts("Commitment can't be made during deauthorization"): + taco_application.makeCommitment( + staking_provider, commitment_duration_3, sender=staking_provider + ) + + # Finish deauthorization + chain.pending_timestamp += deauthorization_duration + taco_application.finishAuthorizationDecrease(staking_provider, sender=creator) + + # Commitment can be made only by staking provider + with ape.reverts("Not owner or provider"): + taco_application.makeCommitment( + staking_provider, commitment_duration_3, sender=another_staking_provider + ) + + # Commitment duration must be equal to one of options + with ape.reverts("Commitment duration must be equal to one of options"): + taco_application.makeCommitment(staking_provider, 0, sender=staking_provider) + with ape.reverts("Commitment duration must be equal to one of options"): + taco_application.makeCommitment( + staking_provider, commitment_duration_1 + 1, sender=staking_provider + ) + + # And make a commitment for shorter duration + tx = taco_application.makeCommitment( + staking_provider, commitment_duration_1, sender=staking_provider + ) + timestamp = chain.pending_timestamp - 1 + end_commitment = timestamp + commitment_duration_1 + assert ( + taco_application.stakingProviderInfo(staking_provider)[END_COMMITMENT_SLOT] + == end_commitment + ) + assert tx.events == [ + taco_application.CommitmentMade( + stakingProvider=staking_provider, endCommitment=end_commitment + ) + ] + + # Commitment can't be made twice + with ape.reverts("Commitment already made"): + taco_application.makeCommitment( + staking_provider, commitment_duration_2, sender=staking_provider + ) + + # Another staking provider makes a commitment for longer period of time + threshold_staking.authorizationIncreased(another_staking_provider, 0, value, sender=creator) + tx = taco_application.makeCommitment( + another_staking_provider, commitment_duration_3, sender=another_staking_provider + ) + timestamp = chain.pending_timestamp - 1 + end_commitment = timestamp + commitment_duration_3 + assert ( + taco_application.stakingProviderInfo(another_staking_provider)[END_COMMITMENT_SLOT] + == end_commitment + ) + assert tx.events == [ + taco_application.CommitmentMade( + stakingProvider=another_staking_provider, endCommitment=end_commitment + ) + ]