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

Formal Verification #5

Merged
merged 26 commits into from
Feb 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
861141d
ERC20 mock
ElliotFriedman Feb 20, 2024
e36aa1c
repayBadDebt -> repayBadDebtWithCash
ElliotFriedman Feb 20, 2024
c17a223
remove extraneous code, simplify
ElliotFriedman Feb 20, 2024
7aff94a
repayBadDebt -> repayBadDebtWithCash
ElliotFriedman Feb 20, 2024
06be734
certora mock to fetch principal and borrow index
ElliotFriedman Feb 20, 2024
d6c8ce9
start of certora specs
ElliotFriedman Feb 20, 2024
8b0810c
.certora_internal added to gitignore
ElliotFriedman Feb 20, 2024
be620a3
remove rule sanity
ElliotFriedman Feb 20, 2024
5f3127c
remove 0xB3E6420941AcC44C2996666b4B5C998C1545fc19 from mFrax liquidat…
ElliotFriedman Feb 20, 2024
53819b0
remove 0xA5D20094Cf1Bc45B6FF1eB3B5A828865C1E8d583 from frax proposal,…
ElliotFriedman Feb 20, 2024
6a9bb83
remove 0xcD111815A4Acb5355E137B2aae6Ec3E043D57ce9 from mxcDot propose…
ElliotFriedman Feb 20, 2024
204005a
two more v's to log out information
ElliotFriedman Feb 20, 2024
decfe0e
remove 0x57e421c8a16bf0a609ef87e296cb931113a5e3ed from mxcDot propose…
ElliotFriedman Feb 20, 2024
68c4d4b
remove checks on mxcDOT users
ElliotFriedman Feb 20, 2024
33e285d
remove two v's to trim log output
ElliotFriedman Feb 20, 2024
872d460
summary and dispatcher, ghosts, additional rules and invariants
ElliotFriedman Feb 21, 2024
1dd5d24
link well address to underlying to prevent havoc
ElliotFriedman Feb 21, 2024
83c11ec
getUserBorrowInterestIndex, getInitialExchangeRateMantissa
ElliotFriedman Feb 21, 2024
272aea8
bump mETH cash 2269023468465447134524 -> 2269023504004122147416
ElliotFriedman Feb 21, 2024
4caf882
summarize borrowBalanceStored
ElliotFriedman Feb 21, 2024
6196964
mock all comptroller behavior, add cannotChangeBadDebt
ElliotFriedman Feb 22, 2024
8ebfbb6
share price specification
ElliotFriedman Feb 22, 2024
679b284
remove todo
ElliotFriedman Feb 22, 2024
a0d5f7e
testFixUserFailsNoUserBorrows, testFixUserFailsUserEqLiquidator
ElliotFriedman Feb 22, 2024
45c3fe4
rule sanity advanced -> basic
ElliotFriedman Feb 22, 2024
bc2e736
getUserBorrowSnapshot gets interest index
ElliotFriedman Feb 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ docs/

# Dotenv file
.env

.certora_internal/
21 changes: 21 additions & 0 deletions certora/confs/MErc20DelegateFixer.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"files": [
"test/mock/MockMErc20DelegateFixer.sol",
"src/Comptroller.sol",
"src/JumpRateModel.sol",
"test/mock/MockERC20.sol"
],
"verify": "MockMErc20DelegateFixer:certora/specs/MErc20DelegateFixer.spec",
"send_only": true,
"optimistic_loop": true,
"solc": "solc",
"msg": "Verification of MErc20DelegateFixer",
"rule_sanity": "basic",
"packages": [],
"link": [
"MockMErc20DelegateFixer:underlying=MockERC20",
"MockMErc20DelegateFixer:comptroller=Comptroller",
"MockMErc20DelegateFixer:interestRateModel=JumpRateModel",
"Comptroller:wellAddress=MockERC20"
]
}
16 changes: 16 additions & 0 deletions certora/confs/SharePrice.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"files": [
"test/mock/MockMErc20DelegateFixer.sol",
"test/mock/MockERC20.sol"
],
"verify": "MockMErc20DelegateFixer:certora/specs/SharePrice.spec",
"send_only": true,
"optimistic_loop": true,
"solc": "solc",
"msg": "Verification of MErc20DelegateFixer",
"rule_sanity": "basic",
"packages": [],
"link": [
"MockMErc20DelegateFixer:underlying=MockERC20"
]
}
231 changes: 231 additions & 0 deletions certora/specs/MErc20DelegateFixer.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
using MockERC20 as token;
using MockMErc20DelegateFixer as fixer;
using Comptroller as comptroller;
using JumpRateModel as jrm;

