ERC20Votes.spec 12 KB

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