|
|
|
@ -1,111 +1,262 @@ |
|
|
|
|
import "GovernorBase.spec" |
|
|
|
|
import "GovernorCountingSimple.spec" |
|
|
|
|
|
|
|
|
|
using ERC20VotesHarness as token |
|
|
|
|
|
|
|
|
|
/*** |
|
|
|
|
## Verification of `GovernorPreventLateQuorum` |
|
|
|
|
|
|
|
|
|
`GovernorPreventLateQuorum` extends the Governor group of contracts to add the |
|
|
|
|
feature of giving voters more time to vote in the case that a proposal reaches |
|
|
|
|
quorum with less than `voteExtension` amount of time left to vote. |
|
|
|
|
|
|
|
|
|
### Assumptions and Simplifications |
|
|
|
|
|
|
|
|
|
None |
|
|
|
|
|
|
|
|
|
#### Harnessing |
|
|
|
|
- The contract that the specification was verified against is |
|
|
|
|
`GovernorPreventLateQuorumHarness`, which inherits from all of the Governor |
|
|
|
|
contracts — excluding Compound variations — and implements a number of view |
|
|
|
|
functions to gain access to values that are impossible/difficult to access in |
|
|
|
|
CVL. It also implements all of the required functions not implemented in the |
|
|
|
|
abstract contracts it inherits from. |
|
|
|
|
|
|
|
|
|
- `_castVote` was overridden to add an additional flag before calling the parent |
|
|
|
|
version. This flag stores the `block.number` in a variable |
|
|
|
|
`latestCastVoteCall` and is used as a way to check when any of variations of |
|
|
|
|
`castVote` are called. |
|
|
|
|
|
|
|
|
|
#### Munging |
|
|
|
|
|
|
|
|
|
- Various variables' visibility was changed from private to internal or from |
|
|
|
|
internal to public throughout the Governor contracts in order to make them |
|
|
|
|
accessible in the spec. |
|
|
|
|
|
|
|
|
|
- Arbitrary low level calls are assumed to change nothing and thus the function |
|
|
|
|
`_execute` is changed to do nothing. The tool normally havocs in this |
|
|
|
|
situation, assuming all storage can change due to possible reentrancy. We |
|
|
|
|
assume, however, there is no risk of reentrancy because `_execute` is a |
|
|
|
|
protected call locked behind the timelocked governance vote. All other |
|
|
|
|
governance functions are verified separately. |
|
|
|
|
*/ |
|
|
|
|
|
|
|
|
|
methods { |
|
|
|
|
// summarized |
|
|
|
|
hashProposal(address[],uint256[],bytes[],bytes32) returns (uint256) => NONDET |
|
|
|
|
_hashTypedDataV4(bytes32) returns (bytes32) |
|
|
|
|
|
|
|
|
|
// envfree |
|
|
|
|
quorumNumerator(uint256) returns uint256 |
|
|
|
|
quorumDenominator() returns uint256 envfree |
|
|
|
|
votingPeriod() returns uint256 envfree |
|
|
|
|
lateQuorumVoteExtension() returns uint64 envfree |
|
|
|
|
|
|
|
|
|
// harness |
|
|
|
|
getDeprecatedQuorumNumerator() returns uint256 envfree |
|
|
|
|
getQuorumNumeratorLength() returns uint256 envfree |
|
|
|
|
getQuorumNumeratorLatest() returns uint256 envfree |
|
|
|
|
getExtendedDeadlineIsUnset(uint256) returns bool envfree |
|
|
|
|
getExtendedDeadlineIsStarted(uint256) returns bool envfree |
|
|
|
|
getExtendedDeadline(uint256) returns uint64 envfree |
|
|
|
|
getPastTotalSupply(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 |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////// |
|
|
|
|
// 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 == castVoteBySig(uint256, uint8,uint8, bytes32, bytes32).selector) { |
|
|
|
|
castVoteBySig@withrevert(e, proposalId, support, v, r, s); |
|
|
|
|
} else { |
|
|
|
|
calldataarg args; |
|
|
|
|
f@withrevert(e, args); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
/// Restricting out common reasons why rules break. We assume quorum length won't overflow (uint256) and that functions |
|
|
|
|
/// called in env `e2` have a `block.number` greater than or equal `e1`'s `block.number`. |
|
|
|
|
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 = |
|
|
|
|
getExtendedDeadlineIsUnset(pId) // deadline == 0 |
|
|
|
|
&& !quorumReached(e, pId); |
|
|
|
|
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 = |
|
|
|
|
getExtendedDeadlineIsStarted(pId) // deadline > 0 |
|
|
|
|
&& quorumReached(e, pId); |
|
|
|
|
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 = |
|
|
|
@ -121,9 +272,9 @@ definition proposalNotCreated(env e, uint256 pId) returns bool = |
|
|
|
|
/// `castVote`. |
|
|
|
|
definition castVoteSubset(method f) returns bool = |
|
|
|
|
f.selector == castVote(uint256, uint8).selector || |
|
|
|
|
f.selector == castVoteWithReason(uint256, uint8, string).selector || |
|
|
|
|
f.selector == castVoteWithReasonAndParamsBySig(uint256,uint8,string,bytes,uint8,bytes32,bytes32).selector || |
|
|
|
|
f.selector == castVoteWithReasonAndParams(uint256,uint8,string,bytes).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; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////// |
|
|
|
@ -135,6 +286,20 @@ definition castVoteSubset(method f) returns bool = |
|
|
|
|
// Invariants // |
|
|
|
|
//////////////////////////////////////////////////////////////////////////////// |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* A created proposal must be in state `deadlineExtendable` or `deadlineExtended`. |
|
|
|
|
* @dev We assume the total supply of the voting token is non-zero |
|
|
|
@ -169,12 +334,14 @@ invariant deprecatedQuorumStateIsUninitialized() |
|
|
|
|
* If a proposal's deadline has been extended, then the proposal must have been created and reached quorum. |
|
|
|
|
*/ |
|
|
|
|
invariant cantExtendWhenQuorumUnreached(env e2, uint256 pId) |
|
|
|
|
getExtendedDeadlineIsStarted(pId) => quorumReached(e2, pId) && proposalCreated(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); |
|
|
|
|
}} |
|
|
|
|
{ |
|
|
|
|
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. |
|
|
|
@ -182,176 +349,20 @@ invariant cantExtendWhenQuorumUnreached(env e2, uint256 pId) |
|
|
|
|
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 e1, uint256 pId) |
|
|
|
|
quorumReached(e1, pId) && getPastTotalSupply(0) > 0 => proposalCreated(pId) |
|
|
|
|
filtered { f -> !f.isFallback && !f.isView && !castVoteSubset(f) && f.selector != relay(address,uint256,bytes).selector } |
|
|
|
|
{ |
|
|
|
|
preserved with (env e2) { |
|
|
|
|
setup(e1, e2); |
|
|
|
|
preserved { |
|
|
|
|
setup(e,e); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////// |
|
|
|
|
// Rules // |
|
|
|
|
////////////////////////////////////////////////////////////////////////////// |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* `updateQuorumNumerator` can only change quorum requirements for future proposals. |
|
|
|
|
* @dev In the case that the array containing past quorum numerators overflows, this rule will fail. |
|
|
|
|
*/ |
|
|
|
|
rule quorumReachedCantChange(method f) filtered { |
|
|
|
|
f -> !f.isFallback && !f.isView && !castVoteSubset(f) && f.selector != relay(address,uint256,bytes).selector |
|
|
|
|
} { |
|
|
|
|
env e1; uint256 pId; |
|
|
|
|
bool _quorumReached = quorumReached(e1, pId); |
|
|
|
|
|
|
|
|
|
env e2; uint256 newQuorumNumerator; |
|
|
|
|
setup(e1, e2); |
|
|
|
|
updateQuorumNumerator(e2, newQuorumNumerator); |
|
|
|
|
|
|
|
|
|
env e3; |
|
|
|
|
bool quorumReached_ = quorumReached(e3, pId); |
|
|
|
|
|
|
|
|
|
assert _quorumReached == quorumReached_, "function changed quorumReached"; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Casting a vote must not decrease any category's total number of votes and increase at least one category's. |
|
|
|
|
*/ |
|
|
|
|
rule hasVotedCorrelationNonzero(uint256 pId, method f, env e) filtered { |
|
|
|
|
f -> !f.isFallback && !f.isView && !castVoteSubset(f) && f.selector != relay(address,uint256,bytes).selector |
|
|
|
|
} { |
|
|
|
|
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(pId, acc); |
|
|
|
|
|
|
|
|
|
helperFunctionsWithRevertOnlyCastVote(pId, f, e); // should be f(e, args) |
|
|
|
|
|
|
|
|
|
uint256 againstAfter = votesAgainst(); |
|
|
|
|
uint256 forAfter = votesFor(); |
|
|
|
|
uint256 abstainAfter = votesAbstain(); |
|
|
|
|
|
|
|
|
|
bool hasVotedAfter = hasVoted(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"; |
|
|
|
|
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"; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Voting against a proposal does not count towards quorum. |
|
|
|
|
*/ |
|
|
|
|
rule againstVotesDontCount(method f) filtered { |
|
|
|
|
f -> !f.isFallback && !f.isView && !castVoteSubset(f) && f.selector != relay(address,uint256,bytes).selector |
|
|
|
|
} { |
|
|
|
|
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"; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Deadline can never be reduced. |
|
|
|
|
*/ |
|
|
|
|
rule deadlineNeverReduced(method f) filtered { |
|
|
|
|
f -> !f.isFallback && !f.isView && !castVoteSubset(f) && f.selector != relay(address,uint256,bytes).selector |
|
|
|
|
} { |
|
|
|
|
env e1; env e2; calldataarg args; uint256 pId; |
|
|
|
|
|
|
|
|
|
requireInvariant quorumReachedEffect(e1, pId); |
|
|
|
|
require proposalCreated(pId); |
|
|
|
|
setup(e1, e2); |
|
|
|
|
|
|
|
|
|
uint256 deadlineBefore = proposalDeadline(pId); |
|
|
|
|
f(e2, args); |
|
|
|
|
uint256 deadlineAfter = proposalDeadline(pId); |
|
|
|
|
|
|
|
|
|
assert(deadlineAfter >= deadlineBefore); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
//// The rules [`deadlineChangeEffects`](#deadlineChangeEffects) and [`deadlineCantBeUnextended`](#deadlineCantBeUnextended) |
|
|
|
|
//// are assumed in rule [`canExtendDeadlineOnce`](#canExtendDeadlineOnce), so we prove them first. |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* If deadline increases then we are in `deadlineExtended` state and `castVote` |
|
|
|
|
* was called. |
|
|
|
|
*/ |
|
|
|
|
rule deadlineChangeEffects(method f) filtered { |
|
|
|
|
f -> !f.isFallback && !f.isView && !castVoteSubset(f) && f.selector != relay(address,uint256,bytes).selector |
|
|
|
|
} { |
|
|
|
|
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)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* @title Deadline can't be unextended |
|
|
|
|
* @notice A proposal can't leave `deadlineExtended` state. |
|
|
|
|
*/ |
|
|
|
|
rule deadlineCantBeUnextended(method f) filtered { |
|
|
|
|
f -> !f.isFallback && !f.isView && !castVoteSubset(f) && f.selector != relay(address,uint256,bytes).selector |
|
|
|
|
} { |
|
|
|
|
env e1; env e2; env e3; env e4; calldataarg args; uint256 pId; |
|
|
|
|
setup(e1, e2); |
|
|
|
|
|
|
|
|
|
require(deadlineExtended(e1, pId)); |
|
|
|
|
requireInvariant quorumReachedEffect(e1, pId); |
|
|
|
|
|
|
|
|
|
f(e2, args); |
|
|
|
|
|
|
|
|
|
assert(deadlineExtended(e1, pId)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* A proposal's deadline can't change in `deadlineExtended` state. |
|
|
|
|
* If a proposal has reached quorum then the proposal snapshot (start `block.number`) must be non-zero |
|
|
|
|
*/ |
|
|
|
|
rule canExtendDeadlineOnce(method f) filtered { |
|
|
|
|
f -> !f.isFallback && !f.isView && !castVoteSubset(f) && f.selector != relay(address,uint256,bytes).selector |
|
|
|
|
} { |
|
|
|
|
env e1; env e2; calldataarg args; uint256 pId; |
|
|
|
|
|
|
|
|
|
require(deadlineExtended(e1, pId)); |
|
|
|
|
require(proposalSnapshot(pId) > 0); |
|
|
|
|
requireInvariant quorumReachedEffect(e1, pId); |
|
|
|
|
setup(e1, e2); |
|
|
|
|
|
|
|
|
|
uint256 deadlineBefore = proposalDeadline(pId); |
|
|
|
|
f(e2, args); |
|
|
|
|
uint256 deadlineAfter = proposalDeadline(pId); |
|
|
|
|
|
|
|
|
|
assert(deadlineBefore == deadlineAfter, "deadline can not be extended twice"); |
|
|
|
|
} |
|
|
|
|
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); |
|
|
|
|
// } |
|
|
|
|
// } |
|
|
|
|