methods {
function badDebt() external returns (uint256) envfree;
function fixUser(address liquidator, address user) external;
function borrowIndex() external returns (uint256) envfree;
function totalBorrows() external returns (uint256) envfree;
function totalReserves() external returns (uint256) envfree;
function token.balanceOf(address) external returns (uint256) envfree;
function balanceOf(address) external returns (uint256) envfree;
function repayBadDebtWithCash(uint256) external;
function accrueInterest() external returns (uint256);
function exchangeRateCurrent() external returns (uint256);
function borrowBalanceCurrent(address account) external returns (uint256);
function repayBadDebtWithReserves() external envfree;
function getUserBorrowSnapshot(address user) external returns (uint256, uint256) envfree;
function getUserBorrowInterestIndex(address user) external returns (uint256) envfree;
function getInitialExchangeRateMantissa() external returns (uint256) envfree;

/// summarize these calls to prevent prover havoc
function _.isComptroller() external => DISPATCHER(true);
function _.isInterestRateModel() external => DISPATCHER(true);
function _.admin() external => DISPATCHER(true);
function _.borrowIndex() external => DISPATCHER(true);
function _._acceptImplementation() external => CONSTANT;
function comptroller._ external => NONDET;
function jrm._ external => NONDET;
}

ghost uint256 borrowIndex {
init_state axiom borrowIndex == 0;
}

ghost uint256 totalBorrows {
init_state axiom totalBorrows == 0;
}

ghost uint256 initialExchangeRateMantissa {
init_state axiom initialExchangeRateMantissa == 0;
}

hook Sstore borrowIndex uint256 newBorrowIndex (uint256 oldBorrowIndex) STORAGE {
borrowIndex = newBorrowIndex;
}

hook Sstore totalBorrows uint256 newTotalBorrows (uint256 oldTotalBorrows) STORAGE {
totalBorrows = newTotalBorrows;
}

hook Sstore initialExchangeRateMantissa uint256 newInitialExchangeRateMantissa (uint256 oldInitialExchangeRateMantissa) STORAGE {
initialExchangeRateMantissa = newInitialExchangeRateMantissa;
}

function one() returns uint256 {
return 1000000000000000000;
}

function uintMax() returns uint256 {
return 2 ^ 256 - 1;
}

/// market initialization check

invariant ghostInitialExchangeRateMantissaMirrorsStorage()
initialExchangeRateMantissa == getInitialExchangeRateMantissa();

invariant ghostBorrowIndexMirrorsStorage()
borrowIndex == borrowIndex();

invariant ghostTotalBorrowsMirrorsStorage()
totalBorrows == totalBorrows();

invariant initialBorrowIndexGteOne()
borrowIndex() != 0 => borrowIndex() >= one();

// invariant initialExchangeRateMantissaGteOne()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this commented?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because it isn't an actual invariant. initializer doesn't ensure exchange rate is gte one on initialization

// borrowIndex() != 0 => initialExchangeRateMantissa() >= one();

invariant exchangeRateGteOne(env e)
borrowIndex() >= one() => exchangeRateCurrent(e) >= one() {
preserved {
requireInvariant initialBorrowIndexGteOne();
// requireInvariant initialExchangeRateMantissaGteOne();
}
}

invariant userBorrowIndexLteExchangeRateCurrent(env e, address user)
getUserBorrowInterestIndex(user) != 0 =>
(getUserBorrowInterestIndex(user) <= exchangeRateCurrent(e) && getUserBorrowInterestIndex(user) >= one()) {
preserved {
requireInvariant ghostBorrowIndexMirrorsStorage();
}
}

