ERC20.spec 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. import "helpers/helpers.spec";
  2. import "methods/IERC20.spec";
  3. import "methods/IERC2612.spec";
  4. methods {
  5. // exposed for FV
  6. function mint(address,uint256) external;
  7. function burn(address,uint256) external;
  8. }
  9. /*
  10. ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
  11. │ Ghost & hooks: sum of all balances │
  12. └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
  13. */
  14. ghost mathint sumOfBalances {
  15. init_state axiom sumOfBalances == 0;
  16. }
  17. // Because `balance` has a uint256 type, any balance addition in CVL1 behaved as a `require_uint256()` casting,
  18. // leaving out the possibility of overflow. This is not the case in CVL2 where casting became more explicit.
  19. // A counterexample in CVL2 is having an initial state where Alice initial balance is larger than totalSupply, which
  20. // overflows Alice's balance when receiving a transfer. This is not possible unless the contract is deployed into an
  21. // already used address (or upgraded from corrupted state).
  22. // We restrict such behavior by making sure no balance is greater than the sum of balances.
  23. hook Sload uint256 balance _balances[KEY address addr] STORAGE {
  24. require sumOfBalances >= to_mathint(balance);
  25. }
  26. hook Sstore _balances[KEY address addr] uint256 newValue (uint256 oldValue) STORAGE {
  27. sumOfBalances = sumOfBalances - oldValue + newValue;
  28. }
  29. /*
  30. ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
  31. │ Invariant: totalSupply is the sum of all balances │
  32. └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
  33. */
  34. invariant totalSupplyIsSumOfBalances()
  35. to_mathint(totalSupply()) == sumOfBalances;
  36. /*
  37. ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
  38. │ Invariant: balance of address(0) is 0 │
  39. └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
  40. */
  41. invariant zeroAddressNoBalance()
  42. balanceOf(0) == 0;
  43. /*
  44. ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
  45. │ Rules: only mint and burn can change total supply │
  46. └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
  47. */
  48. rule noChangeTotalSupply(env e) {
  49. requireInvariant totalSupplyIsSumOfBalances();
  50. method f;
  51. calldataarg args;
  52. uint256 totalSupplyBefore = totalSupply();
  53. f(e, args);
  54. uint256 totalSupplyAfter = totalSupply();
  55. assert totalSupplyAfter > totalSupplyBefore => f.selector == sig:mint(address,uint256).selector;
  56. assert totalSupplyAfter < totalSupplyBefore => f.selector == sig:burn(address,uint256).selector;
  57. }
  58. /*
  59. ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
  60. │ Rules: only the token holder or an approved third party can reduce an account's balance │
  61. └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
  62. */
  63. rule onlyAuthorizedCanTransfer(env e) {
  64. requireInvariant totalSupplyIsSumOfBalances();
  65. method f;
  66. calldataarg args;
  67. address account;
  68. uint256 allowanceBefore = allowance(account, e.msg.sender);
  69. uint256 balanceBefore = balanceOf(account);
  70. f(e, args);
  71. uint256 balanceAfter = balanceOf(account);
  72. assert (
  73. balanceAfter < balanceBefore
  74. ) => (
  75. f.selector == sig:burn(address,uint256).selector ||
  76. e.msg.sender == account ||
  77. balanceBefore - balanceAfter <= to_mathint(allowanceBefore)
  78. );
  79. }
  80. /*
  81. ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
  82. │ Rules: only the token holder (or a permit) can increase allowance. The spender can decrease it by using it │
  83. └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
  84. */
  85. rule onlyHolderOfSpenderCanChangeAllowance(env e) {
  86. requireInvariant totalSupplyIsSumOfBalances();
  87. method f;
  88. calldataarg args;
  89. address holder;
  90. address spender;
  91. uint256 allowanceBefore = allowance(holder, spender);
  92. f(e, args);
  93. uint256 allowanceAfter = allowance(holder, spender);
  94. assert (
  95. allowanceAfter > allowanceBefore
  96. ) => (
  97. (f.selector == sig:approve(address,uint256).selector && e.msg.sender == holder) ||
  98. (f.selector == sig:permit(address,address,uint256,uint256,uint8,bytes32,bytes32).selector)
  99. );
  100. assert (
  101. allowanceAfter < allowanceBefore
  102. ) => (
  103. (f.selector == sig:transferFrom(address,address,uint256).selector && e.msg.sender == spender) ||
  104. (f.selector == sig:approve(address,uint256).selector && e.msg.sender == holder ) ||
  105. (f.selector == sig:permit(address,address,uint256,uint256,uint8,bytes32,bytes32).selector)
  106. );
  107. }
  108. /*
  109. ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
  110. │ Rules: mint behavior and side effects │
  111. └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
  112. */
  113. rule mint(env e) {
  114. requireInvariant totalSupplyIsSumOfBalances();
  115. require nonpayable(e);
  116. address to;
  117. address other;
  118. uint256 amount;
  119. // cache state
  120. uint256 toBalanceBefore = balanceOf(to);
  121. uint256 otherBalanceBefore = balanceOf(other);
  122. uint256 totalSupplyBefore = totalSupply();
  123. // run transaction
  124. mint@withrevert(e, to, amount);
  125. // check outcome
  126. if (lastReverted) {
  127. assert to == 0 || totalSupplyBefore + amount > max_uint256;
  128. } else {
  129. // updates balance and totalSupply
  130. assert to_mathint(balanceOf(to)) == toBalanceBefore + amount;
  131. assert to_mathint(totalSupply()) == totalSupplyBefore + amount;
  132. // no other balance is modified
  133. assert balanceOf(other) != otherBalanceBefore => other == to;
  134. }
  135. }
  136. /*
  137. ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
  138. │ Rules: burn behavior and side effects │
  139. └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
  140. */
  141. rule burn(env e) {
  142. requireInvariant totalSupplyIsSumOfBalances();
  143. require nonpayable(e);
  144. address from;
  145. address other;
  146. uint256 amount;
  147. // cache state
  148. uint256 fromBalanceBefore = balanceOf(from);
  149. uint256 otherBalanceBefore = balanceOf(other);
  150. uint256 totalSupplyBefore = totalSupply();
  151. // run transaction
  152. burn@withrevert(e, from, amount);
  153. // check outcome
  154. if (lastReverted) {
  155. assert from == 0 || fromBalanceBefore < amount;
  156. } else {
  157. // updates balance and totalSupply
  158. assert to_mathint(balanceOf(from)) == fromBalanceBefore - amount;
  159. assert to_mathint(totalSupply()) == totalSupplyBefore - amount;
  160. // no other balance is modified
  161. assert balanceOf(other) != otherBalanceBefore => other == from;
  162. }
  163. }
  164. /*
  165. ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
  166. │ Rule: transfer behavior and side effects │
  167. └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
  168. */
  169. rule transfer(env e) {
  170. requireInvariant totalSupplyIsSumOfBalances();
  171. require nonpayable(e);
  172. address holder = e.msg.sender;
  173. address recipient;
  174. address other;
  175. uint256 amount;
  176. // cache state
  177. uint256 holderBalanceBefore = balanceOf(holder);
  178. uint256 recipientBalanceBefore = balanceOf(recipient);
  179. uint256 otherBalanceBefore = balanceOf(other);
  180. // run transaction
  181. transfer@withrevert(e, recipient, amount);
  182. // check outcome
  183. if (lastReverted) {
  184. assert holder == 0 || recipient == 0 || amount > holderBalanceBefore;
  185. } else {
  186. // balances of holder and recipient are updated
  187. assert to_mathint(balanceOf(holder)) == holderBalanceBefore - (holder == recipient ? 0 : amount);
  188. assert to_mathint(balanceOf(recipient)) == recipientBalanceBefore + (holder == recipient ? 0 : amount);
  189. // no other balance is modified
  190. assert balanceOf(other) != otherBalanceBefore => (other == holder || other == recipient);
  191. }
  192. }
  193. /*
  194. ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
  195. │ Rule: transferFrom behavior and side effects │
  196. └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
  197. */
  198. rule transferFrom(env e) {
  199. requireInvariant totalSupplyIsSumOfBalances();
  200. require nonpayable(e);
  201. address spender = e.msg.sender;
  202. address holder;
  203. address recipient;
  204. address other;
  205. uint256 amount;
  206. // cache state
  207. uint256 allowanceBefore = allowance(holder, spender);
  208. uint256 holderBalanceBefore = balanceOf(holder);
  209. uint256 recipientBalanceBefore = balanceOf(recipient);
  210. uint256 otherBalanceBefore = balanceOf(other);
  211. // run transaction
  212. transferFrom@withrevert(e, holder, recipient, amount);
  213. // check outcome
  214. if (lastReverted) {
  215. assert holder == 0 || recipient == 0 || spender == 0 || amount > holderBalanceBefore || amount > allowanceBefore;
  216. } else {
  217. // allowance is valid & updated
  218. assert allowanceBefore >= amount;
  219. assert to_mathint(allowance(holder, spender)) == (allowanceBefore == max_uint256 ? max_uint256 : allowanceBefore - amount);
  220. // balances of holder and recipient are updated
  221. assert to_mathint(balanceOf(holder)) == holderBalanceBefore - (holder == recipient ? 0 : amount);
  222. assert to_mathint(balanceOf(recipient)) == recipientBalanceBefore + (holder == recipient ? 0 : amount);
  223. // no other balance is modified
  224. assert balanceOf(other) != otherBalanceBefore => (other == holder || other == recipient);
  225. }
  226. }
  227. /*
  228. ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
  229. │ Rule: approve behavior and side effects │
  230. └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
  231. */
  232. rule approve(env e) {
  233. require nonpayable(e);
  234. address holder = e.msg.sender;
  235. address spender;
  236. address otherHolder;
  237. address otherSpender;
  238. uint256 amount;
  239. // cache state
  240. uint256 otherAllowanceBefore = allowance(otherHolder, otherSpender);
  241. // run transaction
  242. approve@withrevert(e, spender, amount);
  243. // check outcome
  244. if (lastReverted) {
  245. assert holder == 0 || spender == 0;
  246. } else {
  247. // allowance is updated
  248. assert allowance(holder, spender) == amount;
  249. // other allowances are untouched
  250. assert allowance(otherHolder, otherSpender) != otherAllowanceBefore => (otherHolder == holder && otherSpender == spender);
  251. }
  252. }
  253. /*
  254. ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
  255. │ Rule: permit behavior and side effects │
  256. └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
  257. */
  258. rule permit(env e) {
  259. require nonpayable(e);
  260. address holder;
  261. address spender;
  262. uint256 amount;
  263. uint256 deadline;
  264. uint8 v;
  265. bytes32 r;
  266. bytes32 s;
  267. address account1;
  268. address account2;
  269. address account3;
  270. // cache state
  271. uint256 nonceBefore = nonces(holder);
  272. uint256 otherNonceBefore = nonces(account1);
  273. uint256 otherAllowanceBefore = allowance(account2, account3);
  274. // sanity: nonce overflow, which possible in theory, is assumed to be impossible in practice
  275. require nonceBefore < max_uint256;
  276. require otherNonceBefore < max_uint256;
  277. // run transaction
  278. permit@withrevert(e, holder, spender, amount, deadline, v, r, s);
  279. // check outcome
  280. if (lastReverted) {
  281. // Without formally checking the signature, we can't verify exactly the revert causes
  282. assert true;
  283. } else {
  284. // allowance and nonce are updated
  285. assert allowance(holder, spender) == amount;
  286. assert to_mathint(nonces(holder)) == nonceBefore + 1;
  287. // deadline was respected
  288. assert deadline >= e.block.timestamp;
  289. // no other allowance or nonce is modified
  290. assert nonces(account1) != otherNonceBefore => account1 == holder;
  291. assert allowance(account2, account3) != otherAllowanceBefore => (account2 == holder && account3 == spender);
  292. }
  293. }