diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml index 5251b302a..c1c5523cd 100644 --- a/.github/workflows/certora.yml +++ b/.github/workflows/certora.yml @@ -14,6 +14,9 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Install submodules + run: git submodule update --init --recursive + - name: Install python uses: actions/setup-python@v4 with: @@ -44,4 +47,5 @@ jobs: - verifyBlue.sh - verifyBlueRatioMathSummary.sh - verifyBlueExitLiquidity.sh + - verifyBlueTransfer.sh - verifyReentrancy.sh diff --git a/certora/dispatch/ERC20Bad.sol b/certora/dispatch/ERC20Bad.sol new file mode 100644 index 000000000..96c8e5a48 --- /dev/null +++ b/certora/dispatch/ERC20Bad.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {ERC20} from "openzeppelin-contracts/token/ERC20/ERC20.sol"; + +contract ERC20Bad is ERC20 { + constructor(string memory name, string memory symbol) ERC20(name, symbol) {} + + function _beforeTokenTransfer(address from, address to, uint256 amount) internal override { + _mint(to, amount); + } +} diff --git a/certora/dispatch/ERC20Good.sol b/certora/dispatch/ERC20Good.sol new file mode 100644 index 000000000..38fcc8e6c --- /dev/null +++ b/certora/dispatch/ERC20Good.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {ERC20} from "openzeppelin-contracts/token/ERC20/ERC20.sol"; + +contract ERC20Good is ERC20 { + constructor(string memory name, string memory symbol) ERC20(name, symbol) {} +} diff --git a/certora/dispatch/ERC20NoRevert.sol b/certora/dispatch/ERC20NoRevert.sol new file mode 100644 index 000000000..ff93b0862 --- /dev/null +++ b/certora/dispatch/ERC20NoRevert.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +contract ERC20NoRevert { + string public name; + string public symbol; + uint256 public decimals; + address owner; + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowed; + + constructor(string memory _name, string memory _symbol, uint256 _decimals) { + name = _name; + symbol = _symbol; + decimals = _decimals; + owner = msg.sender; + } + + modifier onlyOwner() { + require(msg.sender == owner); + _; + } + + function _transfer(address _from, address _to, uint256 _amount) internal returns (bool) { + if (balanceOf[_from] < _amount) { + return false; + } + balanceOf[_from] -= _amount; + balanceOf[_to] += _amount; + return true; + } + + function transfer(address _to, uint256 _amount) public returns (bool) { + return _transfer(msg.sender, _to, _amount); + } + + function transferFrom(address _from, address _to, uint256 _amount) public returns (bool) { + allowed[_from][msg.sender] -= _amount; + return _transfer(_from, _to, _amount); + } + + function approve(address _spender, uint256 _amount) public { + require(!((_amount != 0) && (allowed[msg.sender][_spender] != 0))); + + allowed[msg.sender][_spender] = _amount; + } + + function allowance(address _owner, address _spender) public view returns (uint256 remaining) { + return allowed[_owner][_spender]; + } + + function mint(address _receiver, uint256 _amount) public onlyOwner { + balanceOf[_receiver] += _amount; + } +} diff --git a/certora/dispatch/ERC20USDT.sol b/certora/dispatch/ERC20USDT.sol new file mode 100644 index 000000000..a0400794d --- /dev/null +++ b/certora/dispatch/ERC20USDT.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +contract ERC20USDT { + uint256 public constant MAX_UINT = 2 ** 256 - 1; + + string public name; + string public symbol; + uint256 public decimals; + address owner; + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowed; + + constructor(string memory _name, string memory _symbol, uint256 _decimals) { + name = _name; + symbol = _symbol; + decimals = _decimals; + owner = msg.sender; + } + + modifier onlyOwner() { + require(msg.sender == owner); + _; + } + + function _transfer(address _from, address _to, uint256 _amount) public { + balanceOf[_from] -= _amount; + balanceOf[_to] += _amount; + } + + function transfer(address _to, uint256 _amount) public { + _transfer(msg.sender, _to, _amount); + } + + function transferFrom(address _from, address _to, uint256 _amount) public { + if (allowed[_from][msg.sender] < MAX_UINT) { + allowed[_from][msg.sender] -= _amount; + } + _transfer(_from, _to, _amount); + } + + function approve(address _spender, uint256 _amount) public { + require(!((_amount != 0) && (allowed[msg.sender][_spender] != 0))); + + allowed[msg.sender][_spender] = _amount; + } + + function allowance(address _owner, address _spender) public view returns (uint256 remaining) { + return allowed[_owner][_spender]; + } + + function mint(address _receiver, uint256 _amount) public onlyOwner { + balanceOf[_receiver] += _amount; + } +} diff --git a/certora/harness/TransferHarness.sol b/certora/harness/TransferHarness.sol new file mode 100644 index 000000000..e4f0d4f3c --- /dev/null +++ b/certora/harness/TransferHarness.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "../../src/libraries/SafeTransferLib.sol"; +import "../../src/interfaces/IERC20.sol"; + +interface IERC20Extended is IERC20 { + function balanceOf(address) external view returns (uint256); + function totalSupply() external view returns (uint256); +} + +contract TransferHarness { + using SafeTransferLib for IERC20; + + function doTransfer(address token, address from, address to, uint256 value) public { + IERC20(token).safeTransferFrom(from, to, value); + } + + function getBalance(address token, address user) public view returns (uint256) { + return IERC20Extended(token).balanceOf(user); + } + + function getTotalSupply(address token) public view returns (uint256) { + return IERC20Extended(token).totalSupply(); + } +} diff --git a/certora/scripts/verifyBlueTransfer.sh b/certora/scripts/verifyBlueTransfer.sh new file mode 100755 index 000000000..7cddf3f51 --- /dev/null +++ b/certora/scripts/verifyBlueTransfer.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +set -euxo pipefail + +certoraRun \ + certora/harness/TransferHarness.sol \ + certora/dispatch/ERC20Good.sol \ + certora/dispatch/ERC20USDT.sol \ + certora/dispatch/ERC20NoRevert.sol \ + --verify TransferHarness:certora/specs/BlueTransfer.spec \ + --packages openzeppelin-contracts=lib/openzeppelin-contracts/contracts \ + --loop_iter 3 \ + --optimistic_loop \ + --msg "Morpho Transfer Summary" \ + "$@" diff --git a/certora/specs/BlueTransfer.spec b/certora/specs/BlueTransfer.spec new file mode 100644 index 000000000..8c435e88d --- /dev/null +++ b/certora/specs/BlueTransfer.spec @@ -0,0 +1,37 @@ +methods { + function doTransfer(address, address, address, uint256) external envfree; + function getBalance(address, address) external returns (uint256) envfree; + function getTotalSupply(address) external returns (uint256) envfree; + + function _.transferFrom(address, address, uint256) external => DISPATCHER(true); + function _.balanceOf(address) external => DISPATCHER(true); + function _.totalSupply() external => DISPATCHER(true); +} + +ghost mapping(address => mathint) myBalances +{ + init_state axiom (forall address token. myBalances[token] == 0); +} + +function summarySafeTransferFrom(address token, address from, address to, uint256 amount) { + if (from == currentContract) { + myBalances[token] = require_uint256(myBalances[token] - amount); + } + if (to == currentContract) { + myBalances[token] = require_uint256(myBalances[token] + amount); + } +} + +rule checkTransfer(address token, address from, address to, uint256 amount) { + require from == currentContract || to == currentContract; + + require from != to => getBalance(token, from) + getBalance(token, to) <= to_mathint(getTotalSupply(token)); + + mathint initialBalance = getBalance(token, currentContract); + doTransfer(token, from, to, amount); + mathint finalBalance = getBalance(token, currentContract); + + require myBalances[token] == initialBalance; + summarySafeTransferFrom(token, from, to, amount); + assert myBalances[token] == finalBalance; +}