Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pending DualGovernance state #127

Merged
merged 20 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e7311d7
Use pending state getter in Escrow lock/unlock methods
Psirex Sep 11, 2024
b52e64f
Remove RageQuit state check from the Escrow's lock/unlock methods
Psirex Sep 11, 2024
e3dfb3d
Add nextState property to StateDetails
Psirex Sep 11, 2024
ba6e421
Merge branch 'develop' into feature/pending-dg-state
Psirex Sep 15, 2024
fb061c0
Call activateNextState twice on all signalling escrow operations
Psirex Sep 15, 2024
f3a8e97
Move configProvider in the DG state machine lib. Introduce persisted …
Psirex Sep 15, 2024
6188cbe
Add NatSpec for DualGovernanceStateMachine lib
Psirex Sep 15, 2024
39aa767
Add persisted/effective state to the StateDetails struct
Psirex Sep 15, 2024
fbd77fa
Spec update and minor comments & naming tweaks
Psirex Sep 16, 2024
ae677c1
Add canCancelAllProposals() method to DualGovernance
Psirex Sep 16, 2024
acab4a6
Add effective state tests for canSubmitProposal, canScheduleProposal
Psirex Sep 18, 2024
4ce0948
DualGovernance natspec. Rename canCancelAllPendingProposals(). Return…
Psirex Sep 24, 2024
7b164ad
Additional tests DualGovernance & DualGovernanceStateMachine
Psirex Sep 24, 2024
6bac085
Move related methods in the IEmergencyProtectedTimelock
Psirex Sep 24, 2024
03c76a7
EmergencyProtectedTimelock natspec fixes
Psirex Sep 24, 2024
c41e546
Add natspec fo Escrow contract
Psirex Sep 25, 2024
0c8f50e
Add tests on cancelAllPendingProposals return value
Psirex Sep 25, 2024
2b9b021
DualGovernance and Timelock section comments
Psirex Sep 25, 2024
3a481f9
Update cancelAllPendingProposals spec
Psirex Sep 25, 2024
a3e6a8e
getTiebreakerDetails correct isTie value in edge case
Psirex Sep 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 50 additions & 60 deletions contracts/DualGovernance.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,28 @@
pragma solidity 0.8.26;

import {Duration} from "./types/Duration.sol";
import {Timestamp} from "./types/Timestamp.sol";

import {IStETH} from "./interfaces/IStETH.sol";
import {IWstETH} from "./interfaces/IWstETH.sol";
import {IWithdrawalQueue} from "./interfaces/IWithdrawalQueue.sol";
import {IEscrow} from "./interfaces/IEscrow.sol";
import {ITimelock} from "./interfaces/ITimelock.sol";
import {ITiebreaker} from "./interfaces/ITiebreaker.sol";
import {IWithdrawalQueue} from "./interfaces/IWithdrawalQueue.sol";
import {IDualGovernance} from "./interfaces/IDualGovernance.sol";
import {IResealManager} from "./interfaces/IResealManager.sol";
import {IDualGovernanceConfigProvider} from "./interfaces/IDualGovernanceConfigProvider.sol";

import {Proposers} from "./libraries/Proposers.sol";
import {Tiebreaker} from "./libraries/Tiebreaker.sol";
import {ExternalCall} from "./libraries/ExternalCalls.sol";
import {DualGovernanceConfig} from "./libraries/DualGovernanceConfig.sol";
import {State, DualGovernanceStateMachine} from "./libraries/DualGovernanceStateMachine.sol";

import {Escrow} from "./Escrow.sol";

