Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Halmos] Setup for verification and first invariants #675

Merged
merged 34 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
7d7bfa9
feat: add Halmos test
QGarchery Apr 2, 2024
7aebf25
fix: add foundry to CI for Halmos
QGarchery Apr 2, 2024
6667451
chore: no timeout on counterexample
QGarchery Apr 2, 2024
bb2f9af
chore: remove halmos-cheatcode
QGarchery Apr 2, 2024
b1445fa
chore: add back halmos-cheatcodes with https
QGarchery Apr 2, 2024
787e154
fix: test fee
QGarchery Apr 2, 2024
725c9ee
fix: use distinct args for dynamic types
QGarchery Apr 3, 2024
7a71b62
chore: simpler setup
QGarchery Apr 4, 2024
8489407
feat: pass all market params
QGarchery Apr 4, 2024
49a1ca0
feat: generalize token for flashLoan
QGarchery Apr 5, 2024
b6f96c7
fix: svm create newFee
QGarchery Apr 5, 2024
3e546e5
feat: borrow less than supply and last updated invariant
QGarchery Apr 10, 2024
2b69597
fix: lastUpdate name
QGarchery Apr 10, 2024
0c94fc1
Merge remote-tracking branch 'origin/main' into halmos/setup
QGarchery Apr 16, 2024
72d70b2
chore: specify python-version
QGarchery Apr 16, 2024
ff1427a
chore: bump action setup python
QGarchery Apr 16, 2024
fd6adac
chore: add setuptools
QGarchery Apr 16, 2024
a71df2d
feat: symbolic LLTV
QGarchery Apr 16, 2024
62fc02e
chore: remove setuptools
QGarchery Apr 19, 2024
6ef5c7a
feat: check lltv smaller than WAD
QGarchery Apr 19, 2024
c29c3bc
refactor: use test profile for Halmos
QGarchery Apr 19, 2024
5a41d5d
docs: add description of invariants
QGarchery Apr 19, 2024
1221cce
test: halmost irm and ltv can't be disabled
MathisGD Apr 22, 2024
46c6f46
test: halmos nonce can't decrease
MathisGD Apr 22, 2024
ba6386e
test: halmos fix nonceCannotDecrease
MathisGD Apr 22, 2024
a3bfab0
test: halmos add two little rules
MathisGD Apr 22, 2024
95de5e5
Merge pull request #677 from morpho-org/halmos/more-rules
QGarchery Apr 22, 2024
53f3e28
refactor: simplify setup
QGarchery Apr 22, 2024
4b96f6a
refactor: merge dispatch borrow-withdraw
QGarchery Apr 22, 2024
dc2c664
test: halmos document check_idToMarketParamsForCreatedMarketCannotChange
MathisGD Apr 22, 2024
da3ebee
test: (halmos) fix check_nonceCannotDecrease
MathisGD Apr 22, 2024
0bdca52
refactor: simplify setup, and generalize id
QGarchery Apr 22, 2024
0618762
refactor: make id completely symbolic
QGarchery Apr 22, 2024
5a79282
refactor: move Halmos test to its own folder
QGarchery Apr 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/certora.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ jobs:
- Transfer

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: recursive

- name: Install python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ">=3.9"

- name: Install certora
run: pip install certora-cli
Expand Down
31 changes: 31 additions & 0 deletions .github/workflows/halmos.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Halmos

on:
push:
branches:
- main
pull_request:
workflow_dispatch:

jobs:
verify:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- name: Install python
uses: actions/setup-python@v5
with:
python-version: ">=3.9"

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1

- name: Install halmos
run: pip install halmos

- name: Run Halmos
run: FOUNDRY_PROFILE=test halmos
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/halmos-cheatcodes"]
path = lib/halmos-cheatcodes
url = https://github.com/a16z/halmos-cheatcodes.git
1 change: 1 addition & 0 deletions lib/halmos-cheatcodes
Submodule halmos-cheatcodes added at c0d865
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"test:forge:invariant": "FOUNDRY_MATCH_CONTRACT=InvariantTest yarn test:forge",
"test:forge:integration": "FOUNDRY_MATCH_CONTRACT=IntegrationTest yarn test:forge",
"test:hardhat": "npx hardhat test",
"test:halmos": "FOUNDRY_PROFILE=test halmos",
"lint": "yarn lint:forge && yarn lint:hardhat",
"lint:forge": "forge fmt --check",
"lint:hardhat": "prettier --check test/hardhat",
Expand Down
185 changes: 185 additions & 0 deletions test/forge/HalmosTest.sol
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import "../../lib/forge-std/src/Test.sol";
import {SymTest} from "../../lib/halmos-cheatcodes/src/SymTest.sol";

import {IMorpho} from "../../src/interfaces/IMorpho.sol";
import {IrmMock} from "../../src/mocks/IrmMock.sol";
import {ERC20Mock} from "../../src/mocks/ERC20Mock.sol";
import {OracleMock} from "../../src/mocks/OracleMock.sol";

import "../../src/Morpho.sol";
import "../../src/libraries/ConstantsLib.sol";
import {MorphoLib} from "../../src/libraries/periphery/MorphoLib.sol";

