Skip to content

Commit

Permalink
Merge pull request #227 from lidofinance/feature/change-rage-quit-sup…
Browse files Browse the repository at this point in the history
…port-checks

Update RageQuit support inequalities
  • Loading branch information
Psirex authored Dec 5, 2024
2 parents 8409ce8 + 72e8bdf commit 822670c
Show file tree
Hide file tree
Showing 11 changed files with 61 additions and 59 deletions.
18 changes: 9 additions & 9 deletions contracts/libraries/DualGovernanceConfig.sol
Original file line number Diff line number Diff line change
Expand Up @@ -90,26 +90,26 @@ library DualGovernanceConfig {
}
}

/// @notice Determines whether the first seal Rage Quit support threshold has been exceeded.
/// @notice Determines whether the first seal Rage Quit support threshold has been reached.
/// @param self The configuration context.
/// @param rageQuitSupport The current Rage Quit support level.
/// @return bool A boolean indicating whether the Rage Quit support level exceeds the first seal threshold.
function isFirstSealRageQuitSupportCrossed(
/// @return bool A boolean indicating whether the Rage Quit support level reaches the first seal threshold.
function isFirstSealRageQuitSupportReached(
Context memory self,
PercentD16 rageQuitSupport
) internal pure returns (bool) {
return rageQuitSupport > self.firstSealRageQuitSupport;
return rageQuitSupport >= self.firstSealRageQuitSupport;
}

/// @notice Determines whether the second seal Rage Quit support threshold has been exceeded.
/// @notice Determines whether the second seal Rage Quit support threshold has been reached.
/// @param self The configuration context.
/// @param rageQuitSupport The current Rage Quit support level.
/// @return bool A boolean indicating whether the Rage Quit support level exceeds the second seal threshold.
function isSecondSealRageQuitSupportCrossed(
/// @return bool A boolean indicating whether the Rage Quit support level reaches the second seal threshold.
function isSecondSealRageQuitSupportReached(
Context memory self,
PercentD16 rageQuitSupport
) internal pure returns (bool) {
return rageQuitSupport > self.secondSealRageQuitSupport;
return rageQuitSupport >= self.secondSealRageQuitSupport;
}

/// @notice Determines whether the VetoSignalling duration has passed based on the current time.
Expand Down Expand Up @@ -176,7 +176,7 @@ library DualGovernanceConfig {
Duration vetoSignallingMinDuration = self.vetoSignallingMinDuration;
Duration vetoSignallingMaxDuration = self.vetoSignallingMaxDuration;

if (rageQuitSupport <= firstSealRageQuitSupport) {
if (rageQuitSupport < firstSealRageQuitSupport) {
return Durations.ZERO;
}

Expand Down
2 changes: 1 addition & 1 deletion contracts/libraries/DualGovernanceStateMachine.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {DualGovernanceStateTransitions} from "./DualGovernanceStateTransitions.s
/// @param VetoCooldown A state where the DAO can execute non-cancelled proposals but is prohibited from submitting
/// new proposals.
/// @param RageQuit Represents the process where users opting to leave the protocol can withdraw their funds. This state
/// is triggered when the Second Seal Threshold is crossed. During this state, the scheduling of proposals for
/// is triggered when the Second Seal Threshold is reached. During this state, the scheduling of proposals for
/// execution is forbidden, but new proposals can still be submitted.
enum State {
Unset,
Expand Down
10 changes: 5 additions & 5 deletions contracts/libraries/DualGovernanceStateTransitions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ library DualGovernanceStateTransitions {
DualGovernanceStateMachine.Context storage self,
DualGovernanceConfig.Context memory config
) private view returns (State) {
return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport())
return config.isFirstSealRageQuitSupportReached(self.signallingEscrow.getRageQuitSupport())
? State.VetoSignalling
: State.Normal;
}
Expand All @@ -63,7 +63,7 @@ library DualGovernanceStateTransitions {
return State.VetoSignalling;
}

if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) {
if (config.isSecondSealRageQuitSupportReached(rageQuitSupport)) {
return State.RageQuit;
}

Expand All @@ -82,7 +82,7 @@ library DualGovernanceStateTransitions {
return State.VetoSignalling;
}

if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) {
if (config.isSecondSealRageQuitSupportReached(rageQuitSupport)) {
return State.RageQuit;
}

Expand All @@ -100,7 +100,7 @@ library DualGovernanceStateTransitions {
if (!config.isVetoCooldownDurationPassed(self.enteredAt)) {
return State.VetoCooldown;
}
return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport())
return config.isFirstSealRageQuitSupportReached(self.signallingEscrow.getRageQuitSupport())
? State.VetoSignalling
: State.Normal;
}
Expand All @@ -112,7 +112,7 @@ library DualGovernanceStateTransitions {
if (!self.rageQuitEscrow.isRageQuitFinalized()) {
return State.RageQuit;
}
return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport())
return config.isFirstSealRageQuitSupportReached(self.signallingEscrow.getRageQuitSupport())
? State.VetoSignalling
: State.VetoCooldown;
}
Expand Down
2 changes: 0 additions & 2 deletions contracts/libraries/TimelockState.sol
Original file line number Diff line number Diff line change
Expand Up @@ -151,5 +151,3 @@ library TimelockState {
}
}
}


