ERC20Votes.spec 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. import "erc20methods.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. // sum of user balances is >= total amount of delegated votes
  51. // fails on burn. This is because burn does not remove votes from the users
  52. invariant votes_solvency()
  53. totalSupply() >= to_uint256(totalVotes())
  54. filtered { f -> f.selector != _burn(address, uint256).selector }
  55. {
  56. preserved with(env e) {
  57. require forall address account. numCheckpoints(account) < 1000000;
  58. }
  59. preserved _burn(address a, uint256 amount) with(env e) {
  60. require _delegates(0) == 0;
  61. require forall address a2. (_delegates(a) != _delegates(a2)) && (balanceOf(_delegates(a)) + balanceOf(_delegates(a2)) <= totalVotes());
  62. require balanceOf(_delegates(a)) < totalVotes();
  63. require amount < 100000;
  64. }
  65. }
  66. // for some checkpoint, the fromBlock is less than the current block number
  67. invariant blockNum_constrains_fromBlock(address account, uint32 index, env e)
  68. ckptFromBlock(account, index) < e.block.number
  69. filtered { f -> !f.isView }
  70. // numCheckpoints are less than maxInt
  71. // passes because numCheckpoints does a safeCast
  72. // invariant maxInt_constrains_numBlocks(address account)
  73. // numCheckpoints(account) < 4294967295 // 2^32
  74. // can't have more checkpoints for a given account than the last from block
  75. // passes
  76. invariant fromBlock_constrains_numBlocks(address account)
  77. numCheckpoints(account) <= ckptFromBlock(account, numCheckpoints(account) - 1)
  78. filtered { f -> !f.isView }
  79. {
  80. preserved with(env e) {
  81. require e.block.number >= ckptFromBlock(account, numCheckpoints(account) - 1); // this should be true from the invariant above!!
  82. }
  83. }
  84. // for any given checkpoint, the fromBlock must be greater than the checkpoint
  85. // this proves the above invariant in combination with the below invariant
  86. // if checkpoint has a greater fromBlock than the last, and the FromBlock is always greater than the pos.
  87. // Then the number of positions must be less than the currentFromBlock
  88. // ^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
  89. // passes + rule sanity
  90. invariant fromBlock_greaterThanEq_pos(address account, uint32 pos)
  91. ckptFromBlock(account, pos) >= pos
  92. filtered { f -> !f.isView }
  93. // a larger index must have a larger fromBlock
  94. // passes + rule sanity
  95. invariant fromBlock_increasing(address account, uint32 pos, uint32 pos2)
  96. pos > pos2 => ckptFromBlock(account, pos) > ckptFromBlock(account, pos2)
  97. filtered { f -> !f.isView }
  98. // converted from an invariant to a rule to slightly change the logic
  99. // if the fromBlock is the same as before, then the number of checkpoints stays the same
  100. // however if the fromBlock is new than the number of checkpoints increases
  101. // passes, fails rule sanity because tautology check seems to be bugged
  102. rule unique_checkpoints_rule(method f) {
  103. env e; calldataarg args;
  104. address account;
  105. uint32 num_ckpts_ = numCheckpoints(account);
  106. uint32 fromBlock_ = num_ckpts_ == 0 ? 0 : ckptFromBlock(account, num_ckpts_ - 1);
  107. f(e, args);
  108. uint32 _num_ckpts = numCheckpoints(account);
  109. uint32 _fromBlock = _num_ckpts == 0 ? 0 : ckptFromBlock(account, _num_ckpts - 1);
  110. assert fromBlock_ == _fromBlock => num_ckpts_ == _num_ckpts || _num_ckpts == 1, "same fromBlock, new checkpoint";
  111. }
  112. // assumes neither account has delegated
  113. // currently fails due to this scenario. A has maxint number of checkpoints
  114. // an additional checkpoint is added which overflows and sets A's votes to 0
  115. // passes + rule sanity (- a bad tautology check)
  116. rule transfer_safe() {
  117. env e;
  118. uint256 amount;
  119. address a; address b;
  120. require delegates(a) != delegates(b); // confirmed if they both delegate to the same person then transfer keeps the votes the same
  121. require numCheckpoints(delegates(a)) < 1000000;
  122. require numCheckpoints(delegates(b)) < 1000000;
  123. uint256 votesA_pre = getVotes(delegates(a));
  124. uint256 votesB_pre = getVotes(delegates(b));
  125. uint224 totalVotes_pre = totalVotes();
  126. transferFrom(e, a, b, amount);
  127. uint224 totalVotes_post = totalVotes();
  128. uint256 votesA_post = getVotes(delegates(a));
  129. uint256 votesB_post = getVotes(delegates(b));
  130. // if an account that has not delegated transfers balance to an account that has, it will increase the total supply of votes
  131. assert totalVotes_pre == totalVotes_post, "transfer changed total supply";
  132. assert delegates(a) != 0 => votesA_pre - votesA_post == amount, "A lost the wrong amount of votes";
  133. assert delegates(b) != 0 => votesB_post - votesB_pre == amount, "B lost the wrong amount of votes";
  134. }
  135. // for any given function f, if the delegate is changed the function must be delegate or delegateBySig
  136. // passes
  137. rule delegates_safe(method f)
  138. filtered { f -> (
  139. f.selector != delegate(address).selector &&
  140. f.selector != _delegate(address, address).selector &&
  141. f.selector != delegateBySig(address, uint256, uint256, uint8, bytes32, bytes32).selector
  142. ) }
  143. {
  144. env e; calldataarg args;
  145. address account;
  146. address pre = delegates(account);
  147. f(e, args);
  148. address post = delegates(account);
  149. assert pre == post, "invalid delegate change";
  150. }
  151. // delegates increases the delegatee's votes by the proper amount
  152. // passes + rule sanity
  153. rule delegatee_receives_votes() {
  154. env e;
  155. address delegator; address delegatee;
  156. require delegates(delegator) != delegatee;
  157. require delegatee != 0x0;
  158. uint256 delegator_bal = balanceOf(delegator);
  159. uint256 votes_= getVotes(delegatee);
  160. _delegate(e, delegator, delegatee);
  161. require lastIndex(delegatee) < 1000000;
  162. uint256 _votes = getVotes(delegatee);
  163. assert _votes == votes_ + delegator_bal, "delegatee did not receive votes";
  164. }
  165. // passes + rule sanity
  166. rule previous_delegatee_votes_removed() {
  167. env e;
  168. address delegator; address delegatee; address third;
  169. require third != delegatee;
  170. require delegates(delegator) == third;
  171. require numCheckpoints(third) < 1000000;
  172. uint256 delegator_bal = balanceOf(delegator);
  173. uint256 votes_ = getVotes(third);
  174. _delegate(e, delegator, delegatee);
  175. uint256 _votes = getVotes(third);
  176. assert third != 0x0 => _votes == votes_ - delegator_bal, "votes not removed from the previous delegatee";
  177. }
  178. // passes with rule sanity
  179. rule delegate_contained() {
  180. env e;
  181. address delegator; address delegatee; address other;
  182. require other != delegatee;
  183. require other != delegates(delegator);
  184. uint256 votes_ = getVotes(other);
  185. _delegate(e, delegator, delegatee);
  186. uint256 _votes = getVotes(other);
  187. assert votes_ == _votes, "votes not contained";
  188. }
  189. rule delegate_no_frontrunning(method f) {
  190. env e; calldataarg args;
  191. address delegator; address delegatee; address third; address other;
  192. require numCheckpoints(delegatee) < 1000000;
  193. require numCheckpoints(third) < 1000000;
  194. f(e, args);
  195. uint256 delegator_bal = balanceOf(delegator);
  196. uint256 delegatee_votes_ = getVotes(delegatee);
  197. uint256 third_votes_ = getVotes(third);
  198. uint256 other_votes_ = getVotes(other);
  199. require delegates(delegator) == third;
  200. require third != delegatee;
  201. require other != third;
  202. require other != delegatee;
  203. require delegatee != 0x0;
  204. _delegate(e, delegator, delegatee);
  205. uint256 _delegatee_votes = getVotes(delegatee);
  206. uint256 _third_votes = getVotes(third);
  207. uint256 _other_votes = getVotes(other);
  208. // previous delegatee loses all of their votes
  209. // delegatee gains that many votes
  210. // third loses any votes delegated to them
  211. assert _delegatee_votes == delegatee_votes_ + delegator_bal, "delegatee did not receive votes";
  212. assert third != 0 => _third_votes == third_votes_ - delegator_bal, "votes not removed from third";
  213. assert other_votes_ == _other_votes, "delegate not contained";
  214. }
  215. rule onMint() {
  216. env e;
  217. uint256 amount;
  218. address account;
  219. uint256 fromBlock = e.block.number;
  220. uint224 totalVotesBefore = totalVotes();
  221. uint256 totalSupplyBefore = totalSupply();
  222. _mint(e, account, amount);
  223. assert totalVotes() == totalVotesBefore, "totalVotes changed";
  224. assert getPastTotalSupply(e, fromBlock) == totalSupplyBefore, "previous totalSupply not saved properly";
  225. }
  226. rule onBurn() {
  227. env e;
  228. uint256 amount;
  229. address account;
  230. uint256 fromBlock = e.block.number;
  231. uint224 totalVotesBefore = totalVotes();
  232. uint256 totalSupplyBefore = totalSupply();
  233. _burn(e, account, amount);
  234. assert totalVotes() == totalVotesBefore, "totalVotes changed";
  235. assert getPastTotalSupply(e, fromBlock) == totalSupplyBefore, "previous totalSupply not saved properly";
  236. }