/// @custom:halmos --symbolic-storage --solver-timeout-assertion 0
contract HalmosTest is SymTest, Test {
using MorphoLib for IMorpho;
using MarketParamsLib for MarketParams;

uint256 internal constant BLOCK_TIME = 1;

address internal OWNER;
address internal FEE_RECIPIENT;

IMorpho internal morpho;
ERC20Mock internal loanToken;
ERC20Mock internal collateralToken;
OracleMock internal oracle;
IrmMock internal irm;
uint256 internal lltv;

MarketParams internal marketParams;
Id internal id;

function setUp() public virtual {
OWNER = address(0x10);
FEE_RECIPIENT = address(0x11);

morpho = IMorpho(address(new Morpho(OWNER)));

loanToken = new ERC20Mock();
collateralToken = new ERC20Mock();
oracle = new OracleMock();
oracle.setPrice(ORACLE_PRICE_SCALE);
irm = new IrmMock();
lltv = svm.createUint256("lltv");

marketParams = MarketParams(address(loanToken), address(collateralToken), address(oracle), address(irm), lltv);
id = marketParams.id();

vm.assume(block.timestamp != 0);
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
vm.startPrank(OWNER);
morpho.enableIrm(address(irm));
morpho.enableLltv(lltv);
morpho.createMarket(marketParams);
vm.stopPrank();
}

// Call Morpho, assuming interacting with only the defined market for performance reasons.
function _callMorpho(bytes4 selector, address caller) internal {
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
vm.assume(selector != morpho.extSloads.selector);
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
vm.assume(selector != morpho.createMarket.selector);
MathisGD marked this conversation as resolved.
Show resolved Hide resolved

bytes memory emptyData = hex"";
uint256 assets = svm.createUint256("assets");
uint256 shares = svm.createUint256("shares");
address onBehalf = svm.createAddress("onBehalf");
address receiver = svm.createAddress("receiver");

bytes memory args;

if (selector == morpho.supply.selector || selector == morpho.repay.selector) {
args = abi.encode(marketParams, assets, shares, onBehalf, emptyData);
} else if (selector == morpho.withdraw.selector || selector == morpho.borrow.selector) {
args = abi.encode(marketParams, assets, shares, onBehalf, receiver);
} else if (selector == morpho.supplyCollateral.selector) {
args = abi.encode(marketParams, assets, onBehalf, emptyData);
} else if (selector == morpho.withdrawCollateral.selector) {
args = abi.encode(marketParams, assets, onBehalf, receiver);
} else if (selector == morpho.liquidate.selector) {
address borrower = svm.createAddress("borrower");
args = abi.encode(marketParams, borrower, assets, shares, emptyData);
} else if (selector == morpho.flashLoan.selector) {
uint256 rand = svm.createUint256("rand");
address token;
if (rand == 0) {
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
token = address(loanToken);
} else if (rand == 1) {
token = address(collateralToken);
} else {
ERC20Mock otherToken = new ERC20Mock();
token = address(otherToken);
}
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
args = abi.encode(marketParams, token, assets, emptyData);
} else if (selector == morpho.accrueInterest.selector) {
args = abi.encode(marketParams);
} else if (selector == morpho.setFee.selector) {
uint256 newFee = svm.createUint256("newFee");
args = abi.encode(marketParams, newFee);
} else {
args = svm.createBytes(1024, "data");
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
}

vm.prank(caller);
(bool success,) = address(morpho).call(abi.encodePacked(selector, args));
vm.assume(success);
}

// Check that the fee is always smaller than the max fee.
function check_feeInRange(bytes4 selector, address caller) public {
_callMorpho(selector, caller);
QGarchery marked this conversation as resolved.
Show resolved Hide resolved

assert(morpho.fee(id) <= MAX_FEE);
}

// Check that there is always less borrow than supply on the market.
function check_borrowLessThanSupply(bytes4 selector, address caller) public {
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
_callMorpho(selector, caller);

assert(morpho.totalBorrowAssets(id) <= morpho.totalSupplyAssets(id));
}

// Check that the market cannot be "destroyed".
function check_lastUpdateNonZero(bytes4 selector, address caller) public {
_callMorpho(selector, caller);

assert(morpho.lastUpdate(id) != 0);
}

// Check that the lastUpdate can only increase.
function check_lastUpdateCannotDecrease(bytes4 selector, address caller) public {
uint256 lastUpdateBefore = morpho.lastUpdate(id);

_callMorpho(selector, caller);

uint256 lastUpdateAfter = morpho.lastUpdate(id);
assert(lastUpdateAfter >= lastUpdateBefore);
}

// Check that enabled LLTVs are necessarily less than 1.
function check_lltvSmallerThanWad(bytes4 selector, address caller, uint256 _lltv) public {
vm.assume(!morpho.isLltvEnabled(_lltv) || _lltv < 1e18);

_callMorpho(selector, caller);

assert(!morpho.isLltvEnabled(_lltv) || _lltv < 1e18);
}

// Check that LLTVs can't be disabled.
function check_lltvCannotBeDisabled(bytes4 selector, address caller) public {
_callMorpho(selector, caller);

assert(morpho.isLltvEnabled(lltv));
}

// Check that IRMs can't be disabled.
// Note: IRM is not symbolic, that is not ideal.
function check_irmCannotBeDisabled(bytes4 selector, address caller) public {
_callMorpho(selector, caller);

assert(morpho.isIrmEnabled(address(irm)));
}

// Check that the nonce of users cannot decrease.
function check_nonceCannotDecrease(bytes4 selector, address caller, address user) public {
uint256 nonceBefore = morpho.nonce(user);

_callMorpho(selector, caller);

uint256 nonceAfter = morpho.nonce(user);
assert(nonceAfter == nonceBefore);
}

// Check that idToMarketParams cannot change.
// Note: ok because createMarket is never called by _callMorpho.
function check_idToMarketParamsForCreatedMarketCannotChange(bytes4 selector, address caller) public {
MarketParams memory itmpBefore = morpho.idToMarketParams(id);

_callMorpho(selector, caller);

MarketParams memory itmpAfter = morpho.idToMarketParams(id);
assert(Id.unwrap(itmpBefore.id()) == Id.unwrap(itmpAfter.id()));
}
}
Loading