Files
openzeppelin-contracts/certora/specs/GovernorPreventLateQuorum.spec
2023-02-28 23:33:12 +01:00

369 lines
13 KiB
Ruby

import "GovernorBase.spec"
import "GovernorCountingSimple.spec"
using ERC20VotesHarness as token
methods {
// envfree
quorumNumerator(uint256) returns uint256
quorumDenominator() returns uint256 envfree
// harness
getDeprecatedQuorumNumerator() returns uint256 envfree
getQuorumNumeratorLength() returns uint256 envfree
getQuorumNumeratorLatest() returns uint256 envfree
getExtendedDeadline(uint256) returns uint64 envfree
getPastTotalSupply(uint256) returns (uint256) envfree
}
////////////////////////////////////////////////////////////////////////////////
// Helper Functions //
////////////////////////////////////////////////////////////////////////////////
function setup(env e1, env e2) {
require getQuorumNumeratorLength() + 1 < max_uint;
require e2.block.number >= e1.block.number;
}
////////////////////////////////////////////////////////////////////////////////
//// #### Definitions //
////////////////////////////////////////////////////////////////////////////////
/// The proposal with proposal id `pId` has a deadline which is extendable.
definition deadlineExtendable(env e, uint256 pId) returns bool =
getExtendedDeadline(pId) == 0 && !quorumReached(e, pId);
/// The proposal with proposal id `pId` has a deadline which has been extended.
definition deadlineExtended(env e, uint256 pId) returns bool =
getExtendedDeadline(pId) > 0 && quorumReached(e, pId);
invariant deadlineExtendableConsistency(env e, uint256 pId)
!quorumReached(e, pId) => getExtendedDeadline(pId) == 0
invariant deadlineExtendedConsistency(env e, uint256 pId)
getExtendedDeadline(pId) > 0 => quorumReached(e, pId)
invariant proposalNotCreatedState(uint256 pId)
!proposalCreated(pId) => (getAgainstVotes(pId) == 0 && getAbstainVotes(pId) == 0 && getForVotes(pId) == 0)
/*
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Rule: `updateQuorumNumerator` can only change quorum requirements for future proposals. │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*/
rule quorumReachedCantChange(uint256 pId, env e, method f) filtered { f ->
!f.isFallback
&& !f.isView
&& f.selector != relay(address,uint256,bytes).selector
&& !castVoteSubset(f)
} {
bool quorumReachedBefore = quorumReached(e, pId);
uint256 newQuorumNumerator;
updateQuorumNumerator(e, newQuorumNumerator);
assert quorumReachedBefore == quorumReached(e, pId);
}
/*
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Rule: Casting a vote must not decrease any category's total number of votes and increase at least one category's │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*/
rule hasVotedCorrelationNonzero(uint256 pId, env e, method f, calldataarg args) filtered { f ->
!f.isFallback
&& !f.isView
&& f.selector != relay(address,uint256,bytes).selector
&& !castVoteSubset(f)
} {
require getVotes(e, e.msg.sender, proposalSnapshot(pId)) > 0; // assuming voter has non-zero voting power
uint256 againstBefore = votesAgainst();
uint256 forBefore = votesFor();
uint256 abstainBefore = votesAbstain();
bool hasVotedBefore = hasVoted(pId, e.msg.sender);
f(e, args);
uint256 againstAfter = votesAgainst();
uint256 forAfter = votesFor();
uint256 abstainAfter = votesAbstain();
bool hasVotedAfter = hasVoted(pId, e.msg.sender);
// want all vote categories to not decrease and at least one category to increase
assert
(!hasVotedBefore && hasVotedAfter) =>
(againstBefore <= againstAfter && forBefore <= forAfter && abstainBefore <= abstainAfter),
"after a vote is cast, the number of votes for each category must not decrease";
assert
(!hasVotedBefore && hasVotedAfter) =>
(againstBefore < againstAfter || forBefore < forAfter || abstainBefore < abstainAfter),
"after a vote is cast, the number of votes of at least one category must increase";
}
/*
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Rule: Voting against a proposal does not count towards quorum. │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*/
rule againstVotesDontCount(uint256 pId, env e, method f, calldataarg args) filtered { f ->
!f.isFallback
&& !f.isView
&& f.selector != relay(address,uint256,bytes).selector
&& !castVoteSubset(f)
} {
bool quorumBefore = quorumReached(e, pId);
uint256 againstBefore = votesAgainst();
f(e, args);
bool quorumAfter = quorumReached(e, pId);
uint256 againstAfter = votesAgainst();
assert againstBefore < againstAfter => quorumBefore == quorumAfter, "quorum must not be reached with an against vote";
}
/*
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Rule: │
│ * Deadline can never be reduced │
│ * If deadline increases then we are in `deadlineExtended` state and `castVote` was called. │
│ * A proposal's deadline can't change in `deadlineExtended` state. │
│ * A proposal's deadline can't be unextended. │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*/
rule deadlineChangeEffects(uint256 pId, env e, method f, calldataarg args) filtered { f ->
!f.isFallback
&& !f.isView
&& f.selector != relay(address,uint256,bytes).selector
&& !castVoteSubset(f)
} {
requireInvariant proposalStateConsistency(pId);
uint256 deadlineBefore = proposalDeadline(pId);
bool deadlineExtendedBefore = deadlineExtended(e, pId);
f(e, args);
uint256 deadlineAfter = proposalDeadline(pId);
bool deadlineExtendedAfter = deadlineExtended(e, pId);
// deadline can never be reduced
assert deadlineBefore <= proposalDeadline(pId);
// deadline can only be extended in proposal or on cast vote
assert (
deadlineAfter > deadlineBefore
) => (
(!deadlineExtendedBefore && !deadlineExtendedAfter && f.selector == propose(address[], uint256[], bytes[], string).selector)
||
(!deadlineExtendedBefore && deadlineExtendedAfter && f.selector == castVoteBySig(uint256, uint8,uint8, bytes32, bytes32).selector)
);
// a deadline can only be extended once
assert deadlineExtendedBefore => deadlineBefore == deadlineAfter;
// a deadline cannot be un-extended
assert deadlineExtendedBefore => deadlineExtendedAfter;
}
/// The proposal with proposal id `pId` has not been created.
definition proposalNotCreated(env e, uint256 pId) returns bool =
proposalSnapshot(pId) == 0
&& proposalDeadline(pId) == 0
&& getAgainstVotes(pId) == 0
&& getAbstainVotes(pId) == 0
&& getForVotes(pId) == 0;
/// Method f is a version of `castVote` whose state changing effects are covered by `castVoteBySig`.
/// @dev castVoteBySig allows anyone to cast a vote for anyone else if they can supply the signature. Specifically,
/// it covers the case where the msg.sender supplies a signature for themselves which is normally done using the normal
/// `castVote`.
definition castVoteSubset(method f) returns bool =
f.selector == castVote(uint256, uint8).selector ||
f.selector == castVoteWithReason(uint256, uint8, string).selector ||
f.selector == castVoteWithReasonAndParams(uint256,uint8,string,bytes).selector ||
f.selector == castVoteWithReasonAndParamsBySig(uint256,uint8,string,bytes,uint8,bytes32,bytes32).selector;
////////////////////////////////////////////////////////////////////////////////
//// ### Properties //
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// Invariants //
////////////////////////////////////////////////////////////////////////////////
/**
* A created proposal must be in state `deadlineExtendable` or `deadlineExtended`.
* @dev We assume the total supply of the voting token is non-zero
*/
invariant proposalInOneState(env e1, uint256 pId)
getPastTotalSupply(0) > 0 => (proposalNotCreated(e1, pId) || deadlineExtendable(e1, pId) || deadlineExtended(e1, pId))
filtered { f -> !f.isFallback && !f.isView && !castVoteSubset(f) && f.selector != relay(address,uint256,bytes).selector }
{
preserved with (env e2) {
setup(e1, e2);
}
}
/**
* The quorum numerator is always less than or equal to the quorum denominator.
*/
invariant quorumNumerLTEDenom(env e1, uint256 blockNumber)
quorumNumerator(e1, blockNumber) <= quorumDenominator()
{
preserved with (env e2) {
setup(e1, e2);
}
}
/**
* The deprecated quorum numerator variable `_quorumNumerator` is always 0 in a contract that is not upgraded.
*/
invariant deprecatedQuorumStateIsUninitialized()
getDeprecatedQuorumNumerator() == 0
/**
* If a proposal's deadline has been extended, then the proposal must have been created and reached quorum.
*/
invariant cantExtendWhenQuorumUnreached(env e2, uint256 pId)
getExtendedDeadline(pId) > 0 => (quorumReached(e2, pId) && proposalCreated(pId))
filtered { f -> !f.isFallback && !f.isView && !castVoteSubset(f) && f.selector != relay(address,uint256,bytes).selector }
{
preserved with (env e1) {
require e1.block.number > proposalSnapshot(pId);
setup(e1, e2);
}
}
/**
* The snapshot arrat keeping tracking of quorum numerators must never be uninitialized.
*/
invariant quorumLengthGt0(env e)
getQuorumNumeratorLength() > 0
filtered { f -> !f.isFallback && !f.isView && !castVoteSubset(f) && f.selector != relay(address,uint256,bytes).selector }
{
preserved {
setup(e,e);
}
}
/**
* If a proposal has reached quorum then the proposal snapshot (start `block.number`) must be non-zero
*/
invariant quorumReachedEffect(env e, uint256 pId)
quorumReached(e, pId) => proposalCreated(pId)
// filtered { f -> !f.isFallback && !f.isView && !castVoteSubset(f) && f.selector != relay(address,uint256,bytes).selector }
// {
// preserved with (env e2) {
// setup(e1, e2);
// }
// }