From e419700662289a185e42f9e74349c1520f0e2911 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 18 Jul 2024 17:20:39 +0400 Subject: [PATCH 1/4] Refactor DualGovernanceState -> DualGovernanceStateMachine lib --- contracts/DualGovernance.sol | 96 ++--- contracts/SingleGovernance.sol | 2 +- contracts/interfaces/IConfiguration.sol | 16 +- contracts/interfaces/ITimelock.sol | 2 +- contracts/libraries/DualGovernanceConfig.sol | 116 +++++ contracts/libraries/DualGovernanceState.sol | 395 ------------------ .../libraries/DualGovernanceStateMachine.sol | 266 ++++++++++++ .../last-moment-malicious-proposal.t.sol | 16 +- test/scenario/veto-cooldown-mechanics.t.sol | 8 +- test/unit/SingleGovernance.t.sol | 4 +- test/utils/scenario-test-blueprint.sol | 39 +- 11 files changed, 470 insertions(+), 490 deletions(-) create mode 100644 contracts/libraries/DualGovernanceConfig.sol delete mode 100644 contracts/libraries/DualGovernanceState.sol create mode 100644 contracts/libraries/DualGovernanceStateMachine.sol diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index bc35181b..717890ac 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -1,35 +1,37 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {Duration} from "./types/Duration.sol"; -import {Timestamp} from "./types/Timestamp.sol"; import {ITimelock, IGovernance} from "./interfaces/ITimelock.sol"; import {IResealManager} from "./interfaces/IResealManager.sol"; +import {Duration} from "./types/Duration.sol"; + import {ConfigurationProvider} from "./ConfigurationProvider.sol"; import {Proposers, Proposer} from "./libraries/Proposers.sol"; import {ExternalCall} from "./libraries/ExternalCalls.sol"; import {EmergencyProtection} from "./libraries/EmergencyProtection.sol"; -import {State, DualGovernanceState} from "./libraries/DualGovernanceState.sol"; +import {Status, DualGovernanceStateMachine} from "./libraries/DualGovernanceStateMachine.sol"; import {TiebreakerProtection} from "./libraries/TiebreakerProtection.sol"; contract DualGovernance is IGovernance, ConfigurationProvider { using Proposers for Proposers.State; - using DualGovernanceState for DualGovernanceState.Store; using TiebreakerProtection for TiebreakerProtection.Tiebreaker; + using DualGovernanceStateMachine for DualGovernanceStateMachine.State; - event ProposalScheduled(uint256 proposalId); - + error NotTiebreak(); error NotResealCommitttee(address account); + error ProposalSubmissionBlocked(); + error ProposalSchedulingBlocked(uint256 proposalId); + error ResealIsNotAllowedInNormalState(); ITimelock public immutable TIMELOCK; - TiebreakerProtection.Tiebreaker internal _tiebreaker; Proposers.State internal _proposers; - DualGovernanceState.Store internal _dgState; + DualGovernanceStateMachine.State internal _stateMachine; EmergencyProtection.State internal _emergencyProtection; address internal _resealCommittee; IResealManager internal _resealManager; + TiebreakerProtection.Tiebreaker internal _tiebreaker; constructor( address config, @@ -39,25 +41,30 @@ contract DualGovernance is IGovernance, ConfigurationProvider { ) ConfigurationProvider(config) { TIMELOCK = ITimelock(timelock); - _dgState.initialize(escrowMasterCopy); + _stateMachine.initialize(escrowMasterCopy); _proposers.register(adminProposer, CONFIG.ADMIN_EXECUTOR()); } + // --- + // Proposals Flow + // --- + function submitProposal(ExternalCall[] calldata calls) external returns (uint256 proposalId) { _proposers.checkProposer(msg.sender); - _dgState.activateNextState(CONFIG.getDualGovernanceConfig()); - _dgState.checkProposalsCreationAllowed(); + _stateMachine.activateNextState(CONFIG.getDualGovernanceConfig()); + if (!_stateMachine.canSubmitProposal()) { + revert ProposalSubmissionBlocked(); + } Proposer memory proposer = _proposers.get(msg.sender); proposalId = TIMELOCK.submit(proposer.executor, calls); } function scheduleProposal(uint256 proposalId) external { - _dgState.activateNextState(CONFIG.getDualGovernanceConfig()); - - _dgState.checkCanScheduleProposal(TIMELOCK.getProposalState(proposalId).submittedAt); + _stateMachine.activateNextState(CONFIG.getDualGovernanceConfig()); + if (!_stateMachine.canScheduleProposal(TIMELOCK.getProposalState(proposalId).submittedAt)) { + revert ProposalSchedulingBlocked(proposalId); + } TIMELOCK.schedule(proposalId); - - emit ProposalScheduled(proposalId); } function cancelAllPendingProposals() external { @@ -65,52 +72,41 @@ contract DualGovernance is IGovernance, ConfigurationProvider { TIMELOCK.cancelAllNonExecutedProposals(); } - function getVetoSignallingEscrow() external view returns (address) { - return address(_dgState.signallingEscrow); - } - - function getRageQuitEscrow() external view returns (address) { - return address(_dgState.rageQuitEscrow); + function canSubmitProposal() public view returns (bool) { + return _stateMachine.canSubmitProposal(); } - function canSchedule(uint256 proposalId) external view returns (bool) { - return _dgState.isProposalsAdoptionAllowed() && TIMELOCK.canSchedule(proposalId); + function canScheduleProposal(uint256 proposalId) external view returns (bool) { + return _stateMachine.canScheduleProposal(TIMELOCK.getProposalState(proposalId).submittedAt) + && TIMELOCK.canSchedule(proposalId); } // --- // Dual Governance State // --- - function activateNextState() external { - _dgState.activateNextState(CONFIG.getDualGovernanceConfig()); + function getVetoSignallingEscrow() external view returns (address) { + return address(_stateMachine.signallingEscrow); } - function getCurrentState() external view returns (State) { - return _dgState.currentState(); + function getRageQuitEscrow() external view returns (address) { + return address(_stateMachine.rageQuitEscrow); } - function getVetoSignallingState() - external - view - returns (bool isActive, Duration duration, Timestamp activatedAt, Timestamp enteredAt) - { - (isActive, duration, activatedAt, enteredAt) = _dgState.getVetoSignallingState(CONFIG.getDualGovernanceConfig()); + function activateNextState() external { + _stateMachine.activateNextState(CONFIG.getDualGovernanceConfig()); } - function getVetoSignallingDeactivationState() - external - view - returns (bool isActive, Duration duration, Timestamp enteredAt) - { - (isActive, duration, enteredAt) = _dgState.getVetoSignallingDeactivationState(CONFIG.getDualGovernanceConfig()); + function getCurrentStatus() external view returns (Status) { + return _stateMachine.getCurrentStatus(); } - function getVetoSignallingDuration() external view returns (Duration) { - return _dgState.getVetoSignallingDuration(CONFIG.getDualGovernanceConfig()); + function getCurrentState() external view returns (DualGovernanceStateMachine.State memory) { + return _stateMachine.getCurrentState(); } - function isSchedulingEnabled() external view returns (bool) { - return _dgState.isProposalsAdoptionAllowed(); + function getDynamicTimelockDuration() external view returns (Duration) { + return _stateMachine.getDynamicTimelockDuration(CONFIG.getDualGovernanceConfig()); } // --- @@ -149,13 +145,17 @@ contract DualGovernance is IGovernance, ConfigurationProvider { function tiebreakerResumeSealable(address sealable) external { _tiebreaker.checkTiebreakerCommittee(msg.sender); - _dgState.checkTiebreak(CONFIG); + if (!_stateMachine.isTiebreak(CONFIG)) { + revert NotTiebreak(); + } _tiebreaker.resumeSealable(sealable); } function tiebreakerScheduleProposal(uint256 proposalId) external { _tiebreaker.checkTiebreakerCommittee(msg.sender); - _dgState.checkTiebreak(CONFIG); + if (!_stateMachine.isTiebreak(CONFIG)) { + revert NotTiebreak(); + } TIMELOCK.schedule(proposalId); } @@ -172,7 +172,9 @@ contract DualGovernance is IGovernance, ConfigurationProvider { if (msg.sender != _resealCommittee) { revert NotResealCommitttee(msg.sender); } - _dgState.checkResealState(); + if (_stateMachine.getCurrentStatus() == Status.Normal) { + revert ResealIsNotAllowedInNormalState(); + } _resealManager.reseal(sealables); } diff --git a/contracts/SingleGovernance.sol b/contracts/SingleGovernance.sol index 73262ef2..faab93f2 100644 --- a/contracts/SingleGovernance.sol +++ b/contracts/SingleGovernance.sol @@ -30,7 +30,7 @@ contract SingleGovernance is IGovernance, ConfigurationProvider { TIMELOCK.execute(proposalId); } - function canSchedule(uint256 proposalId) external view returns (bool) { + function canScheduleProposal(uint256 proposalId) external view returns (bool) { return TIMELOCK.canSchedule(proposalId); } diff --git a/contracts/interfaces/IConfiguration.sol b/contracts/interfaces/IConfiguration.sol index 1b572e87..7d3f8535 100644 --- a/contracts/interfaces/IConfiguration.sol +++ b/contracts/interfaces/IConfiguration.sol @@ -2,21 +2,7 @@ pragma solidity 0.8.23; import {Duration} from "../types/Duration.sol"; - -struct DualGovernanceConfig { - uint256 firstSealRageQuitSupport; - uint256 secondSealRageQuitSupport; - Duration dynamicTimelockMaxDuration; - Duration dynamicTimelockMinDuration; - Duration vetoSignallingMinActiveDuration; - Duration vetoSignallingDeactivationMaxDuration; - Duration vetoCooldownDuration; - Duration rageQuitExtraTimelock; - Duration rageQuitExtensionDelay; - Duration rageQuitEthWithdrawalsMinTimelock; - uint256 rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber; - uint256[3] rageQuitEthWithdrawalsTimelockGrowthCoeffs; -} +import {DualGovernanceConfig} from "../libraries//DualGovernanceConfig.sol"; interface IEscrowConfigration { function MIN_WITHDRAWALS_BATCH_SIZE() external view returns (uint256); diff --git a/contracts/interfaces/ITimelock.sol b/contracts/interfaces/ITimelock.sol index 652dacbe..d1610ed3 100644 --- a/contracts/interfaces/ITimelock.sol +++ b/contracts/interfaces/ITimelock.sol @@ -11,7 +11,7 @@ interface IGovernance { function scheduleProposal(uint256 proposalId) external; function cancelAllPendingProposals() external; - function canSchedule(uint256 proposalId) external view returns (bool); + function canScheduleProposal(uint256 proposalId) external view returns (bool); } interface ITimelock { diff --git a/contracts/libraries/DualGovernanceConfig.sol b/contracts/libraries/DualGovernanceConfig.sol new file mode 100644 index 00000000..3d8891a5 --- /dev/null +++ b/contracts/libraries/DualGovernanceConfig.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Duration, Durations} from "../types/Duration.sol"; +import {Timestamp, Timestamps} from "../types/Timestamp.sol"; + +struct DualGovernanceConfig { + uint256 firstSealRageQuitSupport; + uint256 secondSealRageQuitSupport; + Duration dynamicTimelockMaxDuration; + Duration dynamicTimelockMinDuration; + Duration vetoSignallingMinActiveDuration; + Duration vetoSignallingDeactivationMaxDuration; + Duration vetoCooldownDuration; + Duration rageQuitExtraTimelock; + Duration rageQuitExtensionDelay; + Duration rageQuitEthWithdrawalsMinTimelock; + uint256 rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber; + uint256[3] rageQuitEthWithdrawalsTimelockGrowthCoeffs; +} + +library DualGovernanceConfigUtils { + function isFirstSealRageQuitSupportCrossed( + DualGovernanceConfig memory config, + uint256 rageQuitSupport + ) internal pure returns (bool) { + return rageQuitSupport > config.firstSealRageQuitSupport; + } + + function isSecondSealRageQuitSupportCrossed( + DualGovernanceConfig memory config, + uint256 rageQuitSupport + ) internal pure returns (bool) { + return rageQuitSupport > config.secondSealRageQuitSupport; + } + + function isDynamicTimelockMaxDurationPassed( + DualGovernanceConfig memory config, + Timestamp vetoSignallingActivatedAt + ) internal view returns (bool) { + return Timestamps.now() > config.dynamicTimelockMaxDuration.addTo(vetoSignallingActivatedAt); + } + + function isDynamicTimelockDurationPassed( + DualGovernanceConfig memory config, + Timestamp vetoSignallingActivatedAt, + uint256 rageQuitSupport + ) internal view returns (bool) { + Duration dynamicTimelock = calcDynamicTimelockDuration(config, rageQuitSupport); + return Timestamps.now() > dynamicTimelock.addTo(vetoSignallingActivatedAt); + } + + function isVetoSignallingReactivationDurationPassed( + DualGovernanceConfig memory config, + Timestamp vetoSignallingReactivationTime + ) internal view returns (bool) { + return Timestamps.now() > config.vetoSignallingMinActiveDuration.addTo(vetoSignallingReactivationTime); + } + + function isVetoSignallingDeactivationMaxDurationPassed( + DualGovernanceConfig memory config, + Timestamp vetoSignallingDeactivationEnteredAt + ) internal view returns (bool) { + return + Timestamps.now() > config.vetoSignallingDeactivationMaxDuration.addTo(vetoSignallingDeactivationEnteredAt); + } + + function isVetoCooldownDurationPassed( + DualGovernanceConfig memory config, + Timestamp vetoCooldownEnteredAt + ) internal view returns (bool) { + return Timestamps.now() > config.vetoCooldownDuration.addTo(vetoCooldownEnteredAt); + } + + function calcDynamicTimelockDuration( + DualGovernanceConfig memory config, + uint256 rageQuitSupport + ) internal pure returns (Duration duration_) { + uint256 firstSealRageQuitSupport = config.firstSealRageQuitSupport; + uint256 secondSealRageQuitSupport = config.secondSealRageQuitSupport; + Duration dynamicTimelockMinDuration = config.dynamicTimelockMinDuration; + Duration dynamicTimelockMaxDuration = config.dynamicTimelockMaxDuration; + + if (rageQuitSupport < firstSealRageQuitSupport) { + return Durations.ZERO; + } + + if (rageQuitSupport >= secondSealRageQuitSupport) { + return dynamicTimelockMaxDuration; + } + + duration_ = dynamicTimelockMinDuration + + Durations.from( + (rageQuitSupport - firstSealRageQuitSupport) + * (dynamicTimelockMaxDuration - dynamicTimelockMinDuration).toSeconds() + / (secondSealRageQuitSupport - firstSealRageQuitSupport) + ); + } + + function calcRageQuitWithdrawalsTimelock( + DualGovernanceConfig memory config, + uint256 rageQuitRound + ) internal pure returns (Duration) { + if (rageQuitRound < config.rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber) { + return config.rageQuitEthWithdrawalsMinTimelock; + } + return config.rageQuitEthWithdrawalsMinTimelock + + Durations.from( + ( + config.rageQuitEthWithdrawalsTimelockGrowthCoeffs[0] * rageQuitRound * rageQuitRound + + config.rageQuitEthWithdrawalsTimelockGrowthCoeffs[1] * rageQuitRound + + config.rageQuitEthWithdrawalsTimelockGrowthCoeffs[2] + ) / 10 ** 18 + ); // TODO: rewrite in a prettier way + } +} diff --git a/contracts/libraries/DualGovernanceState.sol b/contracts/libraries/DualGovernanceState.sol deleted file mode 100644 index 5b030899..00000000 --- a/contracts/libraries/DualGovernanceState.sol +++ /dev/null @@ -1,395 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.23; - -import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; - -import {IEscrow} from "../interfaces/IEscrow.sol"; -import {ISealable} from "../interfaces/ISealable.sol"; -import {IDualGovernanceConfiguration as IConfiguration, DualGovernanceConfig} from "../interfaces/IConfiguration.sol"; - -import {Duration, Durations} from "../types/Duration.sol"; -import {Timestamp, Timestamps} from "../types/Timestamp.sol"; - -enum State { - Normal, - VetoSignalling, - VetoSignallingDeactivation, - VetoCooldown, - RageQuit -} - -library DualGovernanceState { - // TODO: Optimize storage layout efficiency - struct Store { - State state; - Timestamp enteredAt; - // the time the veto signalling state was entered - Timestamp vetoSignallingActivationTime; - IEscrow signallingEscrow; // 248 - // the time the Deactivation sub-state was last exited without exiting the parent Veto Signalling state - Timestamp vetoSignallingReactivationTime; - // the last time a proposal was submitted to the DG subsystem - Timestamp lastAdoptableStateExitedAt; - IEscrow rageQuitEscrow; - uint8 rageQuitRound; - } - - error NotTie(); - error AlreadyInitialized(); - error ProposalsCreationSuspended(); - error ProposalsAdoptionSuspended(); - error ResealIsNotAllowedInNormalState(); - - event NewSignallingEscrowDeployed(address indexed escrow); - event DualGovernanceStateChanged(State oldState, State newState); - - function initialize(Store storage self, address escrowMasterCopy) internal { - if (address(self.signallingEscrow) != address(0)) { - revert AlreadyInitialized(); - } - _deployNewSignallingEscrow(self, escrowMasterCopy); - } - - function activateNextState( - Store storage self, - DualGovernanceConfig memory config - ) internal returns (State newState) { - State oldState = self.state; - if (oldState == State.Normal) { - newState = _fromNormalState(self, config); - } else if (oldState == State.VetoSignalling) { - newState = _fromVetoSignallingState(self, config); - } else if (oldState == State.VetoSignallingDeactivation) { - newState = _fromVetoSignallingDeactivationState(self, config); - } else if (oldState == State.VetoCooldown) { - newState = _fromVetoCooldownState(self, config); - } else if (oldState == State.RageQuit) { - newState = _fromRageQuitState(self, config); - } else { - assert(false); - } - - if (oldState != newState) { - self.state = newState; - _handleStateTransitionSideEffects(self, config, oldState, newState); - emit DualGovernanceStateChanged(oldState, newState); - } - } - - // --- - // View Methods - // --- - - function checkProposalsCreationAllowed(Store storage self) internal view { - if (!isProposalsCreationAllowed(self)) { - revert ProposalsCreationSuspended(); - } - } - - function checkProposalsAdoptionAllowed(Store storage self) internal view { - if (!isProposalsAdoptionAllowed(self)) { - revert ProposalsAdoptionSuspended(); - } - } - - function checkCanScheduleProposal(Store storage self, Timestamp proposalSubmittedAt) internal view { - if (!canScheduleProposal(self, proposalSubmittedAt)) { - revert ProposalsAdoptionSuspended(); - } - } - - function checkTiebreak(Store storage self, IConfiguration config) internal view { - if (!isTiebreak(self, config)) { - revert NotTie(); - } - } - - function checkResealState(Store storage self) internal view { - if (self.state == State.Normal) { - revert ResealIsNotAllowedInNormalState(); - } - } - - function currentState(Store storage self) internal view returns (State) { - return self.state; - } - - function canScheduleProposal(Store storage self, Timestamp proposalSubmissionTime) internal view returns (bool) { - State state = self.state; - if (state == State.Normal) return true; - if (state == State.VetoCooldown) { - return proposalSubmissionTime <= self.vetoSignallingActivationTime; - } - return false; - } - - function isProposalsCreationAllowed(Store storage self) internal view returns (bool) { - State state = self.state; - return state != State.VetoSignallingDeactivation && state != State.VetoCooldown; - } - - function isProposalsAdoptionAllowed(Store storage self) internal view returns (bool) { - State state = self.state; - return state == State.Normal || state == State.VetoCooldown; - } - - function isTiebreak(Store storage self, IConfiguration config) internal view returns (bool) { - if (isProposalsAdoptionAllowed(self)) return false; - - // for the governance is locked for long period of time - if (Timestamps.now() >= config.TIE_BREAK_ACTIVATION_TIMEOUT().addTo(self.lastAdoptableStateExitedAt)) { - return true; - } - - if (self.state != State.RageQuit) return false; - - address[] memory sealableWithdrawalBlockers = config.sealableWithdrawalBlockers(); - for (uint256 i = 0; i < sealableWithdrawalBlockers.length; ++i) { - if (ISealable(sealableWithdrawalBlockers[i]).isPaused()) return true; - } - return false; - } - - function getVetoSignallingState( - Store storage self, - DualGovernanceConfig memory config - ) internal view returns (bool isActive, Duration duration, Timestamp activatedAt, Timestamp enteredAt) { - isActive = self.state == State.VetoSignalling; - duration = isActive ? getVetoSignallingDuration(self, config) : Duration.wrap(0); - enteredAt = isActive ? self.enteredAt : Timestamps.ZERO; - activatedAt = isActive ? self.vetoSignallingActivationTime : Timestamps.ZERO; - } - - function getVetoSignallingDuration( - Store storage self, - DualGovernanceConfig memory config - ) internal view returns (Duration) { - uint256 totalSupport = self.signallingEscrow.getRageQuitSupport(); - return _calcDynamicTimelockDuration(config, totalSupport); - } - - struct VetoSignallingDeactivationState { - uint256 duration; - uint256 enteredAt; - } - - function getVetoSignallingDeactivationState( - Store storage self, - DualGovernanceConfig memory config - ) internal view returns (bool isActive, Duration duration, Timestamp enteredAt) { - isActive = self.state == State.VetoSignallingDeactivation; - duration = config.vetoSignallingDeactivationMaxDuration; - enteredAt = isActive ? self.enteredAt : Timestamps.ZERO; - } - - // --- - // State Transitions - // --- - - function _fromNormalState(Store storage self, DualGovernanceConfig memory config) private view returns (State) { - return _isFirstSealRageQuitSupportCrossed(config, self.signallingEscrow.getRageQuitSupport()) - ? State.VetoSignalling - : State.Normal; - } - - function _fromVetoSignallingState( - Store storage self, - DualGovernanceConfig memory config - ) private view returns (State) { - uint256 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); - - if (!_isDynamicTimelockDurationPassed(self, config, rageQuitSupport)) { - return State.VetoSignalling; - } - - if (_isSecondSealRageQuitSupportCrossed(config, rageQuitSupport)) { - return State.RageQuit; - } - - return _isVetoSignallingReactivationDurationPassed(self, config) - ? State.VetoSignallingDeactivation - : State.VetoSignalling; - } - - function _fromVetoSignallingDeactivationState( - Store storage self, - DualGovernanceConfig memory config - ) private view returns (State) { - uint256 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); - - if (!_isDynamicTimelockDurationPassed(self, config, rageQuitSupport)) { - return State.VetoSignalling; - } - - if (_isSecondSealRageQuitSupportCrossed(config, rageQuitSupport)) { - return State.RageQuit; - } - - if (_isVetoSignallingDeactivationMaxDurationPassed(self, config)) { - return State.VetoCooldown; - } - - return State.VetoSignallingDeactivation; - } - - function _fromVetoCooldownState( - Store storage self, - DualGovernanceConfig memory config - ) private view returns (State) { - if (!_isVetoCooldownDurationPassed(self, config)) { - return State.VetoCooldown; - } - return _isFirstSealRageQuitSupportCrossed(config, self.signallingEscrow.getRageQuitSupport()) - ? State.VetoSignalling - : State.Normal; - } - - function _fromRageQuitState(Store storage self, DualGovernanceConfig memory config) private view returns (State) { - if (!self.rageQuitEscrow.isRageQuitFinalized()) { - return State.RageQuit; - } - return _isFirstSealRageQuitSupportCrossed(config, self.signallingEscrow.getRageQuitSupport()) - ? State.VetoSignalling - : State.VetoCooldown; - } - - // --- - // Helper Methods - // --- - - function _handleStateTransitionSideEffects( - Store storage self, - DualGovernanceConfig memory config, - State oldState, - State newState - ) private { - Timestamp timestamp = Timestamps.now(); - self.enteredAt = timestamp; - // track the time when the governance state allowed execution - if (oldState == State.Normal || oldState == State.VetoCooldown) { - self.lastAdoptableStateExitedAt = timestamp; - } - - if (newState == State.Normal && self.rageQuitRound != 0) { - self.rageQuitRound = 0; - } - - if (newState == State.VetoSignalling && oldState != State.VetoSignallingDeactivation) { - self.vetoSignallingActivationTime = timestamp; - } - - if (oldState == State.VetoSignallingDeactivation && newState == State.VetoSignalling) { - self.vetoSignallingReactivationTime = timestamp; - } - - if (newState == State.RageQuit) { - IEscrow signallingEscrow = self.signallingEscrow; - signallingEscrow.startRageQuit( - config.rageQuitExtensionDelay, _calcRageQuitWithdrawalsTimelock(config, self.rageQuitRound) - ); - self.rageQuitEscrow = signallingEscrow; - _deployNewSignallingEscrow(self, signallingEscrow.MASTER_COPY()); - self.rageQuitRound += 1; - } - } - - function _isFirstSealRageQuitSupportCrossed( - DualGovernanceConfig memory config, - uint256 rageQuitSupport - ) private pure returns (bool) { - return rageQuitSupport > config.firstSealRageQuitSupport; - } - - function _isSecondSealRageQuitSupportCrossed( - DualGovernanceConfig memory config, - uint256 rageQuitSupport - ) private pure returns (bool) { - return rageQuitSupport > config.secondSealRageQuitSupport; - } - - function _isDynamicTimelockMaxDurationPassed( - Store storage self, - DualGovernanceConfig memory config - ) private view returns (bool) { - return Timestamps.now() > config.dynamicTimelockMaxDuration.addTo(self.vetoSignallingActivationTime); - } - - function _isDynamicTimelockDurationPassed( - Store storage self, - DualGovernanceConfig memory config, - uint256 rageQuitSupport - ) private view returns (bool) { - Duration dynamicTimelock = _calcDynamicTimelockDuration(config, rageQuitSupport); - return Timestamps.now() > dynamicTimelock.addTo(self.vetoSignallingActivationTime); - } - - function _isVetoSignallingReactivationDurationPassed( - Store storage self, - DualGovernanceConfig memory config - ) private view returns (bool) { - return Timestamps.now() > config.vetoSignallingMinActiveDuration.addTo(self.vetoSignallingReactivationTime); - } - - function _isVetoSignallingDeactivationMaxDurationPassed( - Store storage self, - DualGovernanceConfig memory config - ) private view returns (bool) { - return Timestamps.now() > config.vetoSignallingDeactivationMaxDuration.addTo(self.enteredAt); - } - - function _isVetoCooldownDurationPassed( - Store storage self, - DualGovernanceConfig memory config - ) private view returns (bool) { - return Timestamps.now() > config.vetoCooldownDuration.addTo(self.enteredAt); - } - - function _deployNewSignallingEscrow(Store storage self, address escrowMasterCopy) private { - IEscrow clone = IEscrow(Clones.clone(escrowMasterCopy)); - clone.initialize(address(this)); - self.signallingEscrow = clone; - emit NewSignallingEscrowDeployed(address(clone)); - } - - function _calcRageQuitWithdrawalsTimelock( - DualGovernanceConfig memory config, - uint256 rageQuitRound - ) private pure returns (Duration) { - if (rageQuitRound < config.rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber) { - return config.rageQuitEthWithdrawalsMinTimelock; - } - return config.rageQuitEthWithdrawalsMinTimelock - + Durations.from( - ( - config.rageQuitEthWithdrawalsTimelockGrowthCoeffs[0] * rageQuitRound * rageQuitRound - + config.rageQuitEthWithdrawalsTimelockGrowthCoeffs[1] * rageQuitRound - + config.rageQuitEthWithdrawalsTimelockGrowthCoeffs[2] - ) / 10 ** 18 - ); // TODO: rewrite in a prettier way - } - - function _calcDynamicTimelockDuration( - DualGovernanceConfig memory config, - uint256 rageQuitSupport - ) internal pure returns (Duration duration_) { - uint256 firstSealRageQuitSupport = config.firstSealRageQuitSupport; - uint256 secondSealRageQuitSupport = config.secondSealRageQuitSupport; - Duration dynamicTimelockMinDuration = config.dynamicTimelockMinDuration; - Duration dynamicTimelockMaxDuration = config.dynamicTimelockMaxDuration; - - if (rageQuitSupport < firstSealRageQuitSupport) { - return Durations.ZERO; - } - - if (rageQuitSupport >= secondSealRageQuitSupport) { - return dynamicTimelockMaxDuration; - } - - duration_ = dynamicTimelockMinDuration - + Durations.from( - (rageQuitSupport - firstSealRageQuitSupport) - * (dynamicTimelockMaxDuration - dynamicTimelockMinDuration).toSeconds() - / (secondSealRageQuitSupport - firstSealRageQuitSupport) - ); - } -} diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol new file mode 100644 index 00000000..f43e44e4 --- /dev/null +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; + +import {IEscrow} from "../interfaces/IEscrow.sol"; +import {ISealable} from "../interfaces/ISealable.sol"; +import {IDualGovernanceConfiguration} from "../interfaces/IConfiguration.sol"; + +import {Duration} from "../types/Duration.sol"; +import {Timestamp, Timestamps} from "../types/Timestamp.sol"; +import {DualGovernanceConfig, DualGovernanceConfigUtils} from "./DualGovernanceConfig.sol"; + +enum Status { + Unset, + Normal, + VetoSignalling, + VetoSignallingDeactivation, + VetoCooldown, + RageQuit +} + +library DualGovernanceStateMachine { + using DualGovernanceConfigUtils for DualGovernanceConfig; + using DualGovernanceStateTransitions for State; + + struct State { + /// + /// @dev slot 0: [0..7] + /// The current state of the Dual Governance FSM + Status status; + /// + /// @dev slot 0: [8..47] + /// The timestamp when the Dual Governance FSM entered the current state + Timestamp enteredAt; + /// + /// @dev slot 0: [48..87] + /// The time the VetoSignalling FSM state was entered the last time + Timestamp vetoSignallingActivatedAt; + /// + /// @dev slot 0: [88..247] + /// The address of the currently used Veto Signalling Escrow + IEscrow signallingEscrow; + /// + /// @dev slot 0: [248..255] + /// The number of the Rage Quit round. Initial value is 0. + uint8 rageQuitRound; + /// + /// @dev slot 1: [0..39] + /// The last time VetoSignallingDeactivation -> VetoSignalling transition happened + Timestamp vetoSignallingReactivationTime; + /// + /// @dev slot 1: [40..79] + /// The last time when the Dual Governance FSM exited Normal or VetoCooldown state + Timestamp normalOrVetoCooldownExitedAt; + /// + /// @dev slot 1: [80..239] + /// The address of the Escrow used during the last (may be ongoing) Rage Quit process + IEscrow rageQuitEscrow; + } + + error AlreadyInitialized(); + + event NewSignallingEscrowDeployed(address indexed escrow); + event DualGovernanceStateChanged(Status from, Status to, State state); + + function initialize(State storage self, address escrowMasterCopy) internal { + if (self.status != Status.Unset) { + revert AlreadyInitialized(); + } + + self.status = Status.Normal; + self.enteredAt = Timestamps.now(); + _deployNewSignallingEscrow(self, escrowMasterCopy); + + emit DualGovernanceStateChanged(Status.Unset, Status.Normal, self); + } + + function activateNextState(State storage self, DualGovernanceConfig memory config) internal { + (Status currentStatus, Status newStatus) = self.getStateTransition(config); + + if (currentStatus == newStatus) { + return; + } + + self.status = newStatus; + self.enteredAt = Timestamps.now(); + + if (currentStatus == Status.Normal || currentStatus == Status.VetoCooldown) { + self.normalOrVetoCooldownExitedAt = Timestamps.now(); + } + + if (newStatus == Status.Normal && self.rageQuitRound != 0) { + self.rageQuitRound = 0; + } else if (newStatus == Status.VetoSignalling) { + if (currentStatus == Status.VetoSignallingDeactivation) { + self.vetoSignallingReactivationTime = Timestamps.now(); + } else { + self.vetoSignallingActivatedAt = Timestamps.now(); + } + } else if (newStatus == Status.RageQuit) { + IEscrow signallingEscrow = self.signallingEscrow; + uint256 rageQuitRound = Math.min(self.rageQuitRound + 1, type(uint8).max); + self.rageQuitRound = uint8(rageQuitRound); + signallingEscrow.startRageQuit( + config.rageQuitExtensionDelay, config.calcRageQuitWithdrawalsTimelock(rageQuitRound) + ); + self.rageQuitEscrow = signallingEscrow; + _deployNewSignallingEscrow(self, signallingEscrow.MASTER_COPY()); + } + + emit DualGovernanceStateChanged(currentStatus, newStatus, self); + } + + function getCurrentState(State storage self) internal pure returns (State memory) { + return self; + } + + function getCurrentStatus(State storage self) internal view returns (Status) { + return self.status; + } + + function getDynamicTimelockDuration( + State storage self, + DualGovernanceConfig memory config + ) internal view returns (Duration) { + return config.calcDynamicTimelockDuration(self.signallingEscrow.getRageQuitSupport()); + } + + function canSubmitProposal(State storage self) internal view returns (bool) { + Status state = self.status; + return state != Status.VetoSignallingDeactivation && state != Status.VetoCooldown; + } + + function canScheduleProposal(State storage self, Timestamp proposalSubmissionTime) internal view returns (bool) { + Status state = self.status; + if (state == Status.Normal) return true; + if (state == Status.VetoCooldown) { + return proposalSubmissionTime <= self.vetoSignallingActivatedAt; + } + return false; + } + + function isTiebreak(State storage self, IDualGovernanceConfiguration config) internal view returns (bool) { + Status state = self.status; + if (state == Status.Normal || state == Status.VetoCooldown) return false; + + // when the governance is locked for long period of time + if (Timestamps.now() >= config.TIE_BREAK_ACTIVATION_TIMEOUT().addTo(self.normalOrVetoCooldownExitedAt)) { + return true; + } + + if (self.status != Status.RageQuit) return false; + + address[] memory sealableWithdrawalBlockers = config.sealableWithdrawalBlockers(); + for (uint256 i = 0; i < sealableWithdrawalBlockers.length; ++i) { + if (ISealable(sealableWithdrawalBlockers[i]).isPaused()) return true; + } + return false; + } + + function _deployNewSignallingEscrow(State storage self, address escrowMasterCopy) private { + IEscrow clone = IEscrow(Clones.clone(escrowMasterCopy)); + clone.initialize(address(this)); + self.signallingEscrow = clone; + emit NewSignallingEscrowDeployed(address(clone)); + } +} + +library DualGovernanceStateTransitions { + using DualGovernanceConfigUtils for DualGovernanceConfig; + + function getStateTransition( + DualGovernanceStateMachine.State storage self, + DualGovernanceConfig memory config + ) internal view returns (Status currentStatus, Status nextStatus) { + currentStatus = self.status; + if (currentStatus == Status.Normal) { + nextStatus = _fromNormalState(self, config); + } else if (currentStatus == Status.VetoSignalling) { + nextStatus = _fromVetoSignallingState(self, config); + } else if (currentStatus == Status.VetoSignallingDeactivation) { + nextStatus = _fromVetoSignallingDeactivationState(self, config); + } else if (currentStatus == Status.VetoCooldown) { + nextStatus = _fromVetoCooldownState(self, config); + } else if (currentStatus == Status.RageQuit) { + nextStatus = _fromRageQuitState(self, config); + } else { + assert(false); + } + } + + function _fromNormalState( + DualGovernanceStateMachine.State storage self, + DualGovernanceConfig memory config + ) private view returns (Status) { + return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + ? Status.VetoSignalling + : Status.Normal; + } + + function _fromVetoSignallingState( + DualGovernanceStateMachine.State storage self, + DualGovernanceConfig memory config + ) private view returns (Status) { + uint256 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); + + if (!config.isDynamicTimelockDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { + return Status.VetoSignalling; + } + + if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) { + return Status.RageQuit; + } + + return config.isVetoSignallingReactivationDurationPassed(self.vetoSignallingReactivationTime) + ? Status.VetoSignallingDeactivation + : Status.VetoSignalling; + } + + function _fromVetoSignallingDeactivationState( + DualGovernanceStateMachine.State storage self, + DualGovernanceConfig memory config + ) private view returns (Status) { + uint256 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); + + if (!config.isDynamicTimelockDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { + return Status.VetoSignalling; + } + + if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) { + return Status.RageQuit; + } + + if (config.isVetoSignallingDeactivationMaxDurationPassed(self.enteredAt)) { + return Status.VetoCooldown; + } + + return Status.VetoSignallingDeactivation; + } + + function _fromVetoCooldownState( + DualGovernanceStateMachine.State storage self, + DualGovernanceConfig memory config + ) private view returns (Status) { + if (!config.isVetoCooldownDurationPassed(self.enteredAt)) { + return Status.VetoCooldown; + } + return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + ? Status.VetoSignalling + : Status.Normal; + } + + function _fromRageQuitState( + DualGovernanceStateMachine.State storage self, + DualGovernanceConfig memory config + ) private view returns (Status) { + if (!self.rageQuitEscrow.isRageQuitFinalized()) { + return Status.RageQuit; + } + return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + ? Status.VetoSignalling + : Status.VetoCooldown; + } +} diff --git a/test/scenario/last-moment-malicious-proposal.t.sol b/test/scenario/last-moment-malicious-proposal.t.sol index 07952cb6..6d5676e5 100644 --- a/test/scenario/last-moment-malicious-proposal.t.sol +++ b/test/scenario/last-moment-malicious-proposal.t.sol @@ -6,7 +6,7 @@ import { ScenarioTestBlueprint, ExternalCall, ExternalCallHelpers, - DualGovernanceState, + DualGovernance, Durations } from "../utils/scenario-test-blueprint.sol"; @@ -97,7 +97,9 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _executeProposal(proposalId); _assertProposalExecuted(proposalId); - vm.expectRevert(DualGovernanceState.ProposalsAdoptionSuspended.selector); + vm.expectRevert( + abi.encodeWithSelector(DualGovernance.ProposalSchedulingBlocked.selector, maliciousProposalId) + ); this.scheduleProposalExternal(maliciousProposalId); _assertProposalSubmitted(maliciousProposalId); @@ -120,7 +122,9 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _activateNextState(); _assertRageQuitState(); - vm.expectRevert(DualGovernanceState.ProposalsAdoptionSuspended.selector); + vm.expectRevert( + abi.encodeWithSelector(DualGovernance.ProposalSchedulingBlocked.selector, maliciousProposalId) + ); this.scheduleProposalExternal(maliciousProposalId); } } @@ -216,7 +220,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _activateNextState(); _assertVetoCooldownState(); - vm.expectRevert(DualGovernanceState.ProposalsAdoptionSuspended.selector); + vm.expectRevert(abi.encodeWithSelector(DualGovernance.ProposalSchedulingBlocked.selector, proposalId)); this.scheduleProposalExternal(proposalId); } @@ -270,7 +274,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _activateNextState(); _assertVetoCooldownState(); - vm.expectRevert(DualGovernanceState.ProposalsAdoptionSuspended.selector); + vm.expectRevert(abi.encodeWithSelector(DualGovernance.ProposalSchedulingBlocked.selector, proposalId)); this.scheduleProposalExternal(proposalId); } @@ -281,7 +285,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _assertVetoSignalingState(); _logVetoSignallingState(); - vm.expectRevert(DualGovernanceState.ProposalsAdoptionSuspended.selector); + vm.expectRevert(abi.encodeWithSelector(DualGovernance.ProposalSchedulingBlocked.selector, proposalId)); this.scheduleProposalExternal(proposalId); } diff --git a/test/scenario/veto-cooldown-mechanics.t.sol b/test/scenario/veto-cooldown-mechanics.t.sol index 1702dbbe..1a9748d3 100644 --- a/test/scenario/veto-cooldown-mechanics.t.sol +++ b/test/scenario/veto-cooldown-mechanics.t.sol @@ -6,8 +6,8 @@ import { percents, ExternalCall, ExternalCallHelpers, - DualGovernanceState, - ScenarioTestBlueprint + ScenarioTestBlueprint, + DualGovernance } from "../utils/scenario-test-blueprint.sol"; interface IDangerousContract { @@ -97,7 +97,9 @@ contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { _activateNextState(); _assertVetoCooldownState(); - vm.expectRevert(DualGovernanceState.ProposalsAdoptionSuspended.selector); + vm.expectRevert( + abi.encodeWithSelector(DualGovernance.ProposalSchedulingBlocked.selector, anotherProposalId) + ); this.scheduleProposalExternal(anotherProposalId); } } diff --git a/test/unit/SingleGovernance.t.sol b/test/unit/SingleGovernance.t.sol index b0a8dcba..8c6dce04 100644 --- a/test/unit/SingleGovernance.t.sol +++ b/test/unit/SingleGovernance.t.sol @@ -112,10 +112,10 @@ contract SingleGovernanceUnitTests is UnitTest { vm.prank(_governance); _singleGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); - assertFalse(_singleGovernance.canSchedule(1)); + assertFalse(_singleGovernance.canScheduleProposal(1)); _timelock.setSchedule(1); - assertTrue(_singleGovernance.canSchedule(1)); + assertTrue(_singleGovernance.canScheduleProposal(1)); } } diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index b6b57dc9..3b4baed6 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -29,7 +29,9 @@ import { } from "contracts/EmergencyProtectedTimelock.sol"; import {SingleGovernance, IGovernance} from "contracts/SingleGovernance.sol"; -import {DualGovernance, DualGovernanceState, State} from "contracts/DualGovernance.sol"; +import { + DualGovernance, Status as DualGovernanceStatus, DualGovernanceStateMachine +} from "contracts/DualGovernance.sol"; import {ExternalCall} from "contracts/libraries/ExternalCalls.sol"; @@ -124,13 +126,11 @@ contract ScenarioTestBlueprint is Test { view returns (bool isActive, uint256 duration, uint256 activatedAt, uint256 enteredAt) { - DurationType duration_; - Timestamp activatedAt_; - Timestamp enteredAt_; - (isActive, duration_, activatedAt_, enteredAt_) = _dualGovernance.getVetoSignallingState(); - duration = DurationType.unwrap(duration_); - enteredAt = Timestamp.unwrap(enteredAt_); - activatedAt = Timestamp.unwrap(activatedAt_); + DualGovernanceStateMachine.State memory state = _dualGovernance.getCurrentState(); + isActive = state.status == DualGovernanceStatus.VetoSignalling; + duration = _dualGovernance.getDynamicTimelockDuration().toSeconds(); + enteredAt = state.enteredAt.toSeconds(); + activatedAt = state.vetoSignallingActivatedAt.toSeconds(); } function _getVetoSignallingDeactivationState() @@ -138,11 +138,10 @@ contract ScenarioTestBlueprint is Test { view returns (bool isActive, uint256 duration, uint256 enteredAt) { - Timestamp enteredAt_; - DurationType duration_; - (isActive, duration_, enteredAt_) = _dualGovernance.getVetoSignallingDeactivationState(); - duration = DurationType.unwrap(duration_); - enteredAt = Timestamp.unwrap(enteredAt_); + DualGovernanceStateMachine.State memory state = _dualGovernance.getCurrentState(); + isActive = state.status == DualGovernanceStatus.VetoSignallingDeactivation; + duration = _dualGovernance.CONFIG().VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().toSeconds(); + enteredAt = state.enteredAt.toSeconds(); } // --- @@ -401,7 +400,7 @@ contract ScenarioTestBlueprint is Test { } function _assertCanSchedule(IGovernance governance, uint256 proposalId, bool canSchedule) internal { - assertEq(governance.canSchedule(proposalId), canSchedule, "unexpected canSchedule() value"); + assertEq(governance.canScheduleProposal(proposalId), canSchedule, "unexpected canSchedule() value"); } function _assertCanScheduleAndExecute(IGovernance governance, uint256 proposalId) internal { @@ -441,23 +440,23 @@ contract ScenarioTestBlueprint is Test { } function _assertNormalState() internal { - assertEq(uint256(_dualGovernance.getCurrentState()), uint256(State.Normal)); + assertEq(_dualGovernance.getCurrentStatus(), DualGovernanceStatus.Normal); } function _assertVetoSignalingState() internal { - assertEq(uint256(_dualGovernance.getCurrentState()), uint256(State.VetoSignalling)); + assertEq(_dualGovernance.getCurrentStatus(), DualGovernanceStatus.VetoSignalling); } function _assertVetoSignalingDeactivationState() internal { - assertEq(uint256(_dualGovernance.getCurrentState()), uint256(State.VetoSignallingDeactivation)); + assertEq(_dualGovernance.getCurrentStatus(), DualGovernanceStatus.VetoSignallingDeactivation); } function _assertRageQuitState() internal { - assertEq(uint256(_dualGovernance.getCurrentState()), uint256(State.RageQuit)); + assertEq(_dualGovernance.getCurrentStatus(), DualGovernanceStatus.RageQuit); } function _assertVetoCooldownState() internal { - assertEq(uint256(_dualGovernance.getCurrentState()), uint256(State.VetoCooldown)); + assertEq(_dualGovernance.getCurrentStatus(), DualGovernanceStatus.VetoCooldown); } function _assertNoTargetMockCalls() internal { @@ -769,7 +768,7 @@ contract ScenarioTestBlueprint is Test { assertEq(uint256(a), uint256(b), message); } - function assertEq(State a, State b) internal { + function assertEq(DualGovernanceStatus a, DualGovernanceStatus b) internal { assertEq(uint256(a), uint256(b)); } From 6d1dbc22c41f77012e75448870c3f0516058b506 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Fri, 19 Jul 2024 01:43:05 +0400 Subject: [PATCH 2/4] Introduce TiebreakConfig struct --- contracts/Configuration.sol | 25 ++++++------------- contracts/DualGovernance.sol | 4 +-- contracts/interfaces/IConfiguration.sol | 15 +++-------- contracts/libraries/DualGovernanceConfig.sol | 5 ++++ .../libraries/DualGovernanceStateMachine.sol | 13 +++++----- 5 files changed, 23 insertions(+), 39 deletions(-) diff --git a/contracts/Configuration.sol b/contracts/Configuration.sol index 17f3b6dc..e9d16b74 100644 --- a/contracts/Configuration.sol +++ b/contracts/Configuration.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.23; import {Durations, Duration} from "./types/Duration.sol"; -import {IConfiguration, DualGovernanceConfig} from "./interfaces/IConfiguration.sol"; +import {IConfiguration, DualGovernanceConfig, TiebreakConfig} from "./interfaces/IConfiguration.sol"; uint256 constant PERCENT = 10 ** 16; @@ -73,7 +73,7 @@ contract Configuration is IConfiguration { if (SEALABLES_COUNT > 4) SEALABLE_4 = sealableWithdrawalBlockers_[4]; } - function sealableWithdrawalBlockers() external view returns (address[] memory sealableWithdrawalBlockers_) { + function getSealableWithdrawalBlockers() public view returns (address[] memory sealableWithdrawalBlockers_) { sealableWithdrawalBlockers_ = new address[](SEALABLES_COUNT); if (SEALABLES_COUNT > 0) sealableWithdrawalBlockers_[0] = SEALABLE_0; if (SEALABLES_COUNT > 1) sealableWithdrawalBlockers_[1] = SEALABLE_1; @@ -82,22 +82,6 @@ contract Configuration is IConfiguration { if (SEALABLES_COUNT > 4) sealableWithdrawalBlockers_[4] = SEALABLE_4; } - function getSignallingThresholdData() - external - view - returns ( - uint256 firstSealRageQuitSupport, - uint256 secondSealRageQuitSupport, - Duration dynamicTimelockMinDuration, - Duration dynamicTimelockMaxDuration - ) - { - firstSealRageQuitSupport = FIRST_SEAL_RAGE_QUIT_SUPPORT; - secondSealRageQuitSupport = SECOND_SEAL_RAGE_QUIT_SUPPORT; - dynamicTimelockMinDuration = DYNAMIC_TIMELOCK_MIN_DURATION; - dynamicTimelockMaxDuration = DYNAMIC_TIMELOCK_MAX_DURATION; - } - function getDualGovernanceConfig() external view returns (DualGovernanceConfig memory config) { config.firstSealRageQuitSupport = FIRST_SEAL_RAGE_QUIT_SUPPORT; config.secondSealRageQuitSupport = SECOND_SEAL_RAGE_QUIT_SUPPORT; @@ -116,4 +100,9 @@ contract Configuration is IConfiguration { RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_C ]; } + + function getTiebreakConfig() external view returns (TiebreakConfig memory config) { + config.tiebreakActivationTimeout = TIE_BREAK_ACTIVATION_TIMEOUT; + config.potentialDeadlockSealables = getSealableWithdrawalBlockers(); + } } diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 717890ac..aa232d4f 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -145,7 +145,7 @@ contract DualGovernance is IGovernance, ConfigurationProvider { function tiebreakerResumeSealable(address sealable) external { _tiebreaker.checkTiebreakerCommittee(msg.sender); - if (!_stateMachine.isTiebreak(CONFIG)) { + if (!_stateMachine.isDeadlock(CONFIG.getTiebreakConfig())) { revert NotTiebreak(); } _tiebreaker.resumeSealable(sealable); @@ -153,7 +153,7 @@ contract DualGovernance is IGovernance, ConfigurationProvider { function tiebreakerScheduleProposal(uint256 proposalId) external { _tiebreaker.checkTiebreakerCommittee(msg.sender); - if (!_stateMachine.isTiebreak(CONFIG)) { + if (!_stateMachine.isDeadlock(CONFIG.getTiebreakConfig())) { revert NotTiebreak(); } TIMELOCK.schedule(proposalId); diff --git a/contracts/interfaces/IConfiguration.sol b/contracts/interfaces/IConfiguration.sol index 7d3f8535..9eabf6b6 100644 --- a/contracts/interfaces/IConfiguration.sol +++ b/contracts/interfaces/IConfiguration.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.23; import {Duration} from "../types/Duration.sol"; -import {DualGovernanceConfig} from "../libraries//DualGovernanceConfig.sol"; +import {TiebreakConfig, DualGovernanceConfig} from "../libraries//DualGovernanceConfig.sol"; interface IEscrowConfigration { function MIN_WITHDRAWALS_BATCH_SIZE() external view returns (uint256); @@ -43,19 +43,10 @@ interface IDualGovernanceConfiguration { function RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_B() external view returns (uint256); function RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_C() external view returns (uint256); - function sealableWithdrawalBlockers() external view returns (address[] memory); - - function getSignallingThresholdData() - external - view - returns ( - uint256 firstSealThreshold, - uint256 secondSealThreshold, - Duration signallingMinDuration, - Duration signallingMaxDuration - ); + function getSealableWithdrawalBlockers() external view returns (address[] memory); function getDualGovernanceConfig() external view returns (DualGovernanceConfig memory config); + function getTiebreakConfig() external view returns (TiebreakConfig memory config); } interface IConfiguration is diff --git a/contracts/libraries/DualGovernanceConfig.sol b/contracts/libraries/DualGovernanceConfig.sol index 3d8891a5..e12e1ef8 100644 --- a/contracts/libraries/DualGovernanceConfig.sol +++ b/contracts/libraries/DualGovernanceConfig.sol @@ -4,6 +4,11 @@ pragma solidity 0.8.23; import {Duration, Durations} from "../types/Duration.sol"; import {Timestamp, Timestamps} from "../types/Timestamp.sol"; +struct TiebreakConfig { + Duration tiebreakActivationTimeout; + address[] potentialDeadlockSealables; +} + struct DualGovernanceConfig { uint256 firstSealRageQuitSupport; uint256 secondSealRageQuitSupport; diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol index f43e44e4..eddda737 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -6,11 +6,10 @@ import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {IEscrow} from "../interfaces/IEscrow.sol"; import {ISealable} from "../interfaces/ISealable.sol"; -import {IDualGovernanceConfiguration} from "../interfaces/IConfiguration.sol"; import {Duration} from "../types/Duration.sol"; import {Timestamp, Timestamps} from "../types/Timestamp.sol"; -import {DualGovernanceConfig, DualGovernanceConfigUtils} from "./DualGovernanceConfig.sol"; +import {TiebreakConfig, DualGovernanceConfig, DualGovernanceConfigUtils} from "./DualGovernanceConfig.sol"; enum Status { Unset, @@ -142,20 +141,20 @@ library DualGovernanceStateMachine { return false; } - function isTiebreak(State storage self, IDualGovernanceConfiguration config) internal view returns (bool) { + function isDeadlock(State storage self, TiebreakConfig memory config) internal view returns (bool) { Status state = self.status; if (state == Status.Normal || state == Status.VetoCooldown) return false; // when the governance is locked for long period of time - if (Timestamps.now() >= config.TIE_BREAK_ACTIVATION_TIMEOUT().addTo(self.normalOrVetoCooldownExitedAt)) { + if (Timestamps.now() >= config.tiebreakActivationTimeout.addTo(self.normalOrVetoCooldownExitedAt)) { return true; } if (self.status != Status.RageQuit) return false; - address[] memory sealableWithdrawalBlockers = config.sealableWithdrawalBlockers(); - for (uint256 i = 0; i < sealableWithdrawalBlockers.length; ++i) { - if (ISealable(sealableWithdrawalBlockers[i]).isPaused()) return true; + uint256 potentialDeadlockSealablesCount = config.potentialDeadlockSealables.length; + for (uint256 i = 0; i < potentialDeadlockSealablesCount; ++i) { + if (ISealable(config.potentialDeadlockSealables[i]).isPaused()) return true; } return false; } From 6921bc6880fee3c8f3e4825e109a21a0e9013643 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 22 Jul 2024 04:46:44 +0400 Subject: [PATCH 3/4] DualGovernanceState.State -> DualGovernanceState.Context --- contracts/DualGovernance.sol | 50 +++---- contracts/libraries/DualGovernanceConfig.sol | 4 +- ...ateMachine.sol => DualGovernanceState.sol} | 139 +++++++++--------- test/utils/scenario-test-blueprint.sol | 32 ++-- 4 files changed, 111 insertions(+), 114 deletions(-) rename contracts/libraries/{DualGovernanceStateMachine.sol => DualGovernanceState.sol} (63%) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 3713e2d3..8cb0522a 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -11,15 +11,15 @@ import {ConfigurationProvider} from "./ConfigurationProvider.sol"; import {Proposers, Proposer} from "./libraries/Proposers.sol"; import {ExternalCall} from "./libraries/ExternalCalls.sol"; import {EmergencyProtection} from "./libraries/EmergencyProtection.sol"; -import {Status, DualGovernanceStateMachine} from "./libraries/DualGovernanceStateMachine.sol"; +import {State, DualGovernanceState} from "./libraries/DualGovernanceState.sol"; import {TiebreakerProtection} from "./libraries/TiebreakerProtection.sol"; contract DualGovernance is IGovernance, ConfigurationProvider { using Proposers for Proposers.State; using TiebreakerProtection for TiebreakerProtection.Tiebreaker; - using DualGovernanceStateMachine for DualGovernanceStateMachine.State; + using DualGovernanceState for DualGovernanceState.Context; - error NotTiebreak(); + error NotDeadlock(); error NotResealCommitttee(address account); error ProposalSubmissionBlocked(); error ProposalSchedulingBlocked(uint256 proposalId); @@ -28,7 +28,7 @@ contract DualGovernance is IGovernance, ConfigurationProvider { ITimelock public immutable TIMELOCK; Proposers.State internal _proposers; - DualGovernanceStateMachine.State internal _stateMachine; + DualGovernanceState.Context internal _state; EmergencyProtection.State internal _emergencyProtection; address internal _resealCommittee; IResealManager internal _resealManager; @@ -42,7 +42,7 @@ contract DualGovernance is IGovernance, ConfigurationProvider { ) ConfigurationProvider(config) { TIMELOCK = ITimelock(timelock); - _stateMachine.initialize(escrowMasterCopy); + _state.initialize(escrowMasterCopy); _proposers.register(adminProposer, CONFIG.ADMIN_EXECUTOR()); } @@ -52,8 +52,8 @@ contract DualGovernance is IGovernance, ConfigurationProvider { function submitProposal(ExternalCall[] calldata calls) external returns (uint256 proposalId) { _proposers.checkProposer(msg.sender); - _stateMachine.activateNextState(CONFIG.getDualGovernanceConfig()); - if (!_stateMachine.canSubmitProposal()) { + _state.activateNextState(CONFIG.getDualGovernanceConfig()); + if (!_state.canSubmitProposal()) { revert ProposalSubmissionBlocked(); } Proposer memory proposer = _proposers.get(msg.sender); @@ -61,10 +61,10 @@ contract DualGovernance is IGovernance, ConfigurationProvider { } function scheduleProposal(uint256 proposalId) external { - _stateMachine.activateNextState(CONFIG.getDualGovernanceConfig()); + _state.activateNextState(CONFIG.getDualGovernanceConfig()); ( /* id */ , /* status */, /* executor */, Timestamp submittedAt, /* scheduledAt */ ) = TIMELOCK.getProposalInfo(proposalId); - if (!_stateMachine.canScheduleProposal(submittedAt)) { + if (!_state.canScheduleProposal(submittedAt)) { revert ProposalSchedulingBlocked(proposalId); } TIMELOCK.schedule(proposalId); @@ -76,13 +76,13 @@ contract DualGovernance is IGovernance, ConfigurationProvider { } function canSubmitProposal() public view returns (bool) { - return _stateMachine.canSubmitProposal(); + return _state.canSubmitProposal(); } function canScheduleProposal(uint256 proposalId) external view returns (bool) { ( /* id */ , /* status */, /* executor */, Timestamp submittedAt, /* scheduledAt */ ) = TIMELOCK.getProposalInfo(proposalId); - return _stateMachine.canScheduleProposal(submittedAt) && TIMELOCK.canSchedule(proposalId); + return _state.canScheduleProposal(submittedAt) && TIMELOCK.canSchedule(proposalId); } // --- @@ -90,27 +90,27 @@ contract DualGovernance is IGovernance, ConfigurationProvider { // --- function getVetoSignallingEscrow() external view returns (address) { - return address(_stateMachine.signallingEscrow); + return address(_state.signallingEscrow); } function getRageQuitEscrow() external view returns (address) { - return address(_stateMachine.rageQuitEscrow); + return address(_state.rageQuitEscrow); } function activateNextState() external { - _stateMachine.activateNextState(CONFIG.getDualGovernanceConfig()); + _state.activateNextState(CONFIG.getDualGovernanceConfig()); } - function getCurrentStatus() external view returns (Status) { - return _stateMachine.getCurrentStatus(); + function getCurrentState() external view returns (State currentState) { + currentState = _state.getCurrentState(); } - function getCurrentState() external view returns (DualGovernanceStateMachine.State memory) { - return _stateMachine.getCurrentState(); + function getCurrentStateContext() external view returns (DualGovernanceState.Context memory) { + return _state.getCurrentContext(); } - function getDynamicTimelockDuration() external view returns (Duration) { - return _stateMachine.getDynamicTimelockDuration(CONFIG.getDualGovernanceConfig()); + function getDynamicDelayDuration() external view returns (Duration) { + return _state.getDynamicDelayDuration(CONFIG.getDualGovernanceConfig()); } // --- @@ -149,16 +149,16 @@ contract DualGovernance is IGovernance, ConfigurationProvider { function tiebreakerResumeSealable(address sealable) external { _tiebreaker.checkTiebreakerCommittee(msg.sender); - if (!_stateMachine.isDeadlock(CONFIG.getTiebreakConfig())) { - revert NotTiebreak(); + if (!_state.isDeadlock(CONFIG.getTiebreakConfig())) { + revert NotDeadlock(); } _tiebreaker.resumeSealable(sealable); } function tiebreakerScheduleProposal(uint256 proposalId) external { _tiebreaker.checkTiebreakerCommittee(msg.sender); - if (!_stateMachine.isDeadlock(CONFIG.getTiebreakConfig())) { - revert NotTiebreak(); + if (!_state.isDeadlock(CONFIG.getTiebreakConfig())) { + revert NotDeadlock(); } TIMELOCK.schedule(proposalId); } @@ -176,7 +176,7 @@ contract DualGovernance is IGovernance, ConfigurationProvider { if (msg.sender != _resealCommittee) { revert NotResealCommitttee(msg.sender); } - if (_stateMachine.getCurrentStatus() == Status.Normal) { + if (_state.getCurrentState() == State.Normal) { revert ResealIsNotAllowedInNormalState(); } _resealManager.reseal(sealables); diff --git a/contracts/libraries/DualGovernanceConfig.sol b/contracts/libraries/DualGovernanceConfig.sol index 55e9c040..71d1b850 100644 --- a/contracts/libraries/DualGovernanceConfig.sol +++ b/contracts/libraries/DualGovernanceConfig.sol @@ -51,7 +51,7 @@ library DualGovernanceConfigUtils { Timestamp vetoSignallingActivatedAt, uint256 rageQuitSupport ) internal view returns (bool) { - Duration dynamicTimelock = calcDynamicTimelockDuration(config, rageQuitSupport); + Duration dynamicTimelock = calcDynamicDelayDuration(config, rageQuitSupport); return Timestamps.now() > dynamicTimelock.addTo(vetoSignallingActivatedAt); } @@ -77,7 +77,7 @@ library DualGovernanceConfigUtils { return Timestamps.now() > config.vetoCooldownDuration.addTo(vetoCooldownEnteredAt); } - function calcDynamicTimelockDuration( + function calcDynamicDelayDuration( DualGovernanceConfig memory config, uint256 rageQuitSupport ) internal pure returns (Duration duration_) { diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceState.sol similarity index 63% rename from contracts/libraries/DualGovernanceStateMachine.sol rename to contracts/libraries/DualGovernanceState.sol index 9f61caa6..46995e6d 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceState.sol @@ -11,7 +11,7 @@ import {Duration} from "../types/Duration.sol"; import {Timestamp, Timestamps} from "../types/Timestamp.sol"; import {TiebreakConfig, DualGovernanceConfig, DualGovernanceConfigUtils} from "./DualGovernanceConfig.sol"; -enum Status { +enum State { Unset, Normal, VetoSignalling, @@ -20,15 +20,14 @@ enum Status { RageQuit } -library DualGovernanceStateMachine { +library DualGovernanceState { using DualGovernanceConfigUtils for DualGovernanceConfig; - using DualGovernanceStateTransitions for State; - struct State { + struct Context { /// /// @dev slot 0: [0..7] /// The current state of the Dual Governance FSM - Status status; + State state; /// /// @dev slot 0: [8..47] /// The timestamp when the Dual Governance FSM entered the current state @@ -62,43 +61,43 @@ library DualGovernanceStateMachine { error AlreadyInitialized(); event NewSignallingEscrowDeployed(address indexed escrow); - event DualGovernanceStateChanged(Status from, Status to, State state); + event DualGovernanceStateChanged(State from, State to, Context state); - function initialize(State storage self, address escrowMasterCopy) internal { - if (self.status != Status.Unset) { + function initialize(Context storage self, address escrowMasterCopy) internal { + if (self.state != State.Unset) { revert AlreadyInitialized(); } - self.status = Status.Normal; + self.state = State.Normal; self.enteredAt = Timestamps.now(); _deployNewSignallingEscrow(self, escrowMasterCopy); - emit DualGovernanceStateChanged(Status.Unset, Status.Normal, self); + emit DualGovernanceStateChanged(State.Unset, State.Normal, self); } - function activateNextState(State storage self, DualGovernanceConfig memory config) internal { - (Status currentStatus, Status newStatus) = self.getStateTransition(config); + function activateNextState(Context storage self, DualGovernanceConfig memory config) internal { + (State currentStatus, State newStatus) = DualGovernanceStateTransitions.getStateTransition(self, config); if (currentStatus == newStatus) { return; } - self.status = newStatus; + self.state = newStatus; self.enteredAt = Timestamps.now(); - if (currentStatus == Status.Normal || currentStatus == Status.VetoCooldown) { + if (currentStatus == State.Normal || currentStatus == State.VetoCooldown) { self.normalOrVetoCooldownExitedAt = Timestamps.now(); } - if (newStatus == Status.Normal && self.rageQuitRound != 0) { + if (newStatus == State.Normal && self.rageQuitRound != 0) { self.rageQuitRound = 0; - } else if (newStatus == Status.VetoSignalling) { - if (currentStatus == Status.VetoSignallingDeactivation) { + } else if (newStatus == State.VetoSignalling) { + if (currentStatus == State.VetoSignallingDeactivation) { self.vetoSignallingReactivationTime = Timestamps.now(); } else { self.vetoSignallingActivatedAt = Timestamps.now(); } - } else if (newStatus == Status.RageQuit) { + } else if (newStatus == State.RageQuit) { IEscrow signallingEscrow = self.signallingEscrow; uint256 rageQuitRound = Math.min(self.rageQuitRound + 1, type(uint8).max); self.rageQuitRound = uint8(rageQuitRound); @@ -112,45 +111,45 @@ library DualGovernanceStateMachine { emit DualGovernanceStateChanged(currentStatus, newStatus, self); } - function getCurrentState(State storage self) internal pure returns (State memory) { + function getCurrentContext(Context storage self) internal pure returns (Context memory) { return self; } - function getCurrentStatus(State storage self) internal view returns (Status) { - return self.status; + function getCurrentState(Context storage self) internal view returns (State) { + return self.state; } - function getDynamicTimelockDuration( - State storage self, + function getDynamicDelayDuration( + Context storage self, DualGovernanceConfig memory config ) internal view returns (Duration) { - return config.calcDynamicTimelockDuration(self.signallingEscrow.getRageQuitSupport()); + return config.calcDynamicDelayDuration(self.signallingEscrow.getRageQuitSupport()); } - function canSubmitProposal(State storage self) internal view returns (bool) { - Status state = self.status; - return state != Status.VetoSignallingDeactivation && state != Status.VetoCooldown; + function canSubmitProposal(Context storage self) internal view returns (bool) { + State state = self.state; + return state != State.VetoSignallingDeactivation && state != State.VetoCooldown; } - function canScheduleProposal(State storage self, Timestamp proposalSubmissionTime) internal view returns (bool) { - Status state = self.status; - if (state == Status.Normal) return true; - if (state == Status.VetoCooldown) { + function canScheduleProposal(Context storage self, Timestamp proposalSubmissionTime) internal view returns (bool) { + State state = self.state; + if (state == State.Normal) return true; + if (state == State.VetoCooldown) { return proposalSubmissionTime <= self.vetoSignallingActivatedAt; } return false; } - function isDeadlock(State storage self, TiebreakConfig memory config) internal view returns (bool) { - Status state = self.status; - if (state == Status.Normal || state == Status.VetoCooldown) return false; + function isDeadlock(Context storage self, TiebreakConfig memory config) internal view returns (bool) { + State state = self.state; + if (state == State.Normal || state == State.VetoCooldown) return false; // when the governance is locked for long period of time if (Timestamps.now() >= config.tiebreakActivationTimeout.addTo(self.normalOrVetoCooldownExitedAt)) { return true; } - if (self.status != Status.RageQuit) return false; + if (self.state != State.RageQuit) return false; uint256 potentialDeadlockSealablesCount = config.potentialDeadlockSealables.length; for (uint256 i = 0; i < potentialDeadlockSealablesCount; ++i) { @@ -159,7 +158,7 @@ library DualGovernanceStateMachine { return false; } - function _deployNewSignallingEscrow(State storage self, address escrowMasterCopy) private { + function _deployNewSignallingEscrow(Context storage self, address escrowMasterCopy) private { IEscrow clone = IEscrow(Clones.clone(escrowMasterCopy)); clone.initialize(address(this)); self.signallingEscrow = clone; @@ -171,19 +170,19 @@ library DualGovernanceStateTransitions { using DualGovernanceConfigUtils for DualGovernanceConfig; function getStateTransition( - DualGovernanceStateMachine.State storage self, + DualGovernanceState.Context storage self, DualGovernanceConfig memory config - ) internal view returns (Status currentStatus, Status nextStatus) { - currentStatus = self.status; - if (currentStatus == Status.Normal) { + ) internal view returns (State currentStatus, State nextStatus) { + currentStatus = self.state; + if (currentStatus == State.Normal) { nextStatus = _fromNormalState(self, config); - } else if (currentStatus == Status.VetoSignalling) { + } else if (currentStatus == State.VetoSignalling) { nextStatus = _fromVetoSignallingState(self, config); - } else if (currentStatus == Status.VetoSignallingDeactivation) { + } else if (currentStatus == State.VetoSignallingDeactivation) { nextStatus = _fromVetoSignallingDeactivationState(self, config); - } else if (currentStatus == Status.VetoCooldown) { + } else if (currentStatus == State.VetoCooldown) { nextStatus = _fromVetoCooldownState(self, config); - } else if (currentStatus == Status.RageQuit) { + } else if (currentStatus == State.RageQuit) { nextStatus = _fromRageQuitState(self, config); } else { assert(false); @@ -191,75 +190,75 @@ library DualGovernanceStateTransitions { } function _fromNormalState( - DualGovernanceStateMachine.State storage self, + DualGovernanceState.Context storage self, DualGovernanceConfig memory config - ) private view returns (Status) { + ) private view returns (State) { return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) - ? Status.VetoSignalling - : Status.Normal; + ? State.VetoSignalling + : State.Normal; } function _fromVetoSignallingState( - DualGovernanceStateMachine.State storage self, + DualGovernanceState.Context storage self, DualGovernanceConfig memory config - ) private view returns (Status) { + ) private view returns (State) { uint256 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); if (!config.isDynamicTimelockDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { - return Status.VetoSignalling; + return State.VetoSignalling; } if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) { - return Status.RageQuit; + return State.RageQuit; } return config.isVetoSignallingReactivationDurationPassed(self.vetoSignallingReactivationTime) - ? Status.VetoSignallingDeactivation - : Status.VetoSignalling; + ? State.VetoSignallingDeactivation + : State.VetoSignalling; } function _fromVetoSignallingDeactivationState( - DualGovernanceStateMachine.State storage self, + DualGovernanceState.Context storage self, DualGovernanceConfig memory config - ) private view returns (Status) { + ) private view returns (State) { uint256 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); if (!config.isDynamicTimelockDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { - return Status.VetoSignalling; + return State.VetoSignalling; } if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) { - return Status.RageQuit; + return State.RageQuit; } if (config.isVetoSignallingDeactivationMaxDurationPassed(self.enteredAt)) { - return Status.VetoCooldown; + return State.VetoCooldown; } - return Status.VetoSignallingDeactivation; + return State.VetoSignallingDeactivation; } function _fromVetoCooldownState( - DualGovernanceStateMachine.State storage self, + DualGovernanceState.Context storage self, DualGovernanceConfig memory config - ) private view returns (Status) { + ) private view returns (State) { if (!config.isVetoCooldownDurationPassed(self.enteredAt)) { - return Status.VetoCooldown; + return State.VetoCooldown; } return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) - ? Status.VetoSignalling - : Status.Normal; + ? State.VetoSignalling + : State.Normal; } function _fromRageQuitState( - DualGovernanceStateMachine.State storage self, + DualGovernanceState.Context storage self, DualGovernanceConfig memory config - ) private view returns (Status) { + ) private view returns (State) { if (!self.rageQuitEscrow.isRageQuitFinalized()) { - return Status.RageQuit; + return State.RageQuit; } return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) - ? Status.VetoSignalling - : Status.VetoCooldown; + ? State.VetoSignalling + : State.VetoCooldown; } } diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index f07d8a4e..bb7e0ba9 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -30,9 +30,7 @@ import { } from "contracts/EmergencyProtectedTimelock.sol"; import {SingleGovernance, IGovernance} from "contracts/SingleGovernance.sol"; -import { - DualGovernance, Status as DualGovernanceStatus, DualGovernanceStateMachine -} from "contracts/DualGovernance.sol"; +import {DualGovernance, State as DGState, DualGovernanceState} from "contracts/DualGovernance.sol"; import {ExternalCall} from "contracts/libraries/ExternalCalls.sol"; @@ -127,11 +125,11 @@ contract ScenarioTestBlueprint is Test { view returns (bool isActive, uint256 duration, uint256 activatedAt, uint256 enteredAt) { - DualGovernanceStateMachine.State memory state = _dualGovernance.getCurrentState(); - isActive = state.status == DualGovernanceStatus.VetoSignalling; - duration = _dualGovernance.getDynamicTimelockDuration().toSeconds(); - enteredAt = state.enteredAt.toSeconds(); - activatedAt = state.vetoSignallingActivatedAt.toSeconds(); + DualGovernanceState.Context memory stateContext = _dualGovernance.getCurrentStateContext(); + isActive = stateContext.state == DGState.VetoSignalling; + duration = _dualGovernance.getDynamicDelayDuration().toSeconds(); + enteredAt = stateContext.enteredAt.toSeconds(); + activatedAt = stateContext.vetoSignallingActivatedAt.toSeconds(); } function _getVetoSignallingDeactivationState() @@ -139,10 +137,10 @@ contract ScenarioTestBlueprint is Test { view returns (bool isActive, uint256 duration, uint256 enteredAt) { - DualGovernanceStateMachine.State memory state = _dualGovernance.getCurrentState(); - isActive = state.status == DualGovernanceStatus.VetoSignallingDeactivation; + DualGovernanceState.Context memory stateContext = _dualGovernance.getCurrentStateContext(); + isActive = stateContext.state == DGState.VetoSignallingDeactivation; duration = _dualGovernance.CONFIG().VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().toSeconds(); - enteredAt = state.enteredAt.toSeconds(); + enteredAt = stateContext.enteredAt.toSeconds(); } // --- @@ -443,23 +441,23 @@ contract ScenarioTestBlueprint is Test { } function _assertNormalState() internal { - assertEq(_dualGovernance.getCurrentStatus(), DualGovernanceStatus.Normal); + assertEq(_dualGovernance.getCurrentState(), DGState.Normal); } function _assertVetoSignalingState() internal { - assertEq(_dualGovernance.getCurrentStatus(), DualGovernanceStatus.VetoSignalling); + assertEq(_dualGovernance.getCurrentState(), DGState.VetoSignalling); } function _assertVetoSignalingDeactivationState() internal { - assertEq(_dualGovernance.getCurrentStatus(), DualGovernanceStatus.VetoSignallingDeactivation); + assertEq(_dualGovernance.getCurrentState(), DGState.VetoSignallingDeactivation); } function _assertRageQuitState() internal { - assertEq(_dualGovernance.getCurrentStatus(), DualGovernanceStatus.RageQuit); + assertEq(_dualGovernance.getCurrentState(), DGState.RageQuit); } function _assertVetoCooldownState() internal { - assertEq(_dualGovernance.getCurrentStatus(), DualGovernanceStatus.VetoCooldown); + assertEq(_dualGovernance.getCurrentState(), DGState.VetoCooldown); } function _assertNoTargetMockCalls() internal { @@ -771,7 +769,7 @@ contract ScenarioTestBlueprint is Test { assertEq(uint256(a), uint256(b), message); } - function assertEq(DualGovernanceStatus a, DualGovernanceStatus b) internal { + function assertEq(DGState a, DGState b) internal { assertEq(uint256(a), uint256(b)); } From 1f5e0888ba7eb5645a4a60e77dd14762b3e64e4a Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 1 Aug 2024 20:26:01 +0400 Subject: [PATCH 4/4] DualGovernanceState -> DualGovernanceStateMachine --- contracts/Configuration.sol | 4 +- contracts/DualGovernance.sol | 40 ++-- contracts/interfaces/IConfiguration.sol | 4 +- contracts/libraries/DualGovernanceConfig.sol | 121 ----------- ...ate.sol => DualGovernanceStateMachine.sol} | 190 ++++++++++++++---- test/utils/scenario-test-blueprint.sol | 6 +- 6 files changed, 179 insertions(+), 186 deletions(-) delete mode 100644 contracts/libraries/DualGovernanceConfig.sol rename contracts/libraries/{DualGovernanceState.sol => DualGovernanceStateMachine.sol} (53%) diff --git a/contracts/Configuration.sol b/contracts/Configuration.sol index adec8774..a2f95048 100644 --- a/contracts/Configuration.sol +++ b/contracts/Configuration.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.26; import {Durations, Duration} from "./types/Duration.sol"; -import {IConfiguration, DualGovernanceConfig, TiebreakConfig} from "./interfaces/IConfiguration.sol"; +import {IConfiguration, DualGovernanceStateMachine, TiebreakConfig} from "./interfaces/IConfiguration.sol"; uint256 constant PERCENT = 10 ** 16; @@ -82,7 +82,7 @@ contract Configuration is IConfiguration { if (SEALABLES_COUNT > 4) sealableWithdrawalBlockers_[4] = SEALABLE_4; } - function getDualGovernanceConfig() external view returns (DualGovernanceConfig memory config) { + function getDualGovernanceConfig() external view returns (DualGovernanceStateMachine.Config memory config) { config.firstSealRageQuitSupport = FIRST_SEAL_RAGE_QUIT_SUPPORT; config.secondSealRageQuitSupport = SECOND_SEAL_RAGE_QUIT_SUPPORT; config.dynamicTimelockMinDuration = DYNAMIC_TIMELOCK_MIN_DURATION; diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 8cb0522a..25df297c 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -11,13 +11,13 @@ import {ConfigurationProvider} from "./ConfigurationProvider.sol"; import {Proposers, Proposer} from "./libraries/Proposers.sol"; import {ExternalCall} from "./libraries/ExternalCalls.sol"; import {EmergencyProtection} from "./libraries/EmergencyProtection.sol"; -import {State, DualGovernanceState} from "./libraries/DualGovernanceState.sol"; +import {State, DualGovernanceStateMachine} from "./libraries/DualGovernanceStateMachine.sol"; import {TiebreakerProtection} from "./libraries/TiebreakerProtection.sol"; contract DualGovernance is IGovernance, ConfigurationProvider { using Proposers for Proposers.State; using TiebreakerProtection for TiebreakerProtection.Tiebreaker; - using DualGovernanceState for DualGovernanceState.Context; + using DualGovernanceStateMachine for DualGovernanceStateMachine.Context; error NotDeadlock(); error NotResealCommitttee(address account); @@ -28,7 +28,7 @@ contract DualGovernance is IGovernance, ConfigurationProvider { ITimelock public immutable TIMELOCK; Proposers.State internal _proposers; - DualGovernanceState.Context internal _state; + DualGovernanceStateMachine.Context internal _stateMachine; EmergencyProtection.State internal _emergencyProtection; address internal _resealCommittee; IResealManager internal _resealManager; @@ -42,7 +42,7 @@ contract DualGovernance is IGovernance, ConfigurationProvider { ) ConfigurationProvider(config) { TIMELOCK = ITimelock(timelock); - _state.initialize(escrowMasterCopy); + _stateMachine.initialize(escrowMasterCopy); _proposers.register(adminProposer, CONFIG.ADMIN_EXECUTOR()); } @@ -52,8 +52,8 @@ contract DualGovernance is IGovernance, ConfigurationProvider { function submitProposal(ExternalCall[] calldata calls) external returns (uint256 proposalId) { _proposers.checkProposer(msg.sender); - _state.activateNextState(CONFIG.getDualGovernanceConfig()); - if (!_state.canSubmitProposal()) { + _stateMachine.activateNextState(CONFIG.getDualGovernanceConfig()); + if (!_stateMachine.canSubmitProposal()) { revert ProposalSubmissionBlocked(); } Proposer memory proposer = _proposers.get(msg.sender); @@ -61,10 +61,10 @@ contract DualGovernance is IGovernance, ConfigurationProvider { } function scheduleProposal(uint256 proposalId) external { - _state.activateNextState(CONFIG.getDualGovernanceConfig()); + _stateMachine.activateNextState(CONFIG.getDualGovernanceConfig()); ( /* id */ , /* status */, /* executor */, Timestamp submittedAt, /* scheduledAt */ ) = TIMELOCK.getProposalInfo(proposalId); - if (!_state.canScheduleProposal(submittedAt)) { + if (!_stateMachine.canScheduleProposal(submittedAt)) { revert ProposalSchedulingBlocked(proposalId); } TIMELOCK.schedule(proposalId); @@ -76,13 +76,13 @@ contract DualGovernance is IGovernance, ConfigurationProvider { } function canSubmitProposal() public view returns (bool) { - return _state.canSubmitProposal(); + return _stateMachine.canSubmitProposal(); } function canScheduleProposal(uint256 proposalId) external view returns (bool) { ( /* id */ , /* status */, /* executor */, Timestamp submittedAt, /* scheduledAt */ ) = TIMELOCK.getProposalInfo(proposalId); - return _state.canScheduleProposal(submittedAt) && TIMELOCK.canSchedule(proposalId); + return _stateMachine.canScheduleProposal(submittedAt) && TIMELOCK.canSchedule(proposalId); } // --- @@ -90,27 +90,27 @@ contract DualGovernance is IGovernance, ConfigurationProvider { // --- function getVetoSignallingEscrow() external view returns (address) { - return address(_state.signallingEscrow); + return address(_stateMachine.signallingEscrow); } function getRageQuitEscrow() external view returns (address) { - return address(_state.rageQuitEscrow); + return address(_stateMachine.rageQuitEscrow); } function activateNextState() external { - _state.activateNextState(CONFIG.getDualGovernanceConfig()); + _stateMachine.activateNextState(CONFIG.getDualGovernanceConfig()); } function getCurrentState() external view returns (State currentState) { - currentState = _state.getCurrentState(); + currentState = _stateMachine.getCurrentState(); } - function getCurrentStateContext() external view returns (DualGovernanceState.Context memory) { - return _state.getCurrentContext(); + function getCurrentStateContext() external view returns (DualGovernanceStateMachine.Context memory) { + return _stateMachine.getCurrentContext(); } function getDynamicDelayDuration() external view returns (Duration) { - return _state.getDynamicDelayDuration(CONFIG.getDualGovernanceConfig()); + return _stateMachine.getDynamicDelayDuration(CONFIG.getDualGovernanceConfig()); } // --- @@ -149,7 +149,7 @@ contract DualGovernance is IGovernance, ConfigurationProvider { function tiebreakerResumeSealable(address sealable) external { _tiebreaker.checkTiebreakerCommittee(msg.sender); - if (!_state.isDeadlock(CONFIG.getTiebreakConfig())) { + if (!_stateMachine.isDeadlock(CONFIG.getTiebreakConfig())) { revert NotDeadlock(); } _tiebreaker.resumeSealable(sealable); @@ -157,7 +157,7 @@ contract DualGovernance is IGovernance, ConfigurationProvider { function tiebreakerScheduleProposal(uint256 proposalId) external { _tiebreaker.checkTiebreakerCommittee(msg.sender); - if (!_state.isDeadlock(CONFIG.getTiebreakConfig())) { + if (!_stateMachine.isDeadlock(CONFIG.getTiebreakConfig())) { revert NotDeadlock(); } TIMELOCK.schedule(proposalId); @@ -176,7 +176,7 @@ contract DualGovernance is IGovernance, ConfigurationProvider { if (msg.sender != _resealCommittee) { revert NotResealCommitttee(msg.sender); } - if (_state.getCurrentState() == State.Normal) { + if (_stateMachine.getCurrentState() == State.Normal) { revert ResealIsNotAllowedInNormalState(); } _resealManager.reseal(sealables); diff --git a/contracts/interfaces/IConfiguration.sol b/contracts/interfaces/IConfiguration.sol index bc5de3c6..c097079b 100644 --- a/contracts/interfaces/IConfiguration.sol +++ b/contracts/interfaces/IConfiguration.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.26; import {Duration} from "../types/Duration.sol"; -import {TiebreakConfig, DualGovernanceConfig} from "../libraries//DualGovernanceConfig.sol"; +import {DualGovernanceStateMachine, TiebreakConfig} from "../libraries/DualGovernanceStateMachine.sol"; interface IEscrowConfigration { function MIN_WITHDRAWALS_BATCH_SIZE() external view returns (uint256); @@ -45,7 +45,7 @@ interface IDualGovernanceConfiguration { function getSealableWithdrawalBlockers() external view returns (address[] memory); - function getDualGovernanceConfig() external view returns (DualGovernanceConfig memory config); + function getDualGovernanceConfig() external view returns (DualGovernanceStateMachine.Config memory config); function getTiebreakConfig() external view returns (TiebreakConfig memory config); } diff --git a/contracts/libraries/DualGovernanceConfig.sol b/contracts/libraries/DualGovernanceConfig.sol deleted file mode 100644 index 71d1b850..00000000 --- a/contracts/libraries/DualGovernanceConfig.sol +++ /dev/null @@ -1,121 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {Duration, Durations} from "../types/Duration.sol"; -import {Timestamp, Timestamps} from "../types/Timestamp.sol"; - -struct TiebreakConfig { - Duration tiebreakActivationTimeout; - address[] potentialDeadlockSealables; -} - -struct DualGovernanceConfig { - uint256 firstSealRageQuitSupport; - uint256 secondSealRageQuitSupport; - Duration dynamicTimelockMaxDuration; - Duration dynamicTimelockMinDuration; - Duration vetoSignallingMinActiveDuration; - Duration vetoSignallingDeactivationMaxDuration; - Duration vetoCooldownDuration; - Duration rageQuitExtraTimelock; - Duration rageQuitExtensionDelay; - Duration rageQuitEthWithdrawalsMinTimelock; - uint256 rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber; - uint256[3] rageQuitEthWithdrawalsTimelockGrowthCoeffs; -} - -library DualGovernanceConfigUtils { - function isFirstSealRageQuitSupportCrossed( - DualGovernanceConfig memory config, - uint256 rageQuitSupport - ) internal pure returns (bool) { - return rageQuitSupport > config.firstSealRageQuitSupport; - } - - function isSecondSealRageQuitSupportCrossed( - DualGovernanceConfig memory config, - uint256 rageQuitSupport - ) internal pure returns (bool) { - return rageQuitSupport > config.secondSealRageQuitSupport; - } - - function isDynamicTimelockMaxDurationPassed( - DualGovernanceConfig memory config, - Timestamp vetoSignallingActivatedAt - ) internal view returns (bool) { - return Timestamps.now() > config.dynamicTimelockMaxDuration.addTo(vetoSignallingActivatedAt); - } - - function isDynamicTimelockDurationPassed( - DualGovernanceConfig memory config, - Timestamp vetoSignallingActivatedAt, - uint256 rageQuitSupport - ) internal view returns (bool) { - Duration dynamicTimelock = calcDynamicDelayDuration(config, rageQuitSupport); - return Timestamps.now() > dynamicTimelock.addTo(vetoSignallingActivatedAt); - } - - function isVetoSignallingReactivationDurationPassed( - DualGovernanceConfig memory config, - Timestamp vetoSignallingReactivationTime - ) internal view returns (bool) { - return Timestamps.now() > config.vetoSignallingMinActiveDuration.addTo(vetoSignallingReactivationTime); - } - - function isVetoSignallingDeactivationMaxDurationPassed( - DualGovernanceConfig memory config, - Timestamp vetoSignallingDeactivationEnteredAt - ) internal view returns (bool) { - return - Timestamps.now() > config.vetoSignallingDeactivationMaxDuration.addTo(vetoSignallingDeactivationEnteredAt); - } - - function isVetoCooldownDurationPassed( - DualGovernanceConfig memory config, - Timestamp vetoCooldownEnteredAt - ) internal view returns (bool) { - return Timestamps.now() > config.vetoCooldownDuration.addTo(vetoCooldownEnteredAt); - } - - function calcDynamicDelayDuration( - DualGovernanceConfig memory config, - uint256 rageQuitSupport - ) internal pure returns (Duration duration_) { - uint256 firstSealRageQuitSupport = config.firstSealRageQuitSupport; - uint256 secondSealRageQuitSupport = config.secondSealRageQuitSupport; - Duration dynamicTimelockMinDuration = config.dynamicTimelockMinDuration; - Duration dynamicTimelockMaxDuration = config.dynamicTimelockMaxDuration; - - if (rageQuitSupport < firstSealRageQuitSupport) { - return Durations.ZERO; - } - - if (rageQuitSupport >= secondSealRageQuitSupport) { - return dynamicTimelockMaxDuration; - } - - duration_ = dynamicTimelockMinDuration - + Durations.from( - (rageQuitSupport - firstSealRageQuitSupport) - * (dynamicTimelockMaxDuration - dynamicTimelockMinDuration).toSeconds() - / (secondSealRageQuitSupport - firstSealRageQuitSupport) - ); - } - - function calcRageQuitWithdrawalsTimelock( - DualGovernanceConfig memory config, - uint256 rageQuitRound - ) internal pure returns (Duration) { - if (rageQuitRound < config.rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber) { - return config.rageQuitEthWithdrawalsMinTimelock; - } - return config.rageQuitEthWithdrawalsMinTimelock - + Durations.from( - ( - config.rageQuitEthWithdrawalsTimelockGrowthCoeffs[0] * rageQuitRound * rageQuitRound - + config.rageQuitEthWithdrawalsTimelockGrowthCoeffs[1] * rageQuitRound - + config.rageQuitEthWithdrawalsTimelockGrowthCoeffs[2] - ) / 10 ** 18 - ); // TODO: rewrite in a prettier way - } -} diff --git a/contracts/libraries/DualGovernanceState.sol b/contracts/libraries/DualGovernanceStateMachine.sol similarity index 53% rename from contracts/libraries/DualGovernanceState.sol rename to contracts/libraries/DualGovernanceStateMachine.sol index 46995e6d..77a24323 100644 --- a/contracts/libraries/DualGovernanceState.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -7,9 +7,8 @@ import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {IEscrow} from "../interfaces/IEscrow.sol"; import {ISealable} from "../interfaces/ISealable.sol"; -import {Duration} from "../types/Duration.sol"; +import {Duration, Durations} from "../types/Duration.sol"; import {Timestamp, Timestamps} from "../types/Timestamp.sol"; -import {TiebreakConfig, DualGovernanceConfig, DualGovernanceConfigUtils} from "./DualGovernanceConfig.sol"; enum State { Unset, @@ -20,8 +19,27 @@ enum State { RageQuit } -library DualGovernanceState { - using DualGovernanceConfigUtils for DualGovernanceConfig; +struct TiebreakConfig { + Duration tiebreakActivationTimeout; + address[] potentialDeadlockSealables; +} + +library DualGovernanceStateMachine { + using DualGovernanceStateMachineConfig for Config; + + struct Config { + uint256 firstSealRageQuitSupport; + uint256 secondSealRageQuitSupport; + Duration dynamicTimelockMaxDuration; + Duration dynamicTimelockMinDuration; + Duration vetoSignallingMinActiveDuration; + Duration vetoSignallingDeactivationMaxDuration; + Duration vetoCooldownDuration; + Duration rageQuitExtensionDelay; + Duration rageQuitEthWithdrawalsMinTimelock; + uint256 rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber; + uint256[3] rageQuitEthWithdrawalsTimelockGrowthCoeffs; + } struct Context { /// @@ -75,29 +93,29 @@ library DualGovernanceState { emit DualGovernanceStateChanged(State.Unset, State.Normal, self); } - function activateNextState(Context storage self, DualGovernanceConfig memory config) internal { - (State currentStatus, State newStatus) = DualGovernanceStateTransitions.getStateTransition(self, config); + function activateNextState(Context storage self, Config memory config) internal { + (State currentState, State newState) = DualGovernanceStateTransitions.getStateTransition(self, config); - if (currentStatus == newStatus) { + if (currentState == newState) { return; } - self.state = newStatus; + self.state = newState; self.enteredAt = Timestamps.now(); - if (currentStatus == State.Normal || currentStatus == State.VetoCooldown) { + if (currentState == State.Normal || currentState == State.VetoCooldown) { self.normalOrVetoCooldownExitedAt = Timestamps.now(); } - if (newStatus == State.Normal && self.rageQuitRound != 0) { + if (newState == State.Normal && self.rageQuitRound != 0) { self.rageQuitRound = 0; - } else if (newStatus == State.VetoSignalling) { - if (currentStatus == State.VetoSignallingDeactivation) { + } else if (newState == State.VetoSignalling) { + if (currentState == State.VetoSignallingDeactivation) { self.vetoSignallingReactivationTime = Timestamps.now(); } else { self.vetoSignallingActivatedAt = Timestamps.now(); } - } else if (newStatus == State.RageQuit) { + } else if (newState == State.RageQuit) { IEscrow signallingEscrow = self.signallingEscrow; uint256 rageQuitRound = Math.min(self.rageQuitRound + 1, type(uint8).max); self.rageQuitRound = uint8(rageQuitRound); @@ -108,7 +126,7 @@ library DualGovernanceState { _deployNewSignallingEscrow(self, signallingEscrow.MASTER_COPY()); } - emit DualGovernanceStateChanged(currentStatus, newStatus, self); + emit DualGovernanceStateChanged(currentState, newState, self); } function getCurrentContext(Context storage self) internal pure returns (Context memory) { @@ -119,10 +137,11 @@ library DualGovernanceState { return self.state; } - function getDynamicDelayDuration( - Context storage self, - DualGovernanceConfig memory config - ) internal view returns (Duration) { + function getNormalOrVetoCooldownStateExitedAt(Context storage self) internal view returns (Timestamp) { + return self.normalOrVetoCooldownExitedAt; + } + + function getDynamicDelayDuration(Context storage self, Config memory config) internal view returns (Duration) { return config.calcDynamicDelayDuration(self.signallingEscrow.getRageQuitSupport()); } @@ -167,22 +186,22 @@ library DualGovernanceState { } library DualGovernanceStateTransitions { - using DualGovernanceConfigUtils for DualGovernanceConfig; + using DualGovernanceStateMachineConfig for DualGovernanceStateMachine.Config; function getStateTransition( - DualGovernanceState.Context storage self, - DualGovernanceConfig memory config - ) internal view returns (State currentStatus, State nextStatus) { - currentStatus = self.state; - if (currentStatus == State.Normal) { + DualGovernanceStateMachine.Context storage self, + DualGovernanceStateMachine.Config memory config + ) internal view returns (State currentState, State nextStatus) { + currentState = self.state; + if (currentState == State.Normal) { nextStatus = _fromNormalState(self, config); - } else if (currentStatus == State.VetoSignalling) { + } else if (currentState == State.VetoSignalling) { nextStatus = _fromVetoSignallingState(self, config); - } else if (currentStatus == State.VetoSignallingDeactivation) { + } else if (currentState == State.VetoSignallingDeactivation) { nextStatus = _fromVetoSignallingDeactivationState(self, config); - } else if (currentStatus == State.VetoCooldown) { + } else if (currentState == State.VetoCooldown) { nextStatus = _fromVetoCooldownState(self, config); - } else if (currentStatus == State.RageQuit) { + } else if (currentState == State.RageQuit) { nextStatus = _fromRageQuitState(self, config); } else { assert(false); @@ -190,8 +209,8 @@ library DualGovernanceStateTransitions { } function _fromNormalState( - DualGovernanceState.Context storage self, - DualGovernanceConfig memory config + DualGovernanceStateMachine.Context storage self, + DualGovernanceStateMachine.Config memory config ) private view returns (State) { return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) ? State.VetoSignalling @@ -199,8 +218,8 @@ library DualGovernanceStateTransitions { } function _fromVetoSignallingState( - DualGovernanceState.Context storage self, - DualGovernanceConfig memory config + DualGovernanceStateMachine.Context storage self, + DualGovernanceStateMachine.Config memory config ) private view returns (State) { uint256 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); @@ -218,8 +237,8 @@ library DualGovernanceStateTransitions { } function _fromVetoSignallingDeactivationState( - DualGovernanceState.Context storage self, - DualGovernanceConfig memory config + DualGovernanceStateMachine.Context storage self, + DualGovernanceStateMachine.Config memory config ) private view returns (State) { uint256 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); @@ -239,8 +258,8 @@ library DualGovernanceStateTransitions { } function _fromVetoCooldownState( - DualGovernanceState.Context storage self, - DualGovernanceConfig memory config + DualGovernanceStateMachine.Context storage self, + DualGovernanceStateMachine.Config memory config ) private view returns (State) { if (!config.isVetoCooldownDurationPassed(self.enteredAt)) { return State.VetoCooldown; @@ -251,8 +270,8 @@ library DualGovernanceStateTransitions { } function _fromRageQuitState( - DualGovernanceState.Context storage self, - DualGovernanceConfig memory config + DualGovernanceStateMachine.Context storage self, + DualGovernanceStateMachine.Config memory config ) private view returns (State) { if (!self.rageQuitEscrow.isRageQuitFinalized()) { return State.RageQuit; @@ -262,3 +281,98 @@ library DualGovernanceStateTransitions { : State.VetoCooldown; } } + +library DualGovernanceStateMachineConfig { + function isFirstSealRageQuitSupportCrossed( + DualGovernanceStateMachine.Config memory self, + uint256 rageQuitSupport + ) internal pure returns (bool) { + return rageQuitSupport > self.firstSealRageQuitSupport; + } + + function isSecondSealRageQuitSupportCrossed( + DualGovernanceStateMachine.Config memory self, + uint256 rageQuitSupport + ) internal pure returns (bool) { + return rageQuitSupport > self.secondSealRageQuitSupport; + } + + function isDynamicTimelockMaxDurationPassed( + DualGovernanceStateMachine.Config memory self, + Timestamp vetoSignallingActivatedAt + ) internal view returns (bool) { + return Timestamps.now() > self.dynamicTimelockMaxDuration.addTo(vetoSignallingActivatedAt); + } + + function isDynamicTimelockDurationPassed( + DualGovernanceStateMachine.Config memory self, + Timestamp vetoSignallingActivatedAt, + uint256 rageQuitSupport + ) internal view returns (bool) { + Duration dynamicTimelock = calcDynamicDelayDuration(self, rageQuitSupport); + return Timestamps.now() > dynamicTimelock.addTo(vetoSignallingActivatedAt); + } + + function isVetoSignallingReactivationDurationPassed( + DualGovernanceStateMachine.Config memory self, + Timestamp vetoSignallingReactivationTime + ) internal view returns (bool) { + return Timestamps.now() > self.vetoSignallingMinActiveDuration.addTo(vetoSignallingReactivationTime); + } + + function isVetoSignallingDeactivationMaxDurationPassed( + DualGovernanceStateMachine.Config memory self, + Timestamp vetoSignallingDeactivationEnteredAt + ) internal view returns (bool) { + return Timestamps.now() > self.vetoSignallingDeactivationMaxDuration.addTo(vetoSignallingDeactivationEnteredAt); + } + + function isVetoCooldownDurationPassed( + DualGovernanceStateMachine.Config memory self, + Timestamp vetoCooldownEnteredAt + ) internal view returns (bool) { + return Timestamps.now() > self.vetoCooldownDuration.addTo(vetoCooldownEnteredAt); + } + + function calcDynamicDelayDuration( + DualGovernanceStateMachine.Config memory self, + uint256 rageQuitSupport + ) internal pure returns (Duration duration_) { + uint256 firstSealRageQuitSupport = self.firstSealRageQuitSupport; + uint256 secondSealRageQuitSupport = self.secondSealRageQuitSupport; + Duration dynamicTimelockMinDuration = self.dynamicTimelockMinDuration; + Duration dynamicTimelockMaxDuration = self.dynamicTimelockMaxDuration; + + if (rageQuitSupport < firstSealRageQuitSupport) { + return Durations.ZERO; + } + + if (rageQuitSupport >= secondSealRageQuitSupport) { + return dynamicTimelockMaxDuration; + } + + duration_ = dynamicTimelockMinDuration + + Durations.from( + (rageQuitSupport - firstSealRageQuitSupport) + * (dynamicTimelockMaxDuration - dynamicTimelockMinDuration).toSeconds() + / (secondSealRageQuitSupport - firstSealRageQuitSupport) + ); + } + + function calcRageQuitWithdrawalsTimelock( + DualGovernanceStateMachine.Config memory self, + uint256 rageQuitRound + ) internal pure returns (Duration) { + if (rageQuitRound < self.rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber) { + return self.rageQuitEthWithdrawalsMinTimelock; + } + return self.rageQuitEthWithdrawalsMinTimelock + + Durations.from( + ( + self.rageQuitEthWithdrawalsTimelockGrowthCoeffs[0] * rageQuitRound * rageQuitRound + + self.rageQuitEthWithdrawalsTimelockGrowthCoeffs[1] * rageQuitRound + + self.rageQuitEthWithdrawalsTimelockGrowthCoeffs[2] + ) / 10 ** 18 + ); // TODO: rewrite in a prettier way + } +} diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index a7851a45..4d315ad7 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -29,7 +29,7 @@ import { EmergencyProtectedTimelock } from "contracts/EmergencyProtectedTimelock.sol"; -import {DualGovernance, State as DGState, DualGovernanceState} from "contracts/DualGovernance.sol"; +import {DualGovernance, State as DGState, DualGovernanceStateMachine} from "contracts/DualGovernance.sol"; import {TimelockedGovernance, IGovernance} from "contracts/TimelockedGovernance.sol"; import {ExternalCall} from "contracts/libraries/ExternalCalls.sol"; @@ -125,7 +125,7 @@ contract ScenarioTestBlueprint is Test { view returns (bool isActive, uint256 duration, uint256 activatedAt, uint256 enteredAt) { - DualGovernanceState.Context memory stateContext = _dualGovernance.getCurrentStateContext(); + DualGovernanceStateMachine.Context memory stateContext = _dualGovernance.getCurrentStateContext(); isActive = stateContext.state == DGState.VetoSignalling; duration = _dualGovernance.getDynamicDelayDuration().toSeconds(); enteredAt = stateContext.enteredAt.toSeconds(); @@ -137,7 +137,7 @@ contract ScenarioTestBlueprint is Test { view returns (bool isActive, uint256 duration, uint256 enteredAt) { - DualGovernanceState.Context memory stateContext = _dualGovernance.getCurrentStateContext(); + DualGovernanceStateMachine.Context memory stateContext = _dualGovernance.getCurrentStateContext(); isActive = stateContext.state == DGState.VetoSignallingDeactivation; duration = _dualGovernance.CONFIG().VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().toSeconds(); enteredAt = stateContext.enteredAt.toSeconds();