ERC20Votes.spec 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. import "erc20.spec"
  2. methods {
  3. // IVotes
  4. getVotes(address) returns (uint256) envfree
  5. getPastVotes(address, uint256) returns (uint256) // not envfree (reads block.number)
  6. getPastTotalSupply(uint256) returns (uint256) // not envfree (reads block.number)
  7. delegates(address) returns (address) envfree
  8. delegate(address) // not envfree (reads msg.sender)
  9. delegateBySig(address, uint256, uint256, uint8, bytes32, bytes32) // not envfree (reads msg.sender)
  10. // ERC20Votes
  11. checkpoints(address, uint32) envfree
  12. numCheckpoints(address) returns (uint32) envfree
  13. _maxSupply() returns (uint224) envfree
  14. _delegate(address, address) // not envfree (reads block.number when creating checkpoint)
  15. // harnesss functions
  16. ckptFromBlock(address, uint32) returns (uint32) envfree
  17. ckptVotes(address, uint32) returns (uint224) envfree
  18. unsafeNumCheckpoints(address) returns (uint256) envfree
  19. // solidity generated getters
  20. _delegates(address) returns (address) envfree
  21. }
  22. // gets the most recent votes for a user
  23. ghost userVotes(address) returns uint224 {
  24. init_state axiom forall address a. userVotes(a) == 0;
  25. }
  26. // sums the total votes for all users
  27. ghost totalVotes() returns uint224 {
  28. init_state axiom totalVotes() == 0;
  29. }
  30. ghost lastIndex(address) returns uint32;
  31. // helper
  32. hook Sstore _checkpoints[KEY address account][INDEX uint32 index].votes uint224 newVotes (uint224 oldVotes) STORAGE {
  33. havoc userVotes assuming
  34. userVotes@new(account) == newVotes;
  35. havoc totalVotes assuming
  36. totalVotes@new() == totalVotes@old() + newVotes - userVotes(account);
  37. havoc lastIndex assuming
  38. lastIndex@new(account) == index;
  39. }
  40. ghost lastFromBlock(address) returns uint32;
  41. ghost doubleFromBlock(address) returns bool {
  42. init_state axiom forall address a. doubleFromBlock(a) == false;
  43. }
  44. hook Sstore _checkpoints[KEY address account][INDEX uint32 index].fromBlock uint32 newBlock (uint32 oldBlock) STORAGE {
  45. havoc lastFromBlock assuming
  46. lastFromBlock@new(account) == newBlock;
  47. havoc doubleFromBlock assuming
  48. doubleFromBlock@new(account) == (newBlock == lastFromBlock(account));
  49. }
  50. ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  51. // Invariants //
  52. ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  53. // sum of user balances is >= total amount of delegated votes
  54. // fails on burn. This is because burn does not remove votes from the users
  55. invariant votes_solvency()
  56. totalSupply() >= to_uint256(totalVotes())
  57. filtered { f -> f.selector != _burn(address, uint256).selector }
  58. {
  59. preserved with(env e) {
  60. require forall address account. numCheckpoints(account) < 1000000;
  61. } preserved _burn(address a, uint256 amount) with(env e) {
  62. require _delegates(0) == 0;
  63. require forall address a2. (_delegates(a) != _delegates(a2)) && (balanceOf(_delegates(a)) + balanceOf(_delegates(a2)) <= totalVotes());
  64. require balanceOf(_delegates(a)) < totalVotes();
  65. require amount < 100000;
  66. }
  67. }
  68. // can't have more checkpoints for a given account than the last from block
  69. invariant fromBlock_constrains_numBlocks(address account)
  70. numCheckpoints(account) <= ckptFromBlock(account, numCheckpoints(account) - 1)
  71. filtered { f -> !f.isView }
  72. {
  73. preserved with(env e) {
  74. require e.block.number >= ckptFromBlock(account, numCheckpoints(account) - 1); // this should be true from the invariant above!!
  75. }
  76. }
  77. // for any given checkpoint, the fromBlock must be greater than the checkpoint
  78. // this proves the above invariant in combination with the below invariant
  79. // if checkpoint has a greater fromBlock than the last, and the FromBlock is always greater than the pos.
  80. // Then the number of positions must be less than the currentFromBlock
  81. // ^note that the tool is assuming it's possible for the starting fromBlock to be 0 or anything, and does not know the current starting block
  82. // passes + rule sanity
  83. invariant fromBlock_greaterThanEq_pos(address account, uint32 pos)
  84. ckptFromBlock(account, pos) >= pos
  85. filtered { f -> !f.isView }
  86. // a larger index must have a larger fromBlock
  87. // passes + rule sanity
  88. invariant fromBlock_increasing(address account, uint32 pos, uint32 pos2)
  89. pos > pos2 => ckptFromBlock(account, pos) > ckptFromBlock(account, pos2)
  90. filtered { f -> !f.isView }
  91. ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  92. // Rules //
  93. ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  94. // converted from an invariant to a rule to slightly change the logic
  95. // if the fromBlock is the same as before, then the number of checkpoints stays the same
  96. // however if the fromBlock is new than the number of checkpoints increases
  97. // passes, fails rule sanity because tautology check seems to be bugged
  98. rule unique_checkpoints_rule(method f) {
  99. env e; calldataarg args;
  100. address account;
  101. uint32 num_ckpts_ = numCheckpoints(account);
  102. uint32 fromBlock_ = num_ckpts_ == 0 ? 0 : ckptFromBlock(account, num_ckpts_ - 1);
  103. f(e, args);
  104. uint32 _num_ckpts = numCheckpoints(account);
  105. uint32 _fromBlock = _num_ckpts == 0 ? 0 : ckptFromBlock(account, _num_ckpts - 1);
  106. assert fromBlock_ == _fromBlock => num_ckpts_ == _num_ckpts || _num_ckpts == 1, "same fromBlock, new checkpoint";
  107. }
  108. // assumes neither account has delegated
  109. // currently fails due to this scenario. A has maxint number of checkpoints
  110. // an additional checkpoint is added which overflows and sets A's votes to 0
  111. // passes + rule sanity (- a bad tautology check)
  112. rule transfer_safe() {
  113. env e;
  114. uint256 amount;
  115. address a; address b;
  116. require delegates(a) != delegates(b); // confirmed if they both delegate to the same person then transfer keeps the votes the same
  117. require numCheckpoints(delegates(a)) < 1000000;
  118. require numCheckpoints(delegates(b)) < 1000000;
  119. uint256 votesA_pre = getVotes(delegates(a));
  120. uint256 votesB_pre = getVotes(delegates(b));
  121. uint224 totalVotes_pre = totalVotes();
  122. transferFrom(e, a, b, amount);
  123. uint224 totalVotes_post = totalVotes();
  124. uint256 votesA_post = getVotes(delegates(a));
  125. uint256 votesB_post = getVotes(delegates(b));
  126. // if an account that has not delegated transfers balance to an account that has, it will increase the total supply of votes
  127. assert totalVotes_pre == totalVotes_post, "transfer changed total supply";
  128. assert delegates(a) != 0 => votesA_pre - votesA_post == amount, "A lost the wrong amount of votes";
  129. assert delegates(b) != 0 => votesB_post - votesB_pre == amount, "B lost the wrong amount of votes";
  130. }
  131. // for any given function f, if the delegate is changed the function must be delegate or delegateBySig
  132. // passes
  133. rule delegates_safe(method f)
  134. filtered { f -> (
  135. f.selector != delegate(address).selector &&
  136. f.selector != _delegate(address, address).selector &&
  137. f.selector != delegateBySig(address, uint256, uint256, uint8, bytes32, bytes32).selector
  138. ) }
  139. {
  140. env e; calldataarg args;
  141. address account;
  142. address pre = delegates(account);
  143. f(e, args);
  144. address post = delegates(account);
  145. assert pre == post, "invalid delegate change";
  146. }
  147. // delegates increases the delegatee's votes by the proper amount
  148. // passes + rule sanity
  149. rule delegatee_receives_votes() {
  150. env e;
  151. address delegator; address delegatee;
  152. require delegates(delegator) != delegatee;
  153. require delegatee != 0x0;
  154. uint256 delegator_bal = balanceOf(delegator);
  155. uint256 votes_= getVotes(delegatee);
  156. _delegate(e, delegator, delegatee);
  157. require lastIndex(delegatee) < 1000000;
  158. uint256 _votes = getVotes(delegatee);
  159. assert _votes == votes_ + delegator_bal, "delegatee did not receive votes";
  160. }
  161. // passes + rule sanity
  162. rule previous_delegatee_votes_removed() {
  163. env e;
  164. address delegator; address delegatee; address third;
  165. require third != delegatee;
  166. require delegates(delegator) == third;
  167. require numCheckpoints(third) < 1000000;
  168. uint256 delegator_bal = balanceOf(delegator);
  169. uint256 votes_ = getVotes(third);
  170. _delegate(e, delegator, delegatee);
  171. uint256 _votes = getVotes(third);
  172. assert third != 0x0 => _votes == votes_ - delegator_bal, "votes not removed from the previous delegatee";
  173. }
  174. // passes with rule sanity
  175. rule delegate_contained() {
  176. env e;
  177. address delegator; address delegatee; address other;
  178. require other != delegatee;
  179. require other != delegates(delegator);
  180. uint256 votes_ = getVotes(other);
  181. _delegate(e, delegator, delegatee);
  182. uint256 _votes = getVotes(other);
  183. assert votes_ == _votes, "votes not contained";
  184. }
  185. rule delegate_no_frontrunning(method f) {
  186. env e; calldataarg args;
  187. address delegator; address delegatee; address third; address other;
  188. require numCheckpoints(delegatee) < 1000000;
  189. require numCheckpoints(third) < 1000000;
  190. f(e, args);
  191. uint256 delegator_bal = balanceOf(delegator);
  192. uint256 delegatee_votes_ = getVotes(delegatee);
  193. uint256 third_votes_ = getVotes(third);
  194. uint256 other_votes_ = getVotes(other);
  195. require delegates(delegator) == third;
  196. require third != delegatee;
  197. require other != third;
  198. require other != delegatee;
  199. require delegatee != 0x0;
  200. _delegate(e, delegator, delegatee);
  201. uint256 _delegatee_votes = getVotes(delegatee);
  202. uint256 _third_votes = getVotes(third);
  203. uint256 _other_votes = getVotes(other);
  204. // previous delegatee loses all of their votes
  205. // delegatee gains that many votes
  206. // third loses any votes delegated to them
  207. assert _delegatee_votes == delegatee_votes_ + delegator_bal, "delegatee did not receive votes";
  208. assert third != 0 => _third_votes == third_votes_ - delegator_bal, "votes not removed from third";
  209. assert other_votes_ == _other_votes, "delegate not contained";
  210. }
  211. // passes
  212. rule onMint() {
  213. env e;
  214. uint256 amount;
  215. address account;
  216. uint256 fromBlock = e.block.number;
  217. uint224 totalVotesBefore = totalVotes();
  218. uint256 totalSupplyBefore = totalSupply();
  219. _mint(e, account, amount);
  220. assert totalVotes() == totalVotesBefore, "totalVotes decreased";
  221. assert getPastTotalSupply(e, fromBlock) == totalSupplyBefore , "previous total supply not saved properly";
  222. }
  223. // passes
  224. rule onBurn() {
  225. env e;
  226. uint256 amount;
  227. address account;
  228. uint256 fromBlock = e.block.number;
  229. uint224 totalVotesBefore = totalVotes();
  230. uint256 totalSupplyBefore = totalSupply();
  231. _burn(e, account, amount);
  232. assert totalVotes() == totalVotesBefore, "totalVotes decreased";
  233. assert getPastTotalSupply(e, fromBlock) == totalSupplyBefore , "previous total supply not saved properly";
  234. }