rule fixUserIncreasesBadDebt(env e) {
address user;
address liquidator;
uint256 principle;
uint256 interestIndex;

principle, interestIndex = getUserBorrowSnapshot(user);

uint256 badDebt = badDebt();
uint256 liquidatorBalance = balanceOf(liquidator);
uint256 userBalance = balanceOf(user);
uint256 borrowBalance = borrowBalanceCurrent(e, user);

fixUser(e, liquidator, user);

uint256 badDebtAfter = badDebt();

assert badDebtAfter >= badDebt, "bad debt decreased from fixing a user";
assert balanceOf(user) == 0, "user balance not zero";
assert (liquidatorBalance + userBalance == to_mathint(balanceOf(liquidator))), "liquidator balance not increased by user balance";
assert borrowBalanceCurrent(e, user) == 0, "user borrow balance not zero";
assert interestIndex >= one() => (badDebt + borrowBalance == to_mathint(badDebtAfter)), "bad debt not increased by user borrow amt";
}

rule fixingUserDoesNotChangeSharePrice(env e) {
address user;
address liquidator;

uint256 startingSharePrice = exchangeRateCurrent(e);

fixUser(e, liquidator, user);

assert exchangeRateCurrent(e) == startingSharePrice, "share price should not change fixing user";
}

rule repayBadDebtDecreasesBadDebt(env e, uint256 repayAmount) {
require e.msg.sender != fixer;

uint256 badDebt = badDebt();
uint256 userBalance = token.balanceOf(e.msg.sender);
uint256 mTokenBalance = token.balanceOf(fixer);
uint256 startingSharePrice = exchangeRateCurrent(e);

repayBadDebtWithCash(e, repayAmount);

uint256 badDebtAfter = badDebt();

assert repayAmount != 0 => badDebtAfter < badDebt, "bad debt did not decrease from repaying";
assert badDebtAfter <= badDebt, "bad debt increased from repaying";
assert to_mathint(token.balanceOf(fixer)) == mTokenBalance + repayAmount, "underlying balance did not increase";
assert badDebt - repayAmount == to_mathint(badDebt()), "bad debt not decreased by repay amt";
assert exchangeRateCurrent(e) == startingSharePrice, "share price should not change repaying bad debt";
}

rule badDebtRules(method f, env e, calldataarg args)
filtered {
f ->
f.selector == sig:fixUser(address,address).selector ||
f.selector == sig:repayBadDebtWithCash(uint256).selector ||
f.selector == sig:repayBadDebtWithReserves().selector
} {
require e.msg.sender != fixer;

uint256 startingSharePrice = exchangeRateCurrent(e);
uint256 startingBadDebt = badDebt();
uint256 startingCash = token.balanceOf(fixer);

f(e, args);

uint256 endingBadDebt = badDebt();

assert startingCash + startingBadDebt <= to_mathint(uintMax()) =>
exchangeRateCurrent(e) == startingSharePrice,
"share price should not change repaying bad debt";

assert (endingBadDebt > startingBadDebt) =>
(f.selector == sig:fixUser(address,address).selector),
"bad debt should only increase when fixing users";

assert (startingBadDebt >= endingBadDebt) =>
(f.selector == sig:repayBadDebtWithCash(uint256).selector ||
f.selector == sig:repayBadDebtWithReserves().selector),
"bad debt should only increase when fixing users";
}

rule cannotChangeBadDebt(method f, env e, calldataarg args)
filtered {
f ->
!f.isView &&
f.selector != sig:fixUser(address,address).selector &&
f.selector != sig:repayBadDebtWithCash(uint256).selector &&
f.selector != sig:repayBadDebtWithReserves().selector
} {
require e.msg.sender != fixer;

uint256 startingBadDebt = badDebt();

f(e, args);

uint256 endingBadDebt = badDebt();

assert endingBadDebt == startingBadDebt,
"bad debt should not change";
}

rule repayBadDebtWithReserves(env e) {
uint256 startingReserves = totalReserves();
uint256 startingBadDebt = badDebt();

repayBadDebtWithReserves();

uint256 endingReserves = totalReserves();
uint256 endingBadDebt = badDebt();

assert (startingReserves >= startingBadDebt) =>
(endingBadDebt == 0),
"bad debt not fully paid off";

assert (startingReserves < startingBadDebt) =>
(to_mathint(endingBadDebt) == startingBadDebt - startingReserves),
"bad debt not paid off by reserve amount";
}

rule repayBadDebtWithReservesDoesNotChangeSharePrice(env e) {
require e.msg.sender != fixer;

uint256 startingSharePrice = exchangeRateCurrent(e);

repayBadDebtWithReserves();

uint256 endingSharePrice = exchangeRateCurrent(e);

assert endingSharePrice == startingSharePrice, "share price should remain unchanged";
}