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.
300 lines
11 KiB
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);
|
|
}
|
|
|
|
|