GovernorPreventLateQuorum.spec 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. import "GovernorBase.spec"
  2. import "GovernorCountingSimple.spec"
  3. using ERC20VotesHarness as token
  4. methods {
  5. // envfree
  6. quorumNumerator(uint256) returns uint256
  7. quorumDenominator() returns uint256 envfree
  8. // harness
  9. getDeprecatedQuorumNumerator() returns uint256 envfree
  10. getQuorumNumeratorLength() returns uint256 envfree
  11. getQuorumNumeratorLatest() returns uint256 envfree
  12. getExtendedDeadline(uint256) returns uint64 envfree
  13. getPastTotalSupply(uint256) returns (uint256) envfree
  14. }
  15. ////////////////////////////////////////////////////////////////////////////////
  16. // Helper Functions //
  17. ////////////////////////////////////////////////////////////////////////////////
  18. function setup(env e1, env e2) {
  19. require getQuorumNumeratorLength() + 1 < max_uint;
  20. require e2.block.number >= e1.block.number;
  21. }
  22. ////////////////////////////////////////////////////////////////////////////////
  23. //// #### Definitions //
  24. ////////////////////////////////////////////////////////////////////////////////
  25. /// The proposal with proposal id `pId` has a deadline which is extendable.
  26. definition deadlineExtendable(env e, uint256 pId) returns bool =
  27. getExtendedDeadline(pId) == 0 && !quorumReached(e, pId);
  28. /// The proposal with proposal id `pId` has a deadline which has been extended.
  29. definition deadlineExtended(env e, uint256 pId) returns bool =
  30. getExtendedDeadline(pId) > 0 && quorumReached(e, pId);
  31. invariant deadlineExtendableConsistency(env e, uint256 pId)
  32. !quorumReached(e, pId) => getExtendedDeadline(pId) == 0
  33. invariant deadlineExtendedConsistency(env e, uint256 pId)
  34. getExtendedDeadline(pId) > 0 => quorumReached(e, pId)
  35. invariant proposalNotCreatedState(uint256 pId)
  36. !proposalCreated(pId) => (getAgainstVotes(pId) == 0 && getAbstainVotes(pId) == 0 && getForVotes(pId) == 0)
  37. /*
  38. โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  39. โ”‚ Rule: `updateQuorumNumerator` can only change quorum requirements for future proposals. โ”‚
  40. โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
  41. */
  42. rule quorumReachedCantChange(uint256 pId, env e, method f) filtered { f ->
  43. !f.isFallback
  44. && !f.isView
  45. && f.selector != relay(address,uint256,bytes).selector
  46. && !castVoteSubset(f)
  47. } {
  48. bool quorumReachedBefore = quorumReached(e, pId);
  49. uint256 newQuorumNumerator;
  50. updateQuorumNumerator(e, newQuorumNumerator);
  51. assert quorumReachedBefore == quorumReached(e, pId);
  52. }
  53. /*
  54. โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  55. โ”‚ Rule: Casting a vote must not decrease any category's total number of votes and increase at least one category's โ”‚
  56. โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
  57. */
  58. rule hasVotedCorrelationNonzero(uint256 pId, env e, method f, calldataarg args) filtered { f ->
  59. !f.isFallback
  60. && !f.isView
  61. && f.selector != relay(address,uint256,bytes).selector
  62. && !castVoteSubset(f)
  63. } {
  64. require getVotes(e, e.msg.sender, proposalSnapshot(pId)) > 0; // assuming voter has non-zero voting power
  65. uint256 againstBefore = votesAgainst();
  66. uint256 forBefore = votesFor();
  67. uint256 abstainBefore = votesAbstain();
  68. bool hasVotedBefore = hasVoted(pId, e.msg.sender);
  69. f(e, args);
  70. uint256 againstAfter = votesAgainst();
  71. uint256 forAfter = votesFor();
  72. uint256 abstainAfter = votesAbstain();
  73. bool hasVotedAfter = hasVoted(pId, e.msg.sender);
  74. // want all vote categories to not decrease and at least one category to increase
  75. assert
  76. (!hasVotedBefore && hasVotedAfter) =>
  77. (againstBefore <= againstAfter && forBefore <= forAfter && abstainBefore <= abstainAfter),
  78. "after a vote is cast, the number of votes for each category must not decrease";
  79. assert
  80. (!hasVotedBefore && hasVotedAfter) =>
  81. (againstBefore < againstAfter || forBefore < forAfter || abstainBefore < abstainAfter),
  82. "after a vote is cast, the number of votes of at least one category must increase";
  83. }
  84. /*
  85. โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  86. โ”‚ Rule: Voting against a proposal does not count towards quorum. โ”‚
  87. โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
  88. */
  89. rule againstVotesDontCount(uint256 pId, env e, method f, calldataarg args) filtered { f ->
  90. !f.isFallback
  91. && !f.isView
  92. && f.selector != relay(address,uint256,bytes).selector
  93. && !castVoteSubset(f)
  94. } {
  95. bool quorumBefore = quorumReached(e, pId);
  96. uint256 againstBefore = votesAgainst();
  97. f(e, args);
  98. bool quorumAfter = quorumReached(e, pId);
  99. uint256 againstAfter = votesAgainst();
  100. assert againstBefore < againstAfter => quorumBefore == quorumAfter, "quorum must not be reached with an against vote";
  101. }
  102. /*
  103. โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  104. โ”‚ Rule: โ”‚
  105. โ”‚ * Deadline can never be reduced โ”‚
  106. โ”‚ * If deadline increases then we are in `deadlineExtended` state and `castVote` was called. โ”‚
  107. โ”‚ * A proposal's deadline can't change in `deadlineExtended` state. โ”‚
  108. โ”‚ * A proposal's deadline can't be unextended. โ”‚
  109. โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
  110. */
  111. rule deadlineChangeEffects(uint256 pId, env e, method f, calldataarg args) filtered { f ->
  112. !f.isFallback
  113. && !f.isView
  114. && f.selector != relay(address,uint256,bytes).selector
  115. && !castVoteSubset(f)
  116. } {
  117. requireInvariant proposalStateConsistency(pId);
  118. uint256 deadlineBefore = proposalDeadline(pId);
  119. bool deadlineExtendedBefore = deadlineExtended(e, pId);
  120. f(e, args);
  121. uint256 deadlineAfter = proposalDeadline(pId);
  122. bool deadlineExtendedAfter = deadlineExtended(e, pId);
  123. // deadline can never be reduced
  124. assert deadlineBefore <= proposalDeadline(pId);
  125. // deadline can only be extended in proposal or on cast vote
  126. assert (
  127. deadlineAfter > deadlineBefore
  128. ) => (
  129. (!deadlineExtendedBefore && !deadlineExtendedAfter && f.selector == propose(address[], uint256[], bytes[], string).selector)
  130. ||
  131. (!deadlineExtendedBefore && deadlineExtendedAfter && f.selector == castVoteBySig(uint256, uint8,uint8, bytes32, bytes32).selector)
  132. );
  133. // a deadline can only be extended once
  134. assert deadlineExtendedBefore => deadlineBefore == deadlineAfter;
  135. // a deadline cannot be un-extended
  136. assert deadlineExtendedBefore => deadlineExtendedAfter;
  137. }
  138. /// The proposal with proposal id `pId` has not been created.
  139. definition proposalNotCreated(env e, uint256 pId) returns bool =
  140. proposalSnapshot(pId) == 0
  141. && proposalDeadline(pId) == 0
  142. && getAgainstVotes(pId) == 0
  143. && getAbstainVotes(pId) == 0
  144. && getForVotes(pId) == 0;
  145. /// Method f is a version of `castVote` whose state changing effects are covered by `castVoteBySig`.
  146. /// @dev castVoteBySig allows anyone to cast a vote for anyone else if they can supply the signature. Specifically,
  147. /// it covers the case where the msg.sender supplies a signature for themselves which is normally done using the normal
  148. /// `castVote`.
  149. definition castVoteSubset(method f) returns bool =
  150. f.selector == castVote(uint256, uint8).selector ||
  151. f.selector == castVoteWithReason(uint256, uint8, string).selector ||
  152. f.selector == castVoteWithReasonAndParams(uint256,uint8,string,bytes).selector ||
  153. f.selector == castVoteWithReasonAndParamsBySig(uint256,uint8,string,bytes,uint8,bytes32,bytes32).selector;
  154. ////////////////////////////////////////////////////////////////////////////////
  155. //// ### Properties //
  156. ////////////////////////////////////////////////////////////////////////////////
  157. ////////////////////////////////////////////////////////////////////////////////
  158. // Invariants //
  159. ////////////////////////////////////////////////////////////////////////////////
  160. /**
  161. * A created proposal must be in state `deadlineExtendable` or `deadlineExtended`.
  162. * @dev We assume the total supply of the voting token is non-zero
  163. */
  164. invariant proposalInOneState(env e1, uint256 pId)
  165. getPastTotalSupply(0) > 0 => (proposalNotCreated(e1, pId) || deadlineExtendable(e1, pId) || deadlineExtended(e1, pId))
  166. filtered { f -> !f.isFallback && !f.isView && !castVoteSubset(f) && f.selector != relay(address,uint256,bytes).selector }
  167. {
  168. preserved with (env e2) {
  169. setup(e1, e2);
  170. }
  171. }
  172. /**
  173. * The quorum numerator is always less than or equal to the quorum denominator.
  174. */
  175. invariant quorumNumerLTEDenom(env e1, uint256 blockNumber)
  176. quorumNumerator(e1, blockNumber) <= quorumDenominator()
  177. {
  178. preserved with (env e2) {
  179. setup(e1, e2);
  180. }
  181. }
  182. /**
  183. * The deprecated quorum numerator variable `_quorumNumerator` is always 0 in a contract that is not upgraded.
  184. */
  185. invariant deprecatedQuorumStateIsUninitialized()
  186. getDeprecatedQuorumNumerator() == 0
  187. /**
  188. * If a proposal's deadline has been extended, then the proposal must have been created and reached quorum.
  189. */
  190. invariant cantExtendWhenQuorumUnreached(env e2, uint256 pId)
  191. getExtendedDeadline(pId) > 0 => (quorumReached(e2, pId) && proposalCreated(pId))
  192. filtered { f -> !f.isFallback && !f.isView && !castVoteSubset(f) && f.selector != relay(address,uint256,bytes).selector }
  193. {
  194. preserved with (env e1) {
  195. require e1.block.number > proposalSnapshot(pId);
  196. setup(e1, e2);
  197. }
  198. }
  199. /**
  200. * The snapshot arrat keeping tracking of quorum numerators must never be uninitialized.
  201. */
  202. invariant quorumLengthGt0(env e)
  203. getQuorumNumeratorLength() > 0
  204. filtered { f -> !f.isFallback && !f.isView && !castVoteSubset(f) && f.selector != relay(address,uint256,bytes).selector }
  205. {
  206. preserved {
  207. setup(e,e);
  208. }
  209. }
  210. /**
  211. * If a proposal has reached quorum then the proposal snapshot (start `block.number`) must be non-zero
  212. */
  213. invariant quorumReachedEffect(env e, uint256 pId)
  214. quorumReached(e, pId) => proposalCreated(pId)
  215. // filtered { f -> !f.isFallback && !f.isView && !castVoteSubset(f) && f.selector != relay(address,uint256,bytes).selector }
  216. // {
  217. // preserved with (env e2) {
  218. // setup(e1, e2);
  219. // }
  220. // }