contract DualGovernance is IDualGovernance {
using Proposers for Proposers.Context;
using Tiebreaker for Tiebreaker.Context;
using DualGovernanceConfig for DualGovernanceConfig.Context;
using DualGovernanceStateMachine for DualGovernanceStateMachine.Context;

// ---
Expand All @@ -34,7 +34,6 @@ contract DualGovernance is IDualGovernance {
error UnownedAdminExecutor();
error CallerIsNotResealCommittee(address caller);
error CallerIsNotAdminExecutor(address caller);
error InvalidConfigProvider(IDualGovernanceConfigProvider configProvider);
error ProposalSubmissionBlocked();
error ProposalSchedulingBlocked(uint256 proposalId);
error ResealIsNotAllowedInNormalState();
Expand All @@ -45,8 +44,7 @@ contract DualGovernance is IDualGovernance {

event CancelAllPendingProposalsSkipped();
event CancelAllPendingProposalsExecuted();
event EscrowMasterCopyDeployed(address escrowMasterCopy);
event ConfigProviderSet(IDualGovernanceConfigProvider newConfigProvider);
event EscrowMasterCopyDeployed(IEscrow escrowMasterCopy);
event ResealCommitteeSet(address resealCommittee);

// ---
Expand Down Expand Up @@ -78,7 +76,7 @@ contract DualGovernance is IDualGovernance {

ITimelock public immutable TIMELOCK;
IResealManager public immutable RESEAL_MANAGER;
address public immutable ESCROW_MASTER_COPY;
IEscrow public immutable ESCROW_MASTER_COPY;

// ---
// Aspects
Expand All @@ -91,7 +89,6 @@ contract DualGovernance is IDualGovernance {
// ---
// Standalone State Variables
// ---
IDualGovernanceConfigProvider internal _configProvider;
address internal _resealCommittee;

constructor(ExternalDependencies memory dependencies, SanityCheckParams memory sanityCheckParams) {
Expand All @@ -102,20 +99,17 @@ contract DualGovernance is IDualGovernance {
MAX_TIEBREAKER_ACTIVATION_TIMEOUT = sanityCheckParams.maxTiebreakerActivationTimeout;
MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT = sanityCheckParams.maxSealableWithdrawalBlockersCount;

_setConfigProvider(dependencies.configProvider);
ESCROW_MASTER_COPY = new Escrow({
dualGovernance: this,
stETH: dependencies.stETH,
wstETH: dependencies.wstETH,
withdrawalQueue: dependencies.withdrawalQueue,
minWithdrawalsBatchSize: sanityCheckParams.minWithdrawalsBatchSize
});

ESCROW_MASTER_COPY = address(
new Escrow({
dualGovernance: this,
stETH: dependencies.stETH,
wstETH: dependencies.wstETH,
withdrawalQueue: dependencies.withdrawalQueue,
minWithdrawalsBatchSize: sanityCheckParams.minWithdrawalsBatchSize
})
);
emit EscrowMasterCopyDeployed(ESCROW_MASTER_COPY);

_stateMachine.initialize(dependencies.configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY);
_stateMachine.initialize(dependencies.configProvider, ESCROW_MASTER_COPY);
}

// ---
Expand All @@ -126,33 +120,32 @@ contract DualGovernance is IDualGovernance {
ExternalCall[] calldata calls,
string calldata metadata
) external returns (uint256 proposalId) {
_stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY);
if (!_stateMachine.canSubmitProposal()) {
_stateMachine.activateNextState(ESCROW_MASTER_COPY);
if (!_stateMachine.canSubmitProposal({useEffectiveState: false})) {
revert ProposalSubmissionBlocked();
}
Proposers.Proposer memory proposer = _proposers.getProposer(msg.sender);
proposalId = TIMELOCK.submit(proposer.executor, calls, metadata);
}

function scheduleProposal(uint256 proposalId) external {
_stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY);
ITimelock.ProposalDetails memory proposalDetails = TIMELOCK.getProposalDetails(proposalId);
if (!_stateMachine.canScheduleProposal(proposalDetails.submittedAt)) {
_stateMachine.activateNextState(ESCROW_MASTER_COPY);
Timestamp proposalSubmittedAt = TIMELOCK.getProposalDetails(proposalId).submittedAt;
if (!_stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt})) {
revert ProposalSchedulingBlocked(proposalId);
}
TIMELOCK.schedule(proposalId);
}

function cancelAllPendingProposals() external {
_stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY);
_stateMachine.activateNextState(ESCROW_MASTER_COPY);

Proposers.Proposer memory proposer = _proposers.getProposer(msg.sender);
if (proposer.executor != TIMELOCK.getAdminExecutor()) {
revert NotAdminProposer();
}

State state = _stateMachine.getState();
if (state != State.VetoSignalling && state != State.VetoSignallingDeactivation) {
if (!_stateMachine.canCancelAllProposals({useEffectiveState: false})) {
/// @dev Some proposer contracts, like Aragon Voting, may not support canceling decisions that have already
/// reached consensus. This could lead to a situation where a proposer’s cancelAllPendingProposals() call
/// becomes unexecutable if the Dual Governance state changes. However, it might become executable again if
Expand All @@ -167,36 +160,35 @@ contract DualGovernance is IDualGovernance {
emit CancelAllPendingProposalsExecuted();
}

function canSubmitProposal() public view returns (bool) {
return _stateMachine.canSubmitProposal();
function canSubmitProposal() external view returns (bool) {
return _stateMachine.canSubmitProposal({useEffectiveState: true});
}

function canScheduleProposal(uint256 proposalId) external view returns (bool) {
ITimelock.ProposalDetails memory proposalDetails = TIMELOCK.getProposalDetails(proposalId);
return _stateMachine.canScheduleProposal(proposalDetails.submittedAt) && TIMELOCK.canSchedule(proposalId);
Timestamp proposalSubmittedAt = TIMELOCK.getProposalDetails(proposalId).submittedAt;
return _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt})
&& TIMELOCK.canSchedule(proposalId);
}

function canCancelAllProposals() external view returns (bool) {
return _stateMachine.canCancelAllProposals({useEffectiveState: true});
}

// ---
// Dual Governance State
// ---

function activateNextState() external {
_stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY);
_stateMachine.activateNextState(ESCROW_MASTER_COPY);
}

function setConfigProvider(IDualGovernanceConfigProvider newConfigProvider) external {
_checkCallerIsAdminExecutor();
_setConfigProvider(newConfigProvider);

/// @dev the minAssetsLockDuration is kept as a storage variable in the signalling Escrow instance
/// to sync the new value with current signalling escrow, it's value must be manually updated
_stateMachine.signallingEscrow.setMinAssetsLockDuration(
newConfigProvider.getDualGovernanceConfig().minAssetsLockDuration
);
_stateMachine.setConfigProvider(newConfigProvider);
}

function getConfigProvider() external view returns (IDualGovernanceConfigProvider) {
return _configProvider;
return _stateMachine.configProvider;
}

function getVetoSignallingEscrow() external view returns (address) {
Expand All @@ -207,12 +199,16 @@ contract DualGovernance is IDualGovernance {
return address(_stateMachine.rageQuitEscrow);
}

function getState() external view returns (State state) {
state = _stateMachine.getState();
function getPersistedState() external view returns (State persistedState) {
persistedState = _stateMachine.getPersistedState();
}

function getStateDetails() external view returns (IDualGovernance.StateDetails memory stateDetails) {
return _stateMachine.getStateDetails(_configProvider.getDualGovernanceConfig());
function getEffectiveState() external view returns (State effectiveState) {
effectiveState = _stateMachine.getEffectiveState();
}

function getStateDetails() external view returns (StateDetails memory stateDetails) {
return _stateMachine.getStateDetails();
}

// ---
Expand Down Expand Up @@ -278,21 +274,24 @@ contract DualGovernance is IDualGovernance {

function tiebreakerResumeSealable(address sealable) external {
_tiebreaker.checkCallerIsTiebreakerCommittee();
_stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY);
_tiebreaker.checkTie(_stateMachine.getState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt());
_stateMachine.activateNextState(ESCROW_MASTER_COPY);
_tiebreaker.checkTie(_stateMachine.getPersistedState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt());
RESEAL_MANAGER.resume(sealable);
}

function tiebreakerScheduleProposal(uint256 proposalId) external {
_tiebreaker.checkCallerIsTiebreakerCommittee();
_stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY);
_tiebreaker.checkTie(_stateMachine.getState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt());
_stateMachine.activateNextState(ESCROW_MASTER_COPY);
_tiebreaker.checkTie(_stateMachine.getPersistedState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt());
TIMELOCK.schedule(proposalId);
}

function getTiebreakerDetails() external view returns (ITiebreaker.TiebreakerDetails memory tiebreakerState) {
return _tiebreaker.getTiebreakerDetails(
_stateMachine.getState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt()
/// @dev Calling getEffectiveState() doesn't update the normalOrVetoCooldownStateExitedAt value,
/// but this does not distort the result of getTiebreakerDetails()
_stateMachine.getEffectiveState(),
_stateMachine.getNormalOrVetoCooldownStateExitedAt()
);
}

Expand All @@ -301,10 +300,11 @@ contract DualGovernance is IDualGovernance {
// ---

function resealSealable(address sealable) external {
_stateMachine.activateNextState(ESCROW_MASTER_COPY);
if (msg.sender != _resealCommittee) {
revert CallerIsNotResealCommittee(msg.sender);
}
if (_stateMachine.getState() == State.Normal) {
if (_stateMachine.getPersistedState() == State.Normal) {
revert ResealIsNotAllowedInNormalState();
}
RESEAL_MANAGER.reseal(sealable);
Expand All @@ -321,16 +321,6 @@ contract DualGovernance is IDualGovernance {
// Private methods
// ---

function _setConfigProvider(IDualGovernanceConfigProvider newConfigProvider) internal {
if (address(newConfigProvider) == address(0) || newConfigProvider == _configProvider) {
revert InvalidConfigProvider(newConfigProvider);
}

newConfigProvider.getDualGovernanceConfig().validate();
_configProvider = newConfigProvider;
emit ConfigProviderSet(newConfigProvider);
}

function _checkCallerIsAdminExecutor() internal view {
if (TIMELOCK.getAdminExecutor() != msg.sender) {
revert CallerIsNotAdminExecutor(msg.sender);
Expand Down
9 changes: 7 additions & 2 deletions contracts/Escrow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ contract Escrow is IEscrow {
// ---
// Errors
// ---

error UnclaimedBatches();
error UnexpectedUnstETHId();
error UnfinalizedUnstETHIds();
Expand Down Expand Up @@ -186,11 +185,11 @@ contract Escrow is IEscrow {
// ---
// Lock & unlock unstETH
// ---

function lockUnstETH(uint256[] memory unstETHIds) external {
if (unstETHIds.length == 0) {
revert EmptyUnstETHIds();
}

DUAL_GOVERNANCE.activateNextState();
_escrowState.checkSignallingEscrow();

Expand Down Expand Up @@ -219,17 +218,20 @@ contract Escrow is IEscrow {
}

function markUnstETHFinalized(uint256[] memory unstETHIds, uint256[] calldata hints) external {
DUAL_GOVERNANCE.activateNextState();
_escrowState.checkSignallingEscrow();

uint256[] memory claimableAmounts = WITHDRAWAL_QUEUE.getClaimableEther(unstETHIds, hints);
_accounting.accountUnstETHFinalized(unstETHIds, claimableAmounts);
DUAL_GOVERNANCE.activateNextState();
}

// ---
// Convert to NFT
// ---

function requestWithdrawals(uint256[] calldata stETHAmounts) external returns (uint256[] memory unstETHIds) {
DUAL_GOVERNANCE.activateNextState();
_escrowState.checkSignallingEscrow();

unstETHIds = WITHDRAWAL_QUEUE.requestWithdrawals(stETHAmounts, address(this));
Expand All @@ -241,6 +243,9 @@ contract Escrow is IEscrow {
}
_accounting.accountStETHSharesUnlock(msg.sender, SharesValues.from(sharesTotal));
_accounting.accountUnstETHLock(msg.sender, unstETHIds, statuses);

/// @dev Skip calling activateNextState here to save gas, as converting stETH to unstETH NFTs
/// does not affect the RageQuit support.
}

// ---
Expand Down
5 changes: 3 additions & 2 deletions contracts/interfaces/IDualGovernance.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import {State} from "../libraries/DualGovernanceStateMachine.sol";

interface IDualGovernance is IGovernance, ITiebreaker {
struct StateDetails {
State state;
Timestamp enteredAt;
State effectiveState;
State persistedState;
Timestamp persistedStateEnteredAt;
Timestamp vetoSignallingActivatedAt;
Timestamp vetoSignallingReactivationTime;
Timestamp normalOrVetoCooldownExitedAt;
Expand Down
Loading