GovernorPreventLateQuorum.spec 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. import "GovernorCountingSimple.spec"
  2. using ERC20VotesHarness as token
  3. /***
  4. ## Verification of `GovernorPreventLateQuorum`
  5. `GovernorPreventLateQuorum` extends the Governor group of contracts to add the
  6. feature of giving voters more time to vote in the case that a proposal reaches
  7. quorum with less than `voteExtension` amount of time left to vote.
  8. ### Assumptions and Simplifications
  9. None
  10. #### Harnessing
  11. - The contract that the specification was verified against is
  12. `GovernorPreventLateQuorumHarness`, which inherits from all of the Governor
  13. contracts — excluding Compound variations — and implements a number of view
  14. functions to gain access to values that are impossible/difficult to access in
  15. CVL. It also implements all of the required functions not implemented in the
  16. abstract contracts it inherits from.
  17. - `_castVote` was overridden to add an additional flag before calling the parent
  18. version. This flag stores the `block.number` in a variable
  19. `latestCastVoteCall` and is used as a way to check when any of variations of
  20. `castVote` are called.
  21. #### Munging
  22. - Various variables' visibility was changed from private to internal or from
  23. internal to public throughout the Governor contracts in order to make them
  24. accessible in the spec.
  25. - Arbitrary low level calls are assumed to change nothing and thus the function
  26. `_execute` is changed to do nothing. The tool normally havocs in this
  27. situation, assuming all storage can change due to possible reentrancy. We
  28. assume, however, there is no risk of reentrancy because `_execute` is a
  29. protected call locked behind the timelocked governance vote. All other
  30. governance functions are verified separately.
  31. */
  32. methods {
  33. // summarized
  34. hashProposal(address[],uint256[],bytes[],bytes32) returns (uint256) => NONDET
  35. _hashTypedDataV4(bytes32) returns (bytes32)
  36. // envfree
  37. quorumNumerator(uint256) returns uint256
  38. quorumDenominator() returns uint256 envfree
  39. votingPeriod() returns uint256 envfree
  40. lateQuorumVoteExtension() returns uint64 envfree
  41. propose(address[], uint256[], bytes[], string)
  42. // harness
  43. getDeprecatedQuorumNumerator() returns uint256 envfree
  44. getQuorumNumeratorLength() returns uint256 envfree
  45. getQuorumNumeratorLatest() returns uint256 envfree
  46. getExtendedDeadlineIsUnset(uint256) returns bool envfree
  47. getExtendedDeadlineIsStarted(uint256) returns bool envfree
  48. getExtendedDeadline(uint256) returns uint64 envfree
  49. getAgainstVotes(uint256) returns uint256 envfree
  50. getAbstainVotes(uint256) returns uint256 envfree
  51. getForVotes(uint256) returns uint256 envfree
  52. getPastTotalSupply(uint256) returns (uint256) envfree
  53. // more robust check than f.selector == _castVote(...).selector
  54. latestCastVoteCall() returns uint256 envfree
  55. // timelock dispatch
  56. getMinDelay() returns uint256 => DISPATCHER(true)
  57. hashOperationBatch(address[], uint256[], bytes[], bytes32, bytes32) => DISPATCHER(true)
  58. executeBatch(address[], uint256[], bytes[], bytes32, bytes32) => CONSTANT
  59. scheduleBatch(address[], uint256[], bytes[], bytes32, bytes32, uint256) => CONSTANT
  60. }
  61. ////////////////////////////////////////////////////////////////////////////////
  62. // Helper Functions //
  63. ////////////////////////////////////////////////////////////////////////////////
  64. function helperFunctionsWithRevertOnlyCastVote(uint256 proposalId, method f, env e) {
  65. string reason; uint8 support; uint8 v; bytes32 r; bytes32 s; bytes params;
  66. if (f.selector == castVoteBySig(uint256, uint8,uint8, bytes32, bytes32).selector) {
  67. castVoteBySig@withrevert(e, proposalId, support, v, r, s);
  68. } else {
  69. calldataarg args;
  70. f@withrevert(e, args);
  71. }
  72. }
  73. /// Restricting out common reasons why rules break. We assume quorum length won't overflow (uint256) and that functions
  74. /// called in env `e2` have a `block.number` greater than or equal `e1`'s `block.number`.
  75. function setup(env e1, env e2) {
  76. require getQuorumNumeratorLength() + 1 < max_uint;
  77. require e2.block.number >= e1.block.number;
  78. }
  79. ////////////////////////////////////////////////////////////////////////////////
  80. //// #### Definitions //
  81. ////////////////////////////////////////////////////////////////////////////////
  82. /// The proposal with proposal id `pId` has a deadline which is extendable.
  83. definition deadlineExtendable(env e, uint256 pId) returns bool =
  84. getExtendedDeadlineIsUnset(pId) // deadline == 0
  85. && !quorumReached(e, pId);
  86. /// The proposal with proposal id `pId` has a deadline which has been extended.
  87. definition deadlineExtended(env e, uint256 pId) returns bool =
  88. getExtendedDeadlineIsStarted(pId) // deadline > 0
  89. && quorumReached(e, pId);
  90. /// The proposal with proposal id `pId` has not been created.
  91. definition proposalNotCreated(env e, uint256 pId) returns bool =
  92. proposalSnapshot(pId) == 0
  93. && proposalDeadline(pId) == 0
  94. && getAgainstVotes(pId) == 0
  95. && getAbstainVotes(pId) == 0
  96. && getForVotes(pId) == 0;
  97. /// Method f is a version of `castVote` whose state changing effects are covered by `castVoteBySig`.
  98. /// @dev castVoteBySig allows anyone to cast a vote for anyone else if they can supply the signature. Specifically,
  99. /// it covers the case where the msg.sender supplies a signature for themselves which is normally done using the normal
  100. /// `castVote`.
  101. definition castVoteSubset(method f) returns bool =
  102. f.selector == castVote(uint256, uint8).selector ||
  103. f.selector == castVoteWithReason(uint256, uint8, string).selector ||
  104. f.selector == castVoteWithReasonAndParamsBySig(uint256,uint8,string,bytes,uint8,bytes32,bytes32).selector ||
  105. f.selector == castVoteWithReasonAndParams(uint256,uint8,string,bytes).selector;
  106. ////////////////////////////////////////////////////////////////////////////////
  107. //// ### Properties //
  108. ////////////////////////////////////////////////////////////////////////////////
  109. ////////////////////////////////////////////////////////////////////////////////
  110. // Invariants //
  111. ////////////////////////////////////////////////////////////////////////////////
  112. /**
  113. * A created proposal must be in state `deadlineExtendable` or `deadlineExtended`.
  114. * @dev We assume the total supply of the voting token is non-zero
  115. */
  116. invariant proposalInOneState(env e1, uint256 pId)
  117. getPastTotalSupply(0) > 0 => (proposalNotCreated(e1, pId) || deadlineExtendable(e1, pId) || deadlineExtended(e1, pId))
  118. filtered { f -> !f.isFallback && !f.isView && !castVoteSubset(f) && f.selector != relay(address,uint256,bytes).selector }
  119. {
  120. preserved with (env e2) {
  121. setup(e1, e2);
  122. }
  123. }
  124. /**
  125. * The quorum numerator is always less than or equal to the quorum denominator.
  126. */
  127. invariant quorumNumerLTEDenom(env e1, uint256 blockNumber)
  128. quorumNumerator(e1, blockNumber) <= quorumDenominator()
  129. {
  130. preserved with (env e2) {
  131. setup(e1, e2);
  132. }
  133. }
  134. /**
  135. * The deprecated quorum numerator variable `_quorumNumerator` is always 0 in a contract that is not upgraded.
  136. */
  137. invariant deprecatedQuorumStateIsUninitialized()
  138. getDeprecatedQuorumNumerator() == 0
  139. /**
  140. * If a proposal's deadline has been extended, then the proposal must have been created and reached quorum.
  141. */
  142. invariant cantExtendWhenQuorumUnreached(env e2, uint256 pId)
  143. getExtendedDeadlineIsStarted(pId) => quorumReached(e2, pId) && proposalCreated(pId)
  144. filtered { f -> !f.isFallback && !f.isView && !castVoteSubset(f) && f.selector != relay(address,uint256,bytes).selector }
  145. { preserved with (env e1) {
  146. require e1.block.number > proposalSnapshot(pId);
  147. setup(e1, e2);
  148. }}
  149. /**
  150. * The snapshot arrat keeping tracking of quorum numerators must never be uninitialized.
  151. */
  152. invariant quorumLengthGt0(env e)
  153. getQuorumNumeratorLength() > 0
  154. filtered { f -> !f.isFallback && !f.isView && !castVoteSubset(f) && f.selector != relay(address,uint256,bytes).selector }
  155. { preserved {
  156. setup(e,e);
  157. }}
  158. /**
  159. * If a proposal has reached quorum then the proposal snapshot (start `block.number`) must be non-zero
  160. */
  161. invariant quorumReachedEffect(env e1, uint256 pId)
  162. quorumReached(e1, pId) && getPastTotalSupply(0) > 0 => proposalCreated(pId)
  163. filtered { f -> !f.isFallback && !f.isView && !castVoteSubset(f) && f.selector != relay(address,uint256,bytes).selector }
  164. {
  165. preserved with (env e2) {
  166. setup(e1, e2);
  167. }
  168. }
  169. //////////////////////////////////////////////////////////////////////////////
  170. // Rules //
  171. //////////////////////////////////////////////////////////////////////////////
  172. /**
  173. * `updateQuorumNumerator` can only change quorum requirements for future proposals.
  174. * @dev In the case that the array containing past quorum numerators overflows, this rule will fail.
  175. */
  176. rule quorumReachedCantChange(method f) filtered {
  177. f -> !f.isFallback && !f.isView && !castVoteSubset(f) && f.selector != relay(address,uint256,bytes).selector
  178. } {
  179. env e1; uint256 pId;
  180. bool _quorumReached = quorumReached(e1, pId);
  181. env e2; uint256 newQuorumNumerator;
  182. setup(e1, e2);
  183. updateQuorumNumerator(e2, newQuorumNumerator);
  184. env e3;
  185. bool quorumReached_ = quorumReached(e3, pId);
  186. assert _quorumReached == quorumReached_, "function changed quorumReached";
  187. }
  188. /**
  189. * Casting a vote must not decrease any category's total number of votes and increase at least one category's.
  190. */
  191. rule hasVotedCorrelationNonzero(uint256 pId, method f, env e) filtered {
  192. f -> !f.isFallback && !f.isView && !castVoteSubset(f) && f.selector != relay(address,uint256,bytes).selector
  193. } {
  194. address acc = e.msg.sender;
  195. require(getVotes(e, acc, proposalSnapshot(pId)) > 0); // assuming voter has non-zero voting power
  196. uint256 againstBefore = votesAgainst();
  197. uint256 forBefore = votesFor();
  198. uint256 abstainBefore = votesAbstain();
  199. bool hasVotedBefore = hasVoted(e, pId, acc);
  200. helperFunctionsWithRevertOnlyCastVote(pId, f, e); // should be f(e, args)
  201. uint256 againstAfter = votesAgainst();
  202. uint256 forAfter = votesFor();
  203. uint256 abstainAfter = votesAbstain();
  204. bool hasVotedAfter = hasVoted(e, pId, acc);
  205. // want all vote categories to not decrease and at least one category to increase
  206. assert
  207. (!hasVotedBefore && hasVotedAfter) =>
  208. (againstBefore <= againstAfter && forBefore <= forAfter && abstainBefore <= abstainAfter),
  209. "after a vote is cast, the number of votes for each category must not decrease";
  210. assert
  211. (!hasVotedBefore && hasVotedAfter) =>
  212. (againstBefore < againstAfter || forBefore < forAfter || abstainBefore < abstainAfter),
  213. "after a vote is cast, the number of votes of at least one category must increase";
  214. }
  215. /**
  216. * Voting against a proposal does not count towards quorum.
  217. */
  218. rule againstVotesDontCount(method f) filtered {
  219. f -> !f.isFallback && !f.isView && !castVoteSubset(f) && f.selector != relay(address,uint256,bytes).selector
  220. } {
  221. env e; calldataarg args; uint256 pId;
  222. address acc = e.msg.sender;
  223. bool quorumBefore = quorumReached(e, pId);
  224. uint256 againstBefore = votesAgainst();
  225. f(e, args);
  226. bool quorumAfter = quorumReached(e, pId);
  227. uint256 againstAfter = votesAgainst();
  228. assert (againstBefore < againstAfter) => quorumBefore == quorumAfter, "quorum must not be reached with an against vote";
  229. }
  230. /**
  231. * Deadline can never be reduced.
  232. */
  233. rule deadlineNeverReduced(method f) filtered {
  234. f -> !f.isFallback && !f.isView && !castVoteSubset(f) && f.selector != relay(address,uint256,bytes).selector
  235. } {
  236. env e1; env e2; calldataarg args; uint256 pId;
  237. requireInvariant quorumReachedEffect(e1, pId);
  238. require proposalCreated(pId);
  239. setup(e1, e2);
  240. uint256 deadlineBefore = proposalDeadline(pId);
  241. f(e2, args);
  242. uint256 deadlineAfter = proposalDeadline(pId);
  243. assert(deadlineAfter >= deadlineBefore);
  244. }
  245. //// The rules [`deadlineChangeEffects`](#deadlineChangeEffects) and [`deadlineCantBeUnextended`](#deadlineCantBeUnextended)
  246. //// are assumed in rule [`canExtendDeadlineOnce`](#canExtendDeadlineOnce), so we prove them first.
  247. /**
  248. * If deadline increases then we are in `deadlineExtended` state and `castVote`
  249. * was called.
  250. */
  251. rule deadlineChangeEffects(method f) filtered {
  252. f -> !f.isFallback && !f.isView && !castVoteSubset(f) && f.selector != relay(address,uint256,bytes).selector
  253. } {
  254. env e; calldataarg args; uint256 pId;
  255. requireInvariant quorumReachedEffect(e, pId);
  256. uint256 deadlineBefore = proposalDeadline(pId);
  257. f(e, args);
  258. uint256 deadlineAfter = proposalDeadline(pId);
  259. assert(deadlineAfter > deadlineBefore => latestCastVoteCall() == e.block.number && deadlineExtended(e, pId));
  260. }
  261. /**
  262. * @title Deadline can't be unextended
  263. * @notice A proposal can't leave `deadlineExtended` state.
  264. */
  265. rule deadlineCantBeUnextended(method f) filtered {
  266. f -> !f.isFallback && !f.isView && !castVoteSubset(f) && f.selector != relay(address,uint256,bytes).selector
  267. } {
  268. env e1; env e2; env e3; env e4; calldataarg args; uint256 pId;
  269. setup(e1, e2);
  270. require(deadlineExtended(e1, pId));
  271. requireInvariant quorumReachedEffect(e1, pId);
  272. f(e2, args);
  273. assert(deadlineExtended(e1, pId));
  274. }
  275. /**
  276. * A proposal's deadline can't change in `deadlineExtended` state.
  277. */
  278. rule canExtendDeadlineOnce(method f) filtered {
  279. f -> !f.isFallback && !f.isView && !castVoteSubset(f) && f.selector != relay(address,uint256,bytes).selector
  280. } {
  281. env e1; env e2; calldataarg args; uint256 pId;
  282. require(deadlineExtended(e1, pId));
  283. require(proposalSnapshot(pId) > 0);
  284. requireInvariant quorumReachedEffect(e1, pId);
  285. setup(e1, e2);
  286. uint256 deadlineBefore = proposalDeadline(pId);
  287. f(e2, args);
  288. uint256 deadlineAfter = proposalDeadline(pId);
  289. assert(deadlineBefore == deadlineAfter, "deadline can not be extended twice");
  290. }