GovernorPreventLateQuorum.spec 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. import "GovernorCountingSimple.spec"
  2. using ERC721VotesHarness as erc721votes
  3. methods {
  4. quorumDenominator() returns uint256 envfree
  5. votingPeriod() returns uint256 envfree
  6. lateQuorumVoteExtension() returns uint64 envfree
  7. propose(address[], uint256[], bytes[], string)
  8. // harness
  9. getExtendedDeadlineIsUnset(uint256) returns bool envfree
  10. getExtendedDeadlineIsStarted(uint256) returns bool envfree
  11. getExtendedDeadline(uint256) returns uint64 envfree
  12. // more robust check than f.selector == _castVote(...).selector
  13. latestCastVoteCall() returns uint256 envfree
  14. // timelock dispatch
  15. getMinDelay() returns uint256 => DISPATCHER(true)
  16. hashOperationBatch(address[], uint256[], bytes[], bytes32, bytes32) => DISPATCHER(true)
  17. executeBatch(address[], uint256[], bytes[], bytes32, bytes32) => CONSTANT
  18. scheduleBatch(address[], uint256[], bytes[], bytes32, bytes32, uint256) => CONSTANT
  19. }
  20. //////////////////////////////////////////////////////////////////////////////
  21. ///////////////////////////// Helper Functions ///////////////////////////////
  22. //////////////////////////////////////////////////////////////////////////////
  23. function helperFunctionsWithRevertOnlyCastVote(uint256 proposalId, method f, env e) {
  24. string reason; uint8 support; uint8 v; bytes32 r; bytes32 s; bytes params;
  25. if (f.selector == castVote(uint256, uint8).selector) {
  26. castVote@withrevert(e, proposalId, support);
  27. } else if (f.selector == castVoteWithReason(uint256, uint8, string).selector) {
  28. castVoteWithReason@withrevert(e, proposalId, support, reason);
  29. } else if (f.selector == castVoteBySig(uint256, uint8,uint8, bytes32, bytes32).selector) {
  30. castVoteBySig@withrevert(e, proposalId, support, v, r, s);
  31. } else if (f.selector == castVoteWithReasonAndParamsBySig(uint256,uint8,string,bytes,uint8,bytes32,bytes32).selector) {
  32. castVoteWithReasonAndParamsBySig@withrevert(e, proposalId, support, reason, params, v, r, s);
  33. } else if (f.selector == castVoteWithReasonAndParams(uint256,uint8,string,bytes).selector) {
  34. castVoteWithReasonAndParams@withrevert(e, proposalId, support, reason, params);
  35. } else {
  36. calldataarg args;
  37. f@withrevert(e, args);
  38. }
  39. }
  40. //////////////////////////////////////////////////////////////////////////////
  41. //////////////////////////////// Definitions /////////////////////////////////
  42. //////////////////////////////////////////////////////////////////////////////
  43. // proposal deadline can be extended (but isn't)
  44. definition deadlineExtendable(env e, uint256 pId) returns bool =
  45. getExtendedDeadlineIsUnset(pId)
  46. && !quorumReached(e, pId);
  47. // proposal deadline has been extended
  48. definition deadlineExtended(env e, uint256 pId) returns bool =
  49. getExtendedDeadlineIsStarted(pId)
  50. && quorumReached(e, pId);
  51. //////////////////////////////////////////////////////////////////////////////
  52. ///////////////////////////////// Invariants /////////////////////////////////
  53. //////////////////////////////////////////////////////////////////////////////
  54. /*
  55. * I1: A propsal must be in state deadlineExtendable or deadlineExtended.
  56. * --INVARIANT PASSING // fails for updateQuorumNumerator
  57. * --ADVANCED SANITY PASSING // can't sanity test failing rules, not sure how it works for invariants
  58. */
  59. invariant proposalInOneState(env e, uint256 pId)
  60. deadlineExtendable(e, pId) || deadlineExtended(e, pId)
  61. { preserved { require proposalCreated(pId); } }
  62. //////////////////////////////////////////////////////////////////////////////
  63. ////////////////////////////////// Rules /////////////////////////////////////
  64. //////////////////////////////////////////////////////////////////////////////
  65. ///////////////////////////// first set of rules /////////////////////////////
  66. // R1 and R2 are assumed in R3, so we prove them first
  67. /*
  68. * R1: If deadline increases then we are in deadlineExtended state and castVote was called.
  69. * RULE PASSING
  70. * ADVANCED SANITY PASSING
  71. */
  72. rule deadlineChangeEffects(method f) filtered {f -> !f.isView} {
  73. env e; calldataarg args; uint256 pId;
  74. require (proposalCreated(pId));
  75. uint256 deadlineBefore = proposalDeadline(pId);
  76. f(e, args);
  77. uint256 deadlineAfter = proposalDeadline(pId);
  78. assert(deadlineAfter > deadlineBefore => latestCastVoteCall() == e.block.number && deadlineExtended(e, pId));
  79. }
  80. /*
  81. * R2: A proposal can't leave deadlineExtended state.
  82. * RULE PASSING*
  83. * ADVANCED SANITY PASSING
  84. */
  85. rule deadlineCantBeUnextended(method f)
  86. filtered {
  87. f -> !f.isView
  88. && f.selector != updateQuorumNumerator(uint256).selector // * fails for this function
  89. } {
  90. env e; calldataarg args; uint256 pId;
  91. require(deadlineExtended(e, pId));
  92. require(proposalCreated(pId));
  93. f(e, args);
  94. assert(deadlineExtended(e, pId));
  95. }
  96. /*
  97. * R3: A proposal's deadline can't change in deadlineExtended state.
  98. * RULE PASSING*
  99. * ADVANCED SANITY PASSING
  100. */
  101. rule canExtendDeadlineOnce(method f) filtered {f -> !f.isView} {
  102. env e; calldataarg args; uint256 pId;
  103. require(deadlineExtended(e, pId));
  104. require(proposalCreated(pId));
  105. uint256 deadlineBefore = proposalDeadline(pId);
  106. f(e, args);
  107. uint256 deadlineAfter = proposalDeadline(pId);
  108. assert(deadlineBefore == deadlineAfter, "deadline can not be extended twice");
  109. }
  110. //////////////////////////// second set of rules ////////////////////////////
  111. // HIGH LEVEL RULE R6: deadline can only extended if quorum reached w/ <= timeOfExtension left to vote
  112. // I1, R4 and R5 are assumed in R6 so we prove them first
  113. /*
  114. * 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.
  115. * RULE PASSING
  116. * ADVANCED SANITY PASSING
  117. */
  118. rule hasVotedCorrelationNonzero(uint256 pId, method f, env e) filtered {f -> !f.isView} {
  119. address acc = e.msg.sender;
  120. require(getVotes(e, acc, proposalSnapshot(pId)) > 0); // assuming voter has non-zero voting power
  121. uint256 againstBefore = votesAgainst();
  122. uint256 forBefore = votesFor();
  123. uint256 abstainBefore = votesAbstain();
  124. bool hasVotedBefore = hasVoted(e, pId, acc);
  125. helperFunctionsWithRevertOnlyCastVote(pId, f, e);
  126. uint256 againstAfter = votesAgainst();
  127. uint256 forAfter = votesFor();
  128. uint256 abstainAfter = votesAbstain();
  129. bool hasVotedAfter = hasVoted(e, pId, acc);
  130. // want all vote categories to not decrease and at least one category to increase
  131. assert
  132. (!hasVotedBefore && hasVotedAfter) =>
  133. (againstBefore <= againstAfter && forBefore <= forAfter && abstainBefore <= abstainAfter),
  134. "no correlation: some category decreased"; // currently vacous but keeping for CI tests
  135. assert
  136. (!hasVotedBefore && hasVotedAfter) =>
  137. (againstBefore < againstAfter || forBefore < forAfter || abstainBefore < abstainAfter),
  138. "no correlation: no category increased";
  139. }
  140. /*
  141. * R5: An against vote does not make a proposal reach quorum.
  142. * RULE PASSING
  143. * --ADVANCED SANITY PASSING vacuous but keeping
  144. */
  145. rule againstVotesDontCount(method f) filtered {f -> !f.isView} {
  146. env e; calldataarg args; uint256 pId;
  147. address acc = e.msg.sender;
  148. bool quorumBefore = quorumReached(e, pId);
  149. uint256 againstBefore = votesAgainst();
  150. f(e, args);
  151. bool quorumAfter = quorumReached(e, pId);
  152. uint256 againstAfter = votesAgainst();
  153. assert (againstBefore < againstAfter) => quorumBefore == quorumAfter, "quorum reached with against vote";
  154. }
  155. /*
  156. * R6: Deadline can only be extended from a `deadlineExtendible` state with quorum being reached with <= `lateQuorumVoteExtension` time left to vote
  157. * RULE PASSING
  158. * ADVANCED SANITY PASSING
  159. */
  160. rule deadlineExtenededIfQuorumReached(method f) filtered {f -> !f.isView} {
  161. env e; calldataarg args; uint256 pId;
  162. // need invariant that proves that a propsal must be in state deadlineExtendable or deadlineExtended
  163. require(deadlineExtended(e, pId) || deadlineExtendable(e, pId));
  164. require(proposalCreated(pId));
  165. bool wasDeadlineExtendable = deadlineExtendable(e, pId);
  166. uint64 extension = lateQuorumVoteExtension();
  167. uint256 deadlineBefore = proposalDeadline(pId);
  168. f(e, args);
  169. uint256 deadlineAfter = proposalDeadline(pId);
  170. assert(deadlineAfter > deadlineBefore => wasDeadlineExtendable, "deadline was not extendable");
  171. assert(deadlineAfter > deadlineBefore => deadlineBefore - e.block.number <= extension, "deadline extension should not be used");
  172. }
  173. /*
  174. * R7: `extendedDeadlineField` is set iff `_castVote` is called and quroum is reached.
  175. * RULE PASSING
  176. * ADVANCED SANITY PASSING
  177. */
  178. rule extendedDeadlineValueSetIfQuorumReached(method f) filtered {f -> !f.isView} {
  179. env e; calldataarg args; uint256 pId;
  180. require(deadlineExtended(e, pId) || deadlineExtendable(e, pId));
  181. bool extendedBefore = deadlineExtended(e, pId);
  182. f(e, args);
  183. bool extendedAfter = deadlineExtended(e, pId);
  184. uint256 extDeadline = getExtendedDeadline(pId);
  185. assert(
  186. !extendedBefore && extendedAfter
  187. => extDeadline == e.block.number + lateQuorumVoteExtension(),
  188. "extended deadline was not set"
  189. );
  190. }
  191. /*
  192. * R8: If the deadline for a proposal has not been reached, users can still vote.
  193. * --RULE PASSING
  194. * --ADVANCED SANITY PASSING
  195. */
  196. rule canVote(method f) filtered {f -> !f.isView} {
  197. env e; calldataarg args; uint256 pId;
  198. address acc = e.msg.sender;
  199. uint256 deadline = proposalDeadline(pId);
  200. bool votedBefore = hasVoted(e, pId, acc);
  201. require(proposalCreated(pId));
  202. require(deadline >= e.block.number);
  203. // last error?
  204. helperFunctionsWithRevertOnlyCastVote(pId, f, e);
  205. bool votedAfter = hasVoted(e, pId, acc);
  206. assert !votedBefore && votedAfter => deadline >= e.block.number;
  207. }
  208. /*
  209. * R9: Deadline can never be reduced.
  210. * RULE PASSING
  211. * ADVANCED SANITY PASSING
  212. */
  213. rule deadlineNeverReduced(method f) filtered {f -> !f.isView} {
  214. env e; calldataarg args; uint256 pId;
  215. require(proposalCreated(pId));
  216. uint256 deadlineBefore = proposalDeadline(pId);
  217. f(e, args);
  218. uint256 deadlineAfter = proposalDeadline(pId);
  219. assert(deadlineAfter >= deadlineBefore);
  220. }