From e8a7a9f6ef5e561dbdaacf64fc2897329d4d3e18 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 14 Aug 2024 02:11:51 +0400 Subject: [PATCH 01/42] Prevent executed proposals from changing state after cancelAll is called --- contracts/libraries/ExecutableProposals.sol | 2 +- test/unit/libraries/ExecutableProposals.t.sol | 48 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/contracts/libraries/ExecutableProposals.sol b/contracts/libraries/ExecutableProposals.sol index bd65908e..5b868ca9 100644 --- a/contracts/libraries/ExecutableProposals.sol +++ b/contracts/libraries/ExecutableProposals.sol @@ -217,6 +217,6 @@ library ExecutableProposals { uint256 proposalId, ProposalData memory proposalData ) private view returns (bool) { - return proposalId <= self.lastCancelledProposalId || proposalData.status == Status.Cancelled; + return proposalId <= self.lastCancelledProposalId && proposalData.status != Status.Executed; } } diff --git a/test/unit/libraries/ExecutableProposals.t.sol b/test/unit/libraries/ExecutableProposals.t.sol index eb4be119..6f205777 100644 --- a/test/unit/libraries/ExecutableProposals.t.sol +++ b/test/unit/libraries/ExecutableProposals.t.sol @@ -401,6 +401,54 @@ contract ExecutableProposalsUnitTests is UnitTest { assert(!_proposals.canExecute(proposalId, Durations.ZERO)); } + function test_cancelAll_DoesNotModifyStateOfExecutedProposals() external { + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + assertEq(_proposals.getProposalsCount(), 1); + uint256 executedProposalId = 1; + _proposals.schedule(executedProposalId, Durations.ZERO); + _proposals.execute(executedProposalId, Durations.ZERO); + + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + assertEq(_proposals.getProposalsCount(), 2); + uint256 scheduledProposalId = 2; + _proposals.schedule(scheduledProposalId, Durations.ZERO); + + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + assertEq(_proposals.getProposalsCount(), 3); + uint256 submittedProposalId = 3; + + // Validate the state of the proposals is correct before proceeding with cancellation. + + (ProposalStatus executedProposalStatus, /* executor */, /* submittedAt */, /* scheduledAt */ ) = + _proposals.getProposalInfo(executedProposalId); + assertEq(executedProposalStatus, ProposalStatus.Executed); + + (ProposalStatus scheduledProposalStatus, /* executor */, /* submittedAt */, /* scheduledAt */ ) = + _proposals.getProposalInfo(scheduledProposalId); + assertEq(scheduledProposalStatus, ProposalStatus.Scheduled); + + (ProposalStatus submittedProposalStatus, /* executor */, /* submittedAt */, /* scheduledAt */ ) = + _proposals.getProposalInfo(submittedProposalId); + assertEq(submittedProposalStatus, ProposalStatus.Submitted); + + // After canceling the proposals, both submitted and scheduled proposals should transition to the Cancelled state. + // However, executed proposals should remain in the Executed state. + + _proposals.cancelAll(); + + (executedProposalStatus, /* executor */, /* submittedAt */, /* scheduledAt */ ) = + _proposals.getProposalInfo(executedProposalId); + assertEq(executedProposalStatus, ProposalStatus.Executed); + + (scheduledProposalStatus, /* executor */, /* submittedAt */, /* scheduledAt */ ) = + _proposals.getProposalInfo(scheduledProposalId); + assertEq(scheduledProposalStatus, ProposalStatus.Cancelled); + + (submittedProposalStatus, /* executor */, /* submittedAt */, /* scheduledAt */ ) = + _proposals.getProposalInfo(submittedProposalId); + assertEq(submittedProposalStatus, ProposalStatus.Cancelled); + } + function test_can_schedule_proposal() external { Duration delay = Durations.from(100 seconds); _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); From 25014bce766b11c4e1d2b8597e4425a4de499c7e Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 14 Aug 2024 03:14:15 +0400 Subject: [PATCH 02/42] Fix the order of DG.activateNextState() calls in Escrow lock/unlock methods --- contracts/Escrow.sol | 18 ++++++++-------- test/scenario/escrow.t.sol | 44 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index e604017c..7f4854b8 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -131,8 +131,8 @@ contract Escrow is IEscrow { // --- function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares) { - _escrowState.checkSignallingEscrow(); DUAL_GOVERNANCE.activateNextState(); + _escrowState.checkSignallingEscrow(); lockedStETHShares = ST_ETH.getSharesByPooledEth(amount); _accounting.accountStETHSharesLock(msg.sender, SharesValues.from(lockedStETHShares)); @@ -142,10 +142,10 @@ contract Escrow is IEscrow { } function unlockStETH() external returns (uint256 unlockedStETHShares) { - _escrowState.checkSignallingEscrow(); - DUAL_GOVERNANCE.activateNextState(); + _escrowState.checkSignallingEscrow(); _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); + unlockedStETHShares = _accounting.accountStETHSharesUnlock(msg.sender).toUint256(); ST_ETH.transferShares(msg.sender, unlockedStETHShares); @@ -157,8 +157,8 @@ contract Escrow is IEscrow { // --- function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares) { - _escrowState.checkSignallingEscrow(); DUAL_GOVERNANCE.activateNextState(); + _escrowState.checkSignallingEscrow(); WST_ETH.transferFrom(msg.sender, address(this), amount); lockedStETHShares = ST_ETH.getSharesByPooledEth(WST_ETH.unwrap(amount)); @@ -168,10 +168,10 @@ contract Escrow is IEscrow { } function unlockWstETH() external returns (uint256 unlockedStETHShares) { - _escrowState.checkSignallingEscrow(); DUAL_GOVERNANCE.activateNextState(); - + _escrowState.checkSignallingEscrow(); _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); + SharesValue wstETHUnlocked = _accounting.accountStETHSharesUnlock(msg.sender); unlockedStETHShares = WST_ETH.wrap(ST_ETH.getPooledEthByShares(wstETHUnlocked.toUint256())); WST_ETH.transfer(msg.sender, unlockedStETHShares); @@ -183,8 +183,8 @@ contract Escrow is IEscrow { // Lock & unlock unstETH // --- function lockUnstETH(uint256[] memory unstETHIds) external { - _escrowState.checkSignallingEscrow(); DUAL_GOVERNANCE.activateNextState(); + _escrowState.checkSignallingEscrow(); WithdrawalRequestStatus[] memory statuses = WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); _accounting.accountUnstETHLock(msg.sender, unstETHIds, statuses); @@ -197,10 +197,10 @@ contract Escrow is IEscrow { } function unlockUnstETH(uint256[] memory unstETHIds) external { - _escrowState.checkSignallingEscrow(); DUAL_GOVERNANCE.activateNextState(); - + _escrowState.checkSignallingEscrow(); _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); + _accounting.accountUnstETHUnlock(msg.sender, unstETHIds); uint256 unstETHIdsCount = unstETHIds.length; for (uint256 i = 0; i < unstETHIdsCount; ++i) { diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index badee1ef..47eb60d2 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -571,6 +571,50 @@ contract EscrowHappyPath is ScenarioTestBlueprint { this.externalUnlockUnstETH(_VETOER_1, lockedWithdrawalNfts); } + // TODO: rewrite this test to use all stETH/wstETH/unstETH tokens + function testFork_EdgeCase_FrontRunningRageQuitWithTokensUnlockForbidden() external { + // lock enough funds to initiate RageQuit + _lockStETH(_VETOER_1, PercentsD16.fromBasisPoints(7_99)); + _lockStETH(_VETOER_2, PercentsD16.fromBasisPoints(7_99)); + _assertVetoSignalingState(); + + // wait till the last second of the dynamic timelock duration + _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MAX_DURATION()); + _activateNextState(); + _assertVetoSignalingState(); + + ( /* bool isActive */ , uint256 duration, uint256 activatedAt, /* uint256 enteredAt */ ) = + _getVetoSignallingState(); + assertEq(duration + activatedAt, block.timestamp); + + // validate that while the VetoSignalling has not passed, vetoer can unlock funds from Escrow + uint256 snapshotId = vm.snapshot(); + _unlockStETH(_VETOER_1); + _assertVetoSignalingDeactivationState(); + + // Rollback the state of the node before vetoer unlocked his funds + vm.revertTo(snapshotId); + + // validate that the DualGovernance still in the VetoSignalling state + _activateNextState(); + _assertVetoSignalingState(); + + // wait 1 block duration. Full VetoSignalling duration has passed and RageQuit may be started now + _wait(Durations.from(12 seconds)); + + // validate that RageQuit will start when the activateNextState() is called + snapshotId = vm.snapshot(); + _activateNextState(); + _assertRageQuitState(); + + // Rollback the state of the node as it was before RageQuit activation + vm.revertTo(snapshotId); + + // The attempt to unlock funds from Escrow will fail + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); + this.externalUnlockStETH(_VETOER_1); + } + // --- // Helper external methods to test reverts // --- From 1a17265298e1a8f4a47ebd34ab5bc243dce2e883 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Wed, 14 Aug 2024 12:54:55 +0300 Subject: [PATCH 03/42] Update quorumAt after execution quorum changing --- .../EmergencyActivationCommittee.sol | 5 +- .../EmergencyExecutionCommittee.sol | 10 +- contracts/committees/HashConsensus.sol | 22 +++- contracts/committees/ResealCommittee.sol | 5 +- contracts/committees/TiebreakerCore.sol | 10 +- .../committees/TiebreakerSubCommittee.sol | 10 +- test/scenario/emergency-committee.t.sol | 12 +- test/scenario/reseal-committee.t.sol | 4 +- test/scenario/tiebreaker.t.sol | 24 ++-- test/unit/HashConsensus.t.sol | 113 ++++++++++++++++-- 10 files changed, 164 insertions(+), 51 deletions(-) diff --git a/contracts/committees/EmergencyActivationCommittee.sol b/contracts/committees/EmergencyActivationCommittee.sol index 13f1a0bd..f4a96925 100644 --- a/contracts/committees/EmergencyActivationCommittee.sol +++ b/contracts/committees/EmergencyActivationCommittee.sol @@ -34,12 +34,13 @@ contract EmergencyActivationCommittee is HashConsensus { /// @notice Gets the current state of the emergency activation vote /// @return support The number of votes in support of the activation - /// @return execuitionQuorum The required number of votes for execution + /// @return executionQuorum The required number of votes for execution + /// @return quorumAt The timestamp when the quorum was reached /// @return isExecuted Whether the activation has been executed function getActivateEmergencyModeState() public view - returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) + returns (uint256 support, uint256 executionQuorum, uint256 quorumAt, bool isExecuted) { return _getHashState(EMERGENCY_ACTIVATION_HASH); } diff --git a/contracts/committees/EmergencyExecutionCommittee.sol b/contracts/committees/EmergencyExecutionCommittee.sol index c6c0e704..3bc64830 100644 --- a/contracts/committees/EmergencyExecutionCommittee.sol +++ b/contracts/committees/EmergencyExecutionCommittee.sol @@ -46,12 +46,13 @@ contract EmergencyExecutionCommittee is HashConsensus, ProposalsList { /// @notice Gets the current state of an emergency execution proposal /// @param proposalId The ID of the proposal /// @return support The number of votes in support of the proposal - /// @return execuitionQuorum The required number of votes for execution + /// @return executionQuorum The required number of votes for execution + /// @return quorumAt The timestamp when the quorum was reached /// @return isExecuted Whether the proposal has been executed function getEmergencyExecuteState(uint256 proposalId) public view - returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) + returns (uint256 support, uint256 executionQuorum, uint256 quorumAt, bool isExecuted) { (, bytes32 key) = _encodeEmergencyExecute(proposalId); return _getHashState(key); @@ -95,12 +96,13 @@ contract EmergencyExecutionCommittee is HashConsensus, ProposalsList { /// @notice Gets the current state of an emergency reset opprosal /// @return support The number of votes in support of the proposal - /// @return execuitionQuorum The required number of votes for execution + /// @return executionQuorum The required number of votes for execution + /// @return quorumAt The timestamp when the quorum was reached /// @return isExecuted Whether the proposal has been executed function getEmergencyResetState() public view - returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) + returns (uint256 support, uint256 executionQuorum, uint256 quorumAt, bool isExecuted) { bytes32 proposalKey = _encodeEmergencyResetProposalKey(); return _getHashState(proposalKey); diff --git a/contracts/committees/HashConsensus.sol b/contracts/committees/HashConsensus.sol index 31825a1b..fcdeab55 100644 --- a/contracts/committees/HashConsensus.sol +++ b/contracts/committees/HashConsensus.sol @@ -90,15 +90,17 @@ abstract contract HashConsensus is Ownable { /// @dev Internal function to retrieve the state of a hash /// @param hash The hash to get the state for /// @return support The number of votes in support of the hash - /// @return execuitionQuorum The required number of votes for execution + /// @return executionQuorum The required number of votes for execution + /// @return quorumAt The timestamp when the quorum was reached /// @return isUsed Whether the hash has been used function _getHashState(bytes32 hash) internal view - returns (uint256 support, uint256 execuitionQuorum, bool isUsed) + returns (uint256 support, uint256 executionQuorum, uint256 quorumAt, bool isUsed) { support = _getSupport(hash); - execuitionQuorum = quorum; + executionQuorum = quorum; + quorumAt = _hashStates[hash].quorumAt; isUsed = _hashStates[hash].usedAt > 0; } @@ -167,6 +169,20 @@ abstract contract HashConsensus is Ownable { _setQuorum(newQuorum); } + /// @notice Updates the quorum for a given hash if the quorum is reached and not set + /// and the hash has not been used + /// @param hash The hash to update the quorum for + function updateQuorum(bytes32 hash) public { + if (_hashStates[hash].usedAt > 0) { + revert HashAlreadyUsed(hash); + } + + uint256 support = _getSupport(hash); + if (support >= quorum && _hashStates[hash].quorumAt == 0) { + _hashStates[hash].quorumAt = uint40(block.timestamp); + } + } + /// @notice Sets the execution quorum required for certain operations. /// @dev The quorum value must be greater than zero and not exceed the current number of members. /// @param executionQuorum The new quorum value to be set. diff --git a/contracts/committees/ResealCommittee.sol b/contracts/committees/ResealCommittee.sol index b2050ea8..580d473c 100644 --- a/contracts/committees/ResealCommittee.sol +++ b/contracts/committees/ResealCommittee.sol @@ -42,12 +42,13 @@ contract ResealCommittee is HashConsensus, ProposalsList { /// @dev Retrieves the state of the reseal proposal for a sealed address /// @param sealable The addresses for the reseal proposal /// @return support The number of votes in support of the proposal - /// @return execuitionQuorum The required number of votes for execution + /// @return executionQuorum The required number of votes for execution + /// @return quorumAt The timestamp when the quorum was reached /// @return isExecuted Whether the proposal has been executed function getResealState(address sealable) public view - returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) + returns (uint256 support, uint256 executionQuorum, uint256 quorumAt, bool isExecuted) { (, bytes32 key) = _encodeResealProposal(sealable); return _getHashState(key); diff --git a/contracts/committees/TiebreakerCore.sol b/contracts/committees/TiebreakerCore.sol index b82380f7..ce5e80bc 100644 --- a/contracts/committees/TiebreakerCore.sol +++ b/contracts/committees/TiebreakerCore.sol @@ -45,12 +45,13 @@ contract TiebreakerCore is ITiebreakerCore, HashConsensus, ProposalsList { /// @dev Retrieves the state of the schedule proposal for a given proposal ID /// @param proposalId The ID of the proposal /// @return support The number of votes in support of the proposal - /// @return execuitionQuorum The required number of votes for execution + /// @return executionQuorum The required number of votes for execution + /// @return quorumAt The timestamp when the quorum was reached /// @return isExecuted Whether the proposal has been executed function getScheduleProposalState(uint256 proposalId) public view - returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) + returns (uint256 support, uint256 executionQuorum, uint256 quorumAt, bool isExecuted) { (, bytes32 key) = _encodeScheduleProposal(proposalId); return _getHashState(key); @@ -104,12 +105,13 @@ contract TiebreakerCore is ITiebreakerCore, HashConsensus, ProposalsList { /// @param sealable The address to resume /// @param nonce The nonce for the resume proposal /// @return support The number of votes in support of the proposal - /// @return execuitionQuorum The required number of votes for execution + /// @return executionQuorum The required number of votes for execution + /// @return quorumAt The timestamp when the quorum was reached /// @return isExecuted Whether the proposal has been executed function getSealableResumeState( address sealable, uint256 nonce - ) public view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { + ) public view returns (uint256 support, uint256 executionQuorum, uint256 quorumAt, bool isExecuted) { (, bytes32 key) = _encodeSealableResume(sealable, nonce); return _getHashState(key); } diff --git a/contracts/committees/TiebreakerSubCommittee.sol b/contracts/committees/TiebreakerSubCommittee.sol index 810fdff9..e986860c 100644 --- a/contracts/committees/TiebreakerSubCommittee.sol +++ b/contracts/committees/TiebreakerSubCommittee.sol @@ -47,12 +47,13 @@ contract TiebreakerSubCommittee is HashConsensus, ProposalsList { /// @dev Retrieves the state of the schedule proposal for a given proposal ID /// @param proposalId The ID of the proposal /// @return support The number of votes in support of the proposal - /// @return execuitionQuorum The required number of votes for execution + /// @return executionQuorum The required number of votes for execution + /// @return quorumAt The number of votes required to reach quorum /// @return isExecuted Whether the proposal has been executed function getScheduleProposalState(uint256 proposalId) public view - returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) + returns (uint256 support, uint256 executionQuorum, uint256 quorumAt, bool isExecuted) { (, bytes32 key) = _encodeAproveProposal(proposalId); return _getHashState(key); @@ -96,12 +97,13 @@ contract TiebreakerSubCommittee is HashConsensus, ProposalsList { /// @dev Retrieves the state of the resume sealable proposal for a given address /// @param sealable The address to resume /// @return support The number of votes in support of the proposal - /// @return execuitionQuorum The required number of votes for execution + /// @return executionQuorum The required number of votes for execution + /// @return quorumAt The timestamp when the quorum was reached /// @return isExecuted Whether the proposal has been executed function getSealableResumeState(address sealable) public view - returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) + returns (uint256 support, uint256 executionQuorum, uint256 quorumAt, bool isExecuted) { (, bytes32 key,) = _encodeSealableResume(sealable); return _getHashState(key); diff --git a/test/scenario/emergency-committee.t.sol b/test/scenario/emergency-committee.t.sol index c1962a37..b5d3b438 100644 --- a/test/scenario/emergency-committee.t.sol +++ b/test/scenario/emergency-committee.t.sol @@ -39,19 +39,19 @@ contract EmergencyCommitteeTest is ScenarioTestBlueprint { for (uint256 i = 0; i < _emergencyActivationCommittee.quorum() - 1; i++) { vm.prank(members[i]); _emergencyActivationCommittee.approveActivateEmergencyMode(); - (support, quorum, isExecuted) = _emergencyActivationCommittee.getActivateEmergencyModeState(); + (support, quorum,, isExecuted) = _emergencyActivationCommittee.getActivateEmergencyModeState(); assert(support < quorum); assert(isExecuted == false); } vm.prank(members[members.length - 1]); _emergencyActivationCommittee.approveActivateEmergencyMode(); - (support, quorum, isExecuted) = _emergencyActivationCommittee.getActivateEmergencyModeState(); + (support, quorum,, isExecuted) = _emergencyActivationCommittee.getActivateEmergencyModeState(); assert(support == quorum); assert(isExecuted == false); _emergencyActivationCommittee.executeActivateEmergencyMode(); - (support, quorum, isExecuted) = _emergencyActivationCommittee.getActivateEmergencyModeState(); + (support, quorum,, isExecuted) = _emergencyActivationCommittee.getActivateEmergencyModeState(); assert(isExecuted == true); // Emergency Execute @@ -59,19 +59,19 @@ contract EmergencyCommitteeTest is ScenarioTestBlueprint { for (uint256 i = 0; i < _emergencyExecutionCommittee.quorum() - 1; i++) { vm.prank(members[i]); _emergencyExecutionCommittee.voteEmergencyExecute(proposalIdToExecute, true); - (support, quorum, isExecuted) = _emergencyExecutionCommittee.getEmergencyExecuteState(proposalIdToExecute); + (support, quorum,, isExecuted) = _emergencyExecutionCommittee.getEmergencyExecuteState(proposalIdToExecute); assert(support < quorum); assert(isExecuted == false); } vm.prank(members[members.length - 1]); _emergencyExecutionCommittee.voteEmergencyExecute(proposalIdToExecute, true); - (support, quorum, isExecuted) = _emergencyExecutionCommittee.getEmergencyExecuteState(proposalIdToExecute); + (support, quorum,, isExecuted) = _emergencyExecutionCommittee.getEmergencyExecuteState(proposalIdToExecute); assert(support == quorum); assert(isExecuted == false); _emergencyExecutionCommittee.executeEmergencyExecute(proposalIdToExecute); - (support, quorum, isExecuted) = _emergencyExecutionCommittee.getEmergencyExecuteState(proposalIdToExecute); + (support, quorum,, isExecuted) = _emergencyExecutionCommittee.getEmergencyExecuteState(proposalIdToExecute); assert(isExecuted == true); } } diff --git a/test/scenario/reseal-committee.t.sol b/test/scenario/reseal-committee.t.sol index 15414182..9ceb10ee 100644 --- a/test/scenario/reseal-committee.t.sol +++ b/test/scenario/reseal-committee.t.sol @@ -38,14 +38,14 @@ contract ResealCommitteeTest is ScenarioTestBlueprint { for (uint256 i = 0; i < _resealCommittee.quorum() - 1; i++) { vm.prank(members[i]); _resealCommittee.voteReseal(sealable, true); - (support, quorum, isExecuted) = _resealCommittee.getResealState(sealable); + (support, quorum,, isExecuted) = _resealCommittee.getResealState(sealable); assert(support < quorum); assert(isExecuted == false); } vm.prank(members[members.length - 1]); _resealCommittee.voteReseal(sealable, true); - (support, quorum, isExecuted) = _resealCommittee.getResealState(sealable); + (support, quorum,, isExecuted) = _resealCommittee.getResealState(sealable); assert(support == quorum); assert(isExecuted == false); diff --git a/test/scenario/tiebreaker.t.sol b/test/scenario/tiebreaker.t.sol index 54228184..2f362aa6 100644 --- a/test/scenario/tiebreaker.t.sol +++ b/test/scenario/tiebreaker.t.sol @@ -42,19 +42,19 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { for (uint256 i = 0; i < _tiebreakerSubCommittees[0].quorum() - 1; i++) { vm.prank(members[i]); _tiebreakerSubCommittees[0].scheduleProposal(proposalIdToExecute); - (support, quorum, isExecuted) = _tiebreakerSubCommittees[0].getScheduleProposalState(proposalIdToExecute); + (support, quorum,, isExecuted) = _tiebreakerSubCommittees[0].getScheduleProposalState(proposalIdToExecute); assertTrue(support < quorum); assertFalse(isExecuted); } vm.prank(members[members.length - 1]); _tiebreakerSubCommittees[0].scheduleProposal(proposalIdToExecute); - (support, quorum, isExecuted) = _tiebreakerSubCommittees[0].getScheduleProposalState(proposalIdToExecute); + (support, quorum,, isExecuted) = _tiebreakerSubCommittees[0].getScheduleProposalState(proposalIdToExecute); assertEq(support, quorum); assertFalse(isExecuted); _tiebreakerSubCommittees[0].executeScheduleProposal(proposalIdToExecute); - (support, quorum, isExecuted) = _tiebreakerCoreCommittee.getScheduleProposalState(proposalIdToExecute); + (support, quorum,, isExecuted) = _tiebreakerCoreCommittee.getScheduleProposalState(proposalIdToExecute); assertTrue(support < quorum); // Tiebreaker subcommittee 1 @@ -62,20 +62,20 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { for (uint256 i = 0; i < _tiebreakerSubCommittees[1].quorum() - 1; i++) { vm.prank(members[i]); _tiebreakerSubCommittees[1].scheduleProposal(proposalIdToExecute); - (support, quorum, isExecuted) = _tiebreakerSubCommittees[1].getScheduleProposalState(proposalIdToExecute); + (support, quorum,, isExecuted) = _tiebreakerSubCommittees[1].getScheduleProposalState(proposalIdToExecute); assertTrue(support < quorum); assertEq(isExecuted, false); } vm.prank(members[members.length - 1]); _tiebreakerSubCommittees[1].scheduleProposal(proposalIdToExecute); - (support, quorum, isExecuted) = _tiebreakerSubCommittees[1].getScheduleProposalState(proposalIdToExecute); + (support, quorum,, isExecuted) = _tiebreakerSubCommittees[1].getScheduleProposalState(proposalIdToExecute); assertEq(support, quorum); assertFalse(isExecuted); // Approve proposal for scheduling _tiebreakerSubCommittees[1].executeScheduleProposal(proposalIdToExecute); - (support, quorum, isExecuted) = _tiebreakerCoreCommittee.getScheduleProposalState(proposalIdToExecute); + (support, quorum,, isExecuted) = _tiebreakerCoreCommittee.getScheduleProposalState(proposalIdToExecute); assertEq(support, quorum); // Waiting for submit delay pass @@ -114,7 +114,7 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { for (uint256 i = 0; i < _tiebreakerSubCommittees[0].quorum() - 1; i++) { vm.prank(members[i]); _tiebreakerSubCommittees[0].sealableResume(address(_lido.withdrawalQueue)); - (support, quorum, isExecuted) = + (support, quorum,, isExecuted) = _tiebreakerSubCommittees[0].getSealableResumeState(address(_lido.withdrawalQueue)); assertTrue(support < quorum); assertFalse(isExecuted); @@ -122,13 +122,13 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { vm.prank(members[members.length - 1]); _tiebreakerSubCommittees[0].sealableResume(address(_lido.withdrawalQueue)); - (support, quorum, isExecuted) = + (support, quorum,, isExecuted) = _tiebreakerSubCommittees[0].getSealableResumeState(address(_lido.withdrawalQueue)); assertEq(support, quorum); assertFalse(isExecuted); _tiebreakerSubCommittees[0].executeSealableResume(address(_lido.withdrawalQueue)); - (support, quorum, isExecuted) = _tiebreakerCoreCommittee.getSealableResumeState( + (support, quorum,, isExecuted) = _tiebreakerCoreCommittee.getSealableResumeState( address(_lido.withdrawalQueue), _tiebreakerCoreCommittee.getSealableResumeNonce(address(_lido.withdrawalQueue)) ); @@ -139,7 +139,7 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { for (uint256 i = 0; i < _tiebreakerSubCommittees[1].quorum() - 1; i++) { vm.prank(members[i]); _tiebreakerSubCommittees[1].sealableResume(address(_lido.withdrawalQueue)); - (support, quorum, isExecuted) = + (support, quorum,, isExecuted) = _tiebreakerSubCommittees[1].getSealableResumeState(address(_lido.withdrawalQueue)); assertTrue(support < quorum); assertEq(isExecuted, false); @@ -147,13 +147,13 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { vm.prank(members[members.length - 1]); _tiebreakerSubCommittees[1].sealableResume(address(_lido.withdrawalQueue)); - (support, quorum, isExecuted) = + (support, quorum,, isExecuted) = _tiebreakerSubCommittees[1].getSealableResumeState(address(_lido.withdrawalQueue)); assertEq(support, quorum); assertFalse(isExecuted); _tiebreakerSubCommittees[1].executeSealableResume(address(_lido.withdrawalQueue)); - (support, quorum, isExecuted) = _tiebreakerCoreCommittee.getSealableResumeState( + (support, quorum,, isExecuted) = _tiebreakerCoreCommittee.getSealableResumeState( address(_lido.withdrawalQueue), _tiebreakerCoreCommittee.getSealableResumeNonce(address(_lido.withdrawalQueue)) ); diff --git a/test/unit/HashConsensus.t.sol b/test/unit/HashConsensus.t.sol index c9f22039..a6772ac6 100644 --- a/test/unit/HashConsensus.t.sol +++ b/test/unit/HashConsensus.t.sol @@ -354,7 +354,7 @@ contract HashConsensusWrapper is HashConsensus { function getHashState(bytes32 hash) public view - returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) + returns (uint256 support, uint256 executionQuorum, uint256 quorumAt, bool isExecuted) { return _getHashState(hash); } @@ -410,41 +410,57 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { function test_getHashState() public { uint256 support; - uint256 execuitionQuorum; + uint256 executionQuorum; + uint256 quorumAt; bool isExecuted; - (support, execuitionQuorum, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); + (support, executionQuorum, quorumAt, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); assertEq(support, 0); - assertEq(execuitionQuorum, _quorum); + assertEq(executionQuorum, _quorum); + assertEq(quorumAt, 0); assertEq(isExecuted, false); + uint256 expectedQuorumAt = block.timestamp; + for (uint256 i = 0; i < _membersCount; ++i) { - (support, execuitionQuorum, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); + (support, executionQuorum, quorumAt, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); assertEq(support, i); - assertEq(execuitionQuorum, _quorum); + assertEq(executionQuorum, _quorum); + if (i >= executionQuorum) { + assertEq(quorumAt, expectedQuorumAt); + } else { + assertEq(quorumAt, 0); + } assertEq(isExecuted, false); vm.prank(_committeeMembers[i]); _hashConsensusWrapper.vote(dataHash, true); - (support, execuitionQuorum, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); + (support, executionQuorum, quorumAt, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); assertEq(support, i + 1); - assertEq(execuitionQuorum, _quorum); + assertEq(executionQuorum, _quorum); + if (i >= executionQuorum - 1) { + assertEq(quorumAt, expectedQuorumAt); + } else { + assertEq(quorumAt, 0); + } assertEq(isExecuted, false); } - (support, execuitionQuorum, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); + (support, executionQuorum, quorumAt, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); assertEq(support, _membersCount); - assertEq(execuitionQuorum, _quorum); + assertEq(executionQuorum, _quorum); + assertEq(quorumAt, expectedQuorumAt); assertEq(isExecuted, false); _wait(_timelock); _hashConsensusWrapper.execute(dataHash); - (support, execuitionQuorum, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); + (support, executionQuorum, quorumAt, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); assertEq(support, _membersCount); - assertEq(execuitionQuorum, _quorum); + assertEq(executionQuorum, _quorum); + assertEq(quorumAt, expectedQuorumAt); assertEq(isExecuted, true); } @@ -530,4 +546,77 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { emit HashConsensusWrapper.OnlyMemberModifierPassed(); _hashConsensusWrapper.onlyMemberProtected(); } + + function test_updateQuorumRevertsIfHashIsUsed() public { + bytes32 hash = keccak256("hash"); + + for (uint256 i = 0; i < _quorum; ++i) { + vm.prank(_committeeMembers[i]); + _hashConsensusWrapper.vote(hash, true); + } + + _wait(Duration.wrap(3600)); + + _hashConsensusWrapper.execute(hash); + + vm.expectRevert(abi.encodeWithSelector(HashConsensus.HashAlreadyUsed.selector, hash)); + _hashConsensusWrapper.updateQuorum(hash); + } + + function test_updateQuorumDoNothingIfQuorumAlreadyReached() public { + bytes32 hash = keccak256("hash"); + + for (uint256 i = 0; i < _quorum; ++i) { + vm.prank(_committeeMembers[i]); + _hashConsensusWrapper.vote(hash, true); + } + + (,, uint256 quorumAtBefore,) = _hashConsensusWrapper.getHashState(hash); + + _wait(_timelock); + _hashConsensusWrapper.updateQuorum(hash); + + (,, uint256 quorumAtAfter,) = _hashConsensusWrapper.getHashState(hash); + + assertEq(quorumAtBefore, quorumAtAfter); + } + + function test_updateQuorumDoNothingIfQuorumIsNotReached() public { + bytes32 hash = keccak256("hash"); + + for (uint256 i = 0; i < _quorum - 1; ++i) { + vm.prank(_committeeMembers[i]); + _hashConsensusWrapper.vote(hash, true); + } + + (,, uint256 quorumAtBefore,) = _hashConsensusWrapper.getHashState(hash); + assertEq(quorumAtBefore, 0); + + _hashConsensusWrapper.updateQuorum(hash); + + (,, uint256 quorumAtAfter,) = _hashConsensusWrapper.getHashState(hash); + assertEq(quorumAtAfter, 0); + } + + function test_updateQuorum() public { + bytes32 hash = keccak256("hash"); + + for (uint256 i = 0; i < _quorum - 1; ++i) { + vm.prank(_committeeMembers[i]); + _hashConsensusWrapper.vote(hash, true); + } + + vm.prank(_owner); + _hashConsensusWrapper.setQuorum(_quorum - 1); + + (,, uint256 quorumAtBefore,) = _hashConsensusWrapper.getHashState(hash); + + assertEq(quorumAtBefore, 0); + + _hashConsensusWrapper.updateQuorum(hash); + + (,, uint256 quorumAtAfter,) = _hashConsensusWrapper.getHashState(hash); + + assertEq(quorumAtAfter, block.timestamp); + } } From a50a1a4bdf74255d4b303a0392c74ef7f5b82a4b Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Wed, 14 Aug 2024 17:41:03 +0300 Subject: [PATCH 04/42] fix: make tiebreakerScheduleProposal calling activateNextState --- contracts/DualGovernance.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 5fb93df8..f11a9d3b 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -267,6 +267,7 @@ contract DualGovernance is IDualGovernance { function tiebreakerScheduleProposal(uint256 proposalId) external { _tiebreaker.checkCallerIsTiebreakerCommittee(); + _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); _tiebreaker.checkTie(_stateMachine.getCurrentState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt()); TIMELOCK.schedule(proposalId); } From f2c9b567904021cc601c6ef0285ef0e2894cfa38 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Fri, 16 Aug 2024 12:42:17 +0300 Subject: [PATCH 05/42] feat: change timelockstate errors handling --- contracts/libraries/TimelockState.sol | 59 +++++++++++++++++---------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/contracts/libraries/TimelockState.sol b/contracts/libraries/TimelockState.sol index eda71827..05976793 100644 --- a/contracts/libraries/TimelockState.sol +++ b/contracts/libraries/TimelockState.sol @@ -3,6 +3,8 @@ pragma solidity 0.8.26; import {Duration} from "../types/Duration.sol"; +/// @title TimelockState +/// @dev Library for managing the configuration related to emergency protection. library TimelockState { error CallerIsNotGovernance(address caller); error InvalidGovernance(address value); @@ -10,7 +12,6 @@ library TimelockState { error InvalidAfterScheduleDelay(Duration value); event GovernanceSet(address newGovernance); - event AdminExecutorSet(address newAdminExecutor); event AfterSubmitDelaySet(Duration newAfterSubmitDelay); event AfterScheduleDelaySet(Duration newAfterScheduleDelay); @@ -23,55 +24,69 @@ library TimelockState { Duration afterScheduleDelay; } + /// @notice Sets the governance address. + /// @dev Reverts if the new governance address is zero or the same as the current one. + /// @param self The context of the timelock state. + /// @param newGovernance The new governance address. function setGovernance(Context storage self, address newGovernance) internal { - if (newGovernance == address(0)) { + if (newGovernance == address(0) || newGovernance == self.governance) { revert InvalidGovernance(newGovernance); } - if (self.governance == newGovernance) { - return; - } self.governance = newGovernance; emit GovernanceSet(newGovernance); } - function getAfterSubmitDelay(Context storage self) internal view returns (Duration) { - return self.afterSubmitDelay; - } - - function getAfterScheduleDelay(Context storage self) internal view returns (Duration) { - return self.afterScheduleDelay; - } - + /// @notice Sets the after submit delay. + /// @dev Reverts if the new delay is greater than the maximum allowed or the same as the current one. + /// @param self The context of the timelock state. + /// @param newAfterSubmitDelay The new after submit delay. + /// @param maxAfterSubmitDelay The maximum allowed after submit delay. function setAfterSubmitDelay( Context storage self, Duration newAfterSubmitDelay, Duration maxAfterSubmitDelay ) internal { - if (newAfterSubmitDelay > maxAfterSubmitDelay) { - revert InvalidAfterScheduleDelay(newAfterSubmitDelay); - } - if (self.afterSubmitDelay == newAfterSubmitDelay) { - return; + if (newAfterSubmitDelay > maxAfterSubmitDelay || newAfterSubmitDelay == self.afterSubmitDelay) { + revert InvalidAfterSubmitDelay(newAfterSubmitDelay); } self.afterSubmitDelay = newAfterSubmitDelay; emit AfterSubmitDelaySet(newAfterSubmitDelay); } + /// @notice Sets the after schedule delay. + /// @dev Reverts if the new delay is greater than the maximum allowed or the same as the current one. + /// @param self The context of the timelock state. + /// @param newAfterScheduleDelay The new after schedule delay. + /// @param maxAfterScheduleDelay The maximum allowed after schedule delay. function setAfterScheduleDelay( Context storage self, Duration newAfterScheduleDelay, Duration maxAfterScheduleDelay ) internal { - if (newAfterScheduleDelay > maxAfterScheduleDelay) { + if (newAfterScheduleDelay > maxAfterScheduleDelay || newAfterScheduleDelay == self.afterScheduleDelay) { revert InvalidAfterScheduleDelay(newAfterScheduleDelay); } - if (self.afterScheduleDelay == newAfterScheduleDelay) { - return; - } self.afterScheduleDelay = newAfterScheduleDelay; emit AfterScheduleDelaySet(newAfterScheduleDelay); } + /// @notice Gets the after submit delay. + /// @param self The context of the timelock state. + /// @return The current after submit delay. + function getAfterSubmitDelay(Context storage self) internal view returns (Duration) { + return self.afterSubmitDelay; + } + + /// @notice Gets the after schedule delay. + /// @param self The context of the timelock state. + /// @return The current after schedule delay. + function getAfterScheduleDelay(Context storage self) internal view returns (Duration) { + return self.afterScheduleDelay; + } + + /// @notice Checks if the caller is the governance address. + /// @dev Reverts if the caller is not the governance address. + /// @param self The context of the timelock state. function checkCallerIsGovernance(Context storage self) internal view { if (self.governance != msg.sender) { revert CallerIsNotGovernance(msg.sender); From 92e9d49ba4f757f464e08ff6576e6c592bc92c03 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Wed, 14 Aug 2024 13:33:26 +0300 Subject: [PATCH 06/42] feat: tiebreaker improvements and unit tests --- contracts/libraries/Tiebreaker.sol | 69 ++++++++-- test/unit/libraries/Tiebreaker.t.sol | 193 +++++++++++++++++++++++++++ 2 files changed, 249 insertions(+), 13 deletions(-) create mode 100644 test/unit/libraries/Tiebreaker.t.sol diff --git a/contracts/libraries/Tiebreaker.sol b/contracts/libraries/Tiebreaker.sol index 137a089c..37a547cc 100644 --- a/contracts/libraries/Tiebreaker.sol +++ b/contracts/libraries/Tiebreaker.sol @@ -2,23 +2,27 @@ pragma solidity 0.8.26; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; - import {Duration} from "../types/Duration.sol"; import {Timestamp, Timestamps} from "../types/Duration.sol"; - import {ISealable} from "../interfaces/ISealable.sol"; - import {SealableCalls} from "./SealableCalls.sol"; import {State as DualGovernanceState} from "./DualGovernanceStateMachine.sol"; +/// @title Tiebreaker Library +/// @dev The mechanism design allows for a deadlock where the system is stuck in the RageQuit +/// state while protocol withdrawals are paused or dysfunctional and require a DAO vote to resume, +/// and includes a third-party arbiter Tiebreaker committee for resolving it. Tiebreaker gains +/// the power to execute pending proposals, bypassing the DG dynamic timelock, and unpause any +/// protocol contract under the specific conditions of the deadlock. library Tiebreaker { using SealableCalls for ISealable; using EnumerableSet for EnumerableSet.AddressSet; - error TiebreakDisallowed(); - error InvalidSealable(address value); - error InvalidTiebreakerCommittee(address value); - error InvalidTiebreakerActivationTimeout(Duration value); + error TiebreakNotAllowed(); + error InvalidSealable(address sealable); + error InvalidTiebreakerCommittee(address account); + error InvalidTiebreakerActivationTimeout(Duration timeout); + error CallerIsNotTiebreakerCommittee(address caller); error SealableWithdrawalBlockersLimitReached(); event SealableWithdrawalBlockerAdded(address sealable); @@ -26,6 +30,10 @@ library Tiebreaker { event TiebreakerCommitteeSet(address newTiebreakerCommittee); event TiebreakerActivationTimeoutSet(Duration newTiebreakerActivationTimeout); + /// @dev Context struct to store tiebreaker-related data. + /// @param tiebreakerCommittee Address of the tiebreaker committee. + /// @param tiebreakerActivationTimeout Duration for tiebreaker activation timeout. + /// @param sealableWithdrawalBlockers Set of addresses that are sealable withdrawal blockers. struct Context { /// @dev slot0 [0..159] address tiebreakerCommittee; @@ -39,6 +47,11 @@ library Tiebreaker { // Setup functionality // --- + /// @notice Adds a sealable withdrawal blocker. + /// @dev Reverts if the maximum number of sealable withdrawal blockers is reached or if the sealable is invalid. + /// @param self The context storage. + /// @param sealableWithdrawalBlocker The address of the sealable withdrawal blocker to add. + /// @param maxSealableWithdrawalBlockersCount The maximum number of sealable withdrawal blockers allowed. function addSealableWithdrawalBlocker( Context storage self, address sealableWithdrawalBlocker, @@ -48,7 +61,6 @@ library Tiebreaker { if (sealableWithdrawalBlockersCount == maxSealableWithdrawalBlockersCount) { revert SealableWithdrawalBlockersLimitReached(); } - (bool isCallSucceed, /* lowLevelError */, /* isPaused */ ) = ISealable(sealableWithdrawalBlocker).callIsPaused(); if (!isCallSucceed) { revert InvalidSealable(sealableWithdrawalBlocker); @@ -60,6 +72,9 @@ library Tiebreaker { } } + /// @notice Removes a sealable withdrawal blocker. + /// @param self The context storage. + /// @param sealableWithdrawalBlocker The address of the sealable withdrawal blocker to remove. function removeSealableWithdrawalBlocker(Context storage self, address sealableWithdrawalBlocker) internal { bool isSuccessfullyRemoved = self.sealableWithdrawalBlockers.remove(sealableWithdrawalBlocker); if (isSuccessfullyRemoved) { @@ -67,6 +82,10 @@ library Tiebreaker { } } + /// @notice Sets the tiebreaker committee. + /// @dev Reverts if the new tiebreaker committee address is invalid. + /// @param self The context storage. + /// @param newTiebreakerCommittee The address of the new tiebreaker committee. function setTiebreakerCommittee(Context storage self, address newTiebreakerCommittee) internal { if (newTiebreakerCommittee == address(0)) { revert InvalidTiebreakerCommittee(newTiebreakerCommittee); @@ -78,6 +97,12 @@ library Tiebreaker { emit TiebreakerCommitteeSet(newTiebreakerCommittee); } + /// @notice Sets the tiebreaker activation timeout. + /// @dev Reverts if the new timeout is outside the allowed range. + /// @param self The context storage. + /// @param minTiebreakerActivationTimeout The minimum allowed tiebreaker activation timeout. + /// @param newTiebreakerActivationTimeout The new tiebreaker activation timeout. + /// @param maxTiebreakerActivationTimeout The maximum allowed tiebreaker activation timeout. function setTiebreakerActivationTimeout( Context storage self, Duration minTiebreakerActivationTimeout, @@ -102,19 +127,27 @@ library Tiebreaker { // Checks // --- + /// @notice Checks if the caller is the tiebreaker committee. + /// @dev Reverts if the caller is not the tiebreaker committee. + /// @param self The context storage. function checkCallerIsTiebreakerCommittee(Context storage self) internal view { if (msg.sender != self.tiebreakerCommittee) { - revert InvalidTiebreakerCommittee(msg.sender); + revert CallerIsNotTiebreakerCommittee(msg.sender); } } + /// @notice Checks if a tie exists. + /// @dev Reverts if no tie exists. + /// @param self The context storage. + /// @param state The current state of dual governance. + /// @param normalOrVetoCooldownExitedAt The timestamp when normal or veto cooldown exited. function checkTie( Context storage self, DualGovernanceState state, Timestamp normalOrVetoCooldownExitedAt ) internal view { if (!isTie(self, state, normalOrVetoCooldownExitedAt)) { - revert TiebreakDisallowed(); + revert TiebreakNotAllowed(); } } @@ -122,6 +155,11 @@ library Tiebreaker { // Getters // --- + /// @notice Determines if a tie exists. + /// @param self The context storage. + /// @param state The current state of dual governance. + /// @param normalOrVetoCooldownExitedAt The timestamp when normal or veto cooldown exited. + /// @return True if a tie exists, false otherwise. function isTie( Context storage self, DualGovernanceState state, @@ -129,7 +167,6 @@ library Tiebreaker { ) internal view returns (bool) { if (state == DualGovernanceState.Normal || state == DualGovernanceState.VetoCooldown) return false; - // when the governance is locked for long period of time if (Timestamps.now() >= self.tiebreakerActivationTimeout.addTo(normalOrVetoCooldownExitedAt)) { return true; } @@ -137,19 +174,25 @@ library Tiebreaker { return state == DualGovernanceState.RageQuit && isSomeSealableWithdrawalBlockerPaused(self); } + /// @notice Checks if any sealable withdrawal blocker is paused. + /// @param self The context storage. + /// @return True if any sealable withdrawal blocker is paused, false otherwise. function isSomeSealableWithdrawalBlockerPaused(Context storage self) internal view returns (bool) { uint256 sealableWithdrawalBlockersCount = self.sealableWithdrawalBlockers.length(); for (uint256 i = 0; i < sealableWithdrawalBlockersCount; ++i) { (bool isCallSucceed, /* lowLevelError */, bool isPaused) = ISealable(self.sealableWithdrawalBlockers.at(i)).callIsPaused(); - // in normal condition this call must never fail, so if some sealable withdrawal blocker - // started behave unexpectedly tiebreaker action may be the last hope for the protocol saving if (isPaused || !isCallSucceed) return true; } return false; } + /// @notice Gets the tiebreaker information. + /// @param self The context storage. + /// @return tiebreakerCommittee The address of the tiebreaker committee. + /// @return tiebreakerActivationTimeout The duration of the tiebreaker activation timeout. + /// @return sealableWithdrawalBlockers The addresses of the sealable withdrawal blockers. function getTiebreakerInfo(Context storage self) internal view diff --git a/test/unit/libraries/Tiebreaker.t.sol b/test/unit/libraries/Tiebreaker.t.sol new file mode 100644 index 00000000..e213d3df --- /dev/null +++ b/test/unit/libraries/Tiebreaker.t.sol @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +import {State as DualGovernanceState} from "contracts/libraries/DualGovernanceStateMachine.sol"; +import {Tiebreaker} from "contracts/libraries/Tiebreaker.sol"; +import {Duration, Durations, Timestamp, Timestamps} from "contracts/types/Duration.sol"; +import {ISealable} from "contracts/interfaces/ISealable.sol"; + +import {UnitTest} from "test/utils/unit-test.sol"; +import {SealableMock} from "../../mocks/SealableMock.sol"; + +contract TiebreakerTest is UnitTest { + using EnumerableSet for EnumerableSet.AddressSet; + + Tiebreaker.Context private context; + SealableMock private mockSealable1; + SealableMock private mockSealable2; + + function setUp() external { + mockSealable1 = new SealableMock(); + mockSealable2 = new SealableMock(); + } + + function test_addSealableWithdrawalBlocker() external { + vm.expectEmit(); + emit Tiebreaker.SealableWithdrawalBlockerAdded(address(mockSealable1)); + Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable1), 1); + + assertTrue(context.sealableWithdrawalBlockers.contains(address(mockSealable1))); + } + + function test_AddSealableWithdrawalBlocker_RevertOn_LimitReached() external { + Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable1), 1); + + vm.expectRevert(Tiebreaker.SealableWithdrawalBlockersLimitReached.selector); + Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable2), 1); + } + + function test_AddSealableWithdrawalBlocker_RevertOn_InvalidSealable() external { + mockSealable1.setShouldRevertIsPaused(true); + + vm.expectRevert(abi.encodeWithSelector(Tiebreaker.InvalidSealable.selector, address(mockSealable1))); + // external call should be used to intercept the revert + this.external__addSealableWithdrawalBlocker(address(mockSealable1)); + + vm.expectRevert(); + // external call should be used to intercept the revert + this.external__addSealableWithdrawalBlocker(address(0x123)); + } + + function test_RemoveSealableWithdrawalBlocker() external { + Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable1), 1); + assertTrue(context.sealableWithdrawalBlockers.contains(address(mockSealable1))); + + vm.expectEmit(); + emit Tiebreaker.SealableWithdrawalBlockerRemoved(address(mockSealable1)); + + Tiebreaker.removeSealableWithdrawalBlocker(context, address(mockSealable1)); + assertFalse(context.sealableWithdrawalBlockers.contains(address(mockSealable1))); + } + + function test_SetTiebreakerCommittee() external { + address newCommittee = address(0x123); + + vm.expectEmit(); + emit Tiebreaker.TiebreakerCommitteeSet(newCommittee); + Tiebreaker.setTiebreakerCommittee(context, newCommittee); + + assertEq(context.tiebreakerCommittee, newCommittee); + } + + function test_SetTiebreakerCommittee_WithExistingCommitteeAddress() external { + address newCommittee = address(0x123); + + Tiebreaker.setTiebreakerCommittee(context, newCommittee); + Tiebreaker.setTiebreakerCommittee(context, newCommittee); + } + + function test_SetTiebreakerCommittee_RevertOn_ZeroAddress() external { + vm.expectRevert(abi.encodeWithSelector(Tiebreaker.InvalidTiebreakerCommittee.selector, address(0))); + Tiebreaker.setTiebreakerCommittee(context, address(0)); + } + + function testFuzz_SetTiebreakerActivationTimeout(uint32 minTimeout, uint32 maxTimeout, uint32 timeout) external { + vm.assume(minTimeout < timeout && timeout < maxTimeout); + + Duration min = Duration.wrap(minTimeout); + Duration max = Duration.wrap(maxTimeout); + Duration newTimeout = Duration.wrap(timeout); + + vm.expectEmit(); + emit Tiebreaker.TiebreakerActivationTimeoutSet(newTimeout); + + Tiebreaker.setTiebreakerActivationTimeout(context, min, newTimeout, max); + assertEq(context.tiebreakerActivationTimeout, newTimeout); + } + + function test_SetTiebreakerActivationTimeout_RevertOn_InvalidTimeout() external { + Duration minTimeout = Duration.wrap(1 days); + Duration maxTimeout = Duration.wrap(10 days); + Duration newTimeout = Duration.wrap(15 days); + + vm.expectRevert(abi.encodeWithSelector(Tiebreaker.InvalidTiebreakerActivationTimeout.selector, newTimeout)); + Tiebreaker.setTiebreakerActivationTimeout(context, minTimeout, newTimeout, maxTimeout); + + newTimeout = Duration.wrap(0 days); + + vm.expectRevert(abi.encodeWithSelector(Tiebreaker.InvalidTiebreakerActivationTimeout.selector, newTimeout)); + Tiebreaker.setTiebreakerActivationTimeout(context, minTimeout, newTimeout, maxTimeout); + } + + function test_IsSomeSealableWithdrawalBlockerPaused() external { + mockSealable1.pauseFor(1 days); + Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable1), 2); + + bool result = Tiebreaker.isSomeSealableWithdrawalBlockerPaused(context); + assertTrue(result); + + mockSealable1.resume(); + + result = Tiebreaker.isSomeSealableWithdrawalBlockerPaused(context); + assertFalse(result); + + mockSealable1.setShouldRevertIsPaused(true); + + result = Tiebreaker.isSomeSealableWithdrawalBlockerPaused(context); + assertTrue(result); + } + + function test_CheckTie() external { + Timestamp cooldownExitedAt = Timestamps.from(block.timestamp); + + Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable1), 1); + Tiebreaker.setTiebreakerActivationTimeout( + context, Duration.wrap(1 days), Duration.wrap(3 days), Duration.wrap(10 days) + ); + + mockSealable1.pauseFor(1 days); + Tiebreaker.checkTie(context, DualGovernanceState.RageQuit, cooldownExitedAt); + + _wait(Duration.wrap(3 days)); + Tiebreaker.checkTie(context, DualGovernanceState.VetoSignalling, cooldownExitedAt); + } + + function test_CheckTie_RevertOn_NormalOrVetoCooldownState() external { + Timestamp cooldownExitedAt = Timestamps.from(block.timestamp); + + vm.expectRevert(Tiebreaker.TiebreakNotAllowed.selector); + Tiebreaker.checkTie(context, DualGovernanceState.Normal, cooldownExitedAt); + + vm.expectRevert(Tiebreaker.TiebreakNotAllowed.selector); + Tiebreaker.checkTie(context, DualGovernanceState.VetoCooldown, cooldownExitedAt); + } + + function test_CheckCallerIsTiebreakerCommittee() external { + context.tiebreakerCommittee = address(this); + + vm.expectRevert(abi.encodeWithSelector(Tiebreaker.CallerIsNotTiebreakerCommittee.selector, address(0x456))); + vm.prank(address(0x456)); + this.external__checkCallerIsTiebreakerCommittee(); + + this.external__checkCallerIsTiebreakerCommittee(); + } + + function test_GetTimebreakerInfo() external { + Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable1), 1); + + Duration minTimeout = Duration.wrap(1 days); + Duration maxTimeout = Duration.wrap(10 days); + Duration timeout = Duration.wrap(5 days); + + context.tiebreakerActivationTimeout = timeout; + context.tiebreakerCommittee = address(0x123); + + (address committee, Duration activationTimeout, address[] memory blockers) = + Tiebreaker.getTiebreakerInfo(context); + + assertEq(committee, context.tiebreakerCommittee); + assertEq(activationTimeout, context.tiebreakerActivationTimeout); + assertEq(blockers[0], address(mockSealable1)); + assertEq(blockers.length, 1); + } + + function external__checkCallerIsTiebreakerCommittee() external { + Tiebreaker.checkCallerIsTiebreakerCommittee(context); + } + + function external__addSealableWithdrawalBlocker(address sealable) external { + Tiebreaker.addSealableWithdrawalBlocker(context, sealable, 1); + } +} From 06e1db20bb172ff4e7bb137213c5d9a2fdc48830 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Fri, 16 Aug 2024 12:42:38 +0300 Subject: [PATCH 07/42] feat: change escrowstate errors handling --- contracts/libraries/EscrowState.sol | 48 ++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/contracts/libraries/EscrowState.sol b/contracts/libraries/EscrowState.sol index 9b6fd842..5c5b14b0 100644 --- a/contracts/libraries/EscrowState.sol +++ b/contracts/libraries/EscrowState.sol @@ -18,6 +18,7 @@ enum State { RageQuitEscrow } +/// @title EscrowState /// @notice Represents the logic to manipulate the state of the Escrow library EscrowState { // --- @@ -28,13 +29,13 @@ library EscrowState { error UnexpectedState(State value); error RageQuitExtraTimelockNotStarted(); error WithdrawalsTimelockNotPassed(); - error BatchesCreationNotInProgress(); + error InvalidMinAssetsLockDuration(Duration newMinAssetsLockDuration); // --- // Events // --- - event RageQuitTimelockStarted(); + event RageQuitTimelockStarted(Timestamp startedAt); event EscrowStateChanged(State from, State to); event RageQuitStarted(Duration rageQuitExtensionDelay, Duration rageQuitWithdrawalsTimelock); event MinAssetsLockDurationSet(Duration newAssetsLockDuration); @@ -59,12 +60,19 @@ library EscrowState { Duration rageQuitWithdrawalsTimelock; } + /// @notice Initializes the Escrow state to SignallingEscrow + /// @param self The context of the Escrow instance + /// @param minAssetsLockDuration The minimum assets lock duration function initialize(Context storage self, Duration minAssetsLockDuration) internal { _checkState(self, State.NotInitialized); _setState(self, State.SignallingEscrow); _setMinAssetsLockDuration(self, minAssetsLockDuration); } + /// @notice Starts the rage quit process + /// @param self The context of the Escrow instance + /// @param rageQuitExtensionDelay The delay period for the rage quit extension + /// @param rageQuitWithdrawalsTimelock The timelock period for rage quit withdrawals function startRageQuit( Context storage self, Duration rageQuitExtensionDelay, @@ -77,14 +85,19 @@ library EscrowState { emit RageQuitStarted(rageQuitExtensionDelay, rageQuitWithdrawalsTimelock); } + /// @notice Starts the rage quit extension delay + /// @param self The context of the Escrow instance function startRageQuitExtensionDelay(Context storage self) internal { self.rageQuitExtensionDelayStartedAt = Timestamps.now(); - emit RageQuitTimelockStarted(); + emit RageQuitTimelockStarted(self.rageQuitExtensionDelayStartedAt); } + /// @notice Sets the minimum assets lock duration + /// @param self The context of the Escrow instance + /// @param newMinAssetsLockDuration The new minimum assets lock duration function setMinAssetsLockDuration(Context storage self, Duration newMinAssetsLockDuration) internal { if (self.minAssetsLockDuration == newMinAssetsLockDuration) { - return; + revert InvalidMinAssetsLockDuration(newMinAssetsLockDuration); } _setMinAssetsLockDuration(self, newMinAssetsLockDuration); } @@ -93,20 +106,28 @@ library EscrowState { // Checks // --- + /// @notice Checks if the Escrow is in the SignallingEscrow state + /// @param self The context of the Escrow instance function checkSignallingEscrow(Context storage self) internal view { _checkState(self, State.SignallingEscrow); } + /// @notice Checks if the Escrow is in the RageQuitEscrow state + /// @param self The context of the Escrow instance function checkRageQuitEscrow(Context storage self) internal view { _checkState(self, State.RageQuitEscrow); } + /// @notice Checks if batch claiming is in progress + /// @param self The context of the Escrow instance function checkBatchesClaimingInProgress(Context storage self) internal view { if (!self.rageQuitExtensionDelayStartedAt.isZero()) { revert ClaimingIsFinished(); } } + /// @notice Checks if the withdrawals timelock has passed + /// @param self The context of the Escrow instance function checkWithdrawalsTimelockPassed(Context storage self) internal view { if (self.rageQuitExtensionDelayStartedAt.isZero()) { revert RageQuitExtraTimelockNotStarted(); @@ -120,16 +141,26 @@ library EscrowState { // --- // Getters // --- + + /// @notice Checks if the rage quit extension delay has started + /// @param self The context of the Escrow instance + /// @return True if the rage quit extension delay has started, false otherwise function isRageQuitExtensionDelayStarted(Context storage self) internal view returns (bool) { return self.rageQuitExtensionDelayStartedAt.isNotZero(); } + /// @notice Checks if the rage quit extension delay has passed + /// @param self The context of the Escrow instance + /// @return True if the rage quit extension delay has passed, false otherwise function isRageQuitExtensionDelayPassed(Context storage self) internal view returns (bool) { Timestamp rageQuitExtensionDelayStartedAt = self.rageQuitExtensionDelayStartedAt; return rageQuitExtensionDelayStartedAt.isNotZero() && Timestamps.now() > self.rageQuitExtensionDelay.addTo(rageQuitExtensionDelayStartedAt); } + /// @notice Checks if the Escrow is in the RageQuitEscrow state + /// @param self The context of the Escrow instance + /// @return True if the Escrow is in the RageQuitEscrow state, false otherwise function isRageQuitEscrow(Context storage self) internal view returns (bool) { return self.state == State.RageQuitEscrow; } @@ -138,18 +169,27 @@ library EscrowState { // Private Methods // --- + /// @notice Checks if the Escrow is in the expected state + /// @param self The context of the Escrow instance + /// @param state The expected state function _checkState(Context storage self, State state) private view { if (self.state != state) { revert UnexpectedState(state); } } + /// @notice Sets the state of the Escrow + /// @param self The context of the Escrow instance + /// @param newState The new state function _setState(Context storage self, State newState) private { State prevState = self.state; self.state = newState; emit EscrowStateChanged(prevState, newState); } + /// @notice Sets the minimum assets lock duration + /// @param self The context of the Escrow instance + /// @param newMinAssetsLockDuration The new minimum assets lock duration function _setMinAssetsLockDuration(Context storage self, Duration newMinAssetsLockDuration) private { self.minAssetsLockDuration = newMinAssetsLockDuration; emit MinAssetsLockDurationSet(newMinAssetsLockDuration); From ba51f1421dd0b93440aed674a35122daff3c4c9a Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Fri, 16 Aug 2024 12:43:30 +0300 Subject: [PATCH 08/42] feat: change tiebreaker error handling --- contracts/libraries/Tiebreaker.sol | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/contracts/libraries/Tiebreaker.sol b/contracts/libraries/Tiebreaker.sol index 137a089c..43af793b 100644 --- a/contracts/libraries/Tiebreaker.sol +++ b/contracts/libraries/Tiebreaker.sol @@ -68,12 +68,9 @@ library Tiebreaker { } function setTiebreakerCommittee(Context storage self, address newTiebreakerCommittee) internal { - if (newTiebreakerCommittee == address(0)) { + if (newTiebreakerCommittee == address(0) || newTiebreakerCommittee == self.tiebreakerCommittee) { revert InvalidTiebreakerCommittee(newTiebreakerCommittee); } - if (self.tiebreakerCommittee == newTiebreakerCommittee) { - return; - } self.tiebreakerCommittee = newTiebreakerCommittee; emit TiebreakerCommitteeSet(newTiebreakerCommittee); } @@ -87,13 +84,10 @@ library Tiebreaker { if ( newTiebreakerActivationTimeout < minTiebreakerActivationTimeout || newTiebreakerActivationTimeout > maxTiebreakerActivationTimeout + || newTiebreakerActivationTimeout == self.tiebreakerActivationTimeout ) { revert InvalidTiebreakerActivationTimeout(newTiebreakerActivationTimeout); } - - if (self.tiebreakerActivationTimeout == newTiebreakerActivationTimeout) { - return; - } self.tiebreakerActivationTimeout = newTiebreakerActivationTimeout; emit TiebreakerActivationTimeoutSet(newTiebreakerActivationTimeout); } From 97671b98e4f315994551eb87f6120ae3cccb1d79 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Fri, 16 Aug 2024 13:30:43 +0300 Subject: [PATCH 09/42] feat: change emergency protection errors handling --- contracts/libraries/EmergencyProtection.sol | 52 ++- test/unit/libraries/EmergencyProtection.t.sol | 417 ++++++------------ 2 files changed, 179 insertions(+), 290 deletions(-) diff --git a/contracts/libraries/EmergencyProtection.sol b/contracts/libraries/EmergencyProtection.sol index 4adc7bcd..167ed630 100644 --- a/contracts/libraries/EmergencyProtection.sol +++ b/contracts/libraries/EmergencyProtection.sol @@ -11,12 +11,15 @@ library EmergencyProtection { error CallerIsNotEmergencyActivationCommittee(address caller); error CallerIsNotEmergencyExecutionCommittee(address caller); error EmergencyProtectionExpired(Timestamp protectedTill); + error InvalidEmergencyGovernance(address governance); + error InvalidEmergencyActivationCommittee(address committee); + error InvalidEmergencyExecutionCommittee(address committee); error InvalidEmergencyModeDuration(Duration value); error InvalidEmergencyProtectionEndDate(Timestamp value); error UnexpectedEmergencyModeState(bool value); - event EmergencyModeActivated(); - event EmergencyModeDeactivated(); + event EmergencyModeActivated(Timestamp activatedAt); + event EmergencyModeDeactivated(Timestamp deactivatedAt); event EmergencyGovernanceSet(address newEmergencyGovernance); event EmergencyActivationCommitteeSet(address newActivationCommittee); event EmergencyExecutionCommitteeSet(address newActivationCommittee); @@ -52,8 +55,7 @@ library EmergencyProtection { } self.emergencyModeEndsAfter = self.emergencyModeDuration.addTo(now_); - - emit EmergencyModeActivated(); + emit EmergencyModeActivated(Timestamps.now()); } /// @dev Deactivates the emergency mode. @@ -64,64 +66,80 @@ library EmergencyProtection { self.emergencyProtectionEndsAfter = Timestamps.ZERO; self.emergencyModeEndsAfter = Timestamps.ZERO; self.emergencyModeDuration = Durations.ZERO; - emit EmergencyModeDeactivated(); + emit EmergencyModeDeactivated(Timestamps.now()); } // --- // Setup functionality // --- + /// @dev Sets the emergency governance address. + /// @param self The storage reference to the Context struct. + /// @param newEmergencyGovernance The new emergency governance address. function setEmergencyGovernance(Context storage self, address newEmergencyGovernance) internal { if (newEmergencyGovernance == self.emergencyGovernance) { - return; + revert InvalidEmergencyGovernance(newEmergencyGovernance); } self.emergencyGovernance = newEmergencyGovernance; emit EmergencyGovernanceSet(newEmergencyGovernance); } + /// @dev Sets the emergency protection end date. + /// @param self The storage reference to the Context struct. + /// @param newEmergencyProtectionEndDate The new emergency protection end date. + /// @param maxEmergencyProtectionDuration The maximum duration for the emergency protection. function setEmergencyProtectionEndDate( Context storage self, Timestamp newEmergencyProtectionEndDate, Duration maxEmergencyProtectionDuration ) internal { - if (newEmergencyProtectionEndDate > maxEmergencyProtectionDuration.addTo(Timestamps.now())) { + if ( + newEmergencyProtectionEndDate > maxEmergencyProtectionDuration.addTo(Timestamps.now()) + || newEmergencyProtectionEndDate == self.emergencyProtectionEndsAfter + ) { revert InvalidEmergencyProtectionEndDate(newEmergencyProtectionEndDate); } - - if (newEmergencyProtectionEndDate == self.emergencyProtectionEndsAfter) { - return; - } self.emergencyProtectionEndsAfter = newEmergencyProtectionEndDate; emit EmergencyProtectionEndDateSet(newEmergencyProtectionEndDate); } + /// @dev Sets the emergency mode duration. + /// @param self The storage reference to the Context struct. + /// @param newEmergencyModeDuration The new emergency mode duration. + /// @param maxEmergencyModeDuration The maximum duration for the emergency mode. function setEmergencyModeDuration( Context storage self, Duration newEmergencyModeDuration, Duration maxEmergencyModeDuration ) internal { - if (newEmergencyModeDuration > maxEmergencyModeDuration) { + if ( + newEmergencyModeDuration > maxEmergencyModeDuration + || newEmergencyModeDuration == self.emergencyModeDuration + ) { revert InvalidEmergencyModeDuration(newEmergencyModeDuration); } - if (newEmergencyModeDuration == self.emergencyModeDuration) { - return; - } self.emergencyModeDuration = newEmergencyModeDuration; emit EmergencyModeDurationSet(newEmergencyModeDuration); } + /// @dev Sets the emergency activation committee address. + /// @param self The storage reference to the Context struct. + /// @param newActivationCommittee The new emergency activation committee address. function setEmergencyActivationCommittee(Context storage self, address newActivationCommittee) internal { if (newActivationCommittee == self.emergencyActivationCommittee) { - return; + revert InvalidEmergencyActivationCommittee(newActivationCommittee); } self.emergencyActivationCommittee = newActivationCommittee; emit EmergencyActivationCommitteeSet(newActivationCommittee); } + /// @dev Sets the emergency execution committee address. + /// @param self The storage reference to the Context struct. + /// @param newExecutionCommittee The new emergency execution committee address. function setEmergencyExecutionCommittee(Context storage self, address newExecutionCommittee) internal { if (newExecutionCommittee == self.emergencyExecutionCommittee) { - return; + revert InvalidEmergencyExecutionCommittee(newExecutionCommittee); } self.emergencyExecutionCommittee = newExecutionCommittee; emit EmergencyExecutionCommitteeSet(newExecutionCommittee); diff --git a/test/unit/libraries/EmergencyProtection.t.sol b/test/unit/libraries/EmergencyProtection.t.sol index 02856a37..eb2d8c8c 100644 --- a/test/unit/libraries/EmergencyProtection.t.sol +++ b/test/unit/libraries/EmergencyProtection.t.sol @@ -1,352 +1,223 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {Vm} from "forge-std/Test.sol"; - -import {Timestamps} from "contracts/types/Timestamp.sol"; +import {Timestamps, Timestamp} from "contracts/types/Timestamp.sol"; import {Duration, Durations} from "contracts/types/Duration.sol"; - import {EmergencyProtection} from "contracts/libraries/EmergencyProtection.sol"; import {UnitTest} from "test/utils/unit-test.sol"; -contract EmergencyProtectionUnitTests is UnitTest { - using EmergencyProtection for EmergencyProtection.Context; - - address internal _emergencyGovernance = makeAddr("EMERGENCY_GOVERNANCE"); - - EmergencyProtection.Context internal _emergencyProtection; - - function testFuzz_setup_emergency_protection( - address activationCommittee, - address executionCommittee, - address emergencyGovernance, - Duration protectionDuration, - Duration duration - ) external { - vm.assume(protectionDuration > Durations.ZERO); - vm.assume(duration > Durations.ZERO); - // vm.assume(activationCommittee != address(0)); - // vm.assume(executionCommittee != address(0)); - uint256 expectedLogEntiresCount = 2; - if (emergencyGovernance != address(0)) { - vm.expectEmit(); - emit EmergencyProtection.EmergencyGovernanceSet(emergencyGovernance); - expectedLogEntiresCount += 1; - } - - if (activationCommittee != address(0)) { - vm.expectEmit(); - emit EmergencyProtection.EmergencyActivationCommitteeSet(activationCommittee); - expectedLogEntiresCount += 1; - } - if (executionCommittee != address(0)) { - vm.expectEmit(); - emit EmergencyProtection.EmergencyExecutionCommitteeSet(executionCommittee); - expectedLogEntiresCount += 1; - } - vm.expectEmit(); - emit EmergencyProtection.EmergencyProtectionEndDateSet(protectionDuration.addTo(Timestamps.now())); - vm.expectEmit(); - emit EmergencyProtection.EmergencyModeDurationSet(duration); - - vm.recordLogs(); +contract EmergencyProtectionTest is UnitTest { + EmergencyProtection.Context ctx; - _setup(emergencyGovernance, activationCommittee, executionCommittee, protectionDuration, duration); + address emergencyGovernance = address(0x1); + address emergencyActivationCommittee = address(0x2); + address emergencyExecutionCommittee = address(0x3); - Vm.Log[] memory entries = vm.getRecordedLogs(); - assertEq(entries.length, expectedLogEntiresCount); - - assertEq(_emergencyProtection.emergencyGovernance, emergencyGovernance); - assertEq(_emergencyProtection.emergencyActivationCommittee, activationCommittee); - assertEq(_emergencyProtection.emergencyExecutionCommittee, executionCommittee); - assertEq(_emergencyProtection.emergencyProtectionEndsAfter, protectionDuration.addTo(Timestamps.now())); - assertEq(_emergencyProtection.emergencyModeDuration, duration); - assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); + function setUp() external { + // Setup initial values + ctx.emergencyGovernance = emergencyGovernance; + ctx.emergencyActivationCommittee = emergencyActivationCommittee; + ctx.emergencyExecutionCommittee = emergencyExecutionCommittee; + ctx.emergencyModeDuration = Duration.wrap(3600); + ctx.emergencyProtectionEndsAfter = Timestamps.from(block.timestamp + 86400); } - function test_setup_same_activation_committee() external { - Duration protectionDuration = Durations.from(100 seconds); - Duration emergencyModeDuration = Durations.from(100 seconds); - address activationCommittee = makeAddr("activationCommittee"); - - _setup(_emergencyGovernance, activationCommittee, address(0x2), protectionDuration, emergencyModeDuration); - - Duration newProtectionDuration = Durations.from(200 seconds); - Duration newEmergencyModeDuration = Durations.from(300 seconds); - - vm.expectEmit(); - emit EmergencyProtection.EmergencyExecutionCommitteeSet(address(0x3)); + function test_ActivateEmergencyMode() external { vm.expectEmit(); - emit EmergencyProtection.EmergencyProtectionEndDateSet(newProtectionDuration.addTo(Timestamps.now())); - vm.expectEmit(); - emit EmergencyProtection.EmergencyModeDurationSet(newEmergencyModeDuration); - - vm.recordLogs(); - _setup(_emergencyGovernance, activationCommittee, address(0x3), newProtectionDuration, newEmergencyModeDuration); - - Vm.Log[] memory entries = vm.getRecordedLogs(); - assertEq(entries.length, 3); + emit EmergencyProtection.EmergencyModeActivated(Timestamps.now()); + EmergencyProtection.activateEmergencyMode(ctx); - assertEq(_emergencyProtection.emergencyActivationCommittee, activationCommittee); - assertEq(_emergencyProtection.emergencyExecutionCommittee, address(0x3)); - assertEq(_emergencyProtection.emergencyProtectionEndsAfter, newProtectionDuration.addTo(Timestamps.now())); - assertEq(_emergencyProtection.emergencyModeDuration, newEmergencyModeDuration); - assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); + assertTrue(EmergencyProtection.isEmergencyModeActive(ctx)); + assertEq(Timestamp.unwrap(ctx.emergencyModeEndsAfter), block.timestamp + 3600); } - function test_setup_same_execution_committee() external { - Duration protectionDuration = Durations.from(100 seconds); - Duration emergencyModeDuration = Durations.from(100 seconds); - address executionCommittee = makeAddr("executionCommittee"); - - _setup(_emergencyGovernance, address(0x1), executionCommittee, protectionDuration, emergencyModeDuration); - - Duration newProtectionDuration = Durations.from(200 seconds); - Duration newEmergencyModeDuration = Durations.from(300 seconds); - - vm.expectEmit(); - emit EmergencyProtection.EmergencyActivationCommitteeSet(address(0x2)); - vm.expectEmit(); - emit EmergencyProtection.EmergencyProtectionEndDateSet(newProtectionDuration.addTo(Timestamps.now())); - vm.expectEmit(); - emit EmergencyProtection.EmergencyModeDurationSet(newEmergencyModeDuration); - - vm.recordLogs(); - _setup(_emergencyGovernance, address(0x2), executionCommittee, newProtectionDuration, newEmergencyModeDuration); + function test_ActivateEmergencyMode_RevertOn_ProtectionExpired() external { + Duration untilExpiration = + Durations.between(ctx.emergencyProtectionEndsAfter, Timestamps.from(block.timestamp)).plusSeconds(1); - Vm.Log[] memory entries = vm.getRecordedLogs(); - assertEq(entries.length, 3); + _wait(untilExpiration); - assertEq(_emergencyProtection.emergencyActivationCommittee, address(0x2)); - assertEq(_emergencyProtection.emergencyExecutionCommittee, executionCommittee); - assertEq(_emergencyProtection.emergencyProtectionEndsAfter, newProtectionDuration.addTo(Timestamps.now())); - assertEq(_emergencyProtection.emergencyModeDuration, newEmergencyModeDuration); - assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); + vm.expectRevert( + abi.encodeWithSelector( + EmergencyProtection.EmergencyProtectionExpired.selector, ctx.emergencyProtectionEndsAfter + ) + ); + EmergencyProtection.activateEmergencyMode(ctx); } - function test_setup_same_protected_till() external { - Duration protectionDuration = Durations.from(100 seconds); - Duration emergencyModeDuration = Durations.from(100 seconds); - - _setup(_emergencyGovernance, address(0x1), address(0x2), protectionDuration, emergencyModeDuration); + function test_DeactivateEmergencyMode() external { + EmergencyProtection.activateEmergencyMode(ctx); - Duration newProtectionDuration = protectionDuration; // the new value is the same as previous one - Duration newEmergencyModeDuration = Durations.from(200 seconds); - - vm.expectEmit(); - emit EmergencyProtection.EmergencyActivationCommitteeSet(address(0x3)); - vm.expectEmit(); - emit EmergencyProtection.EmergencyExecutionCommitteeSet(address(0x4)); vm.expectEmit(); - emit EmergencyProtection.EmergencyModeDurationSet(newEmergencyModeDuration); - - vm.recordLogs(); - _setup(_emergencyGovernance, address(0x3), address(0x4), newProtectionDuration, newEmergencyModeDuration); - - Vm.Log[] memory entries = vm.getRecordedLogs(); - assertEq(entries.length, 3); + emit EmergencyProtection.EmergencyModeDeactivated(Timestamps.now()); + EmergencyProtection.deactivateEmergencyMode(ctx); - assertEq(_emergencyProtection.emergencyActivationCommittee, address(0x3)); - assertEq(_emergencyProtection.emergencyExecutionCommittee, address(0x4)); - assertEq(_emergencyProtection.emergencyProtectionEndsAfter, protectionDuration.addTo(Timestamps.now())); - assertEq(_emergencyProtection.emergencyModeDuration, newEmergencyModeDuration); - assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); + assertFalse(EmergencyProtection.isEmergencyModeActive(ctx)); + assertEq(ctx.emergencyActivationCommittee, address(0)); + assertEq(ctx.emergencyExecutionCommittee, address(0)); + assertEq(Timestamp.unwrap(ctx.emergencyProtectionEndsAfter), 0); + assertEq(Timestamp.unwrap(ctx.emergencyModeEndsAfter), 0); + assertEq(Duration.unwrap(ctx.emergencyModeDuration), 0); } - function test_setup_same_emergency_mode_duration() external { - Duration protectionDuration = Durations.from(100 seconds); - Duration emergencyModeDuration = Durations.from(100 seconds); + function test_SetEmergencyGovernance() external { + address newGovernance = address(0x4); - _setup(_emergencyGovernance, address(0x1), address(0x2), protectionDuration, emergencyModeDuration); - - Duration newProtectionDuration = Durations.from(200 seconds); - Duration newEmergencyModeDuration = emergencyModeDuration; // the new value is the same as previous one - - vm.expectEmit(); - emit EmergencyProtection.EmergencyActivationCommitteeSet(address(0x3)); vm.expectEmit(); - emit EmergencyProtection.EmergencyExecutionCommitteeSet(address(0x4)); - vm.expectEmit(); - emit EmergencyProtection.EmergencyProtectionEndDateSet(newProtectionDuration.addTo(Timestamps.now())); - - vm.recordLogs(); - _setup(_emergencyGovernance, address(0x3), address(0x4), newProtectionDuration, newEmergencyModeDuration); + emit EmergencyProtection.EmergencyGovernanceSet(newGovernance); + EmergencyProtection.setEmergencyGovernance(ctx, newGovernance); - Vm.Log[] memory entries = vm.getRecordedLogs(); - assertEq(entries.length, 3); - - assertEq(_emergencyProtection.emergencyActivationCommittee, address(0x3)); - assertEq(_emergencyProtection.emergencyExecutionCommittee, address(0x4)); - assertEq(_emergencyProtection.emergencyProtectionEndsAfter, newProtectionDuration.addTo(Timestamps.now())); - assertEq(_emergencyProtection.emergencyModeDuration, newEmergencyModeDuration); - assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); + assertEq(ctx.emergencyGovernance, newGovernance); } - function test_activate_emergency_mode() external { - Duration protectionDuration = Durations.from(100 seconds); - Duration emergencyModeDuration = Durations.from(100 seconds); + function test_SetEmergencyGovernance_RevertOn_SameAddress() external { + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyGovernance.selector, emergencyGovernance) + ); + EmergencyProtection.setEmergencyGovernance(ctx, emergencyGovernance); + } - _setup(_emergencyGovernance, address(0x1), address(0x2), protectionDuration, emergencyModeDuration); + function test_SetEmergencyProtectionEndDate() external { + Timestamp newEndDate = Timestamps.from(block.timestamp + 43200); vm.expectEmit(); - emit EmergencyProtection.EmergencyModeActivated(); + emit EmergencyProtection.EmergencyProtectionEndDateSet(newEndDate); + EmergencyProtection.setEmergencyProtectionEndDate(ctx, newEndDate, Duration.wrap(86400)); - vm.recordLogs(); - - _emergencyProtection.activateEmergencyMode(); - - Vm.Log[] memory entries = vm.getRecordedLogs(); - - assertEq(entries.length, 1); - assertEq(_emergencyProtection.emergencyModeEndsAfter, emergencyModeDuration.addTo(Timestamps.now())); + assertEq(Timestamp.unwrap(ctx.emergencyProtectionEndsAfter), block.timestamp + 43200); } - function test_cannot_activate_emergency_mode_if_protected_till_expired() external { - Duration protectionDuration = Durations.from(100 seconds); - Duration emergencyModeDuration = Durations.from(100 seconds); + function test_SetEmergencyProtectionEndDate_RevertOn_InvalidValue() external { + Timestamp invalidEndDate = Timestamps.from(block.timestamp + 90000); - _setup(_emergencyGovernance, address(0x1), address(0x2), protectionDuration, emergencyModeDuration); - - _wait(protectionDuration.plusSeconds(1)); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyProtectionEndDate.selector, invalidEndDate) + ); + EmergencyProtection.setEmergencyProtectionEndDate(ctx, invalidEndDate, Duration.wrap(86400)); vm.expectRevert( abi.encodeWithSelector( - EmergencyProtection.EmergencyProtectionExpired.selector, - _emergencyProtection.emergencyProtectionEndsAfter + EmergencyProtection.InvalidEmergencyProtectionEndDate.selector, ctx.emergencyProtectionEndsAfter ) ); - _emergencyProtection.activateEmergencyMode(); + EmergencyProtection.setEmergencyProtectionEndDate(ctx, ctx.emergencyProtectionEndsAfter, Duration.wrap(86400)); } - function testFuzz_deactivate_emergency_mode( - address activationCommittee, - address executionCommittee, - Duration protectionDuration, - Duration emergencyModeDuration - ) external { - vm.assume(activationCommittee != address(0)); - vm.assume(executionCommittee != address(0)); - - _setup(_emergencyGovernance, activationCommittee, executionCommittee, protectionDuration, emergencyModeDuration); - _emergencyProtection.activateEmergencyMode(); + function test_SetEmergencyModeDuration() external { + Duration newDuration = Duration.wrap(7200); vm.expectEmit(); - emit EmergencyProtection.EmergencyModeDeactivated(); - - vm.recordLogs(); - - _emergencyProtection.deactivateEmergencyMode(); - - Vm.Log[] memory entries = vm.getRecordedLogs(); - assertEq(entries.length, 1); + emit EmergencyProtection.EmergencyModeDurationSet(newDuration); + EmergencyProtection.setEmergencyModeDuration(ctx, newDuration, Duration.wrap(86400)); - assertEq(_emergencyProtection.emergencyActivationCommittee, address(0)); - assertEq(_emergencyProtection.emergencyExecutionCommittee, address(0)); - assertEq(_emergencyProtection.emergencyProtectionEndsAfter, Timestamps.ZERO); - assertEq(_emergencyProtection.emergencyModeDuration, Durations.ZERO); - assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); + assertEq(Duration.unwrap(ctx.emergencyModeDuration), 7200); } - function test_is_emergency_mode_activated() external { - assertEq(_emergencyProtection.isEmergencyModeActive(), false); + function test_SetEmergencyModeDuration_RevertOn_InvalidValue() external { + Duration invalidDuration = Duration.wrap(90000); - Duration protectionDuration = Durations.from(100 seconds); - Duration emergencyModeDuration = Durations.from(100 seconds); - - _setup(_emergencyGovernance, address(0x1), address(0x2), protectionDuration, emergencyModeDuration); - - assertEq(_emergencyProtection.isEmergencyModeActive(), false); - - _emergencyProtection.activateEmergencyMode(); - - assertEq(_emergencyProtection.isEmergencyModeActive(), true); - - _emergencyProtection.deactivateEmergencyMode(); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeDuration.selector, invalidDuration) + ); + EmergencyProtection.setEmergencyModeDuration(ctx, invalidDuration, Duration.wrap(86400)); - assertEq(_emergencyProtection.isEmergencyModeActive(), false); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeDuration.selector, ctx.emergencyModeDuration) + ); + EmergencyProtection.setEmergencyModeDuration(ctx, ctx.emergencyModeDuration, Duration.wrap(86400)); } - function test_is_emergency_mode_passed() external { - assertEq(_emergencyProtection.isEmergencyModeDurationPassed(), false); - - Duration protectionDuration = Durations.from(100 seconds); - Duration emergencyModeDuration = Durations.from(200 seconds); - - _setup(_emergencyGovernance, address(0x1), address(0x2), protectionDuration, emergencyModeDuration); - - assertEq(_emergencyProtection.isEmergencyModeDurationPassed(), false); - - _emergencyProtection.activateEmergencyMode(); + function test_CheckCallerIsEmergencyActivationCommittee() external { + vm.prank(emergencyActivationCommittee); + this.external__checkCallerIsEmergencyActivationCommittee(); + } - assertEq(_emergencyProtection.isEmergencyModeDurationPassed(), false); + function test_CheckCallerIsEmergencyActivationCommittee_RevertOn_Stranger() external { + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.CallerIsNotEmergencyActivationCommittee.selector, address(0x5)) + ); + vm.prank(address(0x5)); + this.external__checkCallerIsEmergencyActivationCommittee(); + } - _wait(emergencyModeDuration.plusSeconds(1)); + function test_CheckCallerIsEmergencyExecutionCommittee() external { + vm.prank(emergencyExecutionCommittee); + this.external__checkCallerIsEmergencyExecutionCommittee(); + } - assertEq(_emergencyProtection.isEmergencyModeDurationPassed(), true); + function test_CheckCallerIsEmergencyExecutionCommittee_RevertOn_Stranger() external { + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.CallerIsNotEmergencyExecutionCommittee.selector, address(0x5)) + ); + vm.prank(address(0x5)); + this.external__checkCallerIsEmergencyExecutionCommittee(); + } - _emergencyProtection.deactivateEmergencyMode(); + function test_CheckEmergencyMode() external { + EmergencyProtection.activateEmergencyMode(ctx); + EmergencyProtection.checkEmergencyMode(ctx, true); + } - assertEq(_emergencyProtection.isEmergencyModeDurationPassed(), false); + function test_CheckEmergencyMode_RevertOn_NotInEmergencyMode() external { + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, true)); + EmergencyProtection.checkEmergencyMode(ctx, true); } - function test_is_emergency_protection_enabled() external { - Duration protectionDuration = Durations.from(100 seconds); - Duration emergencyModeDuration = Durations.from(200 seconds); + function test_IsEmergencyModeActive() public { + assertFalse(EmergencyProtection.isEmergencyModeActive(ctx)); + EmergencyProtection.activateEmergencyMode(ctx); + assertTrue(EmergencyProtection.isEmergencyModeActive(ctx)); + } - assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), false); + function test_IsEmergencyModeDurationPassed() public { + assertFalse(EmergencyProtection.isEmergencyModeDurationPassed(ctx)); + EmergencyProtection.activateEmergencyMode(ctx); + assertFalse(EmergencyProtection.isEmergencyModeDurationPassed(ctx)); - _setup(_emergencyGovernance, address(0x1), address(0x2), protectionDuration, emergencyModeDuration); + Duration untilExpiration = + Durations.between(ctx.emergencyModeEndsAfter, Timestamps.from(block.timestamp)).plusSeconds(1); + _wait(untilExpiration); - assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), true); + assertTrue(EmergencyProtection.isEmergencyModeDurationPassed(ctx)); + } - EmergencyProtection.Context memory emergencyState = _emergencyProtection; + function test_IsEmergencyProtectionEnabled() public { + assertTrue(EmergencyProtection.isEmergencyProtectionEnabled(ctx)); - _wait(Durations.between(emergencyState.emergencyProtectionEndsAfter, Timestamps.now())); + Duration untilExpiration = + Durations.between(ctx.emergencyProtectionEndsAfter, Timestamps.from(block.timestamp)).plusSeconds(1); + _wait(untilExpiration); - // _wait(emergencyState.emergencyProtectionEndsAfter.absDiff(Timestamps.now())); + assertFalse(EmergencyProtection.isEmergencyProtectionEnabled(ctx)); + } - EmergencyProtection.activateEmergencyMode(_emergencyProtection); + function test_IsEmergencyProtectionEnabled_WhenEmergencyModeActive() public { + assertTrue(EmergencyProtection.isEmergencyProtectionEnabled(ctx)); + EmergencyProtection.activateEmergencyMode(ctx); + assertTrue(EmergencyProtection.isEmergencyProtectionEnabled(ctx)); - _wait(emergencyModeDuration); + Duration untilExpiration = + Durations.between(ctx.emergencyModeEndsAfter, Timestamps.from(block.timestamp)).plusSeconds(1); + _wait(untilExpiration); - assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), true); + assertTrue(EmergencyProtection.isEmergencyProtectionEnabled(ctx)); - _wait(protectionDuration); + untilExpiration = + Durations.between(ctx.emergencyProtectionEndsAfter, Timestamps.from(block.timestamp)).plusSeconds(1); - assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), true); + assertTrue(EmergencyProtection.isEmergencyProtectionEnabled(ctx)); - EmergencyProtection.deactivateEmergencyMode(_emergencyProtection); + EmergencyProtection.deactivateEmergencyMode(ctx); - assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), false); + assertFalse(EmergencyProtection.isEmergencyProtectionEnabled(ctx)); } - function test_check_emergency_mode_active() external { - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, [true])); - _emergencyProtection.checkEmergencyMode(true); - _emergencyProtection.checkEmergencyMode(false); - - Duration protectionDuration = Durations.from(100 seconds); - Duration emergencyModeDuration = Durations.from(100 seconds); - - _setup(_emergencyGovernance, address(0x1), address(0x2), protectionDuration, emergencyModeDuration); - _emergencyProtection.activateEmergencyMode(); - - _emergencyProtection.checkEmergencyMode(true); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, [true])); + function external__checkCallerIsEmergencyActivationCommittee() external view { + EmergencyProtection.checkCallerIsEmergencyActivationCommittee(ctx); } - function _setup( - address newEmergencyGovernance, - address newEmergencyActivationCommittee, - address newEmergencyExecutionCommittee, - Duration protectionDuration, - Duration emergencyModeDuration - ) internal { - _emergencyProtection.setEmergencyGovernance(newEmergencyGovernance); - _emergencyProtection.setEmergencyActivationCommittee(newEmergencyActivationCommittee); - _emergencyProtection.setEmergencyExecutionCommittee(newEmergencyExecutionCommittee); - _emergencyProtection.setEmergencyProtectionEndDate(protectionDuration.addTo(Timestamps.now()), Durations.MAX); - _emergencyProtection.setEmergencyModeDuration(emergencyModeDuration, Durations.MAX); + function external__checkCallerIsEmergencyExecutionCommittee() external view { + EmergencyProtection.checkCallerIsEmergencyExecutionCommittee(ctx); } } From 1c38dd00b37ff8c207bf66bf5cd581baef1d20eb Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Fri, 16 Aug 2024 14:56:04 +0300 Subject: [PATCH 10/42] feat: change emergency protected timelock errors handling --- contracts/EmergencyProtectedTimelock.sol | 41 +++- test/scenario/happy-path-plan-b.t.sol | 52 +++-- test/unit/EmergencyProtectedTimelock.t.sol | 259 +++++++++++++-------- test/utils/SetupDeployment.sol | 30 ++- test/utils/executor-calls.sol | 54 +++++ 5 files changed, 291 insertions(+), 145 deletions(-) diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index 8ef807b1..3bf9a161 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -126,24 +126,43 @@ contract EmergencyProtectedTimelock is ITimelock { // --- // Emergency Protection Functionality - // --- + // - function setupEmergencyProtection( - address emergencyGovernance, - address emergencyActivationCommittee, - address emergencyExecutionCommittee, - Timestamp emergencyProtectionEndDate, - Duration emergencyModeDuration - ) external { + /// @dev Sets the emergency activation committee address. + /// @param emergencyActivationCommittee The address of the emergency activation committee. + function setEmergencyProtectionActivationCommittee(address emergencyActivationCommittee) external { _checkCallerIsAdminExecutor(); - - _emergencyProtection.setEmergencyGovernance(emergencyGovernance); _emergencyProtection.setEmergencyActivationCommittee(emergencyActivationCommittee); + } + + /// @dev Sets the emergency execution committee address. + /// @param emergencyExecutionCommittee The address of the emergency execution committee. + function setEmergencyProtectionExecutionCommittee(address emergencyExecutionCommittee) external { + _checkCallerIsAdminExecutor(); + _emergencyProtection.setEmergencyExecutionCommittee(emergencyExecutionCommittee); + } + + /// @dev Sets the emergency protection end date. + /// @param emergencyProtectionEndDate The timestamp of the emergency protection end date. + function setEmergencyProtectionEndDate(Timestamp emergencyProtectionEndDate) external { + _checkCallerIsAdminExecutor(); _emergencyProtection.setEmergencyProtectionEndDate( emergencyProtectionEndDate, MAX_EMERGENCY_PROTECTION_DURATION ); + } + + /// @dev Sets the emergency mode duration. + /// @param emergencyModeDuration The duration of the emergency mode. + function setEmergencyModeDuration(Duration emergencyModeDuration) external { + _checkCallerIsAdminExecutor(); _emergencyProtection.setEmergencyModeDuration(emergencyModeDuration, MAX_EMERGENCY_MODE_DURATION); - _emergencyProtection.setEmergencyExecutionCommittee(emergencyExecutionCommittee); + } + + /// @dev Sets the emergency governance address. + /// @param emergencyGovernance The address of the emergency governance. + function setEmergencyGovernance(address emergencyGovernance) external { + _checkCallerIsAdminExecutor(); + _emergencyProtection.setEmergencyGovernance(emergencyGovernance); } /// @dev Activates the emergency mode. diff --git a/test/scenario/happy-path-plan-b.t.sol b/test/scenario/happy-path-plan-b.t.sol index 3c3170fd..21604d0d 100644 --- a/test/scenario/happy-path-plan-b.t.sol +++ b/test/scenario/happy-path-plan-b.t.sol @@ -121,7 +121,15 @@ contract PlanBSetup is ScenarioTestBlueprint { }); ExternalCall[] memory dualGovernanceLaunchCalls = ExternalCallHelpers.create( - [address(_dualGovernance), address(_timelock), address(_timelock), address(_timelock)], + [ + address(_dualGovernance), + address(_timelock), + address(_timelock), + address(_timelock), + address(_timelock), + address(_timelock), + address(_timelock) + ], [ abi.encodeCall(_dualGovernance.registerProposer, (address(_lido.voting), _timelock.getAdminExecutor())), // Only Dual Governance contract can call the Timelock contract @@ -130,15 +138,15 @@ contract PlanBSetup is ScenarioTestBlueprint { abi.encodeCall(_timelock.deactivateEmergencyMode, ()), // Setup emergency committee for some period of time until the Dual Governance is battle tested abi.encodeCall( - _timelock.setupEmergencyProtection, - ( - address(_emergencyGovernance), - address(_emergencyActivationCommittee), - address(_emergencyExecutionCommittee), - _EMERGENCY_PROTECTION_DURATION.addTo(Timestamps.now()), - _EMERGENCY_MODE_DURATION - ) - ) + _timelock.setEmergencyProtectionActivationCommittee, (address(_emergencyActivationCommittee)) + ), + abi.encodeCall( + _timelock.setEmergencyProtectionExecutionCommittee, (address(_emergencyExecutionCommittee)) + ), + abi.encodeCall( + _timelock.setEmergencyProtectionEndDate, (_EMERGENCY_PROTECTION_DURATION.addTo(Timestamps.now())) + ), + abi.encodeCall(_timelock.setEmergencyModeDuration, (_EMERGENCY_MODE_DURATION)) ] ); @@ -205,22 +213,16 @@ contract PlanBSetup is ScenarioTestBlueprint { }); ExternalCall[] memory dualGovernanceUpdateCalls = ExternalCallHelpers.create( - [address(dualGovernanceV2), address(_timelock), address(_timelock)], + [address(dualGovernanceV2), address(_timelock), address(_timelock), address(_timelock)], [ abi.encodeCall(_dualGovernance.registerProposer, (address(_lido.voting), _timelock.getAdminExecutor())), // Update the controller for timelock abi.encodeCall(_timelock.setGovernance, address(dualGovernanceV2)), // Assembly the emergency committee again, until the new version of Dual Governance is battle tested abi.encodeCall( - _timelock.setupEmergencyProtection, - ( - address(_emergencyGovernance), - address(_emergencyActivationCommittee), - address(_emergencyExecutionCommittee), - _EMERGENCY_PROTECTION_DURATION.addTo(Timestamps.now()), - Durations.from(30 days) - ) - ) + _timelock.setEmergencyProtectionEndDate, (_EMERGENCY_PROTECTION_DURATION.addTo(Timestamps.now())) + ), + abi.encodeCall(_timelock.setEmergencyModeDuration, (Durations.from(30 days))) ] ); @@ -246,11 +248,11 @@ contract PlanBSetup is ScenarioTestBlueprint { assertFalse(_timelock.isEmergencyModeActive()); - EmergencyProtection.Context memory emergencyState = _timelock.getEmergencyProtectionContext(); - assertEq(emergencyState.emergencyActivationCommittee, address(_emergencyActivationCommittee)); - assertEq(emergencyState.emergencyExecutionCommittee, address(_emergencyExecutionCommittee)); - assertEq(emergencyState.emergencyModeDuration, Durations.from(30 days)); - assertEq(emergencyState.emergencyModeEndsAfter, Timestamps.ZERO); + EmergencyProtection.Context memory localEmergencyState = _timelock.getEmergencyProtectionContext(); + assertEq(localEmergencyState.emergencyActivationCommittee, address(_emergencyActivationCommittee)); + assertEq(localEmergencyState.emergencyExecutionCommittee, address(_emergencyExecutionCommittee)); + assertEq(localEmergencyState.emergencyModeDuration, Durations.from(30 days)); + assertEq(localEmergencyState.emergencyModeEndsAfter, Timestamps.ZERO); // use the new version of the dual governance in the future calls _dualGovernance = dualGovernanceV2; diff --git a/test/unit/EmergencyProtectedTimelock.t.sol b/test/unit/EmergencyProtectedTimelock.t.sol index 69b87217..95d9feac 100644 --- a/test/unit/EmergencyProtectedTimelock.t.sol +++ b/test/unit/EmergencyProtectedTimelock.t.sol @@ -45,19 +45,17 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.startPrank(_adminExecutor); _timelock.setGovernance(_dualGovernance); _timelock.setDelays({afterSubmitDelay: Durations.from(3 days), afterScheduleDelay: Durations.from(2 days)}); - _timelock.setupEmergencyProtection( - _emergencyGovernance, - _emergencyActivator, - _emergencyEnactor, - _emergencyProtectionDuration.addTo(Timestamps.now()), - _emergencyModeDuration - ); + _timelock.setEmergencyProtectionActivationCommittee(_emergencyActivator); + _timelock.setEmergencyProtectionExecutionCommittee(_emergencyEnactor); + _timelock.setEmergencyProtectionEndDate(_emergencyProtectionDuration.addTo(Timestamps.now())); + _timelock.setEmergencyModeDuration(_emergencyModeDuration); + _timelock.setEmergencyGovernance(_emergencyGovernance); vm.stopPrank(); } // EmergencyProtectedTimelock.submit() - function testFuzz_stranger_cannot_submit_proposal(address stranger) external { + function testFuzz_Submit_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _dualGovernance); vm.prank(stranger); @@ -66,7 +64,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_timelock.getProposalsCount(), 0); } - function test_governance_can_submit_proposal() external { + function test_SubmitProposal() external { vm.prank(_dualGovernance); _timelock.submit(_adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock))); @@ -78,7 +76,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.schedule() - function test_governance_can_schedule_proposal() external { + function test_ScheduleProposal() external { _submitProposal(); assertEq(_timelock.getProposalsCount(), 1); @@ -91,7 +89,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(proposal.status, ProposalStatus.Scheduled); } - function testFuzz_stranger_cannot_schedule_proposal(address stranger) external { + function testFuzz_ScheduleProposal_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _dualGovernance); vm.assume(stranger != address(0)); @@ -108,7 +106,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.execute() - function testFuzz_anyone_can_execute_proposal(address stranger) external { + function testFuzz_ExecuteProposal(address stranger) external { vm.assume(stranger != _dualGovernance); vm.assume(stranger != address(0)); @@ -128,7 +126,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(proposal.status, ProposalStatus.Executed); } - function test_cannot_execute_proposal_if_emergency_mode_active() external { + function test_ExecuteProposal_RevertOn_EmergencyModeIsActive() external { _submitProposal(); assertEq(_timelock.getProposalsCount(), 1); @@ -149,7 +147,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.cancelAllNonExecutedProposals() - function test_governance_can_cancel_all_non_executed_proposals() external { + function test_CancelAllNonExecutedProposals() external { _submitProposal(); _submitProposal(); @@ -176,7 +174,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(proposal2.status, ProposalStatus.Cancelled); } - function testFuzz_stranger_cannot_cancel_all_non_executed_proposals(address stranger) external { + function testFuzz_CancelAllNonExecutedProposals_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _dualGovernance); vm.assume(stranger != address(0)); @@ -188,7 +186,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.transferExecutorOwnership() - function testFuzz_admin_executor_can_transfer_executor_ownership(address newOwner) external { + function testFuzz_TransferExecutorOwnership(address newOwner) external { vm.assume(newOwner != _adminExecutor); vm.assume(newOwner != address(0)); @@ -206,7 +204,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(executor.owner(), newOwner); } - function test_stranger_cannot_transfer_executor_ownership(address stranger) external { + function test_TransferExecutorOwnership_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _adminExecutor); vm.prank(stranger); @@ -216,7 +214,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.setGovernance() - function testFuzz_admin_executor_can_set_governance(address newGovernance) external { + function testFuzz_SetGovernance(address newGovernance) external { vm.assume(newGovernance != _dualGovernance); vm.assume(newGovernance != address(0)); @@ -234,23 +232,22 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(entries.length, 1); } - function test_cannot_set_governance_to_zero() external { + function test_SetGovernance_RevertOn_ZeroAddress() external { vm.prank(_adminExecutor); vm.expectRevert(abi.encodeWithSelector(TimelockState.InvalidGovernance.selector, address(0))); _timelock.setGovernance(address(0)); } - // TODO: Update test after the convention about return/revert is resolved - // function test_cannot_set_governance_to_the_same_address() external { - // address currentGovernance = _timelock.getGovernance(); - // vm.prank(_adminExecutor); - // vm.expectRevert(abi.encodeWithSelector(TimelockState.InvalidGovernance.selector, _dualGovernance)); - // _timelock.setGovernance(currentGovernance); + function test_SetGovernance_RevertOn_SameAddress() external { + address currentGovernance = _timelock.getGovernance(); + vm.prank(_adminExecutor); + vm.expectRevert(abi.encodeWithSelector(TimelockState.InvalidGovernance.selector, _dualGovernance)); + _timelock.setGovernance(currentGovernance); - // assertEq(_timelock.getGovernance(), currentGovernance); - // } + assertEq(_timelock.getGovernance(), currentGovernance); + } - function testFuzz_stranger_cannot_set_governance(address stranger) external { + function testFuzz_SetGovernance_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _adminExecutor); vm.prank(stranger); @@ -260,14 +257,14 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.activateEmergencyMode() - function test_emergency_activator_can_activate_emergency_mode() external { + function test_ActivateEmergencyMode() external { vm.prank(_emergencyActivator); _timelock.activateEmergencyMode(); assertEq(_isEmergencyStateActivated(), true); } - function testFuzz_stranger_cannot_activate_emergency_mode(address stranger) external { + function testFuzz_ActivateEmergencyMode_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _emergencyActivator); vm.assume(stranger != address(0)); @@ -280,7 +277,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_isEmergencyStateActivated(), false); } - function test_cannot_activate_emergency_mode_if_already_active() external { + function test_ActivateEmergencyMode_RevertOn_AlreadyActive() external { _activateEmergencyMode(); assertEq(_isEmergencyStateActivated(), true); @@ -294,7 +291,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.emergencyExecute() - function test_emergency_executior_can_execute_proposal() external { + function test_EmergencyExecute_ByEmergencyExecutor() external { _submitProposal(); assertEq(_timelock.getProposalsCount(), 1); @@ -316,7 +313,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(proposal.status, ProposalStatus.Executed); } - function test_cannot_emergency_execute_proposal_if_mode_not_activated() external { + function test_EmergencyExecute_RevertOn_ModeNotActive() external { vm.startPrank(_dualGovernance); _timelock.submit(_adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock))); @@ -335,7 +332,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _timelock.emergencyExecute(1); } - function testFuzz_stranger_cannot_emergency_execute_proposal(address stranger) external { + function testFuzz_EmergencyExecute_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _emergencyEnactor); vm.assume(stranger != address(0)); @@ -362,7 +359,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.deactivateEmergencyMode() - function test_admin_executor_can_deactivate_emergency_mode_if_delay_not_passed() external { + function test_DeactivateEmergencyMode_ByAdminExecutor_WhileModeActive() external { _submitProposal(); _activateEmergencyMode(); @@ -372,7 +369,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_isEmergencyStateActivated(), false); } - function test_after_deactivation_all_proposals_are_cancelled() external { + function test_DeactivateEmergencyMode_AllProposalsCancelled() external { _submitProposal(); assertEq(_timelock.getProposalsCount(), 1); @@ -388,7 +385,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(proposal.status, ProposalStatus.Cancelled); } - function testFuzz_stranger_can_deactivate_emergency_mode_if_passed(address stranger) external { + function testFuzz_DeactivateEmergencyMode_ByStranger_ModeExpired(address stranger) external { vm.assume(stranger != _adminExecutor); _activateEmergencyMode(); @@ -404,7 +401,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_isEmergencyStateActivated(), false); } - function testFuzz_cannot_deactivate_emergency_mode_if_not_activated(address stranger) external { + function testFuzz_DeactivateEmergencyMode_RevertOn_ModeNotActivated(address stranger) external { vm.assume(stranger != _adminExecutor); vm.prank(stranger); @@ -416,7 +413,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _timelock.deactivateEmergencyMode(); } - function testFuzz_stranger_cannot_deactivate_emergency_mode_if_not_passed(address stranger) external { + function testFuzz_DeactivateEmergencyMode_RevertOn_ByStranger_ModeNotExpired(address stranger) external { vm.assume(stranger != _adminExecutor); _activateEmergencyMode(); @@ -429,7 +426,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.emergencyReset() - function test_execution_committee_can_emergency_reset() external { + function test_EmergencyReset_ByExecutionCommittee() external { _activateEmergencyMode(); assertEq(_isEmergencyStateActivated(), true); assertEq(_timelock.isEmergencyProtectionEnabled(), true); @@ -450,7 +447,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(newState.emergencyModeEndsAfter, Timestamps.ZERO); } - function test_after_emergency_reset_all_proposals_are_cancelled() external { + function test_EmergencyReset_AllProposalsCancelled() external { _submitProposal(); _activateEmergencyMode(); @@ -464,7 +461,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(proposal.status, ProposalStatus.Cancelled); } - function testFuzz_stranger_cannot_emergency_reset_governance(address stranger) external { + function testFuzz_EmergencyReset_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _emergencyEnactor); vm.assume(stranger != address(0)); @@ -481,7 +478,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_isEmergencyStateActivated(), true); } - function test_cannot_emergency_reset_if_emergency_mode_not_activated() external { + function test_EmergencyReset_RevertOn_ModeNotActivated() external { assertEq(_isEmergencyStateActivated(), false); EmergencyProtection.Context memory state = _timelock.getEmergencyProtectionContext(); @@ -500,53 +497,119 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertFalse(_timelock.isEmergencyModeActive()); } - // EmergencyProtectedTimelock.setupEmergencyProtection() + // EmergencyProtectedTimelock.setEmergencyProtectionActivationCommittee() - function test_admin_executor_can_set_emenrgency_protection() external { + function test_SetActivationCommittee() external { EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); - vm.prank(_adminExecutor); - _localTimelock.setupEmergencyProtection( - _emergencyGovernance, - _emergencyActivator, - _emergencyEnactor, - _emergencyProtectionDuration.addTo(Timestamps.now()), - _emergencyModeDuration - ); + vm.startPrank(_adminExecutor); + _localTimelock.setEmergencyProtectionActivationCommittee(_emergencyActivator); + vm.stopPrank(); EmergencyProtection.Context memory state = _timelock.getEmergencyProtectionContext(); assertEq(state.emergencyActivationCommittee, _emergencyActivator); - assertEq(state.emergencyExecutionCommittee, _emergencyEnactor); - assertEq(state.emergencyProtectionEndsAfter, _emergencyProtectionDuration.addTo(Timestamps.now())); - assertEq(state.emergencyModeDuration, _emergencyModeDuration); - assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); assertFalse(_timelock.isEmergencyModeActive()); } - function testFuzz_stranger_cannot_set_emergency_protection(address stranger) external { + function testFuzz_SetActivationCommittee_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _adminExecutor); - vm.assume(stranger != address(0)); - EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); - vm.prank(stranger); vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, stranger)); - _localTimelock.setupEmergencyProtection( - _emergencyGovernance, - _emergencyActivator, - _emergencyEnactor, - _emergencyProtectionDuration.addTo(Timestamps.now()), - _emergencyModeDuration - ); + vm.prank(stranger); + _localTimelock.setEmergencyProtectionActivationCommittee(_emergencyActivator); EmergencyProtection.Context memory state = _localTimelock.getEmergencyProtectionContext(); assertEq(state.emergencyActivationCommittee, address(0)); + assertFalse(_localTimelock.isEmergencyModeActive()); + } + + // EmergencyProtectedTimelock.setEmergencyProtectionExecutionCommittee() + + function test_SetExecutionCommittee() external { + EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); + + vm.startPrank(_adminExecutor); + _localTimelock.setEmergencyProtectionExecutionCommittee(_emergencyEnactor); + vm.stopPrank(); + + EmergencyProtection.Context memory state = _localTimelock.getEmergencyProtectionContext(); + + assertEq(state.emergencyExecutionCommittee, _emergencyEnactor); + assertFalse(_localTimelock.isEmergencyModeActive()); + } + + function testFuzz_SetExecutionCommittee_RevertOn_ByStranger(address stranger) external { + vm.assume(stranger != _adminExecutor); + EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); + + vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, stranger)); + vm.prank(stranger); + _localTimelock.setEmergencyProtectionExecutionCommittee(_emergencyEnactor); + + EmergencyProtection.Context memory state = _localTimelock.getEmergencyProtectionContext(); + assertEq(state.emergencyExecutionCommittee, address(0)); + assertFalse(_localTimelock.isEmergencyModeActive()); + } + + // EmergencyProtectedTimelock.setEmergencyProtectionEndDate() + + function test_SetProtectionEndDate() external { + EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); + + vm.startPrank(_adminExecutor); + _localTimelock.setEmergencyProtectionEndDate(_emergencyProtectionDuration.addTo(Timestamps.now())); + vm.stopPrank(); + + EmergencyProtection.Context memory state = _localTimelock.getEmergencyProtectionContext(); + + assertEq(state.emergencyProtectionEndsAfter, _emergencyProtectionDuration.addTo(Timestamps.now())); + assertFalse(_localTimelock.isEmergencyModeActive()); + } + + function testFuzz_SetProtectionEndDate_RevertOn_ByStranger(address stranger) external { + vm.assume(stranger != _adminExecutor); + EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); + + vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, stranger)); + vm.prank(stranger); + _localTimelock.setEmergencyProtectionEndDate(_emergencyProtectionDuration.addTo(Timestamps.now())); + + EmergencyProtection.Context memory state = _localTimelock.getEmergencyProtectionContext(); + assertEq(state.emergencyProtectionEndsAfter, Timestamps.ZERO); + assertFalse(_localTimelock.isEmergencyModeActive()); + } + + // EmergencyProtectedTimelock.setEmergencyModeDuration() + + function test_SetModeDuration() external { + EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); + + vm.startPrank(_adminExecutor); + _localTimelock.setEmergencyModeDuration(_emergencyModeDuration); + vm.stopPrank(); + + EmergencyProtection.Context memory state = _localTimelock.getEmergencyProtectionContext(); + + assertEq(state.emergencyModeDuration, _emergencyModeDuration); + assertFalse(_localTimelock.isEmergencyModeActive()); + } + + function testFuzz_SetModeDuration_RevertOn_ByStranger(address stranger) external { + vm.assume(stranger != _adminExecutor); + EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); + + vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, stranger)); + vm.prank(stranger); + _localTimelock.setEmergencyModeDuration(_emergencyModeDuration); + + EmergencyProtection.Context memory state = _localTimelock.getEmergencyProtectionContext(); + assertEq(state.emergencyModeDuration, Durations.ZERO); - assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); assertFalse(_localTimelock.isEmergencyModeActive()); } @@ -557,14 +620,12 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_localTimelock.isEmergencyProtectionEnabled(), false); - vm.prank(_adminExecutor); - _localTimelock.setupEmergencyProtection( - _emergencyGovernance, - _emergencyActivator, - _emergencyEnactor, - _emergencyProtectionDuration.addTo(Timestamps.now()), - _emergencyModeDuration - ); + vm.startPrank(_adminExecutor); + _localTimelock.setEmergencyProtectionActivationCommittee(_emergencyActivator); + _localTimelock.setEmergencyProtectionExecutionCommittee(_emergencyEnactor); + _localTimelock.setEmergencyProtectionEndDate(_emergencyProtectionDuration.addTo(Timestamps.now())); + _localTimelock.setEmergencyModeDuration(_emergencyModeDuration); + vm.stopPrank(); assertEq(_localTimelock.isEmergencyProtectionEnabled(), true); @@ -584,14 +645,13 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_localTimelock.isEmergencyProtectionEnabled(), false); - vm.prank(_adminExecutor); - _localTimelock.setupEmergencyProtection( - _emergencyGovernance, - _emergencyActivator, - _emergencyEnactor, - _emergencyProtectionDuration.addTo(Timestamps.now()), - _emergencyModeDuration - ); + vm.startPrank(_adminExecutor); + _localTimelock.setEmergencyProtectionActivationCommittee(_emergencyActivator); + _localTimelock.setEmergencyProtectionExecutionCommittee(_emergencyEnactor); + _localTimelock.setEmergencyProtectionEndDate(_emergencyProtectionDuration.addTo(Timestamps.now())); + _localTimelock.setEmergencyModeDuration(_emergencyModeDuration); + _localTimelock.setEmergencyGovernance(_emergencyGovernance); + vm.stopPrank(); assertEq(_localTimelock.isEmergencyProtectionEnabled(), true); @@ -620,14 +680,12 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(state.emergencyModeDuration, Durations.ZERO); assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); - vm.prank(_adminExecutor); - _localTimelock.setupEmergencyProtection( - _emergencyGovernance, - _emergencyActivator, - _emergencyEnactor, - _emergencyProtectionDuration.addTo(Timestamps.now()), - _emergencyModeDuration - ); + vm.startPrank(_adminExecutor); + _localTimelock.setEmergencyProtectionActivationCommittee(_emergencyActivator); + _localTimelock.setEmergencyProtectionExecutionCommittee(_emergencyEnactor); + _localTimelock.setEmergencyProtectionEndDate(_emergencyProtectionDuration.addTo(Timestamps.now())); + _localTimelock.setEmergencyModeDuration(_emergencyModeDuration); + vm.stopPrank(); state = _localTimelock.getEmergencyProtectionContext(); @@ -666,14 +724,13 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { function test_get_emergency_state_reset() external { EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); - vm.prank(_adminExecutor); - _localTimelock.setupEmergencyProtection( - _emergencyGovernance, - _emergencyActivator, - _emergencyEnactor, - _emergencyProtectionDuration.addTo(Timestamps.now()), - _emergencyModeDuration - ); + vm.startPrank(_adminExecutor); + _localTimelock.setEmergencyProtectionActivationCommittee(_emergencyActivator); + _localTimelock.setEmergencyProtectionExecutionCommittee(_emergencyEnactor); + _localTimelock.setEmergencyProtectionEndDate(_emergencyProtectionDuration.addTo(Timestamps.now())); + _localTimelock.setEmergencyModeDuration(_emergencyModeDuration); + _localTimelock.setEmergencyGovernance(_emergencyGovernance); + vm.stopPrank(); vm.prank(_emergencyActivator); _localTimelock.activateEmergencyMode(); diff --git a/test/utils/SetupDeployment.sol b/test/utils/SetupDeployment.sol index c6464c6f..b6e84ded 100644 --- a/test/utils/SetupDeployment.sol +++ b/test/utils/SetupDeployment.sol @@ -260,16 +260,30 @@ abstract contract SetupDeployment is Test { address(_timelock), 0, abi.encodeCall( - _timelock.setupEmergencyProtection, - ( - address(_emergencyGovernance), - address(_emergencyActivationCommittee), - address(_emergencyExecutionCommittee), - _EMERGENCY_PROTECTION_DURATION.addTo(Timestamps.now()), - _EMERGENCY_MODE_DURATION - ) + _timelock.setEmergencyProtectionActivationCommittee, (address(_emergencyActivationCommittee)) ) ); + _adminExecutor.execute( + address(_timelock), + 0, + abi.encodeCall( + _timelock.setEmergencyProtectionExecutionCommittee, (address(_emergencyExecutionCommittee)) + ) + ); + _adminExecutor.execute( + address(_timelock), + 0, + abi.encodeCall( + _timelock.setEmergencyProtectionEndDate, (_EMERGENCY_PROTECTION_DURATION.addTo(Timestamps.now())) + ) + ); + _adminExecutor.execute( + address(_timelock), 0, abi.encodeCall(_timelock.setEmergencyModeDuration, (_EMERGENCY_MODE_DURATION)) + ); + + _adminExecutor.execute( + address(_timelock), 0, abi.encodeCall(_timelock.setEmergencyGovernance, (address(_emergencyGovernance))) + ); } } diff --git a/test/utils/executor-calls.sol b/test/utils/executor-calls.sol index c5ca8600..610d6035 100644 --- a/test/utils/executor-calls.sol +++ b/test/utils/executor-calls.sol @@ -35,6 +35,27 @@ library ExternalCallHelpers { } } + function create(ExternalCall[5] memory calls) internal pure returns (ExternalCall[] memory res) { + res = new ExternalCall[](5); + for (uint256 i = 0; i < 5; ++i) { + res[i] = calls[i]; + } + } + + function create(ExternalCall[6] memory calls) internal pure returns (ExternalCall[] memory res) { + res = new ExternalCall[](6); + for (uint256 i = 0; i < 6; ++i) { + res[i] = calls[i]; + } + } + + function create(ExternalCall[7] memory calls) internal pure returns (ExternalCall[] memory res) { + res = new ExternalCall[](7); + for (uint256 i = 0; i < 7; ++i) { + res[i] = calls[i]; + } + } + // calls with value equal to 0 function create( @@ -79,6 +100,39 @@ library ExternalCallHelpers { } } + function create( + address[5] memory targets, + bytes[5] memory payloads + ) internal pure returns (ExternalCall[] memory res) { + res = new ExternalCall[](5); + for (uint256 i = 0; i < 5; ++i) { + res[i].target = targets[i]; + res[i].payload = payloads[i]; + } + } + + function create( + address[6] memory targets, + bytes[6] memory payloads + ) internal pure returns (ExternalCall[] memory res) { + res = new ExternalCall[](6); + for (uint256 i = 0; i < 6; ++i) { + res[i].target = targets[i]; + res[i].payload = payloads[i]; + } + } + + function create( + address[7] memory targets, + bytes[7] memory payloads + ) internal pure returns (ExternalCall[] memory res) { + res = new ExternalCall[](7); + for (uint256 i = 0; i < 7; ++i) { + res[i].target = targets[i]; + res[i].payload = payloads[i]; + } + } + function create( address[10] memory targets, bytes[10] memory payloads From e27afc4616bde3b431f4c456132e17b55e015036 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Fri, 16 Aug 2024 15:14:40 +0300 Subject: [PATCH 11/42] feat: change hash consensus errors handling --- contracts/committees/HashConsensus.sol | 6 +++++- test/unit/HashConsensus.t.sol | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/contracts/committees/HashConsensus.sol b/contracts/committees/HashConsensus.sol index 31825a1b..57387243 100644 --- a/contracts/committees/HashConsensus.sol +++ b/contracts/committees/HashConsensus.sol @@ -23,6 +23,7 @@ abstract contract HashConsensus is Ownable { error HashAlreadyUsed(bytes32 hash); error QuorumIsNotReached(); error InvalidQuorum(); + error InvalidTimelockDuration(uint256 timelock); error TimelockNotPassed(); struct HashState { @@ -155,6 +156,9 @@ abstract contract HashConsensus is Ownable { /// @param timelock The new timelock duration in seconds function setTimelockDuration(uint256 timelock) public { _checkOwner(); + if (timelock == timelockDuration) { + revert InvalidTimelockDuration(timelock); + } timelockDuration = timelock; emit TimelockDurationSet(timelock); } @@ -171,7 +175,7 @@ abstract contract HashConsensus is Ownable { /// @dev The quorum value must be greater than zero and not exceed the current number of members. /// @param executionQuorum The new quorum value to be set. function _setQuorum(uint256 executionQuorum) internal { - if (executionQuorum == 0 || executionQuorum > _members.length()) { + if (executionQuorum == 0 || executionQuorum > _members.length() || executionQuorum == quorum) { revert InvalidQuorum(); } quorum = executionQuorum; diff --git a/test/unit/HashConsensus.t.sol b/test/unit/HashConsensus.t.sol index c9f22039..966c7a52 100644 --- a/test/unit/HashConsensus.t.sol +++ b/test/unit/HashConsensus.t.sol @@ -260,6 +260,16 @@ abstract contract HashConsensusUnitTest is UnitTest { _hashConsensus.setTimelockDuration(newTimelockDuration); } + function test_setTimelockDurationRevertsIfValueIsSame() public { + uint256 newTimelockDuration = 300; + + vm.startPrank(_owner); + _hashConsensus.setTimelockDuration(newTimelockDuration); + + vm.expectRevert(abi.encodeWithSelector(HashConsensus.InvalidTimelockDuration.selector, newTimelockDuration)); + _hashConsensus.setTimelockDuration(newTimelockDuration); + } + function testTimelockDurationEventEmitted() public { uint256 newTimelockDuration = 300; @@ -307,6 +317,16 @@ abstract contract HashConsensusUnitTest is UnitTest { _hashConsensus.setQuorum(invalidQuorum); } + function test_setQuorumRevertsIfQuorumIsSame() public { + uint256 invalidQuorum = 2; + + vm.startPrank(_owner); + _hashConsensus.setQuorum(invalidQuorum); + + vm.expectRevert(abi.encodeWithSignature("InvalidQuorum()")); + _hashConsensus.setQuorum(invalidQuorum); + } + function test_quorumEventEmitted() public { uint256 newQuorum = 3; From 781a48dd41284c8d8f06cdd670d8c04a37ed6e91 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Fri, 16 Aug 2024 15:35:49 +0300 Subject: [PATCH 12/42] fix: solidity warnings fixes --- test/scenario/escrow.t.sol | 3 +-- test/scenario/happy-path-plan-b.t.sol | 2 +- test/scenario/proposal-deployment-modes.t.sol | 2 +- test/utils/scenario-test-blueprint.sol | 15 ++++----------- 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index badee1ef..b391b6ad 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -201,7 +201,6 @@ contract EscrowHappyPath is ScenarioTestBlueprint { } function test_check_finalization() public { - uint256 totalAmountLocked = 2 ether; uint256[] memory amounts = new uint256[](2); for (uint256 i = 0; i < 2; ++i) { amounts[i] = 1 ether; @@ -345,7 +344,7 @@ contract EscrowHappyPath is ScenarioTestBlueprint { // unstETH holders claim their withdrawal requests // --- { - uint256[] memory hints = + hints = _lido.withdrawalQueue.findCheckpointHints(unstETHIds, 1, _lido.withdrawalQueue.getLastCheckpointIndex()); escrow.claimUnstETH(unstETHIds, hints); diff --git a/test/scenario/happy-path-plan-b.t.sol b/test/scenario/happy-path-plan-b.t.sol index 3c3170fd..ce38c98a 100644 --- a/test/scenario/happy-path-plan-b.t.sol +++ b/test/scenario/happy-path-plan-b.t.sol @@ -246,7 +246,7 @@ contract PlanBSetup is ScenarioTestBlueprint { assertFalse(_timelock.isEmergencyModeActive()); - EmergencyProtection.Context memory emergencyState = _timelock.getEmergencyProtectionContext(); + emergencyState = _timelock.getEmergencyProtectionContext(); assertEq(emergencyState.emergencyActivationCommittee, address(_emergencyActivationCommittee)); assertEq(emergencyState.emergencyExecutionCommittee, address(_emergencyExecutionCommittee)); assertEq(emergencyState.emergencyModeDuration, Durations.from(30 days)); diff --git a/test/scenario/proposal-deployment-modes.t.sol b/test/scenario/proposal-deployment-modes.t.sol index 93d95e69..663ae551 100644 --- a/test/scenario/proposal-deployment-modes.t.sol +++ b/test/scenario/proposal-deployment-modes.t.sol @@ -90,7 +90,7 @@ contract ProposalDeploymentModesTest is ScenarioTestBlueprint { function test_protected_deployment_mode_deactivation_in_emergency_mode() external { _deployDualGovernanceSetup(true); - (uint256 proposalId, ExternalCall[] memory regularStaffCalls) = _createAndAssertProposal(); + (uint256 proposalId,) = _createAndAssertProposal(); _wait(_timelock.getAfterSubmitDelay().dividedBy(2)); diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index 5bb066dc..4cc21da6 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -124,13 +124,6 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { _lido.submitWstETH(account, _lido.calcSharesToDepositFromPercentageOfTVL(tvlPercentage)); } - function _submitStETH( - address account, - uint256 amountToMint - ) internal returns (uint256 sharesMinted, uint256 amountMinted) { - _lido.submitStETH(account, amountToMint); - } - function _getBalances(address vetoer) internal view returns (Balances memory balances) { uint256 stETHAmount = _lido.stETH.balanceOf(vetoer); uint256 wstETHShares = _lido.wstETH.balanceOf(vetoer); @@ -469,7 +462,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { // --- // Logging and Debugging // --- - function _logVetoSignallingState() internal { + function _logVetoSignallingState() internal view { /* solhint-disable no-console */ (bool isActive, uint256 duration, uint256 activatedAt, uint256 enteredAt) = _getVetoSignallingState(); @@ -494,7 +487,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { /* solhint-enable no-console */ } - function _logVetoSignallingDeactivationState() internal { + function _logVetoSignallingDeactivationState() internal view { /* solhint-disable no-console */ (bool isActive, uint256 duration, uint256 enteredAt) = _getVetoSignallingDeactivationState(); @@ -525,7 +518,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { // Utils Methods // --- - function _step(string memory text) internal { + function _step(string memory text) internal view { // solhint-disable-next-line console.log(string.concat(">>> ", text, " <<<")); } @@ -576,7 +569,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { uint256 _seconds; } - function _toDuration(uint256 timestamp) internal view returns (DurationStruct memory duration) { + function _toDuration(uint256 timestamp) internal pure returns (DurationStruct memory duration) { duration._days = timestamp / 1 days; duration._hours = (timestamp - 1 days * duration._days) / 1 hours; duration._minutes = (timestamp - 1 days * duration._days - 1 hours * duration._hours) / 1 minutes; From 04a34edcf44fed3bae595fab86761e3d6e9dba9d Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 19 Aug 2024 04:26:07 +0400 Subject: [PATCH 13/42] Align cancelAllPendingProposals method with specification --- contracts/DualGovernance.sol | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 5fb93df8..26287f2c 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -42,6 +42,8 @@ contract DualGovernance is IDualGovernance { // Events // --- + event CancelAllPendingProposalsSkipped(); + event CancelAllPendingProposalsExecuted(); event EscrowMasterCopyDeployed(address escrowMasterCopy); event ConfigProviderSet(IDualGovernanceConfigProvider newConfigProvider); @@ -138,11 +140,28 @@ contract DualGovernance is IDualGovernance { } function cancelAllPendingProposals() external { + _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); + Proposers.Proposer memory proposer = _proposers.getProposer(msg.sender); if (proposer.executor != TIMELOCK.getAdminExecutor()) { revert NotAdminProposer(); } + + State currentState = _stateMachine.getCurrentState(); + if (currentState != State.VetoSignalling && currentState != State.VetoSignallingDeactivation) { + /// @dev Early return to prevent "hanging" cancelPendingProposals() requests that could become unexpectedly + /// executable in the future. + /// + /// Some proposer contracts, such as Aragon Voting, may not support canceling already-consensed decisions. + /// This could lead to situations where a proposer’s cancelAllPendingProposals() call becomes unexecutable + /// if the Dual Governance state changes. However, it could become executable again if the system state + /// reverts to VetoSignalling or VetoSignallingDeactivation. + emit CancelAllPendingProposalsSkipped(); + return; + } + TIMELOCK.cancelAllNonExecutedProposals(); + emit CancelAllPendingProposalsExecuted(); } function canSubmitProposal() public view returns (bool) { From 962ccb9cf400e63e9d75b2832fb863adf6b2c58a Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Tue, 20 Aug 2024 12:09:17 +0300 Subject: [PATCH 14/42] fix: review fixes --- contracts/EmergencyProtectedTimelock.sol | 4 +- contracts/libraries/EmergencyProtection.sol | 8 +- test/unit/EmergencyProtectedTimelock.t.sol | 101 +++++++++++------- test/unit/libraries/EmergencyProtection.t.sol | 4 +- test/utils/SetupDeployment.sol | 2 +- 5 files changed, 72 insertions(+), 47 deletions(-) diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index 3bf9a161..7dc7bad7 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -109,7 +109,7 @@ contract EmergencyProtectedTimelock is ITimelock { _timelockState.setGovernance(newGovernance); } - function setDelays(Duration afterSubmitDelay, Duration afterScheduleDelay) external { + function setupDelays(Duration afterSubmitDelay, Duration afterScheduleDelay) external { _checkCallerIsAdminExecutor(); _timelockState.setAfterSubmitDelay(afterSubmitDelay, MAX_AFTER_SUBMIT_DELAY); _timelockState.setAfterScheduleDelay(afterScheduleDelay, MAX_AFTER_SCHEDULE_DELAY); @@ -126,7 +126,7 @@ contract EmergencyProtectedTimelock is ITimelock { // --- // Emergency Protection Functionality - // + // --- /// @dev Sets the emergency activation committee address. /// @param emergencyActivationCommittee The address of the emergency activation committee. diff --git a/contracts/libraries/EmergencyProtection.sol b/contracts/libraries/EmergencyProtection.sol index 167ed630..582d3fb7 100644 --- a/contracts/libraries/EmergencyProtection.sol +++ b/contracts/libraries/EmergencyProtection.sol @@ -18,8 +18,8 @@ library EmergencyProtection { error InvalidEmergencyProtectionEndDate(Timestamp value); error UnexpectedEmergencyModeState(bool value); - event EmergencyModeActivated(Timestamp activatedAt); - event EmergencyModeDeactivated(Timestamp deactivatedAt); + event EmergencyModeActivated(); + event EmergencyModeDeactivated(); event EmergencyGovernanceSet(address newEmergencyGovernance); event EmergencyActivationCommitteeSet(address newActivationCommittee); event EmergencyExecutionCommitteeSet(address newActivationCommittee); @@ -55,7 +55,7 @@ library EmergencyProtection { } self.emergencyModeEndsAfter = self.emergencyModeDuration.addTo(now_); - emit EmergencyModeActivated(Timestamps.now()); + emit EmergencyModeActivated(); } /// @dev Deactivates the emergency mode. @@ -66,7 +66,7 @@ library EmergencyProtection { self.emergencyProtectionEndsAfter = Timestamps.ZERO; self.emergencyModeEndsAfter = Timestamps.ZERO; self.emergencyModeDuration = Durations.ZERO; - emit EmergencyModeDeactivated(Timestamps.now()); + emit EmergencyModeDeactivated(); } // --- diff --git a/test/unit/EmergencyProtectedTimelock.t.sol b/test/unit/EmergencyProtectedTimelock.t.sol index 95d9feac..dc208951 100644 --- a/test/unit/EmergencyProtectedTimelock.t.sol +++ b/test/unit/EmergencyProtectedTimelock.t.sol @@ -44,7 +44,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.startPrank(_adminExecutor); _timelock.setGovernance(_dualGovernance); - _timelock.setDelays({afterSubmitDelay: Durations.from(3 days), afterScheduleDelay: Durations.from(2 days)}); + _timelock.setupDelays({afterSubmitDelay: Durations.from(3 days), afterScheduleDelay: Durations.from(2 days)}); _timelock.setEmergencyProtectionActivationCommittee(_emergencyActivator); _timelock.setEmergencyProtectionExecutionCommittee(_emergencyEnactor); _timelock.setEmergencyProtectionEndDate(_emergencyProtectionDuration.addTo(Timestamps.now())); @@ -55,7 +55,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.submit() - function testFuzz_Submit_RevertOn_ByStranger(address stranger) external { + function testFuzz_submit_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _dualGovernance); vm.prank(stranger); @@ -64,7 +64,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_timelock.getProposalsCount(), 0); } - function test_SubmitProposal() external { + function test_submit_HappyPath() external { vm.prank(_dualGovernance); _timelock.submit(_adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock))); @@ -76,7 +76,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.schedule() - function test_ScheduleProposal() external { + function test_schedule_HappyPath() external { _submitProposal(); assertEq(_timelock.getProposalsCount(), 1); @@ -89,7 +89,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(proposal.status, ProposalStatus.Scheduled); } - function testFuzz_ScheduleProposal_RevertOn_ByStranger(address stranger) external { + function testFuzz_schedule_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _dualGovernance); vm.assume(stranger != address(0)); @@ -106,7 +106,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.execute() - function testFuzz_ExecuteProposal(address stranger) external { + function testFuzz_execute_HappyPath(address stranger) external { vm.assume(stranger != _dualGovernance); vm.assume(stranger != address(0)); @@ -126,7 +126,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(proposal.status, ProposalStatus.Executed); } - function test_ExecuteProposal_RevertOn_EmergencyModeIsActive() external { + function test_execute_RevertOn_EmergencyModeIsActive() external { _submitProposal(); assertEq(_timelock.getProposalsCount(), 1); @@ -147,7 +147,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.cancelAllNonExecutedProposals() - function test_CancelAllNonExecutedProposals() external { + function test_cancelAllNonExecutedProposals_HappyPath() external { _submitProposal(); _submitProposal(); @@ -174,7 +174,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(proposal2.status, ProposalStatus.Cancelled); } - function testFuzz_CancelAllNonExecutedProposals_RevertOn_ByStranger(address stranger) external { + function testFuzz_cancelAllNonExecutedProposals_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _dualGovernance); vm.assume(stranger != address(0)); @@ -184,9 +184,34 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _timelock.cancelAllNonExecutedProposals(); } + function testFuzz_setupDelays_HappyPath(Duration afterSubmitDelay, Duration afterScheduleDelay) external { + vm.assume( + afterSubmitDelay != _timelock.getAfterSubmitDelay() && afterSubmitDelay < _timelock.MAX_AFTER_SUBMIT_DELAY() + ); + vm.assume( + afterScheduleDelay != _timelock.getAfterScheduleDelay() + && afterScheduleDelay < _timelock.MAX_AFTER_SCHEDULE_DELAY() + ); + + vm.prank(_adminExecutor); + _timelock.setupDelays({afterSubmitDelay: afterSubmitDelay, afterScheduleDelay: afterScheduleDelay}); + + assertEq(_timelock.getAfterSubmitDelay(), afterSubmitDelay); + assertEq(_timelock.getAfterScheduleDelay(), afterScheduleDelay); + } + + function test_setupDelays_RevertOn_ByStranger(address stranger) external { + vm.assume(stranger != _adminExecutor); + vm.assume(stranger != address(0)); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, stranger)); + _timelock.setupDelays({afterSubmitDelay: Durations.from(1 days), afterScheduleDelay: Durations.from(1 days)}); + } + // EmergencyProtectedTimelock.transferExecutorOwnership() - function testFuzz_TransferExecutorOwnership(address newOwner) external { + function testFuzz_transferExecutorOwnership_HappyPath(address newOwner) external { vm.assume(newOwner != _adminExecutor); vm.assume(newOwner != address(0)); @@ -204,7 +229,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(executor.owner(), newOwner); } - function test_TransferExecutorOwnership_RevertOn_ByStranger(address stranger) external { + function test_transferExecutorOwnership_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _adminExecutor); vm.prank(stranger); @@ -214,7 +239,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.setGovernance() - function testFuzz_SetGovernance(address newGovernance) external { + function testFuzz_setGovernance_HappyPath(address newGovernance) external { vm.assume(newGovernance != _dualGovernance); vm.assume(newGovernance != address(0)); @@ -232,13 +257,13 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(entries.length, 1); } - function test_SetGovernance_RevertOn_ZeroAddress() external { + function test_setGovernance_RevertOn_ZeroAddress() external { vm.prank(_adminExecutor); vm.expectRevert(abi.encodeWithSelector(TimelockState.InvalidGovernance.selector, address(0))); _timelock.setGovernance(address(0)); } - function test_SetGovernance_RevertOn_SameAddress() external { + function test_setGovernance_RevertOn_SameAddress() external { address currentGovernance = _timelock.getGovernance(); vm.prank(_adminExecutor); vm.expectRevert(abi.encodeWithSelector(TimelockState.InvalidGovernance.selector, _dualGovernance)); @@ -247,7 +272,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_timelock.getGovernance(), currentGovernance); } - function testFuzz_SetGovernance_RevertOn_ByStranger(address stranger) external { + function testFuzz_setGovernance_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _adminExecutor); vm.prank(stranger); @@ -257,14 +282,14 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.activateEmergencyMode() - function test_ActivateEmergencyMode() external { + function test_activateEmergencyMode_HappyPath() external { vm.prank(_emergencyActivator); _timelock.activateEmergencyMode(); assertEq(_isEmergencyStateActivated(), true); } - function testFuzz_ActivateEmergencyMode_RevertOn_ByStranger(address stranger) external { + function testFuzz_activateEmergencyMode_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _emergencyActivator); vm.assume(stranger != address(0)); @@ -277,7 +302,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_isEmergencyStateActivated(), false); } - function test_ActivateEmergencyMode_RevertOn_AlreadyActive() external { + function test_activateEmergencyMode_RevertOn_AlreadyActive() external { _activateEmergencyMode(); assertEq(_isEmergencyStateActivated(), true); @@ -291,7 +316,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.emergencyExecute() - function test_EmergencyExecute_ByEmergencyExecutor() external { + function test_emergencyExecute_HappyPath() external { _submitProposal(); assertEq(_timelock.getProposalsCount(), 1); @@ -313,7 +338,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(proposal.status, ProposalStatus.Executed); } - function test_EmergencyExecute_RevertOn_ModeNotActive() external { + function test_emergencyExecute_RevertOn_ModeNotActive() external { vm.startPrank(_dualGovernance); _timelock.submit(_adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock))); @@ -332,7 +357,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _timelock.emergencyExecute(1); } - function testFuzz_EmergencyExecute_RevertOn_ByStranger(address stranger) external { + function testFuzz_emergencyExecute_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _emergencyEnactor); vm.assume(stranger != address(0)); @@ -359,7 +384,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.deactivateEmergencyMode() - function test_DeactivateEmergencyMode_ByAdminExecutor_WhileModeActive() external { + function test_deactivateEmergencyMode_HappyPath() external { _submitProposal(); _activateEmergencyMode(); @@ -369,7 +394,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_isEmergencyStateActivated(), false); } - function test_DeactivateEmergencyMode_AllProposalsCancelled() external { + function test_deactivateEmergencyMode_AllProposalsCancelled() external { _submitProposal(); assertEq(_timelock.getProposalsCount(), 1); @@ -385,7 +410,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(proposal.status, ProposalStatus.Cancelled); } - function testFuzz_DeactivateEmergencyMode_ByStranger_ModeExpired(address stranger) external { + function testFuzz_deactivateEmergencyMode_HappyPath_ByStranger(address stranger) external { vm.assume(stranger != _adminExecutor); _activateEmergencyMode(); @@ -401,7 +426,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_isEmergencyStateActivated(), false); } - function testFuzz_DeactivateEmergencyMode_RevertOn_ModeNotActivated(address stranger) external { + function testFuzz_deactivateEmergencyMode_RevertOn_ModeNotActivated(address stranger) external { vm.assume(stranger != _adminExecutor); vm.prank(stranger); @@ -413,7 +438,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _timelock.deactivateEmergencyMode(); } - function testFuzz_DeactivateEmergencyMode_RevertOn_ByStranger_ModeNotExpired(address stranger) external { + function testFuzz_deactivateEmergencyMode_RevertOn_ByStranger_ModeNotExpired(address stranger) external { vm.assume(stranger != _adminExecutor); _activateEmergencyMode(); @@ -426,7 +451,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.emergencyReset() - function test_EmergencyReset_ByExecutionCommittee() external { + function test_emergencyReset_HappyPath() external { _activateEmergencyMode(); assertEq(_isEmergencyStateActivated(), true); assertEq(_timelock.isEmergencyProtectionEnabled(), true); @@ -447,7 +472,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(newState.emergencyModeEndsAfter, Timestamps.ZERO); } - function test_EmergencyReset_AllProposalsCancelled() external { + function test_emergencyReset_HappyPath_AllProposalsCancelled() external { _submitProposal(); _activateEmergencyMode(); @@ -461,7 +486,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(proposal.status, ProposalStatus.Cancelled); } - function testFuzz_EmergencyReset_RevertOn_ByStranger(address stranger) external { + function testFuzz_emergencyReset_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _emergencyEnactor); vm.assume(stranger != address(0)); @@ -478,7 +503,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_isEmergencyStateActivated(), true); } - function test_EmergencyReset_RevertOn_ModeNotActivated() external { + function test_emergencyReset_RevertOn_ModeNotActivated() external { assertEq(_isEmergencyStateActivated(), false); EmergencyProtection.Context memory state = _timelock.getEmergencyProtectionContext(); @@ -499,7 +524,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.setEmergencyProtectionActivationCommittee() - function test_SetActivationCommittee() external { + function test_setActivationCommittee_HappyPath() external { EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); vm.startPrank(_adminExecutor); @@ -512,7 +537,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertFalse(_timelock.isEmergencyModeActive()); } - function testFuzz_SetActivationCommittee_RevertOn_ByStranger(address stranger) external { + function testFuzz_setActivationCommittee_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _adminExecutor); EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); @@ -528,7 +553,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.setEmergencyProtectionExecutionCommittee() - function test_SetExecutionCommittee() external { + function test_setExecutionCommittee_HappyPath() external { EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); vm.startPrank(_adminExecutor); @@ -541,7 +566,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertFalse(_localTimelock.isEmergencyModeActive()); } - function testFuzz_SetExecutionCommittee_RevertOn_ByStranger(address stranger) external { + function testFuzz_setExecutionCommittee_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _adminExecutor); EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); @@ -557,7 +582,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.setEmergencyProtectionEndDate() - function test_SetProtectionEndDate() external { + function test_setProtectionEndDate_HappyPath() external { EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); vm.startPrank(_adminExecutor); @@ -570,7 +595,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertFalse(_localTimelock.isEmergencyModeActive()); } - function testFuzz_SetProtectionEndDate_RevertOn_ByStranger(address stranger) external { + function testFuzz_setProtectionEndDate_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _adminExecutor); EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); @@ -586,7 +611,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.setEmergencyModeDuration() - function test_SetModeDuration() external { + function test_setModeDuration_HappyPath() external { EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); vm.startPrank(_adminExecutor); @@ -599,7 +624,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertFalse(_localTimelock.isEmergencyModeActive()); } - function testFuzz_SetModeDuration_RevertOn_ByStranger(address stranger) external { + function testFuzz_setModeDuration_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _adminExecutor); EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); diff --git a/test/unit/libraries/EmergencyProtection.t.sol b/test/unit/libraries/EmergencyProtection.t.sol index eb2d8c8c..0f5faf9b 100644 --- a/test/unit/libraries/EmergencyProtection.t.sol +++ b/test/unit/libraries/EmergencyProtection.t.sol @@ -25,7 +25,7 @@ contract EmergencyProtectionTest is UnitTest { function test_ActivateEmergencyMode() external { vm.expectEmit(); - emit EmergencyProtection.EmergencyModeActivated(Timestamps.now()); + emit EmergencyProtection.EmergencyModeActivated(); EmergencyProtection.activateEmergencyMode(ctx); assertTrue(EmergencyProtection.isEmergencyModeActive(ctx)); @@ -50,7 +50,7 @@ contract EmergencyProtectionTest is UnitTest { EmergencyProtection.activateEmergencyMode(ctx); vm.expectEmit(); - emit EmergencyProtection.EmergencyModeDeactivated(Timestamps.now()); + emit EmergencyProtection.EmergencyModeDeactivated(); EmergencyProtection.deactivateEmergencyMode(ctx); assertFalse(EmergencyProtection.isEmergencyModeActive(ctx)); diff --git a/test/utils/SetupDeployment.sol b/test/utils/SetupDeployment.sol index b6e84ded..c90aa848 100644 --- a/test/utils/SetupDeployment.sol +++ b/test/utils/SetupDeployment.sol @@ -289,7 +289,7 @@ abstract contract SetupDeployment is Test { function _finalizeEmergencyProtectedTimelockDeploy(IGovernance governance) internal { _adminExecutor.execute( - address(_timelock), 0, abi.encodeCall(_timelock.setDelays, (_AFTER_SUBMIT_DELAY, _AFTER_SCHEDULE_DELAY)) + address(_timelock), 0, abi.encodeCall(_timelock.setupDelays, (_AFTER_SUBMIT_DELAY, _AFTER_SCHEDULE_DELAY)) ); _adminExecutor.execute(address(_timelock), 0, abi.encodeCall(_timelock.setGovernance, (address(governance)))); _adminExecutor.transferOwnership(address(_timelock)); From 3c3faa86096764b8e0ce1197cda65125bfc32500 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Tue, 20 Aug 2024 15:39:22 +0300 Subject: [PATCH 15/42] fix: sealable calls resume return value fix --- contracts/libraries/SealableCalls.sol | 2 +- test/unit/libraries/SealableCalls.t.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/libraries/SealableCalls.sol b/contracts/libraries/SealableCalls.sol index bff9cc2b..db2738c5 100644 --- a/contracts/libraries/SealableCalls.sol +++ b/contracts/libraries/SealableCalls.sol @@ -63,7 +63,7 @@ library SealableCalls { function callResume(ISealable sealable) internal returns (bool success, bytes memory lowLevelError) { try sealable.resume() { (bool isPausedCallSuccess, bytes memory isPausedLowLevelError, bool isPaused) = callIsPaused(sealable); - success = isPausedCallSuccess && isPaused; + success = isPausedCallSuccess && !isPaused; lowLevelError = isPausedLowLevelError; } catch (bytes memory resumeLowLevelError) { success = false; diff --git a/test/unit/libraries/SealableCalls.t.sol b/test/unit/libraries/SealableCalls.t.sol index d847e50f..12a8771d 100644 --- a/test/unit/libraries/SealableCalls.t.sol +++ b/test/unit/libraries/SealableCalls.t.sol @@ -60,7 +60,7 @@ contract SealableCallsUnitTests is UnitTest { (bool success, bytes memory lowLevelError) = SealableCalls.callResume(_sealableMock); - assertFalse(success); + assertTrue(success); assertEq(lowLevelError.length, 0); (bool isPausedSuccess,, bool isPaused) = SealableCalls.callIsPaused(_sealableMock); From 0f7ca633a2a644f69fdc89e6d62464c3fd935603 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Wed, 21 Aug 2024 11:56:56 +0300 Subject: [PATCH 16/42] fix: withdrawETH reverts on empty array --- contracts/Escrow.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index e604017c..1e3eaf94 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -54,6 +54,7 @@ contract Escrow is IEscrow { error UnfinalizedUnstETHIds(); error NonProxyCallsForbidden(); error BatchesQueueIsNotClosed(); + error EmptyUnstETHIds(); error InvalidBatchSize(uint256 size); error CallerIsNotDualGovernance(address caller); error InvalidHintsLength(uint256 actual, uint256 expected); @@ -364,6 +365,9 @@ contract Escrow is IEscrow { } function withdrawETH(uint256[] calldata unstETHIds) external { + if (unstETHIds.length == 0) { + revert EmptyUnstETHIds(); + } _escrowState.checkRageQuitEscrow(); _escrowState.checkWithdrawalsTimelockPassed(); ETHValue ethToWithdraw = _accounting.accountUnstETHWithdraw(msg.sender, unstETHIds); From 4739b5a1123cdaf94e0c4a4f00131b15524edb5d Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Wed, 21 Aug 2024 11:57:52 +0300 Subject: [PATCH 17/42] fix: lockUnsteth reverts on empty array --- contracts/Escrow.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 1e3eaf94..146eab19 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -184,6 +184,10 @@ contract Escrow is IEscrow { // Lock & unlock unstETH // --- function lockUnstETH(uint256[] memory unstETHIds) external { + if (unstETHIds.length == 0) { + revert EmptyUnstETHIds(); + } + _escrowState.checkSignallingEscrow(); DUAL_GOVERNANCE.activateNextState(); From 44ce534597755dc52c09e43ed2a0e22741fc0c0d Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Wed, 21 Aug 2024 12:28:57 +0300 Subject: [PATCH 18/42] fix: requestNextWithdrawalBatch now closes when balance lower than minimum --- contracts/Escrow.sol | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 146eab19..2c0a0bb7 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -266,7 +266,7 @@ contract Escrow is IEscrow { uint256 maxStETHWithdrawalRequestAmount = WITHDRAWAL_QUEUE.MAX_STETH_WITHDRAWAL_AMOUNT(); if (stETHRemaining < minStETHWithdrawalRequestAmount) { - return _batchesQueue.close(); + _batchesQueue.close(); } uint256[] memory requestAmounts = WithdrawalsBatchesQueue.calcRequestAmounts({ @@ -276,6 +276,12 @@ contract Escrow is IEscrow { }); _batchesQueue.addUnstETHIds(WITHDRAWAL_QUEUE.requestWithdrawals(requestAmounts, address(this))); + + stETHRemaining = ST_ETH.balanceOf(address(this)); + + if (stETHRemaining < minStETHWithdrawalRequestAmount) { + _batchesQueue.close(); + } } // --- From 9dfcfbd38074a65f395e43d9d8ea52a5eb34fc90 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Thu, 22 Aug 2024 15:05:28 +0300 Subject: [PATCH 19/42] fix: proposers lib fix --- contracts/libraries/Proposers.sol | 7 ++- test/unit/libraries/Proposers.t.sol | 78 +++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/contracts/libraries/Proposers.sol b/contracts/libraries/Proposers.sol index fce7f2e0..33024d51 100644 --- a/contracts/libraries/Proposers.sol +++ b/contracts/libraries/Proposers.sol @@ -96,13 +96,16 @@ library Proposers { _checkRegisteredProposer(proposerAccount, executorData); IndexOneBased lastProposerIndex = IndicesOneBased.fromOneBasedValue(self.proposers.length); + IndexOneBased proposerIndex = executorData.proposerIndex; + if (executorData.proposerIndex != lastProposerIndex) { - self.proposers[executorData.proposerIndex.toZeroBasedValue()] = - self.proposers[lastProposerIndex.toZeroBasedValue()]; + self.proposers[proposerIndex.toZeroBasedValue()] = self.proposers[lastProposerIndex.toZeroBasedValue()]; + self.executors[self.proposers[proposerIndex.toZeroBasedValue()]].proposerIndex = proposerIndex; } self.proposers.pop(); delete self.executors[proposerAccount]; + self.executorRefsCounts[executorData.executor] -= 1; emit ProposerUnregistered(proposerAccount, executorData.executor); diff --git a/test/unit/libraries/Proposers.t.sol b/test/unit/libraries/Proposers.t.sol index 6419a6de..7eb2ea42 100644 --- a/test/unit/libraries/Proposers.t.sol +++ b/test/unit/libraries/Proposers.t.sol @@ -175,4 +175,82 @@ contract ProposersLibraryUnitTests is UnitTest { assertEq(allProposers[1].account, _DEFAULT_PROPOSER); assertEq(allProposers[1].executor, _DEFAULT_EXECUTOR); } + + // --- + // Edge cases + // --- + + function test_unregister_Spam() external { + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); + + address dravee = makeAddr("Dravee"); + address draveeExecutor = makeAddr("draveeExecutor"); + address alice = makeAddr("Alice"); + address aliceExecutor = makeAddr("aliceExecutor"); + address celine = makeAddr("Celine"); + address celineExecutor = makeAddr("celineExecutor"); + address bob = makeAddr("Bob"); + address bobExecutor = makeAddr("bobExecutor"); + + _proposers.register(alice, aliceExecutor); + _proposers.register(bob, bobExecutor); + _proposers.register(celine, celineExecutor); + _proposers.register(dravee, draveeExecutor); + + _proposers.unregister(bob); + _proposers.unregister(dravee); + _proposers.unregister(celine); + _proposers.unregister(alice); + } + + function test_unregister_CorrectPosition() external { + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); + + address dravee = makeAddr("Dravee"); + address draveeExecutor = makeAddr("draveeExecutor"); + address alice = makeAddr("Alice"); + address aliceExecutor = makeAddr("aliceExecutor"); + address celine = makeAddr("Celine"); + address celineExecutor = makeAddr("celineExecutor"); + address bob = makeAddr("Bob"); + address bobExecutor = makeAddr("bobExecutor"); + + _proposers.register(alice, aliceExecutor); + _proposers.register(bob, bobExecutor); + _proposers.register(celine, celineExecutor); + _proposers.register(dravee, draveeExecutor); + + _proposers.unregister(bob); + + Proposers.Proposer[] memory allProposers = _proposers.getAllProposers(); + + assertEq(allProposers.length, 4); + assertEq(allProposers[0].account, _ADMIN_PROPOSER); + assertEq(allProposers[1].account, alice); + assertEq(allProposers[1].executor, aliceExecutor); + assertEq(allProposers[2].account, dravee); + assertEq(allProposers[2].executor, draveeExecutor); + assertEq(allProposers[3].account, celine); + assertEq(allProposers[3].executor, celineExecutor); + + _proposers.unregister(alice); + + allProposers = _proposers.getAllProposers(); + + assertEq(allProposers.length, 3); + assertEq(allProposers[0].account, _ADMIN_PROPOSER); + assertEq(allProposers[1].account, celine); + assertEq(allProposers[1].executor, celineExecutor); + assertEq(allProposers[2].account, dravee); + assertEq(allProposers[2].account, dravee); + + _proposers.unregister(dravee); + + allProposers = _proposers.getAllProposers(); + + assertEq(allProposers.length, 2); + assertEq(allProposers[0].account, _ADMIN_PROPOSER); + assertEq(allProposers[1].account, celine); + assertEq(allProposers[1].executor, celineExecutor); + } } From 3515294b66b379ce6e82fc6bbcf40b3278f1f9fb Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Thu, 22 Aug 2024 15:11:52 +0300 Subject: [PATCH 20/42] fix: withdrawal batches fix --- contracts/Escrow.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index d69269c2..6d1190cc 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -266,7 +266,7 @@ contract Escrow is IEscrow { uint256 maxStETHWithdrawalRequestAmount = WITHDRAWAL_QUEUE.MAX_STETH_WITHDRAWAL_AMOUNT(); if (stETHRemaining < minStETHWithdrawalRequestAmount) { - _batchesQueue.close(); + return _batchesQueue.close(); } uint256[] memory requestAmounts = WithdrawalsBatchesQueue.calcRequestAmounts({ From e1018c4ba69eb8732f0fe5b2c11610b63a03c970 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Fri, 23 Aug 2024 15:09:32 +0300 Subject: [PATCH 21/42] proposal scheduling --- contracts/committees/HashConsensus.sol | 23 +++++---- test/unit/HashConsensus.t.sol | 66 +++++++++++++------------- 2 files changed, 44 insertions(+), 45 deletions(-) diff --git a/contracts/committees/HashConsensus.sol b/contracts/committees/HashConsensus.sol index 44534528..b6887fc6 100644 --- a/contracts/committees/HashConsensus.sol +++ b/contracts/committees/HashConsensus.sol @@ -27,7 +27,7 @@ abstract contract HashConsensus is Ownable { error TimelockNotPassed(); struct HashState { - uint40 quorumAt; + uint40 scheduledAt; uint40 usedAt; } @@ -58,7 +58,7 @@ abstract contract HashConsensus is Ownable { uint256 heads = _getSupport(hash); if (heads == quorum - 1 && support == true) { - _hashStates[hash].quorumAt = uint40(block.timestamp); + _hashStates[hash].scheduledAt = uint40(block.timestamp); } approves[msg.sender][hash] = support; @@ -78,7 +78,7 @@ abstract contract HashConsensus is Ownable { if (support == 0 || support < quorum) { revert QuorumIsNotReached(); } - if (block.timestamp < _hashStates[hash].quorumAt + timelockDuration) { + if (block.timestamp < _hashStates[hash].scheduledAt + timelockDuration) { revert TimelockNotPassed(); } @@ -92,16 +92,16 @@ abstract contract HashConsensus is Ownable { /// @param hash The hash to get the state for /// @return support The number of votes in support of the hash /// @return executionQuorum The required number of votes for execution - /// @return quorumAt The timestamp when the quorum was reached + /// @return scheduledAt The timestamp when the quorum was reached or scheduleProposal was called /// @return isUsed Whether the hash has been used function _getHashState(bytes32 hash) internal view - returns (uint256 support, uint256 executionQuorum, uint256 quorumAt, bool isUsed) + returns (uint256 support, uint256 executionQuorum, uint256 scheduledAt, bool isUsed) { support = _getSupport(hash); executionQuorum = quorum; - quorumAt = _hashStates[hash].quorumAt; + scheduledAt = _hashStates[hash].scheduledAt; isUsed = _hashStates[hash].usedAt > 0; } @@ -173,17 +173,16 @@ abstract contract HashConsensus is Ownable { _setQuorum(newQuorum); } - /// @notice Updates the quorum for a given hash if the quorum is reached and not set - /// and the hash has not been used - /// @param hash The hash to update the quorum for - function updateQuorum(bytes32 hash) public { + /// @notice Schedules a proposal for execution if quorum is reached and it has not been scheduled yet. + /// @param hash The hash of the proposal to be scheduled + function scheduleProposal(bytes32 hash) public { if (_hashStates[hash].usedAt > 0) { revert HashAlreadyUsed(hash); } uint256 support = _getSupport(hash); - if (support >= quorum && _hashStates[hash].quorumAt == 0) { - _hashStates[hash].quorumAt = uint40(block.timestamp); + if (support >= quorum && _hashStates[hash].scheduledAt == 0) { + _hashStates[hash].scheduledAt = uint40(block.timestamp); } } diff --git a/test/unit/HashConsensus.t.sol b/test/unit/HashConsensus.t.sol index ed945cdf..1406e734 100644 --- a/test/unit/HashConsensus.t.sol +++ b/test/unit/HashConsensus.t.sol @@ -374,7 +374,7 @@ contract HashConsensusWrapper is HashConsensus { function getHashState(bytes32 hash) public view - returns (uint256 support, uint256 executionQuorum, uint256 quorumAt, bool isExecuted) + returns (uint256 support, uint256 executionQuorum, uint256 scheduledAt, bool isExecuted) { return _getHashState(hash); } @@ -431,56 +431,56 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { function test_getHashState() public { uint256 support; uint256 executionQuorum; - uint256 quorumAt; + uint256 scheduledAt; bool isExecuted; - (support, executionQuorum, quorumAt, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); + (support, executionQuorum, scheduledAt, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); assertEq(support, 0); assertEq(executionQuorum, _quorum); - assertEq(quorumAt, 0); + assertEq(scheduledAt, 0); assertEq(isExecuted, false); uint256 expectedQuorumAt = block.timestamp; for (uint256 i = 0; i < _membersCount; ++i) { - (support, executionQuorum, quorumAt, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); + (support, executionQuorum, scheduledAt, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); assertEq(support, i); assertEq(executionQuorum, _quorum); if (i >= executionQuorum) { - assertEq(quorumAt, expectedQuorumAt); + assertEq(scheduledAt, expectedQuorumAt); } else { - assertEq(quorumAt, 0); + assertEq(scheduledAt, 0); } assertEq(isExecuted, false); vm.prank(_committeeMembers[i]); _hashConsensusWrapper.vote(dataHash, true); - (support, executionQuorum, quorumAt, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); + (support, executionQuorum, scheduledAt, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); assertEq(support, i + 1); assertEq(executionQuorum, _quorum); if (i >= executionQuorum - 1) { - assertEq(quorumAt, expectedQuorumAt); + assertEq(scheduledAt, expectedQuorumAt); } else { - assertEq(quorumAt, 0); + assertEq(scheduledAt, 0); } assertEq(isExecuted, false); } - (support, executionQuorum, quorumAt, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); + (support, executionQuorum, scheduledAt, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); assertEq(support, _membersCount); assertEq(executionQuorum, _quorum); - assertEq(quorumAt, expectedQuorumAt); + assertEq(scheduledAt, expectedQuorumAt); assertEq(isExecuted, false); _wait(_timelock); _hashConsensusWrapper.execute(dataHash); - (support, executionQuorum, quorumAt, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); + (support, executionQuorum, scheduledAt, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); assertEq(support, _membersCount); assertEq(executionQuorum, _quorum); - assertEq(quorumAt, expectedQuorumAt); + assertEq(scheduledAt, expectedQuorumAt); assertEq(isExecuted, true); } @@ -567,7 +567,7 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { _hashConsensusWrapper.onlyMemberProtected(); } - function test_updateQuorumRevertsIfHashIsUsed() public { + function test_scheduleProposalRevertsIfHashIsUsed() public { bytes32 hash = keccak256("hash"); for (uint256 i = 0; i < _quorum; ++i) { @@ -580,10 +580,10 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { _hashConsensusWrapper.execute(hash); vm.expectRevert(abi.encodeWithSelector(HashConsensus.HashAlreadyUsed.selector, hash)); - _hashConsensusWrapper.updateQuorum(hash); + _hashConsensusWrapper.scheduleProposal(hash); } - function test_updateQuorumDoNothingIfQuorumAlreadyReached() public { + function test_scheduleProposalDoNothingIfQuorumAlreadyReached() public { bytes32 hash = keccak256("hash"); for (uint256 i = 0; i < _quorum; ++i) { @@ -591,17 +591,17 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { _hashConsensusWrapper.vote(hash, true); } - (,, uint256 quorumAtBefore,) = _hashConsensusWrapper.getHashState(hash); + (,, uint256 scheduledAtBefore,) = _hashConsensusWrapper.getHashState(hash); _wait(_timelock); - _hashConsensusWrapper.updateQuorum(hash); + _hashConsensusWrapper.scheduleProposal(hash); - (,, uint256 quorumAtAfter,) = _hashConsensusWrapper.getHashState(hash); + (,, uint256 scheduledAtAfter,) = _hashConsensusWrapper.getHashState(hash); - assertEq(quorumAtBefore, quorumAtAfter); + assertEq(scheduledAtBefore, scheduledAtAfter); } - function test_updateQuorumDoNothingIfQuorumIsNotReached() public { + function test_scheduleProposalDoNothingIfQuorumIsNotReached() public { bytes32 hash = keccak256("hash"); for (uint256 i = 0; i < _quorum - 1; ++i) { @@ -609,16 +609,16 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { _hashConsensusWrapper.vote(hash, true); } - (,, uint256 quorumAtBefore,) = _hashConsensusWrapper.getHashState(hash); - assertEq(quorumAtBefore, 0); + (,, uint256 scheduledAtBefore,) = _hashConsensusWrapper.getHashState(hash); + assertEq(scheduledAtBefore, 0); - _hashConsensusWrapper.updateQuorum(hash); + _hashConsensusWrapper.scheduleProposal(hash); - (,, uint256 quorumAtAfter,) = _hashConsensusWrapper.getHashState(hash); - assertEq(quorumAtAfter, 0); + (,, uint256 scheduledAtAfter,) = _hashConsensusWrapper.getHashState(hash); + assertEq(scheduledAtAfter, 0); } - function test_updateQuorum() public { + function test_scheduleProposal() public { bytes32 hash = keccak256("hash"); for (uint256 i = 0; i < _quorum - 1; ++i) { @@ -629,14 +629,14 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { vm.prank(_owner); _hashConsensusWrapper.setQuorum(_quorum - 1); - (,, uint256 quorumAtBefore,) = _hashConsensusWrapper.getHashState(hash); + (,, uint256 scheduledAtBefore,) = _hashConsensusWrapper.getHashState(hash); - assertEq(quorumAtBefore, 0); + assertEq(scheduledAtBefore, 0); - _hashConsensusWrapper.updateQuorum(hash); + _hashConsensusWrapper.scheduleProposal(hash); - (,, uint256 quorumAtAfter,) = _hashConsensusWrapper.getHashState(hash); + (,, uint256 scheduledAtAfter,) = _hashConsensusWrapper.getHashState(hash); - assertEq(quorumAtAfter, block.timestamp); + assertEq(scheduledAtAfter, block.timestamp); } } From a013a29d41ff0cf6d682455115fa1d8b2bc649de Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Fri, 23 Aug 2024 15:25:05 +0300 Subject: [PATCH 22/42] fix: check caller and typo --- contracts/committees/TiebreakerSubCommittee.sol | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/contracts/committees/TiebreakerSubCommittee.sol b/contracts/committees/TiebreakerSubCommittee.sol index e986860c..def43e5e 100644 --- a/contracts/committees/TiebreakerSubCommittee.sol +++ b/contracts/committees/TiebreakerSubCommittee.sol @@ -38,7 +38,7 @@ contract TiebreakerSubCommittee is HashConsensus, ProposalsList { /// @param proposalId The ID of the proposal to schedule function scheduleProposal(uint256 proposalId) public { _checkCallerIsMember(); - (bytes memory proposalData, bytes32 key) = _encodeAproveProposal(proposalId); + (bytes memory proposalData, bytes32 key) = _encodeApproveProposal(proposalId); _vote(key, true); _pushProposal(key, uint256(ProposalType.ScheduleProposal), proposalData); } @@ -55,7 +55,7 @@ contract TiebreakerSubCommittee is HashConsensus, ProposalsList { view returns (uint256 support, uint256 executionQuorum, uint256 quorumAt, bool isExecuted) { - (, bytes32 key) = _encodeAproveProposal(proposalId); + (, bytes32 key) = _encodeApproveProposal(proposalId); return _getHashState(key); } @@ -63,7 +63,7 @@ contract TiebreakerSubCommittee is HashConsensus, ProposalsList { /// @dev Executes the schedule proposal by calling the scheduleProposal function on the Tiebreaker Core contract /// @param proposalId The ID of the proposal to schedule function executeScheduleProposal(uint256 proposalId) public { - (, bytes32 key) = _encodeAproveProposal(proposalId); + (, bytes32 key) = _encodeApproveProposal(proposalId); _markUsed(key); Address.functionCall( TIEBREAKER_CORE, abi.encodeWithSelector(ITiebreakerCore.scheduleProposal.selector, proposalId) @@ -75,7 +75,7 @@ contract TiebreakerSubCommittee is HashConsensus, ProposalsList { /// @param proposalId The ID of the proposal to schedule /// @return data The encoded proposal data /// @return key The generated proposal key - function _encodeAproveProposal(uint256 proposalId) internal pure returns (bytes memory data, bytes32 key) { + function _encodeApproveProposal(uint256 proposalId) internal pure returns (bytes memory data, bytes32 key) { data = abi.encode(ProposalType.ScheduleProposal, proposalId); key = keccak256(data); } @@ -88,6 +88,7 @@ contract TiebreakerSubCommittee is HashConsensus, ProposalsList { /// @dev Allows committee members to vote on resuming a sealable address /// @param sealable The address to resume function sealableResume(address sealable) public { + _checkCallerIsMember(); (bytes memory proposalData, bytes32 key,) = _encodeSealableResume(sealable); _vote(key, true); _pushProposal(key, uint256(ProposalType.ResumeSelable), proposalData); From 4d017c1eaf8095f0508402dc66ff49fa8b15b223 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Fri, 23 Aug 2024 15:25:42 +0300 Subject: [PATCH 23/42] rename HashConsensus schedule --- contracts/committees/HashConsensus.sol | 2 +- test/unit/HashConsensus.t.sol | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/committees/HashConsensus.sol b/contracts/committees/HashConsensus.sol index b6887fc6..df5f36a5 100644 --- a/contracts/committees/HashConsensus.sol +++ b/contracts/committees/HashConsensus.sol @@ -175,7 +175,7 @@ abstract contract HashConsensus is Ownable { /// @notice Schedules a proposal for execution if quorum is reached and it has not been scheduled yet. /// @param hash The hash of the proposal to be scheduled - function scheduleProposal(bytes32 hash) public { + function schedule(bytes32 hash) public { if (_hashStates[hash].usedAt > 0) { revert HashAlreadyUsed(hash); } diff --git a/test/unit/HashConsensus.t.sol b/test/unit/HashConsensus.t.sol index 1406e734..c237d1f2 100644 --- a/test/unit/HashConsensus.t.sol +++ b/test/unit/HashConsensus.t.sol @@ -567,7 +567,7 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { _hashConsensusWrapper.onlyMemberProtected(); } - function test_scheduleProposalRevertsIfHashIsUsed() public { + function test_scheduleRevertsIfHashIsUsed() public { bytes32 hash = keccak256("hash"); for (uint256 i = 0; i < _quorum; ++i) { @@ -580,10 +580,10 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { _hashConsensusWrapper.execute(hash); vm.expectRevert(abi.encodeWithSelector(HashConsensus.HashAlreadyUsed.selector, hash)); - _hashConsensusWrapper.scheduleProposal(hash); + _hashConsensusWrapper.schedule(hash); } - function test_scheduleProposalDoNothingIfQuorumAlreadyReached() public { + function test_scheduleDoNothingIfQuorumAlreadyReached() public { bytes32 hash = keccak256("hash"); for (uint256 i = 0; i < _quorum; ++i) { @@ -594,14 +594,14 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { (,, uint256 scheduledAtBefore,) = _hashConsensusWrapper.getHashState(hash); _wait(_timelock); - _hashConsensusWrapper.scheduleProposal(hash); + _hashConsensusWrapper.schedule(hash); (,, uint256 scheduledAtAfter,) = _hashConsensusWrapper.getHashState(hash); assertEq(scheduledAtBefore, scheduledAtAfter); } - function test_scheduleProposalDoNothingIfQuorumIsNotReached() public { + function test_scheduleDoNothingIfQuorumIsNotReached() public { bytes32 hash = keccak256("hash"); for (uint256 i = 0; i < _quorum - 1; ++i) { @@ -612,13 +612,13 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { (,, uint256 scheduledAtBefore,) = _hashConsensusWrapper.getHashState(hash); assertEq(scheduledAtBefore, 0); - _hashConsensusWrapper.scheduleProposal(hash); + _hashConsensusWrapper.schedule(hash); (,, uint256 scheduledAtAfter,) = _hashConsensusWrapper.getHashState(hash); assertEq(scheduledAtAfter, 0); } - function test_scheduleProposal() public { + function test_schedule() public { bytes32 hash = keccak256("hash"); for (uint256 i = 0; i < _quorum - 1; ++i) { @@ -633,7 +633,7 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { assertEq(scheduledAtBefore, 0); - _hashConsensusWrapper.scheduleProposal(hash); + _hashConsensusWrapper.schedule(hash); (,, uint256 scheduledAtAfter,) = _hashConsensusWrapper.getHashState(hash); From 35acfbab711683a9c39fd803831f299e739aabc4 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Fri, 23 Aug 2024 16:16:22 +0300 Subject: [PATCH 24/42] natspec update --- contracts/committees/HashConsensus.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/contracts/committees/HashConsensus.sol b/contracts/committees/HashConsensus.sol index df5f36a5..b1afa95f 100644 --- a/contracts/committees/HashConsensus.sol +++ b/contracts/committees/HashConsensus.sol @@ -57,7 +57,7 @@ abstract contract HashConsensus is Ownable { } uint256 heads = _getSupport(hash); - if (heads == quorum - 1 && support == true) { + if (heads >= quorum && support == true) { _hashStates[hash].scheduledAt = uint40(block.timestamp); } @@ -174,6 +174,9 @@ abstract contract HashConsensus is Ownable { } /// @notice Schedules a proposal for execution if quorum is reached and it has not been scheduled yet. + /// @dev This function schedules a proposal for execution if the quorum is reached and + /// the proposal has not been scheduled yet. Could happen when execution quorum was set to the same value as + /// current support of the proposal. /// @param hash The hash of the proposal to be scheduled function schedule(bytes32 hash) public { if (_hashStates[hash].usedAt > 0) { From dc8444d29ee0404e996d12d4f97aa6792cfeb113 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Fri, 23 Aug 2024 16:41:47 +0300 Subject: [PATCH 25/42] revert on schedule proposal --- contracts/committees/HashConsensus.sol | 14 ++++++++++---- test/unit/HashConsensus.t.sol | 4 ++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/contracts/committees/HashConsensus.sol b/contracts/committees/HashConsensus.sol index b1afa95f..8fa0f730 100644 --- a/contracts/committees/HashConsensus.sol +++ b/contracts/committees/HashConsensus.sol @@ -25,6 +25,7 @@ abstract contract HashConsensus is Ownable { error InvalidQuorum(); error InvalidTimelockDuration(uint256 timelock); error TimelockNotPassed(); + error ProposalAlreadyScheduled(bytes32 hash); struct HashState { uint40 scheduledAt; @@ -57,7 +58,8 @@ abstract contract HashConsensus is Ownable { } uint256 heads = _getSupport(hash); - if (heads >= quorum && support == true) { + // heads compares to quorum - 1 because the current vote is not counted yet + if (heads >= quorum - 1 && support == true && _hashStates[hash].scheduledAt == 0) { _hashStates[hash].scheduledAt = uint40(block.timestamp); } @@ -183,10 +185,14 @@ abstract contract HashConsensus is Ownable { revert HashAlreadyUsed(hash); } - uint256 support = _getSupport(hash); - if (support >= quorum && _hashStates[hash].scheduledAt == 0) { - _hashStates[hash].scheduledAt = uint40(block.timestamp); + if (_getSupport(hash) < quorum) { + revert QuorumIsNotReached(); } + if (_hashStates[hash].scheduledAt > 0) { + revert ProposalAlreadyScheduled(hash); + } + + _hashStates[hash].scheduledAt = uint40(block.timestamp); } /// @notice Sets the execution quorum required for certain operations. diff --git a/test/unit/HashConsensus.t.sol b/test/unit/HashConsensus.t.sol index c237d1f2..fbfbda66 100644 --- a/test/unit/HashConsensus.t.sol +++ b/test/unit/HashConsensus.t.sol @@ -586,6 +586,8 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { function test_scheduleDoNothingIfQuorumAlreadyReached() public { bytes32 hash = keccak256("hash"); + _wait(_timelock); + for (uint256 i = 0; i < _quorum; ++i) { vm.prank(_committeeMembers[i]); _hashConsensusWrapper.vote(hash, true); @@ -594,6 +596,7 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { (,, uint256 scheduledAtBefore,) = _hashConsensusWrapper.getHashState(hash); _wait(_timelock); + vm.expectRevert(abi.encodeWithSignature("ProposalAlreadyScheduled(bytes32)", hash)); _hashConsensusWrapper.schedule(hash); (,, uint256 scheduledAtAfter,) = _hashConsensusWrapper.getHashState(hash); @@ -612,6 +615,7 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { (,, uint256 scheduledAtBefore,) = _hashConsensusWrapper.getHashState(hash); assertEq(scheduledAtBefore, 0); + vm.expectRevert(abi.encodeWithSignature("QuorumIsNotReached()")); _hashConsensusWrapper.schedule(hash); (,, uint256 scheduledAtAfter,) = _hashConsensusWrapper.getHashState(hash); From 6c083645c47c3d59d892fdcc808d68712c77daea Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Fri, 23 Aug 2024 16:43:14 +0300 Subject: [PATCH 26/42] fix typo --- contracts/committees/TiebreakerCore.sol | 6 +++--- contracts/committees/TiebreakerSubCommittee.sol | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/committees/TiebreakerCore.sol b/contracts/committees/TiebreakerCore.sol index ce5e80bc..c8fea73e 100644 --- a/contracts/committees/TiebreakerCore.sol +++ b/contracts/committees/TiebreakerCore.sol @@ -10,7 +10,7 @@ import {ProposalsList} from "./ProposalsList.sol"; enum ProposalType { ScheduleProposal, - ResumeSelable + ResumeSealable } /// @title Tiebreaker Core Contract @@ -97,7 +97,7 @@ contract TiebreakerCore is ITiebreakerCore, HashConsensus, ProposalsList { } (bytes memory proposalData, bytes32 key) = _encodeSealableResume(sealable, nonce); _vote(key, true); - _pushProposal(key, uint256(ProposalType.ResumeSelable), proposalData); + _pushProposal(key, uint256(ProposalType.ResumeSealable), proposalData); } /// @notice Gets the current state of a resume sealable proposal @@ -138,7 +138,7 @@ contract TiebreakerCore is ITiebreakerCore, HashConsensus, ProposalsList { address sealable, uint256 nonce ) private pure returns (bytes memory data, bytes32 key) { - data = abi.encode(ProposalType.ResumeSelable, sealable, nonce); + data = abi.encode(ProposalType.ResumeSealable, sealable, nonce); key = keccak256(data); } } diff --git a/contracts/committees/TiebreakerSubCommittee.sol b/contracts/committees/TiebreakerSubCommittee.sol index def43e5e..c58bbf97 100644 --- a/contracts/committees/TiebreakerSubCommittee.sol +++ b/contracts/committees/TiebreakerSubCommittee.sol @@ -9,7 +9,7 @@ import {ProposalsList} from "./ProposalsList.sol"; enum ProposalType { ScheduleProposal, - ResumeSelable + ResumeSealable } /// @title Tiebreaker SubCommittee Contract @@ -91,7 +91,7 @@ contract TiebreakerSubCommittee is HashConsensus, ProposalsList { _checkCallerIsMember(); (bytes memory proposalData, bytes32 key,) = _encodeSealableResume(sealable); _vote(key, true); - _pushProposal(key, uint256(ProposalType.ResumeSelable), proposalData); + _pushProposal(key, uint256(ProposalType.ResumeSealable), proposalData); } /// @notice Gets the current state of a resume sealable proposal From 34b2b44fe533a3bb38391aebd239b2a70859c252 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Fri, 23 Aug 2024 16:48:33 +0300 Subject: [PATCH 27/42] tests naming --- test/unit/HashConsensus.t.sol | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/test/unit/HashConsensus.t.sol b/test/unit/HashConsensus.t.sol index fbfbda66..2d03102c 100644 --- a/test/unit/HashConsensus.t.sol +++ b/test/unit/HashConsensus.t.sol @@ -347,7 +347,7 @@ contract Target { } contract HashConsensusWrapper is HashConsensus { - event OnlyMemberModifierPassed(); + event OnlyMemberPassed(); Target internal _target; @@ -385,7 +385,7 @@ contract HashConsensusWrapper is HashConsensus { function onlyMemberProtected() public { _checkCallerIsMember(); - emit OnlyMemberModifierPassed(); + emit OnlyMemberPassed(); } } @@ -514,7 +514,7 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { assertEq(_hashConsensusWrapper.approves(_committeeMembers[0], dataHash), false); } - function test_vote_reverts_on_executed() public { + function test_vote_RevertsOn_executed() public { for (uint256 i = 0; i < _quorum; ++i) { vm.prank(_committeeMembers[i]); _hashConsensusWrapper.vote(dataHash, true); @@ -556,18 +556,24 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { _hashConsensusWrapper.execute(dataHash); } - function test_onlyMemberModifier() public { + function test_onlyMember() public { vm.prank(_stranger); vm.expectRevert(abi.encodeWithSelector(HashConsensus.CallerIsNotMember.selector, _stranger)); _hashConsensusWrapper.onlyMemberProtected(); - vm.prank(_committeeMembers[0]); - vm.expectEmit(address(_hashConsensus)); - emit HashConsensusWrapper.OnlyMemberModifierPassed(); + vm.prank(_owner); + vm.expectRevert(abi.encodeWithSelector(HashConsensus.CallerIsNotMember.selector, _owner)); _hashConsensusWrapper.onlyMemberProtected(); + + for (uint256 i = 0; i < _committeeMembers.length; i++) { + vm.prank(_committeeMembers[i]); + vm.expectEmit(address(_hashConsensus)); + emit HashConsensusWrapper.OnlyMemberPassed(); + _hashConsensusWrapper.onlyMemberProtected(); + } } - function test_scheduleRevertsIfHashIsUsed() public { + function test_schedule_RevertOn_IfHashIsUsed() public { bytes32 hash = keccak256("hash"); for (uint256 i = 0; i < _quorum; ++i) { @@ -583,7 +589,7 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { _hashConsensusWrapper.schedule(hash); } - function test_scheduleDoNothingIfQuorumAlreadyReached() public { + function test_schedule_RevertOn_IfQuorumAlreadyReached() public { bytes32 hash = keccak256("hash"); _wait(_timelock); @@ -604,7 +610,7 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { assertEq(scheduledAtBefore, scheduledAtAfter); } - function test_scheduleDoNothingIfQuorumIsNotReached() public { + function test_schedule_RevertOn_IfQuorumIsNotReached() public { bytes32 hash = keccak256("hash"); for (uint256 i = 0; i < _quorum - 1; ++i) { From c40cd6d558e278aa2c1a66ac2b28ba671efd2e97 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Fri, 23 Aug 2024 16:51:56 +0300 Subject: [PATCH 28/42] tests_naming --- test/unit/HashConsensus.t.sol | 38 +++++++++++++++++------------------ 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/test/unit/HashConsensus.t.sol b/test/unit/HashConsensus.t.sol index 2d03102c..556709b7 100644 --- a/test/unit/HashConsensus.t.sol +++ b/test/unit/HashConsensus.t.sol @@ -37,7 +37,7 @@ abstract contract HashConsensusUnitTest is UnitTest { } } - function test_constructorInitializesCorrectly() public { + function test_constructor_InitializesCorrectly() public { uint256 timelock = 1; vm.expectEmit(); @@ -54,7 +54,7 @@ abstract contract HashConsensusUnitTest is UnitTest { new HashConsensusInstance(_owner, _committeeMembers, _quorum, timelock); } - function test_constructorRevertsWithZeroQuorum() public { + function test_constructor_RevertOn_WithZeroQuorum() public { uint256 invalidQuorum = 0; vm.expectRevert(abi.encodeWithSelector(HashConsensus.InvalidQuorum.selector)); @@ -80,7 +80,7 @@ abstract contract HashConsensusUnitTest is UnitTest { } } - function test_addMembers_stranger_call() public { + function test_addMembers_RevertOn_StrangerCall() public { address[] memory membersToAdd = new address[](1); membersToAdd[0] = makeAddr("NEW_MEMBER"); assertEq(_hashConsensus.isMember(membersToAdd[0]), false); @@ -96,7 +96,7 @@ abstract contract HashConsensusUnitTest is UnitTest { } } - function test_addMembers_reverts_on_duplicate() public { + function test_addMembers_RevertOn_Duplicate() public { address[] memory membersToAdd = new address[](1); membersToAdd[0] = _committeeMembers[0]; assertEq(_hashConsensus.isMember(membersToAdd[0]), true); @@ -106,7 +106,7 @@ abstract contract HashConsensusUnitTest is UnitTest { _hashConsensus.addMembers(membersToAdd, _quorum); } - function test_addMembers_reverts_on_duplicate_in_array() public { + function test_addMembers_RevertOn_DuplicateInArray() public { address[] memory membersToAdd = new address[](2); membersToAdd[0] = makeAddr("NEW_MEMBER"); membersToAdd[1] = makeAddr("NEW_MEMBER"); @@ -117,7 +117,7 @@ abstract contract HashConsensusUnitTest is UnitTest { _hashConsensus.addMembers(membersToAdd, _quorum); } - function test_addMember_reverts_on_invalid_quorum() public { + function test_addMember_RevertOn_InvalidQuorum() public { address[] memory membersToAdd = new address[](1); membersToAdd[0] = makeAddr("NEW_MEMBER"); assertEq(_hashConsensus.isMember(membersToAdd[0]), false); @@ -159,7 +159,7 @@ abstract contract HashConsensusUnitTest is UnitTest { assertEq(committeeMembers[committeeMembers.length - 1], membersToAdd[1]); } - function test_removeMembers_stranger_call() public { + function test_removeMembers_RevertOn_StrangerCall() public { address[] memory membersToRemove = new address[](1); membersToRemove[0] = _committeeMembers[0]; assertEq(_hashConsensus.isMember(membersToRemove[0]), true); @@ -175,7 +175,7 @@ abstract contract HashConsensusUnitTest is UnitTest { } } - function test_removeMembers_reverts_on_member_is_not_exist() public { + function test_removeMembers_RevertOn_member_is_not_exist() public { address[] memory membersToRemove = new address[](1); membersToRemove[0] = _stranger; assertEq(_hashConsensus.isMember(membersToRemove[0]), false); @@ -185,7 +185,7 @@ abstract contract HashConsensusUnitTest is UnitTest { _hashConsensus.removeMembers(membersToRemove, _quorum); } - function test_removeMembers_reverts_on_member_duplicate_in_array() public { + function test_removeMembers_RevertOn_member_duplicate_in_array() public { address[] memory membersToRemove = new address[](2); membersToRemove[0] = _committeeMembers[0]; membersToRemove[1] = _committeeMembers[0]; @@ -197,7 +197,7 @@ abstract contract HashConsensusUnitTest is UnitTest { _hashConsensus.removeMembers(membersToRemove, _quorum); } - function test_removeMembers_reverts_on_invalid_quorum() public { + function test_removeMembers_RevertOn_invalid_quorum() public { address[] memory membersToRemove = new address[](1); membersToRemove[0] = _committeeMembers[0]; assertEq(_hashConsensus.isMember(membersToRemove[0]), true); @@ -240,7 +240,7 @@ abstract contract HashConsensusUnitTest is UnitTest { } } - function test_setTimelockDurationByOwner() public { + function test_setTimelockDuration_ByOwner() public { uint256 newTimelockDuration = 200; vm.expectEmit(true, false, false, true); @@ -252,7 +252,7 @@ abstract contract HashConsensusUnitTest is UnitTest { assertEq(_hashConsensus.timelockDuration(), newTimelockDuration); } - function test_setTimelockDurationRevertsIfNotOwner() public { + function test_setTimelockDuration_RevertOn_IfNotOwner() public { uint256 newTimelockDuration = 200; vm.prank(address(0x123)); @@ -260,7 +260,7 @@ abstract contract HashConsensusUnitTest is UnitTest { _hashConsensus.setTimelockDuration(newTimelockDuration); } - function test_setTimelockDurationRevertsIfValueIsSame() public { + function test_setTimelockDuration_RevertOn_IfValueIsSame() public { uint256 newTimelockDuration = 300; vm.startPrank(_owner); @@ -270,7 +270,7 @@ abstract contract HashConsensusUnitTest is UnitTest { _hashConsensus.setTimelockDuration(newTimelockDuration); } - function testTimelockDurationEventEmitted() public { + function test_setTimelockDuration_EventEmitted() public { uint256 newTimelockDuration = 300; vm.expectEmit(true, false, false, true); @@ -280,7 +280,7 @@ abstract contract HashConsensusUnitTest is UnitTest { _hashConsensus.setTimelockDuration(newTimelockDuration); } - function test_setQuorumByOwner() public { + function test_setQuorum_ByOwner() public { uint256 newQuorum = 2; vm.expectEmit(true, false, false, true); @@ -293,7 +293,7 @@ abstract contract HashConsensusUnitTest is UnitTest { assertEq(_hashConsensus.quorum(), newQuorum); } - function test_setQuorumRevertsIfNotOwner() public { + function test_setQuorum_RevertOn_IfNotOwner() public { uint256 newQuorum = 2; vm.prank(address(0x123)); @@ -301,7 +301,7 @@ abstract contract HashConsensusUnitTest is UnitTest { _hashConsensus.setQuorum(newQuorum); } - function test_setQuorumRevertsIfZeroQuorum() public { + function test_setQuorum_RevertOn_IfZeroQuorum() public { uint256 invalidQuorum = 0; vm.prank(_owner); @@ -309,7 +309,7 @@ abstract contract HashConsensusUnitTest is UnitTest { _hashConsensus.setQuorum(invalidQuorum); } - function test_setQuorumRevertsIfQuorumExceedsMembers() public { + function test_setQuorum_RevertOn_IfQuorumExceedsMembers() public { uint256 invalidQuorum = _committeeMembers.length + 1; vm.prank(_owner); @@ -317,7 +317,7 @@ abstract contract HashConsensusUnitTest is UnitTest { _hashConsensus.setQuorum(invalidQuorum); } - function test_setQuorumRevertsIfQuorumIsSame() public { + function test_setQuorum_RevertOn_IfQuorumIsSame() public { uint256 invalidQuorum = 2; vm.startPrank(_owner); From dc75ebd6571e4134aa3e8f1e9b23bf69729836dc Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Fri, 23 Aug 2024 18:12:10 +0300 Subject: [PATCH 29/42] using Timestamps and Durations --- .../EmergencyActivationCommittee.sol | 6 ++- .../EmergencyExecutionCommittee.sol | 10 ++-- contracts/committees/HashConsensus.sol | 38 ++++++------- contracts/committees/ResealCommittee.sol | 6 ++- contracts/committees/TiebreakerCore.sol | 8 +-- .../committees/TiebreakerSubCommittee.sol | 8 +-- test/scenario/tiebreaker.t.sol | 4 +- test/unit/HashConsensus.t.sol | 54 +++++++++---------- test/utils/SetupDeployment.sol | 8 +-- 9 files changed, 78 insertions(+), 64 deletions(-) diff --git a/contracts/committees/EmergencyActivationCommittee.sol b/contracts/committees/EmergencyActivationCommittee.sol index f4a96925..7e01fd48 100644 --- a/contracts/committees/EmergencyActivationCommittee.sol +++ b/contracts/committees/EmergencyActivationCommittee.sol @@ -5,6 +5,8 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {HashConsensus} from "./HashConsensus.sol"; import {ITimelock} from "../interfaces/ITimelock.sol"; +import {Duration, Durations} from "../types/Duration.sol"; +import {Timestamp} from "../types/Timestamp.sol"; /// @title Emergency Activation Committee Contract /// @notice This contract allows a committee to approve and execute an emergency activation @@ -19,7 +21,7 @@ contract EmergencyActivationCommittee is HashConsensus { address[] memory committeeMembers, uint256 executionQuorum, address emergencyProtectedTimelock - ) HashConsensus(owner, 0) { + ) HashConsensus(owner, Durations.from(0)) { EMERGENCY_PROTECTED_TIMELOCK = emergencyProtectedTimelock; _addMembers(committeeMembers, executionQuorum); @@ -40,7 +42,7 @@ contract EmergencyActivationCommittee is HashConsensus { function getActivateEmergencyModeState() public view - returns (uint256 support, uint256 executionQuorum, uint256 quorumAt, bool isExecuted) + returns (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) { return _getHashState(EMERGENCY_ACTIVATION_HASH); } diff --git a/contracts/committees/EmergencyExecutionCommittee.sol b/contracts/committees/EmergencyExecutionCommittee.sol index 3bc64830..ea6b5622 100644 --- a/contracts/committees/EmergencyExecutionCommittee.sol +++ b/contracts/committees/EmergencyExecutionCommittee.sol @@ -5,6 +5,8 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {HashConsensus} from "./HashConsensus.sol"; import {ProposalsList} from "./ProposalsList.sol"; import {ITimelock} from "../interfaces/ITimelock.sol"; +import {Timestamp} from "../types/Timestamp.sol"; +import {Durations} from "../types/Duration.sol"; enum ProposalType { EmergencyExecute, @@ -22,7 +24,7 @@ contract EmergencyExecutionCommittee is HashConsensus, ProposalsList { address[] memory committeeMembers, uint256 executionQuorum, address emergencyProtectedTimelock - ) HashConsensus(owner, 0) { + ) HashConsensus(owner, Durations.from(0)) { EMERGENCY_PROTECTED_TIMELOCK = emergencyProtectedTimelock; _addMembers(committeeMembers, executionQuorum); @@ -52,7 +54,7 @@ contract EmergencyExecutionCommittee is HashConsensus, ProposalsList { function getEmergencyExecuteState(uint256 proposalId) public view - returns (uint256 support, uint256 executionQuorum, uint256 quorumAt, bool isExecuted) + returns (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) { (, bytes32 key) = _encodeEmergencyExecute(proposalId); return _getHashState(key); @@ -94,7 +96,7 @@ contract EmergencyExecutionCommittee is HashConsensus, ProposalsList { _pushProposal(proposalKey, uint256(ProposalType.EmergencyReset), bytes("")); } - /// @notice Gets the current state of an emergency reset opprosal + /// @notice Gets the current state of an emergency reset proposal /// @return support The number of votes in support of the proposal /// @return executionQuorum The required number of votes for execution /// @return quorumAt The timestamp when the quorum was reached @@ -102,7 +104,7 @@ contract EmergencyExecutionCommittee is HashConsensus, ProposalsList { function getEmergencyResetState() public view - returns (uint256 support, uint256 executionQuorum, uint256 quorumAt, bool isExecuted) + returns (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) { bytes32 proposalKey = _encodeEmergencyResetProposalKey(); return _getHashState(proposalKey); diff --git a/contracts/committees/HashConsensus.sol b/contracts/committees/HashConsensus.sol index 8fa0f730..c0e8a1f0 100644 --- a/contracts/committees/HashConsensus.sol +++ b/contracts/committees/HashConsensus.sol @@ -3,6 +3,8 @@ pragma solidity 0.8.26; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {Duration} from "../types/Duration.sol"; +import {Timestamp, Timestamps} from "../types/Timestamp.sol"; /// @title HashConsensus Contract /// @notice This contract provides a consensus mechanism based on hash voting among members @@ -15,7 +17,7 @@ abstract contract HashConsensus is Ownable { event QuorumSet(uint256 quorum); event HashUsed(bytes32 hash); event Voted(address indexed signer, bytes32 hash, bool support); - event TimelockDurationSet(uint256 timelockDuration); + event TimelockDurationSet(Duration timelockDuration); error DuplicatedMember(address account); error AccountIsNotMember(address account); @@ -23,23 +25,23 @@ abstract contract HashConsensus is Ownable { error HashAlreadyUsed(bytes32 hash); error QuorumIsNotReached(); error InvalidQuorum(); - error InvalidTimelockDuration(uint256 timelock); + error InvalidTimelockDuration(Duration timelock); error TimelockNotPassed(); error ProposalAlreadyScheduled(bytes32 hash); struct HashState { - uint40 scheduledAt; - uint40 usedAt; + Timestamp scheduledAt; + Timestamp usedAt; } uint256 public quorum; - uint256 public timelockDuration; + Duration public timelockDuration; mapping(bytes32 => HashState) private _hashStates; EnumerableSet.AddressSet private _members; mapping(address signer => mapping(bytes32 => bool)) public approves; - constructor(address owner, uint256 timelock) Ownable(owner) { + constructor(address owner, Duration timelock) Ownable(owner) { timelockDuration = timelock; emit TimelockDurationSet(timelock); } @@ -49,7 +51,7 @@ abstract contract HashConsensus is Ownable { /// @param hash The hash to vote on /// @param support Indicates whether the member supports the hash function _vote(bytes32 hash, bool support) internal { - if (_hashStates[hash].usedAt > 0) { + if (_hashStates[hash].usedAt > Timestamps.from(0)) { revert HashAlreadyUsed(hash); } @@ -59,8 +61,8 @@ abstract contract HashConsensus is Ownable { uint256 heads = _getSupport(hash); // heads compares to quorum - 1 because the current vote is not counted yet - if (heads >= quorum - 1 && support == true && _hashStates[hash].scheduledAt == 0) { - _hashStates[hash].scheduledAt = uint40(block.timestamp); + if (heads >= quorum - 1 && support == true && _hashStates[hash].scheduledAt == Timestamps.from(0)) { + _hashStates[hash].scheduledAt = Timestamps.from(block.timestamp); } approves[msg.sender][hash] = support; @@ -71,7 +73,7 @@ abstract contract HashConsensus is Ownable { /// @dev Internal function that handles marking a hash as used /// @param hash The hash to mark as used function _markUsed(bytes32 hash) internal { - if (_hashStates[hash].usedAt > 0) { + if (_hashStates[hash].usedAt > Timestamps.from(0)) { revert HashAlreadyUsed(hash); } @@ -80,11 +82,11 @@ abstract contract HashConsensus is Ownable { if (support == 0 || support < quorum) { revert QuorumIsNotReached(); } - if (block.timestamp < _hashStates[hash].scheduledAt + timelockDuration) { + if (timelockDuration.addTo(_hashStates[hash].scheduledAt) > Timestamps.from(block.timestamp)) { revert TimelockNotPassed(); } - _hashStates[hash].usedAt = uint40(block.timestamp); + _hashStates[hash].usedAt = Timestamps.from(block.timestamp); emit HashUsed(hash); } @@ -99,12 +101,12 @@ abstract contract HashConsensus is Ownable { function _getHashState(bytes32 hash) internal view - returns (uint256 support, uint256 executionQuorum, uint256 scheduledAt, bool isUsed) + returns (uint256 support, uint256 executionQuorum, Timestamp scheduledAt, bool isUsed) { support = _getSupport(hash); executionQuorum = quorum; scheduledAt = _hashStates[hash].scheduledAt; - isUsed = _hashStates[hash].usedAt > 0; + isUsed = _hashStates[hash].usedAt > Timestamps.from(0); } /// @notice Adds new members to the contract and sets the execution quorum. @@ -158,7 +160,7 @@ abstract contract HashConsensus is Ownable { /// @notice Sets the timelock duration /// @dev Only callable by the owner /// @param timelock The new timelock duration in seconds - function setTimelockDuration(uint256 timelock) public { + function setTimelockDuration(Duration timelock) public { _checkOwner(); if (timelock == timelockDuration) { revert InvalidTimelockDuration(timelock); @@ -181,18 +183,18 @@ abstract contract HashConsensus is Ownable { /// current support of the proposal. /// @param hash The hash of the proposal to be scheduled function schedule(bytes32 hash) public { - if (_hashStates[hash].usedAt > 0) { + if (_hashStates[hash].usedAt > Timestamps.from(0)) { revert HashAlreadyUsed(hash); } if (_getSupport(hash) < quorum) { revert QuorumIsNotReached(); } - if (_hashStates[hash].scheduledAt > 0) { + if (_hashStates[hash].scheduledAt > Timestamps.from(0)) { revert ProposalAlreadyScheduled(hash); } - _hashStates[hash].scheduledAt = uint40(block.timestamp); + _hashStates[hash].scheduledAt = Timestamps.from(block.timestamp); } /// @notice Sets the execution quorum required for certain operations. diff --git a/contracts/committees/ResealCommittee.sol b/contracts/committees/ResealCommittee.sol index 580d473c..a6e4a99b 100644 --- a/contracts/committees/ResealCommittee.sol +++ b/contracts/committees/ResealCommittee.sol @@ -6,6 +6,8 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {IDualGovernance} from "../interfaces/IDualGovernance.sol"; import {HashConsensus} from "./HashConsensus.sol"; import {ProposalsList} from "./ProposalsList.sol"; +import {Timestamp} from "../types/Timestamp.sol"; +import {Duration} from "../types/Duration.sol"; /// @title Reseal Committee Contract /// @notice This contract allows a committee to vote on and execute resealing proposals @@ -20,7 +22,7 @@ contract ResealCommittee is HashConsensus, ProposalsList { address[] memory committeeMembers, uint256 executionQuorum, address dualGovernance, - uint256 timelock + Duration timelock ) HashConsensus(owner, timelock) { DUAL_GOVERNANCE = dualGovernance; @@ -48,7 +50,7 @@ contract ResealCommittee is HashConsensus, ProposalsList { function getResealState(address sealable) public view - returns (uint256 support, uint256 executionQuorum, uint256 quorumAt, bool isExecuted) + returns (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) { (, bytes32 key) = _encodeResealProposal(sealable); return _getHashState(key); diff --git a/contracts/committees/TiebreakerCore.sol b/contracts/committees/TiebreakerCore.sol index c8fea73e..bcb75d7a 100644 --- a/contracts/committees/TiebreakerCore.sol +++ b/contracts/committees/TiebreakerCore.sol @@ -7,6 +7,8 @@ import {ITiebreakerCore} from "../interfaces/ITiebreaker.sol"; import {IDualGovernance} from "../interfaces/IDualGovernance.sol"; import {HashConsensus} from "./HashConsensus.sol"; import {ProposalsList} from "./ProposalsList.sol"; +import {Timestamp} from "../types/Timestamp.sol"; +import {Duration} from "../types/Duration.sol"; enum ProposalType { ScheduleProposal, @@ -23,7 +25,7 @@ contract TiebreakerCore is ITiebreakerCore, HashConsensus, ProposalsList { mapping(address => uint256) private _sealableResumeNonces; - constructor(address owner, address dualGovernance, uint256 timelock) HashConsensus(owner, timelock) { + constructor(address owner, address dualGovernance, Duration timelock) HashConsensus(owner, timelock) { DUAL_GOVERNANCE = dualGovernance; } @@ -51,7 +53,7 @@ contract TiebreakerCore is ITiebreakerCore, HashConsensus, ProposalsList { function getScheduleProposalState(uint256 proposalId) public view - returns (uint256 support, uint256 executionQuorum, uint256 quorumAt, bool isExecuted) + returns (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) { (, bytes32 key) = _encodeScheduleProposal(proposalId); return _getHashState(key); @@ -111,7 +113,7 @@ contract TiebreakerCore is ITiebreakerCore, HashConsensus, ProposalsList { function getSealableResumeState( address sealable, uint256 nonce - ) public view returns (uint256 support, uint256 executionQuorum, uint256 quorumAt, bool isExecuted) { + ) public view returns (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) { (, bytes32 key) = _encodeSealableResume(sealable, nonce); return _getHashState(key); } diff --git a/contracts/committees/TiebreakerSubCommittee.sol b/contracts/committees/TiebreakerSubCommittee.sol index c58bbf97..e4113143 100644 --- a/contracts/committees/TiebreakerSubCommittee.sol +++ b/contracts/committees/TiebreakerSubCommittee.sol @@ -6,6 +6,8 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {ITiebreakerCore} from "../interfaces/ITiebreaker.sol"; import {HashConsensus} from "./HashConsensus.sol"; import {ProposalsList} from "./ProposalsList.sol"; +import {Timestamp} from "../types/Timestamp.sol"; +import {Durations} from "../types/Duration.sol"; enum ProposalType { ScheduleProposal, @@ -23,7 +25,7 @@ contract TiebreakerSubCommittee is HashConsensus, ProposalsList { address[] memory committeeMembers, uint256 executionQuorum, address tiebreakerCore - ) HashConsensus(owner, 0) { + ) HashConsensus(owner, Durations.from(0)) { TIEBREAKER_CORE = tiebreakerCore; _addMembers(committeeMembers, executionQuorum); @@ -53,7 +55,7 @@ contract TiebreakerSubCommittee is HashConsensus, ProposalsList { function getScheduleProposalState(uint256 proposalId) public view - returns (uint256 support, uint256 executionQuorum, uint256 quorumAt, bool isExecuted) + returns (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) { (, bytes32 key) = _encodeApproveProposal(proposalId); return _getHashState(key); @@ -104,7 +106,7 @@ contract TiebreakerSubCommittee is HashConsensus, ProposalsList { function getSealableResumeState(address sealable) public view - returns (uint256 support, uint256 executionQuorum, uint256 quorumAt, bool isExecuted) + returns (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) { (, bytes32 key,) = _encodeSealableResume(sealable); return _getHashState(key); diff --git a/test/scenario/tiebreaker.t.sol b/test/scenario/tiebreaker.t.sol index 2f362aa6..a7062717 100644 --- a/test/scenario/tiebreaker.t.sol +++ b/test/scenario/tiebreaker.t.sol @@ -79,7 +79,7 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { assertEq(support, quorum); // Waiting for submit delay pass - _wait(Durations.from(_tiebreakerCoreCommittee.timelockDuration())); + _wait(_tiebreakerCoreCommittee.timelockDuration()); _tiebreakerCoreCommittee.executeScheduleProposal(proposalIdToExecute); } @@ -160,7 +160,7 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { assertEq(support, quorum); // Waiting for submit delay pass - _wait(Durations.from(_tiebreakerCoreCommittee.timelockDuration())); + _wait(_tiebreakerCoreCommittee.timelockDuration()); _tiebreakerCoreCommittee.executeSealableResume(address(_lido.withdrawalQueue)); diff --git a/test/unit/HashConsensus.t.sol b/test/unit/HashConsensus.t.sol index 556709b7..987c8624 100644 --- a/test/unit/HashConsensus.t.sol +++ b/test/unit/HashConsensus.t.sol @@ -7,14 +7,15 @@ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Vm} from "forge-std/Test.sol"; import {HashConsensus} from "../../contracts/committees/HashConsensus.sol"; -import {Duration} from "../../contracts/types/Duration.sol"; +import {Duration, Durations} from "../../contracts/types/Duration.sol"; +import {Timestamp, Timestamps} from "../../contracts/types/Timestamp.sol"; contract HashConsensusInstance is HashConsensus { constructor( address owner, address[] memory newMembers, uint256 executionQuorum, - uint256 timelock + Duration timelock ) HashConsensus(owner, timelock) { _addMembers(newMembers, executionQuorum); } @@ -38,7 +39,7 @@ abstract contract HashConsensusUnitTest is UnitTest { } function test_constructor_InitializesCorrectly() public { - uint256 timelock = 1; + Duration timelock = Durations.from(1); vm.expectEmit(); emit Ownable.OwnershipTransferred(address(0), _owner); @@ -58,7 +59,7 @@ abstract contract HashConsensusUnitTest is UnitTest { uint256 invalidQuorum = 0; vm.expectRevert(abi.encodeWithSelector(HashConsensus.InvalidQuorum.selector)); - new HashConsensusInstance(_owner, _committeeMembers, invalidQuorum, 1); + new HashConsensusInstance(_owner, _committeeMembers, invalidQuorum, Durations.from(1)); } function test_isMember() public { @@ -241,7 +242,7 @@ abstract contract HashConsensusUnitTest is UnitTest { } function test_setTimelockDuration_ByOwner() public { - uint256 newTimelockDuration = 200; + Duration newTimelockDuration = Durations.from(200); vm.expectEmit(true, false, false, true); emit HashConsensus.TimelockDurationSet(newTimelockDuration); @@ -253,7 +254,7 @@ abstract contract HashConsensusUnitTest is UnitTest { } function test_setTimelockDuration_RevertOn_IfNotOwner() public { - uint256 newTimelockDuration = 200; + Duration newTimelockDuration = Durations.from(200); vm.prank(address(0x123)); vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(0x123))); @@ -261,7 +262,7 @@ abstract contract HashConsensusUnitTest is UnitTest { } function test_setTimelockDuration_RevertOn_IfValueIsSame() public { - uint256 newTimelockDuration = 300; + Duration newTimelockDuration = Durations.from(300); vm.startPrank(_owner); _hashConsensus.setTimelockDuration(newTimelockDuration); @@ -271,7 +272,7 @@ abstract contract HashConsensusUnitTest is UnitTest { } function test_setTimelockDuration_EventEmitted() public { - uint256 newTimelockDuration = 300; + Duration newTimelockDuration = Durations.from(300); vm.expectEmit(true, false, false, true); emit HashConsensus.TimelockDurationSet(newTimelockDuration); @@ -355,7 +356,7 @@ contract HashConsensusWrapper is HashConsensus { address owner, address[] memory newMembers, uint256 executionQuorum, - uint256 timelock, + Duration timelock, Target target ) HashConsensus(owner, timelock) { _target = target; @@ -374,7 +375,7 @@ contract HashConsensusWrapper is HashConsensus { function getHashState(bytes32 hash) public view - returns (uint256 support, uint256 executionQuorum, uint256 scheduledAt, bool isExecuted) + returns (uint256 support, uint256 executionQuorum, Timestamp scheduledAt, bool isExecuted) { return _getHashState(hash); } @@ -399,8 +400,7 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { function setUp() public { _target = new Target(); - _hashConsensusWrapper = - new HashConsensusWrapper(_owner, _committeeMembers, _quorum, _timelock.toSeconds(), _target); + _hashConsensusWrapper = new HashConsensusWrapper(_owner, _committeeMembers, _quorum, _timelock, _target); _hashConsensus = HashConsensus(_hashConsensusWrapper); data = abi.encode(address(_target)); dataHash = keccak256(data); @@ -431,16 +431,16 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { function test_getHashState() public { uint256 support; uint256 executionQuorum; - uint256 scheduledAt; + Timestamp scheduledAt; bool isExecuted; (support, executionQuorum, scheduledAt, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); assertEq(support, 0); assertEq(executionQuorum, _quorum); - assertEq(scheduledAt, 0); + assertEq(scheduledAt, Timestamps.from(0)); assertEq(isExecuted, false); - uint256 expectedQuorumAt = block.timestamp; + Timestamp expectedQuorumAt = Timestamps.from(block.timestamp); for (uint256 i = 0; i < _membersCount; ++i) { (support, executionQuorum, scheduledAt, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); @@ -449,7 +449,7 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { if (i >= executionQuorum) { assertEq(scheduledAt, expectedQuorumAt); } else { - assertEq(scheduledAt, 0); + assertEq(scheduledAt, Timestamps.from(0)); } assertEq(isExecuted, false); @@ -462,7 +462,7 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { if (i >= executionQuorum - 1) { assertEq(scheduledAt, expectedQuorumAt); } else { - assertEq(scheduledAt, 0); + assertEq(scheduledAt, Timestamps.from(0)); } assertEq(isExecuted, false); } @@ -599,13 +599,13 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { _hashConsensusWrapper.vote(hash, true); } - (,, uint256 scheduledAtBefore,) = _hashConsensusWrapper.getHashState(hash); + (,, Timestamp scheduledAtBefore,) = _hashConsensusWrapper.getHashState(hash); _wait(_timelock); vm.expectRevert(abi.encodeWithSignature("ProposalAlreadyScheduled(bytes32)", hash)); _hashConsensusWrapper.schedule(hash); - (,, uint256 scheduledAtAfter,) = _hashConsensusWrapper.getHashState(hash); + (,, Timestamp scheduledAtAfter,) = _hashConsensusWrapper.getHashState(hash); assertEq(scheduledAtBefore, scheduledAtAfter); } @@ -618,14 +618,14 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { _hashConsensusWrapper.vote(hash, true); } - (,, uint256 scheduledAtBefore,) = _hashConsensusWrapper.getHashState(hash); - assertEq(scheduledAtBefore, 0); + (,, Timestamp scheduledAtBefore,) = _hashConsensusWrapper.getHashState(hash); + assertEq(scheduledAtBefore, Timestamps.from(0)); vm.expectRevert(abi.encodeWithSignature("QuorumIsNotReached()")); _hashConsensusWrapper.schedule(hash); - (,, uint256 scheduledAtAfter,) = _hashConsensusWrapper.getHashState(hash); - assertEq(scheduledAtAfter, 0); + (,, Timestamp scheduledAtAfter,) = _hashConsensusWrapper.getHashState(hash); + assertEq(scheduledAtAfter, Timestamps.from(0)); } function test_schedule() public { @@ -639,14 +639,14 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { vm.prank(_owner); _hashConsensusWrapper.setQuorum(_quorum - 1); - (,, uint256 scheduledAtBefore,) = _hashConsensusWrapper.getHashState(hash); + (,, Timestamp scheduledAtBefore,) = _hashConsensusWrapper.getHashState(hash); - assertEq(scheduledAtBefore, 0); + assertEq(scheduledAtBefore, Timestamps.from(0)); _hashConsensusWrapper.schedule(hash); - (,, uint256 scheduledAtAfter,) = _hashConsensusWrapper.getHashState(hash); + (,, Timestamp scheduledAtAfter,) = _hashConsensusWrapper.getHashState(hash); - assertEq(scheduledAtAfter, block.timestamp); + assertEq(scheduledAtAfter, Timestamps.from(block.timestamp)); } } diff --git a/test/utils/SetupDeployment.sol b/test/utils/SetupDeployment.sol index c90aa848..34b20a15 100644 --- a/test/utils/SetupDeployment.sol +++ b/test/utils/SetupDeployment.sol @@ -167,7 +167,7 @@ abstract contract SetupDeployment is Test { _tiebreakerCoreCommittee = _deployEmptyTiebreakerCoreCommittee({ owner: address(this), // temporary set owner to deployer, to add sub committees manually dualGovernance: _dualGovernance, - timelock: TIEBREAKER_EXECUTION_DELAY.toSeconds() + timelock: TIEBREAKER_EXECUTION_DELAY }); address[] memory coreCommitteeMembers = new address[](TIEBREAKER_SUB_COMMITTEES_COUNT); @@ -337,7 +337,9 @@ abstract contract SetupDeployment is Test { committeeMembers[i] = makeAddr(string(abi.encode(0xFA + i * membersCount + 65))); } - return new ResealCommittee(address(_adminExecutor), committeeMembers, quorum, address(_dualGovernance), 0); + return new ResealCommittee( + address(_adminExecutor), committeeMembers, quorum, address(_dualGovernance), Durations.from(0) + ); } function _deployTimelockedGovernance( @@ -403,7 +405,7 @@ abstract contract SetupDeployment is Test { function _deployEmptyTiebreakerCoreCommittee( address owner, IDualGovernance dualGovernance, - uint256 timelock + Duration timelock ) internal returns (TiebreakerCore) { return new TiebreakerCore({owner: owner, dualGovernance: address(dualGovernance), timelock: timelock}); } From ecc21065e64ac19c4ba6a0374d916c9dfec7bc5e Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Fri, 9 Aug 2024 19:27:20 +0400 Subject: [PATCH 30/42] Add tests for EscrowState library --- test/unit/libraries/EscrowState.t.sol | 322 ++++++++++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 test/unit/libraries/EscrowState.t.sol diff --git a/test/unit/libraries/EscrowState.t.sol b/test/unit/libraries/EscrowState.t.sol new file mode 100644 index 00000000..d5740784 --- /dev/null +++ b/test/unit/libraries/EscrowState.t.sol @@ -0,0 +1,322 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Vm} from "forge-std/Vm.sol"; + +import {Duration, Durations, MAX_VALUE as DURATION_MAX_VALUE} from "contracts/types/Duration.sol"; +import {Timestamp, Timestamps, MAX_TIMESTAMP_VALUE, TimestampOverflow} from "contracts/types/Timestamp.sol"; +import {EscrowState, State} from "contracts/libraries/EscrowState.sol"; + +import {UnitTest} from "test/utils/unit-test.sol"; + +Duration constant D0 = Durations.ZERO; +Timestamp constant T0 = Timestamps.ZERO; + +contract EscrowStateUnitTests is UnitTest { + EscrowState.Context private _context; + + // --- + // initialize() + // --- + + function testFuzz_initialize_happyPath(uint32 minAssetsLockSeconds) external { + Duration minAssetsLockDuration = Durations.from(minAssetsLockSeconds); + _context.state = State.NotInitialized; + + vm.expectEmit(); + emit EscrowState.EscrowStateChanged(State.NotInitialized, State.SignallingEscrow); + emit EscrowState.MinAssetsLockDurationSet(minAssetsLockDuration); + + EscrowState.initialize(_context, minAssetsLockDuration); + + checkContext(State.SignallingEscrow, minAssetsLockDuration, D0, D0, T0); + } + + function testFuzz_initialize_RevertOn_InvalidState(uint32 minAssetsLockSeconds) external { + _context.state = State.SignallingEscrow; + + // TODO: not very informative, maybe need to change to `revert UnexpectedState(self.state);` + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.NotInitialized)); + + EscrowState.initialize(_context, Durations.from(minAssetsLockSeconds)); + } + + // --- + // startRageQuit() + // --- + + function testFuzz_startRageQuit_happyPath( + uint32 rageQuitExtensionDelaySeconds, + uint32 rageQuitWithdrawalsTimelockSeconds + ) external { + Duration rageQuitExtensionDelay = Durations.from(rageQuitExtensionDelaySeconds); + Duration rageQuitWithdrawalsTimelock = Durations.from(rageQuitWithdrawalsTimelockSeconds); + + _context.state = State.SignallingEscrow; + + vm.expectEmit(); + emit EscrowState.EscrowStateChanged(State.SignallingEscrow, State.RageQuitEscrow); + emit EscrowState.RageQuitStarted(rageQuitExtensionDelay, rageQuitWithdrawalsTimelock); + + EscrowState.startRageQuit(_context, rageQuitExtensionDelay, rageQuitWithdrawalsTimelock); + + checkContext(State.RageQuitEscrow, D0, rageQuitExtensionDelay, rageQuitWithdrawalsTimelock, T0); + } + + function testFuzz_startRageQuit_RevertOn_InvalidState( + uint32 rageQuitExtensionDelaySeconds, + uint32 rageQuitWithdrawalsTimelockSeconds + ) external { + Duration rageQuitExtensionDelay = Durations.from(rageQuitExtensionDelaySeconds); + Duration rageQuitWithdrawalsTimelock = Durations.from(rageQuitWithdrawalsTimelockSeconds); + + _context.state = State.NotInitialized; + + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); + + EscrowState.startRageQuit(_context, rageQuitExtensionDelay, rageQuitWithdrawalsTimelock); + } + + // --- + // startRageQuitExtensionDelay() + // --- + + function test_startRageQuitExtensionDelay_happyPath() external { + vm.expectEmit(); + emit EscrowState.RageQuitTimelockStarted(); + + EscrowState.startRageQuitExtensionDelay(_context); + + checkContext(State.NotInitialized, D0, D0, D0, Timestamps.now()); + } + + // --- + // setMinAssetsLockDuration() + // --- + + function test_setMinAssetsLockDuration_happyPath(uint32 minAssetsLockSeconds) external { + vm.assume(minAssetsLockSeconds != 0); + Duration minAssetsLockDuration = Durations.from(minAssetsLockSeconds); + + vm.expectEmit(); + emit EscrowState.MinAssetsLockDurationSet(minAssetsLockDuration); + + EscrowState.setMinAssetsLockDuration(_context, minAssetsLockDuration); + + checkContext(State.NotInitialized, minAssetsLockDuration, D0, D0, T0); + } + + function test_setMinAssetsLockDuration_WhenDurationNotChanged(uint32 minAssetsLockSeconds) external { + Duration minAssetsLockDuration = Durations.from(minAssetsLockSeconds); + + _context.minAssetsLockDuration = minAssetsLockDuration; + + Vm.Log[] memory entries = vm.getRecordedLogs(); + + EscrowState.setMinAssetsLockDuration(_context, minAssetsLockDuration); + + checkContext(State.NotInitialized, minAssetsLockDuration, D0, D0, T0); + + assertEq(entries.length, 0); + } + + // --- + // checkSignallingEscrow() + // --- + + function test_checkSignallingEscrow_happyPath() external { + _context.state = State.SignallingEscrow; + EscrowState.checkSignallingEscrow(_context); + } + + function test_checkSignallingEscrow_RevertOn_InvalidState() external { + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); + + EscrowState.checkSignallingEscrow(_context); + } + + // --- + // checkRageQuitEscrow() + // --- + + function test_checkRageQuitEscrow_happyPath() external { + _context.state = State.RageQuitEscrow; + EscrowState.checkRageQuitEscrow(_context); + } + + function test_checkRageQuitEscrow_RevertOn_InvalidState() external { + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.RageQuitEscrow)); + + EscrowState.checkRageQuitEscrow(_context); + } + + // --- + // checkBatchesClaimingInProgress() + // --- + + function test_checkBatchesClaimingInProgress_happyPath() external view { + EscrowState.checkBatchesClaimingInProgress(_context); + } + + function testFuzz_checkBatchesClaimingInProgress_RevertOn_InvalidState( + uint40 rageQuitExtensionDelayStartedAtSeconds + ) external { + vm.assume(rageQuitExtensionDelayStartedAtSeconds > 0); + _context.rageQuitExtensionDelayStartedAt = Timestamps.from(rageQuitExtensionDelayStartedAtSeconds); + vm.expectRevert(EscrowState.ClaimingIsFinished.selector); + + EscrowState.checkBatchesClaimingInProgress(_context); + } + + // --- + // checkWithdrawalsTimelockPassed() + // --- + + function testFuzz_checkWithdrawalsTimelockPassed_happyPath( + uint16 rageQuitExtensionDelayStartedAtSeconds, + uint16 rageQuitExtensionDelaySeconds, + uint16 rageQuitWithdrawalsTimelockSeconds + ) external { + vm.assume(rageQuitExtensionDelayStartedAtSeconds > 0); + + Duration rageQuitExtensionDelay = Durations.from(rageQuitExtensionDelaySeconds); + Duration rageQuitWithdrawalsTimelock = Durations.from(rageQuitWithdrawalsTimelockSeconds); + + _context.rageQuitExtensionDelayStartedAt = Timestamps.from(rageQuitExtensionDelayStartedAtSeconds); + _context.rageQuitExtensionDelay = rageQuitExtensionDelay; + _context.rageQuitWithdrawalsTimelock = rageQuitWithdrawalsTimelock; + + vm.warp( + uint256(rageQuitExtensionDelayStartedAtSeconds) + uint256(rageQuitExtensionDelaySeconds) + + uint256(rageQuitWithdrawalsTimelockSeconds) + 1 + ); + EscrowState.checkWithdrawalsTimelockPassed(_context); + } + + function test_checkWithdrawalsTimelockPassed_RevertWhen_RageQuitExtraTimelockNotStarted() external { + vm.expectRevert(EscrowState.RageQuitExtraTimelockNotStarted.selector); + + EscrowState.checkWithdrawalsTimelockPassed(_context); + } + + function testFuzz_checkWithdrawalsTimelockPassed_RevertWhen_WithdrawalsTimelockNotPassed( + uint16 rageQuitExtensionDelayStartedAtSeconds, + uint16 rageQuitExtensionDelaySeconds, + uint16 rageQuitWithdrawalsTimelockSeconds + ) external { + vm.assume(rageQuitExtensionDelayStartedAtSeconds > 0); + + Duration rageQuitExtensionDelay = Durations.from(rageQuitExtensionDelaySeconds); + Duration rageQuitWithdrawalsTimelock = Durations.from(rageQuitWithdrawalsTimelockSeconds); + + _context.rageQuitExtensionDelayStartedAt = Timestamps.from(rageQuitExtensionDelayStartedAtSeconds); + _context.rageQuitExtensionDelay = rageQuitExtensionDelay; + _context.rageQuitWithdrawalsTimelock = rageQuitWithdrawalsTimelock; + + vm.warp( + uint256(rageQuitExtensionDelayStartedAtSeconds) + uint256(rageQuitExtensionDelaySeconds) + + uint256(rageQuitWithdrawalsTimelockSeconds) + ); + + vm.expectRevert(EscrowState.WithdrawalsTimelockNotPassed.selector); + + EscrowState.checkWithdrawalsTimelockPassed(_context); + } + + function test_checkWithdrawalsTimelockPassed_RevertWhen_WithdrawalsTimelockOverflow() external { + Duration rageQuitExtensionDelay = Durations.from(DURATION_MAX_VALUE / 2); + Duration rageQuitWithdrawalsTimelock = Durations.from(DURATION_MAX_VALUE / 2 + 1); + + _context.rageQuitExtensionDelayStartedAt = Timestamps.from(MAX_TIMESTAMP_VALUE - 1); + _context.rageQuitExtensionDelay = rageQuitExtensionDelay; + _context.rageQuitWithdrawalsTimelock = rageQuitWithdrawalsTimelock; + + vm.expectRevert(TimestampOverflow.selector); + + EscrowState.checkWithdrawalsTimelockPassed(_context); + } + + // --- + // isRageQuitExtensionDelayStarted() + // --- + + function testFuzz_isRageQuitExtensionDelayStarted_happyPath(uint40 rageQuitExtensionDelayStartedAtSeconds) + external + { + _context.rageQuitExtensionDelayStartedAt = Timestamps.from(rageQuitExtensionDelayStartedAtSeconds); + bool res = EscrowState.isRageQuitExtensionDelayStarted(_context); + assert(res == _context.rageQuitExtensionDelayStartedAt.isNotZero()); + } + + // --- + // isRageQuitExtensionDelayPassed() + // --- + + function testFuzz_isRageQuitExtensionDelayPassed_ReturnsTrue( + uint16 rageQuitExtensionDelayStartedAtSeconds, + uint16 rageQuitExtensionDelaySeconds + ) external { + vm.assume(rageQuitExtensionDelayStartedAtSeconds > 0); + + Duration rageQuitExtensionDelay = Durations.from(rageQuitExtensionDelaySeconds); + + _context.rageQuitExtensionDelayStartedAt = Timestamps.from(rageQuitExtensionDelayStartedAtSeconds); + _context.rageQuitExtensionDelay = rageQuitExtensionDelay; + + vm.warp(uint256(rageQuitExtensionDelayStartedAtSeconds) + uint256(rageQuitExtensionDelaySeconds) + 1); + bool res = EscrowState.isRageQuitExtensionDelayPassed(_context); + assert(res == true); + } + + function testFuzz_isRageQuitExtensionDelayPassed_ReturnsFalse( + uint16 rageQuitExtensionDelayStartedAtSeconds, + uint16 rageQuitExtensionDelaySeconds + ) external { + vm.assume(rageQuitExtensionDelayStartedAtSeconds > 0); + + Duration rageQuitExtensionDelay = Durations.from(rageQuitExtensionDelaySeconds); + + _context.rageQuitExtensionDelayStartedAt = Timestamps.from(rageQuitExtensionDelayStartedAtSeconds); + _context.rageQuitExtensionDelay = rageQuitExtensionDelay; + + vm.warp(uint256(rageQuitExtensionDelayStartedAtSeconds) + uint256(rageQuitExtensionDelaySeconds)); + bool res = EscrowState.isRageQuitExtensionDelayPassed(_context); + assert(res == false); + } + + function test_isRageQuitExtensionDelayPassed_ReturnsFalseWhenRageQuitExtraTimelockNotStarted() external { + vm.warp(1234); + bool res = EscrowState.isRageQuitExtensionDelayPassed(_context); + assert(res == false); + } + + // --- + // isRageQuitEscrow() + // --- + + function testFuzz_isRageQuitEscrow(bool expectedResult) external { + if (expectedResult) { + _context.state = State.RageQuitEscrow; + } + bool actualResult = EscrowState.isRageQuitEscrow(_context); + assert(actualResult == expectedResult); + } + + // --- + // helpers() + // --- + + function checkContext( + State state, + Duration minAssetsLockDuration, + Duration rageQuitExtensionDelay, + Duration rageQuitWithdrawalsTimelock, + Timestamp rageQuitExtensionDelayStartedAt + ) internal view { + assert(_context.state == state); + assert(_context.minAssetsLockDuration == minAssetsLockDuration); + assert(_context.rageQuitExtensionDelay == rageQuitExtensionDelay); + assert(_context.rageQuitWithdrawalsTimelock == rageQuitWithdrawalsTimelock); + assert(_context.rageQuitExtensionDelayStartedAt == rageQuitExtensionDelayStartedAt); + } +} From a400f978785f6bed06425483a57586e81301dfac Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Mon, 12 Aug 2024 17:52:02 +0400 Subject: [PATCH 31/42] Fix AssetsAccounting tests after renaming --- test/unit/libraries/AssetsAccounting.t.sol | 582 +++++++++++---------- 1 file changed, 293 insertions(+), 289 deletions(-) diff --git a/test/unit/libraries/AssetsAccounting.t.sol b/test/unit/libraries/AssetsAccounting.t.sol index f41865cc..408d4674 100644 --- a/test/unit/libraries/AssetsAccounting.t.sol +++ b/test/unit/libraries/AssetsAccounting.t.sol @@ -17,7 +17,7 @@ import { import {UnitTest, Duration} from "test/utils/unit-test.sol"; contract AssetsAccountingUnitTests is UnitTest { - AssetsAccounting.Context private _accountingState; + AssetsAccounting.Context private _accountingContext; // --- // accountStETHSharesLock() @@ -34,19 +34,21 @@ contract AssetsAccountingUnitTests is UnitTest { SharesValue shares = SharesValues.from(sharesAmount); - _accountingState.stETHTotals.lockedShares = totalLockedShares; - _accountingState.assets[holder].stETHLockedShares = holderLockedShares; + _accountingContext.stETHTotals.lockedShares = totalLockedShares; + _accountingContext.assets[holder].stETHLockedShares = holderLockedShares; vm.expectEmit(); emit AssetsAccounting.StETHSharesLocked(holder, shares); - AssetsAccounting.accountStETHSharesLock(_accountingState, holder, shares); + AssetsAccounting.accountStETHSharesLock(_accountingContext, holder, shares); - checkAccountingStateTotalCounters(totalLockedShares + shares, ETHValues.ZERO, SharesValues.ZERO, ETHValues.ZERO); - assert(_accountingState.assets[holder].stETHLockedShares == holderLockedShares + shares); - assert(_accountingState.assets[holder].unstETHLockedShares == SharesValues.ZERO); - assert(_accountingState.assets[holder].lastAssetsLockTimestamp <= Timestamps.now()); - assert(_accountingState.assets[holder].unstETHIds.length == 0); + checkAccountingContextTotalCounters( + totalLockedShares + shares, ETHValues.ZERO, SharesValues.ZERO, ETHValues.ZERO + ); + assert(_accountingContext.assets[holder].stETHLockedShares == holderLockedShares + shares); + assert(_accountingContext.assets[holder].unstETHLockedShares == SharesValues.ZERO); + assert(_accountingContext.assets[holder].lastAssetsLockTimestamp <= Timestamps.now()); + assert(_accountingContext.assets[holder].unstETHIds.length == 0); } function testFuzz_accountStETHSharesLock_RevertWhen_ZeroSharesProvided(address holder) external { @@ -54,7 +56,7 @@ contract AssetsAccountingUnitTests is UnitTest { vm.expectRevert(abi.encodeWithSelector(AssetsAccounting.InvalidSharesValue.selector, shares)); - AssetsAccounting.accountStETHSharesLock(_accountingState, holder, shares); + AssetsAccounting.accountStETHSharesLock(_accountingContext, holder, shares); } function testFuzz_accountStETHSharesLock_WhenNoSharesWereLockedBefore( @@ -68,20 +70,20 @@ contract AssetsAccountingUnitTests is UnitTest { SharesValue shares = SharesValues.from(sharesAmount); - _accountingState.stETHTotals.lockedShares = totalLockedShares; + _accountingContext.stETHTotals.lockedShares = totalLockedShares; vm.expectEmit(); emit AssetsAccounting.StETHSharesLocked(stranger, shares); - AssetsAccounting.accountStETHSharesLock(_accountingState, stranger, shares); + AssetsAccounting.accountStETHSharesLock(_accountingContext, stranger, shares); - assert(_accountingState.stETHTotals.lockedShares == totalLockedShares + shares); - assert(_accountingState.assets[stranger].stETHLockedShares == shares); - assert(_accountingState.assets[stranger].lastAssetsLockTimestamp <= Timestamps.now()); + assert(_accountingContext.stETHTotals.lockedShares == totalLockedShares + shares); + assert(_accountingContext.assets[stranger].stETHLockedShares == shares); + assert(_accountingContext.assets[stranger].lastAssetsLockTimestamp <= Timestamps.now()); } // --- - // accountStETHSharesUnlock(State storage self, address holder, SharesValue shares) + // accountStETHSharesUnlock(Context storage self, address holder, SharesValue shares) // --- function testFuzz_accountStETHSharesUnlock_happyPath( @@ -98,19 +100,21 @@ contract AssetsAccountingUnitTests is UnitTest { SharesValue holderLockedShares = SharesValues.from(holderSharesAmount); SharesValue totalLockedShares = totalLockedSharesWithoutHolder + holderLockedShares; - _accountingState.stETHTotals.lockedShares = totalLockedShares; - _accountingState.assets[holder].stETHLockedShares = holderLockedShares; + _accountingContext.stETHTotals.lockedShares = totalLockedShares; + _accountingContext.assets[holder].stETHLockedShares = holderLockedShares; vm.expectEmit(); emit AssetsAccounting.StETHSharesUnlocked(holder, shares); - AssetsAccounting.accountStETHSharesUnlock(_accountingState, holder, shares); + AssetsAccounting.accountStETHSharesUnlock(_accountingContext, holder, shares); - checkAccountingStateTotalCounters(totalLockedShares - shares, ETHValues.ZERO, SharesValues.ZERO, ETHValues.ZERO); - assert(_accountingState.assets[holder].stETHLockedShares == holderLockedShares - shares); - assert(_accountingState.assets[holder].unstETHLockedShares == SharesValues.ZERO); - assert(_accountingState.assets[holder].lastAssetsLockTimestamp == Timestamps.ZERO); - assert(_accountingState.assets[holder].unstETHIds.length == 0); + checkAccountingContextTotalCounters( + totalLockedShares - shares, ETHValues.ZERO, SharesValues.ZERO, ETHValues.ZERO + ); + assert(_accountingContext.assets[holder].stETHLockedShares == holderLockedShares - shares); + assert(_accountingContext.assets[holder].unstETHLockedShares == SharesValues.ZERO); + assert(_accountingContext.assets[holder].lastAssetsLockTimestamp == Timestamps.ZERO); + assert(_accountingContext.assets[holder].unstETHIds.length == 0); } function testFuzz_accountStETHSharesUnlock_RevertOn_ZeroSharesProvided(address holder) external { @@ -118,7 +122,7 @@ contract AssetsAccountingUnitTests is UnitTest { vm.expectRevert(abi.encodeWithSelector(AssetsAccounting.InvalidSharesValue.selector, shares)); - AssetsAccounting.accountStETHSharesUnlock(_accountingState, holder, shares); + AssetsAccounting.accountStETHSharesUnlock(_accountingContext, holder, shares); } function testFuzz_accountStETHSharesUnlock_RevertWhen_HolderHaveLessSharesThanProvided( @@ -135,12 +139,12 @@ contract AssetsAccountingUnitTests is UnitTest { SharesValue holderLockedShares = SharesValues.from(holderSharesAmount); SharesValue totalLockedShares = totalLockedSharesWithoutHolder + holderLockedShares; - _accountingState.stETHTotals.lockedShares = totalLockedShares; - _accountingState.assets[holder].stETHLockedShares = holderLockedShares; + _accountingContext.stETHTotals.lockedShares = totalLockedShares; + _accountingContext.assets[holder].stETHLockedShares = holderLockedShares; vm.expectRevert(abi.encodeWithSelector(AssetsAccounting.InvalidSharesValue.selector, shares)); - AssetsAccounting.accountStETHSharesUnlock(_accountingState, holder, shares); + AssetsAccounting.accountStETHSharesUnlock(_accountingContext, holder, shares); } function testFuzz_accountStETHSharesUnlock_RevertOn_AccountingError_TotalLockedSharesCounterIsLessThanProvidedSharesAmount( @@ -155,12 +159,12 @@ contract AssetsAccountingUnitTests is UnitTest { SharesValue holderLockedShares = SharesValues.from(sharesAmount); SharesValue totalLockedShares = SharesValues.from(totalSharesAmount); - _accountingState.stETHTotals.lockedShares = totalLockedShares; - _accountingState.assets[holder].stETHLockedShares = holderLockedShares; + _accountingContext.stETHTotals.lockedShares = totalLockedShares; + _accountingContext.assets[holder].stETHLockedShares = holderLockedShares; vm.expectRevert(stdError.arithmeticError); - AssetsAccounting.accountStETHSharesUnlock(_accountingState, holder, shares); + AssetsAccounting.accountStETHSharesUnlock(_accountingContext, holder, shares); } function testFuzz_accountStETHSharesUnlock_RevertWhen_NoSharesWereLockedBefore( @@ -173,11 +177,11 @@ contract AssetsAccountingUnitTests is UnitTest { vm.expectRevert(abi.encodeWithSelector(AssetsAccounting.InvalidSharesValue.selector, shares)); - AssetsAccounting.accountStETHSharesUnlock(_accountingState, stranger, shares); + AssetsAccounting.accountStETHSharesUnlock(_accountingContext, stranger, shares); } // --- - // accountStETHSharesUnlock(State storage self, address holder) + // accountStETHSharesUnlock(Context storage self, address holder) // --- function testFuzz_accountStETHSharesUnlock_simple_happyPath(address holder, uint128 holderSharesAmount) external { @@ -188,28 +192,28 @@ contract AssetsAccountingUnitTests is UnitTest { SharesValue holderLockedShares = SharesValues.from(holderSharesAmount); SharesValue totalLockedShares = totalLockedSharesWithoutHolder + holderLockedShares; - _accountingState.stETHTotals.lockedShares = totalLockedShares; - _accountingState.assets[holder].stETHLockedShares = holderLockedShares; + _accountingContext.stETHTotals.lockedShares = totalLockedShares; + _accountingContext.assets[holder].stETHLockedShares = holderLockedShares; vm.expectEmit(); emit AssetsAccounting.StETHSharesUnlocked(holder, holderLockedShares); - SharesValue unlockedShares = AssetsAccounting.accountStETHSharesUnlock(_accountingState, holder); + SharesValue unlockedShares = AssetsAccounting.accountStETHSharesUnlock(_accountingContext, holder); assert(unlockedShares == holderLockedShares); - checkAccountingStateTotalCounters( + checkAccountingContextTotalCounters( totalLockedShares - holderLockedShares, ETHValues.ZERO, SharesValues.ZERO, ETHValues.ZERO ); - assert(_accountingState.assets[holder].stETHLockedShares == holderLockedShares - holderLockedShares); - assert(_accountingState.assets[holder].unstETHLockedShares == SharesValues.ZERO); - assert(_accountingState.assets[holder].lastAssetsLockTimestamp == Timestamps.ZERO); - assert(_accountingState.assets[holder].unstETHIds.length == 0); + assert(_accountingContext.assets[holder].stETHLockedShares == holderLockedShares - holderLockedShares); + assert(_accountingContext.assets[holder].unstETHLockedShares == SharesValues.ZERO); + assert(_accountingContext.assets[holder].lastAssetsLockTimestamp == Timestamps.ZERO); + assert(_accountingContext.assets[holder].unstETHIds.length == 0); } function testFuzz_accountStETHSharesUnlock_simple_RevertWhen_NoSharesWereLockedBefore(address stranger) external { vm.expectRevert(abi.encodeWithSelector(AssetsAccounting.InvalidSharesValue.selector, SharesValues.ZERO)); - AssetsAccounting.accountStETHSharesUnlock(_accountingState, stranger); + AssetsAccounting.accountStETHSharesUnlock(_accountingContext, stranger); } // --- @@ -230,9 +234,9 @@ contract AssetsAccountingUnitTests is UnitTest { SharesValue totalLockedShares = SharesValues.from(totalLockedSharesAmount); ETHValue totalClaimedETH = ETHValues.from(totalClaimedETHAmount); - _accountingState.assets[holder].stETHLockedShares = holderLockedShares; - _accountingState.stETHTotals.lockedShares = totalLockedShares; - _accountingState.stETHTotals.claimedETH = totalClaimedETH; + _accountingContext.assets[holder].stETHLockedShares = holderLockedShares; + _accountingContext.stETHTotals.lockedShares = totalLockedShares; + _accountingContext.stETHTotals.claimedETH = totalClaimedETH; ETHValue expectedETHWithdrawn = ETHValues.from((uint256(totalClaimedETHAmount) * holderLockedSharesAmount) / totalLockedSharesAmount); @@ -240,20 +244,20 @@ contract AssetsAccountingUnitTests is UnitTest { vm.expectEmit(); emit AssetsAccounting.ETHWithdrawn(holder, holderLockedShares, expectedETHWithdrawn); - ETHValue ethWithdrawn = AssetsAccounting.accountStETHSharesWithdraw(_accountingState, holder); + ETHValue ethWithdrawn = AssetsAccounting.accountStETHSharesWithdraw(_accountingContext, holder); assert(ethWithdrawn == expectedETHWithdrawn); - checkAccountingStateTotalCounters(totalLockedShares, totalClaimedETH, SharesValues.ZERO, ETHValues.ZERO); - assert(_accountingState.assets[holder].stETHLockedShares == SharesValues.ZERO); - assert(_accountingState.assets[holder].unstETHLockedShares == SharesValues.ZERO); - assert(_accountingState.assets[holder].lastAssetsLockTimestamp == Timestamps.ZERO); - assert(_accountingState.assets[holder].unstETHIds.length == 0); + checkAccountingContextTotalCounters(totalLockedShares, totalClaimedETH, SharesValues.ZERO, ETHValues.ZERO); + assert(_accountingContext.assets[holder].stETHLockedShares == SharesValues.ZERO); + assert(_accountingContext.assets[holder].unstETHLockedShares == SharesValues.ZERO); + assert(_accountingContext.assets[holder].lastAssetsLockTimestamp == Timestamps.ZERO); + assert(_accountingContext.assets[holder].unstETHIds.length == 0); } function testFuzz_accountStETHSharesWithdraw_RevertWhen_HolderHaveZeroShares(address stranger) external { vm.expectRevert(abi.encodeWithSelector(AssetsAccounting.InvalidSharesValue.selector, SharesValues.ZERO)); - AssetsAccounting.accountStETHSharesWithdraw(_accountingState, stranger); + AssetsAccounting.accountStETHSharesWithdraw(_accountingContext, stranger); } function testFuzz_accountStETHSharesWithdraw_RevertOn_AccountingError_TotalLockedSharesCounterIsZero( @@ -266,13 +270,13 @@ contract AssetsAccountingUnitTests is UnitTest { SharesValue holderLockedShares = SharesValues.from(holderLockedSharesAmount); ETHValue totalClaimedETH = ETHValues.from(totalClaimedETHAmount); - _accountingState.assets[holder].stETHLockedShares = holderLockedShares; - _accountingState.stETHTotals.lockedShares = SharesValues.ZERO; - _accountingState.stETHTotals.claimedETH = totalClaimedETH; + _accountingContext.assets[holder].stETHLockedShares = holderLockedShares; + _accountingContext.stETHTotals.lockedShares = SharesValues.ZERO; + _accountingContext.stETHTotals.claimedETH = totalClaimedETH; vm.expectRevert(stdError.divisionError); - AssetsAccounting.accountStETHSharesWithdraw(_accountingState, holder); + AssetsAccounting.accountStETHSharesWithdraw(_accountingContext, holder); } function testFuzz_accountStETHSharesWithdraw_AccountingError_WithdrawAmountMoreThanTotalClaimedETH( @@ -289,9 +293,9 @@ contract AssetsAccountingUnitTests is UnitTest { SharesValue totalLockedShares = SharesValues.from(totalLockedSharesAmount); ETHValue totalClaimedETH = ETHValues.from(totalClaimedETHAmount); - _accountingState.assets[holder].stETHLockedShares = holderLockedShares; - _accountingState.stETHTotals.lockedShares = totalLockedShares; - _accountingState.stETHTotals.claimedETH = totalClaimedETH; + _accountingContext.assets[holder].stETHLockedShares = holderLockedShares; + _accountingContext.stETHTotals.lockedShares = totalLockedShares; + _accountingContext.stETHTotals.claimedETH = totalClaimedETH; ETHValue expectedETHWithdrawn = ETHValues.from((uint256(totalClaimedETHAmount) * holderLockedSharesAmount) / totalLockedSharesAmount); @@ -299,11 +303,11 @@ contract AssetsAccountingUnitTests is UnitTest { vm.expectEmit(); emit AssetsAccounting.ETHWithdrawn(holder, holderLockedShares, expectedETHWithdrawn); - ETHValue ethWithdrawn = AssetsAccounting.accountStETHSharesWithdraw(_accountingState, holder); + ETHValue ethWithdrawn = AssetsAccounting.accountStETHSharesWithdraw(_accountingContext, holder); assert(ethWithdrawn == expectedETHWithdrawn); assert(ethWithdrawn.toUint256() >= totalClaimedETHAmount); - assert(_accountingState.assets[holder].stETHLockedShares == SharesValues.ZERO); + assert(_accountingContext.assets[holder].stETHLockedShares == SharesValues.ZERO); } function testFuzz_accountStETHSharesWithdraw_RevertOn_AccountingError_WithdrawAmountOverflow(address holder) @@ -313,13 +317,13 @@ contract AssetsAccountingUnitTests is UnitTest { SharesValue totalLockedShares = SharesValues.from(1); ETHValue totalClaimedETH = ETHValues.from(type(uint96).max); - _accountingState.assets[holder].stETHLockedShares = holderLockedShares; - _accountingState.stETHTotals.lockedShares = totalLockedShares; - _accountingState.stETHTotals.claimedETH = totalClaimedETH; + _accountingContext.assets[holder].stETHLockedShares = holderLockedShares; + _accountingContext.stETHTotals.lockedShares = totalLockedShares; + _accountingContext.stETHTotals.claimedETH = totalClaimedETH; vm.expectRevert(ETHValueOverflow.selector); - AssetsAccounting.accountStETHSharesWithdraw(_accountingState, holder); + AssetsAccounting.accountStETHSharesWithdraw(_accountingContext, holder); } // --- @@ -333,14 +337,14 @@ contract AssetsAccountingUnitTests is UnitTest { ETHValue amount = ETHValues.from(ethAmount); ETHValue totalClaimedETH = ETHValues.from(totalClaimedETHAmount); - _accountingState.stETHTotals.claimedETH = totalClaimedETH; + _accountingContext.stETHTotals.claimedETH = totalClaimedETH; vm.expectEmit(); emit AssetsAccounting.ETHClaimed(amount); - AssetsAccounting.accountClaimedStETH(_accountingState, amount); + AssetsAccounting.accountClaimedStETH(_accountingContext, amount); - checkAccountingStateTotalCounters( + checkAccountingContextTotalCounters( SharesValues.ZERO, totalClaimedETH + amount, SharesValues.ZERO, ETHValues.ZERO ); } @@ -363,9 +367,9 @@ contract AssetsAccountingUnitTests is UnitTest { SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); uint256 expectedTotalUnstETHLockedAmount = 0; - _accountingState.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; - _accountingState.assets[holder].unstETHIds.push(genRandomUnstEthId(1024)); - _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; + _accountingContext.assets[holder].unstETHIds.push(genRandomUnstEthId(1024)); + _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; WithdrawalRequestStatus[] memory withdrawalRequestStatuses = new WithdrawalRequestStatus[](amountsOfShares.length); @@ -376,33 +380,33 @@ contract AssetsAccountingUnitTests is UnitTest { withdrawalRequestStatuses[i].amountOfShares = amountsOfShares[i]; withdrawalRequestStatuses[i].isFinalized = false; withdrawalRequestStatuses[i].isClaimed = false; - _accountingState.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.NotLocked; + _accountingContext.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.NotLocked; expectedTotalUnstETHLockedAmount += amountsOfShares[i]; } vm.expectEmit(); emit AssetsAccounting.UnstETHLocked(holder, unstETHIds, SharesValues.from(expectedTotalUnstETHLockedAmount)); - AssetsAccounting.accountUnstETHLock(_accountingState, holder, unstETHIds, withdrawalRequestStatuses); + AssetsAccounting.accountUnstETHLock(_accountingContext, holder, unstETHIds, withdrawalRequestStatuses); - checkAccountingStateTotalCounters( + checkAccountingContextTotalCounters( SharesValues.ZERO, ETHValues.ZERO, initialTotalUnfinalizedShares + SharesValues.from(expectedTotalUnstETHLockedAmount), ETHValues.ZERO ); - assert(_accountingState.assets[holder].stETHLockedShares == SharesValues.ZERO); + assert(_accountingContext.assets[holder].stETHLockedShares == SharesValues.ZERO); assert( - _accountingState.assets[holder].unstETHLockedShares + _accountingContext.assets[holder].unstETHLockedShares == holderUnstETHLockedShares + SharesValues.from(expectedTotalUnstETHLockedAmount) ); - assert(_accountingState.assets[holder].lastAssetsLockTimestamp <= Timestamps.now()); - assert(_accountingState.assets[holder].unstETHIds.length == amountsOfShares.length + 1); + assert(_accountingContext.assets[holder].lastAssetsLockTimestamp <= Timestamps.now()); + assert(_accountingContext.assets[holder].unstETHIds.length == amountsOfShares.length + 1); for (uint256 i = 0; i < unstETHIds.length; ++i) { - assert(_accountingState.unstETHRecords[unstETHIds[i]].lockedBy == holder); - assert(_accountingState.unstETHRecords[unstETHIds[i]].status == UnstETHRecordStatus.Locked); - assert(_accountingState.unstETHRecords[unstETHIds[i]].index.toZeroBasedValue() == i + 1); - assert(_accountingState.unstETHRecords[unstETHIds[i]].shares == SharesValues.from(amountsOfShares[i])); - assert(_accountingState.unstETHRecords[unstETHIds[i]].claimableAmount == ETHValues.ZERO); + assert(_accountingContext.unstETHRecords[unstETHIds[i]].lockedBy == holder); + assert(_accountingContext.unstETHRecords[unstETHIds[i]].status == UnstETHRecordStatus.Locked); + assert(_accountingContext.unstETHRecords[unstETHIds[i]].index.toZeroBasedValue() == i + 1); + assert(_accountingContext.unstETHRecords[unstETHIds[i]].shares == SharesValues.from(amountsOfShares[i])); + assert(_accountingContext.unstETHRecords[unstETHIds[i]].claimableAmount == ETHValues.ZERO); } } @@ -414,7 +418,7 @@ contract AssetsAccountingUnitTests is UnitTest { vm.expectRevert(); - AssetsAccounting.accountUnstETHLock(_accountingState, holder, unstETHIds, withdrawalRequestStatuses); + AssetsAccounting.accountUnstETHLock(_accountingContext, holder, unstETHIds, withdrawalRequestStatuses); } function testFuzz_accountUnstETHLock_RevertOn_WithdrawalRequestStatusIsFinalized( @@ -429,8 +433,8 @@ contract AssetsAccountingUnitTests is UnitTest { SharesValue holderUnstETHLockedShares = SharesValues.from(holderUnstETHLockedSharesAmount); SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); - _accountingState.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; - _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; + _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; WithdrawalRequestStatus[] memory withdrawalRequestStatuses = new WithdrawalRequestStatus[](amountsOfShares.length); @@ -441,7 +445,7 @@ contract AssetsAccountingUnitTests is UnitTest { withdrawalRequestStatuses[i].amountOfShares = amountsOfShares[i]; withdrawalRequestStatuses[i].isFinalized = false; withdrawalRequestStatuses[i].isClaimed = false; - _accountingState.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.NotLocked; + _accountingContext.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.NotLocked; } withdrawalRequestStatuses[withdrawalRequestStatuses.length - 1].isFinalized = true; @@ -454,7 +458,7 @@ contract AssetsAccountingUnitTests is UnitTest { ) ); - AssetsAccounting.accountUnstETHLock(_accountingState, holder, unstETHIds, withdrawalRequestStatuses); + AssetsAccounting.accountUnstETHLock(_accountingContext, holder, unstETHIds, withdrawalRequestStatuses); } function testFuzz_accountUnstETHLock_RevertOn_WithdrawalRequestStatusIsClaimed( @@ -469,8 +473,8 @@ contract AssetsAccountingUnitTests is UnitTest { SharesValue holderUnstETHLockedShares = SharesValues.from(holderUnstETHLockedSharesAmount); SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); - _accountingState.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; - _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; + _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; WithdrawalRequestStatus[] memory withdrawalRequestStatuses = new WithdrawalRequestStatus[](amountsOfShares.length); @@ -481,14 +485,14 @@ contract AssetsAccountingUnitTests is UnitTest { withdrawalRequestStatuses[i].amountOfShares = amountsOfShares[i]; withdrawalRequestStatuses[i].isFinalized = false; withdrawalRequestStatuses[i].isClaimed = false; - _accountingState.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.NotLocked; + _accountingContext.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.NotLocked; } withdrawalRequestStatuses[withdrawalRequestStatuses.length - 1].isClaimed = true; vm.expectRevert(); - AssetsAccounting.accountUnstETHLock(_accountingState, holder, unstETHIds, withdrawalRequestStatuses); + AssetsAccounting.accountUnstETHLock(_accountingContext, holder, unstETHIds, withdrawalRequestStatuses); } function testFuzz_accountUnstETHLock_RevertOn_UnstETHRecordStatusIsNot_NotLocked( @@ -503,8 +507,8 @@ contract AssetsAccountingUnitTests is UnitTest { SharesValue holderUnstETHLockedShares = SharesValues.from(holderUnstETHLockedSharesAmount); SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); - _accountingState.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; - _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; + _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; WithdrawalRequestStatus[] memory withdrawalRequestStatuses = new WithdrawalRequestStatus[](amountsOfShares.length); @@ -515,10 +519,10 @@ contract AssetsAccountingUnitTests is UnitTest { withdrawalRequestStatuses[i].amountOfShares = amountsOfShares[i]; withdrawalRequestStatuses[i].isFinalized = false; withdrawalRequestStatuses[i].isClaimed = false; - _accountingState.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.NotLocked; + _accountingContext.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.NotLocked; } - _accountingState.unstETHRecords[unstETHIds[unstETHIds.length - 1]].status = UnstETHRecordStatus.Withdrawn; + _accountingContext.unstETHRecords[unstETHIds[unstETHIds.length - 1]].status = UnstETHRecordStatus.Withdrawn; vm.expectRevert( abi.encodeWithSelector( @@ -528,7 +532,7 @@ contract AssetsAccountingUnitTests is UnitTest { ) ); - AssetsAccounting.accountUnstETHLock(_accountingState, holder, unstETHIds, withdrawalRequestStatuses); + AssetsAccounting.accountUnstETHLock(_accountingContext, holder, unstETHIds, withdrawalRequestStatuses); } function testFuzz_accountUnstETHLock_RevertWhen_DuplicatingUnstETHIdsProvided( @@ -543,8 +547,8 @@ contract AssetsAccountingUnitTests is UnitTest { SharesValue holderUnstETHLockedShares = SharesValues.from(holderUnstETHLockedSharesAmount); SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); - _accountingState.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; - _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; + _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; WithdrawalRequestStatus[] memory withdrawalRequestStatuses = new WithdrawalRequestStatus[](amountsOfShares.length); @@ -555,7 +559,7 @@ contract AssetsAccountingUnitTests is UnitTest { withdrawalRequestStatuses[i].amountOfShares = amountsOfShares[i]; withdrawalRequestStatuses[i].isFinalized = false; withdrawalRequestStatuses[i].isClaimed = false; - _accountingState.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.NotLocked; + _accountingContext.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.NotLocked; } unstETHIds[unstETHIds.length - 1] = unstETHIds[unstETHIds.length - 2]; @@ -568,7 +572,7 @@ contract AssetsAccountingUnitTests is UnitTest { ) ); - AssetsAccounting.accountUnstETHLock(_accountingState, holder, unstETHIds, withdrawalRequestStatuses); + AssetsAccounting.accountUnstETHLock(_accountingContext, holder, unstETHIds, withdrawalRequestStatuses); } // TODO: is it expected behavior? @@ -580,23 +584,23 @@ contract AssetsAccountingUnitTests is UnitTest { SharesValue holderUnstETHLockedShares = SharesValues.from(holderUnstETHLockedSharesAmount); SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); - _accountingState.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; - _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; + _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; WithdrawalRequestStatus[] memory withdrawalRequestStatuses = new WithdrawalRequestStatus[](0); uint256[] memory unstETHIds = new uint256[](0); vm.expectEmit(); emit AssetsAccounting.UnstETHLocked(holder, unstETHIds, SharesValues.ZERO); - AssetsAccounting.accountUnstETHLock(_accountingState, holder, unstETHIds, withdrawalRequestStatuses); + AssetsAccounting.accountUnstETHLock(_accountingContext, holder, unstETHIds, withdrawalRequestStatuses); - checkAccountingStateTotalCounters( + checkAccountingContextTotalCounters( SharesValues.ZERO, ETHValues.ZERO, initialTotalUnfinalizedShares, ETHValues.ZERO ); - assert(_accountingState.assets[holder].stETHLockedShares == SharesValues.ZERO); - assert(_accountingState.assets[holder].unstETHLockedShares == holderUnstETHLockedShares); - assert(_accountingState.assets[holder].lastAssetsLockTimestamp <= Timestamps.now()); - assert(_accountingState.assets[holder].unstETHIds.length == 0); + assert(_accountingContext.assets[holder].stETHLockedShares == SharesValues.ZERO); + assert(_accountingContext.assets[holder].unstETHLockedShares == holderUnstETHLockedShares); + assert(_accountingContext.assets[holder].lastAssetsLockTimestamp <= Timestamps.now()); + assert(_accountingContext.assets[holder].unstETHIds.length == 0); } function testFuzz_accountUnstETHLock_AccountingError_WithdrawalRequestStatusAmountOfSharesOverflow( @@ -607,8 +611,8 @@ contract AssetsAccountingUnitTests is UnitTest { SharesValue holderUnstETHLockedShares = SharesValues.from(holderUnstETHLockedSharesAmount); SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); - _accountingState.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; - _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; + _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; WithdrawalRequestStatus[] memory withdrawalRequestStatuses = new WithdrawalRequestStatus[](1); uint256[] memory unstETHIds = new uint256[](1); @@ -617,11 +621,11 @@ contract AssetsAccountingUnitTests is UnitTest { withdrawalRequestStatuses[0].amountOfShares = uint256(type(uint128).max) + 1; withdrawalRequestStatuses[0].isFinalized = false; withdrawalRequestStatuses[0].isClaimed = false; - _accountingState.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.NotLocked; + _accountingContext.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.NotLocked; vm.expectRevert(SharesValueOverflow.selector); - AssetsAccounting.accountUnstETHLock(_accountingState, holder, unstETHIds, withdrawalRequestStatuses); + AssetsAccounting.accountUnstETHLock(_accountingContext, holder, unstETHIds, withdrawalRequestStatuses); } function testFuzz_accountUnstETHLock_AccountingError_HolderUnstETHLockedSharesOverflow( @@ -631,8 +635,8 @@ contract AssetsAccountingUnitTests is UnitTest { SharesValue holderUnstETHLockedShares = SharesValues.from(type(uint128).max / 2 + 1); SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); - _accountingState.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; - _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; + _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; WithdrawalRequestStatus[] memory withdrawalRequestStatuses = new WithdrawalRequestStatus[](1); uint256[] memory unstETHIds = new uint256[](1); @@ -641,11 +645,11 @@ contract AssetsAccountingUnitTests is UnitTest { withdrawalRequestStatuses[0].amountOfShares = uint128(type(uint128).max / 2) + 1; withdrawalRequestStatuses[0].isFinalized = false; withdrawalRequestStatuses[0].isClaimed = false; - _accountingState.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.NotLocked; + _accountingContext.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.NotLocked; vm.expectRevert(stdError.arithmeticError); - AssetsAccounting.accountUnstETHLock(_accountingState, holder, unstETHIds, withdrawalRequestStatuses); + AssetsAccounting.accountUnstETHLock(_accountingContext, holder, unstETHIds, withdrawalRequestStatuses); } function testFuzz_accountUnstETHLock_AccountingError_TotalUnfinalizedSharesOverflow( @@ -655,8 +659,8 @@ contract AssetsAccountingUnitTests is UnitTest { SharesValue holderUnstETHLockedShares = SharesValues.from(holderUnstETHLockedSharesAmount); SharesValue initialTotalUnfinalizedShares = SharesValues.from(type(uint128).max / 2 + 1); - _accountingState.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; - _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; + _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; WithdrawalRequestStatus[] memory withdrawalRequestStatuses = new WithdrawalRequestStatus[](1); uint256[] memory unstETHIds = new uint256[](1); @@ -665,11 +669,11 @@ contract AssetsAccountingUnitTests is UnitTest { withdrawalRequestStatuses[0].amountOfShares = uint128(type(uint128).max / 2) + 1; withdrawalRequestStatuses[0].isFinalized = false; withdrawalRequestStatuses[0].isClaimed = false; - _accountingState.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.NotLocked; + _accountingContext.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.NotLocked; vm.expectRevert(stdError.arithmeticError); - AssetsAccounting.accountUnstETHLock(_accountingState, holder, unstETHIds, withdrawalRequestStatuses); + AssetsAccounting.accountUnstETHLock(_accountingContext, holder, unstETHIds, withdrawalRequestStatuses); } // --- @@ -694,19 +698,19 @@ contract AssetsAccountingUnitTests is UnitTest { SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); uint256 expectedTotalSharesUnlockedAmount = 0; - _accountingState.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; - _accountingState.unstETHTotals.finalizedETH = initialTotalFinalizedETH; - _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; + _accountingContext.unstETHTotals.finalizedETH = initialTotalFinalizedETH; + _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; uint256[] memory unstETHIds = new uint256[](amountsOfShares.length); for (uint256 i = 0; i < amountsOfShares.length; ++i) { unstETHIds[i] = genRandomUnstEthId(i); - _accountingState.unstETHRecords[unstETHIds[i]].lockedBy = holder; - _accountingState.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.Locked; - _accountingState.unstETHRecords[unstETHIds[i]].shares = SharesValues.from(amountsOfShares[i]); - _accountingState.unstETHRecords[unstETHIds[i]].index = IndicesOneBased.fromOneBasedValue(i + 1); - _accountingState.assets[holder].unstETHIds.push(unstETHIds[i]); + _accountingContext.unstETHRecords[unstETHIds[i]].lockedBy = holder; + _accountingContext.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.Locked; + _accountingContext.unstETHRecords[unstETHIds[i]].shares = SharesValues.from(amountsOfShares[i]); + _accountingContext.unstETHRecords[unstETHIds[i]].index = IndicesOneBased.fromOneBasedValue(i + 1); + _accountingContext.assets[holder].unstETHIds.push(unstETHIds[i]); expectedTotalSharesUnlockedAmount += amountsOfShares[i]; } @@ -715,23 +719,23 @@ contract AssetsAccountingUnitTests is UnitTest { holder, unstETHIds, SharesValues.from(expectedTotalSharesUnlockedAmount), ETHValues.ZERO ); - AssetsAccounting.accountUnstETHUnlock(_accountingState, holder, unstETHIds); + AssetsAccounting.accountUnstETHUnlock(_accountingContext, holder, unstETHIds); - checkAccountingStateTotalCounters( + checkAccountingContextTotalCounters( SharesValues.ZERO, ETHValues.ZERO, initialTotalUnfinalizedShares - SharesValues.from(expectedTotalSharesUnlockedAmount), initialTotalFinalizedETH ); - assert(_accountingState.assets[holder].stETHLockedShares == SharesValues.ZERO); + assert(_accountingContext.assets[holder].stETHLockedShares == SharesValues.ZERO); assert( - _accountingState.assets[holder].unstETHLockedShares + _accountingContext.assets[holder].unstETHLockedShares == holderUnstETHLockedShares - SharesValues.from(expectedTotalSharesUnlockedAmount) ); - assert(_accountingState.assets[holder].lastAssetsLockTimestamp == Timestamps.ZERO); - assert(_accountingState.assets[holder].unstETHIds.length == 0); + assert(_accountingContext.assets[holder].lastAssetsLockTimestamp == Timestamps.ZERO); + assert(_accountingContext.assets[holder].unstETHIds.length == 0); for (uint256 i = 0; i < unstETHIds.length; ++i) { - assert(_accountingState.unstETHRecords[unstETHIds[i]].shares == SharesValues.ZERO); + assert(_accountingContext.unstETHRecords[unstETHIds[i]].shares == SharesValues.ZERO); } } @@ -753,20 +757,20 @@ contract AssetsAccountingUnitTests is UnitTest { SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); uint256 expectedTotalSharesUnlockedAmount = 0; - _accountingState.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; - _accountingState.unstETHTotals.finalizedETH = initialTotalFinalizedETH; - _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; + _accountingContext.unstETHTotals.finalizedETH = initialTotalFinalizedETH; + _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; uint256[] memory unstETHIds = new uint256[](amountsOfShares.length); for (uint256 i = 0; i < amountsOfShares.length; ++i) { unstETHIds[i] = genRandomUnstEthId(i); - _accountingState.unstETHRecords[unstETHIds[i]].lockedBy = holder; - _accountingState.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.Finalized; - _accountingState.unstETHRecords[unstETHIds[i]].shares = SharesValues.from(amountsOfShares[i]); - _accountingState.unstETHRecords[unstETHIds[i]].claimableAmount = ETHValues.from(amountsOfShares[i]); - _accountingState.unstETHRecords[unstETHIds[i]].index = IndicesOneBased.fromOneBasedValue(i + 1); - _accountingState.assets[holder].unstETHIds.push(unstETHIds[i]); + _accountingContext.unstETHRecords[unstETHIds[i]].lockedBy = holder; + _accountingContext.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.Finalized; + _accountingContext.unstETHRecords[unstETHIds[i]].shares = SharesValues.from(amountsOfShares[i]); + _accountingContext.unstETHRecords[unstETHIds[i]].claimableAmount = ETHValues.from(amountsOfShares[i]); + _accountingContext.unstETHRecords[unstETHIds[i]].index = IndicesOneBased.fromOneBasedValue(i + 1); + _accountingContext.assets[holder].unstETHIds.push(unstETHIds[i]); expectedTotalSharesUnlockedAmount += amountsOfShares[i]; } @@ -778,23 +782,23 @@ contract AssetsAccountingUnitTests is UnitTest { ETHValues.from(expectedTotalSharesUnlockedAmount) ); - AssetsAccounting.accountUnstETHUnlock(_accountingState, holder, unstETHIds); + AssetsAccounting.accountUnstETHUnlock(_accountingContext, holder, unstETHIds); - checkAccountingStateTotalCounters( + checkAccountingContextTotalCounters( SharesValues.ZERO, ETHValues.ZERO, initialTotalUnfinalizedShares, initialTotalFinalizedETH - ETHValues.from(expectedTotalSharesUnlockedAmount) ); - assert(_accountingState.assets[holder].stETHLockedShares == SharesValues.ZERO); + assert(_accountingContext.assets[holder].stETHLockedShares == SharesValues.ZERO); assert( - _accountingState.assets[holder].unstETHLockedShares + _accountingContext.assets[holder].unstETHLockedShares == holderUnstETHLockedShares - SharesValues.from(expectedTotalSharesUnlockedAmount) ); - assert(_accountingState.assets[holder].lastAssetsLockTimestamp == Timestamps.ZERO); - assert(_accountingState.assets[holder].unstETHIds.length == 0); + assert(_accountingContext.assets[holder].lastAssetsLockTimestamp == Timestamps.ZERO); + assert(_accountingContext.assets[holder].unstETHIds.length == 0); for (uint256 i = 0; i < unstETHIds.length; ++i) { - assert(_accountingState.unstETHRecords[unstETHIds[i]].shares == SharesValues.ZERO); + assert(_accountingContext.unstETHRecords[unstETHIds[i]].shares == SharesValues.ZERO); } } @@ -808,7 +812,7 @@ contract AssetsAccountingUnitTests is UnitTest { abi.encodeWithSelector(AssetsAccounting.InvalidUnstETHHolder.selector, unstETHIds[0], holder, address(0x0)) ); - AssetsAccounting.accountUnstETHUnlock(_accountingState, holder, unstETHIds); + AssetsAccounting.accountUnstETHUnlock(_accountingContext, holder, unstETHIds); } function testFuzz_accountUnstETHUnlock_RevertWhen_UnstETHRecordDoesNotBelongToCurrent( @@ -819,20 +823,20 @@ contract AssetsAccountingUnitTests is UnitTest { uint256[] memory unstETHIds = new uint256[](1); unstETHIds[0] = genRandomUnstEthId(1234); - _accountingState.unstETHRecords[unstETHIds[0]].lockedBy = holder; + _accountingContext.unstETHRecords[unstETHIds[0]].lockedBy = holder; vm.expectRevert( abi.encodeWithSelector(AssetsAccounting.InvalidUnstETHHolder.selector, unstETHIds[0], current, holder) ); - AssetsAccounting.accountUnstETHUnlock(_accountingState, current, unstETHIds); + AssetsAccounting.accountUnstETHUnlock(_accountingContext, current, unstETHIds); } function testFuzz_accountUnstETHUnlock_RevertWhen_UnstETHRecordStatusInvalid(address holder) external { uint256[] memory unstETHIds = new uint256[](1); unstETHIds[0] = genRandomUnstEthId(1234); - _accountingState.unstETHRecords[unstETHIds[0]].lockedBy = holder; - _accountingState.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.NotLocked; + _accountingContext.unstETHRecords[unstETHIds[0]].lockedBy = holder; + _accountingContext.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.NotLocked; vm.expectRevert( abi.encodeWithSelector( @@ -840,21 +844,21 @@ contract AssetsAccountingUnitTests is UnitTest { ) ); - AssetsAccounting.accountUnstETHUnlock(_accountingState, holder, unstETHIds); + AssetsAccounting.accountUnstETHUnlock(_accountingContext, holder, unstETHIds); } function testFuzz_accountUnstETHUnlock_RevertWhen_UnstETHRecordIndexInvalid_OOB(address holder) external { uint256[] memory unstETHIds = new uint256[](1); unstETHIds[0] = genRandomUnstEthId(1234); - _accountingState.unstETHRecords[unstETHIds[0]].lockedBy = holder; - _accountingState.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Locked; - _accountingState.unstETHRecords[unstETHIds[0]].shares = SharesValues.from(1234); - _accountingState.unstETHRecords[unstETHIds[0]].index = IndicesOneBased.fromOneBasedValue(10); - _accountingState.assets[holder].unstETHIds.push(unstETHIds[0]); + _accountingContext.unstETHRecords[unstETHIds[0]].lockedBy = holder; + _accountingContext.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Locked; + _accountingContext.unstETHRecords[unstETHIds[0]].shares = SharesValues.from(1234); + _accountingContext.unstETHRecords[unstETHIds[0]].index = IndicesOneBased.fromOneBasedValue(10); + _accountingContext.assets[holder].unstETHIds.push(unstETHIds[0]); vm.expectRevert(stdError.indexOOBError); - AssetsAccounting.accountUnstETHUnlock(_accountingState, holder, unstETHIds); + AssetsAccounting.accountUnstETHUnlock(_accountingContext, holder, unstETHIds); } // TODO: is it expected behavior? @@ -871,81 +875,81 @@ contract AssetsAccountingUnitTests is UnitTest { ETHValue initialTotalFinalizedETH = ETHValues.from(initialTotalFinalizedETHAmount); SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); - _accountingState.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; - _accountingState.unstETHTotals.finalizedETH = initialTotalFinalizedETH; - _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; + _accountingContext.unstETHTotals.finalizedETH = initialTotalFinalizedETH; + _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; uint256[] memory unstETHIds = new uint256[](0); vm.expectEmit(); emit AssetsAccounting.UnstETHUnlocked(holder, unstETHIds, SharesValues.ZERO, ETHValues.ZERO); - AssetsAccounting.accountUnstETHUnlock(_accountingState, holder, unstETHIds); + AssetsAccounting.accountUnstETHUnlock(_accountingContext, holder, unstETHIds); - checkAccountingStateTotalCounters( + checkAccountingContextTotalCounters( SharesValues.ZERO, ETHValues.ZERO, initialTotalUnfinalizedShares, initialTotalFinalizedETH ); - assert(_accountingState.assets[holder].stETHLockedShares == SharesValues.ZERO); - assert(_accountingState.assets[holder].unstETHLockedShares == holderUnstETHLockedShares); - assert(_accountingState.assets[holder].lastAssetsLockTimestamp == Timestamps.ZERO); - assert(_accountingState.assets[holder].unstETHIds.length == 0); + assert(_accountingContext.assets[holder].stETHLockedShares == SharesValues.ZERO); + assert(_accountingContext.assets[holder].unstETHLockedShares == holderUnstETHLockedShares); + assert(_accountingContext.assets[holder].lastAssetsLockTimestamp == Timestamps.ZERO); + assert(_accountingContext.assets[holder].unstETHIds.length == 0); } function testFuzz_accountUnstETHUnlock_RevertOn_AccountingError_HolderUnstETHLockedSharesUnderflow(address holder) external { - _accountingState.assets[holder].unstETHLockedShares = SharesValues.from(5); + _accountingContext.assets[holder].unstETHLockedShares = SharesValues.from(5); uint256[] memory unstETHIds = new uint256[](1); unstETHIds[0] = genRandomUnstEthId(1234); - _accountingState.unstETHRecords[unstETHIds[0]].lockedBy = holder; - _accountingState.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Locked; - _accountingState.unstETHRecords[unstETHIds[0]].shares = SharesValues.from(10); - _accountingState.unstETHRecords[unstETHIds[0]].index = IndicesOneBased.fromOneBasedValue(1); - _accountingState.assets[holder].unstETHIds.push(unstETHIds[0]); + _accountingContext.unstETHRecords[unstETHIds[0]].lockedBy = holder; + _accountingContext.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Locked; + _accountingContext.unstETHRecords[unstETHIds[0]].shares = SharesValues.from(10); + _accountingContext.unstETHRecords[unstETHIds[0]].index = IndicesOneBased.fromOneBasedValue(1); + _accountingContext.assets[holder].unstETHIds.push(unstETHIds[0]); vm.expectRevert(stdError.arithmeticError); - AssetsAccounting.accountUnstETHUnlock(_accountingState, holder, unstETHIds); + AssetsAccounting.accountUnstETHUnlock(_accountingContext, holder, unstETHIds); } function testFuzz_accountUnstETHUnlock_RevertOn_AccountingError_TotalFinalizedETHUnderflow(address holder) external { - _accountingState.assets[holder].unstETHLockedShares = SharesValues.from(10); - _accountingState.unstETHTotals.finalizedETH = ETHValues.from(5); + _accountingContext.assets[holder].unstETHLockedShares = SharesValues.from(10); + _accountingContext.unstETHTotals.finalizedETH = ETHValues.from(5); uint256[] memory unstETHIds = new uint256[](1); unstETHIds[0] = genRandomUnstEthId(1234); - _accountingState.unstETHRecords[unstETHIds[0]].lockedBy = holder; - _accountingState.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Finalized; - _accountingState.unstETHRecords[unstETHIds[0]].shares = SharesValues.from(5); - _accountingState.unstETHRecords[unstETHIds[0]].index = IndicesOneBased.fromOneBasedValue(1); - _accountingState.unstETHRecords[unstETHIds[0]].claimableAmount = ETHValues.from(10); - _accountingState.assets[holder].unstETHIds.push(unstETHIds[0]); + _accountingContext.unstETHRecords[unstETHIds[0]].lockedBy = holder; + _accountingContext.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Finalized; + _accountingContext.unstETHRecords[unstETHIds[0]].shares = SharesValues.from(5); + _accountingContext.unstETHRecords[unstETHIds[0]].index = IndicesOneBased.fromOneBasedValue(1); + _accountingContext.unstETHRecords[unstETHIds[0]].claimableAmount = ETHValues.from(10); + _accountingContext.assets[holder].unstETHIds.push(unstETHIds[0]); vm.expectRevert(ETHValueUnderflow.selector); - AssetsAccounting.accountUnstETHUnlock(_accountingState, holder, unstETHIds); + AssetsAccounting.accountUnstETHUnlock(_accountingContext, holder, unstETHIds); } function testFuzz_accountUnstETHUnlock_RevertOn_AccountingError_TotalUnfinalizedSharesUnderflow(address holder) external { - _accountingState.assets[holder].unstETHLockedShares = SharesValues.from(10); - _accountingState.unstETHTotals.unfinalizedShares = SharesValues.from(5); + _accountingContext.assets[holder].unstETHLockedShares = SharesValues.from(10); + _accountingContext.unstETHTotals.unfinalizedShares = SharesValues.from(5); uint256[] memory unstETHIds = new uint256[](1); unstETHIds[0] = genRandomUnstEthId(1234); - _accountingState.unstETHRecords[unstETHIds[0]].lockedBy = holder; - _accountingState.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Locked; - _accountingState.unstETHRecords[unstETHIds[0]].shares = SharesValues.from(10); - _accountingState.unstETHRecords[unstETHIds[0]].index = IndicesOneBased.fromOneBasedValue(1); - _accountingState.assets[holder].unstETHIds.push(unstETHIds[0]); + _accountingContext.unstETHRecords[unstETHIds[0]].lockedBy = holder; + _accountingContext.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Locked; + _accountingContext.unstETHRecords[unstETHIds[0]].shares = SharesValues.from(10); + _accountingContext.unstETHRecords[unstETHIds[0]].index = IndicesOneBased.fromOneBasedValue(1); + _accountingContext.assets[holder].unstETHIds.push(unstETHIds[0]); vm.expectRevert(stdError.arithmeticError); - AssetsAccounting.accountUnstETHUnlock(_accountingState, holder, unstETHIds); + AssetsAccounting.accountUnstETHUnlock(_accountingContext, holder, unstETHIds); } // --- @@ -967,17 +971,17 @@ contract AssetsAccountingUnitTests is UnitTest { uint256 expectedTotalSharesFinalized = 0; uint256 expectedTotalAmountFinalized = 0; - _accountingState.unstETHTotals.finalizedETH = initialTotalFinalizedETH; - _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + _accountingContext.unstETHTotals.finalizedETH = initialTotalFinalizedETH; + _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; uint256[] memory unstETHIds = new uint256[](claimableAmounts.length); uint256[] memory claimableAmountsPrepared = new uint256[](claimableAmounts.length); for (uint256 i = 0; i < claimableAmounts.length; ++i) { unstETHIds[i] = genRandomUnstEthId(i); - _accountingState.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.Locked; + _accountingContext.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.Locked; uint256 sharesAmount = 5 * uint256(claimableAmounts[i]); - _accountingState.unstETHRecords[unstETHIds[i]].shares = SharesValues.from(sharesAmount); + _accountingContext.unstETHRecords[unstETHIds[i]].shares = SharesValues.from(sharesAmount); expectedTotalSharesFinalized += sharesAmount; expectedTotalAmountFinalized += claimableAmounts[i]; claimableAmountsPrepared[i] = claimableAmounts[i]; @@ -988,9 +992,9 @@ contract AssetsAccountingUnitTests is UnitTest { unstETHIds, SharesValues.from(expectedTotalSharesFinalized), ETHValues.from(expectedTotalAmountFinalized) ); - AssetsAccounting.accountUnstETHFinalized(_accountingState, unstETHIds, claimableAmountsPrepared); + AssetsAccounting.accountUnstETHFinalized(_accountingContext, unstETHIds, claimableAmountsPrepared); - checkAccountingStateTotalCounters( + checkAccountingContextTotalCounters( SharesValues.ZERO, ETHValues.ZERO, initialTotalUnfinalizedShares - SharesValues.from(expectedTotalSharesFinalized), @@ -1009,7 +1013,7 @@ contract AssetsAccountingUnitTests is UnitTest { vm.expectRevert(stdError.assertionError); - AssetsAccounting.accountUnstETHFinalized(_accountingState, unstETHIds, claimableAmountsPrepared); + AssetsAccounting.accountUnstETHFinalized(_accountingContext, unstETHIds, claimableAmountsPrepared); } function testFuzz_accountUnstETHFinalized_When_NoClaimableAmountsProvided( @@ -1021,8 +1025,8 @@ contract AssetsAccountingUnitTests is UnitTest { ETHValue initialTotalFinalizedETH = ETHValues.from(initialTotalFinalizedETHAmount); SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); - _accountingState.unstETHTotals.finalizedETH = initialTotalFinalizedETH; - _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + _accountingContext.unstETHTotals.finalizedETH = initialTotalFinalizedETH; + _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; uint256[] memory unstETHIds = new uint256[](0); uint256[] memory claimableAmountsPrepared = new uint256[](0); @@ -1030,9 +1034,9 @@ contract AssetsAccountingUnitTests is UnitTest { vm.expectEmit(); emit AssetsAccounting.UnstETHFinalized(unstETHIds, SharesValues.from(0), ETHValues.from(0)); - AssetsAccounting.accountUnstETHFinalized(_accountingState, unstETHIds, claimableAmountsPrepared); + AssetsAccounting.accountUnstETHFinalized(_accountingContext, unstETHIds, claimableAmountsPrepared); - checkAccountingStateTotalCounters( + checkAccountingContextTotalCounters( SharesValues.ZERO, ETHValues.ZERO, initialTotalUnfinalizedShares, initialTotalFinalizedETH ); } @@ -1047,8 +1051,8 @@ contract AssetsAccountingUnitTests is UnitTest { ETHValue initialTotalFinalizedETH = ETHValues.from(initialTotalFinalizedETHAmount); SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); - _accountingState.unstETHTotals.finalizedETH = initialTotalFinalizedETH; - _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + _accountingContext.unstETHTotals.finalizedETH = initialTotalFinalizedETH; + _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; uint256[] memory unstETHIds = new uint256[](1); uint256[] memory claimableAmountsPrepared = new uint256[](1); @@ -1059,9 +1063,9 @@ contract AssetsAccountingUnitTests is UnitTest { vm.expectEmit(); emit AssetsAccounting.UnstETHFinalized(unstETHIds, SharesValues.from(0), ETHValues.from(0)); - AssetsAccounting.accountUnstETHFinalized(_accountingState, unstETHIds, claimableAmountsPrepared); + AssetsAccounting.accountUnstETHFinalized(_accountingContext, unstETHIds, claimableAmountsPrepared); - checkAccountingStateTotalCounters( + checkAccountingContextTotalCounters( SharesValues.ZERO, ETHValues.ZERO, initialTotalUnfinalizedShares, initialTotalFinalizedETH ); } @@ -1075,23 +1079,23 @@ contract AssetsAccountingUnitTests is UnitTest { ETHValue initialTotalFinalizedETH = ETHValues.from(initialTotalFinalizedETHAmount); SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); - _accountingState.unstETHTotals.finalizedETH = initialTotalFinalizedETH; - _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + _accountingContext.unstETHTotals.finalizedETH = initialTotalFinalizedETH; + _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; uint256[] memory unstETHIds = new uint256[](1); uint256[] memory claimableAmountsPrepared = new uint256[](1); unstETHIds[0] = genRandomUnstEthId(9876); - _accountingState.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Locked; - _accountingState.unstETHRecords[unstETHIds[0]].shares = SharesValues.from(123); + _accountingContext.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Locked; + _accountingContext.unstETHRecords[unstETHIds[0]].shares = SharesValues.from(123); claimableAmountsPrepared[0] = 0; vm.expectEmit(); emit AssetsAccounting.UnstETHFinalized(unstETHIds, SharesValues.from(0), ETHValues.from(0)); - AssetsAccounting.accountUnstETHFinalized(_accountingState, unstETHIds, claimableAmountsPrepared); + AssetsAccounting.accountUnstETHFinalized(_accountingContext, unstETHIds, claimableAmountsPrepared); - checkAccountingStateTotalCounters( + checkAccountingContextTotalCounters( SharesValues.ZERO, ETHValues.ZERO, initialTotalUnfinalizedShares, initialTotalFinalizedETH ); } @@ -1105,20 +1109,20 @@ contract AssetsAccountingUnitTests is UnitTest { ETHValue initialTotalFinalizedETH = ETHValues.from(initialTotalFinalizedETHAmount); SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); - _accountingState.unstETHTotals.finalizedETH = initialTotalFinalizedETH; - _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + _accountingContext.unstETHTotals.finalizedETH = initialTotalFinalizedETH; + _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; uint256[] memory unstETHIds = new uint256[](1); uint256[] memory claimableAmountsPrepared = new uint256[](1); unstETHIds[0] = genRandomUnstEthId(9876); - _accountingState.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Locked; - _accountingState.unstETHRecords[unstETHIds[0]].shares = SharesValues.from(123); + _accountingContext.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Locked; + _accountingContext.unstETHRecords[unstETHIds[0]].shares = SharesValues.from(123); claimableAmountsPrepared[0] = uint256(type(uint128).max) + 1; vm.expectRevert(ETHValueOverflow.selector); - AssetsAccounting.accountUnstETHFinalized(_accountingState, unstETHIds, claimableAmountsPrepared); + AssetsAccounting.accountUnstETHFinalized(_accountingContext, unstETHIds, claimableAmountsPrepared); } function testFuzz_accountUnstETHFinalized_RevertOn_TotalFinalizedETHOverflow( @@ -1130,20 +1134,20 @@ contract AssetsAccountingUnitTests is UnitTest { ETHValue initialTotalFinalizedETH = ETHValues.from(initialTotalFinalizedETHAmount); SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); - _accountingState.unstETHTotals.finalizedETH = initialTotalFinalizedETH; - _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + _accountingContext.unstETHTotals.finalizedETH = initialTotalFinalizedETH; + _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; uint256[] memory unstETHIds = new uint256[](1); uint256[] memory claimableAmountsPrepared = new uint256[](1); unstETHIds[0] = genRandomUnstEthId(9876); - _accountingState.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Locked; - _accountingState.unstETHRecords[unstETHIds[0]].shares = SharesValues.from(123); + _accountingContext.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Locked; + _accountingContext.unstETHRecords[unstETHIds[0]].shares = SharesValues.from(123); claimableAmountsPrepared[0] = uint256(type(uint128).max - 2); vm.expectRevert(stdError.arithmeticError); - AssetsAccounting.accountUnstETHFinalized(_accountingState, unstETHIds, claimableAmountsPrepared); + AssetsAccounting.accountUnstETHFinalized(_accountingContext, unstETHIds, claimableAmountsPrepared); } function testFuzz_accountUnstETHFinalized_RevertOn_TotalUnfinalizedSharesUnderflow( @@ -1155,20 +1159,20 @@ contract AssetsAccountingUnitTests is UnitTest { ETHValue initialTotalFinalizedETH = ETHValues.from(initialTotalFinalizedETHAmount); SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); - _accountingState.unstETHTotals.finalizedETH = initialTotalFinalizedETH; - _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + _accountingContext.unstETHTotals.finalizedETH = initialTotalFinalizedETH; + _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; uint256[] memory unstETHIds = new uint256[](1); uint256[] memory claimableAmountsPrepared = new uint256[](1); unstETHIds[0] = genRandomUnstEthId(9876); - _accountingState.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Locked; - _accountingState.unstETHRecords[unstETHIds[0]].shares = SharesValues.from(type(uint64).max); + _accountingContext.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Locked; + _accountingContext.unstETHRecords[unstETHIds[0]].shares = SharesValues.from(type(uint64).max); claimableAmountsPrepared[0] = 1; vm.expectRevert(stdError.arithmeticError); - AssetsAccounting.accountUnstETHFinalized(_accountingState, unstETHIds, claimableAmountsPrepared); + AssetsAccounting.accountUnstETHFinalized(_accountingContext, unstETHIds, claimableAmountsPrepared); } // --- @@ -1187,7 +1191,7 @@ contract AssetsAccountingUnitTests is UnitTest { for (uint256 i = 0; i < claimableAmounts.length; ++i) { unstETHIds[i] = genRandomUnstEthId(i); - _accountingState.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.Locked; + _accountingContext.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.Locked; expectedTotalAmountClaimed += claimableAmounts[i]; claimableAmountsPrepared[i] = claimableAmounts[i]; } @@ -1195,14 +1199,14 @@ contract AssetsAccountingUnitTests is UnitTest { vm.expectEmit(); emit AssetsAccounting.UnstETHClaimed(unstETHIds, ETHValues.from(expectedTotalAmountClaimed)); - AssetsAccounting.accountUnstETHClaimed(_accountingState, unstETHIds, claimableAmountsPrepared); + AssetsAccounting.accountUnstETHClaimed(_accountingContext, unstETHIds, claimableAmountsPrepared); - checkAccountingStateTotalCounters(SharesValues.ZERO, ETHValues.ZERO, SharesValues.ZERO, ETHValues.ZERO); + checkAccountingContextTotalCounters(SharesValues.ZERO, ETHValues.ZERO, SharesValues.ZERO, ETHValues.ZERO); for (uint256 i = 0; i < unstETHIds.length; ++i) { assert( - _accountingState.unstETHRecords[unstETHIds[i]].claimableAmount == ETHValues.from(claimableAmounts[i]) + _accountingContext.unstETHRecords[unstETHIds[i]].claimableAmount == ETHValues.from(claimableAmounts[i]) ); - assert(_accountingState.unstETHRecords[unstETHIds[i]].status == UnstETHRecordStatus.Claimed); + assert(_accountingContext.unstETHRecords[unstETHIds[i]].status == UnstETHRecordStatus.Claimed); } } @@ -1218,13 +1222,13 @@ contract AssetsAccountingUnitTests is UnitTest { for (uint256 i = 0; i < claimableAmounts.length; ++i) { unstETHIds[i] = genRandomUnstEthId(i); - _accountingState.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.Locked; + _accountingContext.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.Locked; claimableAmountsPrepared[i] = claimableAmounts[i]; } vm.expectRevert(stdError.indexOOBError); - AssetsAccounting.accountUnstETHClaimed(_accountingState, unstETHIds, claimableAmountsPrepared); + AssetsAccounting.accountUnstETHClaimed(_accountingContext, unstETHIds, claimableAmountsPrepared); } function test_accountUnstETHClaimed_WhenNoUnstETHIdsProvided() external { @@ -1236,9 +1240,9 @@ contract AssetsAccountingUnitTests is UnitTest { vm.expectEmit(); emit AssetsAccounting.UnstETHClaimed(unstETHIds, ETHValues.from(expectedTotalAmountClaimed)); - AssetsAccounting.accountUnstETHClaimed(_accountingState, unstETHIds, claimableAmountsPrepared); + AssetsAccounting.accountUnstETHClaimed(_accountingContext, unstETHIds, claimableAmountsPrepared); - checkAccountingStateTotalCounters(SharesValues.ZERO, ETHValues.ZERO, SharesValues.ZERO, ETHValues.ZERO); + checkAccountingContextTotalCounters(SharesValues.ZERO, ETHValues.ZERO, SharesValues.ZERO, ETHValues.ZERO); } function testFuzz_accountUnstETHClaimed_RevertWhen_UnstETHRecordNotFoundOrHasWrongStatus( @@ -1261,7 +1265,7 @@ contract AssetsAccountingUnitTests is UnitTest { ) ); - AssetsAccounting.accountUnstETHClaimed(_accountingState, unstETHIds, claimableAmountsPrepared); + AssetsAccounting.accountUnstETHClaimed(_accountingContext, unstETHIds, claimableAmountsPrepared); } function testFuzz_accountUnstETHClaimed_RevertWhen_UnstETHRecordIsFinalizedAndClaimableAmountIsIncorrect( @@ -1275,8 +1279,8 @@ contract AssetsAccountingUnitTests is UnitTest { for (uint256 i = 0; i < claimableAmounts.length; ++i) { unstETHIds[i] = genRandomUnstEthId(i); - _accountingState.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.Finalized; - _accountingState.unstETHRecords[unstETHIds[i]].claimableAmount = + _accountingContext.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.Finalized; + _accountingContext.unstETHRecords[unstETHIds[i]].claimableAmount = ETHValues.from(uint256(claimableAmounts[i]) + 1); claimableAmountsPrepared[i] = claimableAmounts[i]; } @@ -1290,7 +1294,7 @@ contract AssetsAccountingUnitTests is UnitTest { ) ); - AssetsAccounting.accountUnstETHClaimed(_accountingState, unstETHIds, claimableAmountsPrepared); + AssetsAccounting.accountUnstETHClaimed(_accountingContext, unstETHIds, claimableAmountsPrepared); } function testFuzz_accountUnstETHClaimed_When_UnstETHRecordIsFinalizedAndClaimableAmountIsCorrect( @@ -1306,8 +1310,8 @@ contract AssetsAccountingUnitTests is UnitTest { for (uint256 i = 0; i < claimableAmounts.length; ++i) { unstETHIds[i] = genRandomUnstEthId(i); - _accountingState.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.Finalized; - _accountingState.unstETHRecords[unstETHIds[i]].claimableAmount = ETHValues.from(claimableAmounts[i]); + _accountingContext.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.Finalized; + _accountingContext.unstETHRecords[unstETHIds[i]].claimableAmount = ETHValues.from(claimableAmounts[i]); claimableAmountsPrepared[i] = claimableAmounts[i]; expectedTotalAmountClaimed += claimableAmounts[i]; } @@ -1315,14 +1319,14 @@ contract AssetsAccountingUnitTests is UnitTest { vm.expectEmit(); emit AssetsAccounting.UnstETHClaimed(unstETHIds, ETHValues.from(expectedTotalAmountClaimed)); - AssetsAccounting.accountUnstETHClaimed(_accountingState, unstETHIds, claimableAmountsPrepared); + AssetsAccounting.accountUnstETHClaimed(_accountingContext, unstETHIds, claimableAmountsPrepared); - checkAccountingStateTotalCounters(SharesValues.ZERO, ETHValues.ZERO, SharesValues.ZERO, ETHValues.ZERO); + checkAccountingContextTotalCounters(SharesValues.ZERO, ETHValues.ZERO, SharesValues.ZERO, ETHValues.ZERO); for (uint256 i = 0; i < unstETHIds.length; ++i) { assert( - _accountingState.unstETHRecords[unstETHIds[i]].claimableAmount == ETHValues.from(claimableAmounts[i]) + _accountingContext.unstETHRecords[unstETHIds[i]].claimableAmount == ETHValues.from(claimableAmounts[i]) ); - assert(_accountingState.unstETHRecords[unstETHIds[i]].status == UnstETHRecordStatus.Claimed); + assert(_accountingContext.unstETHRecords[unstETHIds[i]].status == UnstETHRecordStatus.Claimed); } } @@ -1331,12 +1335,12 @@ contract AssetsAccountingUnitTests is UnitTest { uint256[] memory claimableAmountsPrepared = new uint256[](1); unstETHIds[0] = genRandomUnstEthId(1); - _accountingState.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Locked; + _accountingContext.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Locked; claimableAmountsPrepared[0] = uint256(type(uint128).max) + 1; vm.expectRevert(ETHValueOverflow.selector); - AssetsAccounting.accountUnstETHClaimed(_accountingState, unstETHIds, claimableAmountsPrepared); + AssetsAccounting.accountUnstETHClaimed(_accountingContext, unstETHIds, claimableAmountsPrepared); } // --- @@ -1354,26 +1358,26 @@ contract AssetsAccountingUnitTests is UnitTest { for (uint256 i = 0; i < claimableAmounts.length; ++i) { unstETHIds[i] = genRandomUnstEthId(i); - _accountingState.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.Claimed; - _accountingState.unstETHRecords[unstETHIds[i]].lockedBy = holder; - _accountingState.unstETHRecords[unstETHIds[i]].claimableAmount = ETHValues.from(claimableAmounts[i]); + _accountingContext.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.Claimed; + _accountingContext.unstETHRecords[unstETHIds[i]].lockedBy = holder; + _accountingContext.unstETHRecords[unstETHIds[i]].claimableAmount = ETHValues.from(claimableAmounts[i]); expectedAmountWithdrawn += claimableAmounts[i]; } vm.expectEmit(); emit AssetsAccounting.UnstETHWithdrawn(unstETHIds, ETHValues.from(expectedAmountWithdrawn)); - ETHValue amountWithdrawn = AssetsAccounting.accountUnstETHWithdraw(_accountingState, holder, unstETHIds); + ETHValue amountWithdrawn = AssetsAccounting.accountUnstETHWithdraw(_accountingContext, holder, unstETHIds); assert(amountWithdrawn == ETHValues.from(expectedAmountWithdrawn)); - checkAccountingStateTotalCounters(SharesValues.ZERO, ETHValues.ZERO, SharesValues.ZERO, ETHValues.ZERO); + checkAccountingContextTotalCounters(SharesValues.ZERO, ETHValues.ZERO, SharesValues.ZERO, ETHValues.ZERO); for (uint256 i = 0; i < unstETHIds.length; ++i) { assert( - _accountingState.unstETHRecords[unstETHIds[i]].claimableAmount == ETHValues.from(claimableAmounts[i]) + _accountingContext.unstETHRecords[unstETHIds[i]].claimableAmount == ETHValues.from(claimableAmounts[i]) ); - assert(_accountingState.unstETHRecords[unstETHIds[i]].status == UnstETHRecordStatus.Withdrawn); - assert(_accountingState.unstETHRecords[unstETHIds[i]].lockedBy == holder); + assert(_accountingContext.unstETHRecords[unstETHIds[i]].status == UnstETHRecordStatus.Withdrawn); + assert(_accountingContext.unstETHRecords[unstETHIds[i]].lockedBy == holder); } } @@ -1383,7 +1387,7 @@ contract AssetsAccountingUnitTests is UnitTest { vm.expectEmit(); emit AssetsAccounting.UnstETHWithdrawn(unstETHIds, ETHValues.ZERO); - ETHValue amountWithdrawn = AssetsAccounting.accountUnstETHWithdraw(_accountingState, holder, unstETHIds); + ETHValue amountWithdrawn = AssetsAccounting.accountUnstETHWithdraw(_accountingContext, holder, unstETHIds); assert(amountWithdrawn == ETHValues.ZERO); } @@ -1407,7 +1411,7 @@ contract AssetsAccountingUnitTests is UnitTest { ) ); - AssetsAccounting.accountUnstETHWithdraw(_accountingState, holder, unstETHIds); + AssetsAccounting.accountUnstETHWithdraw(_accountingContext, holder, unstETHIds); } function testFuzz_accountUnstETHWithdraw_RevertWhen_UnstETHRecordDoesNotBelongToCurrent( @@ -1419,15 +1423,15 @@ contract AssetsAccountingUnitTests is UnitTest { uint256[] memory unstETHIds = new uint256[](1); unstETHIds[0] = genRandomUnstEthId(567); - _accountingState.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Claimed; - _accountingState.unstETHRecords[unstETHIds[0]].lockedBy = holder; - _accountingState.unstETHRecords[unstETHIds[0]].claimableAmount = ETHValues.from(123); + _accountingContext.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Claimed; + _accountingContext.unstETHRecords[unstETHIds[0]].lockedBy = holder; + _accountingContext.unstETHRecords[unstETHIds[0]].claimableAmount = ETHValues.from(123); vm.expectRevert( abi.encodeWithSelector(AssetsAccounting.InvalidUnstETHHolder.selector, unstETHIds[0], current, holder) ); - AssetsAccounting.accountUnstETHWithdraw(_accountingState, current, unstETHIds); + AssetsAccounting.accountUnstETHWithdraw(_accountingContext, current, unstETHIds); } function testFuzz_accountUnstETHWithdraw_RevertOn_WithdrawnAmountOverflow(address holder) external { @@ -1435,15 +1439,15 @@ contract AssetsAccountingUnitTests is UnitTest { for (uint256 i = 0; i < unstETHIds.length; ++i) { unstETHIds[i] = genRandomUnstEthId(i); - _accountingState.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.Claimed; - _accountingState.unstETHRecords[unstETHIds[i]].lockedBy = holder; - _accountingState.unstETHRecords[unstETHIds[i]].claimableAmount = + _accountingContext.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.Claimed; + _accountingContext.unstETHRecords[unstETHIds[i]].lockedBy = holder; + _accountingContext.unstETHRecords[unstETHIds[i]].claimableAmount = ETHValues.from(uint256(type(uint128).max) / 2 + 1); } vm.expectRevert(stdError.arithmeticError); - AssetsAccounting.accountUnstETHWithdraw(_accountingState, holder, unstETHIds); + AssetsAccounting.accountUnstETHWithdraw(_accountingContext, holder, unstETHIds); } // --- @@ -1459,12 +1463,12 @@ contract AssetsAccountingUnitTests is UnitTest { SharesValue totalUnfinalizedShares = SharesValues.from(totalUnfinalizedSharesAmount); SharesValue totalLockedShares = SharesValues.from(totalLockedSharesAmount); - _accountingState.unstETHTotals.finalizedETH = totalFinalizedETH; - _accountingState.unstETHTotals.unfinalizedShares = totalUnfinalizedShares; - _accountingState.stETHTotals.lockedShares = totalLockedShares; + _accountingContext.unstETHTotals.finalizedETH = totalFinalizedETH; + _accountingContext.unstETHTotals.unfinalizedShares = totalUnfinalizedShares; + _accountingContext.stETHTotals.lockedShares = totalLockedShares; (SharesValue unfinalizedShares, ETHValue finalizedETH) = - AssetsAccounting.getLockedAssetsTotals(_accountingState); + AssetsAccounting.getLockedAssetsTotals(_accountingContext); assert(unfinalizedShares == totalLockedShares + totalUnfinalizedShares); assert(finalizedETH == totalFinalizedETH); @@ -1475,12 +1479,12 @@ contract AssetsAccountingUnitTests is UnitTest { SharesValue totalUnfinalizedShares = SharesValues.from(type(uint128).max - 1); SharesValue totalLockedShares = SharesValues.from(type(uint128).max - 1); - _accountingState.unstETHTotals.finalizedETH = totalFinalizedETH; - _accountingState.unstETHTotals.unfinalizedShares = totalUnfinalizedShares; - _accountingState.stETHTotals.lockedShares = totalLockedShares; + _accountingContext.unstETHTotals.finalizedETH = totalFinalizedETH; + _accountingContext.unstETHTotals.unfinalizedShares = totalUnfinalizedShares; + _accountingContext.stETHTotals.lockedShares = totalLockedShares; vm.expectRevert(stdError.arithmeticError); - AssetsAccounting.getLockedAssetsTotals(_accountingState); + AssetsAccounting.getLockedAssetsTotals(_accountingContext); } // --- @@ -1489,22 +1493,22 @@ contract AssetsAccountingUnitTests is UnitTest { function testFuzz_checkMinAssetsLockDurationPassed_happyPath(address holder) external { Duration minAssetsLockDuration = Durations.from(0); - _accountingState.assets[holder].lastAssetsLockTimestamp = Timestamps.from(Timestamps.now().toSeconds() - 1); + _accountingContext.assets[holder].lastAssetsLockTimestamp = Timestamps.from(Timestamps.now().toSeconds() - 1); - AssetsAccounting.checkMinAssetsLockDurationPassed(_accountingState, holder, minAssetsLockDuration); + AssetsAccounting.checkMinAssetsLockDurationPassed(_accountingContext, holder, minAssetsLockDuration); } function testFuzz_checkMinAssetsLockDurationPassed_RevertOn_MinAssetsLockDurationNotPassed(address holder) external { Duration minAssetsLockDuration = Durations.from(1); - _accountingState.assets[holder].lastAssetsLockTimestamp = Timestamps.from(Timestamps.now().toSeconds() - 1); + _accountingContext.assets[holder].lastAssetsLockTimestamp = Timestamps.from(Timestamps.now().toSeconds() - 1); vm.expectRevert( abi.encodeWithSelector(AssetsAccounting.MinAssetsLockDurationNotPassed.selector, Timestamps.now()) ); - AssetsAccounting.checkMinAssetsLockDurationPassed(_accountingState, holder, minAssetsLockDuration); + AssetsAccounting.checkMinAssetsLockDurationPassed(_accountingContext, holder, minAssetsLockDuration); } // --- @@ -1515,15 +1519,15 @@ contract AssetsAccountingUnitTests is UnitTest { return uint256(keccak256(abi.encodePacked(block.timestamp, salt))); // random id } - function checkAccountingStateTotalCounters( + function checkAccountingContextTotalCounters( SharesValue lockedShares, ETHValue claimedETH, SharesValue unfinalizedShares, ETHValue finalizedETH ) internal view { - assert(_accountingState.stETHTotals.lockedShares == lockedShares); - assert(_accountingState.stETHTotals.claimedETH == claimedETH); - assert(_accountingState.unstETHTotals.unfinalizedShares == unfinalizedShares); - assert(_accountingState.unstETHTotals.finalizedETH == finalizedETH); + assert(_accountingContext.stETHTotals.lockedShares == lockedShares); + assert(_accountingContext.stETHTotals.claimedETH == claimedETH); + assert(_accountingContext.unstETHTotals.unfinalizedShares == unfinalizedShares); + assert(_accountingContext.unstETHTotals.finalizedETH == finalizedETH); } } From d5e6b02ebad0d1d82f6d886f7fbe1fc8773ce617 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Mon, 19 Aug 2024 17:39:57 +0400 Subject: [PATCH 32/42] Use Duration/Timestamp instead of primitive type as a fuzzing parameter --- test/unit/libraries/EscrowState.t.sol | 116 ++++++++++++-------------- 1 file changed, 52 insertions(+), 64 deletions(-) diff --git a/test/unit/libraries/EscrowState.t.sol b/test/unit/libraries/EscrowState.t.sol index d5740784..dbd4e12c 100644 --- a/test/unit/libraries/EscrowState.t.sol +++ b/test/unit/libraries/EscrowState.t.sol @@ -19,8 +19,7 @@ contract EscrowStateUnitTests is UnitTest { // initialize() // --- - function testFuzz_initialize_happyPath(uint32 minAssetsLockSeconds) external { - Duration minAssetsLockDuration = Durations.from(minAssetsLockSeconds); + function testFuzz_initialize_happyPath(Duration minAssetsLockDuration) external { _context.state = State.NotInitialized; vm.expectEmit(); @@ -32,13 +31,13 @@ contract EscrowStateUnitTests is UnitTest { checkContext(State.SignallingEscrow, minAssetsLockDuration, D0, D0, T0); } - function testFuzz_initialize_RevertOn_InvalidState(uint32 minAssetsLockSeconds) external { + function testFuzz_initialize_RevertOn_InvalidState(Duration minAssetsLockDuration) external { _context.state = State.SignallingEscrow; - // TODO: not very informative, maybe need to change to `revert UnexpectedState(self.state);` + // TODO: not very informative, maybe need to change to `revert UnexpectedState(self.state);`: UnexpectedState(NotInitialized)[current implementation] => UnexpectedState(SignallingEscrow)[proposed] vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.NotInitialized)); - EscrowState.initialize(_context, Durations.from(minAssetsLockSeconds)); + EscrowState.initialize(_context, minAssetsLockDuration); } // --- @@ -46,12 +45,9 @@ contract EscrowStateUnitTests is UnitTest { // --- function testFuzz_startRageQuit_happyPath( - uint32 rageQuitExtensionDelaySeconds, - uint32 rageQuitWithdrawalsTimelockSeconds + Duration rageQuitExtensionDelay, + Duration rageQuitWithdrawalsTimelock ) external { - Duration rageQuitExtensionDelay = Durations.from(rageQuitExtensionDelaySeconds); - Duration rageQuitWithdrawalsTimelock = Durations.from(rageQuitWithdrawalsTimelockSeconds); - _context.state = State.SignallingEscrow; vm.expectEmit(); @@ -64,12 +60,9 @@ contract EscrowStateUnitTests is UnitTest { } function testFuzz_startRageQuit_RevertOn_InvalidState( - uint32 rageQuitExtensionDelaySeconds, - uint32 rageQuitWithdrawalsTimelockSeconds + Duration rageQuitExtensionDelay, + Duration rageQuitWithdrawalsTimelock ) external { - Duration rageQuitExtensionDelay = Durations.from(rageQuitExtensionDelaySeconds); - Duration rageQuitWithdrawalsTimelock = Durations.from(rageQuitWithdrawalsTimelockSeconds); - _context.state = State.NotInitialized; vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); @@ -94,9 +87,8 @@ contract EscrowStateUnitTests is UnitTest { // setMinAssetsLockDuration() // --- - function test_setMinAssetsLockDuration_happyPath(uint32 minAssetsLockSeconds) external { - vm.assume(minAssetsLockSeconds != 0); - Duration minAssetsLockDuration = Durations.from(minAssetsLockSeconds); + function test_setMinAssetsLockDuration_happyPath(Duration minAssetsLockDuration) external { + vm.assume(minAssetsLockDuration != Durations.ZERO); vm.expectEmit(); emit EscrowState.MinAssetsLockDurationSet(minAssetsLockDuration); @@ -106,9 +98,7 @@ contract EscrowStateUnitTests is UnitTest { checkContext(State.NotInitialized, minAssetsLockDuration, D0, D0, T0); } - function test_setMinAssetsLockDuration_WhenDurationNotChanged(uint32 minAssetsLockSeconds) external { - Duration minAssetsLockDuration = Durations.from(minAssetsLockSeconds); - + function test_setMinAssetsLockDuration_WhenDurationNotChanged(Duration minAssetsLockDuration) external { _context.minAssetsLockDuration = minAssetsLockDuration; Vm.Log[] memory entries = vm.getRecordedLogs(); @@ -158,11 +148,11 @@ contract EscrowStateUnitTests is UnitTest { EscrowState.checkBatchesClaimingInProgress(_context); } - function testFuzz_checkBatchesClaimingInProgress_RevertOn_InvalidState( - uint40 rageQuitExtensionDelayStartedAtSeconds - ) external { - vm.assume(rageQuitExtensionDelayStartedAtSeconds > 0); - _context.rageQuitExtensionDelayStartedAt = Timestamps.from(rageQuitExtensionDelayStartedAtSeconds); + function testFuzz_checkBatchesClaimingInProgress_RevertOn_InvalidState(Timestamp rageQuitExtensionDelayStartedAt) + external + { + vm.assume(rageQuitExtensionDelayStartedAt > Timestamps.ZERO); + _context.rageQuitExtensionDelayStartedAt = rageQuitExtensionDelayStartedAt; vm.expectRevert(EscrowState.ClaimingIsFinished.selector); EscrowState.checkBatchesClaimingInProgress(_context); @@ -173,22 +163,22 @@ contract EscrowStateUnitTests is UnitTest { // --- function testFuzz_checkWithdrawalsTimelockPassed_happyPath( - uint16 rageQuitExtensionDelayStartedAtSeconds, - uint16 rageQuitExtensionDelaySeconds, - uint16 rageQuitWithdrawalsTimelockSeconds + Timestamp rageQuitExtensionDelayStartedAt, + Duration rageQuitExtensionDelay, + Duration rageQuitWithdrawalsTimelock ) external { - vm.assume(rageQuitExtensionDelayStartedAtSeconds > 0); - - Duration rageQuitExtensionDelay = Durations.from(rageQuitExtensionDelaySeconds); - Duration rageQuitWithdrawalsTimelock = Durations.from(rageQuitWithdrawalsTimelockSeconds); + vm.assume(rageQuitExtensionDelayStartedAt > Timestamps.ZERO); + vm.assume(rageQuitExtensionDelayStartedAt < Timestamps.from(type(uint16).max)); + vm.assume(rageQuitExtensionDelay < Durations.from(type(uint16).max)); + vm.assume(rageQuitWithdrawalsTimelock < Durations.from(type(uint16).max)); - _context.rageQuitExtensionDelayStartedAt = Timestamps.from(rageQuitExtensionDelayStartedAtSeconds); + _context.rageQuitExtensionDelayStartedAt = rageQuitExtensionDelayStartedAt; _context.rageQuitExtensionDelay = rageQuitExtensionDelay; _context.rageQuitWithdrawalsTimelock = rageQuitWithdrawalsTimelock; vm.warp( - uint256(rageQuitExtensionDelayStartedAtSeconds) + uint256(rageQuitExtensionDelaySeconds) - + uint256(rageQuitWithdrawalsTimelockSeconds) + 1 + rageQuitExtensionDelayStartedAt.toSeconds() + rageQuitExtensionDelay.toSeconds() + + rageQuitWithdrawalsTimelock.toSeconds() + 1 ); EscrowState.checkWithdrawalsTimelockPassed(_context); } @@ -200,22 +190,22 @@ contract EscrowStateUnitTests is UnitTest { } function testFuzz_checkWithdrawalsTimelockPassed_RevertWhen_WithdrawalsTimelockNotPassed( - uint16 rageQuitExtensionDelayStartedAtSeconds, - uint16 rageQuitExtensionDelaySeconds, - uint16 rageQuitWithdrawalsTimelockSeconds + Timestamp rageQuitExtensionDelayStartedAt, + Duration rageQuitExtensionDelay, + Duration rageQuitWithdrawalsTimelock ) external { - vm.assume(rageQuitExtensionDelayStartedAtSeconds > 0); - - Duration rageQuitExtensionDelay = Durations.from(rageQuitExtensionDelaySeconds); - Duration rageQuitWithdrawalsTimelock = Durations.from(rageQuitWithdrawalsTimelockSeconds); + vm.assume(rageQuitExtensionDelayStartedAt > Timestamps.ZERO); + vm.assume(rageQuitExtensionDelayStartedAt < Timestamps.from(type(uint16).max)); + vm.assume(rageQuitExtensionDelay < Durations.from(type(uint16).max)); + vm.assume(rageQuitWithdrawalsTimelock < Durations.from(type(uint16).max)); - _context.rageQuitExtensionDelayStartedAt = Timestamps.from(rageQuitExtensionDelayStartedAtSeconds); + _context.rageQuitExtensionDelayStartedAt = rageQuitExtensionDelayStartedAt; _context.rageQuitExtensionDelay = rageQuitExtensionDelay; _context.rageQuitWithdrawalsTimelock = rageQuitWithdrawalsTimelock; vm.warp( - uint256(rageQuitExtensionDelayStartedAtSeconds) + uint256(rageQuitExtensionDelaySeconds) - + uint256(rageQuitWithdrawalsTimelockSeconds) + rageQuitExtensionDelayStartedAt.toSeconds() + rageQuitExtensionDelay.toSeconds() + + rageQuitWithdrawalsTimelock.toSeconds() ); vm.expectRevert(EscrowState.WithdrawalsTimelockNotPassed.selector); @@ -240,10 +230,8 @@ contract EscrowStateUnitTests is UnitTest { // isRageQuitExtensionDelayStarted() // --- - function testFuzz_isRageQuitExtensionDelayStarted_happyPath(uint40 rageQuitExtensionDelayStartedAtSeconds) - external - { - _context.rageQuitExtensionDelayStartedAt = Timestamps.from(rageQuitExtensionDelayStartedAtSeconds); + function testFuzz_isRageQuitExtensionDelayStarted_happyPath(Timestamp rageQuitExtensionDelayStartedAt) external { + _context.rageQuitExtensionDelayStartedAt = rageQuitExtensionDelayStartedAt; bool res = EscrowState.isRageQuitExtensionDelayStarted(_context); assert(res == _context.rageQuitExtensionDelayStartedAt.isNotZero()); } @@ -253,33 +241,33 @@ contract EscrowStateUnitTests is UnitTest { // --- function testFuzz_isRageQuitExtensionDelayPassed_ReturnsTrue( - uint16 rageQuitExtensionDelayStartedAtSeconds, - uint16 rageQuitExtensionDelaySeconds + Timestamp rageQuitExtensionDelayStartedAt, + Duration rageQuitExtensionDelay ) external { - vm.assume(rageQuitExtensionDelayStartedAtSeconds > 0); + vm.assume(rageQuitExtensionDelayStartedAt > Timestamps.ZERO); + vm.assume(rageQuitExtensionDelayStartedAt < Timestamps.from(type(uint16).max)); + vm.assume(rageQuitExtensionDelay < Durations.from(type(uint16).max)); - Duration rageQuitExtensionDelay = Durations.from(rageQuitExtensionDelaySeconds); - - _context.rageQuitExtensionDelayStartedAt = Timestamps.from(rageQuitExtensionDelayStartedAtSeconds); + _context.rageQuitExtensionDelayStartedAt = rageQuitExtensionDelayStartedAt; _context.rageQuitExtensionDelay = rageQuitExtensionDelay; - vm.warp(uint256(rageQuitExtensionDelayStartedAtSeconds) + uint256(rageQuitExtensionDelaySeconds) + 1); + vm.warp(rageQuitExtensionDelayStartedAt.toSeconds() + rageQuitExtensionDelay.toSeconds() + 1); bool res = EscrowState.isRageQuitExtensionDelayPassed(_context); assert(res == true); } function testFuzz_isRageQuitExtensionDelayPassed_ReturnsFalse( - uint16 rageQuitExtensionDelayStartedAtSeconds, - uint16 rageQuitExtensionDelaySeconds + Timestamp rageQuitExtensionDelayStartedAt, + Duration rageQuitExtensionDelay ) external { - vm.assume(rageQuitExtensionDelayStartedAtSeconds > 0); - - Duration rageQuitExtensionDelay = Durations.from(rageQuitExtensionDelaySeconds); + vm.assume(rageQuitExtensionDelayStartedAt > Timestamps.ZERO); + vm.assume(rageQuitExtensionDelayStartedAt < Timestamps.from(type(uint16).max)); + vm.assume(rageQuitExtensionDelay < Durations.from(type(uint16).max)); - _context.rageQuitExtensionDelayStartedAt = Timestamps.from(rageQuitExtensionDelayStartedAtSeconds); + _context.rageQuitExtensionDelayStartedAt = rageQuitExtensionDelayStartedAt; _context.rageQuitExtensionDelay = rageQuitExtensionDelay; - vm.warp(uint256(rageQuitExtensionDelayStartedAtSeconds) + uint256(rageQuitExtensionDelaySeconds)); + vm.warp(rageQuitExtensionDelayStartedAt.toSeconds() + rageQuitExtensionDelay.toSeconds()); bool res = EscrowState.isRageQuitExtensionDelayPassed(_context); assert(res == false); } From a06b67aa37f28daf7d00985d79b979d453aedf38 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Mon, 19 Aug 2024 17:50:32 +0400 Subject: [PATCH 33/42] Pass named parameters to checkContext() --- test/unit/libraries/EscrowState.t.sol | 40 +++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/test/unit/libraries/EscrowState.t.sol b/test/unit/libraries/EscrowState.t.sol index dbd4e12c..f54b9c54 100644 --- a/test/unit/libraries/EscrowState.t.sol +++ b/test/unit/libraries/EscrowState.t.sol @@ -28,7 +28,13 @@ contract EscrowStateUnitTests is UnitTest { EscrowState.initialize(_context, minAssetsLockDuration); - checkContext(State.SignallingEscrow, minAssetsLockDuration, D0, D0, T0); + checkContext({ + state: State.SignallingEscrow, + minAssetsLockDuration: minAssetsLockDuration, + rageQuitExtensionDelay: D0, + rageQuitWithdrawalsTimelock: D0, + rageQuitExtensionDelayStartedAt: T0 + }); } function testFuzz_initialize_RevertOn_InvalidState(Duration minAssetsLockDuration) external { @@ -56,7 +62,13 @@ contract EscrowStateUnitTests is UnitTest { EscrowState.startRageQuit(_context, rageQuitExtensionDelay, rageQuitWithdrawalsTimelock); - checkContext(State.RageQuitEscrow, D0, rageQuitExtensionDelay, rageQuitWithdrawalsTimelock, T0); + checkContext({ + state: State.RageQuitEscrow, + minAssetsLockDuration: D0, + rageQuitExtensionDelay: rageQuitExtensionDelay, + rageQuitWithdrawalsTimelock: rageQuitWithdrawalsTimelock, + rageQuitExtensionDelayStartedAt: T0 + }); } function testFuzz_startRageQuit_RevertOn_InvalidState( @@ -80,7 +92,13 @@ contract EscrowStateUnitTests is UnitTest { EscrowState.startRageQuitExtensionDelay(_context); - checkContext(State.NotInitialized, D0, D0, D0, Timestamps.now()); + checkContext({ + state: State.NotInitialized, + minAssetsLockDuration: D0, + rageQuitExtensionDelay: D0, + rageQuitWithdrawalsTimelock: D0, + rageQuitExtensionDelayStartedAt: Timestamps.now() + }); } // --- @@ -95,7 +113,13 @@ contract EscrowStateUnitTests is UnitTest { EscrowState.setMinAssetsLockDuration(_context, minAssetsLockDuration); - checkContext(State.NotInitialized, minAssetsLockDuration, D0, D0, T0); + checkContext({ + state: State.NotInitialized, + minAssetsLockDuration: minAssetsLockDuration, + rageQuitExtensionDelay: D0, + rageQuitWithdrawalsTimelock: D0, + rageQuitExtensionDelayStartedAt: T0 + }); } function test_setMinAssetsLockDuration_WhenDurationNotChanged(Duration minAssetsLockDuration) external { @@ -105,7 +129,13 @@ contract EscrowStateUnitTests is UnitTest { EscrowState.setMinAssetsLockDuration(_context, minAssetsLockDuration); - checkContext(State.NotInitialized, minAssetsLockDuration, D0, D0, T0); + checkContext({ + state: State.NotInitialized, + minAssetsLockDuration: minAssetsLockDuration, + rageQuitExtensionDelay: D0, + rageQuitWithdrawalsTimelock: D0, + rageQuitExtensionDelayStartedAt: T0 + }); assertEq(entries.length, 0); } From b1103f101406a0c9f7f0c742c1b78ac7d50ce0ee Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Mon, 19 Aug 2024 18:08:28 +0400 Subject: [PATCH 34/42] Use assertEq() instead of regular assert() --- test/unit/libraries/EscrowState.t.sol | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/test/unit/libraries/EscrowState.t.sol b/test/unit/libraries/EscrowState.t.sol index f54b9c54..74b452cc 100644 --- a/test/unit/libraries/EscrowState.t.sol +++ b/test/unit/libraries/EscrowState.t.sol @@ -263,7 +263,7 @@ contract EscrowStateUnitTests is UnitTest { function testFuzz_isRageQuitExtensionDelayStarted_happyPath(Timestamp rageQuitExtensionDelayStartedAt) external { _context.rageQuitExtensionDelayStartedAt = rageQuitExtensionDelayStartedAt; bool res = EscrowState.isRageQuitExtensionDelayStarted(_context); - assert(res == _context.rageQuitExtensionDelayStartedAt.isNotZero()); + assertEq(res, _context.rageQuitExtensionDelayStartedAt.isNotZero()); } // --- @@ -283,7 +283,7 @@ contract EscrowStateUnitTests is UnitTest { vm.warp(rageQuitExtensionDelayStartedAt.toSeconds() + rageQuitExtensionDelay.toSeconds() + 1); bool res = EscrowState.isRageQuitExtensionDelayPassed(_context); - assert(res == true); + assertTrue(res); } function testFuzz_isRageQuitExtensionDelayPassed_ReturnsFalse( @@ -299,13 +299,13 @@ contract EscrowStateUnitTests is UnitTest { vm.warp(rageQuitExtensionDelayStartedAt.toSeconds() + rageQuitExtensionDelay.toSeconds()); bool res = EscrowState.isRageQuitExtensionDelayPassed(_context); - assert(res == false); + assertFalse(res); } function test_isRageQuitExtensionDelayPassed_ReturnsFalseWhenRageQuitExtraTimelockNotStarted() external { vm.warp(1234); bool res = EscrowState.isRageQuitExtensionDelayPassed(_context); - assert(res == false); + assertFalse(res); } // --- @@ -317,7 +317,7 @@ contract EscrowStateUnitTests is UnitTest { _context.state = State.RageQuitEscrow; } bool actualResult = EscrowState.isRageQuitEscrow(_context); - assert(actualResult == expectedResult); + assertEq(actualResult, expectedResult); } // --- @@ -330,11 +330,15 @@ contract EscrowStateUnitTests is UnitTest { Duration rageQuitExtensionDelay, Duration rageQuitWithdrawalsTimelock, Timestamp rageQuitExtensionDelayStartedAt - ) internal view { - assert(_context.state == state); - assert(_context.minAssetsLockDuration == minAssetsLockDuration); - assert(_context.rageQuitExtensionDelay == rageQuitExtensionDelay); - assert(_context.rageQuitWithdrawalsTimelock == rageQuitWithdrawalsTimelock); - assert(_context.rageQuitExtensionDelayStartedAt == rageQuitExtensionDelayStartedAt); + ) internal { + assertEq(_context.state, state); + assertEq(_context.minAssetsLockDuration, minAssetsLockDuration); + assertEq(_context.rageQuitExtensionDelay, rageQuitExtensionDelay); + assertEq(_context.rageQuitWithdrawalsTimelock, rageQuitWithdrawalsTimelock); + assertEq(_context.rageQuitExtensionDelayStartedAt, rageQuitExtensionDelayStartedAt); + } + + function assertEq(State a, State b) internal { + assertEq(uint256(a), uint256(b)); } } From 4e4982e42ae1ff0d3e919589ec37e1bf9e49c44b Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Mon, 19 Aug 2024 19:18:02 +0400 Subject: [PATCH 35/42] Use _wait() instead of vm.warp() --- test/unit/libraries/EscrowState.t.sol | 28 ++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/test/unit/libraries/EscrowState.t.sol b/test/unit/libraries/EscrowState.t.sol index 74b452cc..6602520c 100644 --- a/test/unit/libraries/EscrowState.t.sol +++ b/test/unit/libraries/EscrowState.t.sol @@ -206,9 +206,13 @@ contract EscrowStateUnitTests is UnitTest { _context.rageQuitExtensionDelay = rageQuitExtensionDelay; _context.rageQuitWithdrawalsTimelock = rageQuitWithdrawalsTimelock; - vm.warp( - rageQuitExtensionDelayStartedAt.toSeconds() + rageQuitExtensionDelay.toSeconds() - + rageQuitWithdrawalsTimelock.toSeconds() + 1 + _wait( + Durations.between( + (rageQuitExtensionDelay + rageQuitWithdrawalsTimelock).plusSeconds(1).addTo( + rageQuitExtensionDelayStartedAt + ), + Timestamps.now() + ) ); EscrowState.checkWithdrawalsTimelockPassed(_context); } @@ -233,9 +237,11 @@ contract EscrowStateUnitTests is UnitTest { _context.rageQuitExtensionDelay = rageQuitExtensionDelay; _context.rageQuitWithdrawalsTimelock = rageQuitWithdrawalsTimelock; - vm.warp( - rageQuitExtensionDelayStartedAt.toSeconds() + rageQuitExtensionDelay.toSeconds() - + rageQuitWithdrawalsTimelock.toSeconds() + _wait( + Durations.between( + (rageQuitExtensionDelay + rageQuitWithdrawalsTimelock).addTo(rageQuitExtensionDelayStartedAt), + Timestamps.now() + ) ); vm.expectRevert(EscrowState.WithdrawalsTimelockNotPassed.selector); @@ -281,7 +287,11 @@ contract EscrowStateUnitTests is UnitTest { _context.rageQuitExtensionDelayStartedAt = rageQuitExtensionDelayStartedAt; _context.rageQuitExtensionDelay = rageQuitExtensionDelay; - vm.warp(rageQuitExtensionDelayStartedAt.toSeconds() + rageQuitExtensionDelay.toSeconds() + 1); + _wait( + Durations.between( + rageQuitExtensionDelay.plusSeconds(1).addTo(rageQuitExtensionDelayStartedAt), Timestamps.now() + ) + ); bool res = EscrowState.isRageQuitExtensionDelayPassed(_context); assertTrue(res); } @@ -297,13 +307,13 @@ contract EscrowStateUnitTests is UnitTest { _context.rageQuitExtensionDelayStartedAt = rageQuitExtensionDelayStartedAt; _context.rageQuitExtensionDelay = rageQuitExtensionDelay; - vm.warp(rageQuitExtensionDelayStartedAt.toSeconds() + rageQuitExtensionDelay.toSeconds()); + _wait(Durations.between(rageQuitExtensionDelay.addTo(rageQuitExtensionDelayStartedAt), Timestamps.now())); bool res = EscrowState.isRageQuitExtensionDelayPassed(_context); assertFalse(res); } function test_isRageQuitExtensionDelayPassed_ReturnsFalseWhenRageQuitExtraTimelockNotStarted() external { - vm.warp(1234); + _wait(Durations.from(1234)); bool res = EscrowState.isRageQuitExtensionDelayPassed(_context); assertFalse(res); } From ba395fa76536190a62c05668413adf39f16d9b7f Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Mon, 19 Aug 2024 21:56:51 +0400 Subject: [PATCH 36/42] Remove excessive type casts in AssetsAccounting tests --- test/unit/libraries/AssetsAccounting.t.sol | 500 ++++++++++----------- 1 file changed, 238 insertions(+), 262 deletions(-) diff --git a/test/unit/libraries/AssetsAccounting.t.sol b/test/unit/libraries/AssetsAccounting.t.sol index 408d4674..b624ed33 100644 --- a/test/unit/libraries/AssetsAccounting.t.sol +++ b/test/unit/libraries/AssetsAccounting.t.sol @@ -23,17 +23,16 @@ contract AssetsAccountingUnitTests is UnitTest { // accountStETHSharesLock() // --- - function testFuzz_accountStETHSharesLock_happyPath(address holder, uint128 sharesAmount) external { + function testFuzz_accountStETHSharesLock_happyPath(address holder, SharesValue shares) external { SharesValue totalLockedShares = SharesValues.from(3); SharesValue holderLockedShares = SharesValues.from(1); - vm.assume(sharesAmount > 0); + vm.assume(shares.toUint256() > 0); vm.assume( - sharesAmount < type(uint128).max - Math.max(totalLockedShares.toUint256(), holderLockedShares.toUint256()) + shares.toUint256() + < type(uint128).max - Math.max(totalLockedShares.toUint256(), holderLockedShares.toUint256()) ); - SharesValue shares = SharesValues.from(sharesAmount); - _accountingContext.stETHTotals.lockedShares = totalLockedShares; _accountingContext.assets[holder].stETHLockedShares = holderLockedShares; @@ -45,10 +44,10 @@ contract AssetsAccountingUnitTests is UnitTest { checkAccountingContextTotalCounters( totalLockedShares + shares, ETHValues.ZERO, SharesValues.ZERO, ETHValues.ZERO ); - assert(_accountingContext.assets[holder].stETHLockedShares == holderLockedShares + shares); - assert(_accountingContext.assets[holder].unstETHLockedShares == SharesValues.ZERO); - assert(_accountingContext.assets[holder].lastAssetsLockTimestamp <= Timestamps.now()); - assert(_accountingContext.assets[holder].unstETHIds.length == 0); + assertEq(_accountingContext.assets[holder].stETHLockedShares, holderLockedShares + shares); + assertEq(_accountingContext.assets[holder].unstETHLockedShares, SharesValues.ZERO); + assertLe(_accountingContext.assets[holder].lastAssetsLockTimestamp.toSeconds(), Timestamps.now().toSeconds()); + assertEq(_accountingContext.assets[holder].unstETHIds.length, 0); } function testFuzz_accountStETHSharesLock_RevertWhen_ZeroSharesProvided(address holder) external { @@ -61,14 +60,12 @@ contract AssetsAccountingUnitTests is UnitTest { function testFuzz_accountStETHSharesLock_WhenNoSharesWereLockedBefore( address stranger, - uint128 sharesAmount + SharesValue shares ) external { SharesValue totalLockedShares = SharesValues.from(3); - vm.assume(sharesAmount > 0); - vm.assume(sharesAmount < type(uint128).max - totalLockedShares.toUint256()); - - SharesValue shares = SharesValues.from(sharesAmount); + vm.assume(shares.toUint256() > 0); + vm.assume(shares.toUint256() < type(uint128).max - totalLockedShares.toUint256()); _accountingContext.stETHTotals.lockedShares = totalLockedShares; @@ -77,9 +74,9 @@ contract AssetsAccountingUnitTests is UnitTest { AssetsAccounting.accountStETHSharesLock(_accountingContext, stranger, shares); - assert(_accountingContext.stETHTotals.lockedShares == totalLockedShares + shares); - assert(_accountingContext.assets[stranger].stETHLockedShares == shares); - assert(_accountingContext.assets[stranger].lastAssetsLockTimestamp <= Timestamps.now()); + assertEq(_accountingContext.stETHTotals.lockedShares, totalLockedShares + shares); + assertEq(_accountingContext.assets[stranger].stETHLockedShares, shares); + assertLe(_accountingContext.assets[stranger].lastAssetsLockTimestamp.toSeconds(), Timestamps.now().toSeconds()); } // --- @@ -88,16 +85,14 @@ contract AssetsAccountingUnitTests is UnitTest { function testFuzz_accountStETHSharesUnlock_happyPath( address holder, - uint128 sharesAmount, - uint128 holderSharesAmount + SharesValue shares, + SharesValue holderLockedShares ) external { SharesValue totalLockedSharesWithoutHolder = SharesValues.from(3); - vm.assume(sharesAmount > 0); - vm.assume(holderSharesAmount < type(uint128).max - totalLockedSharesWithoutHolder.toUint256()); - vm.assume(sharesAmount <= holderSharesAmount); + vm.assume(shares.toUint256() > 0); + vm.assume(holderLockedShares.toUint256() < type(uint128).max - totalLockedSharesWithoutHolder.toUint256()); + vm.assume(shares.toUint256() <= holderLockedShares.toUint256()); - SharesValue shares = SharesValues.from(sharesAmount); - SharesValue holderLockedShares = SharesValues.from(holderSharesAmount); SharesValue totalLockedShares = totalLockedSharesWithoutHolder + holderLockedShares; _accountingContext.stETHTotals.lockedShares = totalLockedShares; @@ -111,10 +106,10 @@ contract AssetsAccountingUnitTests is UnitTest { checkAccountingContextTotalCounters( totalLockedShares - shares, ETHValues.ZERO, SharesValues.ZERO, ETHValues.ZERO ); - assert(_accountingContext.assets[holder].stETHLockedShares == holderLockedShares - shares); - assert(_accountingContext.assets[holder].unstETHLockedShares == SharesValues.ZERO); - assert(_accountingContext.assets[holder].lastAssetsLockTimestamp == Timestamps.ZERO); - assert(_accountingContext.assets[holder].unstETHIds.length == 0); + assertEq(_accountingContext.assets[holder].stETHLockedShares, holderLockedShares - shares); + assertEq(_accountingContext.assets[holder].unstETHLockedShares, SharesValues.ZERO); + assertEq(_accountingContext.assets[holder].lastAssetsLockTimestamp, Timestamps.ZERO); + assertEq(_accountingContext.assets[holder].unstETHIds.length, 0); } function testFuzz_accountStETHSharesUnlock_RevertOn_ZeroSharesProvided(address holder) external { @@ -127,19 +122,15 @@ contract AssetsAccountingUnitTests is UnitTest { function testFuzz_accountStETHSharesUnlock_RevertWhen_HolderHaveLessSharesThanProvided( address holder, - uint128 sharesAmount, - uint128 holderSharesAmount + SharesValue shares, + SharesValue holderLockedShares ) external { SharesValue totalLockedSharesWithoutHolder = SharesValues.from(3); - vm.assume(sharesAmount > 0); - vm.assume(holderSharesAmount < type(uint128).max - totalLockedSharesWithoutHolder.toUint256()); - vm.assume(sharesAmount > holderSharesAmount); + vm.assume(shares.toUint256() > 0); + vm.assume(holderLockedShares.toUint256() < type(uint128).max - totalLockedSharesWithoutHolder.toUint256()); + vm.assume(shares.toUint256() > holderLockedShares.toUint256()); - SharesValue shares = SharesValues.from(sharesAmount); - SharesValue holderLockedShares = SharesValues.from(holderSharesAmount); - SharesValue totalLockedShares = totalLockedSharesWithoutHolder + holderLockedShares; - - _accountingContext.stETHTotals.lockedShares = totalLockedShares; + _accountingContext.stETHTotals.lockedShares = totalLockedSharesWithoutHolder + holderLockedShares; _accountingContext.assets[holder].stETHLockedShares = holderLockedShares; vm.expectRevert(abi.encodeWithSelector(AssetsAccounting.InvalidSharesValue.selector, shares)); @@ -149,18 +140,14 @@ contract AssetsAccountingUnitTests is UnitTest { function testFuzz_accountStETHSharesUnlock_RevertOn_AccountingError_TotalLockedSharesCounterIsLessThanProvidedSharesAmount( address holder, - uint128 sharesAmount, - uint128 totalSharesAmount + SharesValue shares, + SharesValue totalLockedShares ) external { - vm.assume(sharesAmount > 0); - vm.assume(totalSharesAmount < sharesAmount); - - SharesValue shares = SharesValues.from(sharesAmount); - SharesValue holderLockedShares = SharesValues.from(sharesAmount); - SharesValue totalLockedShares = SharesValues.from(totalSharesAmount); + vm.assume(shares.toUint256() > 0); + vm.assume(totalLockedShares.toUint256() < shares.toUint256()); _accountingContext.stETHTotals.lockedShares = totalLockedShares; - _accountingContext.assets[holder].stETHLockedShares = holderLockedShares; + _accountingContext.assets[holder].stETHLockedShares = shares; vm.expectRevert(stdError.arithmeticError); @@ -169,11 +156,9 @@ contract AssetsAccountingUnitTests is UnitTest { function testFuzz_accountStETHSharesUnlock_RevertWhen_NoSharesWereLockedBefore( address stranger, - uint128 sharesAmount + SharesValue shares ) external { - vm.assume(sharesAmount > 0); - - SharesValue shares = SharesValues.from(sharesAmount); + vm.assume(shares.toUint256() > 0); vm.expectRevert(abi.encodeWithSelector(AssetsAccounting.InvalidSharesValue.selector, shares)); @@ -184,12 +169,14 @@ contract AssetsAccountingUnitTests is UnitTest { // accountStETHSharesUnlock(Context storage self, address holder) // --- - function testFuzz_accountStETHSharesUnlock_simple_happyPath(address holder, uint128 holderSharesAmount) external { + function testFuzz_accountStETHSharesUnlock_simple_happyPath( + address holder, + SharesValue holderLockedShares + ) external { SharesValue totalLockedSharesWithoutHolder = SharesValues.from(3); - vm.assume(holderSharesAmount > 0); - vm.assume(holderSharesAmount < type(uint128).max - totalLockedSharesWithoutHolder.toUint256()); + vm.assume(holderLockedShares.toUint256() > 0); + vm.assume(holderLockedShares.toUint256() < type(uint128).max - totalLockedSharesWithoutHolder.toUint256()); - SharesValue holderLockedShares = SharesValues.from(holderSharesAmount); SharesValue totalLockedShares = totalLockedSharesWithoutHolder + holderLockedShares; _accountingContext.stETHTotals.lockedShares = totalLockedShares; @@ -200,14 +187,14 @@ contract AssetsAccountingUnitTests is UnitTest { SharesValue unlockedShares = AssetsAccounting.accountStETHSharesUnlock(_accountingContext, holder); - assert(unlockedShares == holderLockedShares); + assertEq(unlockedShares, holderLockedShares); checkAccountingContextTotalCounters( totalLockedShares - holderLockedShares, ETHValues.ZERO, SharesValues.ZERO, ETHValues.ZERO ); - assert(_accountingContext.assets[holder].stETHLockedShares == holderLockedShares - holderLockedShares); - assert(_accountingContext.assets[holder].unstETHLockedShares == SharesValues.ZERO); - assert(_accountingContext.assets[holder].lastAssetsLockTimestamp == Timestamps.ZERO); - assert(_accountingContext.assets[holder].unstETHIds.length == 0); + assertEq(_accountingContext.assets[holder].stETHLockedShares, holderLockedShares - holderLockedShares); + assertEq(_accountingContext.assets[holder].unstETHLockedShares, SharesValues.ZERO); + assertEq(_accountingContext.assets[holder].lastAssetsLockTimestamp, Timestamps.ZERO); + assertEq(_accountingContext.assets[holder].unstETHIds.length, 0); } function testFuzz_accountStETHSharesUnlock_simple_RevertWhen_NoSharesWereLockedBefore(address stranger) external { @@ -222,36 +209,33 @@ contract AssetsAccountingUnitTests is UnitTest { function testFuzz_accountStETHSharesWithdraw_happyPath( address holder, - uint128 holderLockedSharesAmount, - uint128 totalLockedSharesAmount, - uint128 totalClaimedETHAmount + SharesValue holderLockedShares, + SharesValue totalLockedShares, + ETHValue totalClaimedETH ) external { - vm.assume(totalLockedSharesAmount > 0); - vm.assume(holderLockedSharesAmount > 0); - vm.assume(holderLockedSharesAmount <= totalLockedSharesAmount); - - SharesValue holderLockedShares = SharesValues.from(holderLockedSharesAmount); - SharesValue totalLockedShares = SharesValues.from(totalLockedSharesAmount); - ETHValue totalClaimedETH = ETHValues.from(totalClaimedETHAmount); + vm.assume(totalLockedShares.toUint256() > 0); + vm.assume(holderLockedShares.toUint256() > 0); + vm.assume(holderLockedShares.toUint256() <= totalLockedShares.toUint256()); _accountingContext.assets[holder].stETHLockedShares = holderLockedShares; _accountingContext.stETHTotals.lockedShares = totalLockedShares; _accountingContext.stETHTotals.claimedETH = totalClaimedETH; - ETHValue expectedETHWithdrawn = - ETHValues.from((uint256(totalClaimedETHAmount) * holderLockedSharesAmount) / totalLockedSharesAmount); + ETHValue expectedETHWithdrawn = ETHValues.from( + (totalClaimedETH.toUint256() * holderLockedShares.toUint256()) / totalLockedShares.toUint256() + ); vm.expectEmit(); emit AssetsAccounting.ETHWithdrawn(holder, holderLockedShares, expectedETHWithdrawn); ETHValue ethWithdrawn = AssetsAccounting.accountStETHSharesWithdraw(_accountingContext, holder); - assert(ethWithdrawn == expectedETHWithdrawn); + assertEq(ethWithdrawn, expectedETHWithdrawn); checkAccountingContextTotalCounters(totalLockedShares, totalClaimedETH, SharesValues.ZERO, ETHValues.ZERO); - assert(_accountingContext.assets[holder].stETHLockedShares == SharesValues.ZERO); - assert(_accountingContext.assets[holder].unstETHLockedShares == SharesValues.ZERO); - assert(_accountingContext.assets[holder].lastAssetsLockTimestamp == Timestamps.ZERO); - assert(_accountingContext.assets[holder].unstETHIds.length == 0); + assertEq(_accountingContext.assets[holder].stETHLockedShares, SharesValues.ZERO); + assertEq(_accountingContext.assets[holder].unstETHLockedShares, SharesValues.ZERO); + assertEq(_accountingContext.assets[holder].lastAssetsLockTimestamp, Timestamps.ZERO); + assertEq(_accountingContext.assets[holder].unstETHIds.length, 0); } function testFuzz_accountStETHSharesWithdraw_RevertWhen_HolderHaveZeroShares(address stranger) external { @@ -262,13 +246,10 @@ contract AssetsAccountingUnitTests is UnitTest { function testFuzz_accountStETHSharesWithdraw_RevertOn_AccountingError_TotalLockedSharesCounterIsZero( address holder, - uint128 holderLockedSharesAmount, - uint128 totalClaimedETHAmount + SharesValue holderLockedShares, + ETHValue totalClaimedETH ) external { - vm.assume(holderLockedSharesAmount > 0); - - SharesValue holderLockedShares = SharesValues.from(holderLockedSharesAmount); - ETHValue totalClaimedETH = ETHValues.from(totalClaimedETHAmount); + vm.assume(holderLockedShares.toUint256() > 0); _accountingContext.assets[holder].stETHLockedShares = holderLockedShares; _accountingContext.stETHTotals.lockedShares = SharesValues.ZERO; @@ -281,33 +262,31 @@ contract AssetsAccountingUnitTests is UnitTest { function testFuzz_accountStETHSharesWithdraw_AccountingError_WithdrawAmountMoreThanTotalClaimedETH( address holder, - uint128 holderLockedSharesAmount, - uint128 totalClaimedETHAmount + SharesValue holderLockedShares, + ETHValue totalClaimedETH ) external { uint128 totalLockedSharesAmount = 10; - vm.assume(holderLockedSharesAmount > totalLockedSharesAmount); - vm.assume(holderLockedSharesAmount < type(uint64).max); - vm.assume(totalClaimedETHAmount < type(uint64).max); + vm.assume(holderLockedShares.toUint256() > totalLockedSharesAmount); + vm.assume(holderLockedShares.toUint256() < type(uint64).max); + vm.assume(totalClaimedETH.toUint256() < type(uint64).max); - SharesValue holderLockedShares = SharesValues.from(holderLockedSharesAmount); SharesValue totalLockedShares = SharesValues.from(totalLockedSharesAmount); - ETHValue totalClaimedETH = ETHValues.from(totalClaimedETHAmount); _accountingContext.assets[holder].stETHLockedShares = holderLockedShares; _accountingContext.stETHTotals.lockedShares = totalLockedShares; _accountingContext.stETHTotals.claimedETH = totalClaimedETH; ETHValue expectedETHWithdrawn = - ETHValues.from((uint256(totalClaimedETHAmount) * holderLockedSharesAmount) / totalLockedSharesAmount); + ETHValues.from((totalClaimedETH.toUint256() * holderLockedShares.toUint256()) / totalLockedSharesAmount); vm.expectEmit(); emit AssetsAccounting.ETHWithdrawn(holder, holderLockedShares, expectedETHWithdrawn); ETHValue ethWithdrawn = AssetsAccounting.accountStETHSharesWithdraw(_accountingContext, holder); - assert(ethWithdrawn == expectedETHWithdrawn); - assert(ethWithdrawn.toUint256() >= totalClaimedETHAmount); - assert(_accountingContext.assets[holder].stETHLockedShares == SharesValues.ZERO); + assertEq(ethWithdrawn, expectedETHWithdrawn); + assertGe(ethWithdrawn.toUint256(), totalClaimedETH.toUint256()); + assertEq(_accountingContext.assets[holder].stETHLockedShares, SharesValues.ZERO); } function testFuzz_accountStETHSharesWithdraw_RevertOn_AccountingError_WithdrawAmountOverflow(address holder) @@ -330,12 +309,9 @@ contract AssetsAccountingUnitTests is UnitTest { // accountClaimedStETH // --- - function testFuzz_accountClaimedStETH_happyPath(uint128 ethAmount, uint128 totalClaimedETHAmount) external { - vm.assume(ethAmount < type(uint128).max / 2); - vm.assume(totalClaimedETHAmount < type(uint128).max / 2); - - ETHValue amount = ETHValues.from(ethAmount); - ETHValue totalClaimedETH = ETHValues.from(totalClaimedETHAmount); + function testFuzz_accountClaimedStETH_happyPath(ETHValue amount, ETHValue totalClaimedETH) external { + vm.assume(amount.toUint256() < type(uint128).max / 2); + vm.assume(totalClaimedETH.toUint256() < type(uint128).max / 2); _accountingContext.stETHTotals.claimedETH = totalClaimedETH; @@ -357,14 +333,14 @@ contract AssetsAccountingUnitTests is UnitTest { function testFuzz_accountUnstETHLock_happyPath( address holder, uint96[] memory amountsOfShares, - uint96 holderUnstETHLockedSharesAmount, - uint96 initialTotalUnfinalizedSharesAmount + SharesValue holderUnstETHLockedShares, + SharesValue initialTotalUnfinalizedShares ) external { vm.assume(amountsOfShares.length > 1); vm.assume(amountsOfShares.length <= 500); + vm.assume(holderUnstETHLockedShares.toUint256() < type(uint96).max); + vm.assume(initialTotalUnfinalizedShares.toUint256() < type(uint96).max); - SharesValue holderUnstETHLockedShares = SharesValues.from(holderUnstETHLockedSharesAmount); - SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); uint256 expectedTotalUnstETHLockedAmount = 0; _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; @@ -394,19 +370,19 @@ contract AssetsAccountingUnitTests is UnitTest { initialTotalUnfinalizedShares + SharesValues.from(expectedTotalUnstETHLockedAmount), ETHValues.ZERO ); - assert(_accountingContext.assets[holder].stETHLockedShares == SharesValues.ZERO); - assert( - _accountingContext.assets[holder].unstETHLockedShares - == holderUnstETHLockedShares + SharesValues.from(expectedTotalUnstETHLockedAmount) + assertEq(_accountingContext.assets[holder].stETHLockedShares, SharesValues.ZERO); + assertEq( + _accountingContext.assets[holder].unstETHLockedShares, + holderUnstETHLockedShares + SharesValues.from(expectedTotalUnstETHLockedAmount) ); - assert(_accountingContext.assets[holder].lastAssetsLockTimestamp <= Timestamps.now()); - assert(_accountingContext.assets[holder].unstETHIds.length == amountsOfShares.length + 1); + assertLe(_accountingContext.assets[holder].lastAssetsLockTimestamp.toSeconds(), Timestamps.now().toSeconds()); + assertEq(_accountingContext.assets[holder].unstETHIds.length, amountsOfShares.length + 1); for (uint256 i = 0; i < unstETHIds.length; ++i) { - assert(_accountingContext.unstETHRecords[unstETHIds[i]].lockedBy == holder); - assert(_accountingContext.unstETHRecords[unstETHIds[i]].status == UnstETHRecordStatus.Locked); - assert(_accountingContext.unstETHRecords[unstETHIds[i]].index.toZeroBasedValue() == i + 1); - assert(_accountingContext.unstETHRecords[unstETHIds[i]].shares == SharesValues.from(amountsOfShares[i])); - assert(_accountingContext.unstETHRecords[unstETHIds[i]].claimableAmount == ETHValues.ZERO); + assertEq(_accountingContext.unstETHRecords[unstETHIds[i]].lockedBy, holder); + assertEq(_accountingContext.unstETHRecords[unstETHIds[i]].status, UnstETHRecordStatus.Locked); + assertEq(_accountingContext.unstETHRecords[unstETHIds[i]].index.toZeroBasedValue(), i + 1); + assertEq(_accountingContext.unstETHRecords[unstETHIds[i]].shares, SharesValues.from(amountsOfShares[i])); + assertEq(_accountingContext.unstETHRecords[unstETHIds[i]].claimableAmount, ETHValues.ZERO); } } @@ -424,14 +400,13 @@ contract AssetsAccountingUnitTests is UnitTest { function testFuzz_accountUnstETHLock_RevertOn_WithdrawalRequestStatusIsFinalized( address holder, uint96[] memory amountsOfShares, - uint96 holderUnstETHLockedSharesAmount, - uint96 initialTotalUnfinalizedSharesAmount + SharesValue holderUnstETHLockedShares, + SharesValue initialTotalUnfinalizedShares ) external { vm.assume(amountsOfShares.length > 0); vm.assume(amountsOfShares.length <= 500); - - SharesValue holderUnstETHLockedShares = SharesValues.from(holderUnstETHLockedSharesAmount); - SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + vm.assume(holderUnstETHLockedShares.toUint256() < type(uint96).max); + vm.assume(initialTotalUnfinalizedShares.toUint256() < type(uint96).max); _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; @@ -464,14 +439,13 @@ contract AssetsAccountingUnitTests is UnitTest { function testFuzz_accountUnstETHLock_RevertOn_WithdrawalRequestStatusIsClaimed( address holder, uint96[] memory amountsOfShares, - uint96 holderUnstETHLockedSharesAmount, - uint96 initialTotalUnfinalizedSharesAmount + SharesValue holderUnstETHLockedShares, + SharesValue initialTotalUnfinalizedShares ) external { vm.assume(amountsOfShares.length > 0); vm.assume(amountsOfShares.length <= 500); - - SharesValue holderUnstETHLockedShares = SharesValues.from(holderUnstETHLockedSharesAmount); - SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + vm.assume(holderUnstETHLockedShares.toUint256() < type(uint96).max); + vm.assume(initialTotalUnfinalizedShares.toUint256() < type(uint96).max); _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; @@ -498,14 +472,13 @@ contract AssetsAccountingUnitTests is UnitTest { function testFuzz_accountUnstETHLock_RevertOn_UnstETHRecordStatusIsNot_NotLocked( address holder, uint96[] memory amountsOfShares, - uint96 holderUnstETHLockedSharesAmount, - uint96 initialTotalUnfinalizedSharesAmount + SharesValue holderUnstETHLockedShares, + SharesValue initialTotalUnfinalizedShares ) external { vm.assume(amountsOfShares.length > 0); vm.assume(amountsOfShares.length <= 500); - - SharesValue holderUnstETHLockedShares = SharesValues.from(holderUnstETHLockedSharesAmount); - SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + vm.assume(holderUnstETHLockedShares.toUint256() < type(uint96).max); + vm.assume(initialTotalUnfinalizedShares.toUint256() < type(uint96).max); _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; @@ -538,14 +511,13 @@ contract AssetsAccountingUnitTests is UnitTest { function testFuzz_accountUnstETHLock_RevertWhen_DuplicatingUnstETHIdsProvided( address holder, uint96[] memory amountsOfShares, - uint96 holderUnstETHLockedSharesAmount, - uint96 initialTotalUnfinalizedSharesAmount + SharesValue holderUnstETHLockedShares, + SharesValue initialTotalUnfinalizedShares ) external { vm.assume(amountsOfShares.length > 1); vm.assume(amountsOfShares.length <= 500); - - SharesValue holderUnstETHLockedShares = SharesValues.from(holderUnstETHLockedSharesAmount); - SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + vm.assume(holderUnstETHLockedShares.toUint256() < type(uint96).max); + vm.assume(initialTotalUnfinalizedShares.toUint256() < type(uint96).max); _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; @@ -578,11 +550,11 @@ contract AssetsAccountingUnitTests is UnitTest { // TODO: is it expected behavior? function testFuzz_accountUnstETHLock_WhenNoUnstETHIdsProvided( address holder, - uint96 holderUnstETHLockedSharesAmount, - uint96 initialTotalUnfinalizedSharesAmount + SharesValue holderUnstETHLockedShares, + SharesValue initialTotalUnfinalizedShares ) external { - SharesValue holderUnstETHLockedShares = SharesValues.from(holderUnstETHLockedSharesAmount); - SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + vm.assume(holderUnstETHLockedShares.toUint256() < type(uint96).max); + vm.assume(initialTotalUnfinalizedShares.toUint256() < type(uint96).max); _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; @@ -597,19 +569,19 @@ contract AssetsAccountingUnitTests is UnitTest { checkAccountingContextTotalCounters( SharesValues.ZERO, ETHValues.ZERO, initialTotalUnfinalizedShares, ETHValues.ZERO ); - assert(_accountingContext.assets[holder].stETHLockedShares == SharesValues.ZERO); - assert(_accountingContext.assets[holder].unstETHLockedShares == holderUnstETHLockedShares); - assert(_accountingContext.assets[holder].lastAssetsLockTimestamp <= Timestamps.now()); - assert(_accountingContext.assets[holder].unstETHIds.length == 0); + assertEq(_accountingContext.assets[holder].stETHLockedShares, SharesValues.ZERO); + assertEq(_accountingContext.assets[holder].unstETHLockedShares, holderUnstETHLockedShares); + assertLe(_accountingContext.assets[holder].lastAssetsLockTimestamp.toSeconds(), Timestamps.now().toSeconds()); + assertEq(_accountingContext.assets[holder].unstETHIds.length, 0); } function testFuzz_accountUnstETHLock_AccountingError_WithdrawalRequestStatusAmountOfSharesOverflow( address holder, - uint96 holderUnstETHLockedSharesAmount, - uint96 initialTotalUnfinalizedSharesAmount + SharesValue holderUnstETHLockedShares, + SharesValue initialTotalUnfinalizedShares ) external { - SharesValue holderUnstETHLockedShares = SharesValues.from(holderUnstETHLockedSharesAmount); - SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + vm.assume(holderUnstETHLockedShares.toUint256() < type(uint96).max); + vm.assume(initialTotalUnfinalizedShares.toUint256() < type(uint96).max); _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; @@ -630,10 +602,11 @@ contract AssetsAccountingUnitTests is UnitTest { function testFuzz_accountUnstETHLock_AccountingError_HolderUnstETHLockedSharesOverflow( address holder, - uint96 initialTotalUnfinalizedSharesAmount + SharesValue initialTotalUnfinalizedShares ) external { + vm.assume(initialTotalUnfinalizedShares.toUint256() < type(uint96).max); + SharesValue holderUnstETHLockedShares = SharesValues.from(type(uint128).max / 2 + 1); - SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; @@ -654,9 +627,9 @@ contract AssetsAccountingUnitTests is UnitTest { function testFuzz_accountUnstETHLock_AccountingError_TotalUnfinalizedSharesOverflow( address holder, - uint96 holderUnstETHLockedSharesAmount + SharesValue holderUnstETHLockedShares ) external { - SharesValue holderUnstETHLockedShares = SharesValues.from(holderUnstETHLockedSharesAmount); + vm.assume(holderUnstETHLockedShares.toUint256() < type(uint96).max); SharesValue initialTotalUnfinalizedShares = SharesValues.from(type(uint128).max / 2 + 1); _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; @@ -684,18 +657,18 @@ contract AssetsAccountingUnitTests is UnitTest { function testFuzz_accountUnstETHUnlock_happyPath( address holder, uint64[] memory amountsOfShares, - uint96 holderUnstETHLockedSharesAmount, - uint96 initialTotalFinalizedETHAmount, - uint96 initialTotalUnfinalizedSharesAmount + SharesValue holderUnstETHLockedShares, + ETHValue initialTotalFinalizedETH, + SharesValue initialTotalUnfinalizedShares ) external { vm.assume(amountsOfShares.length > 0); vm.assume(amountsOfShares.length <= 500); - vm.assume(holderUnstETHLockedSharesAmount > 500 * uint128(type(uint64).max)); - vm.assume(initialTotalUnfinalizedSharesAmount > 500 * uint128(type(uint64).max)); + vm.assume(holderUnstETHLockedShares.toUint256() < type(uint96).max); + vm.assume(initialTotalFinalizedETH.toUint256() < type(uint96).max); + vm.assume(initialTotalUnfinalizedShares.toUint256() < type(uint96).max); + vm.assume(holderUnstETHLockedShares.toUint256() > 500 * uint128(type(uint64).max)); + vm.assume(initialTotalUnfinalizedShares.toUint256() > 500 * uint128(type(uint64).max)); - SharesValue holderUnstETHLockedShares = SharesValues.from(holderUnstETHLockedSharesAmount); - ETHValue initialTotalFinalizedETH = ETHValues.from(initialTotalFinalizedETHAmount); - SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); uint256 expectedTotalSharesUnlockedAmount = 0; _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; @@ -727,34 +700,34 @@ contract AssetsAccountingUnitTests is UnitTest { initialTotalUnfinalizedShares - SharesValues.from(expectedTotalSharesUnlockedAmount), initialTotalFinalizedETH ); - assert(_accountingContext.assets[holder].stETHLockedShares == SharesValues.ZERO); - assert( - _accountingContext.assets[holder].unstETHLockedShares - == holderUnstETHLockedShares - SharesValues.from(expectedTotalSharesUnlockedAmount) + assertEq(_accountingContext.assets[holder].stETHLockedShares, SharesValues.ZERO); + assertEq( + _accountingContext.assets[holder].unstETHLockedShares, + holderUnstETHLockedShares - SharesValues.from(expectedTotalSharesUnlockedAmount) ); - assert(_accountingContext.assets[holder].lastAssetsLockTimestamp == Timestamps.ZERO); - assert(_accountingContext.assets[holder].unstETHIds.length == 0); + assertEq(_accountingContext.assets[holder].lastAssetsLockTimestamp, Timestamps.ZERO); + assertEq(_accountingContext.assets[holder].unstETHIds.length, 0); for (uint256 i = 0; i < unstETHIds.length; ++i) { - assert(_accountingContext.unstETHRecords[unstETHIds[i]].shares == SharesValues.ZERO); + assertEq(_accountingContext.unstETHRecords[unstETHIds[i]].shares, SharesValues.ZERO); } } function testFuzz_accountUnstETHUnlock_WhenFinalizedUnstETHUnlocked( address holder, uint64[] memory amountsOfShares, - uint96 holderUnstETHLockedSharesAmount, - uint96 initialTotalFinalizedETHAmount, - uint96 initialTotalUnfinalizedSharesAmount + SharesValue holderUnstETHLockedShares, + ETHValue initialTotalFinalizedETH, + SharesValue initialTotalUnfinalizedShares ) external { vm.assume(amountsOfShares.length > 0); vm.assume(amountsOfShares.length <= 500); - vm.assume(holderUnstETHLockedSharesAmount > 500 * uint128(type(uint64).max)); - vm.assume(initialTotalFinalizedETHAmount > 500 * uint128(type(uint64).max)); - vm.assume(initialTotalUnfinalizedSharesAmount > 500 * uint128(type(uint64).max)); + vm.assume(holderUnstETHLockedShares.toUint256() < type(uint96).max); + vm.assume(initialTotalFinalizedETH.toUint256() < type(uint96).max); + vm.assume(initialTotalUnfinalizedShares.toUint256() < type(uint96).max); + vm.assume(holderUnstETHLockedShares.toUint256() > 500 * uint128(type(uint64).max)); + vm.assume(initialTotalFinalizedETH.toUint256() > 500 * uint128(type(uint64).max)); + vm.assume(initialTotalFinalizedETH.toUint256() > 500 * uint128(type(uint64).max)); - SharesValue holderUnstETHLockedShares = SharesValues.from(holderUnstETHLockedSharesAmount); - ETHValue initialTotalFinalizedETH = ETHValues.from(initialTotalFinalizedETHAmount); - SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); uint256 expectedTotalSharesUnlockedAmount = 0; _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; @@ -790,15 +763,15 @@ contract AssetsAccountingUnitTests is UnitTest { initialTotalUnfinalizedShares, initialTotalFinalizedETH - ETHValues.from(expectedTotalSharesUnlockedAmount) ); - assert(_accountingContext.assets[holder].stETHLockedShares == SharesValues.ZERO); - assert( - _accountingContext.assets[holder].unstETHLockedShares - == holderUnstETHLockedShares - SharesValues.from(expectedTotalSharesUnlockedAmount) + assertEq(_accountingContext.assets[holder].stETHLockedShares, SharesValues.ZERO); + assertEq( + _accountingContext.assets[holder].unstETHLockedShares, + holderUnstETHLockedShares - SharesValues.from(expectedTotalSharesUnlockedAmount) ); - assert(_accountingContext.assets[holder].lastAssetsLockTimestamp == Timestamps.ZERO); - assert(_accountingContext.assets[holder].unstETHIds.length == 0); + assertEq(_accountingContext.assets[holder].lastAssetsLockTimestamp, Timestamps.ZERO); + assertEq(_accountingContext.assets[holder].unstETHIds.length, 0); for (uint256 i = 0; i < unstETHIds.length; ++i) { - assert(_accountingContext.unstETHRecords[unstETHIds[i]].shares == SharesValues.ZERO); + assertEq(_accountingContext.unstETHRecords[unstETHIds[i]].shares, SharesValues.ZERO); } } @@ -864,16 +837,15 @@ contract AssetsAccountingUnitTests is UnitTest { // TODO: is it expected behavior? function testFuzz_accountUnstETHUnlock_WhenNoUnstETHIdsProvided( address holder, - uint96 holderUnstETHLockedSharesAmount, - uint96 initialTotalFinalizedETHAmount, - uint96 initialTotalUnfinalizedSharesAmount + SharesValue holderUnstETHLockedShares, + ETHValue initialTotalFinalizedETH, + SharesValue initialTotalUnfinalizedShares ) external { - vm.assume(holderUnstETHLockedSharesAmount > 500 * uint128(type(uint64).max)); - vm.assume(initialTotalUnfinalizedSharesAmount > 500 * uint128(type(uint64).max)); - - SharesValue holderUnstETHLockedShares = SharesValues.from(holderUnstETHLockedSharesAmount); - ETHValue initialTotalFinalizedETH = ETHValues.from(initialTotalFinalizedETHAmount); - SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + vm.assume(holderUnstETHLockedShares.toUint256() > 500 * uint128(type(uint64).max)); + vm.assume(initialTotalFinalizedETH.toUint256() > 500 * uint128(type(uint64).max)); + vm.assume(holderUnstETHLockedShares.toUint256() < type(uint96).max); + vm.assume(initialTotalFinalizedETH.toUint256() < type(uint96).max); + vm.assume(initialTotalUnfinalizedShares.toUint256() < type(uint96).max); _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; _accountingContext.unstETHTotals.finalizedETH = initialTotalFinalizedETH; @@ -889,10 +861,10 @@ contract AssetsAccountingUnitTests is UnitTest { checkAccountingContextTotalCounters( SharesValues.ZERO, ETHValues.ZERO, initialTotalUnfinalizedShares, initialTotalFinalizedETH ); - assert(_accountingContext.assets[holder].stETHLockedShares == SharesValues.ZERO); - assert(_accountingContext.assets[holder].unstETHLockedShares == holderUnstETHLockedShares); - assert(_accountingContext.assets[holder].lastAssetsLockTimestamp == Timestamps.ZERO); - assert(_accountingContext.assets[holder].unstETHIds.length == 0); + assertEq(_accountingContext.assets[holder].stETHLockedShares, SharesValues.ZERO); + assertEq(_accountingContext.assets[holder].unstETHLockedShares, holderUnstETHLockedShares); + assertEq(_accountingContext.assets[holder].lastAssetsLockTimestamp, Timestamps.ZERO); + assertEq(_accountingContext.assets[holder].unstETHIds.length, 0); } function testFuzz_accountUnstETHUnlock_RevertOn_AccountingError_HolderUnstETHLockedSharesUnderflow(address holder) @@ -959,15 +931,15 @@ contract AssetsAccountingUnitTests is UnitTest { // TODO: make a research on gas consumption when a lot of unstNFTs provided. function testFuzz_accountUnstETHFinalized_happyPath( uint64[] memory claimableAmounts, - uint96 initialTotalFinalizedETHAmount, - uint96 initialTotalUnfinalizedSharesAmount + ETHValue initialTotalFinalizedETH, + SharesValue initialTotalUnfinalizedShares ) external { vm.assume(claimableAmounts.length > 0); vm.assume(claimableAmounts.length <= 500); - vm.assume(initialTotalUnfinalizedSharesAmount > 500 * uint128(type(uint64).max)); + vm.assume(initialTotalUnfinalizedShares.toUint256() > 500 * uint128(type(uint64).max)); + vm.assume(initialTotalFinalizedETH.toUint256() < type(uint96).max); + vm.assume(initialTotalUnfinalizedShares.toUint256() < type(uint96).max); - ETHValue initialTotalFinalizedETH = ETHValues.from(initialTotalFinalizedETHAmount); - SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); uint256 expectedTotalSharesFinalized = 0; uint256 expectedTotalAmountFinalized = 0; @@ -1017,13 +989,12 @@ contract AssetsAccountingUnitTests is UnitTest { } function testFuzz_accountUnstETHFinalized_When_NoClaimableAmountsProvided( - uint96 initialTotalFinalizedETHAmount, - uint96 initialTotalUnfinalizedSharesAmount + ETHValue initialTotalFinalizedETH, + SharesValue initialTotalUnfinalizedShares ) external { - vm.assume(initialTotalUnfinalizedSharesAmount > 500 * uint128(type(uint64).max)); - - ETHValue initialTotalFinalizedETH = ETHValues.from(initialTotalFinalizedETHAmount); - SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + vm.assume(initialTotalFinalizedETH.toUint256() > 500 * uint128(type(uint64).max)); + vm.assume(initialTotalFinalizedETH.toUint256() < type(uint96).max); + vm.assume(initialTotalUnfinalizedShares.toUint256() < type(uint96).max); _accountingContext.unstETHTotals.finalizedETH = initialTotalFinalizedETH; _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; @@ -1043,13 +1014,12 @@ contract AssetsAccountingUnitTests is UnitTest { function testFuzz_accountUnstETHFinalized_When_UnstETHRecordNotFound( uint64 claimableAmount, - uint96 initialTotalFinalizedETHAmount, - uint96 initialTotalUnfinalizedSharesAmount + ETHValue initialTotalFinalizedETH, + SharesValue initialTotalUnfinalizedShares ) external { - vm.assume(initialTotalUnfinalizedSharesAmount > 500 * uint128(type(uint64).max)); - - ETHValue initialTotalFinalizedETH = ETHValues.from(initialTotalFinalizedETHAmount); - SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + vm.assume(initialTotalFinalizedETH.toUint256() > 500 * uint128(type(uint64).max)); + vm.assume(initialTotalFinalizedETH.toUint256() < type(uint96).max); + vm.assume(initialTotalUnfinalizedShares.toUint256() < type(uint96).max); _accountingContext.unstETHTotals.finalizedETH = initialTotalFinalizedETH; _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; @@ -1071,13 +1041,12 @@ contract AssetsAccountingUnitTests is UnitTest { } function testFuzz_accountUnstETHFinalized_When_ClaimableAmountIsZero( - uint96 initialTotalFinalizedETHAmount, - uint96 initialTotalUnfinalizedSharesAmount + ETHValue initialTotalFinalizedETH, + SharesValue initialTotalUnfinalizedShares ) external { - vm.assume(initialTotalUnfinalizedSharesAmount > 500 * uint128(type(uint64).max)); - - ETHValue initialTotalFinalizedETH = ETHValues.from(initialTotalFinalizedETHAmount); - SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + vm.assume(initialTotalFinalizedETH.toUint256() > 500 * uint128(type(uint64).max)); + vm.assume(initialTotalFinalizedETH.toUint256() < type(uint96).max); + vm.assume(initialTotalUnfinalizedShares.toUint256() < type(uint96).max); _accountingContext.unstETHTotals.finalizedETH = initialTotalFinalizedETH; _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; @@ -1101,13 +1070,12 @@ contract AssetsAccountingUnitTests is UnitTest { } function testFuzz_accountUnstETHFinalized_RevertOn_ClaimableAmountOverflow( - uint96 initialTotalFinalizedETHAmount, - uint96 initialTotalUnfinalizedSharesAmount + ETHValue initialTotalFinalizedETH, + SharesValue initialTotalUnfinalizedShares ) external { - vm.assume(initialTotalUnfinalizedSharesAmount > 500 * uint128(type(uint64).max)); - - ETHValue initialTotalFinalizedETH = ETHValues.from(initialTotalFinalizedETHAmount); - SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + vm.assume(initialTotalFinalizedETH.toUint256() > 500 * uint128(type(uint64).max)); + vm.assume(initialTotalFinalizedETH.toUint256() < type(uint96).max); + vm.assume(initialTotalUnfinalizedShares.toUint256() < type(uint96).max); _accountingContext.unstETHTotals.finalizedETH = initialTotalFinalizedETH; _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; @@ -1126,13 +1094,11 @@ contract AssetsAccountingUnitTests is UnitTest { } function testFuzz_accountUnstETHFinalized_RevertOn_TotalFinalizedETHOverflow( - uint128 initialTotalFinalizedETHAmount, - uint96 initialTotalUnfinalizedSharesAmount + ETHValue initialTotalFinalizedETH, + SharesValue initialTotalUnfinalizedShares ) external { - vm.assume(initialTotalFinalizedETHAmount > type(uint96).max); - - ETHValue initialTotalFinalizedETH = ETHValues.from(initialTotalFinalizedETHAmount); - SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + vm.assume(initialTotalFinalizedETH.toUint256() > type(uint96).max); + vm.assume(initialTotalUnfinalizedShares.toUint256() < type(uint96).max); _accountingContext.unstETHTotals.finalizedETH = initialTotalFinalizedETH; _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; @@ -1151,13 +1117,12 @@ contract AssetsAccountingUnitTests is UnitTest { } function testFuzz_accountUnstETHFinalized_RevertOn_TotalUnfinalizedSharesUnderflow( - uint96 initialTotalFinalizedETHAmount, - uint96 initialTotalUnfinalizedSharesAmount + ETHValue initialTotalFinalizedETH, + SharesValue initialTotalUnfinalizedShares ) external { - vm.assume(initialTotalUnfinalizedSharesAmount < type(uint64).max); - - ETHValue initialTotalFinalizedETH = ETHValues.from(initialTotalFinalizedETHAmount); - SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + vm.assume(initialTotalUnfinalizedShares.toUint256() < type(uint64).max); + vm.assume(initialTotalFinalizedETH.toUint256() < type(uint96).max); + vm.assume(initialTotalUnfinalizedShares.toUint256() < type(uint96).max); _accountingContext.unstETHTotals.finalizedETH = initialTotalFinalizedETH; _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; @@ -1203,10 +1168,10 @@ contract AssetsAccountingUnitTests is UnitTest { checkAccountingContextTotalCounters(SharesValues.ZERO, ETHValues.ZERO, SharesValues.ZERO, ETHValues.ZERO); for (uint256 i = 0; i < unstETHIds.length; ++i) { - assert( - _accountingContext.unstETHRecords[unstETHIds[i]].claimableAmount == ETHValues.from(claimableAmounts[i]) + assertEq( + _accountingContext.unstETHRecords[unstETHIds[i]].claimableAmount, ETHValues.from(claimableAmounts[i]) ); - assert(_accountingContext.unstETHRecords[unstETHIds[i]].status == UnstETHRecordStatus.Claimed); + assertEq(_accountingContext.unstETHRecords[unstETHIds[i]].status, UnstETHRecordStatus.Claimed); } } @@ -1323,10 +1288,10 @@ contract AssetsAccountingUnitTests is UnitTest { checkAccountingContextTotalCounters(SharesValues.ZERO, ETHValues.ZERO, SharesValues.ZERO, ETHValues.ZERO); for (uint256 i = 0; i < unstETHIds.length; ++i) { - assert( - _accountingContext.unstETHRecords[unstETHIds[i]].claimableAmount == ETHValues.from(claimableAmounts[i]) + assertEq( + _accountingContext.unstETHRecords[unstETHIds[i]].claimableAmount, ETHValues.from(claimableAmounts[i]) ); - assert(_accountingContext.unstETHRecords[unstETHIds[i]].status == UnstETHRecordStatus.Claimed); + assertEq(_accountingContext.unstETHRecords[unstETHIds[i]].status, UnstETHRecordStatus.Claimed); } } @@ -1369,15 +1334,15 @@ contract AssetsAccountingUnitTests is UnitTest { ETHValue amountWithdrawn = AssetsAccounting.accountUnstETHWithdraw(_accountingContext, holder, unstETHIds); - assert(amountWithdrawn == ETHValues.from(expectedAmountWithdrawn)); + assertEq(amountWithdrawn, ETHValues.from(expectedAmountWithdrawn)); checkAccountingContextTotalCounters(SharesValues.ZERO, ETHValues.ZERO, SharesValues.ZERO, ETHValues.ZERO); for (uint256 i = 0; i < unstETHIds.length; ++i) { - assert( - _accountingContext.unstETHRecords[unstETHIds[i]].claimableAmount == ETHValues.from(claimableAmounts[i]) + assertEq( + _accountingContext.unstETHRecords[unstETHIds[i]].claimableAmount, ETHValues.from(claimableAmounts[i]) ); - assert(_accountingContext.unstETHRecords[unstETHIds[i]].status == UnstETHRecordStatus.Withdrawn); - assert(_accountingContext.unstETHRecords[unstETHIds[i]].lockedBy == holder); + assertEq(_accountingContext.unstETHRecords[unstETHIds[i]].status, UnstETHRecordStatus.Withdrawn); + assertEq(_accountingContext.unstETHRecords[unstETHIds[i]].lockedBy, holder); } } @@ -1389,7 +1354,7 @@ contract AssetsAccountingUnitTests is UnitTest { ETHValue amountWithdrawn = AssetsAccounting.accountUnstETHWithdraw(_accountingContext, holder, unstETHIds); - assert(amountWithdrawn == ETHValues.ZERO); + assertEq(amountWithdrawn, ETHValues.ZERO); } function testFuzz_accountUnstETHWithdraw_RevertWhen_UnstETHRecordNotFound( @@ -1455,14 +1420,13 @@ contract AssetsAccountingUnitTests is UnitTest { // --- function testFuzz_getLockedAssetsTotals_happyPath( - uint96 totalFinalizedETHAmount, - uint96 totalLockedSharesAmount, - uint96 totalUnfinalizedSharesAmount + ETHValue totalFinalizedETH, + SharesValue totalLockedShares, + SharesValue totalUnfinalizedShares ) external { - ETHValue totalFinalizedETH = ETHValues.from(totalFinalizedETHAmount); - SharesValue totalUnfinalizedShares = SharesValues.from(totalUnfinalizedSharesAmount); - SharesValue totalLockedShares = SharesValues.from(totalLockedSharesAmount); - + vm.assume(totalFinalizedETH.toUint256() < type(uint96).max); + vm.assume(totalLockedShares.toUint256() < type(uint96).max); + vm.assume(totalUnfinalizedShares.toUint256() < type(uint96).max); _accountingContext.unstETHTotals.finalizedETH = totalFinalizedETH; _accountingContext.unstETHTotals.unfinalizedShares = totalUnfinalizedShares; _accountingContext.stETHTotals.lockedShares = totalLockedShares; @@ -1470,8 +1434,8 @@ contract AssetsAccountingUnitTests is UnitTest { (SharesValue unfinalizedShares, ETHValue finalizedETH) = AssetsAccounting.getLockedAssetsTotals(_accountingContext); - assert(unfinalizedShares == totalLockedShares + totalUnfinalizedShares); - assert(finalizedETH == totalFinalizedETH); + assertEq(unfinalizedShares, totalLockedShares + totalUnfinalizedShares); + assertEq(finalizedETH, totalFinalizedETH); } function test_getLockedAssetsTotals_RevertOn_UnfinalizedSharesOverflow() external { @@ -1524,10 +1488,22 @@ contract AssetsAccountingUnitTests is UnitTest { ETHValue claimedETH, SharesValue unfinalizedShares, ETHValue finalizedETH - ) internal view { - assert(_accountingContext.stETHTotals.lockedShares == lockedShares); - assert(_accountingContext.stETHTotals.claimedETH == claimedETH); - assert(_accountingContext.unstETHTotals.unfinalizedShares == unfinalizedShares); - assert(_accountingContext.unstETHTotals.finalizedETH == finalizedETH); + ) internal { + assertEq(_accountingContext.stETHTotals.lockedShares, lockedShares); + assertEq(_accountingContext.stETHTotals.claimedETH, claimedETH); + assertEq(_accountingContext.unstETHTotals.unfinalizedShares, unfinalizedShares); + assertEq(_accountingContext.unstETHTotals.finalizedETH, finalizedETH); + } + + function assertEq(SharesValue a, SharesValue b) internal { + assertEq(a.toUint256(), b.toUint256()); + } + + function assertEq(ETHValue a, ETHValue b) internal { + assertEq(a.toUint256(), b.toUint256()); + } + + function assertEq(UnstETHRecordStatus a, UnstETHRecordStatus b) internal { + assertEq(uint256(a), uint256(b)); } } From 3b9eb9dcda0b496d5a736a636a13b2d690634f1e Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:01:43 +0400 Subject: [PATCH 37/42] Fix EscrowState tests --- test/unit/libraries/EscrowState.t.sol | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/test/unit/libraries/EscrowState.t.sol b/test/unit/libraries/EscrowState.t.sol index 6602520c..918fbebb 100644 --- a/test/unit/libraries/EscrowState.t.sol +++ b/test/unit/libraries/EscrowState.t.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {Vm} from "forge-std/Vm.sol"; - import {Duration, Durations, MAX_VALUE as DURATION_MAX_VALUE} from "contracts/types/Duration.sol"; import {Timestamp, Timestamps, MAX_TIMESTAMP_VALUE, TimestampOverflow} from "contracts/types/Timestamp.sol"; import {EscrowState, State} from "contracts/libraries/EscrowState.sol"; @@ -88,7 +86,7 @@ contract EscrowStateUnitTests is UnitTest { function test_startRageQuitExtensionDelay_happyPath() external { vm.expectEmit(); - emit EscrowState.RageQuitTimelockStarted(); + emit EscrowState.RageQuitTimelockStarted(Timestamps.now()); EscrowState.startRageQuitExtensionDelay(_context); @@ -122,22 +120,13 @@ contract EscrowStateUnitTests is UnitTest { }); } - function test_setMinAssetsLockDuration_WhenDurationNotChanged(Duration minAssetsLockDuration) external { + function test_setMinAssetsLockDuration_RevertWhen_DurationNotChanged(Duration minAssetsLockDuration) external { _context.minAssetsLockDuration = minAssetsLockDuration; - Vm.Log[] memory entries = vm.getRecordedLogs(); - + vm.expectRevert( + abi.encodeWithSelector(EscrowState.InvalidMinAssetsLockDuration.selector, minAssetsLockDuration) + ); EscrowState.setMinAssetsLockDuration(_context, minAssetsLockDuration); - - checkContext({ - state: State.NotInitialized, - minAssetsLockDuration: minAssetsLockDuration, - rageQuitExtensionDelay: D0, - rageQuitWithdrawalsTimelock: D0, - rageQuitExtensionDelayStartedAt: T0 - }); - - assertEq(entries.length, 0); } // --- From 8831953d2284aa35dbfb36960202f8c7331671a8 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Tue, 27 Aug 2024 16:24:52 +0300 Subject: [PATCH 38/42] fix: review fixes --- test/unit/libraries/Tiebreaker.t.sol | 47 ++++++++++++++-------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/test/unit/libraries/Tiebreaker.t.sol b/test/unit/libraries/Tiebreaker.t.sol index e213d3df..c3bccddb 100644 --- a/test/unit/libraries/Tiebreaker.t.sol +++ b/test/unit/libraries/Tiebreaker.t.sol @@ -23,7 +23,7 @@ contract TiebreakerTest is UnitTest { mockSealable2 = new SealableMock(); } - function test_addSealableWithdrawalBlocker() external { + function test_addSealableWithdrawalBlocker_HappyPath() external { vm.expectEmit(); emit Tiebreaker.SealableWithdrawalBlockerAdded(address(mockSealable1)); Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable1), 1); @@ -31,14 +31,14 @@ contract TiebreakerTest is UnitTest { assertTrue(context.sealableWithdrawalBlockers.contains(address(mockSealable1))); } - function test_AddSealableWithdrawalBlocker_RevertOn_LimitReached() external { + function test_addSealableWithdrawalBlocker_RevertOn_LimitReached() external { Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable1), 1); vm.expectRevert(Tiebreaker.SealableWithdrawalBlockersLimitReached.selector); Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable2), 1); } - function test_AddSealableWithdrawalBlocker_RevertOn_InvalidSealable() external { + function test_addSealableWithdrawalBlocker_RevertOn_InvalidSealable() external { mockSealable1.setShouldRevertIsPaused(true); vm.expectRevert(abi.encodeWithSelector(Tiebreaker.InvalidSealable.selector, address(mockSealable1))); @@ -50,7 +50,7 @@ contract TiebreakerTest is UnitTest { this.external__addSealableWithdrawalBlocker(address(0x123)); } - function test_RemoveSealableWithdrawalBlocker() external { + function test_removeSealableWithdrawalBlocker_HappyPath() external { Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable1), 1); assertTrue(context.sealableWithdrawalBlockers.contains(address(mockSealable1))); @@ -61,7 +61,7 @@ contract TiebreakerTest is UnitTest { assertFalse(context.sealableWithdrawalBlockers.contains(address(mockSealable1))); } - function test_SetTiebreakerCommittee() external { + function test_setTiebreakerCommittee_HappyPath() external { address newCommittee = address(0x123); vm.expectEmit(); @@ -71,33 +71,34 @@ contract TiebreakerTest is UnitTest { assertEq(context.tiebreakerCommittee, newCommittee); } - function test_SetTiebreakerCommittee_WithExistingCommitteeAddress() external { + function test_setTiebreakerCommittee_WithExistingCommitteeAddress() external { address newCommittee = address(0x123); Tiebreaker.setTiebreakerCommittee(context, newCommittee); + vm.expectRevert(abi.encodeWithSelector(Tiebreaker.InvalidTiebreakerCommittee.selector, newCommittee)); Tiebreaker.setTiebreakerCommittee(context, newCommittee); } - function test_SetTiebreakerCommittee_RevertOn_ZeroAddress() external { + function test_setTiebreakerCommittee_RevertOn_ZeroAddress() external { vm.expectRevert(abi.encodeWithSelector(Tiebreaker.InvalidTiebreakerCommittee.selector, address(0))); Tiebreaker.setTiebreakerCommittee(context, address(0)); } - function testFuzz_SetTiebreakerActivationTimeout(uint32 minTimeout, uint32 maxTimeout, uint32 timeout) external { + function testFuzz_SetTiebreakerActivationTimeout( + Duration minTimeout, + Duration maxTimeout, + Duration timeout + ) external { vm.assume(minTimeout < timeout && timeout < maxTimeout); - Duration min = Duration.wrap(minTimeout); - Duration max = Duration.wrap(maxTimeout); - Duration newTimeout = Duration.wrap(timeout); - vm.expectEmit(); - emit Tiebreaker.TiebreakerActivationTimeoutSet(newTimeout); + emit Tiebreaker.TiebreakerActivationTimeoutSet(timeout); - Tiebreaker.setTiebreakerActivationTimeout(context, min, newTimeout, max); - assertEq(context.tiebreakerActivationTimeout, newTimeout); + Tiebreaker.setTiebreakerActivationTimeout(context, minTimeout, timeout, timeout); + assertEq(context.tiebreakerActivationTimeout, timeout); } - function test_SetTiebreakerActivationTimeout_RevertOn_InvalidTimeout() external { + function test_setTiebreakerActivationTimeout_RevertOn_InvalidTimeout() external { Duration minTimeout = Duration.wrap(1 days); Duration maxTimeout = Duration.wrap(10 days); Duration newTimeout = Duration.wrap(15 days); @@ -111,7 +112,7 @@ contract TiebreakerTest is UnitTest { Tiebreaker.setTiebreakerActivationTimeout(context, minTimeout, newTimeout, maxTimeout); } - function test_IsSomeSealableWithdrawalBlockerPaused() external { + function test_isSomeSealableWithdrawalBlockerPaused_HappyPath() external { mockSealable1.pauseFor(1 days); Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable1), 2); @@ -129,7 +130,7 @@ contract TiebreakerTest is UnitTest { assertTrue(result); } - function test_CheckTie() external { + function test_checkTie_HappyPath() external { Timestamp cooldownExitedAt = Timestamps.from(block.timestamp); Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable1), 1); @@ -144,7 +145,7 @@ contract TiebreakerTest is UnitTest { Tiebreaker.checkTie(context, DualGovernanceState.VetoSignalling, cooldownExitedAt); } - function test_CheckTie_RevertOn_NormalOrVetoCooldownState() external { + function test_checkTie_RevertOn_NormalOrVetoCooldownState() external { Timestamp cooldownExitedAt = Timestamps.from(block.timestamp); vm.expectRevert(Tiebreaker.TiebreakNotAllowed.selector); @@ -154,7 +155,7 @@ contract TiebreakerTest is UnitTest { Tiebreaker.checkTie(context, DualGovernanceState.VetoCooldown, cooldownExitedAt); } - function test_CheckCallerIsTiebreakerCommittee() external { + function test_checkCallerIsTiebreakerCommittee_HappyPath() external { context.tiebreakerCommittee = address(this); vm.expectRevert(abi.encodeWithSelector(Tiebreaker.CallerIsNotTiebreakerCommittee.selector, address(0x456))); @@ -164,11 +165,9 @@ contract TiebreakerTest is UnitTest { this.external__checkCallerIsTiebreakerCommittee(); } - function test_GetTimebreakerInfo() external { + function test_getTimebreakerInfo_HappyPath() external { Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable1), 1); - Duration minTimeout = Duration.wrap(1 days); - Duration maxTimeout = Duration.wrap(10 days); Duration timeout = Duration.wrap(5 days); context.tiebreakerActivationTimeout = timeout; @@ -183,7 +182,7 @@ contract TiebreakerTest is UnitTest { assertEq(blockers.length, 1); } - function external__checkCallerIsTiebreakerCommittee() external { + function external__checkCallerIsTiebreakerCommittee() external view { Tiebreaker.checkCallerIsTiebreakerCommittee(context); } From 81e23ec85f69786a83f83f1287ee51835fc42f9d Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 28 Aug 2024 03:05:29 +0400 Subject: [PATCH 39/42] Add unit tests for cancelAllPendingProposals() --- contracts/DualGovernance.sol | 13 +- test/mocks/StETHMock.sol | 40 +++++ test/mocks/TimelockMock.sol | 4 + test/mocks/WithdrawalQueueMock.sol | 135 ++++++++++++++++ test/unit/DualGovernance.t.sol | 242 +++++++++++++++++++++++++++++ 5 files changed, 427 insertions(+), 7 deletions(-) create mode 100644 test/mocks/StETHMock.sol create mode 100644 test/mocks/WithdrawalQueueMock.sol create mode 100644 test/unit/DualGovernance.t.sol diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 26287f2c..bf4cad64 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -149,13 +149,12 @@ contract DualGovernance is IDualGovernance { State currentState = _stateMachine.getCurrentState(); if (currentState != State.VetoSignalling && currentState != State.VetoSignallingDeactivation) { - /// @dev Early return to prevent "hanging" cancelPendingProposals() requests that could become unexpectedly - /// executable in the future. - /// - /// Some proposer contracts, such as Aragon Voting, may not support canceling already-consensed decisions. - /// This could lead to situations where a proposer’s cancelAllPendingProposals() call becomes unexecutable - /// if the Dual Governance state changes. However, it could become executable again if the system state - /// reverts to VetoSignalling or VetoSignallingDeactivation. + /// @dev Some proposer contracts, like Aragon Voting, may not support canceling decisions that have already + /// reached consensus. This could lead to a situation where a proposer’s cancelAllPendingProposals() call + /// becomes unexecutable if the Dual Governance state changes. However, it might become executable again if + /// the system state shifts back to VetoSignalling or VetoSignallingDeactivation. + /// To avoid such a scenario, an early return is used instead of a revert when proposals cannot be canceled + /// due to an unsuitable Dual Governance state. emit CancelAllPendingProposalsSkipped(); return; } diff --git a/test/mocks/StETHMock.sol b/test/mocks/StETHMock.sol new file mode 100644 index 00000000..dcc5813a --- /dev/null +++ b/test/mocks/StETHMock.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol"; +import {IStETH} from "contracts/interfaces/IStETH.sol"; + +/* solhint-disable no-unused-vars,custom-errors */ +contract StETHMock is ERC20Mock, IStETH { + uint256 public __shareRate = 1 gwei; + + constructor() { + /// @dev the total supply of the stETH always > 0 + _mint(address(this), 100 wei); + } + + function __setShareRate(uint256 newShareRate) public { + __shareRate = newShareRate; + } + + function getSharesByPooledEth(uint256 ethAmount) external view returns (uint256) { + return ethAmount / __shareRate; + } + + function getPooledEthByShares(uint256 sharesAmount) external view returns (uint256) { + return __shareRate * sharesAmount; + } + + function transferShares(address to, uint256 sharesAmount) external { + transfer(to, sharesAmount * __shareRate); + } + + function transferSharesFrom( + address _sender, + address _recipient, + uint256 _sharesAmount + ) external returns (uint256 tokensAmount) { + tokensAmount = _sharesAmount * __shareRate; + transferFrom(_sender, _recipient, tokensAmount); + } +} diff --git a/test/mocks/TimelockMock.sol b/test/mocks/TimelockMock.sol index 314caed3..276726e0 100644 --- a/test/mocks/TimelockMock.sol +++ b/test/mocks/TimelockMock.sol @@ -107,6 +107,10 @@ contract TimelockMock is ITimelock { revert("Not Implemented"); } + function getProposalsCount() external view returns (uint256 count) { + return submittedProposals.length; + } + function getAdminExecutor() external view returns (address) { return _ADMIN_EXECUTOR; } diff --git a/test/mocks/WithdrawalQueueMock.sol b/test/mocks/WithdrawalQueueMock.sol new file mode 100644 index 00000000..57290346 --- /dev/null +++ b/test/mocks/WithdrawalQueueMock.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +// import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; /*, ERC721("test", "test")*/ +import {IWithdrawalQueue, WithdrawalRequestStatus} from "contracts/interfaces/IWithdrawalQueue.sol"; + +/* solhint-disable no-unused-vars,custom-errors */ +contract WithdrawalQueueMock is IWithdrawalQueue { + uint256 private _lastRequestId; + uint256 private _lastFinalizedRequestId; + uint256 private _minStETHWithdrawalAmount; + uint256 private _maxStETHWithdrawalAmount; + uint256[] private _requestWithdrawalsResult; + + constructor() {} + + function MIN_STETH_WITHDRAWAL_AMOUNT() external view returns (uint256) { + return _minStETHWithdrawalAmount; + } + + function MAX_STETH_WITHDRAWAL_AMOUNT() external view returns (uint256) { + return _maxStETHWithdrawalAmount; + } + + function claimWithdrawals(uint256[] calldata requestIds, uint256[] calldata hints) external { + revert("Not Implemented"); + } + + function getLastRequestId() external view returns (uint256) { + return _lastRequestId; + } + + function getLastFinalizedRequestId() external view returns (uint256) { + return _lastFinalizedRequestId; + } + + function getWithdrawalStatus(uint256[] calldata _requestIds) + external + view + returns (WithdrawalRequestStatus[] memory statuses) + { + revert("Not Implemented"); + } + + /// @notice Returns amount of ether available for claim for each provided request id + /// @param _requestIds array of request ids + /// @param _hints checkpoint hints. can be found with `findCheckpointHints(_requestIds, 1, getLastCheckpointIndex())` + /// @return claimableEthValues amount of claimable ether for each request, amount is equal to 0 if request + /// is not finalized or already claimed + function getClaimableEther( + uint256[] calldata _requestIds, + uint256[] calldata _hints + ) external view returns (uint256[] memory claimableEthValues) { + revert("Not Implemented"); + } + + function findCheckpointHints( + uint256[] calldata _requestIds, + uint256 _firstIndex, + uint256 _lastIndex + ) external view returns (uint256[] memory hintIds) { + revert("Not Implemented"); + } + + function getLastCheckpointIndex() external view returns (uint256) { + revert("Not Implemented"); + } + + function requestWithdrawals( + uint256[] calldata _amounts, + address _owner + ) external returns (uint256[] memory requestIds) { + return _requestWithdrawalsResult; + } + + function balanceOf(address owner) external view returns (uint256 balance) { + revert("Not Implemented"); + } + + function ownerOf(uint256 tokenId) external view returns (address owner) { + revert("Not Implemented"); + } + + function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external { + revert("Not Implemented"); + } + + function safeTransferFrom(address from, address to, uint256 tokenId) external { + revert("Not Implemented"); + } + + function transferFrom(address from, address to, uint256 tokenId) external { + revert("Not Implemented"); + } + + function approve(address to, uint256 tokenId) external { + revert("Not Implemented"); + } + + function setApprovalForAll(address operator, bool approved) external { + revert("Not Implemented"); + } + + function getApproved(uint256 tokenId) external view returns (address operator) { + revert("Not Implemented"); + } + + function isApprovedForAll(address owner, address operator) external view returns (bool) { + revert("Not Implemented"); + } + + function supportsInterface(bytes4 interfaceId) external view returns (bool) { + revert("Not Implemented"); + } + + function setLastRequestId(uint256 id) public { + _lastRequestId = id; + } + + function setLastFinalizedRequestId(uint256 id) public { + _lastFinalizedRequestId = id; + } + + function setMinStETHWithdrawalAmount(uint256 amount) public { + _minStETHWithdrawalAmount = amount; + } + + function setMaxStETHWithdrawalAmount(uint256 amount) public { + _maxStETHWithdrawalAmount = amount; + } + + function setRequestWithdrawalsResult(uint256[] memory requestIds) public { + _requestWithdrawalsResult = requestIds; + } +} diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol new file mode 100644 index 00000000..032f043f --- /dev/null +++ b/test/unit/DualGovernance.t.sol @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Durations} from "contracts/types/Duration.sol"; +import {PercentsD16} from "contracts/types/PercentD16.sol"; + +import {ExternalCall} from "contracts/libraries/ExternalCalls.sol"; + +import {Escrow} from "contracts/Escrow.sol"; +import {Executor} from "contracts/Executor.sol"; +import {DualGovernance, State} from "contracts/DualGovernance.sol"; +import {IResealManager} from "contracts/interfaces/IResealManager.sol"; +import { + DualGovernanceConfig, + IDualGovernanceConfigProvider, + ImmutableDualGovernanceConfigProvider +} from "contracts/DualGovernanceConfigProvider.sol"; + +import {IWstETH} from "contracts/interfaces/IWstETH.sol"; +import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; + +import {UnitTest} from "test/utils/unit-test.sol"; +import {StETHMock} from "test/mocks/StETHMock.sol"; +import {TimelockMock} from "test/mocks/TimelockMock.sol"; +import {WithdrawalQueueMock} from "test/mocks/WithdrawalQueueMock.sol"; + +contract DualGovernanceUnitTests is UnitTest { + Executor private _executor = new Executor(address(this)); + + StETHMock private immutable _STETH_MOCK = new StETHMock(); + IWithdrawalQueue private immutable _WITHDRAWAL_QUEUE_MOCK = new WithdrawalQueueMock(); + + // TODO: Replace with mocks + IWstETH private immutable _WSTETH_STUB = IWstETH(makeAddr("WSTETH_STUB")); + IResealManager private immutable _RESEAL_MANAGER_STUB = IResealManager(makeAddr("RESEAL_MANAGER_STUB")); + + TimelockMock internal _timelock = new TimelockMock(address(_executor)); + ImmutableDualGovernanceConfigProvider internal _configProvider = new ImmutableDualGovernanceConfigProvider( + DualGovernanceConfig.Context({ + firstSealRageQuitSupport: PercentsD16.fromBasisPoints(3_00), // 3% + secondSealRageQuitSupport: PercentsD16.fromBasisPoints(15_00), // 15% + // + minAssetsLockDuration: Durations.from(5 hours), + dynamicTimelockMinDuration: Durations.from(3 days), + dynamicTimelockMaxDuration: Durations.from(30 days), + // + vetoSignallingMinActiveDuration: Durations.from(5 hours), + vetoSignallingDeactivationMaxDuration: Durations.from(5 days), + vetoCooldownDuration: Durations.from(4 days), + // + rageQuitExtensionDelay: Durations.from(7 days), + rageQuitEthWithdrawalsMinTimelock: Durations.from(60 days), + rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber: 2, + rageQuitEthWithdrawalsTimelockGrowthCoeffs: [uint256(0), 0, 0] + }) + ); + + DualGovernance internal _dualGovernance = new DualGovernance({ + dependencies: DualGovernance.ExternalDependencies({ + stETH: _STETH_MOCK, + wstETH: _WSTETH_STUB, + withdrawalQueue: _WITHDRAWAL_QUEUE_MOCK, + timelock: _timelock, + resealManager: _RESEAL_MANAGER_STUB, + configProvider: _configProvider + }), + sanityCheckParams: DualGovernance.SanityCheckParams({ + minWithdrawalsBatchSize: 4, + minTiebreakerActivationTimeout: Durations.from(30 days), + maxTiebreakerActivationTimeout: Durations.from(180 days), + maxSealableWithdrawalBlockersCount: 128 + }) + }); + + function setUp() external { + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.registerProposer.selector, address(this), address(_executor)) + ); + } + + // --- + // cancelAllPendingProposals() + // --- + + function test_cancelAllPendingProposals_HappyPath_SkippedInNormalState() external { + _submitMockProposal(); + assertEq(_timelock.getProposalsCount(), 1); + assertEq(_timelock.lastCancelledProposalId(), 0); + assertEq(_dualGovernance.getCurrentState(), State.Normal); + + vm.expectEmit(); + emit DualGovernance.CancelAllPendingProposalsSkipped(); + + _dualGovernance.cancelAllPendingProposals(); + + assertEq(_timelock.getProposalsCount(), 1); + assertEq(_timelock.lastCancelledProposalId(), 0); + } + + function test_cancelAllPendingProposals_HappyPath_SkippedInVetoCooldownState() external { + _submitMockProposal(); + assertEq(_timelock.getProposalsCount(), 1); + assertEq(_timelock.lastCancelledProposalId(), 0); + assertEq(_dualGovernance.getCurrentState(), State.Normal); + + Escrow signallingEscrow = Escrow(payable(_dualGovernance.getVetoSignallingEscrow())); + + address vetoer = makeAddr("VETOER"); + _STETH_MOCK.mint(vetoer, 10 ether); + + vm.startPrank(vetoer); + _STETH_MOCK.approve(address(signallingEscrow), 10 ether); + signallingEscrow.lockStETH(5 ether); + vm.stopPrank(); + + assertEq(_dualGovernance.getCurrentState(), State.VetoSignalling); + + _wait(_configProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); + + vm.prank(vetoer); + signallingEscrow.unlockStETH(); + + assertEq(_dualGovernance.getCurrentState(), State.VetoSignallingDeactivation); + + _wait(_configProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); + + _dualGovernance.activateNextState(); + assertEq(_dualGovernance.getCurrentState(), State.VetoCooldown); + + vm.expectEmit(); + emit DualGovernance.CancelAllPendingProposalsSkipped(); + + _dualGovernance.cancelAllPendingProposals(); + + assertEq(_timelock.getProposalsCount(), 1); + assertEq(_timelock.lastCancelledProposalId(), 0); + } + + function test_cancelAllPendingProposals_HappyPath_SkippedInRageQuitState() external { + _submitMockProposal(); + assertEq(_timelock.getProposalsCount(), 1); + assertEq(_timelock.lastCancelledProposalId(), 0); + assertEq(_dualGovernance.getCurrentState(), State.Normal); + + Escrow signallingEscrow = Escrow(payable(_dualGovernance.getVetoSignallingEscrow())); + + address vetoer = makeAddr("VETOER"); + _STETH_MOCK.mint(vetoer, 10 ether); + + vm.startPrank(vetoer); + _STETH_MOCK.approve(address(signallingEscrow), 10 ether); + signallingEscrow.lockStETH(5 ether); + vm.stopPrank(); + + assertEq(_dualGovernance.getCurrentState(), State.VetoSignalling); + + _wait(_configProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); + + _dualGovernance.activateNextState(); + assertEq(_dualGovernance.getCurrentState(), State.RageQuit); + + vm.expectEmit(); + emit DualGovernance.CancelAllPendingProposalsSkipped(); + + _dualGovernance.cancelAllPendingProposals(); + + assertEq(_timelock.getProposalsCount(), 1); + assertEq(_timelock.lastCancelledProposalId(), 0); + } + + function test_cancelAllPendingProposals_HappyPath_ExecutedInVetoSignallingState() external { + _submitMockProposal(); + assertEq(_timelock.getProposalsCount(), 1); + assertEq(_timelock.lastCancelledProposalId(), 0); + assertEq(_dualGovernance.getCurrentState(), State.Normal); + + Escrow signallingEscrow = Escrow(payable(_dualGovernance.getVetoSignallingEscrow())); + + address vetoer = makeAddr("VETOER"); + _STETH_MOCK.mint(vetoer, 10 ether); + + vm.startPrank(vetoer); + _STETH_MOCK.approve(address(signallingEscrow), 10 ether); + signallingEscrow.lockStETH(5 ether); + vm.stopPrank(); + + assertEq(_dualGovernance.getCurrentState(), State.VetoSignalling); + + vm.expectEmit(); + emit DualGovernance.CancelAllPendingProposalsExecuted(); + + _dualGovernance.cancelAllPendingProposals(); + + assertEq(_timelock.getProposalsCount(), 1); + assertEq(_timelock.lastCancelledProposalId(), 1); + } + + function test_cancelAllPendingProposals_HappyPath_ExecutedInVetoSignallingDeactivationState() external { + _submitMockProposal(); + assertEq(_timelock.getProposalsCount(), 1); + assertEq(_timelock.lastCancelledProposalId(), 0); + assertEq(_dualGovernance.getCurrentState(), State.Normal); + + Escrow signallingEscrow = Escrow(payable(_dualGovernance.getVetoSignallingEscrow())); + + address vetoer = makeAddr("VETOER"); + _STETH_MOCK.mint(vetoer, 10 ether); + + vm.startPrank(vetoer); + _STETH_MOCK.approve(address(signallingEscrow), 10 ether); + signallingEscrow.lockStETH(5 ether); + vm.stopPrank(); + + assertEq(_dualGovernance.getCurrentState(), State.VetoSignalling); + + _wait(_configProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); + + vm.prank(vetoer); + signallingEscrow.unlockStETH(); + + assertEq(_dualGovernance.getCurrentState(), State.VetoSignallingDeactivation); + + vm.expectEmit(); + emit DualGovernance.CancelAllPendingProposalsExecuted(); + + _dualGovernance.cancelAllPendingProposals(); + + assertEq(_timelock.getProposalsCount(), 1); + assertEq(_timelock.lastCancelledProposalId(), 1); + } + + // --- + // Helper methods + // --- + + function _submitMockProposal() internal { + // mock timelock doesn't uses proposal data + _timelock.submit(address(0), new ExternalCall[](0)); + } +} From dab02d6898c1403b0fd3350ad0aec4b75e0dbbdd Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 28 Aug 2024 03:09:32 +0400 Subject: [PATCH 40/42] Fix typo in the unstETHLockedShares value of getVetoerState method --- contracts/Escrow.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 6d1190cc..f71d5f54 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -403,7 +403,7 @@ contract Escrow is IEscrow { state.unstETHIdsCount = assets.unstETHIds.length; state.stETHLockedShares = assets.stETHLockedShares.toUint256(); - state.unstETHLockedShares = assets.stETHLockedShares.toUint256(); + state.unstETHLockedShares = assets.unstETHLockedShares.toUint256(); state.lastAssetsLockTimestamp = assets.lastAssetsLockTimestamp.toSeconds(); } From 16631755e1bb535e0ee146a4636f82c295511c56 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 28 Aug 2024 04:47:39 +0400 Subject: [PATCH 41/42] Fix the overflow of the rage quit round --- .../libraries/DualGovernanceStateMachine.sol | 14 +++- test/mocks/EscrowMock.sol | 43 ++++++++++ .../DualGovernanceStateMachine.t.sol | 80 +++++++++++++++++++ 3 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 test/mocks/EscrowMock.sol create mode 100644 test/unit/libraries/DualGovernanceStateMachine.t.sol diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol index 59a9c810..d783d55f 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -64,6 +64,8 @@ library DualGovernanceStateMachine { event NewSignallingEscrowDeployed(IEscrow indexed escrow); event DualGovernanceStateChanged(State from, State to, Context state); + uint256 internal constant MAX_RAGE_QUIT_ROUND = type(uint8).max; + function initialize( Context storage self, DualGovernanceConfig.Context memory config, @@ -108,10 +110,16 @@ library DualGovernanceStateMachine { } } else if (newState == State.RageQuit) { IEscrow signallingEscrow = self.signallingEscrow; - uint256 rageQuitRound = Math.min(self.rageQuitRound + 1, type(uint8).max); - self.rageQuitRound = uint8(rageQuitRound); + + uint256 currentRageQuitRound = self.rageQuitRound; + + /// @dev Limits the maximum value of the rage quit round to prevent failures due to arithmetic overflow + /// if the number of consecutive rage quits reaches MAX_RAGE_QUIT_ROUND. + uint256 newRageQuitRound = Math.min(currentRageQuitRound + 1, MAX_RAGE_QUIT_ROUND); + self.rageQuitRound = uint8(newRageQuitRound); + signallingEscrow.startRageQuit( - config.rageQuitExtensionDelay, config.calcRageQuitWithdrawalsTimelock(rageQuitRound) + config.rageQuitExtensionDelay, config.calcRageQuitWithdrawalsTimelock(newRageQuitRound) ); self.rageQuitEscrow = signallingEscrow; _deployNewSignallingEscrow(self, escrowMasterCopy, config.minAssetsLockDuration); diff --git a/test/mocks/EscrowMock.sol b/test/mocks/EscrowMock.sol new file mode 100644 index 00000000..fe3e0798 --- /dev/null +++ b/test/mocks/EscrowMock.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "contracts/types/Duration.sol"; +import {PercentD16} from "contracts/types/PercentD16.sol"; + +import {IEscrow} from "contracts/interfaces/IEscrow.sol"; + +contract EscrowMock is IEscrow { + event __RageQuitStarted(Duration rageQuitExtraTimelock, Duration rageQuitWithdrawalsTimelock); + + Duration public __minAssetsLockDuration; + PercentD16 public __rageQuitSupport; + bool public __isRageQuitFinalized; + + function __setRageQuitSupport(PercentD16 newRageQuitSupport) external { + __rageQuitSupport = newRageQuitSupport; + } + + function __setIsRageQuitFinalized(bool newIsRageQuitFinalized) external { + __isRageQuitFinalized = newIsRageQuitFinalized; + } + + function initialize(Duration minAssetsLockDuration) external { + __minAssetsLockDuration = minAssetsLockDuration; + } + + function startRageQuit(Duration rageQuitExtraTimelock, Duration rageQuitWithdrawalsTimelock) external { + emit __RageQuitStarted(rageQuitExtraTimelock, rageQuitWithdrawalsTimelock); + } + + function isRageQuitFinalized() external view returns (bool) { + return __isRageQuitFinalized; + } + + function getRageQuitSupport() external view returns (PercentD16 rageQuitSupport) { + return __rageQuitSupport; + } + + function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external { + __minAssetsLockDuration = newMinAssetsLockDuration; + } +} diff --git a/test/unit/libraries/DualGovernanceStateMachine.t.sol b/test/unit/libraries/DualGovernanceStateMachine.t.sol new file mode 100644 index 00000000..8aafa5c8 --- /dev/null +++ b/test/unit/libraries/DualGovernanceStateMachine.t.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +import {Durations} from "contracts/types/Duration.sol"; +import {PercentsD16} from "contracts/types/PercentD16.sol"; + +import {DualGovernanceStateMachine, State} from "contracts/libraries/DualGovernanceStateMachine.sol"; +import {DualGovernanceConfig, ImmutableDualGovernanceConfigProvider} from "contracts/DualGovernanceConfigProvider.sol"; + +import {UnitTest} from "test/utils/unit-test.sol"; +import {EscrowMock} from "test/mocks/EscrowMock.sol"; + +contract DualGovernanceStateMachineUnitTests is UnitTest { + using DualGovernanceStateMachine for DualGovernanceStateMachine.Context; + + address private immutable _ESCROW_MASTER_COPY = address(new EscrowMock()); + ImmutableDualGovernanceConfigProvider internal immutable _CONFIG_PROVIDER = new ImmutableDualGovernanceConfigProvider( + DualGovernanceConfig.Context({ + firstSealRageQuitSupport: PercentsD16.fromBasisPoints(3_00), // 3% + secondSealRageQuitSupport: PercentsD16.fromBasisPoints(15_00), // 15% + // + minAssetsLockDuration: Durations.from(5 hours), + dynamicTimelockMinDuration: Durations.from(3 days), + dynamicTimelockMaxDuration: Durations.from(30 days), + // + vetoSignallingMinActiveDuration: Durations.from(5 hours), + vetoSignallingDeactivationMaxDuration: Durations.from(5 days), + vetoCooldownDuration: Durations.from(4 days), + // + rageQuitExtensionDelay: Durations.from(7 days), + rageQuitEthWithdrawalsMinTimelock: Durations.from(60 days), + rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber: 2, + rageQuitEthWithdrawalsTimelockGrowthCoeffs: [uint256(0), 0, 0] + }) + ); + + DualGovernanceStateMachine.Context private _stateMachine; + + function setUp() external { + _stateMachine.initialize(_CONFIG_PROVIDER.getDualGovernanceConfig(), _ESCROW_MASTER_COPY); + } + + function test_activateNextState_HappyPath_MaxRageQuitsRound() external { + assertEq(_stateMachine.state, State.Normal); + + for (uint256 i = 0; i < 2 * DualGovernanceStateMachine.MAX_RAGE_QUIT_ROUND; ++i) { + address signallingEscrow = address(_stateMachine.signallingEscrow); + EscrowMock(signallingEscrow).__setRageQuitSupport( + _CONFIG_PROVIDER.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1_00) + ); + assertTrue( + _stateMachine.signallingEscrow.getRageQuitSupport() > _CONFIG_PROVIDER.SECOND_SEAL_RAGE_QUIT_SUPPORT() + ); + assertEq(_stateMachine.rageQuitRound, Math.min(i, DualGovernanceStateMachine.MAX_RAGE_QUIT_ROUND)); + + // wait here the full duration of the veto cooldown to make sure it's over from the previous iteration + _wait(_CONFIG_PROVIDER.VETO_COOLDOWN_DURATION().plusSeconds(1)); + + _stateMachine.activateNextState(_CONFIG_PROVIDER.getDualGovernanceConfig(), _ESCROW_MASTER_COPY); + assertEq(_stateMachine.state, State.VetoSignalling); + + _wait(_CONFIG_PROVIDER.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); + _stateMachine.activateNextState(_CONFIG_PROVIDER.getDualGovernanceConfig(), _ESCROW_MASTER_COPY); + + assertEq(_stateMachine.state, State.RageQuit); + assertEq(_stateMachine.rageQuitRound, Math.min(i + 1, DualGovernanceStateMachine.MAX_RAGE_QUIT_ROUND)); + + EscrowMock(signallingEscrow).__setIsRageQuitFinalized(true); + _stateMachine.activateNextState(_CONFIG_PROVIDER.getDualGovernanceConfig(), _ESCROW_MASTER_COPY); + assertEq(_stateMachine.state, State.VetoCooldown); + } + + // after the sequential rage quits chain is broken, the rage quit resets to 0 + _wait(_CONFIG_PROVIDER.VETO_COOLDOWN_DURATION().plusSeconds(1)); + _stateMachine.activateNextState(_CONFIG_PROVIDER.getDualGovernanceConfig(), _ESCROW_MASTER_COPY); + assertEq(_stateMachine.state, State.Normal); + } +} From 51ad0efe33a2fee1f0239295c5ed35d5ec0d9b5c Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 28 Aug 2024 17:03:57 +0400 Subject: [PATCH 42/42] Add check for rageQuitRound into the unit test --- test/unit/libraries/DualGovernanceStateMachine.t.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/unit/libraries/DualGovernanceStateMachine.t.sol b/test/unit/libraries/DualGovernanceStateMachine.t.sol index 8aafa5c8..3e5d6cdf 100644 --- a/test/unit/libraries/DualGovernanceStateMachine.t.sol +++ b/test/unit/libraries/DualGovernanceStateMachine.t.sol @@ -75,6 +75,8 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { // after the sequential rage quits chain is broken, the rage quit resets to 0 _wait(_CONFIG_PROVIDER.VETO_COOLDOWN_DURATION().plusSeconds(1)); _stateMachine.activateNextState(_CONFIG_PROVIDER.getDualGovernanceConfig(), _ESCROW_MASTER_COPY); + + assertEq(_stateMachine.rageQuitRound, 0); assertEq(_stateMachine.state, State.Normal); } }