23 changes: 13 additions & 10 deletions docs/mechanism.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ The Normal state is the state the mechanism is designed to spend the most time w
**Transition to Veto Signalling**. If, while the state is active, the following expression becomes true:

```math
R > R_1
R \geq R_1
```

where $R_1$ is `FirstSealRageQuitSupport`, the Normal state is exited and the Veto Signalling state is entered.
Expand Down Expand Up @@ -171,8 +171,8 @@ The **dynamic timelock duration** $T_{lock}(R)$ depends on the current rage quit
```math
T_{lock}(R) =
\left\{ \begin{array}{lr}
0, & \text{if } R \leq R_1 \\
L(R), & \text{if } R_1 < R < R_2 \\
0, & \text{if } R < R_1 \\
L(R), & \text{if } R_1 \leq R < R_2 \\
L_{max}, & \text{if } R \geq R_2
\end{array} \right.
```
Expand All @@ -183,7 +183,8 @@ L(R) = L_{min} + \frac{(R - R_1)} {R_2 - R_1} (L_{max} - L_{min})

where $R_1$ is `FirstSealRageQuitSupport`, $R_2$ is `SecondSealRageQuitSupport`, $L_{min}$ is `DynamicTimelockMinDuration`, $L_{max}$ is `DynamicTimelockMaxDuration`. The dependence of the dynamic timelock on the rage quit support $R$ can be illustrated by the following graph:

