ERC20Votes.spec 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. using ERC20VotesHarness as erc20votes
  2. methods {
  3. // functions
  4. checkpoints(address, uint32) envfree
  5. numCheckpoints(address) returns (uint32) envfree
  6. delegates(address) returns (address) envfree
  7. getVotes(address) returns (uint256) envfree
  8. getPastVotes(address, uint256) returns (uint256)
  9. getPastTotalSupply(uint256) returns (uint256)
  10. delegate(address)
  11. _delegate(address, address)
  12. // delegateBySig(address, uint256, uint256, uint8, bytes32, bytes32)
  13. totalSupply() returns (uint256) envfree
  14. _maxSupply() returns (uint224) envfree
  15. // harnesss functions
  16. ckptFromBlock(address, uint32) returns (uint32) envfree
  17. ckptVotes(address, uint32) returns (uint224) envfree
  18. mint(address, uint256)
  19. burn(address, uint256)
  20. unsafeNumCheckpoints(address) returns (uint256) envfree
  21. // solidity generated getters
  22. _delegates(address) returns (address) envfree
  23. // external functions
  24. }
  25. // gets the most recent votes for a user
  26. ghost userVotes(address) returns uint224;
  27. // sums the total votes for all users
  28. ghost totalVotes() returns mathint {
  29. init_state axiom totalVotes() == 0;
  30. axiom totalVotes() >= 0;
  31. }
  32. ghost lastIndex(address) returns uint32;
  33. // helper
  34. // blocked by tool error
  35. invariant totalVotes_gte_accounts()
  36. forall address a. forall address b. a != b => totalVotes() >= getVotes(a) + getVotes(b)
  37. hook Sstore _checkpoints[KEY address account][INDEX uint32 index].votes uint224 newVotes (uint224 oldVotes) STORAGE {
  38. havoc userVotes assuming
  39. userVotes@new(account) == newVotes;
  40. havoc totalVotes assuming
  41. totalVotes@new() == totalVotes@old() + to_mathint(newVotes) - to_mathint(userVotes(account));
  42. havoc lastIndex assuming
  43. lastIndex@new(account) == index;
  44. }
  45. ghost lastFromBlock(address) returns uint32;
  46. ghost doubleFromBlock(address) returns bool {
  47. init_state axiom forall address a. doubleFromBlock(a) == false;
  48. }
  49. hook Sstore _checkpoints[KEY address account][INDEX uint32 index].fromBlock uint32 newBlock (uint32 oldBlock) STORAGE {
  50. havoc lastFromBlock assuming
  51. lastFromBlock@new(account) == newBlock;
  52. havoc doubleFromBlock assuming
  53. doubleFromBlock@new(account) == (newBlock == lastFromBlock(account));
  54. }
  55. rule sanity(method f) {
  56. env e;
  57. calldataarg arg;
  58. f(e, arg);
  59. assert false;
  60. }
  61. // something stupid just to see
  62. invariant sanity_invariant()
  63. totalSupply() >= 0
  64. // sum of user balances is >= total amount of delegated votes
  65. // blocked by tool error
  66. invariant votes_solvency()
  67. to_mathint(totalSupply()) >= totalVotes()
  68. { preserved {
  69. require forall address account. unsafeNumCheckpoints(account) < 4294967295;
  70. requireInvariant totalVotes_gte_accounts();
  71. }}
  72. // for some checkpoint, the fromBlock is less than the current block number
  73. // passes but fails rule sanity from hash on delegate by sig
  74. invariant timestamp_constrains_fromBlock(address account, uint32 index, env e)
  75. ckptFromBlock(account, index) < e.block.number
  76. {
  77. preserved {
  78. require index < numCheckpoints(account);
  79. }
  80. }
  81. // TODO add a note about this in the report
  82. // // numCheckpoints are less than maxInt
  83. // // passes because numCheckpoints does a safeCast
  84. // invariant maxInt_constrains_numBlocks(address account)
  85. // numCheckpoints(account) < 4294967295 // 2^32
  86. // // fails because there are no checks to stop it
  87. // invariant maxInt_constrains_ckptsLength(address account)
  88. // unsafeNumCheckpoints(account) < 4294967295 // 2^32
  89. // can't have more checkpoints for a given account than the last from block
  90. // passes
  91. invariant fromBlock_constrains_numBlocks(address account)
  92. numCheckpoints(account) <= ckptFromBlock(account, numCheckpoints(account) - 1)
  93. { preserved with(env e) {
  94. uint32 pos;
  95. uint32 pos2;
  96. requireInvariant fromBlock_greaterThanEq_pos(account, pos);
  97. requireInvariant fromBlock_increasing(account, pos, pos2);
  98. require e.block.number >= ckptFromBlock(account, numCheckpoints(account) - 1); // this should be true from the invariant above!!
  99. }}
  100. // for any given checkpoint, the fromBlock must be greater than the checkpoint
  101. // this proves the above invariant in combination with the below invariant
  102. // if checkpoint has a greater fromBlock than the last, and the FromBlock is always greater than the pos.
  103. // Then the number of positions must be less than the currentFromBlock
  104. // ^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
  105. // passes + rule sanity
  106. invariant fromBlock_greaterThanEq_pos(address account, uint32 pos)
  107. ckptFromBlock(account, pos) >= pos
  108. // a larger index must have a larger fromBlock
  109. // passes + rule sanity
  110. invariant fromBlock_increasing(address account, uint32 pos, uint32 pos2)
  111. pos > pos2 => ckptFromBlock(account, pos) > ckptFromBlock(account, pos2)
  112. invariant no_delegate_no_checkpoints(address account)
  113. delegates(account) == 0x0 => numCheckpoints(account) == 0
  114. { preserved delegate(address delegatee) with(env e) {
  115. require delegatee != 0;
  116. } preserved _delegate(address delegator, address delegatee) with(env e) {
  117. require delegatee != 0;
  118. }}
  119. // converted from an invariant to a rule to slightly change the logic
  120. // if the fromBlock is the same as before, then the number of checkpoints stays the same
  121. // however if the fromBlock is new than the number of checkpoints increases
  122. rule unique_checkpoints_rule(method f) {
  123. env e; calldataarg args;
  124. require e.block.number > 0; // we don't care about this exception
  125. address account;
  126. require unsafeNumCheckpoints(account) < 4294967295; // 2^32 // we don't want to deal with the checkpoint overflow error here
  127. uint32 num_ckpts_ = numCheckpoints(account);
  128. uint32 fromBlock_ = num_ckpts_ == 0 ? 0 : ckptFromBlock(account, num_ckpts_ - 1);
  129. f(e, args);
  130. uint32 _num_ckpts = numCheckpoints(account);
  131. uint32 _fromBlock = _num_ckpts == 0 ? 0 : ckptFromBlock(account, _num_ckpts - 1);
  132. // assert fromBlock_ == _fromBlock => num_ckpts_ == _num_ckpts, "same fromBlock, new checkpoint";
  133. assert doubleFromBlock(account) => num_ckpts_ == _num_ckpts, "same fromBlock, new checkpoint";
  134. // this assert fails consistently
  135. // assert !doubleFromBlock(account) => ckpts_ != _ckpts, "new fromBlock but total checkpoints not being increased";
  136. }
  137. // assumes neither account has delegated
  138. // currently fails due to this scenario. A has maxint number of checkpoints
  139. // an additional checkpoint is added which overflows and sets A's votes to 0
  140. rule transfer_safe() {
  141. env e;
  142. uint256 amount;
  143. address a; address b;
  144. require a != b;
  145. require delegates(a) != delegates(b); // confirmed if they both delegate to the same person then transfer keeps the votes the sameå
  146. // requireInvariant fromBlock_constrains_numBlocks(a);
  147. // requireInvariant fromBlock_constrains_numBlocks(b);
  148. // requireInvariant totalVotes_gte_accounts(a, b);
  149. uint256 votesA_pre = getVotes(delegates(a));
  150. uint256 votesB_pre = getVotes(delegates(b));
  151. // for debugging
  152. uint256 balA_ = balanceOf(e, a);
  153. uint256 balB_ = balanceOf(e, b);
  154. mathint totalVotes_pre = totalVotes();
  155. erc20votes.transferFrom(e, a, b, amount);
  156. require lastIndex(delegates(a)) < 1000000;
  157. require lastIndex(delegates(b)) < 1000000;
  158. mathint totalVotes_post = totalVotes();
  159. uint256 votesA_post = getVotes(delegates(a));
  160. uint256 votesB_post = getVotes(delegates(b));
  161. // for debugging
  162. uint256 _balA = balanceOf(e, a);
  163. uint256 _balB = balanceOf(e, b);
  164. // if an account that has not delegated transfers balance to an account that has, it will increase the total supply of votes
  165. assert totalVotes_pre == totalVotes_post, "transfer changed total supply";
  166. assert delegates(a) != 0 => votesA_pre - votesA_post == amount, "A lost the wrong amount of votes";
  167. assert delegates(b) != 0 => votesB_post - votesB_pre == amount, "B lost the wrong amount of votes";
  168. }
  169. // for any given function f, if the delegate is changed the function must be delegate or delegateBySig
  170. // passes
  171. rule delegates_safe(method f) filtered {f -> (f.selector != delegate(address).selector &&
  172. f.selector != _delegate(address, address).selector &&
  173. f.selector != delegateBySig(address, uint256, uint256, uint8, bytes32, bytes32).selector) }
  174. {
  175. env e; calldataarg args;
  176. address account;
  177. address pre = delegates(account);
  178. f(e, args);
  179. address post = delegates(account);
  180. assert pre == post, "invalid delegate change";
  181. }
  182. rule delegator_votes_removed() {
  183. env e;
  184. address delegator; address delegatee;
  185. require delegator != delegatee;
  186. require delegates(delegator) == 0; // has not delegated
  187. uint256 pre = getVotes(delegator);
  188. _delegate(e, delegator, delegatee);
  189. uint256 balance = balanceOf(e, delegator);
  190. uint256 post = getVotes(delegator);
  191. assert post == pre - balance, "delegator retained votes";
  192. }
  193. // delegates increases the delegatee's votes by the proper amount
  194. // passes + rule sanity
  195. rule delegatee_receives_votes() {
  196. env e;
  197. address delegator; address delegatee;
  198. require delegates(delegator) != delegatee;
  199. require delegatee != 0x0;
  200. uint256 delegator_bal = balanceOf(e, delegator);
  201. uint256 votes_= getVotes(delegatee);
  202. _delegate(e, delegator, delegatee);
  203. require lastIndex(delegatee) < 1000000;
  204. uint256 _votes = getVotes(delegatee);
  205. assert _votes == votes_ + delegator_bal, "delegatee did not receive votes";
  206. }
  207. rule previous_delegatee_zeroed() {
  208. env e;
  209. address delegator; address delegatee; address third;
  210. require third != delegatee;
  211. require third != delegator;
  212. require delegates(delegator) == third;
  213. // for third to have been delegated to, it must have a checkpoint
  214. uint256 delegator_bal = balanceOf(e, delegator);
  215. uint256 votes_ = getVotes(third);
  216. _delegate(e, delegator, delegatee);
  217. uint256 _votes = getVotes(third);
  218. assert _votes == votes_ - delegator_bal, "votes not removed from the previous delegatee";
  219. }
  220. // passes with rule sanity
  221. rule delegate_contained() {
  222. env e;
  223. address delegator; address delegatee; address other;
  224. require other != delegatee;
  225. require other != delegates(delegator);
  226. uint256 votes_ = getVotes(other);
  227. _delegate(e, delegator, delegatee);
  228. uint256 _votes = getVotes(other);
  229. assert votes_ == _votes, "votes not contained";
  230. }
  231. // checks all of the above rules with front running
  232. rule delegate_no_frontrunning(method f) {
  233. env e; calldataarg args;
  234. address delegator; address delegatee; address third; address other;
  235. require delegator != delegatee;
  236. require delegates(delegator) == third;
  237. require other != third;
  238. uint256 delegator_bal = erc20votes.balanceOf(e, delegator);
  239. uint256 dr_ = getVotes(delegator);
  240. uint256 de_ = getVotes(delegatee);
  241. uint256 third_ = getVotes(third);
  242. uint256 other_ = getVotes(other);
  243. // require third is address for previous delegator
  244. f(e, args);
  245. _delegate(e, delegator, delegatee);
  246. uint256 _dr = getVotes(delegator);
  247. uint256 _de = getVotes(delegatee);
  248. uint256 _third = getVotes(third);
  249. uint256 _other = getVotes(other);
  250. // delegator loses all of their votes
  251. // delegatee gains that many votes
  252. // third loses any votes delegated to them
  253. assert _dr == 0, "delegator retained votes";
  254. assert _de == de_ + delegator_bal, "delegatee not received votes";
  255. assert _third != 0 => _third == third_ - delegator_bal, "votes not removed from third";
  256. assert other_ == _other, "delegate not contained";
  257. }
  258. // passes
  259. rule mint_increases_totalSupply() {
  260. env e;
  261. uint256 amount; address account;
  262. uint256 fromBlock = e.block.number;
  263. uint256 totalSupply_ = totalSupply();
  264. mint(e, account, amount);
  265. uint256 _totalSupply = totalSupply();
  266. require _totalSupply < _maxSupply();
  267. assert _totalSupply == totalSupply_ + amount, "totalSupply not increased properly";
  268. assert getPastTotalSupply(e, fromBlock) == totalSupply_ , "previous total supply not saved properly";
  269. }
  270. // passes
  271. rule burn_decreases_totalSupply() {
  272. env e;
  273. uint256 amount; address account;
  274. uint256 fromBlock = e.block.number;
  275. uint256 totalSupply_ = totalSupply();
  276. require totalSupply_ > balanceOf(e, account);
  277. burn(e, account, amount);
  278. uint256 _totalSupply = totalSupply();
  279. require _totalSupply < _maxSupply();
  280. assert _totalSupply == totalSupply_ - amount, "totalSupply not decreased properly";
  281. assert getPastTotalSupply(e, fromBlock) == totalSupply_ , "previous total supply not saved properly";
  282. }
  283. // passes
  284. rule mint_doesnt_increase_totalVotes() {
  285. env e;
  286. uint256 amount; address account;
  287. mathint totalVotes_ = totalVotes();
  288. mint(e, account, amount);
  289. assert totalVotes() == totalVotes_, "totalVotes increased";
  290. }
  291. // passes
  292. rule burn_doesnt_decrease_totalVotes() {
  293. env e;
  294. uint256 amount; address account;
  295. mathint totalVotes_ = totalVotes();
  296. burn(e, account, amount);
  297. assert totalVotes() == totalVotes_, "totalVotes decreased";
  298. }
  299. // // fails
  300. // rule mint_increases_totalVotes() {
  301. // env e;
  302. // uint256 amount; address account;
  303. // mathint totalVotes_ = totalVotes();
  304. // mint(e, account, amount);
  305. // assert totalVotes() == totalVotes_ + to_mathint(amount), "totalVotes not increased";
  306. // }
  307. // // fails
  308. // rule burn_decreases_totalVotes() {
  309. // env e;
  310. // uint256 amount; address account;
  311. // mathint totalVotes_ = totalVotes();
  312. // burn(e, account, amount);
  313. // assert totalVotes() == totalVotes_ - to_mathint(amount), "totalVotes not decreased";
  314. // }