mirror of openzeppelin-contracts
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
openzeppelin-contracts/certora/specs/GovernorPreventLateQuorum.spec

300 lines
11 KiB

import "GovernorCountingSimple.spec"
using ERC721VotesHarness as erc721votes
methods {
quorumDenominator() returns uint256 envfree
votingPeriod() returns uint256 envfree
lateQuorumVoteExtension() returns uint64 envfree
propose(address[], uint256[], bytes[], string)
// harness
getExtendedDeadlineIsUnset(uint256) returns bool envfree
getExtendedDeadlineIsStarted(uint256) returns bool envfree
getExtendedDeadline(uint256) returns uint64 envfree
getAgainstVotes(uint256) returns uint256 envfree
getAbstainVotes(uint256) returns uint256 envfree
getForVotes(uint256) returns uint256 envfree
// more robust check than f.selector == _castVote(...).selector
latestCastVoteCall() returns uint256 envfree
// timelock dispatch
getMinDelay() returns uint256 => DISPATCHER(true)
hashOperationBatch(address[], uint256[], bytes[], bytes32, bytes32) => DISPATCHER(true)
executeBatch(address[], uint256[], bytes[], bytes32, bytes32) => CONSTANT
scheduleBatch(address[], uint256[], bytes[], bytes32, bytes32, uint256) => CONSTANT
// checkpoint length ERC721
numCheckpoints(address) returns uint32
}
//////////////////////////////////////////////////////////////////////////////
///////////////////////////// Helper Functions ///////////////////////////////
//////////////////////////////////////////////////////////////////////////////
function helperFunctionsWithRevertOnlyCastVote(uint256 proposalId, method f, env e) {
string reason; uint8 support; uint8 v; bytes32 r; bytes32 s; bytes params;
if (f.selector == castVote(uint256, uint8).selector) {
castVote@withrevert(e, proposalId, support);
} else if (f.selector == castVoteWithReason(uint256, uint8, string).selector) {
castVoteWithReason@withrevert(e, proposalId, support, reason);
} else if (f.selector == castVoteBySig(uint256, uint8,uint8, bytes32, bytes32).selector) {
castVoteBySig@withrevert(e, proposalId, support, v, r, s);
} else if (f.selector == castVoteWithReasonAndParamsBySig(uint256,uint8,string,bytes,uint8,bytes32,bytes32).selector) {
castVoteWithReasonAndParamsBySig@withrevert(e, proposalId, support, reason, params, v, r, s);
} else if (f.selector == castVoteWithReasonAndParams(uint256,uint8,string,bytes).selector) {
castVoteWithReasonAndParams@withrevert(e, proposalId, support, reason, params);
} else {
calldataarg args;
f@withrevert(e, args);
}
}
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////// Definitions /////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
// proposal deadline can be extended (but isn't)
definition deadlineExtendable(env e, uint256 pId) returns bool =
getExtendedDeadlineIsUnset(pId) // deadline == 0
&& !quorumReached(e, pId);
// proposal deadline has been extended
definition deadlineExtended(env e, uint256 pId) returns bool =
getExtendedDeadlineIsStarted(pId) // deadline > 0
&& quorumReached(e, pId);
definition proposalNotCreated(env e, uint256 pId) returns bool =
proposalSnapshot(pId) == 0
&& proposalDeadline(pId) == 0
&& getExtendedDeadlineIsUnset(pId)
&& getAgainstVotes(pId) == 0
&& getAbstainVotes(pId) == 0
&& getForVotes(pId) == 0
&& !quorumReached(e, pId);
//////////////////////////////////////////////////////////////////////////////
///////////////////////////////// Invariants /////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
/*
* I1: If a proposal has reached quorum then the proposal snapshot (start block.number) must be non-zero
* INVARIANT NOT PASSING // fails for updateQuorumNumerator and in the initial state when voting token total supply is 0 (causes quoromReached to return true)
* ADVANCED SANITY NOT RAN
*/
invariant quorumReachedEffect(env e, uint256 pId)
quorumReached(e, pId) => proposalCreated(pId) // bug: 0 supply 0 votes => quorumReached
// filtered { f -> f.selector != updateQuorumNumerator(uint256).selector } // * fails for this function
/*
* I2: A non-existant proposal must meet the definition of one.
* INVARIANT NOT PASSING // fails for updateQuorumNumerator and in the initial state when voting token total supply is 0 (causes quoromReached to return true)
* ADVANCED SANITY NOT RAN
*/
invariant proposalNotCreatedEffects(env e, uint256 pId)
!proposalCreated(pId) => proposalNotCreated(e, pId)
// filtered { f -> f.selector != updateQuorumNumerator(uint256).selector } // * fails for this function
/*
* I3: A created propsal must be in state deadlineExtendable or deadlineExtended.
* INVARIANT NOT PASSING // fails for updateQuorumNumerator and in the initial state when voting token total supply is 0 (causes quoromReached to return true)
* ADVANCED SANITY NOT RAN
*/
invariant proposalInOneState(env e, uint256 pId)
proposalNotCreated(e, pId) || deadlineExtendable(e, pId) || deadlineExtended(e, pId)
// filtered { f -> f.selector != updateQuorumNumerator(uint256).selector } // * fails for this function
{ preserved { requireInvariant proposalNotCreatedEffects(e, pId); }}
//////////////////////////////////////////////////////////////////////////////
////////////////////////////////// Rules /////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
///////////////////////////// first set of rules /////////////////////////////
// R1 and R2 are assumed in R3, so we prove them first
/*
* R1: If deadline increases then we are in deadlineExtended state and castVote was called.
* RULE PASSING
* ADVANCED SANITY PASSING
*/
rule deadlineChangeEffects(method f) filtered {f -> !f.isView} {
env e; calldataarg args; uint256 pId;
requireInvariant quorumReachedEffect(e, pId);
uint256 deadlineBefore = proposalDeadline(pId);
f(e, args);
uint256 deadlineAfter = proposalDeadline(pId);
assert(deadlineAfter > deadlineBefore => latestCastVoteCall() == e.block.number && deadlineExtended(e, pId));
}
/*
* R2: A proposal can't leave deadlineExtended state.
* RULE PASSING*
* ADVANCED SANITY PASSING
*/
rule deadlineCantBeUnextended(method f)
filtered {
f -> !f.isView
// && f.selector != updateQuorumNumerator(uint256).selector // * fails for this function
} {
env e; calldataarg args; uint256 pId;
require(deadlineExtended(e, pId));
requireInvariant quorumReachedEffect(e, pId);
f(e, args);
assert(deadlineExtended(e, pId));
}
/*
* R3: A proposal's deadline can't change in deadlineExtended state.
* RULE PASSING
* ADVANCED SANITY PASSING
*/
rule canExtendDeadlineOnce(method f) filtered {f -> !f.isView} {
env e; calldataarg args; uint256 pId;
require(deadlineExtended(e, pId));
requireInvariant quorumReachedEffect(e, pId);
uint256 deadlineBefore = proposalDeadline(pId);
f(e, args);
uint256 deadlineAfter = proposalDeadline(pId);
assert(deadlineBefore == deadlineAfter, "deadline can not be extended twice");
}
//////////////////////////// second set of rules ////////////////////////////
// HIGH LEVEL RULE R6: deadline can only extended if quorum reached w/ <= timeOfExtension left to vote
// I3, R4 and R5 are assumed in R6 so we prove them first
/*
* R4: A change in hasVoted must be correlated with an increasing of the vote supports, i.e. casting a vote increases the total number of votes.
* RULE PASSING
* ADVANCED SANITY PASSING
*/
rule hasVotedCorrelationNonzero(uint256 pId, method f, env e) filtered {f -> !f.isView} {
address acc = e.msg.sender;
require(getVotes(e, acc, proposalSnapshot(pId)) > 0); // assuming voter has non-zero voting power
uint256 againstBefore = votesAgainst();
uint256 forBefore = votesFor();
uint256 abstainBefore = votesAbstain();
bool hasVotedBefore = hasVoted(e, pId, acc);
helperFunctionsWithRevertOnlyCastVote(pId, f, e); // should be f(e, args)
uint256 againstAfter = votesAgainst();
uint256 forAfter = votesFor();
uint256 abstainAfter = votesAbstain();
bool hasVotedAfter = hasVoted(e, pId, acc);
// 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"; // currently vacous but keeping for CI tests
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";
}
/*
* R5: An against vote does not make a proposal reach quorum.
* RULE PASSING
* --ADVANCED SANITY PASSING vacuous but keeping
*/
rule againstVotesDontCount(method f) filtered {f -> !f.isView} {
env e; calldataarg args; uint256 pId;
address acc = e.msg.sender;
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";
}
/*
* R6: Deadline can only be extended from a `deadlineExtendible` state with quorum being reached with <= `lateQuorumVoteExtension` time left to vote
* RULE PASSING
* ADVANCED SANITY PASSING
*/
rule deadlineExtenededIfQuorumReached(method f) filtered {f -> !f.isView} {
env e; calldataarg args; uint256 pId;
requireInvariant proposalInOneState(e, pId);
requireInvariant quorumReachedEffect(e, pId);
requireInvariant proposalNotCreatedEffects(e, pId);
bool wasDeadlineExtendable = deadlineExtendable(e, pId);
uint64 extension = lateQuorumVoteExtension();
uint256 deadlineBefore = proposalDeadline(pId);
f(e, args);
uint256 deadlineAfter = proposalDeadline(pId);
assert deadlineAfter > deadlineBefore => wasDeadlineExtendable, "deadline must have been extendable for the deadline to be extended";
assert deadlineAfter > deadlineBefore => deadlineBefore - e.block.number <= extension, "deadline extension should not be used";
}
/*
* R7: `extendedDeadlineField` is set iff `_castVote` is called and quroum is reached.
* RULE PASSING
* ADVANCED SANITY PASSING
*/
rule extendedDeadlineValueSetIfQuorumReached(method f) filtered {f -> !f.isView} {
env e; calldataarg args; uint256 pId;
requireInvariant proposalInOneState(e, pId);
bool extendedBefore = deadlineExtended(e, pId);
f(e, args);
bool extendedAfter = deadlineExtended(e, pId);
uint256 extDeadline = getExtendedDeadline(pId);
assert(
!extendedBefore && extendedAfter
=> extDeadline == e.block.number + lateQuorumVoteExtension(),
"extended deadline was not set"
);
}
/*
* R8: Deadline can never be reduced.
* RULE PASSING
* ADVANCED SANITY PASSING
*/
rule deadlineNeverReduced(method f) filtered {f -> !f.isView} {
env e; calldataarg args; uint256 pId;
requireInvariant quorumReachedEffect(e, pId);
requireInvariant proposalNotCreatedEffects(e, pId);
uint256 deadlineBefore = proposalDeadline(pId);
f(e, args);
uint256 deadlineAfter = proposalDeadline(pId);
assert(deadlineAfter >= deadlineBefore);
}