cleanup GovernorBase.spec

This commit is contained in:
Hadrien Croubois
2023-02-28 14:04:27 +01:00
parent d4e9d8d54d
commit 2e7bca424a
2 changed files with 169 additions and 216 deletions

View File

@ -43,21 +43,20 @@ methods {
voteSucceeded(uint256) returns bool envfree voteSucceeded(uint256) returns bool envfree
} }
////////////////////////////////////////////////////////////////////////////// /*
//////////////////////////////// Definitions ///////////////////////////////// ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
////////////////////////////////////////////////////////////////////////////// Definitions
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*/
// proposal was created - relation proved in noStartBeforeCreation
definition proposalCreated(uint256 pId) returns bool = definition proposalCreated(uint256 pId) returns bool =
proposalSnapshot(pId) > 0 proposalSnapshot(pId) > 0 && proposalDeadline(pId) > 0;
&& proposalDeadline(pId) > 0;
//////////////////////////////////////////////////////////////////////////////
///////////////////////////// Helper Functions ///////////////////////////////
//////////////////////////////////////////////////////////////////////////////
/*
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Helper functions
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*/
function helperFunctionsWithRevert(uint256 proposalId, method f, env e) { function helperFunctionsWithRevert(uint256 proposalId, method f, env e) {
address[] targets; uint256[] values; bytes[] calldatas; string reason; bytes32 descriptionHash; address[] targets; uint256[] values; bytes[] calldatas; string reason; bytes32 descriptionHash;
uint8 support; uint8 v; bytes32 r; bytes32 s; bytes params; uint8 support; uint8 v; bytes32 r; bytes32 s; bytes params;
@ -110,268 +109,224 @@ function helperFunctionsWithRevert(uint256 proposalId, method f, env e) {
} }
/* /*
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
///////////////////////////////////////////////////// State Diagram ////////////////////////////////////////////////////////// Invariant: Votes start and end are either initialized (non zero) or uninitialized (zero) simultaneously
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// // This invariant assumes that the block number cannot be 0 at any stage of the contract cycle
// castVote(s)() // This is very safe assumption as usually the 0 block is genesis block which is uploaded with data
// ------------- propose() ---------------------- time pass --------------- time passes ----------- // by the developers and will not be valid to raise proposals (at the current way that block chain is functioning)
// | No Proposal | --------> | Before Start (Delay) | --------> | Voting Period | ----------------------> | execute() | // └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
// ------------- ---------------------- --------------- -> Executed/Canceled ----------- //
// ------------------------------------------------------------|---------------|-------------------------|--------------> //
// t start end timelock //
// //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
*/ */
///////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////// Global Valid States /////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////
/*
* Start and end date are either initialized (non zero) or uninitialized (zero) simultaneously
* This invariant assumes that the block number cannot be 0 at any stage of the contract cycle
* This is very safe assumption as usually the 0 block is genesis block which is uploaded with data
* by the developers and will not be valid to raise proposals (at the current way that block chain is functioning)
*/
// To use env with general preserved block disable type checking [--disableLocalTypeChecking]
invariant startAndEndDatesNonZero(uint256 pId) invariant startAndEndDatesNonZero(uint256 pId)
proposalSnapshot(pId) != 0 <=> proposalDeadline(pId) != 0 proposalSnapshot(pId) != 0 <=> proposalDeadline(pId) != 0
{ preserved with (env e){ {
require e.block.number > 0; preserved with (env e) {
}} require e.block.number > 0;
}
}
/* /*
* If a proposal is canceled it must have a start and an end date ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
*/ Invariant: cancel => created
// To use env with general preserved block disable type checking [--disableLocalTypeChecking] └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
invariant canceledImplyStartAndEndDateNonZero(uint pId) */
isCanceled(pId) => proposalSnapshot(pId) != 0 invariant canceledImplyCreated(uint pId)
{preserved with (env e){ isCanceled(pId) => proposalCreated(pId)
require e.block.number > 0; {
}} preserved with (env e) {
/*
* If a proposal is executed it must have a start and an end date
*/
// To use env with general preserved block disable type checking [--disableLocalTypeChecking]
invariant executedImplyStartAndEndDateNonZero(uint pId)
isExecuted(pId) => proposalSnapshot(pId) != 0
{ preserved with (env e){
requireInvariant startAndEndDatesNonZero(pId); requireInvariant startAndEndDatesNonZero(pId);
require e.block.number > 0; require e.block.number > 0;
}} }
}
/* /*
* A proposal starting block number must be less or equal than the proposal end date ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
*/ Invariant: executed => created
invariant voteStartBeforeVoteEnd(uint256 pId) └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
// from < to <= because snapshot and deadline can be the same block number if delays are set to 0 */
// This is possible before the integration of GovernorSettings.sol to the system. invariant executedImplyCreated(uint pId)
// After integration of GovernorSettings.sol the invariant expression should be changed from <= to < isExecuted(pId) => proposalCreated(pId)
(proposalSnapshot(pId) > 0 => proposalSnapshot(pId) <= proposalDeadline(pId)) {
// (proposalSnapshot(pId) > 0 => proposalSnapshot(pId) <= proposalDeadline(pId)) preserved with (env e) {
{ preserved {
requireInvariant startAndEndDatesNonZero(pId); requireInvariant startAndEndDatesNonZero(pId);
}} require e.block.number > 0;
}
}
/* /*
* A proposal cannot be both executed and canceled simultaneously. ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
*/ Invariant: Votes start before it ends
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*/
invariant voteStartBeforeVoteEnd(uint256 pId)
//proposalCreated(pId) => proposalSnapshot(pId) <= proposalDeadline(pId)
proposalSnapshot(pId) <= proposalDeadline(pId)
{
preserved {
requireInvariant startAndEndDatesNonZero(pId);
}
}
/*
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Invariant: A proposal cannot be both executed and canceled simultaneously
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*/
invariant noBothExecutedAndCanceled(uint256 pId) invariant noBothExecutedAndCanceled(uint256 pId)
!isExecuted(pId) || !isCanceled(pId) !isExecuted(pId) || !isCanceled(pId)
/* /*
* A proposal could be executed only if quorum was reached and vote succeeded ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
*/ Rule: A proposal could be executed only if quorum was reached and vote succeeded
rule executionOnlyIfQuoromReachedAndVoteSucceeded(uint256 pId, env e, method f){ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
bool isExecutedBefore = isExecuted(pId); */
rule executionOnlyIfQuoromReachedAndVoteSucceeded(uint256 pId, env e, method f, calldataarg args) {
require(!isExecuted(pId));
bool quorumReachedBefore = quorumReached(e, pId); bool quorumReachedBefore = quorumReached(e, pId);
bool voteSucceededBefore = voteSucceeded(pId); bool voteSucceededBefore = voteSucceeded(pId);
calldataarg args;
f(e, args); f(e, args);
bool isExecutedAfter = isExecuted(pId); assert isExecuted(pId) => (quorumReachedBefore && voteSucceededBefore), "quorum was changed";
assert (!isExecutedBefore && isExecutedAfter) => (quorumReachedBefore && voteSucceededBefore), "quorum was changed";
} }
///////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////// In-State Rules /////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////
//==========================================
//------------- Voting Period --------------
//==========================================
/* /*
* A user cannot vote twice ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
*/ Rule: A user cannot vote twice
// Checked for castVote only. all 3 castVote functions call _castVote, so the completeness of the verification is counted on
// the fact that the 3 functions themselves makes no changes, but rather call an internal function to execute. Checked for castVote only. all 3 castVote functions call _castVote, so the completeness of the verification is
// That means that we do not check those 3 functions directly, however for castVote & castVoteWithReason it is quite trivial counted on the fact that the 3 functions themselves makes no changes, but rather call an internal function to
// to understand why this is ok. For castVoteBySig we basically assume that the signature referendum is correct without checking it. execute. That means that we do not check those 3 functions directly, however for castVote & castVoteWithReason it
// We could check each function separately and pass the rule, but that would have uglyfied the code with no concrete is quite trivial to understand why this is ok. For castVoteBySig we basically assume that the signature referendum
// benefit, as it is evident that nothing is happening in the first 2 functions (calling a view function), and we do not desire to check the signature verification. is correct without checking it. We could check each function separately and pass the rule, but that would have
rule doubleVoting(uint256 pId, uint8 sup, method f) { uglyfied the code with no concrete benefit, as it is evident that nothing is happening in the first 2 functions
env e; (calling a view function), and we do not desire to check the signature verification.
address user = e.msg.sender; └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
bool votedCheck = hasVoted(pId, user); rule noDoubleVoting(uint256 pId, env e, uint8 sup) {
bool votedCheck = hasVoted(pId, e.msg.sender);
castVote@withrevert(e, pId, sup); castVote@withrevert(e, pId, sup);
assert votedCheck => lastReverted, "double voting occurred"; assert votedCheck => lastReverted, "double voting occurred";
} }
///////////////////////////////////////////////////////////////////////////////////////
//////////////////////////// State Transitions Rules //////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////
//===========================================
//-------- Propose() --> End of Time --------
//===========================================
/* /*
* Once a proposal is created, voteStart and voteEnd are immutable ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
*/ Rule: Once a proposal is created, voteStart and voteEnd are immutable
rule immutableFieldsAfterProposalCreation(uint256 pId, method f) { └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
uint256 _voteStart = proposalSnapshot(pId); */
uint256 _voteEnd = proposalDeadline(pId); rule immutableFieldsAfterProposalCreation(uint256 pId, env e, method f, calldataarg arg) {
require proposalCreated(pId);
require proposalCreated(pId); // startDate > 0 uint256 voteStart = proposalSnapshot(pId);
uint256 voteEnd = proposalDeadline(pId);
env e; calldataarg arg;
f(e, arg); f(e, arg);
uint256 voteStart_ = proposalSnapshot(pId); assert voteStart == proposalSnapshot(pId), "Start date was changed";
uint256 voteEnd_ = proposalDeadline(pId); assert voteEnd == proposalDeadline(pId), "End date was changed";
assert _voteStart == voteStart_, "Start date was changed";
assert _voteEnd == voteEnd_, "End date was changed";
} }
/* /*
* Voting cannot start at a block number prior to proposals creation block number ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
*/ Rule: Voting cannot start at a block number prior to proposals creation block number
rule noStartBeforeCreation(uint256 pId) { └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
uint256 previousStart = proposalSnapshot(pId); */
// This line makes sure that we see only cases where start date is changed from 0, i.e. creation of proposal rule noStartBeforeCreation(uint256 pId, env e, method f, calldataarg args){
// We proved in immutableFieldsAfterProposalCreation that once dates set for proposal, it cannot be changed require !proposalCreated(pId);
require !proposalCreated(pId); // previousStart == 0; f(e, args);
assert proposalCreated(pId) => proposalSnapshot(pId) >= e.block.number, "starts before proposal";
env e; calldataarg args;
propose(e, args);
uint256 newStart = proposalSnapshot(pId);
// if created, start is after current block number (creation block)
assert(newStart != previousStart => newStart >= e.block.number);
} }
//============================================
//--- End of Voting Period --> End of Time ---
//============================================
/* /*
* A proposal cannot be executed before it ends ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
*/ Rule: A proposal cannot be executed before it ends
// By induction it cannot be executed before it starts, due to voteStartBeforeVoteEnd └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
rule noExecuteBeforeDeadline(uint256 pId, env e, method f, calldataarg args){ */
rule noExecuteBeforeDeadline(uint256 pId, env e, method f, calldataarg args) {
require !isExecuted(pId); require !isExecuted(pId);
f(e, args); f(e, args);
assert isExecuted(pId) => (e.block.number >= proposalDeadline(pId)), "executed before deadline"; assert isExecuted(pId) => proposalDeadline(pId) <= e.block.number, "executed before deadline";
}
////////////////////////////////////////////////////////////////////////////////
////////////////////// Integrity Of Functions (Unit Tests) /////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////// High Level Rules ////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
///////////////////////////// Not Categorized Yet //////////////////////////////
////////////////////////////////////////////////////////////////////////////////
/*
* All proposal specific (non-view) functions should revert if proposal is executed
*/
// In this rule we show that if a function is executed, i.e. execute() was called on the proposal ID,
// non of the proposal specific functions can make changes again. In executedOnlyAfterExecuteFunc
// we connected the executed attribute to the execute() function, showing that only execute() can
// change it, and that it will always change it.
rule allFunctionsRevertIfExecuted(method f) filtered { f ->
!f.isView && !f.isFallback
&& f.selector != updateTimelock(address).selector
&& f.selector != updateQuorumNumerator(uint256).selector
&& f.selector != queue(address[],uint256[],bytes[],bytes32).selector
&& f.selector != relay(address,uint256,bytes).selector
&& f.selector != 0xb9a61961 // __acceptAdmin()
&& f.selector != onERC721Received(address,address,uint256,bytes).selector
&& f.selector != onERC1155Received(address,address,uint256,uint256,bytes).selector
&& f.selector != onERC1155BatchReceived(address,address,uint256[],uint256[],bytes).selector
} {
env e; calldataarg args;
uint256 pId;
require(isExecuted(pId));
requireInvariant noBothExecutedAndCanceled(pId);
requireInvariant executedImplyStartAndEndDateNonZero(pId);
helperFunctionsWithRevert(pId, f, e);
assert(lastReverted, "Function was not reverted");
} }
/* /*
* All proposal specific (non-view) functions should revert if proposal is canceled ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
*/ Rule: All proposal specific (non-view) functions should revert if proposal is executed
rule allFunctionsRevertIfCanceled(method f) filtered {
In this rule we show that if a function is executed, i.e. execute() was called on the proposal ID, non of the
proposal specific functions can make changes again. In executedOnlyAfterExecuteFunc we connected the executed
attribute to the execute() function, showing that only execute() can change it, and that it will always change it.
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*/
rule allFunctionsRevertIfExecuted(uint256 pId, env e, method f, calldataarg args) filtered {
f -> !f.isView && !f.isFallback f -> !f.isView && !f.isFallback
&& f.selector != updateTimelock(address).selector && f.selector != updateTimelock(address).selector
&& f.selector != updateQuorumNumerator(uint256).selector && f.selector != updateQuorumNumerator(uint256).selector
&& f.selector != queue(address[],uint256[],bytes[],bytes32).selector
&& f.selector != relay(address,uint256,bytes).selector && f.selector != relay(address,uint256,bytes).selector
&& f.selector != 0xb9a61961 // __acceptAdmin() && f.selector != 0xb9a61961 // __acceptAdmin()
&& f.selector != onERC721Received(address,address,uint256,bytes).selector && f.selector != onERC721Received(address,address,uint256,bytes).selector
&& f.selector != onERC1155Received(address,address,uint256,uint256,bytes).selector && f.selector != onERC1155Received(address,address,uint256,uint256,bytes).selector
&& f.selector != onERC1155BatchReceived(address,address,uint256[],uint256[],bytes).selector && f.selector != onERC1155BatchReceived(address,address,uint256[],uint256[],bytes).selector
} { } {
env e; calldataarg args; require isExecuted(pId);
uint256 pId;
require(isCanceled(pId));
requireInvariant noBothExecutedAndCanceled(pId); requireInvariant noBothExecutedAndCanceled(pId);
requireInvariant canceledImplyStartAndEndDateNonZero(pId); requireInvariant executedImplyCreated(pId);
helperFunctionsWithRevert(pId, f, e); helperFunctionsWithRevert(pId, f, e);
assert(lastReverted, "Function was not reverted"); assert lastReverted, "Function was not reverted";
} }
/* /*
* Proposal can be switched to executed only via execute() function ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
*/ Rule: All proposal specific (non-view) functions should revert if proposal is canceled
rule executedOnlyAfterExecuteFunc(address[] targets, uint256[] values, bytes[] calldatas, bytes32 descriptionHash, method f) {
env e; calldataarg args; In this rule we show that if a function is executed, i.e. execute() was called on the proposal ID, non of the
uint256 pId; proposal specific functions can make changes again. In executedOnlyAfterExecuteFunc we connected the executed
bool executedBefore = isExecuted(pId); attribute to the execute() function, showing that only execute() can change it, and that it will always change it.
require(!executedBefore); └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*/
rule allFunctionsRevertIfCanceled(uint256 pId, env e, method f, calldataarg args) filtered {
f -> !f.isView && !f.isFallback
&& f.selector != updateTimelock(address).selector
&& f.selector != updateQuorumNumerator(uint256).selector
&& f.selector != relay(address,uint256,bytes).selector
&& f.selector != 0xb9a61961 // __acceptAdmin()
&& f.selector != onERC721Received(address,address,uint256,bytes).selector
&& f.selector != onERC1155Received(address,address,uint256,uint256,bytes).selector
&& f.selector != onERC1155BatchReceived(address,address,uint256[],uint256[],bytes).selector
} {
require isCanceled(pId);
requireInvariant noBothExecutedAndCanceled(pId);
requireInvariant canceledImplyCreated(pId);
helperFunctionsWithRevert(pId, f, e); helperFunctionsWithRevert(pId, f, e);
bool executedAfter = isExecuted(pId); assert lastReverted, "Function was not reverted";
assert(executedAfter != executedBefore => f.selector == execute(address[], uint256[], bytes[], bytes32).selector, "isExecuted only changes in the execute method"); }
/*
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Rule: Proposal can be switched state only by specific functions
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*/
rule proposedOnlyAfterProposeFunc(uint256 pId, env e, method f) {
bool createdBefore = proposalCreated(pId);
bool executedBefore = isExecuted(pId);
bool canceledBefore = isCanceled(pId);
helperFunctionsWithRevert(pId, f, e);
assert (proposalCreated(pId) != createdBefore)
=> (createdBefore == false && f.selector == propose(address[], uint256[], bytes[], string).selector),
"proposalCreated only changes in the propose method";
assert (isExecuted(pId) != executedBefore)
=> (executedBefore == false && f.selector == execute(address[], uint256[], bytes[], bytes32).selector),
"isExecuted only changes in the execute method";
assert (isCanceled(pId) != canceledBefore)
=> (canceledBefore == false && f.selector == cancel(address[], uint256[], bytes[], bytes32).selector),
"isCanceled only changes in the cancel method";
} }

View File

@ -6,9 +6,7 @@ How it works:
- If all execution paths are reverting, we never call the assertion, and the method will pass this rule vacuously. - If all execution paths are reverting, we never call the assertion, and the method will pass this rule vacuously.
*/ */
rule sanity(method f) { rule sanity(env e, method f, calldataarg arg) {
env e;
calldataarg arg;
f(e, arg); f(e, arg);
assert false; assert false;
} }