GovernorPreventLateQuorum.spec 11 KB

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