![image](https://github.com/lidofinance/dual-governance/assets/1699593/b98dd9f1-1e55-4b5d-8ce1-56539f4cc3f8)
![image](https://github.com/user-attachments/assets/15cb6cdb-68a6-41ce-8d47-c34da19b84f1)


When the current rage quit support changes due to stakers locking or unlocking tokens into/out of the signalling escrow or the total stETH supply changing, the dynamic timelock duration is re-evaluated.

Expand All @@ -192,7 +193,7 @@ Let's now define the outgoing transitions.
**Transition to Rage Quit**. If, while Veto Signalling is active and the Deactivation sub-state is not active, the following expression becomes true:

```math
\big( t - t^S_{act} > L_{max} \big) \, \land \, \big( R > R_2 \big)
\big( t - t^S_{act} > L_{max} \big) \, \land \, \big( R \geq R_2 \big)
```

the Veto Signalling state is exited and the Rage Quit state is entered.
Expand Down Expand Up @@ -229,7 +230,7 @@ then the Deactivation sub-state is exited so only the parent Veto Signalling sta

**Transition to Rage Quit**. If, while the sub-state is active, the following condition becomes true:
```math
\big( t - t^S_{act} > L_{max} \big) \, \land \, \big( R > R_2 \big)
\big( t - t^S_{act} > L_{max} \big) \, \land \, \big( R \geq R_2 \big)
```
then the Deactivation sub-state is exited along with its parent Veto Signalling state and the Rage Quit state is entered.

Expand All @@ -253,15 +254,15 @@ In the Veto Cooldown state, the DAO cannot submit proposals to the DG but can ex
**Transition to Veto Signalling**. If, while the state is active, the following condition becomes true:

```math
\big( t - t^C_{act} > T^C \big) \,\land\, \big( R(t) > R_1 \big)
\big( t - t^C_{act} > T^C \big) \,\land\, \big( R(t) \geq R_1 \big)
```

where $t^{C}_{act}$ is the time the Veto Cooldown state was entered and $T^{C}$ is `VetoCooldownDuration`, then the Veto Cooldown state is exited and the Veto Signalling state is entered.

**Transition to Normal**. If, while the state is active, the following condition becomes true:

```math
\big( t - t^C_{act} > T^C \big) \,\land\, \big( R(t) \leq R_1 \big)
\big( t - t^C_{act} > T^C \big) \,\land\, \big( R(t) < R_1 \big)
```

then the Veto Cooldown state is exited and the Normal state is entered.
Expand Down Expand Up @@ -295,9 +296,9 @@ When the withdrawal is complete and the extension period elapses, two things hap
1. A timelock lasting $W(i)$ days is started, during which the withdrawn ETH remains locked in the rage quit escrow. After the timelock elapses, stakers who participated in the rage quit can obtain their ETH from the rage quit escrow.
2. The Rage Quit state is exited.

**Transition to Veto Signalling**. If, at the moment of the Rage Quit state exit, $R(t) > R_1$, the Veto Signalling state is entered.
**Transition to Veto Signalling**. If, at the moment of the Rage Quit state exit, $R(t) \geq R_1$, the Veto Signalling state is entered.

**Transition to Veto Cooldown**. If, at the moment of the Rage Quit state exit, $R(t) \leq R_1$, the Veto Cooldown state is entered.
**Transition to Veto Cooldown**. If, at the moment of the Rage Quit state exit, $R(t) < R_1$, the Veto Cooldown state is entered.

The duration of the ETH withdraw timelock $W(i)$ is a linear function that depends on the rage quit sequence number $i$ (see below):

Expand Down Expand Up @@ -398,6 +399,8 @@ Dual governance should not cover:


## Changelog
### 2024-12-04
- Updated calculations of Rage Quit support first/second threshold's reaching. The transition to VetoSignalling state now starts when the amount of locked funds reaches the first seal threshold (including the exact threshold value), and the transition to RageQuit state appropriately starts when the amount of locked funds reaches the second seal threshold (including the exact threshold value; the appropriate duration of time in VetoSignalling state still should pass).

### 2024-11-15
- The rage quit sequence number is now reset in the `VetoCooldown` state instead of the `Normal` state. This adjustment ensures that the ETH withdrawal timelock does not increase unnecessarily in cases where, after a Rage Quit, Dual Governance cycles through `VetoSignalling``VetoSignallingDeactivation``VetoCooldown` without entering the `Normal` state, as the DAO remains operational and can continue submitting and executing proposals in this scenario.
Expand Down
4 changes: 2 additions & 2 deletions test/scenario/dg-update-tokens-rotation.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ contract DualGovernanceUpdateTokensRotation is ScenarioTestBlueprint {

_step("3. Users accumulate some stETH in the Signalling Escrow");
{
_lockStETH(_VETOER, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT());
_lockStETH(_VETOER, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT() - PercentsD16.from(1));
_assertVetoSignalingState();
_wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1));

Expand Down Expand Up @@ -88,7 +88,7 @@ contract DualGovernanceUpdateTokensRotation is ScenarioTestBlueprint {
_step("7. Users can withdraw funds even if the Rage Quit is started in the old instance of the Dual Governance");
{
// the Rage Quit started on the old DualGovernance instance
_lockStETH(_VETOER, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.from(1));
_lockStETH(_VETOER, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT());
_wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1));
_activateNextState();
_assertRageQuitState();
Expand Down
6 changes: 3 additions & 3 deletions test/scenario/gov-state-transitions.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint {
function test_signalling_state_min_duration() public {
_assertNormalState();

_lockStETH(_VETOER, _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT());
_lockStETH(_VETOER, _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT() - PercentsD16.from(1));
_assertNormalState();

_lockStETH(_VETOER, 1 gwei);
Expand Down Expand Up @@ -63,7 +63,7 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint {
function test_signalling_to_normal() public {
_assertNormalState();

_lockStETH(_VETOER, _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT());
_lockStETH(_VETOER, _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT() - PercentsD16.from(1));

_assertNormalState();

Expand Down Expand Up @@ -93,7 +93,7 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint {
function test_signalling_non_stop() public {
_assertNormalState();

_lockStETH(_VETOER, _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT());
_lockStETH(_VETOER, _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT() - PercentsD16.from(1));
_assertNormalState();

_lockStETH(_VETOER, 1 gwei);
Expand Down
4 changes: 2 additions & 2 deletions test/unit/DualGovernance.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@ contract DualGovernanceUnitTests is UnitTest {

_wait(_configProvider.VETO_SIGNALLING_MAX_DURATION().dividedBy(2));

// The RageQuit second seal threshold wasn't crossed, the system should enter Deactivation state
// The RageQuit second seal threshold wasn't reached, the system should enter Deactivation state
// where the proposals submission is not allowed
assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling);
assertEq(_dualGovernance.getEffectiveState(), State.VetoSignallingDeactivation);
Expand Down Expand Up @@ -754,7 +754,7 @@ contract DualGovernanceUnitTests is UnitTest {
abi.encode(true)
);

// The RageQuit second seal threshold wasn't crossed, the system should enter Deactivation state
// The RageQuit second seal threshold wasn't reached, the system should enter Deactivation state
// where the proposals submission is not allowed
assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling);
assertEq(_dualGovernance.getEffectiveState(), State.VetoSignallingDeactivation);
Expand Down
21 changes: 11 additions & 10 deletions test/unit/libraries/DualGovernanceConfig.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -106,31 +106,32 @@ contract DualGovernanceConfigTest is UnitTest {
}

// ---
// isFirstSealRageQuitSupportCrossed()
// isFirstSealRageQuitSupportReached()
// ---

function testFuzz_isFirstSealRageQuitSupportCrossed_HappyPath(
function testFuzz_isFirstSealRageQuitSupportReached_HappyPath(
DualGovernanceConfig.Context memory config,
PercentD16 rageQuitSupport
) external {
_assumeConfigParams(config);
assertEq(
config.isFirstSealRageQuitSupportCrossed(rageQuitSupport), rageQuitSupport > config.firstSealRageQuitSupport
config.isFirstSealRageQuitSupportReached(rageQuitSupport),
rageQuitSupport >= config.firstSealRageQuitSupport
);
}

// ---
// isSecondSealRageQuitSupportCrossed()
// isSecondSealRageQuitSupportReached()
// ---

function testFuzz_isSecondSealRageQuitSupportCrossed_HappyPath(
function testFuzz_isSecondSealRageQuitSupportReached_HappyPath(
DualGovernanceConfig.Context memory config,
PercentD16 rageQuitSupport
) external {
_assumeConfigParams(config);
assertEq(
config.isSecondSealRageQuitSupportCrossed(rageQuitSupport),
rageQuitSupport > config.secondSealRageQuitSupport
config.isSecondSealRageQuitSupportReached(rageQuitSupport),
rageQuitSupport >= config.secondSealRageQuitSupport
);
}

Expand Down Expand Up @@ -264,12 +265,12 @@ contract DualGovernanceConfigTest is UnitTest {
// calcVetoSignallingDuration()
// ---

function testFuzz_calcVetoSignallingDuration_HappyPath_RageQuitSupportLessOrEqualThanFirstSeal(
function testFuzz_calcVetoSignallingDuration_HappyPath_RageQuitSupportLessThanFirstSeal(
DualGovernanceConfig.Context memory config,
PercentD16 rageQuitSupport
) external {
_assumeConfigParams(config);
vm.assume(rageQuitSupport <= config.firstSealRageQuitSupport);
vm.assume(rageQuitSupport < config.firstSealRageQuitSupport);
assertEq(config.calcVetoSignallingDuration(rageQuitSupport), Durations.ZERO);
}

Expand All @@ -287,7 +288,7 @@ contract DualGovernanceConfigTest is UnitTest {
PercentD16 rageQuitSupport
) external {
_assumeConfigParams(config);
vm.assume(rageQuitSupport > config.firstSealRageQuitSupport);
vm.assume(rageQuitSupport >= config.firstSealRageQuitSupport);
vm.assume(rageQuitSupport < config.secondSealRageQuitSupport);

PercentD16 rageQuitSupportFirstSealDelta = rageQuitSupport - config.firstSealRageQuitSupport;
Expand Down
4 changes: 2 additions & 2 deletions test/unit/libraries/DualGovernanceStateMachine.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ contract DualGovernanceStateMachineUnitTests is UnitTest {
_activateNextState();
_wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1));

// Simulate the Rage Quit process has completed and in the SignallingEscrow the first seal is not crossed
// Simulate the Rage Quit process has completed and in the SignallingEscrow the first seal is not reached
_mockRageQuitFinalized(true);
_activateNextState();
_mockRageQuitSupport(PercentsD16.fromBasisPoints(0));
Expand All @@ -90,7 +90,7 @@ contract DualGovernanceStateMachineUnitTests is UnitTest {
_activateNextState();
_wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1));

// Simulate the Rage Quit process has completed and in the SignallingEscrow the first seal is crossed
// Simulate the Rage Quit process has completed and in the SignallingEscrow the first seal is reached
_mockRageQuitFinalized(true);
_activateNextState();

Expand Down
Loading

0 comments on commit 822670c

Please sign in to comment.