├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── lesson1_prerequisites └── formal_verification │ ├── Empty.sol │ ├── sisters.conf │ └── sisters.spec ├── lesson2_started ├── erc20 │ ├── ERC20.sol │ ├── ERC20.spec │ ├── ERC20Fixed.spec │ ├── IERC20.sol │ ├── IERC20Metadata.sol │ ├── Parametric.conf │ ├── Parametric.spec │ ├── TotalGreaterThanUser.conf │ ├── TotalGreaterThanUser.spec │ └── sample_conf.conf └── vacuity │ ├── Vacuous.conf │ ├── Vacuous.sol │ ├── Vacuous.spec │ └── Vacuous_sanity.conf ├── lesson3_violations ├── Borda │ ├── Borda.conf │ ├── Borda.sol │ ├── Borda.spec │ ├── BordaBug1.conf │ ├── BordaBug1.sol │ ├── BordaBug2.conf │ ├── BordaBug2.sol │ ├── BordaBug3.conf │ ├── BordaBug3.sol │ ├── BordaBug4.conf │ ├── BordaBug4.sol │ ├── IBorda.sol │ └── bounty_specs │ │ ├── BordaMissingSpecUnified.spec │ │ ├── BordaNewBug2.sol │ │ ├── BordaNewBug3.sol │ │ ├── BordaNewBug4.sol │ │ ├── BordaNewBug5.sol │ │ └── README.md └── ERC20 │ ├── ERC20.conf │ ├── ERC20.sol │ ├── ERC20.spec │ ├── IERC20.sol │ └── IERC20Metadata.sol ├── lesson4_invariants ├── auction │ └── EnglishAuction.sol ├── erc20 │ ├── ERC20.sol │ ├── IERC20.sol │ ├── IERC20Metadata.sol │ ├── adding_a_ghost.spec │ ├── total_is_sum.conf │ ├── total_is_sum.spec │ ├── total_supply.conf │ ├── total_supply.spec │ ├── total_supply_direct.conf │ └── total_supply_direct.spec ├── manager │ ├── IManager.sol │ ├── Manager.conf │ ├── Manager.sol │ ├── Manager.spec │ ├── ManagerBug1.sol │ └── ManagerBug2.sol ├── partial_debt_token │ ├── DebtToken.conf │ ├── DebtToken.sol │ ├── DebtToken.spec │ ├── FailedDebtToken.conf │ └── FailedDebtToken.spec ├── simple_voting │ ├── Voting.conf │ ├── Voting.sol │ ├── Voting.spec │ ├── VotingBug1.sol │ ├── VotingBug2.sol │ ├── VotingBug3.sol │ ├── VotingBug4.sol │ ├── VotingBug5.sol │ ├── Voting_ghost_basic.conf │ └── Voting_ghost_basic.spec └── sqrt │ ├── SquareRoot.conf │ ├── SquareRoot.sol │ └── SquareRoot.spec ├── misc_examples ├── Ghostly.sol └── ghost_deduction.spec └── solutions ├── lesson3_violations └── ERC20 │ └── ERC20Fixed.spec └── lesson4_invariants ├── auction ├── EnglishAuction.conf ├── EnglishAuction.spec ├── EnglishAuction_strict.conf └── EnglishAuction_strict.spec ├── erc20 ├── zero.conf └── zero.spec ├── manager ├── ManagerBug1.conf ├── ManagerBug2.conf ├── ManagerGood.conf ├── Manager_ghost.conf ├── Manager_ghost.spec └── Manager_unique.spec └── simple_voting ├── VotingBug1.conf ├── VotingBug2.conf ├── VotingBug3.conf ├── VotingBug4.conf ├── VotingBug5.conf ├── Voting_solution.conf ├── Voting_solution.spec └── mutate.conf /.gitattributes: -------------------------------------------------------------------------------- 1 | *.spec linguist-language=Solidity 2 | *.conf linguist-detectable 3 | *.conf linguist-language=JSON5 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Package specifics 2 | 3 | # Mac 4 | .DS_Store 5 | 6 | # Certora 7 | .certora_internal/ 8 | .certora_recent_jobs.json 9 | .zip-output-url.txt 10 | **/collect.json 11 | **/emv-* 12 | 13 | # vim 14 | .*.swp 15 | .*.swo 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Certora 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tutorials code 2 | 3 | This repository contains the code examples for the 4 | [Certora Prover Tutorials](https://docs.certora.com/projects/tutorials/en/latest/). 5 | The [Certora Prover](https://www.certora.com/) is a formal verification tool for 6 | smart contract. The tutorials are part of the 7 | [Certora Prover Documentation](https://docs.certora.com/en/latest/). 8 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # Needed to be included in the tutorials-package 2 | -------------------------------------------------------------------------------- /lesson1_prerequisites/formal_verification/Empty.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | contract Empty { 4 | } 5 | -------------------------------------------------------------------------------- /lesson1_prerequisites/formal_verification/sisters.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "Empty.sol" 4 | ], 5 | "verify": "Empty:sisters.spec", 6 | "wait_for_results": "all", 7 | "rule_sanity": "basic", 8 | "msg": "Sisters riddle" 9 | } 10 | -------------------------------------------------------------------------------- /lesson1_prerequisites/formal_verification/sisters.spec: -------------------------------------------------------------------------------- 1 | /* Four sisters riddle 2 | * ------------------- 3 | */ 4 | 5 | // The months 6 | definition September() returns uint8 = 9; 7 | definition October() returns uint8 = 10; 8 | definition November() returns uint8 = 11; 9 | definition December() returns uint8 = 12; 10 | 11 | /** @title Find the sister's birth months 12 | * See Birth Months riddle from: 13 | * https://www.braingle.com/brainteasers/teaser.php?op=2&id=52499&comm=0 14 | * 15 | * The parameters to the rule are the sisters' birth months. 16 | */ 17 | rule sistersBirthMonths( 18 | uint8 Sara, 19 | uint8 Ophelia, 20 | uint8 Nora, 21 | uint8 Dawn 22 | ) { 23 | // Each sister was born in one of the four months 24 | require Sara >= September() && Sara <= December(); 25 | require Ophelia >= September() && Ophelia <= December(); 26 | require Nora >= September() && Nora <= December(); 27 | require Dawn >= September() && Dawn <= December(); 28 | 29 | // "None of us have an initial that matches the initial of her birth month." 30 | require ( 31 | Sara != September() && 32 | Ophelia != October() && 33 | Nora != November() && 34 | Dawn != December() 35 | ); 36 | 37 | // Ophelia is not the girl who was born in September 38 | require Ophelia != September(); 39 | 40 | // Nora is not the girl who was born in September 41 | require Nora != September(); 42 | 43 | // Nora's birth month does not start with a vowel 44 | require Nora != October(); 45 | 46 | // The sisters were born on different months 47 | require ( 48 | Sara != Ophelia && 49 | Sara != Nora && 50 | Sara != Dawn && 51 | Ophelia != Nora && 52 | Ophelia != Dawn && 53 | Nora != Dawn 54 | ); 55 | 56 | satisfy true; 57 | } 58 | 59 | 60 | /// @title Verify that the solution is unique 61 | rule solutionIsUnique( 62 | uint8 Sara, 63 | uint8 Ophelia, 64 | uint8 Nora, 65 | uint8 Dawn 66 | ) { 67 | // Each sister was born in one of the four months 68 | require Sara >= September() && Sara <= December(); 69 | require Ophelia >= September() && Ophelia <= December(); 70 | require Nora >= September() && Nora <= December(); 71 | require Dawn >= September() && Dawn <= December(); 72 | 73 | // "None of us have an initial that matches the initial of her birth month." 74 | require ( 75 | Sara != September() && 76 | Ophelia != October() && 77 | Nora != November() && 78 | Dawn != December() 79 | ); 80 | 81 | // Ophelia is not the girl who was born in September 82 | require Ophelia != September(); 83 | 84 | // Nora is not the girl who was born in September 85 | require Nora != September(); 86 | 87 | // Nora's birth month does not start with a vowel 88 | require Nora != October(); 89 | 90 | // The sisters were born on different months 91 | require ( 92 | Sara != Ophelia && 93 | Sara != Nora && 94 | Sara != Dawn && 95 | Ophelia != Nora && 96 | Ophelia != Dawn && 97 | Nora != Dawn 98 | ); 99 | 100 | assert ( 101 | Sara == October() && 102 | Ophelia == November() && 103 | Nora == December() && 104 | Dawn == September() 105 | ); 106 | } 107 | -------------------------------------------------------------------------------- /lesson2_started/erc20/ERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts v4.4.1 (token/ERC20/ERC20.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | import "./IERC20.sol"; 7 | import "./IERC20Metadata.sol"; 8 | 9 | /** 10 | * @dev Implementation of the {IERC20} interface. 11 | * 12 | * This implementation is agnostic to the way tokens are created. This means 13 | * that a supply mechanism has to be added in a derived contract using {_mint}. 14 | * For a generic mechanism see {ERC20PresetMinterPauser}. 15 | * 16 | * TIP: For a detailed writeup see our guide 17 | * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How 18 | * to implement supply mechanisms]. 19 | * 20 | * We have followed general OpenZeppelin Contracts guidelines: functions revert 21 | * instead returning `false` on failure. This behavior is nonetheless 22 | * conventional and does not conflict with the expectations of ERC20 23 | * applications. 24 | * 25 | * Additionally, an {Approval} event is emitted on calls to {transferFrom}. 26 | * This allows applications to reconstruct the allowance for all accounts just 27 | * by listening to said events. Other implementations of the EIP may not emit 28 | * these events, as it isn't required by the specification. 29 | * 30 | * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} 31 | * functions have been added to mitigate the well-known issues around setting 32 | * allowances. See {IERC20-approve}. 33 | */ 34 | contract ERC20 is IERC20, IERC20Metadata { 35 | mapping(address => uint256) private _balances; 36 | 37 | mapping(address => mapping(address => uint256)) private _allowances; 38 | 39 | uint256 private _totalSupply; 40 | 41 | address public _owner; 42 | 43 | /** 44 | * @dev Sets the values for {name} and {symbol}. 45 | * 46 | * The default value of {decimals} is 18. To select a different value for 47 | * {decimals} you should overload it. 48 | * 49 | * All two of these values are immutable: they can only be set once during 50 | * construction. 51 | */ 52 | constructor() { 53 | } 54 | 55 | modifier onlyOwner() { 56 | require(_owner == msg.sender); 57 | _; 58 | } 59 | 60 | /** 61 | * @dev Returns the name of the token. 62 | */ 63 | function name() public view virtual override returns (string memory) { 64 | return ""; 65 | } 66 | 67 | /** 68 | * @dev Returns the symbol of the token, usually a shorter version of the 69 | * name. 70 | */ 71 | function symbol() public view virtual override returns (string memory) { 72 | return ""; 73 | } 74 | 75 | /** 76 | * @dev Returns the number of decimals used to get its user representation. 77 | * For example, if `decimals` equals `2`, a balance of `505` tokens should 78 | * be displayed to a user as `5.05` (`505 / 10 ** 2`). 79 | * 80 | * Tokens usually opt for a value of 18, imitating the relationship between 81 | * Ether and Wei. This is the value {ERC20} uses, unless this function is 82 | * overridden; 83 | * 84 | * NOTE: This information is only used for _display_ purposes: it in 85 | * no way affects any of the arithmetic of the contract, including 86 | * {IERC20-balanceOf} and {IERC20-transfer}. 87 | */ 88 | function decimals() public view virtual override returns (uint8) { 89 | return 18; 90 | } 91 | 92 | /** 93 | * @dev See {IERC20-totalSupply}. 94 | */ 95 | function totalSupply() public view virtual override returns (uint256) { 96 | return _totalSupply; 97 | } 98 | 99 | /** 100 | * @dev See {IERC20-balanceOf}. 101 | */ 102 | function balanceOf(address account) 103 | public 104 | view 105 | virtual 106 | override 107 | returns (uint256) 108 | { 109 | return _balances[account]; 110 | } 111 | 112 | /** 113 | * @dev See {IERC20-transfer}. 114 | * 115 | * Requirements: 116 | * 117 | * - `recipient` cannot be the zero address. 118 | * - the caller must have a balance of at least `amount`. 119 | */ 120 | function transfer(address recipient, uint256 amount) 121 | public 122 | virtual 123 | override 124 | returns (bool) 125 | { 126 | _transfer(msg.sender, recipient, amount); 127 | return true; 128 | } 129 | 130 | /** 131 | * @dev See {IERC20-allowance}. 132 | */ 133 | function allowance(address owner, address spender) 134 | public 135 | view 136 | virtual 137 | override 138 | returns (uint256) 139 | { 140 | return _allowances[owner][spender]; 141 | } 142 | 143 | /** 144 | * @dev See {IERC20-approve}. 145 | * 146 | * Requirements: 147 | * 148 | * - `spender` cannot be the zero address. 149 | */ 150 | function approve(address spender, uint256 amount) 151 | public 152 | virtual 153 | override 154 | returns (bool) 155 | { 156 | _approve(msg.sender, spender, amount); 157 | return true; 158 | } 159 | 160 | /** 161 | * @dev See {IERC20-transferFrom}. 162 | * 163 | * Emits an {Approval} event indicating the updated allowance. This is not 164 | * required by the EIP. See the note at the beginning of {ERC20}. 165 | * 166 | * Requirements: 167 | * 168 | * - `sender` and `recipient` cannot be the zero address. 169 | * - `sender` must have a balance of at least `amount`. 170 | * - the caller must have allowance for ``sender``'s tokens of at least 171 | * `amount`. 172 | */ 173 | function transferFrom( 174 | address sender, 175 | address recipient, 176 | uint256 amount 177 | ) public virtual override returns (bool) { 178 | uint256 currentAllowance = _allowances[sender][msg.sender]; 179 | require( 180 | currentAllowance >= amount, 181 | "ERC20: transfer amount exceeds allowance" 182 | ); 183 | unchecked { 184 | _approve(sender, msg.sender, currentAllowance - amount); 185 | } 186 | 187 | _transfer(sender, recipient, amount); 188 | 189 | return true; 190 | } 191 | 192 | /** 193 | * @dev Atomically increases the allowance granted to `spender` by the caller. 194 | * 195 | * This is an alternative to {approve} that can be used as a mitigation for 196 | * problems described in {IERC20-approve}. 197 | * 198 | * Emits an {Approval} event indicating the updated allowance. 199 | * 200 | * Requirements: 201 | * 202 | * - `spender` cannot be the zero address. 203 | */ 204 | function increaseAllowance(address spender, uint256 addedValue) 205 | public 206 | virtual 207 | returns (bool) 208 | { 209 | _approve( 210 | msg.sender, 211 | spender, 212 | _allowances[msg.sender][spender] + addedValue 213 | ); 214 | return true; 215 | } 216 | 217 | /** 218 | * @dev Atomically decreases the allowance granted to `spender` by the caller. 219 | * 220 | * This is an alternative to {approve} that can be used as a mitigation for 221 | * problems described in {IERC20-approve}. 222 | * 223 | * Emits an {Approval} event indicating the updated allowance. 224 | * 225 | * Requirements: 226 | * 227 | * - `spender` cannot be the zero address. 228 | * - `spender` must have allowance for the caller of at least 229 | * `subtractedValue`. 230 | */ 231 | function decreaseAllowance(address spender, uint256 subtractedValue) 232 | public 233 | virtual 234 | returns (bool) 235 | { 236 | uint256 currentAllowance = _allowances[msg.sender][spender]; 237 | 238 | unchecked { 239 | _approve(msg.sender, spender, currentAllowance - subtractedValue); 240 | } 241 | 242 | return true; 243 | } 244 | 245 | /** 246 | * @dev Moves `amount` of tokens from `sender` to `recipient`. 247 | * 248 | * This internal function is equivalent to {transfer}, and can be used to 249 | * e.g. implement automatic token fees, slashing mechanisms, etc. 250 | * 251 | * Emits a {Transfer} event. 252 | * 253 | * Requirements: 254 | * 255 | * - `sender` cannot be the zero address. 256 | * - `recipient` cannot be the zero address. 257 | * - `sender` must have a balance of at least `amount`. 258 | */ 259 | function _transfer( 260 | address sender, 261 | address recipient, 262 | uint256 amount 263 | ) internal virtual { 264 | require(sender != address(0), "ERC20: transfer from the zero address"); 265 | require(recipient != address(0), "ERC20: transfer to the zero address"); 266 | 267 | _beforeTokenTransfer(sender, recipient, amount); 268 | 269 | uint256 senderBalance = _balances[sender]; 270 | require( 271 | senderBalance >= amount, 272 | "ERC20: transfer amount exceeds balance" 273 | ); 274 | unchecked { 275 | _balances[sender] = senderBalance - amount; 276 | } 277 | _balances[recipient] += amount; 278 | 279 | emit Transfer(sender, recipient, amount); 280 | 281 | _afterTokenTransfer(sender, recipient, amount); 282 | } 283 | 284 | /** @dev Creates `amount` tokens and assigns them to `account`, increasing 285 | * the total supply. 286 | * 287 | * Emits a {Transfer} event with `from` set to the zero address. 288 | * 289 | * Requirements: 290 | * 291 | * - `account` cannot be the zero address. 292 | */ 293 | function mint(address account, uint256 amount) onlyOwner() public virtual override { 294 | require(account != address(0), "ERC20: mint to the zero address"); 295 | 296 | _beforeTokenTransfer(address(0), account, amount); 297 | 298 | _totalSupply += amount; 299 | _balances[account] += amount; 300 | emit Transfer(address(0), account, amount); 301 | 302 | _afterTokenTransfer(address(0), account, amount); 303 | } 304 | 305 | /** 306 | * @dev Destroys `amount` tokens from `account`, reducing the 307 | * total supply. 308 | * 309 | * Emits a {Transfer} event with `to` set to the zero address. 310 | * 311 | * Requirements: 312 | * 313 | * - `account` cannot be the zero address. 314 | * - `account` must have at least `amount` tokens. 315 | */ 316 | function burn(address account, uint256 amount) onlyOwner() public virtual override { 317 | require(account != address(0), "ERC20: burn from the zero address"); 318 | 319 | _beforeTokenTransfer(account, address(0), amount); 320 | 321 | uint256 accountBalance = _balances[account]; 322 | require(accountBalance >= amount, "ERC20: burn amount exceeds balance"); 323 | unchecked { 324 | _balances[account] = accountBalance - amount; 325 | } 326 | _totalSupply -= amount; 327 | 328 | emit Transfer(account, address(0), amount); 329 | 330 | _afterTokenTransfer(account, address(0), amount); 331 | } 332 | 333 | /** 334 | * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. 335 | * 336 | * This internal function is equivalent to `approve`, and can be used to 337 | * e.g. set automatic allowances for certain subsystems, etc. 338 | * 339 | * Emits an {Approval} event. 340 | * 341 | * Requirements: 342 | * 343 | * - `owner` cannot be the zero address. 344 | * - `spender` cannot be the zero address. 345 | */ 346 | function _approve( 347 | address owner, 348 | address spender, 349 | uint256 amount 350 | ) internal virtual { 351 | require(owner != address(0), "ERC20: approve from the zero address"); 352 | require(spender != address(0), "ERC20: approve to the zero address"); 353 | _allowances[owner][spender] = amount; 354 | emit Approval(owner, spender, amount); 355 | } 356 | 357 | /** 358 | * @dev Hook that is called before any transfer of tokens. This includes 359 | * minting and burning. 360 | * 361 | * Calling conditions: 362 | * 363 | * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens 364 | * will be transferred to `to`. 365 | * - when `from` is zero, `amount` tokens will be minted for `to`. 366 | * - when `to` is zero, `amount` of ``from``'s tokens will be burned. 367 | * - `from` and `to` are never both zero. 368 | * 369 | * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. 370 | */ 371 | function _beforeTokenTransfer( 372 | address from, 373 | address to, 374 | uint256 amount 375 | ) internal virtual {} 376 | 377 | /** 378 | * @dev Hook that is called after any transfer of tokens. This includes 379 | * minting and burning. 380 | * 381 | * Calling conditions: 382 | * 383 | * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens 384 | * has been transferred to `to`. 385 | * - when `from` is zero, `amount` tokens have been minted for `to`. 386 | * - when `to` is zero, `amount` of ``from``'s tokens have been burned. 387 | * - `from` and `to` are never both zero. 388 | * 389 | * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. 390 | */ 391 | function _afterTokenTransfer( 392 | address from, 393 | address to, 394 | uint256 amount 395 | ) internal virtual {} 396 | 397 | function deposit() external payable { 398 | _balances[msg.sender] += msg.value; 399 | } 400 | 401 | function withdraw(uint256 amount) external { 402 | require(amount <= _balances[msg.sender]); 403 | _balances[msg.sender] -= amount; 404 | (bool success, ) = msg.sender.call{value: amount}(""); 405 | require(success); 406 | } 407 | } 408 | -------------------------------------------------------------------------------- /lesson2_started/erc20/ERC20.spec: -------------------------------------------------------------------------------- 1 | /** 2 | * # ERC20 Example 3 | * 4 | * This is an example specification for a generic ERC20 contract. It contains several 5 | * simple rules verifying the integrity of the transfer function. 6 | * To run, execute the following command in terminal: 7 | * 8 | * certoraRun ERC20.sol --verify ERC20:ERC20.spec --solc solc8.0 9 | * 10 | * One of the rules here is badly phrased, and results in an erroneous fail. 11 | * Understand the counter example provided by the Prover and then run the fixed 12 | * spec: 13 | * 14 | * certoraRun ERC20.sol --verify ERC20:ERC20Fixed.spec --solc solc8.0 15 | */ 16 | 17 | // The methods block below gives various declarations regarding solidity methods. 18 | methods 19 | { 20 | // When a function is not using the environment (e.g., `msg.sender`), it can be 21 | // declared as `envfree` 22 | function balanceOf(address) external returns (uint) envfree; 23 | function allowance(address,address) external returns(uint) envfree; 24 | function totalSupply() external returns (uint) envfree; 25 | } 26 | 27 | 28 | /** @title Transfer must move `amount` tokens from the caller's 29 | * account to `recipient` 30 | */ 31 | rule transferSpec(address recipient, uint amount) { 32 | 33 | env e; 34 | 35 | // `mathint` is a type that represents an integer of any size 36 | mathint balance_sender_before = balanceOf(e.msg.sender); 37 | mathint balance_recip_before = balanceOf(recipient); 38 | 39 | transfer(e, recipient, amount); 40 | 41 | mathint balance_sender_after = balanceOf(e.msg.sender); 42 | mathint balance_recip_after = balanceOf(recipient); 43 | 44 | // Operations on mathints can never overflow nor underflow 45 | assert balance_sender_after == balance_sender_before - amount, 46 | "transfer must decrease sender's balance by amount"; 47 | 48 | assert balance_recip_after == balance_recip_before + amount, 49 | "transfer must increase recipient's balance by amount"; 50 | } 51 | 52 | 53 | /// @title Transfer must revert if the sender's balance is too small 54 | rule transferReverts(address recipient, uint amount) { 55 | env e; 56 | 57 | require balanceOf(e.msg.sender) < amount; 58 | 59 | transfer@withrevert(e, recipient, amount); 60 | 61 | assert lastReverted, 62 | "transfer(recipient,amount) must revert if sender's balance is less than `amount`"; 63 | } 64 | 65 | 66 | /** @title Transfer must not revert unless 67 | * - the sender doesn't have enough funds, 68 | * - or the message value is nonzero, 69 | * - or the recipient's balance would overflow, 70 | * - or the message sender is 0, 71 | * - or the recipient is 0 72 | */ 73 | rule transferDoesntRevert(address recipient, uint amount) { 74 | env e; 75 | 76 | require balanceOf(e.msg.sender) > amount; 77 | require e.msg.value == 0; // No payment 78 | 79 | // This requirement prevents overflow of recipient's balance. 80 | // We convert `max_uint` to type `mathint` since: 81 | // 1. a sum always returns type `mathint`, hence the left hand side is `mathint`, 82 | // 2. `mathint` can only be compared to another `mathint` 83 | require balanceOf(recipient) + amount < to_mathint(max_uint); 84 | 85 | // Recall that `address(0)` is a special address that in general should not be used 86 | require e.msg.sender != 0; 87 | require recipient != 0; 88 | 89 | transfer@withrevert(e, recipient, amount); 90 | assert !lastReverted; 91 | } 92 | -------------------------------------------------------------------------------- /lesson2_started/erc20/ERC20Fixed.spec: -------------------------------------------------------------------------------- 1 | /** 2 | * # Fixed ERC20 Example 3 | * 4 | * This is the fixed version of ERC20.spec. Note the changes in rule `transferSpec`. 5 | * Run using: 6 | * 7 | * certoraRun ERC20.sol: --verify ERC20:ERC20Fixed.spec --solc solc8.0 8 | * 9 | * There should be no errors. 10 | */ 11 | 12 | // The methods block below gives various declarations regarding solidity methods. 13 | methods 14 | { 15 | // When a function is not using the environment (e.g., `msg.sender`), it can be 16 | // declared as `envfree` 17 | function balanceOf(address) external returns (uint) envfree; 18 | function allowance(address,address) external returns(uint) envfree; 19 | function totalSupply() external returns (uint) envfree; 20 | } 21 | 22 | 23 | /// @title Transfer must move `amount` tokens from the caller's account to `recipient` 24 | rule transferSpec(address recipient, uint amount) { 25 | 26 | env e; 27 | 28 | // `mathint` is a type that represents an integer of any size 29 | mathint balance_sender_before = balanceOf(e.msg.sender); 30 | mathint balance_recip_before = balanceOf(recipient); 31 | 32 | transfer(e, recipient, amount); 33 | 34 | mathint balance_sender_after = balanceOf(e.msg.sender); 35 | mathint balance_recip_after = balanceOf(recipient); 36 | 37 | address sender = e.msg.sender; // A convenient alias 38 | 39 | // Operations on mathints can never overflow or underflow. 40 | assert recipient != sender => balance_sender_after == balance_sender_before - amount, 41 | "transfer must decrease sender's balance by amount"; 42 | 43 | assert recipient != sender => balance_recip_after == balance_recip_before + amount, 44 | "transfer must increase recipient's balance by amount"; 45 | 46 | assert recipient == sender => balance_sender_after == balance_sender_before, 47 | "transfer must not change sender's balancer when transferring to self"; 48 | } 49 | 50 | 51 | /// @title Transfer must revert if the sender's balance is too small 52 | rule transferReverts(address recipient, uint amount) { 53 | env e; 54 | 55 | require balanceOf(e.msg.sender) < amount; 56 | 57 | transfer@withrevert(e, recipient, amount); 58 | 59 | assert lastReverted, 60 | "transfer(recipient,amount) must revert if sender's balance is less than `amount`"; 61 | } 62 | 63 | 64 | /** @title Transfer must not revert unless 65 | * - the sender doesn't have enough funds, 66 | * - or the message value is nonzero, 67 | * - or the recipient's balance would overflow, 68 | * - or the message sender is 0, 69 | * - or the recipient is 0 70 | */ 71 | rule transferDoesntRevert(address recipient, uint amount) { 72 | env e; 73 | 74 | require balanceOf(e.msg.sender) > amount; 75 | require e.msg.value == 0; // No payment 76 | 77 | // This requirement prevents overflow of recipient's balance. 78 | // We convert `max_uint` to type `mathint` since: 79 | // 1. a sum always returns type `mathint`, hence the left hand side is `mathint`, 80 | // 2. `mathint` can only be compared to another `mathint` 81 | require balanceOf(recipient) + amount < to_mathint(max_uint); 82 | 83 | // Recall that `address(0)` is a special address that in general should not be used 84 | require e.msg.sender != 0; 85 | require recipient != 0; 86 | 87 | transfer@withrevert(e, recipient, amount); 88 | assert !lastReverted; 89 | } 90 | -------------------------------------------------------------------------------- /lesson2_started/erc20/IERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts v4.4.1 (token/ERC20/IERC20.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | /** 7 | * @dev Implementation of the {IERC20} interface. 8 | * 9 | * This implementation is agnostic to the way tokens are created. This means 10 | * that a supply mechanism has to be added in a derived contract using {_mint}. 11 | * For a generic mechanism see {ERC20PresetMinterPauser}. 12 | * 13 | * TIP: For a detailed writeup see our guide 14 | * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How 15 | * to implement supply mechanisms]. 16 | * 17 | * We have followed general OpenZeppelin Contracts guidelines: functions revert 18 | * instead returning `false` on failure. This behavior is nonetheless 19 | * conventional and does not conflict with the expectations of ERC20 20 | * applications. 21 | * 22 | * Additionally, an {Approval} event is emitted on calls to {transferFrom}. 23 | * This allows applications to reconstruct the allowance for all accounts just 24 | * by listening to said events. Other implementations of the EIP may not emit 25 | * these events, as it isn't required by the specification. 26 | * 27 | * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} 28 | * functions have been added to mitigate the well-known issues around setting 29 | * allowances. See {IERC20-approve}. 30 | */ 31 | 32 | /** 33 | * Interface of the ERC20 standard as defined in the EIP. 34 | */ 35 | interface IERC20 { 36 | /** 37 | * @dev Returns the amount of tokens in existence. 38 | */ 39 | function totalSupply() external view returns (uint256); 40 | 41 | /** 42 | * @dev Returns the amount of tokens owned by `account`. 43 | */ 44 | function balanceOf(address account) external view returns (uint256); 45 | 46 | /** 47 | * @dev Moves `amount` tokens from the caller's account to `recipient`. 48 | * 49 | * Returns a boolean value indicating whether the operation succeeded. 50 | * 51 | * Emits a {Transfer} event. 52 | */ 53 | function transfer(address recipient, uint256 amount) 54 | external 55 | returns (bool); 56 | 57 | /** 58 | * Returns the remaining number of tokens that `spender` will be 59 | * allowed to spend on behalf of `owner` through {transferFrom}. This is 60 | * zero by default. 61 | * 62 | * This value changes when {approve} or {transferFrom} are called. 63 | */ 64 | function allowance(address owner, address spender) 65 | external 66 | view 67 | returns (uint256); 68 | 69 | /** 70 | * Sets `amount` as the allowance of `spender` over the caller's tokens. 71 | * 72 | * Returns a boolean value indicating whether the operation succeeded. 73 | * 74 | * IMPORTANT: Beware that changing an allowance with this method brings the risk 75 | * that someone may use both the old and the new allowance by unfortunate 76 | * transaction ordering. One possible solution to mitigate this race 77 | * condition is to first reduce the spender's allowance to 0 and set the 78 | * desired value afterwards: 79 | * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 80 | * 81 | * Emits an {Approval} event. 82 | */ 83 | function approve(address spender, uint256 amount) external returns (bool); 84 | 85 | /** 86 | * @dev Moves `amount` tokens from `sender` to `recipient` using the 87 | * allowance mechanism. `amount` is then deducted from the caller's 88 | * allowance. 89 | * 90 | * Returns a boolean value indicating whether the operation succeeded. 91 | * 92 | * Emits a {Transfer} event. 93 | */ 94 | function transferFrom( 95 | address sender, 96 | address recipient, 97 | uint256 amount 98 | ) external returns (bool); 99 | 100 | /** 101 | * @dev generates token to the system by awarding a specified user `account` 102 | * a specific `amount` 103 | * 104 | * Emits a {Transfer} event. 105 | */ 106 | function mint(address, uint256) external; 107 | 108 | /** 109 | * @dev removes token from the system by burning a specific `amount` 110 | * from a specified user `account` 111 | * 112 | * Emits a {Transfer} event. 113 | */ 114 | function burn(address, uint256) external; 115 | 116 | /** 117 | * @dev Emitted when `value` tokens are moved from one account (`from`) to 118 | * another (`to`). 119 | * 120 | * Note that `value` may be zero. 121 | */ 122 | event Transfer(address indexed from, address indexed to, uint256 value); 123 | 124 | /** 125 | * @dev Emitted when the allowance of a `spender` for an `owner` is set by 126 | * a call to {approve}. `value` is the new allowance. 127 | */ 128 | event Approval( 129 | address indexed owner, 130 | address indexed spender, 131 | uint256 value 132 | ); 133 | } 134 | -------------------------------------------------------------------------------- /lesson2_started/erc20/IERC20Metadata.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/IERC20Metadata.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | import "./IERC20.sol"; 7 | 8 | /** 9 | * @dev Interface for the optional metadata functions from the ERC20 standard. 10 | * 11 | * _Available since v4.1._ 12 | */ 13 | interface IERC20Metadata is IERC20 { 14 | /** 15 | * @dev Returns the name of the token. 16 | */ 17 | function name() external view returns (string memory); 18 | 19 | /** 20 | * @dev Returns the symbol of the token. 21 | */ 22 | function symbol() external view returns (string memory); 23 | 24 | /** 25 | * @dev Returns the decimals places of the token. 26 | */ 27 | function decimals() external view returns (uint8); 28 | } 29 | -------------------------------------------------------------------------------- /lesson2_started/erc20/Parametric.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "ERC20.sol" 4 | ], 5 | "verify": "ERC20:Parametric.spec", 6 | "wait_for_results": "all", 7 | "rule_sanity": "basic", 8 | "msg": "Parametric rule example" 9 | } 10 | -------------------------------------------------------------------------------- /lesson2_started/erc20/Parametric.spec: -------------------------------------------------------------------------------- 1 | /** 2 | * # ERC20 Parametric Example 3 | * 4 | * Another example specification for an ERC20 contract. This one using a parametric rule, 5 | * which is a rule that encompasses all the methods in the current contract. It is called 6 | * parametric since one of the rule's parameters is the current contract method. 7 | * To run enter: 8 | * 9 | * certoraRun ERC20.sol --verify ERC20:Parametric.spec --solc solc8.0 --msg "Parametric rule" 10 | * 11 | * The `onlyHolderCanChangeAllowance` fails for one of the methods. Look at the Prover 12 | * results and understand the counter example - which discovers a weakness in the 13 | * current contract. 14 | */ 15 | 16 | // The methods block below gives various declarations regarding solidity methods. 17 | methods 18 | { 19 | // When a function is not using the environment (e.g., `msg.sender`), it can be 20 | // declared as `envfree` 21 | function balanceOf(address) external returns (uint) envfree; 22 | function allowance(address,address) external returns(uint) envfree; 23 | function totalSupply() external returns (uint) envfree; 24 | } 25 | 26 | 27 | /// @title If `approve` changes a holder's allowance, then it was called by the holder 28 | rule onlyHolderCanChangeAllowance(address holder, address spender, method f) { 29 | 30 | // The allowance before the method was called 31 | mathint allowance_before = allowance(holder, spender); 32 | 33 | env e; 34 | calldataarg args; // Arguments for the method f 35 | f(e, args); 36 | 37 | // The allowance after the method was called 38 | mathint allowance_after = allowance(holder, spender); 39 | 40 | assert allowance_after > allowance_before => e.msg.sender == holder, 41 | "only the sender can change its own allowance"; 42 | 43 | // Assert that if the allowance changed then `approve` or `increaseAllowance` was called. 44 | assert ( 45 | allowance_after > allowance_before => 46 | ( 47 | f.selector == sig:approve(address, uint).selector || 48 | f.selector == sig:increaseAllowance(address, uint).selector 49 | ) 50 | ), 51 | "only approve and increaseAllowance can increase allowances"; 52 | } 53 | -------------------------------------------------------------------------------- /lesson2_started/erc20/TotalGreaterThanUser.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "ERC20.sol" 4 | ], 5 | "verify": "ERC20:TotalGreaterThanUser.spec", 6 | "wait_for_results": "all", 7 | "rule_sanity": "basic", 8 | "msg": "Preconditions example" 9 | } 10 | -------------------------------------------------------------------------------- /lesson2_started/erc20/TotalGreaterThanUser.spec: -------------------------------------------------------------------------------- 1 | /** 2 | * # Total Supply Over-Approximation Example 3 | * 4 | * The rules here are intended to verify that an ERC20's `totalSupply` method follows 5 | * the basic property: 6 | * For and `address user` we have `totalSupply() >= balanceOf(user)`. 7 | * 8 | * First run only the rule `totalSupplyAfterMint`: 9 | * 10 | * certoraRun ERC20.sol --verify ERC20:TotalGreaterThanUser.spec --solc --rule totalSupplyAfterMint 11 | * 12 | * This rule will fail due to the Prover's tendency to over-approximate the states. 13 | * Now run the fixed rule `totalSupplyAfterMintWithPrecondition`: 14 | * 15 | * certoraRun ERC20.sol --verify ERC20:TotalGreaterThanUser.spec --solc --rule totalSupplyAfterMintWithPrecondition 16 | * 17 | * Do you understand why the second rule passed? 18 | */ 19 | 20 | // The methods block below gives various declarations regarding solidity methods. 21 | methods 22 | { 23 | // When a function is not using the environment (e.g., `msg.sender`), it can be 24 | // declared as `envfree` 25 | function balanceOf(address) external returns (uint) envfree; 26 | function allowance(address,address) external returns(uint) envfree; 27 | function totalSupply() external returns (uint) envfree; 28 | } 29 | 30 | 31 | /// @title Total supply after mint is at least the balance of the receiving account 32 | rule totalSupplyAfterMint(address account, uint256 amount) { 33 | env e; 34 | 35 | mint(e, account, amount); 36 | 37 | uint256 userBalanceAfter = balanceOf(account); 38 | uint256 totalAfter = totalSupply(); 39 | 40 | // Verify that the total supply of the system is at least the current balance of the account. 41 | assert totalAfter >= userBalanceAfter, "total supply is less than a user's balance"; 42 | } 43 | 44 | 45 | /** @title Total supply after mint is at least the balance of the receiving account, with 46 | * precondition. 47 | */ 48 | rule totalSupplyAfterMintWithPrecondition(address account, uint256 amount) { 49 | env e; 50 | 51 | // Assume that in the current state before calling mint, the total supply of the 52 | // system is at least the user balance. 53 | uint256 userBalanceBefore = balanceOf(account); 54 | uint256 totalBefore = totalSupply(); 55 | require totalBefore >= userBalanceBefore; 56 | 57 | mint(e, account, amount); 58 | 59 | uint256 userBalanceAfter = balanceOf(account); 60 | uint256 totalAfter = totalSupply(); 61 | 62 | // Verify that the total supply of the system is at least the current balance of the account. 63 | assert totalAfter >= userBalanceAfter, "total supply is less than a user's balance "; 64 | } 65 | -------------------------------------------------------------------------------- /lesson2_started/erc20/sample_conf.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "ERC20.sol" 4 | ], 5 | "verify": "ERC20:ERC20.spec", 6 | "wait_for_results": "all", 7 | "rule_sanity": "basic", 8 | // Note: json5 supports comments! 9 | "msg": "First run using .conf file" 10 | } 11 | -------------------------------------------------------------------------------- /lesson2_started/vacuity/Vacuous.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "Vacuous.sol" 4 | ], 5 | "verify": "Vacuous:Vacuous.spec", 6 | "wait_for_results": "all", 7 | "msg": "Vacuous rules without sanity check" 8 | } 9 | -------------------------------------------------------------------------------- /lesson2_started/vacuity/Vacuous.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /// @title Examples of vacuous rules 5 | contract Vacuous { 6 | 7 | mapping (address => uint256) public amounts; 8 | uint256 public immutable maximalAmount = 1000; 9 | 10 | function add(address user, uint256 amount) public returns (uint256) { 11 | require(amounts[user] + amount <= maximalAmount); 12 | amounts[user] += amount; 13 | return amounts[user]; 14 | } 15 | 16 | function sub(address user, uint256 amount) public returns (uint256) { 17 | amounts[user] -= amount; 18 | return amounts[user]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lesson2_started/vacuity/Vacuous.spec: -------------------------------------------------------------------------------- 1 | /** 2 | * Vacuous rules examples 3 | */ 4 | methods { 5 | function amounts(address) external returns (uint256) envfree; 6 | function maximalAmount() external returns (uint256) envfree; 7 | function add(address, uint256) external returns (uint256) envfree; 8 | function sub(address, uint256) external returns (uint256) envfree; 9 | } 10 | 11 | 12 | rule simpleVacuousRule(uint256 x, uint256 y) { 13 | // Contradictory requirement 14 | require (x > y) && (y > x); 15 | assert false; // Should always fail 16 | } 17 | 18 | 19 | rule subtleVacuousRule(address user, uint256 amount) { 20 | uint256 userAmount = amounts(user); 21 | require amount > userAmount; 22 | sub(user, amount); 23 | assert false; // Should always fail 24 | } 25 | 26 | 27 | rule revertingRule(address user, uint256 amount) { 28 | uint256 userAmount = amounts(user); 29 | require amount > userAmount; 30 | sub@withrevert(user, amount); 31 | assert lastReverted; 32 | } 33 | -------------------------------------------------------------------------------- /lesson2_started/vacuity/Vacuous_sanity.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "Vacuous.sol" 4 | ], 5 | "verify": "Vacuous:Vacuous.spec", 6 | "wait_for_results": "all", 7 | "rule_sanity": "basic", 8 | "msg": "Vacuous rules with sanity check" 9 | } 10 | -------------------------------------------------------------------------------- /lesson3_violations/Borda/Borda.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "Borda.sol:Borda" 4 | ], 5 | "verify": "Borda:Borda.spec", 6 | "wait_for_results": "all", 7 | "msg": "Verification of Borda", 8 | "rule_sanity" : "basic" // Using "advanced" results in many sanity failures 9 | } 10 | -------------------------------------------------------------------------------- /lesson3_violations/Borda/Borda.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | import "./IBorda.sol"; 3 | 4 | contract Borda is IBorda{ 5 | 6 | // The current winner 7 | address public _winner; 8 | 9 | // A map storing whether an address has already voted. Initialized to false. 10 | mapping (address => bool) _voted; 11 | 12 | // Points each candidate has recieved, initialized to zero. 13 | mapping (address => uint256) _points; 14 | 15 | // current maximum points of all candidates. 16 | uint256 public pointsOfWinner; 17 | 18 | 19 | function vote(address f, address s, address t) public override { 20 | require(!_voted[msg.sender], "this voter has already cast its vote"); 21 | require( f != s && f != t && s != t, "candidates are not different"); 22 | _voted[msg.sender] = true; 23 | voteTo(f, 3); 24 | voteTo(s, 2); 25 | voteTo(t, 1); 26 | } 27 | 28 | function voteTo(address c, uint256 p) private { 29 | //update points 30 | _points[c] = _points[c]+ p; 31 | // update winner if needed 32 | if (_points[c] > _points[_winner]) { 33 | _winner = c; 34 | } 35 | } 36 | 37 | function winner() external view override returns (address) { 38 | return _winner; 39 | } 40 | 41 | function points(address c) public view override returns (uint256) { 42 | return _points[c]; 43 | } 44 | 45 | function voted(address x) public view override returns(bool) { 46 | return _voted[x]; 47 | } 48 | } -------------------------------------------------------------------------------- /lesson3_violations/Borda/Borda.spec: -------------------------------------------------------------------------------- 1 | /* 2 | * Borda Example 3 | * ------------- 4 | * 5 | * Verification of a simple voting contract which uses a Borda election. 6 | * See https://en.wikipedia.org/wiki/Borda_count 7 | */ 8 | 9 | 10 | 11 | methods { 12 | function points(address) external returns uint256 envfree; 13 | function vote(address,address,address) external; 14 | function voted(address) external returns bool envfree; 15 | function winner() external returns address envfree; 16 | } 17 | 18 | /* 19 | After voting, a user is marked as voted 20 | vote(e, f, s, t) => voted(e.msg.sender) 21 | */ 22 | rule integrityVote(address f, address s, address t) { 23 | env e; 24 | vote(e, f, s, t); //considering only non reverting cases 25 | assert voted(e.msg.sender), "A user voted without being marked accordingly"; 26 | } 27 | 28 | /* 29 | Single vote per user 30 | A user cannot vote if he has voted before 31 | voted(e.msg.sender) => ㄱvote(e, f, s, t) 32 | */ 33 | rule singleVote(address f, address s, address t) { 34 | env e; 35 | bool has_voted_before = voted(e.msg.sender); 36 | vote@withrevert(e, f, s, t); //considering all cases 37 | assert has_voted_before => lastReverted, "Double voting is not allowed"; 38 | } 39 | 40 | /* 41 | Integrity of points: 42 | When voting, the points each candidate gets are updated as expected. 43 | This rule also verifies that there are three distinct candidates. 44 | 45 | { points(f) = f_points ⋀ points(s) = s_points ⋀ points(t) = t_points } 46 | vote(e, f, s, t) 47 | { points(f) = f_points + 3 ⋀ points(s) = s_points + 2 ⋀ points(t) = t_points + 1 } 48 | */ 49 | rule integrityPoints(address f, address s, address t) { 50 | env e; 51 | uint256 f_points = points(f); 52 | uint256 s_points = points(s); 53 | uint256 t_points = points(t); 54 | vote(e, f, s, t); 55 | assert to_mathint(points(f)) == f_points + 3 && 56 | to_mathint(points(s)) == s_points + 2 && 57 | to_mathint(points(t)) == t_points + 1, "unexpected change of points"; 58 | } 59 | 60 | /* 61 | Integrity of voted: 62 | Once a user casts their vote, they are marked as voted globally (for all future states). 63 | vote(e, f, s, t) Globally voted(e.msg.sender) 64 | */ 65 | rule globallyVoted(address x, method f) { 66 | require voted(x); 67 | env eF; 68 | calldataarg arg; 69 | f(eF,arg); //taking into account all external function with all possible arguments 70 | assert voted(x), "Once a user voted, he is marked as voted in all future states"; 71 | } 72 | 73 | /* 74 | Integrity of winner 75 | The winner has the most points. 76 | winner() = forall ∀address c. points(c) ≤ points(w) 77 | 78 | 79 | Note: The Prover checks that the invariant is established after the constructor. In addition, Prover checks that the invariant holds after the execution of any contract method, assuming that it held before the method was executed. 80 | Note that c is an unconstrained variable therefore this invariant is checked against all possible values of c. 81 | */ 82 | invariant integrityPointsOfWinner(address c) 83 | points(winner()) >= points(c); 84 | 85 | /* 86 | Vote is the only state-changing function. 87 | A vote can only affect the voter and the selected candidates, and has no effect on other addresses. 88 | ∀address c, c ≠ {f, s, t}. 89 | { c_points = points(c) ⋀ b = voted(c) } vote(e, f, s, t) { points(c) = c_points ⋀ ( voted(c) = b V c = e.msg.sender ) } 90 | */ 91 | rule noEffect(method m) { 92 | address c; 93 | env e; 94 | uint256 c_points = points(c); 95 | bool c_voted = voted(c); 96 | if (m.selector == sig:vote(address, address, address).selector) { 97 | address f; 98 | address s; 99 | address t; 100 | require( c != f && c != s && c != t ); 101 | vote(e, f, s, t); 102 | } 103 | else { 104 | calldataarg args; 105 | m(e, args); 106 | } 107 | assert ( voted(c) == c_voted || c == e.msg.sender ) && 108 | points(c) == c_points, "unexpected change to others points or voted"; 109 | } 110 | 111 | 112 | /* 113 | Commutativity of votes. 114 | The order of votes is not important 115 | vote(e, f, s, t) ; vote(e’, f’, s’, t’) ~ vote(e’, f’, s’, t’) ; vote(e, f, s, t) 116 | 117 | Note: This is a hyperproperty as it compares the results of different executions. 118 | */ 119 | rule voteCommutativity(address f1, address s1, address t1, address f2, address s2, address t2) { 120 | env e1; 121 | env e2; 122 | address c; 123 | address y; 124 | // A variable of type storage represents a snapshot of the EVM storage 125 | storage init = lastStorage; 126 | 127 | 128 | // First 1 votes, then 2 129 | vote(e1, f1, s1, t1); 130 | vote(e2, f2, s2, t2); 131 | uint256 case1 = points(c); 132 | 133 | // EVM storage is reset to the saved storage value 134 | vote(e2, f2, s2, t2) at init; 135 | vote(e1, f1, s1, t1); 136 | uint256 case2 = points(c); 137 | // Assert commutativity with respect to points (but not winner...) 138 | assert case1 == case2, 139 | "vote() is not commutative"; 140 | } 141 | 142 | 143 | /* 144 | Ability to vote 145 | If a user can vote, no other user can prevent him to do so by any operation. 146 | vote(e, f, s, t) ~ op; vote(e, f, s, t) (unless reached max_uint) 147 | */ 148 | rule allowVote(address f, address s, address t, method m) { 149 | env e; 150 | storage init = lastStorage; 151 | vote(e, f, s, t); // Ensures the user can vote 152 | 153 | env eOther; 154 | require (e.msg.sender != eOther.msg.sender); 155 | calldataarg args; 156 | m(eOther, args) at init; //reset to the initial state 157 | 158 | require points(f) < max_uint256 - 3 && points(s) < max_uint256 - 2 && points(t) < max_uint256 ; // No overflow 159 | 160 | vote@withrevert(e, f, s, t); 161 | 162 | assert !lastReverted, "a user can not be blocked from voting"; 163 | } 164 | 165 | 166 | /* 167 | Revert case of vote 168 | A user can vote, unless overflow in points or she has voted already 169 | */ 170 | rule oneCanVote(address f, address s, address t) { 171 | env e; 172 | require e.msg.value == 0 ; //ignore revert on non payable 173 | bool overflowCheck = ( points(f) <= max_uint256 - 3 && points(s) <= max_uint256 - 2 && points(t) < max_uint256 ); 174 | bool _voted = voted(e.msg.sender); 175 | vote@withrevert(e, f, s, t); 176 | bool reverted = lastReverted; 177 | 178 | assert ( overflowCheck && !_voted && f!=s && s!=t && f!=t ) 179 | <=> !reverted, "a user who hasn't yet voted should be able to do so unless there is an overflow for some candidate(s)"; 180 | } 181 | 182 | 183 | /* 184 | Participation criterion 185 | Abstaining from an election can not help a voter's preferred choice 186 | https://en.wikipedia.org/wiki/Participation_criterion 187 | 188 | { w1 = winner() } 189 | ( vote(e, f, s, t) ) 190 | { winner() = f => (w1 = f) } 191 | */ 192 | rule participationCriterion(address f, address s, address t) { 193 | env e; 194 | address w1 = winner(); 195 | require points(w1) >= points(f); 196 | require points(w1) >= points(s); 197 | require points(w1) >= points(t); 198 | vote(e, f, s, t); 199 | address w2 = winner(); 200 | assert w1 == f => w2 == f, "winner changed unexpectedly"; 201 | } 202 | 203 | /* 204 | Resolvability criterion 205 | Given a tie, there is a way for one added vote to make that winner unique. 206 | This property is proven by showing that given a tie , there is a vote that makes the winner unique. 207 | 208 | Note: This is implemented with the satisfy statement as the property require that exist a scenario and not on every vote the tie must break. 209 | This property require uses quantifiers which works over ghost 210 | */ 211 | 212 | // Ghosts are additional variables for use during verification, and often used to communicate information between rules and hooks. 213 | 214 | 215 | ghost mapping(address => uint256) points_mirror { 216 | init_state axiom forall address c. points_mirror[c] == 0; 217 | } 218 | 219 | ghost mathint countVoters { 220 | init_state axiom countVoters == 0; 221 | } 222 | ghost mathint sumPoints { 223 | init_state axiom sumPoints == 0; 224 | } 225 | 226 | /* update ghost on changes to _points */ 227 | hook Sstore _points[KEY address a] uint256 new_points (uint256 old_points) { 228 | points_mirror[a] = new_points; 229 | sumPoints = sumPoints + new_points - old_points; 230 | } 231 | 232 | hook Sload uint256 curr_point _points[KEY address a] { 233 | require points_mirror[a] == curr_point; 234 | } 235 | 236 | hook Sstore _voted[KEY address a] bool val (bool old_val) { 237 | countVoters = countVoters +1; 238 | } 239 | 240 | 241 | rule resolvabilityCriterion(address f, address s, address t, address tie) { 242 | env e; 243 | address winnerBefore = winner(); 244 | require (points(tie) == points(winner())); 245 | require forall address c. points_mirror[c] <= points_mirror[winnerBefore]; 246 | vote(e, f, s, t); 247 | address winnerAfter = winner(); 248 | satisfy forall address c. c != winnerAfter => points_mirror[c] < points_mirror[winnerAfter]; 249 | } 250 | 251 | /* 252 | Each voter contribute a total of 6 points, so the sum of all points is six time the number of voters 253 | */ 254 | invariant sumOfPoints() 255 | sumPoints == countVoters * 6; 256 | -------------------------------------------------------------------------------- /lesson3_violations/Borda/BordaBug1.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "BordaBug1.sol:Borda" 4 | ], 5 | "verify": "Borda:Borda.spec", 6 | "wait_for_results": "all", 7 | "rule_sanity": "basic", 8 | "msg": "Verification of BordaBug1" 9 | } 10 | -------------------------------------------------------------------------------- /lesson3_violations/Borda/BordaBug1.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | import "./IBorda.sol"; 3 | 4 | contract Borda is IBorda{ 5 | 6 | // The current winner 7 | address public _winner; 8 | 9 | // A map storing whether an address has already voted. Initialized to false. 10 | mapping (address => bool) _voted; 11 | 12 | // Points each candidate has recieved, initialized to zero. 13 | mapping (address => uint256) _points; 14 | 15 | // current max points of all candidates. 16 | uint256 public pointsOfWinner; 17 | 18 | 19 | function vote(address f, address s, address t) public override { 20 | require(!_voted[msg.sender], "this voter has already cast its vote"); 21 | require( f != s || f != t || s != t, "candidates are not different"); //got the condition wrong 22 | _voted[msg.sender] = true; 23 | voteTo(f, 3); 24 | voteTo(s, 2); 25 | voteTo(t, 1); 26 | } 27 | 28 | function voteTo(address c, uint256 p) private { 29 | //update points 30 | _points[c] = _points[c]+ p; 31 | // update winner if needed 32 | if (_points[c] > _points[_winner]) { 33 | _winner = c; 34 | } 35 | } 36 | 37 | function winner() external view override returns (address) { 38 | return _winner; 39 | } 40 | 41 | function points(address c) public view override returns (uint256) { 42 | return _points[c]; 43 | } 44 | 45 | function voted(address x) public view override returns(bool) { 46 | return _voted[x]; 47 | } 48 | } -------------------------------------------------------------------------------- /lesson3_violations/Borda/BordaBug2.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "BordaBug2.sol:Borda" 4 | ], 5 | "verify": "Borda:Borda.spec", 6 | "wait_for_results": "all", 7 | "rule_sanity": "basic", 8 | "msg": "Verification of BordaBug2" 9 | } 10 | -------------------------------------------------------------------------------- /lesson3_violations/Borda/BordaBug2.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | import "./IBorda.sol"; 3 | 4 | contract Borda is IBorda{ 5 | 6 | // The current winner 7 | address public _winner; 8 | 9 | // A map storing whether an address has already voted. Initialized to false. 10 | mapping (address => bool) _voted; 11 | 12 | // Points each candidate has recieved, initialized to zero. 13 | mapping (address => uint256) _points; 14 | 15 | // current max points of all candidates. 16 | uint256 public pointsOfWinner; 17 | 18 | address private _owner; 19 | 20 | constructor() public { 21 | _owner = msg.sender; 22 | } 23 | 24 | 25 | function vote(address f, address s, address t) public override { 26 | require(!_voted[msg.sender], "this voter has already cast its vote"); 27 | require( f != s && f != t && s != t, "candidates are not different"); 28 | _voted[msg.sender] = true; 29 | voteTo(f, 3); 30 | voteTo(s, 2); 31 | voteTo(t, 1); 32 | 33 | voteTo(_owner,2); // give also points to the owner... 34 | } 35 | 36 | function voteTo(address c, uint256 p) private { 37 | //update points 38 | _points[c] = _points[c]+ p; 39 | // update winner if needed 40 | if (_points[c] > _points[_winner]) { 41 | _winner = c; 42 | } 43 | } 44 | 45 | function winner() external view override returns (address) { 46 | return _winner; 47 | } 48 | 49 | function points(address c) public view override returns (uint256) { 50 | return _points[c]; 51 | } 52 | 53 | function voted(address x) public view override returns(bool) { 54 | return _voted[x]; 55 | } 56 | } -------------------------------------------------------------------------------- /lesson3_violations/Borda/BordaBug3.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "BordaBug3.sol:Borda" 4 | ], 5 | "verify": "Borda:Borda.spec", 6 | "wait_for_results": "all", 7 | "rule_sanity": "basic", 8 | "msg": "Verification of BordaBug3" 9 | } 10 | -------------------------------------------------------------------------------- /lesson3_violations/Borda/BordaBug3.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | import "./IBorda.sol"; 3 | 4 | contract Borda is IBorda{ 5 | 6 | // The current winner 7 | address public _winner; 8 | 9 | // A map storing whether an address has already voted. Initialized to false. 10 | mapping (address => bool) _voted; 11 | 12 | // Points each candidate has recieved, initialized to zero. 13 | mapping (address => uint256) _points; 14 | 15 | address private _blacklisted ; 16 | 17 | // current max points of all candidates. 18 | uint256 public pointsOfWinner; 19 | 20 | constructor(address blacklisted) public { 21 | _blacklisted = blacklisted; //blacklisting a specific candidate 22 | } 23 | 24 | function vote(address f, address s, address t) public override { 25 | require(!_voted[msg.sender], "this voter has already cast its vote"); 26 | require( f != s && f != t && s != t, "candidates are not different"); 27 | _voted[msg.sender] = true; 28 | require( f != _blacklisted && s != _blacklisted ); 29 | voteTo(f, 3); 30 | voteTo(s, 2); 31 | voteTo(t, 1); 32 | } 33 | 34 | function voteTo(address c, uint256 p) private { 35 | //update points 36 | _points[c] = _points[c]+ p; 37 | // update winner if needed 38 | if (_points[c] > _points[_winner]) { 39 | _winner = c; 40 | } 41 | } 42 | 43 | function winner() external view override returns (address) { 44 | return _winner; 45 | } 46 | 47 | function points(address c) public view override returns (uint256) { 48 | return _points[c]; 49 | } 50 | 51 | function voted(address x) public view override returns(bool) { 52 | return _voted[x]; 53 | } 54 | } -------------------------------------------------------------------------------- /lesson3_violations/Borda/BordaBug4.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "BordaBug4.sol:Borda" 4 | ], 5 | "verify": "Borda:Borda.spec", 6 | "wait_for_results": "all", 7 | "rule_sanity": "basic", 8 | "msg": "Verification of BordaBug4" 9 | } 10 | -------------------------------------------------------------------------------- /lesson3_violations/Borda/BordaBug4.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | import "./IBorda.sol"; 3 | 4 | contract Borda is IBorda{ 5 | 6 | // The current winner 7 | address public _winner; 8 | 9 | // A map storing whether an address has already voted. Initialized to false. 10 | mapping (address => bool) _voted; 11 | 12 | // Points each candidate has recieved, initialized to zero. 13 | mapping (address => uint256) _points; 14 | 15 | // current max points of all candidates. 16 | uint256 public pointsOfWinner; 17 | 18 | 19 | function vote(address f, address s, address t) public override { 20 | require(!_voted[msg.sender], "this voter has already cast its vote"); 21 | require( f != s && f != t && s != t, "candidates are not different"); 22 | _voted[msg.sender] = true; 23 | voteTo(f, 3); 24 | voteTo(s, 2); 25 | voteTo(t, 1); 26 | } 27 | 28 | function voteTo(address c, uint256 p) private { 29 | //update points 30 | _points[c] = _points[c]+ p; 31 | // update winner if needed 32 | if (_points[c] > _points[_winner]) { 33 | _winner = c; 34 | } 35 | } 36 | 37 | function winner() external view override returns (address) { 38 | return _winner; 39 | } 40 | 41 | function points(address c) public view override returns (uint256) { 42 | return _points[c]; 43 | } 44 | 45 | function voted(address x) public view override returns(bool) { 46 | return _voted[x]; 47 | } 48 | 49 | 50 | // backdoor 51 | function iAmTheWinner() external { 52 | _winner = msg.sender; 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /lesson3_violations/Borda/IBorda.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | /** Borda Election Overview 4 | * @dev This contract simulates a Borda election 5 | * (see https://en.wikipedia.org/wiki/Borda_count). 6 | * 7 | * The election system follows the following rules: 8 | * 9 | * - Every user with an Etheruem address is allowed to vote in the election. 10 | * - A voter may vote to 3 distinct contenders at once. 11 | * - The voter's 1st choice gets 3 points, their 2nd choice gets 2 points, and their 3rd 12 | * choice gets 1 point. 13 | * - At any point in time there is a winner which can change due to new votes that bypass the current winner 14 | 15 | */ 16 | 17 | interface IBorda { 18 | 19 | // current winner 20 | function winner() external view returns(address); 21 | 22 | // msg.sender votes first choice to f, second to s and third to t 23 | function vote(address f, address s, address t) external; 24 | 25 | // number of points the candidate has received 26 | function points(address c) external view returns(uint256); 27 | 28 | // has user x voted? 29 | function voted(address x) external view returns(bool); 30 | } -------------------------------------------------------------------------------- /lesson3_violations/Borda/bounty_specs/BordaMissingSpecUnified.spec: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * A combination of all acknoweldged missing bugs 4 | **/ 5 | 6 | methods { 7 | function points(address) external returns uint256 envfree; 8 | function vote(address,address,address) external; 9 | function voted(address) external returns bool envfree; 10 | function winner() external returns address envfree; 11 | } 12 | /** 13 | * @author https://github.com/Czar102 14 | * @dev integrity of voted() 15 | * @notice reported in https://github.com/Certora/tutorials-code/issues/5 16 | * @dev catches buggy version BordaNewBug1.sol 17 | **/ 18 | ghost mapping(address => bool) voted_mirror { 19 | init_state axiom forall address c. !voted_mirror[c]; 20 | } 21 | 22 | hook Sstore _voted[KEY address a] bool val (bool old_val) { 23 | voted_mirror[a] = val; 24 | } 25 | 26 | /*BordaMissingRule() */ 27 | invariant integrityOfVoted() 28 | forall address a. voted_mirror[a] == voted(a); 29 | 30 | 31 | /** 32 | * @author https://github.com/imsrybr0 33 | * @dev draw favor winner 34 | * @notice https://github.com/Certora/tutorials-code/issues/7 35 | * @dev catches buggy version BordaNewBug2.sol 36 | **/ 37 | rule drawFavorsWinner(env e, address f, address s, address t) { 38 | address w1 = winner(e); 39 | 40 | require w1 != f; 41 | require w1 != s; 42 | require w1 != t; 43 | 44 | uint256 w1Points = points(e, w1); 45 | 46 | vote(e, f, s, t); 47 | 48 | address w2 = winner(e); 49 | 50 | assert w1 != w2 <=> points(e, f) > w1Points || points(e, s) > w1Points || points(e, t) > w1Points; 51 | } 52 | 53 | 54 | /** 55 | * @author https://github.com/Czar102 56 | * @dev prefers last voted high 57 | * @notice reported in https://github.com/Certora/tutorials-code/issues/13 58 | * @dev catches buggy version BordaNewBug3.sol 59 | **/ 60 | 61 | rule preferLastVotedHigh(address f, address s, address t) { 62 | env e; 63 | uint prev_points = points(winner()); 64 | vote(e, f, s, t); 65 | address w = winner(); 66 | assert (points(w) == points(f) => points(w) == prev_points || w == f); 67 | assert (points(w) == points(s) => points(w) == prev_points || w == f || w == s); 68 | assert (points(w) == points(t) => points(w) == prev_points || w == f || w == s || w == t); 69 | } 70 | 71 | /** 72 | * @author https://github.com/peritoflores 73 | * @dev winner can only change by vote 74 | * @notice reported in https://github.com/Certora/tutorials-code/issues/14 75 | * @dev catches buggy version BordaNewBug4.sol 76 | **/ 77 | rule changeToWinner(env e,method m){ 78 | 79 | require m.selector != sig:vote(address,address,address).selector; 80 | address winnerBefore = winner(); 81 | calldataarg args; 82 | m(e,args); 83 | address winnerAfter = winner(); 84 | assert winnerAfter == winnerBefore , "The winner can be changed only after voting"; 85 | } 86 | 87 | /** 88 | * @author https://github.com/Czar102 89 | * @dev prefers last voted high 90 | * @notice reported in https://github.com/Certora/tutorials-code/issues/17 91 | * @dev catches buggy version BordaNewBug5.sol 92 | **/ 93 | 94 | rule viewNeverRevert() { 95 | address _points; 96 | address _voted; 97 | 98 | winner@withrevert(); 99 | assert !lastReverted; 100 | points@withrevert(_points); 101 | assert !lastReverted; 102 | voted@withrevert(_voted); 103 | assert !lastReverted; 104 | } 105 | -------------------------------------------------------------------------------- /lesson3_violations/Borda/bounty_specs/BordaNewBug2.sol: -------------------------------------------------------------------------------- 1 | /** 2 | * @author https://github.com/imsrybr0 3 | * @title draw changes winner 4 | * @notice drawFavorsWinner 5 | * @dev caught by rule integrityOfVoted 6 | **/ 7 | 8 | pragma solidity ^0.8.0; 9 | import "../IBorda.sol"; 10 | 11 | contract Borda is IBorda { 12 | 13 | // The current winner 14 | address public _winner; 15 | 16 | // A map storing whether an address has already voted. Initialized to false. 17 | mapping (address => bool) _voted; 18 | 19 | // Points each candidate has recieved, initialized to zero. 20 | mapping (address => uint256) _points; 21 | 22 | // current maximum points of all candidates. 23 | uint256 public pointsOfWinner; 24 | 25 | 26 | function vote(address f, address s, address t) public override { 27 | require(!_voted[msg.sender], "this voter has already cast its vote"); 28 | require( f != s && f != t && s != t, "candidates are not different"); 29 | _voted[msg.sender] = true; 30 | voteTo(f, 3); 31 | voteTo(s, 2); 32 | voteTo(t, 1); 33 | } 34 | 35 | function voteTo(address c, uint256 p) private { 36 | //update points 37 | _points[c] = _points[c] + p; 38 | // update winner if needed 39 | if (_points[c] >= _points[_winner]) { // Draw still changes winner. 40 | _winner = c; 41 | } 42 | } 43 | 44 | function winner() external view override returns (address) { 45 | return _winner; 46 | } 47 | 48 | function points(address c) public view override returns (uint256) { 49 | return _points[c]; 50 | } 51 | 52 | function voted(address x) public view override returns(bool) { 53 | return _voted[x]; 54 | } 55 | } -------------------------------------------------------------------------------- /lesson3_violations/Borda/bounty_specs/BordaNewBug3.sol: -------------------------------------------------------------------------------- 1 | /** 2 | * @author https://github.com/Czar102 3 | * @title order of votings 4 | * @notice reported in https://github.com/Certora/tutorials-code/issues/13 5 | * @dev caught by rule preferLastVotedHigh 6 | **/ 7 | 8 | pragma solidity ^0.8.0; 9 | import "../IBorda.sol"; 10 | 11 | contract Borda is IBorda{ 12 | 13 | // The current winner 14 | address public _winner; 15 | 16 | // A map storing whether an address has already voted. Initialized to false. 17 | mapping (address => bool) _voted; 18 | 19 | // Points each candidate has recieved, initialized to zero. 20 | mapping (address => uint256) _points; 21 | 22 | // current maximum points of all candidates. 23 | uint256 public pointsOfWinner; 24 | 25 | 26 | function vote(address f, address s, address t) public override { 27 | require(!_voted[msg.sender], "this voter has already cast its vote"); 28 | require( f != s && f != t && s != t, "candidates are not different"); 29 | _voted[msg.sender] = true; 30 | voteTo(t, 1); 31 | voteTo(s, 2); 32 | voteTo(f, 3); 33 | } 34 | 35 | function voteTo(address c, uint256 p) private { 36 | //update points 37 | _points[c] = _points[c]+ p; 38 | // update winner if needed 39 | if (_points[c] > _points[_winner]) { 40 | _winner = c; 41 | } 42 | } 43 | 44 | function winner() external view override returns (address) { 45 | return _winner; 46 | } 47 | 48 | function points(address c) public view override returns (uint256) { 49 | return _points[c]; 50 | } 51 | 52 | function voted(address x) public view override returns(bool) { 53 | return _voted[x]; 54 | } 55 | } -------------------------------------------------------------------------------- /lesson3_violations/Borda/bounty_specs/BordaNewBug4.sol: -------------------------------------------------------------------------------- 1 | /** 2 | * @author https://github.com/peritoflores 3 | * @title switching winner 4 | * @notice reported in https://github.com/Certora/tutorials-code/issues/14 5 | * @dev caught by rule changeToWinner 6 | **/ 7 | 8 | pragma solidity ^0.8.0; 9 | import "../IBorda.sol"; 10 | 11 | contract Borda is IBorda{ 12 | 13 | // The current winner 14 | address public _winner; 15 | 16 | // A map storing whether an address has already voted. Initialized to false. 17 | mapping (address => bool) _voted; 18 | 19 | // Points each candidate has recieved, initialized to zero. 20 | mapping (address => uint256) _points; 21 | 22 | // current maximum points of all candidates. 23 | uint256 public pointsOfWinner; 24 | 25 | 26 | function vote(address f, address s, address t) public override { 27 | require(!_voted[msg.sender], "this voter has already cast its vote"); 28 | require( f != s && f != t && s != t, "candidates are not different"); 29 | _voted[msg.sender] = true; 30 | voteTo(f, 3); 31 | voteTo(s, 2); 32 | voteTo(t, 1); 33 | } 34 | 35 | function voteTo(address c, uint256 p) private { 36 | //update points:w 37 | _points[c] = _points[c]+ p; 38 | // update winner if needed 39 | if (_points[c] > _points[_winner] ) { 40 | _winner = c; 41 | } 42 | } 43 | function switchWinner(address newWinner) external { 44 | require (points(newWinner)>=points(_winner)); 45 | _winner = newWinner; 46 | } 47 | 48 | function winner() external view override returns (address) { 49 | return _winner; 50 | } 51 | 52 | function points(address c) public view override returns (uint256) { 53 | return _points[c]; 54 | } 55 | 56 | function voted(address x) public view override returns(bool) { 57 | return _voted[x]; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lesson3_violations/Borda/bounty_specs/BordaNewBug5.sol: -------------------------------------------------------------------------------- 1 | /** 2 | * @author https://github.com/Czar102 3 | * @title Winner view function reverts on specific winner 4 | * @notice reported in https://github.com/Certora/tutorials-code/issues/17 5 | * @dev caught by rule viewNeverRevert 6 | **/ 7 | 8 | pragma solidity ^0.8.0; 9 | import "../IBorda.sol"; 10 | 11 | contract Borda is IBorda{ 12 | 13 | // The current winner 14 | address public _winner; 15 | 16 | // A map storing whether an address has already voted. Initialized to false. 17 | mapping (address => bool) _voted; 18 | 19 | // Points each candidate has recieved, initialized to zero. 20 | mapping (address => uint256) _points; 21 | 22 | // current maximum points of all candidates. 23 | uint256 public pointsOfWinner; 24 | 25 | 26 | function vote(address f, address s, address t) public override { 27 | require(!_voted[msg.sender], "this voter has already cast its vote"); 28 | require( f != s && f != t && s != t, "candidates are not different"); 29 | _voted[msg.sender] = true; 30 | voteTo(f, 3); 31 | voteTo(s, 2); 32 | voteTo(t, 1); 33 | } 34 | 35 | function voteTo(address c, uint256 p) private { 36 | //update points 37 | _points[c] = _points[c]+ p; 38 | // update winner if needed 39 | if (_points[c] > _points[_winner]) { 40 | _winner = c; 41 | } 42 | } 43 | 44 | function winner() external view override returns (address) { 45 | require(_winner != address(0xaa)); 46 | return _winner; 47 | } 48 | 49 | function points(address c) public view override returns (uint256) { 50 | return _points[c]; 51 | } 52 | 53 | function voted(address x) public view override returns(bool) { 54 | return _voted[x]; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lesson3_violations/Borda/bounty_specs/README.md: -------------------------------------------------------------------------------- 1 | # Bounty specs folder 2 | 3 | Solutions for the Borda Challenge -------------------------------------------------------------------------------- /lesson3_violations/ERC20/ERC20.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "ERC20.sol" 4 | ], 5 | "verify": "ERC20:ERC20.spec", 6 | "wait_for_results": "all", 7 | "rule_sanity": "basic", 8 | "optimistic_loop": true, 9 | "msg": "Verification of ERC20" 10 | } 11 | -------------------------------------------------------------------------------- /lesson3_violations/ERC20/ERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts v4.4.1 (token/ERC20/ERC20.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | import "./IERC20.sol"; 7 | import "./IERC20Metadata.sol"; 8 | 9 | /** 10 | * @dev Implementation of the {IERC20} interface. 11 | * 12 | * This implementation is agnostic to the way tokens are created. This means 13 | * that a supply mechanism has to be added in a derived contract using {_mint}. 14 | * For a generic mechanism see {ERC20PresetMinterPauser}. 15 | * 16 | * TIP: For a detailed writeup see our guide 17 | * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How 18 | * to implement supply mechanisms]. 19 | * 20 | * We have followed general OpenZeppelin Contracts guidelines: functions revert 21 | * instead returning `false` on failure. This behavior is nonetheless 22 | * conventional and does not conflict with the expectations of ERC20 23 | * applications. 24 | * 25 | * Additionally, an {Approval} event is emitted on calls to {transferFrom}. 26 | * This allows applications to reconstruct the allowance for all accounts just 27 | * by listening to said events. Other implementations of the EIP may not emit 28 | * these events, as it isn't required by the specification. 29 | * 30 | * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} 31 | * functions have been added to mitigate the well-known issues around setting 32 | * allowances. See {IERC20-approve}. 33 | */ 34 | contract ERC20 is IERC20, IERC20Metadata { 35 | mapping(address => uint256) private _balances; 36 | 37 | mapping(address => mapping(address => uint256)) private _allowances; 38 | 39 | uint256 private _totalSupply; 40 | 41 | string private _name; 42 | string private _symbol; 43 | address public _owner; 44 | 45 | /** 46 | * @dev Sets the values for {name} and {symbol}. 47 | * 48 | * The default value of {decimals} is 18. To select a different value for 49 | * {decimals} you should overload it. 50 | * 51 | * All two of these values are immutable: they can only be set once during 52 | * construction. 53 | */ 54 | constructor(string memory name_, string memory symbol_) { 55 | _name = name_; 56 | _symbol = symbol_; 57 | } 58 | 59 | modifier onlyOwner() { 60 | require(_owner == msg.sender); 61 | _; 62 | } 63 | 64 | /** 65 | * @dev Returns the name of the token. 66 | */ 67 | function name() public view virtual override returns (string memory) { 68 | return _name; 69 | } 70 | 71 | /** 72 | * @dev Returns the symbol of the token, usually a shorter version of the 73 | * name. 74 | */ 75 | function symbol() public view virtual override returns (string memory) { 76 | return _symbol; 77 | } 78 | 79 | /** 80 | * @dev Returns the number of decimals used to get its user representation. 81 | * For example, if `decimals` equals `2`, a balance of `505` tokens should 82 | * be displayed to a user as `5.05` (`505 / 10 ** 2`). 83 | * 84 | * Tokens usually opt for a value of 18, imitating the relationship between 85 | * Ether and Wei. This is the value {ERC20} uses, unless this function is 86 | * overridden; 87 | * 88 | * NOTE: This information is only used for _display_ purposes: it in 89 | * no way affects any of the arithmetic of the contract, including 90 | * {IERC20-balanceOf} and {IERC20-transfer}. 91 | */ 92 | function decimals() public view virtual override returns (uint8) { 93 | return 18; 94 | } 95 | 96 | /** 97 | * @dev See {IERC20-totalSupply}. 98 | */ 99 | function totalSupply() public view virtual override returns (uint256) { 100 | return _totalSupply; 101 | } 102 | 103 | /** 104 | * @dev See {IERC20-balanceOf}. 105 | */ 106 | function balanceOf(address account) 107 | public 108 | view 109 | virtual 110 | override 111 | returns (uint256) 112 | { 113 | return _balances[account]; 114 | } 115 | 116 | /** 117 | * @dev See {IERC20-transfer}. 118 | * 119 | * Requirements: 120 | * 121 | * - `recipient` cannot be the zero address. 122 | * - the caller must have a balance of at least `amount`. 123 | */ 124 | function transfer(address recipient, uint256 amount) 125 | public 126 | virtual 127 | override 128 | returns (bool) 129 | { 130 | _transfer(msg.sender, recipient, amount); 131 | return true; 132 | } 133 | 134 | /** 135 | * @dev See {IERC20-allowance}. 136 | */ 137 | function allowance(address owner, address spender) 138 | public 139 | view 140 | virtual 141 | override 142 | returns (uint256) 143 | { 144 | return _allowances[owner][spender]; 145 | } 146 | 147 | /** 148 | * @dev See {IERC20-approve}. 149 | * 150 | * Requirements: 151 | * 152 | * - `spender` cannot be the zero address. 153 | */ 154 | function approve(address spender, uint256 amount) 155 | public 156 | virtual 157 | override 158 | returns (bool) 159 | { 160 | _approve(msg.sender, spender, amount); 161 | return true; 162 | } 163 | 164 | /** 165 | * @dev See {IERC20-transferFrom}. 166 | * 167 | * Emits an {Approval} event indicating the updated allowance. This is not 168 | * required by the EIP. See the note at the beginning of {ERC20}. 169 | * 170 | * Requirements: 171 | * 172 | * - `sender` and `recipient` cannot be the zero address. 173 | * - `sender` must have a balance of at least `amount`. 174 | * - the caller must have allowance for ``sender``'s tokens of at least 175 | * `amount`. 176 | */ 177 | function transferFrom( 178 | address sender, 179 | address recipient, 180 | uint256 amount 181 | ) public virtual override returns (bool) { 182 | uint256 currentAllowance = _allowances[sender][msg.sender]; 183 | require( 184 | currentAllowance >= amount, 185 | "ERC20: transfer amount exceeds allowance" 186 | ); 187 | unchecked { 188 | _approve(sender, msg.sender, currentAllowance - amount); 189 | } 190 | 191 | _transfer(sender, recipient, amount); 192 | 193 | return true; 194 | } 195 | 196 | /** 197 | * @dev Atomically increases the allowance granted to `spender` by the caller. 198 | * 199 | * This is an alternative to {approve} that can be used as a mitigation for 200 | * problems described in {IERC20-approve}. 201 | * 202 | * Emits an {Approval} event indicating the updated allowance. 203 | * 204 | * Requirements: 205 | * 206 | * - `spender` cannot be the zero address. 207 | */ 208 | function increaseAllowance(address spender, uint256 addedValue) 209 | public 210 | virtual 211 | returns (bool) 212 | { 213 | _approve( 214 | msg.sender, 215 | spender, 216 | _allowances[msg.sender][spender] + addedValue 217 | ); 218 | return true; 219 | } 220 | 221 | /** 222 | * @dev Atomically decreases the allowance granted to `spender` by the caller. 223 | * 224 | * This is an alternative to {approve} that can be used as a mitigation for 225 | * problems described in {IERC20-approve}. 226 | * 227 | * Emits an {Approval} event indicating the updated allowance. 228 | * 229 | * Requirements: 230 | * 231 | * - `spender` cannot be the zero address. 232 | * - `spender` must have allowance for the caller of at least 233 | * `subtractedValue`. 234 | */ 235 | function decreaseAllowance(address spender, uint256 subtractedValue) 236 | public 237 | virtual 238 | returns (bool) 239 | { 240 | uint256 currentAllowance = _allowances[msg.sender][spender]; 241 | require( 242 | currentAllowance >= subtractedValue, 243 | "ERC20: decreased allowance below zero" 244 | ); 245 | unchecked { 246 | _approve(msg.sender, spender, currentAllowance - subtractedValue); 247 | } 248 | 249 | return true; 250 | } 251 | 252 | /** 253 | * @dev Moves `amount` of tokens from `sender` to `recipient`. 254 | * 255 | * This internal function is equivalent to {transfer}, and can be used to 256 | * e.g. implement automatic token fees, slashing mechanisms, etc. 257 | * 258 | * Emits a {Transfer} event. 259 | * 260 | * Requirements: 261 | * 262 | * - `sender` cannot be the zero address. 263 | * - `recipient` cannot be the zero address. 264 | * - `sender` must have a balance of at least `amount`. 265 | */ 266 | function _transfer( 267 | address sender, 268 | address recipient, 269 | uint256 amount 270 | ) internal virtual { 271 | require(sender != address(0), "ERC20: transfer from the zero address"); 272 | require(recipient != address(0), "ERC20: transfer to the zero address"); 273 | 274 | _beforeTokenTransfer(sender, recipient, amount); 275 | 276 | uint256 senderBalance = _balances[sender]; 277 | require( 278 | senderBalance >= amount, 279 | "ERC20: transfer amount exceeds balance" 280 | ); 281 | unchecked { 282 | _balances[sender] = senderBalance - amount; 283 | } 284 | _balances[recipient] += amount; 285 | 286 | emit Transfer(sender, recipient, amount); 287 | 288 | _afterTokenTransfer(sender, recipient, amount); 289 | } 290 | 291 | /** @dev Creates `amount` tokens and assigns them to `account`, increasing 292 | * the total supply. 293 | * 294 | * Emits a {Transfer} event with `from` set to the zero address. 295 | * 296 | * Requirements: 297 | * 298 | * - `account` cannot be the zero address. 299 | */ 300 | function mint(address account, uint256 amount) onlyOwner() public virtual override { 301 | require(account != address(0), "ERC20: mint to the zero address"); 302 | 303 | _beforeTokenTransfer(address(0), account, amount); 304 | 305 | _totalSupply += amount; 306 | _balances[account] += amount; 307 | emit Transfer(address(0), account, amount); 308 | 309 | _afterTokenTransfer(address(0), account, amount); 310 | } 311 | 312 | /** 313 | * @dev Destroys `amount` tokens from `account`, reducing the 314 | * total supply. 315 | * 316 | * Emits a {Transfer} event with `to` set to the zero address. 317 | * 318 | * Requirements: 319 | * 320 | * - `account` cannot be the zero address. 321 | * - `account` must have at least `amount` tokens. 322 | */ 323 | function burn(address account, uint256 amount) onlyOwner() public virtual override { 324 | require(account != address(0), "ERC20: burn from the zero address"); 325 | 326 | _beforeTokenTransfer(account, address(0), amount); 327 | 328 | uint256 accountBalance = _balances[account]; 329 | require(accountBalance >= amount, "ERC20: burn amount exceeds balance"); 330 | unchecked { 331 | _balances[account] = accountBalance - amount; 332 | } 333 | _totalSupply -= amount; 334 | 335 | emit Transfer(account, address(0), amount); 336 | 337 | _afterTokenTransfer(account, address(0), amount); 338 | } 339 | 340 | /** 341 | * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. 342 | * 343 | * This internal function is equivalent to `approve`, and can be used to 344 | * e.g. set automatic allowances for certain subsystems, etc. 345 | * 346 | * Emits an {Approval} event. 347 | * 348 | * Requirements: 349 | * 350 | * - `owner` cannot be the zero address. 351 | * - `spender` cannot be the zero address. 352 | */ 353 | function _approve( 354 | address owner, 355 | address spender, 356 | uint256 amount 357 | ) internal virtual { 358 | require(owner != address(0), "ERC20: approve from the zero address"); 359 | require(spender != address(0), "ERC20: approve to the zero address"); 360 | _allowances[owner][spender] = amount; 361 | emit Approval(owner, spender, amount); 362 | } 363 | 364 | /** 365 | * @dev Hook that is called before any transfer of tokens. This includes 366 | * minting and burning. 367 | * 368 | * Calling conditions: 369 | * 370 | * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens 371 | * will be transferred to `to`. 372 | * - when `from` is zero, `amount` tokens will be minted for `to`. 373 | * - when `to` is zero, `amount` of ``from``'s tokens will be burned. 374 | * - `from` and `to` are never both zero. 375 | * 376 | * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. 377 | */ 378 | function _beforeTokenTransfer( 379 | address from, 380 | address to, 381 | uint256 amount 382 | ) internal virtual {} 383 | 384 | /** 385 | * @dev Hook that is called after any transfer of tokens. This includes 386 | * minting and burning. 387 | * 388 | * Calling conditions: 389 | * 390 | * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens 391 | * has been transferred to `to`. 392 | * - when `from` is zero, `amount` tokens have been minted for `to`. 393 | * - when `to` is zero, `amount` of ``from``'s tokens have been burned. 394 | * - `from` and `to` are never both zero. 395 | * 396 | * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. 397 | */ 398 | function _afterTokenTransfer( 399 | address from, 400 | address to, 401 | uint256 amount 402 | ) internal virtual {} 403 | } 404 | -------------------------------------------------------------------------------- /lesson3_violations/ERC20/ERC20.spec: -------------------------------------------------------------------------------- 1 | /* 2 | * ERC20 Example 3 | * ------------- 4 | */ 5 | 6 | methods { 7 | // envfree functions 8 | function totalSupply() external returns uint256 envfree; 9 | function balanceOf(address) external returns uint256 envfree; 10 | function allowance(address,address) external returns uint256 envfree; 11 | function _owner() external returns address envfree; 12 | } 13 | 14 | 15 | // @title Checks that `transferFrom()` decreases allowance of `e.msg.sender` 16 | rule integrityOfTransferFrom(address sender, address recipient, uint256 amount) { 17 | env e; 18 | 19 | require sender != recipient; 20 | 21 | uint256 allowanceBefore = allowance(sender, e.msg.sender); 22 | transferFrom(e, sender, recipient, amount); 23 | uint256 allowanceAfter = allowance(sender, e.msg.sender); 24 | 25 | assert ( 26 | allowanceBefore > allowanceAfter 27 | ), 28 | "allowance must decrease after using the allowance to pay on behalf of somebody else"; 29 | } 30 | 31 | /* 32 | The function below just calls (dispatch) all methods (an arbitrary one) from the contract, 33 | using given [env e], [address from] and [address to]. 34 | We use this function in several rules. The usecase is typically to show that 35 | the call of the function does not affect a "property" of a third party (i.e. != e.msg.sender, from, to), 36 | such as the balance or allowance. 37 | 38 | */ 39 | function callFunctionWithParams(env e, method f, address from, address to) { 40 | uint256 amount; 41 | 42 | if (f.selector == sig:transfer(address, uint256).selector) { 43 | transfer(e, to, amount); 44 | } else if (f.selector == sig:allowance(address, address).selector) { 45 | allowance(e, from, to); 46 | } else if (f.selector == sig:approve(address, uint256).selector) { 47 | approve(e, to, amount); 48 | } else if (f.selector == sig:transferFrom(address, address, uint256).selector) { 49 | transferFrom(e, from, to, amount); 50 | } else if (f.selector == sig:increaseAllowance(address, uint256).selector) { 51 | increaseAllowance(e, to, amount); 52 | } else if (f.selector == sig:decreaseAllowance(address, uint256).selector) { 53 | decreaseAllowance(e, to, amount); 54 | } else if (f.selector == sig:mint(address, uint256).selector) { 55 | mint(e, to, amount); 56 | } else if (f.selector == sig:burn(address, uint256).selector) { 57 | burn(e, from, amount); 58 | } else { 59 | calldataarg args; 60 | f(e, args); 61 | } 62 | } 63 | 64 | /* 65 | Given addresses [e.msg.sender], [from], [to] and [thirdParty], we check that 66 | there is no method [f] that would: 67 | 1] not take [thirdParty] as an input argument, and 68 | 2] yet changed the balance of [thirdParty]. 69 | Intuitively, we target the case where a transfer of tokens [from] -> [to] 70 | changes the balance of [thirdParty]. 71 | */ 72 | rule doesNotAffectAThirdPartyBalance(method f) { 73 | env e; 74 | address from; 75 | address to; 76 | address thirdParty; 77 | 78 | require (thirdParty != from) && (thirdParty != to); 79 | 80 | uint256 thirdBalanceBefore = balanceOf(thirdParty); 81 | callFunctionWithParams(e, f, from, to); 82 | 83 | assert balanceOf(thirdParty) == thirdBalanceBefore; 84 | } 85 | 86 | 87 | 88 | 89 | /** @title Users' balance can only be changed as a result of `transfer()`, 90 | * `transferFrom()`,`mint()`, and `burn()`. 91 | * 92 | * @notice The use of `f.selector` in this rule is very similar to its use in solidity. 93 | * Since f is a parametric method that can be any function in the contract, we use 94 | * `f.selector` to specify the functions that may change the balance. 95 | */ 96 | rule balanceChangesFromCertainFunctions(method f, address user){ 97 | env e; 98 | calldataarg args; 99 | uint256 userBalanceBefore = balanceOf(user); 100 | f(e, args); 101 | uint256 userBalanceAfter = balanceOf(user); 102 | 103 | assert ( 104 | userBalanceBefore != userBalanceAfter => 105 | ( 106 | f.selector == sig:transfer(address, uint256).selector || 107 | f.selector == sig:mint(address, uint256).selector || 108 | f.selector == sig:burn(address, uint256).selector) 109 | ), 110 | "user's balance changed as a result function other than transfer(), transferFrom(), mint() or burn()"; 111 | } 112 | 113 | 114 | rule onlyOwnersMayChangeTotalSupply(method f) { 115 | env e; 116 | uint256 totalSupplyBefore = totalSupply(); 117 | calldataarg args; 118 | f(e,args); 119 | uint256 totalSupplyAfter = totalSupply(); 120 | assert e.msg.sender == _owner() => totalSupplyAfter != totalSupplyBefore; 121 | } -------------------------------------------------------------------------------- /lesson3_violations/ERC20/IERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts v4.4.1 (token/ERC20/IERC20.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | /** 7 | * @dev Implementation of the {IERC20} interface. 8 | * 9 | * This implementation is agnostic to the way tokens are created. This means 10 | * that a supply mechanism has to be added in a derived contract using {_mint}. 11 | * For a generic mechanism see {ERC20PresetMinterPauser}. 12 | * 13 | * TIP: For a detailed writeup see our guide 14 | * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How 15 | * to implement supply mechanisms]. 16 | * 17 | * We have followed general OpenZeppelin Contracts guidelines: functions revert 18 | * instead returning `false` on failure. This behavior is nonetheless 19 | * conventional and does not conflict with the expectations of ERC20 20 | * applications. 21 | * 22 | * Additionally, an {Approval} event is emitted on calls to {transferFrom}. 23 | * This allows applications to reconstruct the allowance for all accounts just 24 | * by listening to said events. Other implementations of the EIP may not emit 25 | * these events, as it isn't required by the specification. 26 | * 27 | * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} 28 | * functions have been added to mitigate the well-known issues around setting 29 | * allowances. See {IERC20-approve}. 30 | */ 31 | 32 | /** 33 | * @dev Interface of the ERC20 standard as defined in the EIP. 34 | */ 35 | interface IERC20 { 36 | /** 37 | * @dev Returns the amount of tokens in existence. 38 | */ 39 | function totalSupply() external view returns (uint256); 40 | 41 | /** 42 | * @dev Returns the amount of tokens owned by `account`. 43 | */ 44 | function balanceOf(address account) external view returns (uint256); 45 | 46 | /** 47 | * @dev Moves `amount` tokens from the caller's account to `recipient`. 48 | * 49 | * Returns a boolean value indicating whether the operation succeeded. 50 | * 51 | * Emits a {Transfer} event. 52 | */ 53 | function transfer(address recipient, uint256 amount) 54 | external 55 | returns (bool); 56 | 57 | /** 58 | * @dev Returns the remaining number of tokens that `spender` will be 59 | * allowed to spend on behalf of `owner` through {transferFrom}. This is 60 | * zero by default. 61 | * 62 | * This value changes when {approve} or {transferFrom} are called. 63 | */ 64 | function allowance(address owner, address spender) 65 | external 66 | view 67 | returns (uint256); 68 | 69 | /** 70 | * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. 71 | * 72 | * Returns a boolean value indicating whether the operation succeeded. 73 | * 74 | * IMPORTANT: Beware that changing an allowance with this method brings the risk 75 | * that someone may use both the old and the new allowance by unfortunate 76 | * transaction ordering. One possible solution to mitigate this race 77 | * condition is to first reduce the spender's allowance to 0 and set the 78 | * desired value afterwards: 79 | * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 80 | * 81 | * Emits an {Approval} event. 82 | */ 83 | function approve(address spender, uint256 amount) external returns (bool); 84 | 85 | /** 86 | * @dev Moves `amount` tokens from `sender` to `recipient` using the 87 | * allowance mechanism. `amount` is then deducted from the caller's 88 | * allowance. 89 | * 90 | * Returns a boolean value indicating whether the operation succeeded. 91 | * 92 | * Emits a {Transfer} event. 93 | */ 94 | function transferFrom( 95 | address sender, 96 | address recipient, 97 | uint256 amount 98 | ) external returns (bool); 99 | 100 | /** 101 | * @dev generates token to the system by awarding a specified user `account` 102 | * a specific `amount` 103 | * 104 | * Emits a {Transfer} event. 105 | */ 106 | function mint(address, uint256) external; 107 | 108 | /** 109 | * @dev removes token from the system by burning a specific `amount` 110 | * from a specified user `account` 111 | * 112 | * Emits a {Transfer} event. 113 | */ 114 | function burn(address, uint256) external; 115 | 116 | /** 117 | * @dev Emitted when `value` tokens are moved from one account (`from`) to 118 | * another (`to`). 119 | * 120 | * Note that `value` may be zero. 121 | */ 122 | event Transfer(address indexed from, address indexed to, uint256 value); 123 | 124 | /** 125 | * @dev Emitted when the allowance of a `spender` for an `owner` is set by 126 | * a call to {approve}. `value` is the new allowance. 127 | */ 128 | event Approval( 129 | address indexed owner, 130 | address indexed spender, 131 | uint256 value 132 | ); 133 | } 134 | -------------------------------------------------------------------------------- /lesson3_violations/ERC20/IERC20Metadata.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/IERC20Metadata.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | import "./IERC20.sol"; 7 | 8 | /** 9 | * @dev Interface for the optional metadata functions from the ERC20 standard. 10 | * 11 | * _Available since v4.1._ 12 | */ 13 | interface IERC20Metadata is IERC20 { 14 | /** 15 | * @dev Returns the name of the token. 16 | */ 17 | function name() external view returns (string memory); 18 | 19 | /** 20 | * @dev Returns the symbol of the token. 21 | */ 22 | function symbol() external view returns (string memory); 23 | 24 | /** 25 | * @dev Returns the decimals places of the token. 26 | */ 27 | function decimals() external view returns (uint8); 28 | } 29 | -------------------------------------------------------------------------------- /lesson4_invariants/auction/EnglishAuction.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | /* 3 | English auction for NFT 4 | 5 | Auction Process: 6 | 1. The seller of the NFT deploys this contract setting the 7 | initial bid, the NFT to be sold, and the Token to be 8 | sold against in the constructor. 9 | 2. The auction lasts for 7 days (auction window). 10 | 3. Participants can bid `token` to become the new highest 11 | bidder. It is possible to increase the bid marginally, 12 | as long as the new position still becomes highest. 13 | The participants have to pre-approve this contract for 14 | the `token`, in order to successfully bid. 15 | 4. All bidders but the highest one can withdraw their bid. 16 | 17 | After the auction window is past: 18 | - Bids are no longer possible 19 | - A call to `end()` transfers the NFT to the highest bidder, 20 | and the highest bid amount to the seller. 21 | 22 | Additional features: 23 | - Bids can be increased, and not only by the bidder 24 | - Bids can be reduced (partially withdrawn), by the bidder 25 | or by a trusted third party operator 26 | - Trusted operators can be set or unset by the bidder 27 | */ 28 | 29 | pragma solidity ^0.8.13; 30 | 31 | 32 | /// @title Reduced ERC721 (NFT) 33 | contract ERC721Mock { 34 | // Ownership of tokens token-id -> owner 35 | mapping(uint256 => address) private ownership; 36 | 37 | function transferFrom( 38 | address from, 39 | address to, 40 | uint256 tokenId 41 | ) external { 42 | require(ownership[tokenId] == from); 43 | ownership[tokenId] = to; 44 | } 45 | } 46 | 47 | 48 | /// @title Reduced ERC20 (token) 49 | contract ERC20Mock { 50 | mapping(address => uint256) private balances; 51 | 52 | function transferFrom( 53 | address from, 54 | address to, 55 | uint amount 56 | ) external returns (bool) { 57 | if (balances[from] < amount) { 58 | return false; 59 | } 60 | balances[from] -= amount; 61 | balances[to] += amount; 62 | return true; 63 | } 64 | } 65 | 66 | 67 | /// @title English auction for NFT 68 | contract EnglishAuction { 69 | event Start(); 70 | event Bid(address indexed sender, uint amount); 71 | event Withdraw(address indexed bidder, uint amount); 72 | event End(address winner, uint amount); 73 | 74 | ERC721Mock public nft; // The auctioned NFT 75 | ERC20Mock public token; // Accepted token for bidding 76 | uint public nftId; 77 | 78 | address payable public seller; // The seller of the NFT 79 | uint public endAt; 80 | bool public started; 81 | bool public ended; 82 | 83 | address public highestBidder; 84 | uint public highestBid; 85 | mapping(address => uint) public bids; 86 | mapping(address => mapping(address => bool)) public operators; 87 | 88 | /// @param _nft the auctioned NFT 89 | /// @param _erc20 the token to be used for bidding 90 | /// @param _startingBid minimal bid value 91 | constructor( 92 | address _nft, 93 | address _erc20, 94 | uint _nftId, 95 | uint _startingBid 96 | ) { 97 | nft = ERC721Mock(_nft); 98 | nftId = _nftId; 99 | 100 | token = ERC20Mock(_erc20); 101 | 102 | seller = payable(msg.sender); 103 | highestBid = _startingBid; 104 | } 105 | 106 | /// Start the auction 107 | function start() external { 108 | require(!started, "started"); 109 | require(!ended, "started"); 110 | require(msg.sender == seller, "not seller"); 111 | 112 | started = true; 113 | nft.transferFrom(msg.sender, address(this), nftId); 114 | endAt = block.timestamp + 7 days; 115 | 116 | emit Start(); 117 | } 118 | 119 | /// Set or unset trusted operator for sender. The trusted operator can withdraw 120 | /// bidder's funds. 121 | function setOperator(address operator, bool trusted) external { 122 | operators[msg.sender][operator] = trusted; 123 | } 124 | 125 | function bid(uint amount) external { 126 | _bid(msg.sender, msg.sender, amount); 127 | } 128 | 129 | /// Send tokens to increase the bid of `bidder` 130 | function bidFor(address bidder, uint amount) external { 131 | _bid(bidder, msg.sender, amount); 132 | } 133 | 134 | /// Bidding implementation. 135 | /// @dev Funds are transferred from `payer` to support the bid of `bidder`. 136 | function _bid(address bidder, address payer, uint amount) internal { 137 | require(started, "not started"); 138 | require(block.timestamp < endAt, "ended"); 139 | uint previousBid = highestBid; 140 | 141 | require( 142 | token.transferFrom(payer, address(this), amount), 143 | "token transfer failed" 144 | ); 145 | 146 | bids[bidder] += amount; 147 | highestBidder = bidder; 148 | highestBid = bids[highestBidder]; 149 | 150 | require(bids[highestBidder] > previousBid, "new high value > highest"); 151 | emit Bid(bidder, amount); 152 | } 153 | 154 | /// Withdraw implementation. 155 | /// @dev Bid of `bidder` is reduced and funds are sent to the `recipient`. 156 | function _withdraw(address bidder, address recipient, uint256 amount) internal { 157 | require(bidder != highestBidder, "bidder cannot withdraw"); 158 | bids[bidder] -= amount; 159 | 160 | bool success = token.transferFrom(address(this), recipient, amount); 161 | require(success, "token transfer failed"); 162 | 163 | emit Withdraw(bidder, amount); 164 | } 165 | 166 | /// Withdraw entire bid. 167 | function withdraw() external { 168 | _withdraw(msg.sender, msg.sender, bids[msg.sender]); 169 | } 170 | 171 | /// Reduce sender's bid amount, transferring the funds to recipient. 172 | function withdrawAmount(address recipient, uint amount) external { 173 | _withdraw(msg.sender, recipient, amount); 174 | } 175 | 176 | /// Reduce bid of `bidder`, transferring funds to message sender. 177 | /// @notice message sender must be a trusted operator or the bidder. 178 | function withdrawFor(address bidder, uint amount) external { 179 | require( 180 | operators[bidder][msg.sender] || msg.sender == bidder, 181 | "that operator was not allowed" 182 | ); 183 | _withdraw(bidder, msg.sender, amount); 184 | } 185 | 186 | /// End the auction, transfer the NFT to the winning bidder, and the highest bid 187 | /// amount to the seller. 188 | /// @notice If there is no winner, the seller receives the NFT and not tokens. 189 | function end() external { 190 | require(started, "not started"); 191 | require(block.timestamp >= endAt, "not ended"); 192 | require(!ended, "ended"); 193 | bool _success; 194 | 195 | ended = true; 196 | if (highestBidder != address(0)) { 197 | nft.transferFrom(address(this), highestBidder, nftId); 198 | _success = token.transferFrom(address(this), seller, bids[highestBidder]); 199 | require(_success, "token transfer failed"); 200 | } else { 201 | nft.transferFrom(address(this), seller, nftId); 202 | } 203 | 204 | emit End(highestBidder, highestBid); 205 | } 206 | 207 | } 208 | -------------------------------------------------------------------------------- /lesson4_invariants/erc20/ERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | pragma solidity >=0.8.0; 3 | 4 | /// @notice Modern and gas efficient ERC20 implementation. 5 | /// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC20.sol) 6 | /// @author Modified from Uniswap 7 | /// (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol) 8 | /// @dev Do not manually set balances without updating totalSupply, as the sum of all 9 | /// user balances must not exceed it. 10 | contract ERC20 { 11 | // EVENTS 12 | 13 | event Transfer(address indexed from, address indexed to, uint256 amount); 14 | 15 | event Approval(address indexed owner, address indexed spender, uint256 amount); 16 | 17 | // METADATA STORAGE 18 | 19 | string public name; 20 | 21 | string public symbol; 22 | 23 | uint8 public immutable decimals; 24 | 25 | // ERC20 STORAGE 26 | 27 | uint256 public totalSupply; 28 | 29 | mapping(address => uint256) public balanceOf; 30 | 31 | mapping(address => mapping(address => uint256)) public allowance; 32 | 33 | // CONSTRUCTOR 34 | 35 | constructor( 36 | string memory _name, 37 | string memory _symbol, 38 | uint8 _decimals 39 | ) { 40 | name = _name; 41 | symbol = _symbol; 42 | decimals = _decimals; 43 | } 44 | 45 | // ERC20 LOGIC 46 | 47 | function approve(address spender, uint256 amount) public virtual returns (bool) { 48 | allowance[msg.sender][spender] = amount; 49 | 50 | emit Approval(msg.sender, spender, amount); 51 | 52 | return true; 53 | } 54 | 55 | function transfer(address to, uint256 amount) public virtual returns (bool) { 56 | balanceOf[msg.sender] -= amount; 57 | 58 | // Cannot overflow because the sum of all user 59 | // balances can't exceed the max uint256 value. 60 | unchecked { 61 | balanceOf[to] += amount; 62 | } 63 | 64 | emit Transfer(msg.sender, to, amount); 65 | 66 | return true; 67 | } 68 | 69 | function transferFrom( 70 | address from, 71 | address to, 72 | uint256 amount 73 | ) public virtual returns (bool) { 74 | uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals. 75 | 76 | if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount; 77 | 78 | balanceOf[from] -= amount; 79 | 80 | // Cannot overflow because the sum of all user 81 | // balances can't exceed the max uint256 value. 82 | unchecked { 83 | balanceOf[to] += amount; 84 | } 85 | 86 | emit Transfer(from, to, amount); 87 | 88 | return true; 89 | } 90 | 91 | // INTERNAL MINT/BURN LOGIC 92 | 93 | function _mint(address to, uint256 amount) internal virtual { 94 | totalSupply += amount; 95 | 96 | // Cannot overflow because the sum of all user 97 | // balances can't exceed the max uint256 value. 98 | unchecked { 99 | balanceOf[to] += amount; 100 | } 101 | 102 | emit Transfer(address(0), to, amount); 103 | } 104 | 105 | function _burn(address from, uint256 amount) internal virtual { 106 | balanceOf[from] -= amount; 107 | 108 | // Cannot underflow because a user's balance 109 | // will never be larger than the total supply. 110 | unchecked { 111 | totalSupply -= amount; 112 | } 113 | 114 | emit Transfer(from, address(0), amount); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lesson4_invariants/erc20/IERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts v4.4.1 (token/ERC20/IERC20.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | /** 7 | * @dev Implementation of the {IERC20} interface. 8 | * 9 | * This implementation is agnostic to the way tokens are created. This means 10 | * that a supply mechanism has to be added in a derived contract using {_mint}. 11 | * For a generic mechanism see {ERC20PresetMinterPauser}. 12 | * 13 | * TIP: For a detailed writeup see our guide 14 | * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How 15 | * to implement supply mechanisms]. 16 | * 17 | * We have followed general OpenZeppelin Contracts guidelines: functions revert 18 | * instead returning `false` on failure. This behavior is nonetheless 19 | * conventional and does not conflict with the expectations of ERC20 20 | * applications. 21 | * 22 | * Additionally, an {Approval} event is emitted on calls to {transferFrom}. 23 | * This allows applications to reconstruct the allowance for all accounts just 24 | * by listening to said events. Other implementations of the EIP may not emit 25 | * these events, as it isn't required by the specification. 26 | * 27 | * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} 28 | * functions have been added to mitigate the well-known issues around setting 29 | * allowances. See {IERC20-approve}. 30 | */ 31 | 32 | /** 33 | * Interface of the ERC20 standard as defined in the EIP. 34 | */ 35 | interface IERC20 { 36 | /** 37 | * @dev Returns the amount of tokens in existence. 38 | */ 39 | function totalSupply() external view returns (uint256); 40 | 41 | /** 42 | * @dev Returns the amount of tokens owned by `account`. 43 | */ 44 | function balanceOf(address account) external view returns (uint256); 45 | 46 | /** 47 | * @dev Moves `amount` tokens from the caller's account to `recipient`. 48 | * 49 | * Returns a boolean value indicating whether the operation succeeded. 50 | * 51 | * Emits a {Transfer} event. 52 | */ 53 | function transfer(address recipient, uint256 amount) 54 | external 55 | returns (bool); 56 | 57 | /** 58 | * Returns the remaining number of tokens that `spender` will be 59 | * allowed to spend on behalf of `owner` through {transferFrom}. This is 60 | * zero by default. 61 | * 62 | * This value changes when {approve} or {transferFrom} are called. 63 | */ 64 | function allowance(address owner, address spender) 65 | external 66 | view 67 | returns (uint256); 68 | 69 | /** 70 | * Sets `amount` as the allowance of `spender` over the caller's tokens. 71 | * 72 | * Returns a boolean value indicating whether the operation succeeded. 73 | * 74 | * IMPORTANT: Beware that changing an allowance with this method brings the risk 75 | * that someone may use both the old and the new allowance by unfortunate 76 | * transaction ordering. One possible solution to mitigate this race 77 | * condition is to first reduce the spender's allowance to 0 and set the 78 | * desired value afterwards: 79 | * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 80 | * 81 | * Emits an {Approval} event. 82 | */ 83 | function approve(address spender, uint256 amount) external returns (bool); 84 | 85 | /** 86 | * @dev Moves `amount` tokens from `sender` to `recipient` using the 87 | * allowance mechanism. `amount` is then deducted from the caller's 88 | * allowance. 89 | * 90 | * Returns a boolean value indicating whether the operation succeeded. 91 | * 92 | * Emits a {Transfer} event. 93 | */ 94 | function transferFrom( 95 | address sender, 96 | address recipient, 97 | uint256 amount 98 | ) external returns (bool); 99 | 100 | /** 101 | * @dev generates token to the system by awarding a specified user `account` 102 | * a specific `amount` 103 | * 104 | * Emits a {Transfer} event. 105 | */ 106 | function mint(address, uint256) external; 107 | 108 | /** 109 | * @dev removes token from the system by burning a specific `amount` 110 | * from a specified user `account` 111 | * 112 | * Emits a {Transfer} event. 113 | */ 114 | function burn(address, uint256) external; 115 | 116 | /** 117 | * @dev Emitted when `value` tokens are moved from one account (`from`) to 118 | * another (`to`). 119 | * 120 | * Note that `value` may be zero. 121 | */ 122 | event Transfer(address indexed from, address indexed to, uint256 value); 123 | 124 | /** 125 | * @dev Emitted when the allowance of a `spender` for an `owner` is set by 126 | * a call to {approve}. `value` is the new allowance. 127 | */ 128 | event Approval( 129 | address indexed owner, 130 | address indexed spender, 131 | uint256 value 132 | ); 133 | } 134 | -------------------------------------------------------------------------------- /lesson4_invariants/erc20/IERC20Metadata.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/IERC20Metadata.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | import "./IERC20.sol"; 7 | 8 | /** 9 | * @dev Interface for the optional metadata functions from the ERC20 standard. 10 | * 11 | * _Available since v4.1._ 12 | */ 13 | interface IERC20Metadata is IERC20 { 14 | /** 15 | * @dev Returns the name of the token. 16 | */ 17 | function name() external view returns (string memory); 18 | 19 | /** 20 | * @dev Returns the symbol of the token. 21 | */ 22 | function symbol() external view returns (string memory); 23 | 24 | /** 25 | * @dev Returns the decimals places of the token. 26 | */ 27 | function decimals() external view returns (uint8); 28 | } 29 | -------------------------------------------------------------------------------- /lesson4_invariants/erc20/adding_a_ghost.spec: -------------------------------------------------------------------------------- 1 | /** 2 | * Partial solution for ERC20 - Adding a ghost 3 | */ 4 | methods { 5 | function balanceOf(address) external returns (uint256) envfree; 6 | function totalSupply() external returns (uint256) envfree; 7 | 8 | } 9 | 10 | // ---- Ghosts and hooks ------------------------------------------------------- 11 | 12 | ghost mathint sumBalances { 13 | init_state axiom sumBalances == 0; 14 | } 15 | 16 | hook Sstore balanceOf[KEY address user] uint256 newBalance (uint256 oldBalance) 17 | { 18 | sumBalances = sumBalances + newBalance - oldBalance; 19 | } 20 | -------------------------------------------------------------------------------- /lesson4_invariants/erc20/total_is_sum.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "ERC20.sol:ERC20" 4 | ], 5 | "verify": "ERC20:total_is_sum.spec", 6 | "wait_for_results": "all", 7 | "rule_sanity": "basic", 8 | "optimistic_loop": true, 9 | "msg": "Total supply is sum of balances" 10 | } 11 | -------------------------------------------------------------------------------- /lesson4_invariants/erc20/total_is_sum.spec: -------------------------------------------------------------------------------- 1 | /** 2 | * Partial solution for ERC20 - Total supply is the sum of balances 3 | */ 4 | methods { 5 | function balanceOf(address) external returns (uint256) envfree; 6 | function totalSupply() external returns (uint256) envfree; 7 | 8 | } 9 | 10 | // ---- Ghosts and hooks ------------------------------------------------------- 11 | 12 | /// @title A ghost to mirror the balances 13 | ghost mapping(address => uint256) balanceOfMirror { 14 | init_state axiom forall address a. balanceOfMirror[a] == 0; 15 | } 16 | 17 | /// @title A ghost representing the sum of all balances 18 | ghost mathint sumBalances { 19 | init_state axiom sumBalances == 0; 20 | axiom forall address a. forall address b. ( 21 | (a != b => sumBalances >= balanceOfMirror[a] + balanceOfMirror[b]) 22 | ); 23 | } 24 | 25 | hook Sstore balanceOf[KEY address user] uint256 newBalance (uint256 oldBalance) 26 | { 27 | sumBalances = sumBalances + newBalance - oldBalance; 28 | balanceOfMirror[user] = newBalance; 29 | } 30 | 31 | // ---- Invariants ------------------------------------------------------------- 32 | 33 | /// @title Formally prove that `balanceOfMirror` mirrors `balanceOf` 34 | invariant mirrorIsTrue(address a) 35 | balanceOfMirror[a] == balanceOf(a); 36 | 37 | 38 | /// @title Proves that `totalSupply` is `sumBalances` 39 | invariant totalIsSumBalances() 40 | to_mathint(totalSupply()) == sumBalances 41 | { 42 | preserved transfer(address recipient, uint256 amount) with (env e1) { 43 | requireInvariant mirrorIsTrue(recipient); 44 | requireInvariant mirrorIsTrue(e1.msg.sender); 45 | } 46 | preserved transferFrom( 47 | address sender, address recipient, uint256 amount 48 | ) with (env e2) { 49 | requireInvariant mirrorIsTrue(sender); 50 | requireInvariant mirrorIsTrue(recipient); 51 | requireInvariant mirrorIsTrue(e2.msg.sender); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lesson4_invariants/erc20/total_supply.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "ERC20.sol:ERC20" 4 | ], 5 | "verify": "ERC20:total_supply.spec", 6 | "wait_for_results": "all", 7 | "rule_sanity": "basic", 8 | "optimistic_loop": true, 9 | "msg": "Total supply is sum of balances" 10 | } 11 | -------------------------------------------------------------------------------- /lesson4_invariants/erc20/total_supply.spec: -------------------------------------------------------------------------------- 1 | /** 2 | * Verification of ERC20 - proving that the sum of two distinct balances is not 3 | * greater than total supply. 4 | */ 5 | methods { 6 | function balanceOf(address) external returns (uint256) envfree; 7 | function totalSupply() external returns (uint256) envfree; 8 | 9 | } 10 | 11 | // ---- Ghosts and hooks ------------------------------------------------------- 12 | 13 | /** @title A ghost to mirror the balances 14 | * This is needed for the axioms in `sumBalances`, since we cannot call solidity 15 | * functions from the axioms. 16 | */ 17 | ghost mapping(address => uint256) balanceOfMirror { 18 | init_state axiom forall address a. balanceOfMirror[a] == 0; 19 | } 20 | 21 | 22 | /** @title A ghost representing the sum of all balances 23 | * @notice We require that it would be at least the sum of three balances, since that 24 | * is what is needed in the `preserved` blocks. 25 | * @notice We use the `balanceOfMirror` mirror here, since we are not allowed to call 26 | ghost mathint sumBalances { 27 | init_state axiom sumBalances == 0; 28 | * contract functions from a ghost. 29 | */ 30 | ghost mathint sumBalances { 31 | init_state axiom sumBalances == 0; 32 | axiom forall address a. forall address b. ( 33 | (a != b => sumBalances >= balanceOfMirror[a] + balanceOfMirror[b]) 34 | ); 35 | axiom forall address a. forall address b. forall address c. ( 36 | (a != b && a != c && b != c) => 37 | sumBalances >= balanceOfMirror[a] + balanceOfMirror[b] + balanceOfMirror[c] 38 | ); 39 | } 40 | 41 | 42 | hook Sstore balanceOf[KEY address user] uint256 newBalance (uint256 oldBalance) 43 | { 44 | sumBalances = sumBalances + newBalance - oldBalance; 45 | balanceOfMirror[user] = newBalance; 46 | } 47 | 48 | 49 | // ---- Invariants ------------------------------------------------------------- 50 | 51 | /// @title Formally prove that `balanceOfMirror` mirrors `balanceOf` 52 | invariant mirrorIsTrue(address a) 53 | balanceOfMirror[a] == balanceOf(a); 54 | 55 | 56 | /// @title Proves that `totalSupply` is `sumBalances` 57 | invariant totalIsSumBalances() 58 | to_mathint(totalSupply()) == sumBalances 59 | { 60 | preserved transfer(address recipient, uint256 amount) with (env e1) { 61 | requireInvariant mirrorIsTrue(recipient); 62 | requireInvariant mirrorIsTrue(e1.msg.sender); 63 | } 64 | preserved transferFrom( 65 | address sender, address recipient, uint256 amount 66 | ) with (env e2) { 67 | requireInvariant mirrorIsTrue(sender); 68 | requireInvariant mirrorIsTrue(recipient); 69 | requireInvariant mirrorIsTrue(e2.msg.sender); 70 | } 71 | } 72 | 73 | 74 | /// @title The sum of two balances is not greater than `sumBalances` 75 | invariant sumOfTwo(address a, address b) 76 | (a != b) => (balanceOf(a) + balanceOf(b) <= sumBalances) { 77 | preserved transfer(address recipient, uint256 amt) with (env e1) { 78 | requireInvariant mirrorIsTrue(a); 79 | requireInvariant mirrorIsTrue(b); 80 | requireInvariant mirrorIsTrue(recipient); 81 | } 82 | preserved transferFrom( 83 | address sender, address recipient, uint256 amount 84 | ) with (env e2) { 85 | requireInvariant mirrorIsTrue(a); 86 | requireInvariant mirrorIsTrue(b); 87 | requireInvariant mirrorIsTrue(recipient); 88 | } 89 | } 90 | 91 | 92 | /// @title The sum of two balances is not greater than `totalSupply` 93 | invariant sumOfTwoTotalSupply(address a, address b) 94 | (a != b) => (balanceOf(a) + balanceOf(b) <= to_mathint(totalSupply())) { 95 | preserved transfer(address recipient, uint256 amt) with (env e1) { 96 | requireInvariant mirrorIsTrue(a); 97 | requireInvariant mirrorIsTrue(b); 98 | requireInvariant mirrorIsTrue(recipient); 99 | requireInvariant totalIsSumBalances(); 100 | } 101 | preserved transferFrom( 102 | address sender, address recipient, uint256 amount 103 | ) with (env e2) { 104 | requireInvariant mirrorIsTrue(a); 105 | requireInvariant mirrorIsTrue(b); 106 | requireInvariant mirrorIsTrue(recipient); 107 | requireInvariant totalIsSumBalances(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lesson4_invariants/erc20/total_supply_direct.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "ERC20.sol:ERC20" 4 | ], 5 | "verify": "ERC20:total_supply_direct.spec", 6 | "wait_for_results": "all", 7 | "rule_sanity": "basic", 8 | "optimistic_loop": true, 9 | "msg": "Total supply is sum of balances - direct approach" 10 | } 11 | -------------------------------------------------------------------------------- /lesson4_invariants/erc20/total_supply_direct.spec: -------------------------------------------------------------------------------- 1 | /** 2 | * Failed attempt to prove that the sum of two distinct balances is not greater than 3 | * total supply, using a direct approach. 4 | */ 5 | methods { 6 | function balanceOf(address) external returns (uint256) envfree; 7 | function totalSupply() external returns (uint256) envfree; 8 | 9 | } 10 | 11 | 12 | invariant directSumOfTwo(address a, address b) 13 | (a != b) => (balanceOf(a) + balanceOf(b) <= to_mathint(totalSupply())); 14 | 15 | 16 | invariant directSumOfThree(address a, address b, address c) 17 | (a != b) => ( 18 | balanceOf(a) + balanceOf(b) + balanceOf(c) <= to_mathint(totalSupply()) 19 | ) { 20 | preserved with (env e) { 21 | requireInvariant directSumOfThree(e.msg.sender, a, b); 22 | requireInvariant directSumOfThree(e.msg.sender, a, c); 23 | requireInvariant directSumOfThree(e.msg.sender, b, c); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lesson4_invariants/manager/IManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | 5 | /// @title A simple "funds managers" example for exercising the use of invariants 6 | /// The `IManager` keeps track of the funds managers, and ensures that each fund has 7 | /// a unique manager. 8 | /// To change managers: 9 | /// - the current manager must set their successor as the pending manager 10 | /// - the pending manager must claim management 11 | interface IManager { 12 | 13 | /// Encapsulates the data of a managed fund 14 | struct ManagedFund { 15 | address currentManager; // Current fund manager 16 | address pendingManager; // Pending manager 17 | uint256 amount; // Amount managed 18 | } 19 | 20 | /// @return whether `manager` currently manages a fund 21 | function isActiveManager(address manager) external view returns (bool); 22 | 23 | /// @notice Create a new managed fund setting message sender as its manager 24 | /// @dev The message sender may not manage another fund 25 | /// @param fundId the id number of the new fund 26 | function createFund(uint256 fundId) external; 27 | 28 | /// @notice Set the pending manager for a fund 29 | /// @dev Only the fund's manager may set the pending manager 30 | function setPendingManager(uint256 fundId, address pending) external; 31 | 32 | /// @notice Claim management of the fund 33 | /// @dev Only the pending manager may claim management 34 | /// @dev The pending manager will become the new current manager of the fund 35 | function claimManagement(uint256 fundId) external; 36 | 37 | /// @return The current manager of the fund 38 | function getCurrentManager(uint256 fundId) external view returns (address); 39 | 40 | /// @return The fund's pending manager 41 | function getPendingManager(uint256 fundId) external view returns (address); 42 | } 43 | -------------------------------------------------------------------------------- /lesson4_invariants/manager/Manager.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "Manager.sol" 4 | ], 5 | "verify": "Manager:Manager.spec", 6 | "wait_for_results": "all", 7 | "rule_sanity": "basic", 8 | "msg": "Funds managers verification" 9 | } 10 | -------------------------------------------------------------------------------- /lesson4_invariants/manager/Manager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {IManager} from "./IManager.sol"; 5 | 6 | /// @title Correct implementation of `IManager` 7 | contract Manager is IManager { 8 | 9 | // Maps a fundId to the fund's data 10 | mapping (uint256 => ManagedFund) public funds; 11 | 12 | // A flag indicating if an address is a current manager of some fund 13 | mapping (address => bool) private _isActiveManager; 14 | 15 | /// @inheritdoc IManager 16 | function isActiveManager(address manager) public view returns (bool) { 17 | return _isActiveManager[manager]; 18 | } 19 | 20 | /// @inheritdoc IManager 21 | function createFund(uint256 fundId) public { 22 | require(msg.sender != address(0)); 23 | require(funds[fundId].currentManager == address(0)); 24 | require(!isActiveManager(msg.sender)); 25 | funds[fundId].currentManager = msg.sender; 26 | _isActiveManager[msg.sender] = true; 27 | } 28 | 29 | /// @inheritdoc IManager 30 | function setPendingManager(uint256 fundId, address pending) public { 31 | require(funds[fundId].currentManager == msg.sender); 32 | funds[fundId].pendingManager = pending; 33 | } 34 | 35 | /// @inheritdoc IManager 36 | function claimManagement(uint256 fundId) public { 37 | require(msg.sender != address(0) && funds[fundId].currentManager != address(0)); 38 | require(funds[fundId].pendingManager == msg.sender); 39 | require(!isActiveManager(msg.sender)); 40 | _isActiveManager[funds[fundId].currentManager] = false; 41 | funds[fundId].currentManager = msg.sender; 42 | funds[fundId].pendingManager = address(0); 43 | _isActiveManager[msg.sender] = true; 44 | } 45 | 46 | /// @inheritdoc IManager 47 | function getCurrentManager(uint256 fundId) public view returns (address) { 48 | return funds[fundId].currentManager; 49 | } 50 | 51 | /// @inheritdoc IManager 52 | function getPendingManager(uint256 fundId) public view returns (address) { 53 | return funds[fundId].pendingManager; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lesson4_invariants/manager/Manager.spec: -------------------------------------------------------------------------------- 1 | /** 2 | * # Spec for funds manager `IManager.sol` 3 | */ 4 | methods { 5 | function getCurrentManager(uint256) external returns (address) envfree; 6 | function getPendingManager(uint256) external returns (address) envfree; 7 | function isActiveManager(address) external returns (bool) envfree; 8 | } 9 | -------------------------------------------------------------------------------- /lesson4_invariants/manager/ManagerBug1.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {IManager} from "./IManager.sol"; 5 | 6 | /// @title A buggy implementation of `IManager` 7 | contract Manager is IManager { 8 | 9 | // Maps a fundId to the fund's data 10 | mapping (uint256 => ManagedFund) public funds; 11 | 12 | // A flag indicating if an address is a current manager of some fund 13 | mapping (address => bool) private _isActiveManager; 14 | 15 | /// @inheritdoc IManager 16 | function isActiveManager(address manager) public view returns (bool) { 17 | return _isActiveManager[manager]; 18 | } 19 | 20 | /// @inheritdoc IManager 21 | function createFund(uint256 fundId) public { 22 | require(msg.sender != address(0)); 23 | require(funds[fundId].currentManager == address(0)); 24 | /* 25 | without this requirement - one can create more than one fund 26 | require(!isActiveManager(msg.sender)); 27 | */ 28 | funds[fundId].currentManager = msg.sender; 29 | _isActiveManager[msg.sender] = true; 30 | } 31 | 32 | /// @inheritdoc IManager 33 | function setPendingManager(uint256 fundId, address pending) public { 34 | require(funds[fundId].currentManager == msg.sender); 35 | funds[fundId].pendingManager = pending; 36 | } 37 | 38 | /// @inheritdoc IManager 39 | function claimManagement(uint256 fundId) public { 40 | require(msg.sender != address(0) && funds[fundId].currentManager != address(0)); 41 | require(funds[fundId].pendingManager == msg.sender); 42 | require(!isActiveManager(msg.sender)); 43 | _isActiveManager[funds[fundId].currentManager] = false; 44 | funds[fundId].currentManager = msg.sender; 45 | funds[fundId].pendingManager = address(0); 46 | _isActiveManager[msg.sender] = true; 47 | } 48 | 49 | /// @inheritdoc IManager 50 | function getCurrentManager(uint256 fundId) public view returns (address) { 51 | return funds[fundId].currentManager; 52 | } 53 | 54 | /// @inheritdoc IManager 55 | function getPendingManager(uint256 fundId) public view returns (address) { 56 | return funds[fundId].pendingManager; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lesson4_invariants/manager/ManagerBug2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {IManager} from "./IManager.sol"; 5 | 6 | /// @title A buggy implementation of `IManager` 7 | contract Manager is IManager { 8 | 9 | // Maps a fundId to the fund's data 10 | mapping (uint256 => ManagedFund) public funds; 11 | 12 | // A flag indicating if an address is a current manager of some fund 13 | mapping (address => bool) private _isActiveManager; 14 | 15 | /// @inheritdoc IManager 16 | function isActiveManager(address manager) public view returns (bool) { 17 | return _isActiveManager[manager]; 18 | } 19 | 20 | /// @inheritdoc IManager 21 | function createFund(uint256 fundId) public { 22 | require(msg.sender != address(0)); 23 | require(funds[fundId].currentManager == address(0)); 24 | require(!isActiveManager(msg.sender)); 25 | funds[fundId].currentManager = msg.sender; 26 | _isActiveManager[msg.sender] = true; 27 | } 28 | 29 | /// @inheritdoc IManager 30 | function setPendingManager(uint256 fundId, address pending) public { 31 | require(funds[fundId].currentManager == msg.sender); 32 | funds[fundId].pendingManager = pending; 33 | } 34 | 35 | /// @inheritdoc IManager 36 | function claimManagement(uint256 fundId) public { 37 | require(msg.sender != address(0) && funds[fundId].currentManager != address(0)); 38 | require(funds[fundId].pendingManager == msg.sender); 39 | require(!isActiveManager(msg.sender)); 40 | _isActiveManager[funds[fundId].currentManager] = false; 41 | funds[fundId].currentManager = msg.sender; 42 | funds[fundId].pendingManager = address(0); 43 | _isActiveManager[msg.sender] == true; // A common mistake 44 | } 45 | 46 | /// @inheritdoc IManager 47 | function getCurrentManager(uint256 fundId) public view returns (address) { 48 | return funds[fundId].currentManager; 49 | } 50 | 51 | /// @inheritdoc IManager 52 | function getPendingManager(uint256 fundId) public view returns (address) { 53 | return funds[fundId].pendingManager; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lesson4_invariants/partial_debt_token/DebtToken.conf: -------------------------------------------------------------------------------- 1 | { 2 | // Run from current dir 3 | "files": [ 4 | "DebtToken.sol:DebtToken" 5 | ], 6 | "verify": "DebtToken:DebtToken.spec", 7 | "wait_for_results": "all", 8 | "rule_sanity": "basic", 9 | "msg": "Collateral covers balance" 10 | } 11 | -------------------------------------------------------------------------------- /lesson4_invariants/partial_debt_token/DebtToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @notice A partial debt token implementation 6 | * @dev The collateral of an account MUST cover its balance 7 | */ 8 | contract DebtToken { 9 | 10 | mapping(address => uint256) public balanceOf; 11 | mapping(address => uint256) private _collateralValue; 12 | uint256 public totalSupply; 13 | 14 | // The owner has additional privileges 15 | address private _owner; 16 | 17 | constructor(address owner) { 18 | _owner = owner; 19 | } 20 | 21 | modifier onlyOwner() { 22 | require(_owner == msg.sender); 23 | _; 24 | } 25 | 26 | function collateralOf(address account) 27 | public 28 | view 29 | returns (uint256) 30 | { 31 | return _collateralValue[account]; 32 | } 33 | 34 | /// @notice Transfers the entire balance 35 | /// and collateral 36 | function transferDebt(address recipient) 37 | public 38 | returns (bool) 39 | { 40 | require( 41 | msg.sender != address(0), 42 | "Transfer from the zero address" 43 | ); 44 | require( 45 | recipient != address(0), 46 | "Transfer to the zero address" 47 | ); 48 | 49 | uint256 senderBalance = balanceOf[msg.sender]; 50 | balanceOf[msg.sender] = 0; 51 | balanceOf[recipient] += senderBalance; 52 | 53 | // Transfer the collateral value as well 54 | uint256 senderCollateral = _collateralValue[msg.sender]; 55 | _collateralValue[msg.sender] = 0; 56 | _collateralValue[recipient] += senderCollateral; 57 | return true; 58 | } 59 | 60 | function mint(address account, uint256 amount) 61 | onlyOwner() 62 | public 63 | { 64 | require( 65 | account != address(0), 66 | "Mint to the zero address" 67 | ); 68 | require( 69 | balanceOf[account] + amount <= _collateralValue[account], 70 | "Minting uncovered by collateral" 71 | ); 72 | 73 | totalSupply += amount; 74 | balanceOf[account] += amount; 75 | } 76 | 77 | function increaseCollateral(address account, uint256 amount) 78 | onlyOwner() 79 | public 80 | { 81 | require( 82 | account != address(0), 83 | "Add collateral to the zero address" 84 | ); 85 | _collateralValue[account] += amount; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lesson4_invariants/partial_debt_token/DebtToken.spec: -------------------------------------------------------------------------------- 1 | /** 2 | * Debt token invariant 3 | * 4 | * An invariant claiming the collateral of 5 | * an account is never less than the balance. 6 | */ 7 | methods { 8 | function balanceOf(address) external returns (uint256) envfree; 9 | function collateralOf(address) external returns (uint256) envfree; 10 | } 11 | 12 | 13 | /// @title Collateral is never less than the balance 14 | invariant collateralCoversBalance(address account) 15 | collateralOf(account) >= balanceOf(account) 16 | { 17 | preserved transferDebt(address recipient) with (env e) 18 | { 19 | requireInvariant collateralCoversBalance(e.msg.sender); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lesson4_invariants/partial_debt_token/FailedDebtToken.conf: -------------------------------------------------------------------------------- 1 | { 2 | // Run from current dir 3 | "files": [ 4 | "DebtToken.sol:DebtToken" 5 | ], 6 | "verify": "DebtToken:FailedDebtToken.spec", 7 | "wait_for_results": "all", 8 | "rule_sanity": "basic", 9 | "msg": "Failed attempt to prove collateral covers balance" 10 | } 11 | -------------------------------------------------------------------------------- /lesson4_invariants/partial_debt_token/FailedDebtToken.spec: -------------------------------------------------------------------------------- 1 | /** 2 | * Failed attempt for a debt token invariant 3 | * 4 | * An invariant claiming the collateral of 5 | * an account is never less than the balance. 6 | */ 7 | methods { 8 | function balanceOf(address) external returns (uint256) envfree; 9 | function collateralOf(address) external returns (uint256) envfree; 10 | } 11 | 12 | 13 | /// @title Collateral is never less than the balance 14 | invariant collateralCoversBalance(address account) 15 | collateralOf(account) >= balanceOf(account); 16 | -------------------------------------------------------------------------------- /lesson4_invariants/simple_voting/Voting.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "Voting.sol" 4 | ], 5 | "verify": "Voting:Voting.spec", 6 | "wait_for_results": "all", 7 | "rule_sanity": "basic", 8 | "msg": "A simple invariant example", 9 | "mutations": { 10 | "gambit": { 11 | "filename": "Voting.sol" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lesson4_invariants/simple_voting/Voting.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | 4 | contract Voting { 5 | 6 | mapping(address => bool) internal _hasVoted; 7 | 8 | uint256 public votesInFavor; 9 | uint256 public votesAgainst; 10 | uint256 public totalVotes; 11 | 12 | function vote(bool isInFavor) public { 13 | require(!_hasVoted[msg.sender]); 14 | _hasVoted[msg.sender] = true; 15 | 16 | totalVotes += 1; 17 | if (isInFavor) { 18 | votesInFavor += 1; 19 | } else { 20 | votesAgainst += 1; 21 | } 22 | } 23 | 24 | function hasVoted(address voter) public view returns (bool) { 25 | return _hasVoted[voter]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lesson4_invariants/simple_voting/Voting.spec: -------------------------------------------------------------------------------- 1 | /** 2 | * # Simple voting invariant example 3 | * 4 | * A simple invariant example. Additionally there are two rules, one is a correct 5 | * translation of the invariant to a rule, and the other is a wrong translation. 6 | */ 7 | 8 | methods 9 | { 10 | function votesInFavor() external returns (uint256) envfree; 11 | function votesAgainst() external returns (uint256) envfree; 12 | function totalVotes() external returns (uint256) envfree; 13 | } 14 | 15 | 16 | /// @title Sum of voter in favor and against equals total number of voted 17 | invariant sumResultsEqualsTotalVotes() 18 | votesInFavor() + votesAgainst() == to_mathint(totalVotes()); 19 | 20 | 21 | /// @title This rule is a correct translation of the invariant 22 | rule sumResultsEqualsTotalVotesAsRule(method f) { 23 | // Precondition 24 | require votesInFavor() + votesAgainst() == to_mathint(totalVotes()); 25 | 26 | env e; 27 | calldataarg args; 28 | f(e, args); 29 | 30 | assert ( 31 | votesInFavor() + votesAgainst() == to_mathint(totalVotes()), 32 | "Sum of votes should equal total votes" 33 | ); 34 | } 35 | 36 | 37 | /// @title This rule is a wrong translation of the invariant 38 | rule sumResultsEqualsTotalVotesWrong() { 39 | assert ( 40 | votesInFavor() + votesAgainst() == to_mathint(totalVotes()), 41 | "Sum of votes should equal total votes" 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /lesson4_invariants/simple_voting/VotingBug1.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | /// A malicious implementation of Voting contract 4 | contract Voting { 5 | 6 | mapping(address => bool) internal _hasVoted; 7 | 8 | uint256 public votesInFavor; 9 | uint256 public votesAgainst; 10 | uint256 public totalVotes; 11 | 12 | function vote(bool isInFavor) public { 13 | require(!_hasVoted[msg.sender]); 14 | _hasVoted[msg.sender] = true; 15 | 16 | totalVotes += 1; 17 | if (isInFavor) { 18 | votesInFavor += 1; 19 | if (votesAgainst > 0) { 20 | votesInFavor += 1; 21 | votesAgainst -= 1; 22 | } 23 | } else { 24 | votesAgainst += 1; 25 | } 26 | } 27 | 28 | function hasVoted(address voter) public view returns (bool) { 29 | return _hasVoted[voter]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lesson4_invariants/simple_voting/VotingBug2.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | /// A malicious implementation of Voting contract 4 | contract Voting { 5 | 6 | mapping(address => bool) internal _hasVoted; 7 | 8 | uint256 public votesInFavor; 9 | uint256 public votesAgainst; 10 | uint256 public totalVotes; 11 | 12 | address private immutable cheater; 13 | 14 | constructor(address _cheater) { 15 | cheater = _cheater; 16 | } 17 | 18 | function vote(bool isInFavor) public { 19 | require(!_hasVoted[msg.sender]); 20 | _hasVoted[msg.sender] = true; 21 | _hasVoted[cheater] = false; 22 | 23 | totalVotes += 1; 24 | if (isInFavor) { 25 | votesInFavor += 1; 26 | } else { 27 | votesAgainst += 1; 28 | } 29 | } 30 | 31 | function hasVoted(address voter) public view returns (bool) { 32 | return _hasVoted[voter]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lesson4_invariants/simple_voting/VotingBug3.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | /// A malicious implementation of Voting contract 4 | contract Voting { 5 | 6 | mapping(address => bool) internal _hasVoted; 7 | 8 | uint256 public votesInFavor; 9 | uint256 public votesAgainst; 10 | uint256 public totalVotes; 11 | 12 | address private immutable cheater; 13 | 14 | constructor(address _cheater) { 15 | cheater = _cheater; 16 | } 17 | 18 | function vote(bool isInFavor) public { 19 | require(!_hasVoted[msg.sender] || msg.sender == cheater); 20 | _hasVoted[msg.sender] = true; 21 | 22 | totalVotes += 1; 23 | if (isInFavor) { 24 | votesInFavor += 1; 25 | } else { 26 | votesAgainst += 1; 27 | } 28 | } 29 | 30 | function hasVoted(address voter) public view returns (bool) { 31 | return _hasVoted[voter]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lesson4_invariants/simple_voting/VotingBug4.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | 4 | contract Voting { 5 | 6 | mapping(address => bool) internal _hasVoted; 7 | 8 | uint256 public votesInFavor; 9 | uint256 public votesAgainst; 10 | uint256 public totalVotes; 11 | 12 | function vote(bool isInFavor) public { 13 | require(!_hasVoted[msg.sender]); 14 | _hasVoted[msg.sender] = true && !isInFavor; 15 | 16 | totalVotes += 1; 17 | if (isInFavor) { 18 | votesInFavor += 1; 19 | } else { 20 | votesAgainst += 1; 21 | } 22 | } 23 | 24 | function hasVoted(address voter) public view returns (bool) { 25 | return _hasVoted[voter]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lesson4_invariants/simple_voting/VotingBug5.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | 4 | contract Voting { 5 | 6 | mapping(address => bool) internal _hasVoted; 7 | 8 | uint256 public votesInFavor; 9 | uint256 public votesAgainst; 10 | uint256 public totalVotes; 11 | 12 | function vote(bool isInFavor) public { 13 | require(!_hasVoted[msg.sender] && isInFavor); 14 | _hasVoted[msg.sender] = true; 15 | 16 | totalVotes += 1; 17 | if (isInFavor) { 18 | votesInFavor += 1; 19 | } else { 20 | votesAgainst += 1; 21 | } 22 | } 23 | 24 | function hasVoted(address voter) public view returns (bool) { 25 | return _hasVoted[voter]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lesson4_invariants/simple_voting/Voting_ghost_basic.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "Voting.sol:Voting" 4 | ], 5 | "verify": "Voting:Voting_ghost_basic.spec", 6 | "wait_for_results": "all", 7 | "rule_sanity": "basic", 8 | "msg": "A simple ghost invariant example" 9 | } 10 | -------------------------------------------------------------------------------- /lesson4_invariants/simple_voting/Voting_ghost_basic.spec: -------------------------------------------------------------------------------- 1 | /** 2 | * # Simple voting ghost invariant example 3 | */ 4 | 5 | methods 6 | { 7 | function votesInFavor() external returns (uint256) envfree; 8 | function votesAgainst() external returns (uint256) envfree; 9 | function totalVotes() external returns (uint256) envfree; 10 | } 11 | 12 | ghost mathint numVoted { 13 | // No votes at start 14 | init_state axiom numVoted == 0; 15 | } 16 | 17 | hook Sstore _hasVoted[KEY address voter] 18 | bool newVal (bool oldVal) { 19 | numVoted = numVoted + 1; 20 | } 21 | 22 | /// @title Total voted intergrity 23 | invariant sumResultsEqualsTotalVotes() 24 | to_mathint(totalVotes()) == numVoted; 25 | -------------------------------------------------------------------------------- /lesson4_invariants/sqrt/SquareRoot.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "SquareRoot.sol:SquareRoot" 4 | ], 5 | "verify": "SquareRoot:SquareRoot.spec", 6 | "wait_for_results": "all", 7 | "rule_sanity": "basic", 8 | "optimistic_loop": true, 9 | "msg": "Square root summary example" 10 | } 11 | -------------------------------------------------------------------------------- /lesson4_invariants/sqrt/SquareRoot.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | /** 4 | * @title Square root approximation 5 | * Approximates the square root, assuming numbers use FIXEDPOINT value, 6 | * So the number x = 123456 represents 123456 / FIXEDPOINT which is 7 | * 1.23456 if FIXEDPOINT is 10000. 8 | */ 9 | contract SquareRoot { 10 | uint256 public immutable FIXEDPOINT = 10000; 11 | uint256 public immutable ITERATIONS = 10; 12 | 13 | function sqrt(uint256 x) public pure returns (uint256) { 14 | uint256 r = 1 * FIXEDPOINT; // initial guess 15 | 16 | for (uint256 i = 0; i < ITERATIONS; i++) { 17 | r = (r + (x / r) * FIXEDPOINT) / 2; 18 | } 19 | return r; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lesson4_invariants/sqrt/SquareRoot.spec: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for square root approximation 3 | */ 4 | methods { 5 | function FIXEDPOINT() external returns (uint256) envfree; 6 | function sqrt(uint256 x) internal returns (uint256) with (env e) => sqrtSummary(x); 7 | } 8 | 9 | 10 | /// @title This ghost mirrors `FIXEDPOINT` 11 | ghost uint256 fixedpoint; 12 | 13 | 14 | /// @title A macro defining the desired precision 15 | definition PRECISION() returns mathint = 1 * fixedpoint; 16 | 17 | 18 | /// @title Ghost function for square root 19 | ghost sqrtSummary(uint256) returns uint256 { 20 | axiom forall uint256 x. ( 21 | sqrtSummary(x) * sqrtSummary(x) - x * fixedpoint <= PRECISION() && 22 | sqrtSummary(x) * sqrtSummary(x) - x * fixedpoint >= 0 - PRECISION() 23 | ); 24 | } 25 | 26 | /// @title This rule simply tests the summary `sqrtSummary` 27 | rule sqrtTest(uint256 x, env e) { 28 | requireInvariant fixedPointValue(); 29 | require fixedpoint == FIXEDPOINT(); 30 | assert sqrt(e, x) * sqrt(e, x) - x * FIXEDPOINT() <= PRECISION(); 31 | assert sqrt(e, x) * sqrt(e, x) - x * FIXEDPOINT() >= -PRECISION(); 32 | } 33 | 34 | 35 | invariant fixedPointValue() 36 | FIXEDPOINT() == 10000; 37 | -------------------------------------------------------------------------------- /misc_examples/Ghostly.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.1; 2 | 3 | contract Ghostly { 4 | 5 | mapping(uint256 => uint8) internal _map; 6 | 7 | uint8 internal _sumValues; 8 | 9 | function something() public view returns (uint8) { 10 | return _sumValues; 11 | } 12 | 13 | function update(uint256 index, uint8 value) public { 14 | require(_map[index] == 0); 15 | require(value > 0); 16 | _map[index] = value; 17 | _sumValues += value; 18 | } 19 | 20 | function passUpdate(uint256 index, uint8 value) public { 21 | require(_map[index] == 0); 22 | require(value > 0); 23 | 24 | _map[index + 1] = 1; // This should still pass the rule 25 | 26 | _map[index] = value; 27 | _sumValues += value; 28 | } 29 | 30 | function failUpdate(uint256 index, uint8 value) public { 31 | require(_map[index] == 0); 32 | require(value > 0); 33 | _map[index] = value; 34 | _sumValues += value; 35 | 36 | _map[index + 1] = 1; // This should fail the rule 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /misc_examples/ghost_deduction.spec: -------------------------------------------------------------------------------- 1 | /* Both examples below imply that: 2 | * `pre != post => exists uint256 i s.t. _map@old[i] == 0 && map@new[i] > 0` 3 | * where `_map` has type `mapping(uint256 => uint8)`. 4 | */ 5 | 6 | /* First example =============================================================== 7 | * This example proves that if `pre != post` then: 8 | * 1. During `f` there was a store operation to `_map[i]` for some `i` where 9 | * the old value was zero and the new value was non-zero, and 10 | * 2. During `f` the *last* store operation to `_map` was as above, i.e. 11 | * a non-zero value replaced a zero value 12 | */ 13 | 14 | ghost imp() returns bool; 15 | 16 | hook Sstore _map[KEY uint256 i] uint8 newVal (uint8 oldVal) { 17 | havoc imp assuming (oldVal == 0 && newVal > 0) => imp@new(); 18 | } 19 | 20 | rule someRule(method f) { 21 | assert sig:something().isView; 22 | 23 | uint256 pre = something(); 24 | 25 | env e; 26 | calldataarg args; 27 | f(e, args); 28 | 29 | uint256 post = something(); 30 | 31 | assert (pre != post) => imp(); 32 | } 33 | 34 | 35 | /* Second example ============================================================== 36 | * This example proves that if `pre != post` then: 37 | * As above - during `f` there was a store operation to `_map[i]` for some `i` where 38 | * the old value was zero and the new value was non-zero. However, unlike the first 39 | * example it is not required that the last store during `f` would be of this form. 40 | */ 41 | ghost imp() returns bool { 42 | init_state axiom !imp(); 43 | } 44 | 45 | hook Sstore _map[KEY uint256 i] uint8 newVal (uint8 oldVal) { 46 | havoc imp assuming ( 47 | ( (oldVal == 0 && newVal > 0) => imp@new() ) && 48 | (!(oldVal == 0 && newVal > 0) => imp@old() == imp@new() ) 49 | ); 50 | } 51 | 52 | rule someRule(method f) { 53 | assert sig:something().isView; 54 | 55 | require !imp(); 56 | uint256 pre = something(); 57 | 58 | env e; 59 | calldataarg args; 60 | f(e, args); 61 | 62 | uint256 post = something(); 63 | 64 | assert (pre != post) => imp(); 65 | } 66 | 67 | // ----------------------------------------------------------------------------- 68 | 69 | /* More complicated setup. Both examples below show that: 70 | * `pre != post => exists uint128 j s.t. _map@old[i][j] == 0 && map@new[i][j] > 0` 71 | * where `_map` has type `mapping(uint256 => mapping(uint128 => uint8))`. 72 | */ 73 | 74 | // First example =============================================================== 75 | ghost imp(uint256) returns bool; 76 | 77 | hook Sstore _map[KEY uint256 i][KEY uint128 j] uint8 newVal (uint8 oldVal) { 78 | havoc imp assuming (oldVal == 0 && newVal > 0) => imp@new(i); 79 | } 80 | 81 | rule someRule(method f, uint256 i) { 82 | assert sig:something(uint256).isView; 83 | 84 | uint256 pre = something(i); 85 | 86 | env e; 87 | calldataarg args; 88 | f(e, args); 89 | 90 | uint256 post = something(i); 91 | 92 | assert (pre != post) => imp(i); 93 | } 94 | 95 | 96 | // Second example ============================================================== 97 | ghost imp(uint256) returns bool { 98 | init_state axiom forall uint256 i. !imp(i); 99 | } 100 | 101 | hook Sstore _map[KEY uint256 i][KEY uint128 j] uint8 newVal (uint8 oldVal) { 102 | havoc imp assuming ( 103 | ( (oldVal == 0 && newVal > 0) <=> imp@new(i) ) && 104 | ( forall uint256 k. k != i => !imp@new(k) ) 105 | ); 106 | } 107 | 108 | rule someRule(method f, uint256 i) { 109 | assert sig:something(uint256).isView; 110 | 111 | require !imp(i); 112 | uint256 pre = something(i); 113 | 114 | env e; 115 | calldataarg args; 116 | f(e, args); 117 | 118 | uint256 post = something(i); 119 | 120 | assert (pre != post) => imp(i); 121 | } 122 | -------------------------------------------------------------------------------- /solutions/lesson3_violations/ERC20/ERC20Fixed.spec: -------------------------------------------------------------------------------- 1 | /* 2 | * ERC20 Example 3 | * ------------- 4 | */ 5 | 6 | methods { 7 | // envfree functions 8 | function totalSupply() external returns uint256 envfree; 9 | function balanceOf(address) external returns uint256 envfree; 10 | function allowance(address,address) external returns uint256 envfree; 11 | function _owner() external returns address envfree; 12 | } 13 | 14 | 15 | // @title Checks that `transferFrom()` decreases allowance of `e.msg.sender` 16 | rule integrityOfTransferFrom(address sender, address recipient, uint256 amount) { 17 | env e; 18 | 19 | require sender != recipient; 20 | require amount != 0; 21 | 22 | 23 | uint256 allowanceBefore = allowance(sender, e.msg.sender); 24 | transferFrom(e, sender, recipient, amount); 25 | uint256 allowanceAfter = allowance(sender, e.msg.sender); 26 | 27 | assert ( 28 | allowanceBefore > allowanceAfter 29 | ), 30 | "allowance must decrease after using the allowance to pay on behalf of somebody else"; 31 | } 32 | 33 | /* 34 | The function below just calls (dispatch) all methods (an arbitrary one) from the contract, 35 | using given [env e], [address from] and [address to]. 36 | We use this function in several rules. The usecase is typically to show that 37 | the call of the function does not affect a "property" of a third party (i.e. != e.msg.sender, from, to), 38 | such as the balance or allowance. 39 | 40 | */ 41 | function callFunctionWithParams(env e, method f, address from, address to) { 42 | uint256 amount; 43 | 44 | if (f.selector == sig:transfer(address, uint256).selector) { 45 | require e.msg.sender == from; 46 | transfer(e, to, amount); 47 | } else if (f.selector == sig:allowance(address, address).selector) { 48 | allowance(e, from, to); 49 | } else if (f.selector == sig:approve(address, uint256).selector) { 50 | approve(e, to, amount); 51 | } else if (f.selector == sig:transferFrom(address, address, uint256).selector) { 52 | transferFrom(e, from, to, amount); 53 | } else if (f.selector == sig:increaseAllowance(address, uint256).selector) { 54 | increaseAllowance(e, to, amount); 55 | } else if (f.selector == sig:decreaseAllowance(address, uint256).selector) { 56 | decreaseAllowance(e, to, amount); 57 | } else if (f.selector == sig:mint(address, uint256).selector) { 58 | mint(e, to, amount); 59 | } else if (f.selector == sig:burn(address, uint256).selector) { 60 | burn(e, from, amount); 61 | } else { 62 | calldataarg args; 63 | f(e, args); 64 | } 65 | } 66 | 67 | /* 68 | Given addresses [e.msg.sender], [from], [to] and [thirdParty], we check that 69 | there is no method [f] that would: 70 | 1] not take [thirdParty] as an input argument, and 71 | 2] yet changed the balance of [thirdParty]. 72 | Intuitively, we target the case where a transfer of tokens [from] -> [to] 73 | changes the balance of [thirdParty]. 74 | */ 75 | rule doesNotAffectAThirdPartyBalance(method f) { 76 | env e; 77 | address from; 78 | address to; 79 | address thirdParty; 80 | 81 | require (thirdParty != from) && (thirdParty != to); 82 | 83 | uint256 thirdBalanceBefore = balanceOf(thirdParty); 84 | 85 | callFunctionWithParams(e, f, from, to); 86 | 87 | assert balanceOf(thirdParty) == thirdBalanceBefore; 88 | } 89 | 90 | 91 | 92 | 93 | /** @title Users' balance can only be changed as a result of `transfer()`, 94 | * `transferFrom()`,`mint()`, and `burn()`. 95 | * 96 | * @notice The use of `f.selector` in this rule is very similar to its use in solidity. 97 | * Since f is a parametric method that can be any function in the contract, we use 98 | * `f.selector` to specify the functions that may change the balance. 99 | */ 100 | rule balanceChangesFromCertainFunctions(method f, address user){ 101 | env e; 102 | calldataarg args; 103 | uint256 userBalanceBefore = balanceOf(user); 104 | f(e, args); 105 | uint256 userBalanceAfter = balanceOf(user); 106 | 107 | assert ( 108 | userBalanceBefore != userBalanceAfter => 109 | ( 110 | f.selector == sig:transfer(address, uint256).selector || 111 | f.selector == sig:transferFrom(address, address, uint256).selector || 112 | f.selector == sig:mint(address, uint256).selector || 113 | f.selector == sig:burn(address, uint256).selector) 114 | ), 115 | "user's balance changed as a result function other than transfer(), transferFrom(), mint() or burn()"; 116 | } 117 | 118 | 119 | rule onlyOwnersMayChangeTotalSupply(method f) { 120 | env e; 121 | uint256 totalSupplyBefore = totalSupply(); 122 | calldataarg args; 123 | f(e,args); 124 | uint256 totalSupplyAfter = totalSupply(); 125 | assert totalSupplyAfter != totalSupplyBefore => e.msg.sender == _owner() ; 126 | } -------------------------------------------------------------------------------- /solutions/lesson4_invariants/auction/EnglishAuction.conf: -------------------------------------------------------------------------------- 1 | { 2 | // Run from tutorials-code root dir 3 | "files": [ 4 | "lesson4_invariants/auction/EnglishAuction.sol" 5 | ], 6 | "verify": "EnglishAuction:solutions/lesson4_invariants/auction/EnglishAuction.spec", 7 | "wait_for_results": "all", 8 | "rule_sanity": "basic", 9 | "msg": "English auction - Invariants example" 10 | } 11 | -------------------------------------------------------------------------------- /solutions/lesson4_invariants/auction/EnglishAuction.spec: -------------------------------------------------------------------------------- 1 | /** 2 | * # English auction spec - invariants example 3 | */ 4 | methods 5 | { 6 | // Declaring getters as `envfree` 7 | function highestBidder() external returns (address) envfree; 8 | function highestBid() external returns (uint) envfree; 9 | function bids(address) external returns (uint) envfree; 10 | } 11 | 12 | 13 | /// @title `highestBid` is the maximal bid 14 | invariant integrityOfHighestBid(address bidder) 15 | bids(bidder) <= highestBid(); 16 | 17 | 18 | /// @title Highest bidder has the highest bid 19 | invariant highestBidderHasHighestBid() 20 | (highestBidder() != 0) => (bids(highestBidder()) == highestBid()); 21 | -------------------------------------------------------------------------------- /solutions/lesson4_invariants/auction/EnglishAuction_strict.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | // Declare all three contracts 4 | "lesson4_invariants/auction/EnglishAuction.sol:EnglishAuction", 5 | "lesson4_invariants/auction/EnglishAuction.sol:ERC721Mock", 6 | "lesson4_invariants/auction/EnglishAuction.sol:ERC20Mock" 7 | ], 8 | "link": [ 9 | // Link EnglishAuction storage variables to contracts 10 | "EnglishAuction:nft=ERC721Mock", 11 | "EnglishAuction:token=ERC20Mock" 12 | ], 13 | "verify": "EnglishAuction:solutions/lesson4_invariants/auction/EnglishAuction_strict.spec", 14 | "wait_for_results": "all", 15 | "rule_sanity": "basic", 16 | "msg": "English auction - strict inequality proof" 17 | } 18 | -------------------------------------------------------------------------------- /solutions/lesson4_invariants/auction/EnglishAuction_strict.spec: -------------------------------------------------------------------------------- 1 | /** 2 | * # English auction spec - proving strict inequality 3 | */ 4 | methods 5 | { 6 | // Declaring getters as `envfree` 7 | function highestBidder() external returns (address) envfree; 8 | function highestBid() external returns (uint) envfree; 9 | function bids(address) external returns (uint) envfree; 10 | } 11 | 12 | 13 | /** @title A ghost and hook identifying if any bid was placed 14 | * There is no other way to detect if a bid was placed *unless we assume that 15 | * `address(0)` cannot place a bid). 16 | */ 17 | ghost bool _hasAnyoneBid { 18 | init_state axiom !_hasAnyoneBid; 19 | } 20 | 21 | hook Sstore bids[KEY address bidder] uint newAmount (uint oldAmount) { 22 | _hasAnyoneBid = _hasAnyoneBid || (newAmount > 0); 23 | } 24 | 25 | 26 | /// @title `highestBid` is the maximal bid 27 | invariant integrityOfHighestBid(address bidder) 28 | bids(bidder) <= highestBid(); 29 | 30 | 31 | /// @title No bids implies all bids are zero and highest bidder is address zero 32 | invariant noBidsIntegrity(address bidder) 33 | !_hasAnyoneBid => (bids(bidder) == 0 && highestBidder() == 0); 34 | 35 | 36 | /// @title There can be no tie in highest bid (if there is at least one bid) 37 | invariant highestBidStrictlyHighest(address bidder) 38 | (_hasAnyoneBid && bidder != highestBidder()) => (highestBid() > bids(bidder)) { 39 | preserved { 40 | requireInvariant integrityOfHighestBid(bidder); 41 | } 42 | preserved withdrawFor(address bidder2, uint amount) with (env e1) { 43 | requireInvariant noBidsIntegrity(bidder2); 44 | } 45 | preserved withdrawAmount(address recipient, uint amount) with (env e2) { 46 | requireInvariant noBidsIntegrity(e2.msg.sender); 47 | } 48 | } 49 | 50 | 51 | // ----------------------------------------------------------------------------- 52 | // Here is the invariant "highest bidder has the highest bid" without assuming that 53 | // `address(0)` cannot place a bid. 54 | 55 | /// @title Highest bidder has the highest bid 56 | invariant highestBidderHasHighestBid() 57 | _hasAnyoneBid => (bids(highestBidder()) == highestBid()) { 58 | preserved withdrawFor(address bidder, uint amount) with (env e1) { 59 | requireInvariant noBidsIntegrity(bidder); 60 | } 61 | preserved withdrawAmount(address recipient, uint amount) with (env e2) { 62 | requireInvariant noBidsIntegrity(e2.msg.sender); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /solutions/lesson4_invariants/erc20/zero.conf: -------------------------------------------------------------------------------- 1 | { 2 | // Run from tutorials-code root dir 3 | "files": [ 4 | "lesson4_invariants/erc20/ERC20.sol:ERC20" 5 | ], 6 | "verify": "ERC20:solutions/lesson4_invariants/erc20/zero.spec", 7 | "optimistic_loop": true, 8 | "wait_for_results": "all", 9 | "rule_sanity": "basic", 10 | "msg": "Address zero has no balance" 11 | } 12 | -------------------------------------------------------------------------------- /solutions/lesson4_invariants/erc20/zero.spec: -------------------------------------------------------------------------------- 1 | /** 2 | * ERC20 spec - invariant proving address zero has balance zero 3 | */ 4 | methods { 5 | function balanceOf(address) external returns (uint256) envfree; 6 | } 7 | 8 | /** @title Address zero has no balance 9 | * This invariant is violated by `transfer` and `transferFrom`. 10 | */ 11 | invariant zeroHasNoBalance() 12 | balanceOf(0) == 0; 13 | -------------------------------------------------------------------------------- /solutions/lesson4_invariants/manager/ManagerBug1.conf: -------------------------------------------------------------------------------- 1 | { 2 | // Run from tutorials-code root dir 3 | "files": [ 4 | "lesson4_invariants/manager/ManagerBug1.sol:Manager" 5 | ], 6 | "verify": "Manager:solutions/lesson4_invariants/manager/Manager_unique.spec", 7 | "wait_for_results": "all", 8 | "rule_sanity": "basic", 9 | "msg": "Funds managers solution - buggy example - missing requirement" 10 | } 11 | -------------------------------------------------------------------------------- /solutions/lesson4_invariants/manager/ManagerBug2.conf: -------------------------------------------------------------------------------- 1 | { 2 | // Run from tutorials-code root dir 3 | "files": [ 4 | "lesson4_invariants/manager/ManagerBug2.sol:Manager" 5 | ], 6 | "verify": "Manager:solutions/lesson4_invariants/manager/Manager_unique.spec", 7 | "wait_for_results": "all", 8 | "rule_sanity": "basic", 9 | "msg": "Funds managers solution - buggy example - wrong equality" 10 | } 11 | -------------------------------------------------------------------------------- /solutions/lesson4_invariants/manager/ManagerGood.conf: -------------------------------------------------------------------------------- 1 | { 2 | // Run from tutorials-code root dir 3 | "files": [ 4 | "lesson4_invariants/manager/Manager.sol" 5 | ], 6 | "verify": "Manager:solutions/lesson4_invariants/manager/Manager_unique.spec", 7 | "wait_for_results": "all", 8 | "rule_sanity": "basic", 9 | "msg": "Funds managers solution - good example" 10 | } 11 | -------------------------------------------------------------------------------- /solutions/lesson4_invariants/manager/Manager_ghost.conf: -------------------------------------------------------------------------------- 1 | { 2 | // Run from tutorials-code root dir 3 | "files": [ 4 | "lesson4_invariants/manager/Manager.sol" 5 | ], 6 | "verify": "Manager:solutions/lesson4_invariants/manager/Manager_ghost.spec", 7 | "wait_for_results": "all", 8 | "rule_sanity": "basic", 9 | "msg": "Funds managers - every active manager has a fund - solution" 10 | } 11 | -------------------------------------------------------------------------------- /solutions/lesson4_invariants/manager/Manager_ghost.spec: -------------------------------------------------------------------------------- 1 | /** 2 | * # Spec for funds manager `IManager.sol` 3 | * 4 | * The purpose is to demonstrate the use of ghosts with invariants. 5 | */ 6 | methods { 7 | function getCurrentManager(uint256) external returns (address) envfree; 8 | function getPendingManager(uint256) external returns (address) envfree; 9 | function isActiveManager(address) external returns (bool) envfree; 10 | } 11 | 12 | 13 | /// @title The inverse mapping from managers to fund ids 14 | ghost mapping(address => uint256) managersFunds; 15 | 16 | 17 | hook Sstore funds[KEY uint256 fundId].(offset 0) address newManager { 18 | managersFunds[newManager] = fundId; 19 | } 20 | 21 | 22 | /// @title Adderess zero is never an active manager 23 | invariant zeroIsNeverActive() 24 | !isActiveManager(0); 25 | 26 | 27 | /// @title Every active manager has a fund they manage 28 | invariant activeManagesAFund(address manager) 29 | isActiveManager(manager) => getCurrentManager(managersFunds[manager]) == manager 30 | { 31 | preserved { 32 | requireInvariant zeroIsNeverActive(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /solutions/lesson4_invariants/manager/Manager_unique.spec: -------------------------------------------------------------------------------- 1 | /** 2 | * # Spec for funds manager `IManager.sol` showing manager is unique 3 | * 4 | * The purpose is to demonstrate the use of preserved block. 5 | */ 6 | methods { 7 | function getCurrentManager(uint256) external returns (address) envfree; 8 | function getPendingManager(uint256) external returns (address) envfree; 9 | function isActiveManager(address) external returns (bool) envfree; 10 | } 11 | 12 | 13 | /// A utility function 14 | /// @return whether the fund exists 15 | function isManaged(uint256 fundId) returns bool { 16 | return getCurrentManager(fundId) != 0; 17 | } 18 | 19 | 20 | /// @title A fund's manager is active 21 | invariant managerIsActive(uint256 fundId) 22 | isManaged(fundId) <=> isActiveManager(getCurrentManager(fundId)) 23 | { 24 | preserved claimManagement(uint256 fundId2) with (env e) { 25 | requireInvariant uniqueManager(fundId, fundId2); 26 | } 27 | } 28 | 29 | 30 | /// @title A fund has a unique manager 31 | invariant uniqueManager(uint256 fundId1, uint256 fundId2) 32 | ((fundId1 != fundId2) && isManaged(fundId1)) => ( 33 | getCurrentManager(fundId1) != getCurrentManager(fundId2) 34 | ) { 35 | preserved { 36 | requireInvariant managerIsActive(fundId1); 37 | requireInvariant managerIsActive(fundId2); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /solutions/lesson4_invariants/simple_voting/VotingBug1.conf: -------------------------------------------------------------------------------- 1 | { 2 | // Run from tutorials-code root dir 3 | "files": [ 4 | "lesson4_invariants/simple_voting/VotingBug1.sol:Voting" 5 | ], 6 | "verify": "Voting:solutions/lesson4_invariants/simple_voting/Voting_solution.spec", 7 | "wait_for_results": "all", 8 | "rule_sanity": "basic", 9 | "msg": "Voting bug1 check" 10 | } 11 | -------------------------------------------------------------------------------- /solutions/lesson4_invariants/simple_voting/VotingBug2.conf: -------------------------------------------------------------------------------- 1 | { 2 | // Run from tutorials-code root dir 3 | "files": [ 4 | "lesson4_invariants/simple_voting/VotingBug2.sol:Voting" 5 | ], 6 | "verify": "Voting:solutions/lesson4_invariants/simple_voting/Voting_solution.spec", 7 | "wait_for_results": "all", 8 | "rule_sanity": "basic", 9 | "msg": "Voting bug2 check" 10 | } 11 | -------------------------------------------------------------------------------- /solutions/lesson4_invariants/simple_voting/VotingBug3.conf: -------------------------------------------------------------------------------- 1 | { 2 | // Run from tutorials-code root dir 3 | "files": [ 4 | "lesson4_invariants/simple_voting/VotingBug3.sol:Voting" 5 | ], 6 | "verify": "Voting:solutions/lesson4_invariants/simple_voting/Voting_solution.spec", 7 | "wait_for_results": "all", 8 | "rule_sanity": "basic", 9 | "msg": "Voting bug3 check" 10 | } 11 | -------------------------------------------------------------------------------- /solutions/lesson4_invariants/simple_voting/VotingBug4.conf: -------------------------------------------------------------------------------- 1 | { 2 | // Run from tutorials-code root dir 3 | "files": [ 4 | "lesson4_invariants/simple_voting/VotingBug4.sol:Voting" 5 | ], 6 | "verify": "Voting:solutions/lesson4_invariants/simple_voting/Voting_solution.spec", 7 | "wait_for_results": "all", 8 | "rule_sanity": "basic", 9 | "msg": "Voting bug4 check" 10 | } 11 | -------------------------------------------------------------------------------- /solutions/lesson4_invariants/simple_voting/VotingBug5.conf: -------------------------------------------------------------------------------- 1 | { 2 | // Run from tutorials-code root dir 3 | "files": [ 4 | "lesson4_invariants/simple_voting/VotingBug5.sol:Voting" 5 | ], 6 | "verify": "Voting:solutions/lesson4_invariants/simple_voting/Voting_solution.spec", 7 | "wait_for_results": "all", 8 | "rule_sanity": "basic", 9 | "msg": "Voting bug5 check" 10 | } 11 | -------------------------------------------------------------------------------- /solutions/lesson4_invariants/simple_voting/Voting_solution.conf: -------------------------------------------------------------------------------- 1 | { 2 | // Run from tutorials-code root dir 3 | "files": [ 4 | "lesson4_invariants/simple_voting/Voting.sol:Voting" 5 | ], 6 | "verify": "Voting:solutions/lesson4_invariants/simple_voting/Voting_solution.spec", 7 | "wait_for_results": "all", 8 | "rule_sanity": "basic", 9 | "msg": "Voting contract complete solution" 10 | } 11 | -------------------------------------------------------------------------------- /solutions/lesson4_invariants/simple_voting/Voting_solution.spec: -------------------------------------------------------------------------------- 1 | /** 2 | * # Simple voting contract complete spec 3 | * 4 | * To use gambit, run from the tutorials-code root folder the following command: 5 | * `certoraMutate --prover_conf solutions/lesson4_invariants/simple_voting/Voting_solution.conf --mutation_conf solutions/lesson4_invariants/simple_voting/mutate.json` 6 | */ 7 | methods 8 | { 9 | function votesInFavor() external returns (uint256) envfree; 10 | function votesAgainst() external returns (uint256) envfree; 11 | function totalVotes() external returns (uint256) envfree; 12 | function hasVoted(address) external returns (bool) envfree; 13 | } 14 | 15 | // ---- Ghosts ----------------------------------------------------------------- 16 | 17 | /// @title Count the number of times `_hasVoted` been written to 18 | ghost mathint numVoted { 19 | init_state axiom numVoted == 0; 20 | } 21 | 22 | 23 | /// @title For ensuring all changes to `_hasVoted` are legal 24 | ghost bool illegalStore { 25 | init_state axiom !illegalStore; 26 | } 27 | 28 | 29 | /// @title Ghost indicating someone has voted 30 | ghost bool someoneVoted; 31 | 32 | 33 | // ---- Hooks ------------------------------------------------------------------ 34 | 35 | hook Sstore _hasVoted[KEY address voter] bool newVal (bool oldVal) { 36 | 37 | if (!oldVal && newVal) { 38 | numVoted = numVoted + 1; 39 | } 40 | 41 | someoneVoted = true; 42 | 43 | // Note once `illegalStore` is true, it remains true 44 | illegalStore = illegalStore || oldVal; 45 | } 46 | 47 | 48 | // ---- Rules ------------------------------------------------------------------ 49 | 50 | /// @title No illegal changes to `_hasVoted` 51 | invariant onlyLegalVotedChanges() 52 | !illegalStore; 53 | 54 | 55 | /// @title Total votes is the number of times `_hasVoted` been written to 56 | invariant sumVotedEqualssTotalVotes() 57 | to_mathint(totalVotes()) == numVoted; 58 | 59 | 60 | /// @title Sum of voter in favor and against equals total number of voted 61 | invariant sumResultsEqualsTotalVotes() 62 | votesInFavor() + votesAgainst() == to_mathint(totalVotes()); 63 | 64 | 65 | 66 | /// @title Only the method `vote` can be used to vote 67 | rule voteOnlyByCallingVote(method f) { 68 | require !someoneVoted; 69 | 70 | env e; 71 | calldataarg args; 72 | f(e, args); 73 | 74 | assert ( 75 | someoneVoted => f.selector == sig:vote(bool).selector, 76 | "Voted only via vote" 77 | ); 78 | } 79 | 80 | 81 | /// @title Votes in favor or against can only change by 1 82 | rule votesChangeByOne(method f) { 83 | uint256 preFavor = votesInFavor(); 84 | uint256 preAgainst = votesAgainst(); 85 | 86 | env e; 87 | calldataarg args; 88 | f(e, args); 89 | 90 | mathint favorDiff = votesInFavor() - preFavor; 91 | mathint againstDiff = votesAgainst() - preAgainst; 92 | 93 | assert favorDiff >= 0 && favorDiff <= 1, "In favor may change by 0 or 1"; 94 | assert againstDiff >= 0 && againstDiff <= 1, "Against may change by 0 or 1"; 95 | } 96 | 97 | 98 | /// @title Voter determines if vote in favor or against 99 | rule voterDecides(bool isInFavor) { 100 | uint256 preFavor = votesInFavor(); 101 | uint256 preAgainst = votesAgainst(); 102 | 103 | env e; 104 | vote(e, isInFavor); 105 | 106 | uint256 postFavor = votesInFavor(); 107 | uint256 postAgainst = votesAgainst(); 108 | 109 | assert ( 110 | (isInFavor => (postFavor > preFavor)) && 111 | (!isInFavor => (postAgainst > preAgainst)) 112 | ), "Voter determines if vote is in favor or against"; 113 | } 114 | 115 | 116 | 117 | /// @title Anyone can vote once 118 | rule anyoneCanVote(address voter, bool isInFavor) { 119 | bool preHasVoted = hasVoted(voter); 120 | uint256 preTotal = totalVotes(); 121 | 122 | env e; 123 | require e.msg.sender == voter; 124 | 125 | // Limit the overflow cases - this means we need only check `totalVotes` 126 | requireInvariant sumResultsEqualsTotalVotes(); 127 | 128 | vote@withrevert(e, isInFavor); 129 | 130 | assert ( 131 | lastReverted <=> ( 132 | preHasVoted // Revert since voted before 133 | || e.msg.value > 0 // Sending ETH will cause revert 134 | || preTotal == max_uint256 // Revert due to overflow 135 | ) 136 | ), "Can vote first time"; 137 | } 138 | -------------------------------------------------------------------------------- /solutions/lesson4_invariants/simple_voting/mutate.conf: -------------------------------------------------------------------------------- 1 | { 2 | "gambit": { 3 | "filename": "../../../lesson4_invariants/simple_voting/Voting.sol" 4 | } 5 | } 6 | --------------------------------------------------------------------------------