diff --git a/CVLByExample/TrackingSums/ERC20.spec b/CVLByExample/TrackingSums/ERC20.spec new file mode 100644 index 00000000..34fd739a --- /dev/null +++ b/CVLByExample/TrackingSums/ERC20.spec @@ -0,0 +1,47 @@ +using ERC20 as token; + +methods { + function token.balanceOf(address) external returns (uint256) envfree; + function token.totalSupply() external returns (uint256) envfree; + function token.transfer(address,uint256) external returns (bool); +} + +ghost mathint sumOfBalances { + init_state axiom sumOfBalances == 0; +} + +/* +hook Sload uint256 _balance _balances[KEY address account] { + require sumOfBalances >= to_mathint(_balance); +} +*/ + +hook Sstore _balances[KEY address account] uint256 _balance (uint256 _balance_old) { + sumOfBalances = sumOfBalances + _balance - _balance_old; +} + +invariant SumOfBalancesEqualsTotalSupply() + sumOfBalances == to_mathint(token.totalSupply()); + +rule transferPreservesSumOfBalances(address to, uint256 amount) { + env e; + address from = e.msg.sender; + uint256 balanceFrom_before = token.balanceOf(from); + uint256 balanceTo_before = token.balanceOf(to); + /// We must explictly require the sum of balances to be capped by max(uint256) + require balanceFrom_before + balanceTo_before <= max_uint256; + token.transfer(e, to, amount); + uint256 balanceFrom_after = token.balanceOf(from); + uint256 balanceTo_after = token.balanceOf(to); + + assert balanceTo_after + balanceFrom_after == balanceTo_before + balanceFrom_before; +} + +rule twoBalancesCannotExceedTotalSupply(address accountA, address accountB) { + requireInvariant SumOfBalancesEqualsTotalSupply(); + uint256 balanceA = token.balanceOf(accountA); + uint256 balanceB = token.balanceOf(accountB); + + assert accountA != accountB => + balanceA + balanceB <= to_mathint(token.totalSupply()); +} \ No newline at end of file diff --git a/CVLByExample/TrackingSums/ERC20_bug.sol b/CVLByExample/TrackingSums/ERC20_bug.sol new file mode 100644 index 00000000..bc95cecb --- /dev/null +++ b/CVLByExample/TrackingSums/ERC20_bug.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/ERC20.sol) + +pragma solidity ^0.8.0; + +import {ERC20} from "contracts/token/ERC20/ERC20.sol"; + +contract ERC20_bug is ERC20 { + constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {} + + uint256 private constant _hiddenFee = 1; + + function _transfer(address from, address to, uint256 amount) internal override { + super._transfer(from, to, amount); + _balances[to] -= _hiddenFee; + } +} \ No newline at end of file diff --git a/CVLByExample/TrackingSums/ERC20_trackingSums.spec b/CVLByExample/TrackingSums/ERC20_trackingSums.spec new file mode 100644 index 00000000..90aee65d --- /dev/null +++ b/CVLByExample/TrackingSums/ERC20_trackingSums.spec @@ -0,0 +1,97 @@ +using ERC20 as token; +//using ERC20_bug as token; + +methods { + function token.balanceOf(address) external returns (uint256) envfree; + function token.totalSupply() external returns (uint256) envfree; + function token.transfer(address,uint256) external returns (bool); +} + +/// True sum of balances. +ghost mathint sumOfBalances { + init_state axiom sumOfBalances == 0; +} + +/// The initial value is being updated as we access the acounts balances one-by-one. +/// Should only be used as an initial value, never post-action! +ghost mathint sumOfBalances_init { + init_state axiom sumOfBalances_init == 0; +} + +ghost mapping(address => bool) didAccessAccount; + +function SumTrackingSetup() { + require sumOfBalances == sumOfBalances_init; + require forall address account. !didAccessAccount[account]; +} + +/* +For every storage load of the balance for some account, for the first time, +we deduct the storage amount from the initial value and require that the result is non-negative. + +Example: +Suppose that at first the initial sum of balances that are going to be accessed in the flow is x. +sumOfBalances_init = x. +The function flow is going to access: + _balances[0x1] = 1 + _balances[0x2] = 2 + _balances[0x3] = 3 + +x(0) = x +At first access [0x1]: + x(1) -> x(0) - 1 = x - 1 ; require x - 1 >=0 +At second access [0x2]: + x(2) -> x(1) - 2 = x - 3 ; require x - 3 >=0 +At third access [0x3]: + x(3) -> x(2) - 3 = x - 6 ; require x - 6 >=0 + +The result is that x = sumOfBalances_init >= 6 = 1 + 2 + 3, as expected. +*/ +hook Sload uint256 _balance _balances[KEY address account] { + if(!didAccessAccount[account]) { + didAccessAccount[account] = true; + sumOfBalances_init = sumOfBalances_init - _balance; + require sumOfBalances_init >= 0; + } +} + +hook Sstore _balances[KEY address account] uint256 _balance (uint256 _balance_old) { + if(!didAccessAccount[account]) { + didAccessAccount[account] = true; + sumOfBalances_init = sumOfBalances_init - _balance_old; + require sumOfBalances_init >= 0; + } + sumOfBalances = sumOfBalances + _balance - _balance_old; +} + +invariant SumOfBalancesEqualsTotalSupply() + sumOfBalances == to_mathint(token.totalSupply()) + { + preserved { + SumTrackingSetup(); + } + } + +rule transferPreservesSumOfBalances(address to, uint256 amount) { + env e; + address from = e.msg.sender; + uint256 balanceFrom_before = token.balanceOf(from); + uint256 balanceTo_before = token.balanceOf(to); + /// We must explictly require the sum of balances to be capped by max(uint256) + require balanceFrom_before + balanceTo_before <= max_uint256; + token.transfer(e, to, amount); + uint256 balanceFrom_after = token.balanceOf(from); + uint256 balanceTo_after = token.balanceOf(to); + + assert balanceTo_after + balanceFrom_after == balanceTo_before + balanceFrom_before; +} + +rule TwoBalancesCannotExceedTotalSupply(address accountA, address accountB) { + SumTrackingSetup(); + requireInvariant SumOfBalancesEqualsTotalSupply(); + uint256 balanceA = token.balanceOf(accountA); + uint256 balanceB = token.balanceOf(accountB); + + assert accountA != accountB => + balanceA + balanceB <= to_mathint(token.totalSupply()); +} diff --git a/CVLByExample/TrackingSums/Run.conf b/CVLByExample/TrackingSums/Run.conf new file mode 100755 index 00000000..37d9c5a6 --- /dev/null +++ b/CVLByExample/TrackingSums/Run.conf @@ -0,0 +1,11 @@ +{ + "files": [ + "contracts/token/ERC20/ERC20.sol", + ], + "verify":"ERC20:ERC20.spec", + "loop_iter": "3", + "optimistic_loop": true, + "solc": "solc8.18", + "server":"production", + "msg": "ERC20 sum of balances" +} \ No newline at end of file diff --git a/CVLByExample/TrackingSums/Run_bug.conf b/CVLByExample/TrackingSums/Run_bug.conf new file mode 100755 index 00000000..d51daa5a --- /dev/null +++ b/CVLByExample/TrackingSums/Run_bug.conf @@ -0,0 +1,11 @@ +{ + "files": [ + "ERC20_bug.sol", + ], + "verify":"ERC20_bug:ERC20_trackingSums.spec", + "loop_iter": "3", + "optimistic_loop": true, + "solc": "solc8.18", + "server":"production", + "msg": "ERC20 sum of balances with bug" +} \ No newline at end of file diff --git a/CVLByExample/TrackingSums/Run_trackingSums.conf b/CVLByExample/TrackingSums/Run_trackingSums.conf new file mode 100755 index 00000000..18fe9092 --- /dev/null +++ b/CVLByExample/TrackingSums/Run_trackingSums.conf @@ -0,0 +1,11 @@ +{ + "files": [ + "contracts/token/ERC20/ERC20.sol", + ], + "verify":"ERC20:ERC20_trackingSums.spec", + "loop_iter": "3", + "optimistic_loop": true, + "solc": "solc8.18", + "server":"production", + "msg": "ERC20 sum of balances" +} \ No newline at end of file diff --git a/CVLByExample/TrackingSums/contracts/token/ERC20/ERC20.sol b/CVLByExample/TrackingSums/contracts/token/ERC20/ERC20.sol new file mode 100644 index 00000000..564e3175 --- /dev/null +++ b/CVLByExample/TrackingSums/contracts/token/ERC20/ERC20.sol @@ -0,0 +1,365 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/ERC20.sol) + +pragma solidity ^0.8.0; + +import "./IERC20.sol"; +import "./extensions/IERC20Metadata.sol"; +import "../../utils/Context.sol"; + +/** + * @dev Implementation of the {IERC20} interface. + * + * This implementation is agnostic to the way tokens are created. This means + * that a supply mechanism has to be added in a derived contract using {_mint}. + * For a generic mechanism see {ERC20PresetMinterPauser}. + * + * TIP: For a detailed writeup see our guide + * https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How + * to implement supply mechanisms]. + * + * The default value of {decimals} is 18. To change this, you should override + * this function so it returns a different value. + * + * We have followed general OpenZeppelin Contracts guidelines: functions revert + * instead returning `false` on failure. This behavior is nonetheless + * conventional and does not conflict with the expectations of ERC20 + * applications. + * + * Additionally, an {Approval} event is emitted on calls to {transferFrom}. + * This allows applications to reconstruct the allowance for all accounts just + * by listening to said events. Other implementations of the EIP may not emit + * these events, as it isn't required by the specification. + * + * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} + * functions have been added to mitigate the well-known issues around setting + * allowances. See {IERC20-approve}. + */ +contract ERC20 is Context, IERC20, IERC20Metadata { + mapping(address => uint256) internal _balances; + + mapping(address => mapping(address => uint256)) private _allowances; + + uint256 private _totalSupply; + + string private _name; + string private _symbol; + + /** + * @dev Sets the values for {name} and {symbol}. + * + * All two of these values are immutable: they can only be set once during + * construction. + */ + constructor(string memory name_, string memory symbol_) { + _name = name_; + _symbol = symbol_; + } + + /** + * @dev Returns the name of the token. + */ + function name() public view virtual override returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view virtual override returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5.05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. This is the default value returned by this function, unless + * it's overridden. + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view virtual override returns (uint8) { + return 18; + } + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view virtual override returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) public view virtual override returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer(address to, uint256 amount) public virtual override returns (bool) { + address owner = _msgSender(); + _transfer(owner, to, amount); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address owner, address spender) public view virtual override returns (uint256) { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on + * `transferFrom`. This is semantically equivalent to an infinite approval. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 amount) public virtual override returns (bool) { + address owner = _msgSender(); + _approve(owner, spender, amount); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}. + * + * NOTE: Does not update the allowance if the current allowance + * is the maximum `uint256`. + * + * Requirements: + * + * - `from` and `to` cannot be the zero address. + * - `from` must have a balance of at least `amount`. + * - the caller must have allowance for ``from``'s tokens of at least + * `amount`. + */ + function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) { + address spender = _msgSender(); + _spendAllowance(from, spender, amount); + _transfer(from, to, amount); + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { + address owner = _msgSender(); + _approve(owner, spender, allowance(owner, spender) + addedValue); + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `spender` must have allowance for the caller of at least + * `subtractedValue`. + */ + function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) { + address owner = _msgSender(); + uint256 currentAllowance = allowance(owner, spender); + require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero"); + unchecked { + _approve(owner, spender, currentAllowance - subtractedValue); + } + + return true; + } + + /** + * @dev Moves `amount` of tokens from `from` to `to`. + * + * This internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `from` must have a balance of at least `amount`. + */ + function _transfer(address from, address to, uint256 amount) internal virtual { + require(from != address(0), "ERC20: transfer from the zero address"); + require(to != address(0), "ERC20: transfer to the zero address"); + + _beforeTokenTransfer(from, to, amount); + + uint256 fromBalance = _balances[from]; + require(fromBalance >= amount, "ERC20: transfer amount exceeds balance"); + unchecked { + _balances[from] = fromBalance - amount; + // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by + // decrementing then incrementing. + _balances[to] += amount; + } + + emit Transfer(from, to, amount); + + _afterTokenTransfer(from, to, amount); + } + + /** @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * Requirements: + * + * - `account` cannot be the zero address. + */ + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: mint to the zero address"); + + _beforeTokenTransfer(address(0), account, amount); + + _totalSupply += amount; + unchecked { + // Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above. + _balances[account] += amount; + } + emit Transfer(address(0), account, amount); + + _afterTokenTransfer(address(0), account, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`, reducing the + * total supply. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * Requirements: + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens. + */ + function _burn(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: burn from the zero address"); + + _beforeTokenTransfer(account, address(0), amount); + + uint256 accountBalance = _balances[account]; + require(accountBalance >= amount, "ERC20: burn amount exceeds balance"); + unchecked { + _balances[account] = accountBalance - amount; + // Overflow not possible: amount <= accountBalance <= totalSupply. + _totalSupply -= amount; + } + + emit Transfer(account, address(0), amount); + + _afterTokenTransfer(account, address(0), amount); + } + + /** + * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + function _approve(address owner, address spender, uint256 amount) internal virtual { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + /** + * @dev Updates `owner` s allowance for `spender` based on spent `amount`. + * + * Does not update the allowance amount in case of infinite allowance. + * Revert if not enough allowance is available. + * + * Might emit an {Approval} event. + */ + function _spendAllowance(address owner, address spender, uint256 amount) internal virtual { + uint256 currentAllowance = allowance(owner, spender); + if (currentAllowance != type(uint256).max) { + require(currentAllowance >= amount, "ERC20: insufficient allowance"); + unchecked { + _approve(owner, spender, currentAllowance - amount); + } + } + } + + /** + * @dev Hook that is called before any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * will be transferred to `to`. + * - when `from` is zero, `amount` tokens will be minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens will be burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {} + + /** + * @dev Hook that is called after any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * has been transferred to `to`. + * - when `from` is zero, `amount` tokens have been minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens have been burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {} +} diff --git a/CVLByExample/TrackingSums/contracts/token/ERC20/IERC20.sol b/CVLByExample/TrackingSums/contracts/token/ERC20/IERC20.sol new file mode 100644 index 00000000..6d5b4e9f --- /dev/null +++ b/CVLByExample/TrackingSums/contracts/token/ERC20/IERC20.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/IERC20.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); + + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 amount) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `from` to `to` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address from, address to, uint256 amount) external returns (bool); +} diff --git a/CVLByExample/TrackingSums/contracts/token/ERC20/extensions/IERC20Metadata.sol b/CVLByExample/TrackingSums/contracts/token/ERC20/extensions/IERC20Metadata.sol new file mode 100644 index 00000000..83ba6ac5 --- /dev/null +++ b/CVLByExample/TrackingSums/contracts/token/ERC20/extensions/IERC20Metadata.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/IERC20Metadata.sol) + +pragma solidity ^0.8.0; + +import "../IERC20.sol"; + +/** + * @dev Interface for the optional metadata functions from the ERC20 standard. + * + * _Available since v4.1._ + */ +interface IERC20Metadata is IERC20 { + /** + * @dev Returns the name of the token. + */ + function name() external view returns (string memory); + + /** + * @dev Returns the symbol of the token. + */ + function symbol() external view returns (string memory); + + /** + * @dev Returns the decimals places of the token. + */ + function decimals() external view returns (uint8); +} diff --git a/CVLByExample/TrackingSums/contracts/utils/Context.sol b/CVLByExample/TrackingSums/contracts/utils/Context.sol new file mode 100644 index 00000000..f304065b --- /dev/null +++ b/CVLByExample/TrackingSums/contracts/utils/Context.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (utils/Context.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract Context { + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } +}