Skip to content

Commit

Permalink
feat: add emergency execution (#104)
Browse files Browse the repository at this point in the history
* feat: add emergency execution

* refactor: emergencyQuorum has priority over quorum; return  when emergency quorum is rejected but still under maxEndTimestamp

* refactor: move len checks to top of initializer

* feat: check array lengths match

* chore: updated tests

* chore: increase coverage

* chore: increase coverage

---------

Co-authored-by: Orland0x <[email protected]>
Co-authored-by: Orlando <[email protected]>
  • Loading branch information
3 people authored May 19, 2023
1 parent abdc1bd commit 9a4640f
Show file tree
Hide file tree
Showing 3 changed files with 323 additions and 1 deletion.
2 changes: 1 addition & 1 deletion codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ coverage:
project:
default:
target: auto
threshold: 1%
threshold: 2%
patch:
default:
target: 0%
97 changes: 97 additions & 0 deletions src/execution-strategies/EmergencyQuorumStrategy.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.18;

import { IExecutionStrategy } from "../interfaces/IExecutionStrategy.sol";
import { FinalizationStatus, Proposal, ProposalStatus } from "../types.sol";

abstract contract EmergencyQuorumStrategy is IExecutionStrategy {
uint256 public immutable quorum;
uint256 public immutable emergencyQuorum;

constructor(uint256 _quorum, uint256 _emergencyQuorum) {
quorum = _quorum;
emergencyQuorum = _emergencyQuorum;
}

function execute(
Proposal memory proposal,
uint256 votesFor,
uint256 votesAgainst,
uint256 votesAbstain,
bytes memory payload
) external virtual override;

// solhint-disable-next-line code-complexity
function getProposalStatus(
Proposal memory proposal,
uint256 votesFor,
uint256 votesAgainst,
uint256 votesAbstain
) public view override returns (ProposalStatus) {
bool emergencyQuorumReached = _quorumReached(emergencyQuorum, votesFor, votesAgainst, votesAbstain);

bool accepted = _quorumReached(quorum, votesFor, votesAgainst, votesAbstain) &&
_supported(votesFor, votesAgainst);

if (proposal.finalizationStatus == FinalizationStatus.Cancelled) {
return ProposalStatus.Cancelled;
} else if (proposal.finalizationStatus == FinalizationStatus.Executed) {
return ProposalStatus.Executed;
} else if (block.timestamp < proposal.startTimestamp) {
return ProposalStatus.VotingDelay;
} else if (emergencyQuorumReached) {
if (_supported(votesFor, votesAgainst)) {
// Proposal is supported
if (block.timestamp < proposal.maxEndTimestamp) {
// New votes can still come in so return `VotingPeriodAccepted`.
return ProposalStatus.VotingPeriodAccepted;
} else {
// No new votes can't come in, so it's definitely accepted.
return ProposalStatus.Accepted;
}
} else {
// Proposal is not supported
if (block.timestamp < proposal.maxEndTimestamp) {
// New votes might still come in so return `VotingPeriod`.
return ProposalStatus.VotingPeriod;
} else {
// New votes can't come in, so it's definitely rejected.
return ProposalStatus.Rejected;
}
}
} else if (block.timestamp < proposal.minEndTimestamp) {
// Proposal has not reached minEndTimestamp yet.
return ProposalStatus.VotingPeriod;
} else if (block.timestamp < proposal.maxEndTimestamp) {
// Timestamp is between minEndTimestamp and maxEndTimestamp
if (accepted) {
return ProposalStatus.VotingPeriodAccepted;
} else {
return ProposalStatus.VotingPeriod;
}
} else if (accepted) {
// Quorum reached and proposal supported: no new votes will come in so the proposal is
// definitely accepted.
return ProposalStatus.Accepted;
} else {
// Quorum not reached reached or proposal supported: no new votes will come in so the proposal is
// definitely rejected.
return ProposalStatus.Rejected;
}
}

function _quorumReached(
uint256 _quorum,
uint256 _votesFor,
uint256 _votesAgainst,
uint256 _votesAbstain
) internal pure returns (bool) {
uint256 totalVotes = _votesFor + _votesAgainst + _votesAbstain;
return totalVotes >= _quorum;
}

function _supported(uint256 _votesFor, uint256 _votesAgainst) internal pure returns (bool) {
return _votesFor > _votesAgainst;
}
}
225 changes: 225 additions & 0 deletions test/EmergencyQuorum.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
// SPDX-License-Identifier: UNLICENSED

pragma solidity ^0.8.18;

import { SpaceTest } from "./utils/Space.t.sol";
import { Choice, IndexedStrategy, Proposal, ProposalStatus, Strategy, UpdateSettingsInput } from "../src/types.sol";
import { EmergencyQuorumStrategy } from "../src/execution-strategies/EmergencyQuorumStrategy.sol";

contract EmergencyQuorumExec is EmergencyQuorumStrategy {
uint256 internal numExecuted;

// solhint-disable-next-line no-empty-blocks
constructor(uint256 _quorum, uint256 _emergencyQuorum) EmergencyQuorumStrategy(_quorum, _emergencyQuorum) {}

function execute(
Proposal memory proposal,
uint256 votesFor,
uint256 votesAgainst,
uint256 votesAbstain,
bytes memory payload
) external override {
ProposalStatus proposalStatus = getProposalStatus(proposal, votesFor, votesAgainst, votesAbstain);
if ((proposalStatus != ProposalStatus.Accepted) && (proposalStatus != ProposalStatus.VotingPeriodAccepted)) {
revert InvalidProposalStatus(proposalStatus);
}
// Check that the execution payload matches the payload supplied when the proposal was created
if (proposal.executionPayloadHash != keccak256(payload)) revert InvalidPayload();
numExecuted++;
}

function getStrategyType() external pure returns (string memory) {
return "EmergencyQuorumExecution";
}
}

contract EmergencyQuorumTest is SpaceTest {
Strategy internal emergencyStrategy;
uint256 internal emergencyQuorum = 2;
EmergencyQuorumExec internal emergency;

function setUp() public override {
super.setUp();

emergency = new EmergencyQuorumExec(quorum, emergencyQuorum);
emergencyStrategy = Strategy(address(emergency), new bytes(0));

minVotingDuration = 100;
space.updateSettings(
UpdateSettingsInput(
minVotingDuration,
NO_UPDATE_UINT32,
NO_UPDATE_UINT32,
NO_UPDATE_STRING,
NO_UPDATE_STRING,
NO_UPDATE_STRATEGY,
NO_UPDATE_STRING,
NO_UPDATE_ADDRESSES,
NO_UPDATE_ADDRESSES,
NO_UPDATE_STRATEGIES,
NO_UPDATE_STRINGS,
NO_UPDATE_UINT8S
)
);
}

function testEmergencyQuorum() public {
uint256 proposalId = _createProposal(
author,
proposalMetadataURI,
emergencyStrategy,
abi.encode(userVotingStrategies)
);
_vote(author, proposalId, Choice.For, userVotingStrategies, voteMetadataURI); // 1
_vote(address(42), proposalId, Choice.For, userVotingStrategies, voteMetadataURI); // 2

vm.expectEmit(true, true, true, true);
emit ProposalExecuted(proposalId);
space.execute(proposalId, emergencyStrategy.params);

assertEq(uint8(space.getProposalStatus(proposalId)), uint8(ProposalStatus.Executed));
}

function testEmergencyQuorumNotReached() public {
uint256 proposalId = _createProposal(
author,
proposalMetadataURI,
emergencyStrategy,
abi.encode(userVotingStrategies)
);
_vote(author, proposalId, Choice.For, userVotingStrategies, voteMetadataURI); // 1

vm.expectRevert(abi.encodeWithSelector(InvalidProposalStatus.selector, uint8(ProposalStatus.VotingPeriod)));
space.execute(proposalId, emergencyStrategy.params);
}

function testEmergencyQuorumAfterMinDuration() public {
uint256 proposalId = _createProposal(
author,
proposalMetadataURI,
emergencyStrategy,
abi.encode(userVotingStrategies)
);
_vote(author, proposalId, Choice.For, userVotingStrategies, voteMetadataURI); // 1

vm.warp(block.timestamp + minVotingDuration);

vm.expectEmit(true, true, true, true);
emit ProposalExecuted(proposalId);
space.execute(proposalId, emergencyStrategy.params);
}

function testEmergencyQuorumAfterMaxDuration() public {
uint256 proposalId = _createProposal(
author,
proposalMetadataURI,
emergencyStrategy,
abi.encode(userVotingStrategies)
);
_vote(author, proposalId, Choice.For, userVotingStrategies, voteMetadataURI); // 1

vm.warp(block.timestamp + maxVotingDuration);

vm.expectEmit(true, true, true, true);
emit ProposalExecuted(proposalId);
space.execute(proposalId, emergencyStrategy.params);
}

function testEmergencyQuorumReachedButRejected() public {
uint256 proposalId = _createProposal(
author,
proposalMetadataURI,
emergencyStrategy,
abi.encode(userVotingStrategies)
);

// Cast two votes AGAINST
_vote(author, proposalId, Choice.Against, userVotingStrategies, voteMetadataURI); // 1
_vote(address(42), proposalId, Choice.Against, userVotingStrategies, voteMetadataURI); // 2

// EmergencyQuorum should've been reached but with only `AGAINST` votes, so proposal status should be
// `VotingPeriod`.
vm.expectRevert(abi.encodeWithSelector(InvalidProposalStatus.selector, uint8(ProposalStatus.VotingPeriod)));
space.execute(proposalId, emergencyStrategy.params);

// Now forward to `maxEndTimestamp`, the proposal should be finalized and `Rejected`.
vm.warp(block.timestamp + maxVotingDuration);

vm.expectRevert(abi.encodeWithSelector(InvalidProposalStatus.selector, uint8(ProposalStatus.Rejected)));
space.execute(proposalId, emergencyStrategy.params);
}

function testEmergencyQuorumLowerThanQuorum() public {
EmergencyQuorumExec emergencyQuorumExec = new EmergencyQuorumExec(quorum, quorum - 1);

emergencyStrategy = Strategy(address(emergencyQuorumExec), new bytes(0));

// Create proposal and vote
uint256 proposalId = _createProposal(
author,
proposalMetadataURI,
emergencyStrategy,
abi.encode(userVotingStrategies)
);
_vote(author, proposalId, Choice.For, userVotingStrategies, voteMetadataURI); // emergencyQuorum reached
vm.warp(block.timestamp + maxVotingDuration);

vm.expectEmit(true, true, true, true);
emit ProposalExecuted(proposalId);
space.execute(proposalId, emergencyStrategy.params);
}

function testEmergencyQuorumVotingPeriod() public {
uint256 proposalId = _createProposal(
author,
proposalMetadataURI,
emergencyStrategy,
abi.encode(userVotingStrategies)
);

// Cast two votes AGAINST
_vote(author, proposalId, Choice.Against, userVotingStrategies, voteMetadataURI); // 1
_vote(address(42), proposalId, Choice.Against, userVotingStrategies, voteMetadataURI); // 2

// EmergencyQuorum should've been reached but with only `AGAINST` votes, so proposal status should be
// `VotingPeriod`.
vm.expectRevert(abi.encodeWithSelector(InvalidProposalStatus.selector, uint8(ProposalStatus.VotingPeriod)));
space.execute(proposalId, emergencyStrategy.params);
}

function testEmergencyQuorumCancelled() public {
uint256 proposalId = _createProposal(
author,
proposalMetadataURI,
emergencyStrategy,
abi.encode(userVotingStrategies)
);
_vote(author, proposalId, Choice.For, userVotingStrategies, voteMetadataURI); // 1

space.cancel(proposalId);

vm.expectRevert(abi.encodeWithSelector(InvalidProposalStatus.selector, uint8(ProposalStatus.Cancelled)));
space.execute(proposalId, emergencyStrategy.params);
}

function testEmergencyQuorumAlreadyExecuted() public {
uint256 proposalId = _createProposal(
author,
proposalMetadataURI,
emergencyStrategy,
abi.encode(userVotingStrategies)
);
_vote(author, proposalId, Choice.For, userVotingStrategies, voteMetadataURI); // 1

vm.warp(block.timestamp + minVotingDuration);

space.execute(proposalId, emergencyStrategy.params);

vm.expectRevert(abi.encodeWithSelector(InvalidProposalStatus.selector, uint8(ProposalStatus.Executed)));
space.execute(proposalId, emergencyStrategy.params);
}

function testGetStrategyType() public {
assertEq(emergency.getStrategyType(), "EmergencyQuorumExecution");
}
}

0 comments on commit 9a4640f

Please sign in to comment.