From 930fb5f73f178aa163f0936699c1422b969c48c5 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Mon, 17 Jul 2023 11:46:04 +0200 Subject: [PATCH 001/204] feat: initial Certora development --- .gitignore | 3 +++ certora/scripts/verifyBlue.sh | 12 ++++++++++++ certora/specs/Blue.spec | 17 +++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100755 certora/scripts/verifyBlue.sh create mode 100644 certora/specs/Blue.spec diff --git a/.gitignore b/.gitignore index 6f198470a..98487917b 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,9 @@ docs/ # Node modules /node_modules +# Certora +.certora** + # Hardhat /types /cache_hardhat diff --git a/certora/scripts/verifyBlue.sh b/certora/scripts/verifyBlue.sh new file mode 100755 index 000000000..da52a288d --- /dev/null +++ b/certora/scripts/verifyBlue.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +certoraRun \ + src/Blue.sol \ + --verify Blue:certora/specs/Blue.spec \ + --solc_allow_path src \ + --solc solc \ + --loop_iter 3 \ + --optimistic_loop \ + --msg "Blue" \ + --send_only \ + "$@" diff --git a/certora/specs/Blue.spec b/certora/specs/Blue.spec new file mode 100644 index 000000000..fb864b4af --- /dev/null +++ b/certora/specs/Blue.spec @@ -0,0 +1,17 @@ +methods { + function supply(Blue.Market, uint256 amount) external; + + + function _.borrowRate(Blue.Market) external returns (uint256) => DISPATCH(true); + + function _.safeTransfer(address, uint256) internal returns (bool) envfree => DISPATCH(true); + function _.safeTransferFrom(address, address, uint256) internal returns (bool) envfree => DISPATCH(true); +} + +rule supplyRevertZero(Blue.Market market) { + env e; + + supply@withrevert(market, 0); + + assert lastReverted; +} From 1b982a15d2f12fd8f7e0a897330a5ca29acfb379 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 8 Aug 2023 15:26:01 +0200 Subject: [PATCH 002/204] refactor: update to new version of CVL --- certora/specs/Blue.spec | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/certora/specs/Blue.spec b/certora/specs/Blue.spec index fb864b4af..2081f6f2e 100644 --- a/certora/specs/Blue.spec +++ b/certora/specs/Blue.spec @@ -1,17 +1,17 @@ methods { - function supply(Blue.Market, uint256 amount) external; + function supply(Blue.Market, uint256, address, bytes) external; + function _.borrowRate(Blue.Market) external => DISPATCHER(true); - function _.borrowRate(Blue.Market) external returns (uint256) => DISPATCH(true); - - function _.safeTransfer(address, uint256) internal returns (bool) envfree => DISPATCH(true); - function _.safeTransferFrom(address, address, uint256) internal returns (bool) envfree => DISPATCH(true); + // function _.safeTransfer(address, uint256) internal => DISPATCHER(true); + // function _.safeTransferFrom(address, address, uint256) internal => DISPATCHER(true); } rule supplyRevertZero(Blue.Market market) { env e; + bytes b; - supply@withrevert(market, 0); + supply@withrevert(e, market, 0, e.msg.sender, b); assert lastReverted; } From 4ea4a4d19c108711763ea851ef60b688019903ab Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 9 Aug 2023 15:41:53 +0200 Subject: [PATCH 003/204] fix: hot fix waiting for CVL update --- src/Blue.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Blue.sol b/src/Blue.sol index 660ffd91c..82ca70311 100644 --- a/src/Blue.sol +++ b/src/Blue.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.21; +pragma solidity 0.8.19; import { IBlueLiquidateCallback, From b8a2d0d12ae59881bda7a3c8cdb28a32d1676806 Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Sun, 13 Aug 2023 23:01:46 +0200 Subject: [PATCH 004/204] New rules for checking totalShare and ratio --- certora/harness/MorphoHarness.sol | 31 ++++++++++++++++ certora/scripts/verifyBlue.sh | 8 ++-- certora/specs/Blue.spec | 61 +++++++++++++++++++++++++++++-- 3 files changed, 92 insertions(+), 8 deletions(-) create mode 100644 certora/harness/MorphoHarness.sol diff --git a/certora/harness/MorphoHarness.sol b/certora/harness/MorphoHarness.sol new file mode 100644 index 000000000..b40f8c688 --- /dev/null +++ b/certora/harness/MorphoHarness.sol @@ -0,0 +1,31 @@ +pragma solidity 0.8.19; + +import "../../src/Morpho.sol"; +import "../../src/libraries/SharesMathLib.sol"; + + +contract MorphoHarness is Morpho { + constructor(address newOwner) Morpho(newOwner) { + + } + + function getVirtualTotalSupply(Id id) external view returns (uint256) { + return totalSupply[id] + SharesMathLib.VIRTUAL_ASSETS; + } + + function getVirtualTotalSupplyShares(Id id) external view returns (uint256) { + return totalSupplyShares[id] + SharesMathLib.VIRTUAL_SHARES; + } + + function getTotalSupply(Id id) external view returns (uint256) { + return totalSupply[id]; + } + + function getTotalSupplyShares(Id id) external view returns (uint256) { + return totalSupplyShares[id]; + } + + function getTotalBorrowShares(Id id) external view returns (uint256) { + return totalBorrowShares[id]; + } +} \ No newline at end of file diff --git a/certora/scripts/verifyBlue.sh b/certora/scripts/verifyBlue.sh index da52a288d..5e1115374 100755 --- a/certora/scripts/verifyBlue.sh +++ b/certora/scripts/verifyBlue.sh @@ -1,12 +1,12 @@ #!/bin/sh certoraRun \ - src/Blue.sol \ - --verify Blue:certora/specs/Blue.spec \ + certora/harness/MorphoHarness.sol \ + --verify MorphoHarness:certora/specs/Blue.spec \ --solc_allow_path src \ - --solc solc \ + --solc solc8.19 \ --loop_iter 3 \ --optimistic_loop \ - --msg "Blue" \ + --msg "Morpho Blue" \ --send_only \ "$@" diff --git a/certora/specs/Blue.spec b/certora/specs/Blue.spec index 2081f6f2e..96818620a 100644 --- a/certora/specs/Blue.spec +++ b/certora/specs/Blue.spec @@ -1,17 +1,70 @@ methods { - function supply(Blue.Market, uint256, address, bytes) external; + function supply(MorphoHarness.Market, uint256, uint256, address, bytes) external; + function getVirtualTotalSupply(MorphoHarness.Id) external returns uint256 envfree; + function getVirtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; + function getTotalSupply(MorphoHarness.Id) external returns uint256 envfree; + function getTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; + function getTotalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; - function _.borrowRate(Blue.Market) external => DISPATCHER(true); + function _.borrowRate(MorphoHarness.Market) external => DISPATCHER(true); // function _.safeTransfer(address, uint256) internal => DISPATCHER(true); // function _.safeTransferFrom(address, address, uint256) internal => DISPATCHER(true); } -rule supplyRevertZero(Blue.Market market) { +ghost mapping(MorphoHarness.Id => mathint) sumSupplyShares +{ + init_state axiom (forall MorphoHarness.Id id. sumSupplyShares[id] == 0); +} +ghost mapping(MorphoHarness.Id => mathint) sumBorrowShares +{ + init_state axiom (forall MorphoHarness.Id id. sumBorrowShares[id] == 0); +} +ghost mapping(MorphoHarness.Id => mathint) sumCollateral +{ + init_state axiom (forall MorphoHarness.Id id. sumCollateral[id] == 0); +} + +hook Sstore supplyShares[KEY MorphoHarness.Id id][KEY address owner] uint256 newShares (uint256 oldShares) STORAGE { + sumSupplyShares[id] = sumSupplyShares[id] - oldShares + newShares; +} + +hook Sstore borrowShares[KEY MorphoHarness.Id id][KEY address owner] uint256 newShares (uint256 oldShares) STORAGE { + sumBorrowShares[id] = sumBorrowShares[id] - oldShares + newShares; +} + +hook Sstore collateral[KEY MorphoHarness.Id id][KEY address owner] uint256 newAmount (uint256 oldAmount) STORAGE { + sumCollateral[id] = sumCollateral[id] - oldAmount + newAmount; +} + +invariant sumSupplySharesCorrect(MorphoHarness.Id id) + to_mathint(getTotalSupplyShares(id)) == sumSupplyShares[id]; +invariant sumBorrowSharesCorrect(MorphoHarness.Id id) + to_mathint(getTotalBorrowShares(id)) == sumBorrowShares[id]; + +rule whatDoesNotIncreaseRatio(MorphoHarness.Id id) { + mathint assetsBefore = getVirtualTotalSupply(id); + mathint sharesBefore = getVirtualTotalSupplyShares(id); + + method f; + env e; + calldataarg args; + + f(e,args); + + mathint assetsAfter = getVirtualTotalSupply(id); + mathint sharesAfter = getVirtualTotalSupplyShares(id); + + // check if assetsBefore/shareBefore <= assetsAfter / sharesAfter; + assert assetsBefore * sharesAfter <= assetsAfter * sharesBefore; +} + + +rule supplyRevertZero(MorphoHarness.Market market) { env e; bytes b; - supply@withrevert(e, market, 0, e.msg.sender, b); + supply@withrevert(e, market, 0, 0, e.msg.sender, b); assert lastReverted; } From b74a28efe0f2747cefe46247c962109f498e6fd4 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Mon, 14 Aug 2023 08:57:09 +0200 Subject: [PATCH 005/204] chore: change formatting --- certora/harness/MorphoHarness.sol | 7 ++----- certora/scripts/verifyBlue.sh | 1 - 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/certora/harness/MorphoHarness.sol b/certora/harness/MorphoHarness.sol index b40f8c688..e85bd58e7 100644 --- a/certora/harness/MorphoHarness.sol +++ b/certora/harness/MorphoHarness.sol @@ -3,12 +3,9 @@ pragma solidity 0.8.19; import "../../src/Morpho.sol"; import "../../src/libraries/SharesMathLib.sol"; - contract MorphoHarness is Morpho { - constructor(address newOwner) Morpho(newOwner) { + constructor(address newOwner) Morpho(newOwner) {} - } - function getVirtualTotalSupply(Id id) external view returns (uint256) { return totalSupply[id] + SharesMathLib.VIRTUAL_ASSETS; } @@ -28,4 +25,4 @@ contract MorphoHarness is Morpho { function getTotalBorrowShares(Id id) external view returns (uint256) { return totalBorrowShares[id]; } -} \ No newline at end of file +} diff --git a/certora/scripts/verifyBlue.sh b/certora/scripts/verifyBlue.sh index 5e1115374..603ab075c 100755 --- a/certora/scripts/verifyBlue.sh +++ b/certora/scripts/verifyBlue.sh @@ -4,7 +4,6 @@ certoraRun \ certora/harness/MorphoHarness.sol \ --verify MorphoHarness:certora/specs/Blue.spec \ --solc_allow_path src \ - --solc solc8.19 \ --loop_iter 3 \ --optimistic_loop \ --msg "Morpho Blue" \ From 75716b274dd340de558735212969df268dfdaacb Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Mon, 14 Aug 2023 10:51:20 +0200 Subject: [PATCH 006/204] feat: start only enlabled lltv --- certora/harness/MorphoHarness.sol | 5 +++++ certora/specs/Blue.spec | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/certora/harness/MorphoHarness.sol b/certora/harness/MorphoHarness.sol index e85bd58e7..ae468d7e3 100644 --- a/certora/harness/MorphoHarness.sol +++ b/certora/harness/MorphoHarness.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.19; import "../../src/Morpho.sol"; import "../../src/libraries/SharesMathLib.sol"; +import "../../src/libraries/MarketLib.sol"; contract MorphoHarness is Morpho { constructor(address newOwner) Morpho(newOwner) {} @@ -25,4 +26,8 @@ contract MorphoHarness is Morpho { function getTotalBorrowShares(Id id) external view returns (uint256) { return totalBorrowShares[id]; } + + function toId(Market memory market) external pure returns (Id) { + return MarketLib.id(market); + } } diff --git a/certora/specs/Blue.spec b/certora/specs/Blue.spec index 96818620a..a8fbff720 100644 --- a/certora/specs/Blue.spec +++ b/certora/specs/Blue.spec @@ -6,8 +6,12 @@ methods { function getTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; function getTotalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; + function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; + function isLltvEnabled(uint256) external returns bool envfree; + function _.borrowRate(MorphoHarness.Market) external => DISPATCHER(true); + function toId(MorphoHarness.Market) external returns MorphoHarness.Id envfree; // function _.safeTransfer(address, uint256) internal => DISPATCHER(true); // function _.safeTransferFrom(address, address, uint256) internal => DISPATCHER(true); } @@ -68,3 +72,14 @@ rule supplyRevertZero(MorphoHarness.Market market) { assert lastReverted; } + +rule onlyEnabledLLTV(MorphoHarness.Market market, MorphoHarness.Id id) { + env e; method f; calldataarg args; + + require id == toId(market); + require lastUpdate(id) != 0 => isLltvEnabled(market.lltv); + + f(e, args); + + assert lastUpdate(id) != 0 => isLltvEnabled(market.lltv); +} From 6bee7b8815f79a823d3d241ee0e601ec1807595b Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Mon, 14 Aug 2023 11:28:28 +0200 Subject: [PATCH 007/204] feat: only enabled irm --- certora/specs/Blue.spec | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/certora/specs/Blue.spec b/certora/specs/Blue.spec index a8fbff720..3d64488dd 100644 --- a/certora/specs/Blue.spec +++ b/certora/specs/Blue.spec @@ -8,6 +8,7 @@ methods { function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; function isLltvEnabled(uint256) external returns bool envfree; + function isIrmEnabled(address) external returns bool envfree; function _.borrowRate(MorphoHarness.Market) external => DISPATCHER(true); @@ -73,13 +74,8 @@ rule supplyRevertZero(MorphoHarness.Market market) { assert lastReverted; } -rule onlyEnabledLLTV(MorphoHarness.Market market, MorphoHarness.Id id) { - env e; method f; calldataarg args; +invariant invOnlyEnabledLltv(MorphoHarness.Market market) + lastUpdate(toId(market)) != 0 => isLltvEnabled(market.lltv); - require id == toId(market); - require lastUpdate(id) != 0 => isLltvEnabled(market.lltv); - - f(e, args); - - assert lastUpdate(id) != 0 => isLltvEnabled(market.lltv); -} +invariant invOnlyEnabledIrm(MorphoHarness.Market market) + lastUpdate(toId(market)) != 0 => isIrmEnabled(market.irm); From 12294f07050cb749240f2bfd93859cbcce3eacf2 Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Mon, 14 Aug 2023 11:46:50 +0200 Subject: [PATCH 008/204] Added borrow less supply invariant --- certora/harness/MorphoHarness.sol | 14 ++++--------- certora/specs/Blue.spec | 34 +++++++++++-------------------- 2 files changed, 16 insertions(+), 32 deletions(-) diff --git a/certora/harness/MorphoHarness.sol b/certora/harness/MorphoHarness.sol index e85bd58e7..679185ef4 100644 --- a/certora/harness/MorphoHarness.sol +++ b/certora/harness/MorphoHarness.sol @@ -4,6 +4,8 @@ import "../../src/Morpho.sol"; import "../../src/libraries/SharesMathLib.sol"; contract MorphoHarness is Morpho { + using MarketLib for Market; + constructor(address newOwner) Morpho(newOwner) {} function getVirtualTotalSupply(Id id) external view returns (uint256) { @@ -14,15 +16,7 @@ contract MorphoHarness is Morpho { return totalSupplyShares[id] + SharesMathLib.VIRTUAL_SHARES; } - function getTotalSupply(Id id) external view returns (uint256) { - return totalSupply[id]; - } - - function getTotalSupplyShares(Id id) external view returns (uint256) { - return totalSupplyShares[id]; - } - - function getTotalBorrowShares(Id id) external view returns (uint256) { - return totalBorrowShares[id]; + function getMarketId(Market memory market) external pure returns (Id) { + return market.id(); } } diff --git a/certora/specs/Blue.spec b/certora/specs/Blue.spec index 96818620a..c6bcd7e5b 100644 --- a/certora/specs/Blue.spec +++ b/certora/specs/Blue.spec @@ -2,14 +2,16 @@ methods { function supply(MorphoHarness.Market, uint256, uint256, address, bytes) external; function getVirtualTotalSupply(MorphoHarness.Id) external returns uint256 envfree; function getVirtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; - function getTotalSupply(MorphoHarness.Id) external returns uint256 envfree; - function getTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; - function getTotalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; + function totalSupply(MorphoHarness.Id) external returns uint256 envfree; + function totalBorrow(MorphoHarness.Id) external returns uint256 envfree; + function totalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; + function totalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; function _.borrowRate(MorphoHarness.Market) external => DISPATCHER(true); // function _.safeTransfer(address, uint256) internal => DISPATCHER(true); // function _.safeTransferFrom(address, address, uint256) internal => DISPATCHER(true); + } ghost mapping(MorphoHarness.Id => mathint) sumSupplyShares @@ -37,28 +39,16 @@ hook Sstore collateral[KEY MorphoHarness.Id id][KEY address owner] uint256 newAm sumCollateral[id] = sumCollateral[id] - oldAmount + newAmount; } +definition VIRTUAL_ASSETS() returns mathint = 1; +definition VIRTUAL_SHARES() returns mathint = 1000000000000000000; + invariant sumSupplySharesCorrect(MorphoHarness.Id id) - to_mathint(getTotalSupplyShares(id)) == sumSupplyShares[id]; + to_mathint(totalSupplyShares(id)) == sumSupplyShares[id]; invariant sumBorrowSharesCorrect(MorphoHarness.Id id) - to_mathint(getTotalBorrowShares(id)) == sumBorrowShares[id]; - -rule whatDoesNotIncreaseRatio(MorphoHarness.Id id) { - mathint assetsBefore = getVirtualTotalSupply(id); - mathint sharesBefore = getVirtualTotalSupplyShares(id); - - method f; - env e; - calldataarg args; - - f(e,args); - - mathint assetsAfter = getVirtualTotalSupply(id); - mathint sharesAfter = getVirtualTotalSupplyShares(id); - - // check if assetsBefore/shareBefore <= assetsAfter / sharesAfter; - assert assetsBefore * sharesAfter <= assetsAfter * sharesBefore; -} + to_mathint(totalBorrowShares(id)) == sumBorrowShares[id]; +invariant borrowLessSupply(MorphoHarness.Id id) + totalBorrow(id) <= totalSupply(id); rule supplyRevertZero(MorphoHarness.Market market) { env e; From 789917f86b4faad881ca6d890be1f0e70043ba48 Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Mon, 14 Aug 2023 16:36:25 +0200 Subject: [PATCH 009/204] Optimization for ratio computation Also fix bug in accrueInterest. --- certora/harness/MorphoHarness.sol | 8 ++++ certora/scripts/verifyBlueRatioMath.sh | 9 ++++ certora/specs/Blue.spec | 25 +++++++++-- certora/specs/BlueRatioMath.spec | 62 ++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 4 deletions(-) create mode 100755 certora/scripts/verifyBlueRatioMath.sh create mode 100644 certora/specs/BlueRatioMath.spec diff --git a/certora/harness/MorphoHarness.sol b/certora/harness/MorphoHarness.sol index 679185ef4..897525f87 100644 --- a/certora/harness/MorphoHarness.sol +++ b/certora/harness/MorphoHarness.sol @@ -19,4 +19,12 @@ contract MorphoHarness is Morpho { function getMarketId(Market memory market) external pure returns (Id) { return market.id(); } + + function mathLibMulDivUp(uint256 x, uint256 y, uint256 d) public pure returns (uint256) { + return MathLib.mulDivUp(x, y, d); + } + + function mathLibMulDivDown(uint256 x, uint256 y, uint256 d) public pure returns (uint256) { + return MathLib.mulDivDown(x, y, d); + } } diff --git a/certora/scripts/verifyBlueRatioMath.sh b/certora/scripts/verifyBlueRatioMath.sh new file mode 100755 index 000000000..c8cbf3987 --- /dev/null +++ b/certora/scripts/verifyBlueRatioMath.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +certoraRun \ + certora/harness/MorphoHarness.sol \ + --verify MorphoHarness:certora/specs/BlueRatioMath.spec \ + --solc_allow_path src \ + --msg "Morpho Ratio Math" \ + --send_only \ + "$@" diff --git a/certora/specs/Blue.spec b/certora/specs/Blue.spec index c6bcd7e5b..775025395 100644 --- a/certora/specs/Blue.spec +++ b/certora/specs/Blue.spec @@ -6,12 +6,14 @@ methods { function totalBorrow(MorphoHarness.Id) external returns uint256 envfree; function totalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; function totalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; + function fee(MorphoHarness.Id) external returns uint256 envfree; - function _.borrowRate(MorphoHarness.Market) external => DISPATCHER(true); - - // function _.safeTransfer(address, uint256) internal => DISPATCHER(true); - // function _.safeTransferFrom(address, address, uint256) internal => DISPATCHER(true); + function _.borrowRate(MorphoHarness.Market) external => HAVOC_ECF; + function mathLibMulDivUp(uint256, uint256, uint256) external returns uint256 envfree; + function mathLibMulDivDown(uint256, uint256, uint256) external returns uint256 envfree; + function _.safeTransfer(address, uint256) internal => DISPATCHER(true); + function _.safeTransferFrom(address, address, uint256) internal => DISPATCHER(true); } ghost mapping(MorphoHarness.Id => mathint) sumSupplyShares @@ -41,6 +43,10 @@ hook Sstore collateral[KEY MorphoHarness.Id id][KEY address owner] uint256 newAm definition VIRTUAL_ASSETS() returns mathint = 1; definition VIRTUAL_SHARES() returns mathint = 1000000000000000000; +definition MAX_FEE() returns mathint = 250000000000000000; + +invariant feeInRange(MorphoHarness.Id id) + to_mathint(fee(id)) <= MAX_FEE(); invariant sumSupplySharesCorrect(MorphoHarness.Id id) to_mathint(totalSupplyShares(id)) == sumSupplyShares[id]; @@ -58,3 +64,14 @@ rule supplyRevertZero(MorphoHarness.Market market) { assert lastReverted; } + +/* Check the summaries required by BlueRatioMath.spec */ +rule checkSummaryToAssetsUp(uint256 x, uint256 y, uint256 d) { + uint256 result = mathLibMulDivUp(x, y, d); + assert result * d >= x * y; +} + +rule checkSummaryToAssetsDown(uint256 x, uint256 y, uint256 d) { + uint256 result = mathLibMulDivDown(x, y, d); + assert result * d <= x * y; +} diff --git a/certora/specs/BlueRatioMath.spec b/certora/specs/BlueRatioMath.spec new file mode 100644 index 000000000..c9a0d768b --- /dev/null +++ b/certora/specs/BlueRatioMath.spec @@ -0,0 +1,62 @@ +methods { + function totalSupply(MorphoHarness.Id) external returns uint256 envfree; + function totalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; + function fee(MorphoHarness.Id) external returns uint256 envfree; + + function MathLib.mulDivDown(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivDown(a,b,c); + function MathLib.mulDivUp(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivUp(a,b,c); + function MathLib.wTaylorCompounded(uint256, uint256) internal returns uint256 => NONDET; + //function MathLib.wMulDown(uint256, uint256) internal returns uint256 => NONDET; + + //function SharesMathLib.toAssetsUp(uint256 shares, uint256 totalAssets, uint256 totalShares) internal returns uint256 => summaryToAssetsUp(shares, totalAssets, totalShares); + //function SharesMathLib.toAssetsDown(uint256 shares, uint256 totalAssets, uint256 totalShares) internal returns uint256 => summaryToAssetsDown(shares, totalAssets, totalShares); + + function _.borrowRate(MorphoHarness.Market) external => HAVOC_ECF; +} + +definition VIRTUAL_ASSETS() returns mathint = 1; +definition VIRTUAL_SHARES() returns mathint = 1000000000000000000; +definition MAX_FEE() returns mathint = 250000000000000000; + +invariant feeInRange(MorphoHarness.Id id) + to_mathint(fee(id)) <= MAX_FEE(); + +/* This is a simple overapproximative summary, stating that it rounds in the right direction. + * The summary is checked by the specification in Blue.spec. + */ +function summaryMulDivUp(uint256 x, uint256 y, uint256 d) returns uint256 { + uint256 result; + require result * d >= x * y; + return result; +} + +/* This is a simple overapproximative summary, stating that it rounds in the right direction. + * The summary is checked by the specification in Blue.spec. + */ +function summaryMulDivDown(uint256 x, uint256 y, uint256 d) returns uint256 { + uint256 result; + require result * d <= x * y; + return result; +} + +rule onlyLiquidateCanDecreasesRatio(method f) +filtered { + f -> f.selector != sig:liquidate(MorphoHarness.Market, address, uint256, bytes).selector +} +{ + MorphoHarness.Id id; + requireInvariant feeInRange(id); + + mathint assetsBefore = totalSupply(id) + VIRTUAL_ASSETS(); + mathint sharesBefore = totalSupplyShares(id) + VIRTUAL_SHARES(); + + env e; + calldataarg args; + f(e,args); + + mathint assetsAfter = totalSupply(id) + VIRTUAL_ASSETS(); + mathint sharesAfter = totalSupplyShares(id) + VIRTUAL_SHARES(); + + // check if ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter; + assert assetsBefore * sharesAfter <= assetsAfter * sharesBefore; +} From 34c1598ce80096954a297928f1dd9ebae8d47a8b Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Mon, 14 Aug 2023 18:38:45 +0200 Subject: [PATCH 010/204] fix: hardhat ci --- hardhat.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index d0e7619f0..c0136c1e2 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -32,7 +32,7 @@ const config: HardhatUserConfig = { solidity: { compilers: [ { - version: "0.8.21", + version: "0.8.19", settings: { optimizer: { enabled: true, From 799781d39c43e65fb5d2add1642a5aa1a88ccb8c Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Mon, 14 Aug 2023 18:39:09 +0200 Subject: [PATCH 011/204] feat: add Certora CI --- .github/workflows/certora.yml | 44 +++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/certora.yml diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml new file mode 100644 index 000000000..f43d9489f --- /dev/null +++ b/.github/workflows/certora.yml @@ -0,0 +1,44 @@ +name: Certora + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +jobs: + verify: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Install python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install certora + run: pip install certora-cli + + - name: Install solc + run: | + wget https://github.com/ethereum/solidity/releases/download/v0.8.19/solc-static-linux + chmod +x solc-static-linux + sudo mv solc-static-linux /usr/local/bin/solc8.19 + + - name: Verify rule ${{ matrix.script }} + run: | + echo "key length" ${#CERTORAKEY} + bash certora/scripts/${{ matrix.script }} --solc solc8.19 + env: + CERTORAKEY: ${{ secrets.CERTORAKEY }} + + strategy: + fail-fast: false + max-parallel: 4 + + matrix: + script: + - verifyBlue.sh From 6bafcf11a970de91f25fea1838d32e8488994472 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Mon, 14 Aug 2023 18:42:05 +0200 Subject: [PATCH 012/204] fix: adapt script to CI --- certora/scripts/verifyBlue.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/certora/scripts/verifyBlue.sh b/certora/scripts/verifyBlue.sh index 603ab075c..bffdad175 100755 --- a/certora/scripts/verifyBlue.sh +++ b/certora/scripts/verifyBlue.sh @@ -1,4 +1,6 @@ -#!/bin/sh +#!/bin/bash + +set -euxo pipefail certoraRun \ certora/harness/MorphoHarness.sol \ @@ -7,5 +9,4 @@ certoraRun \ --loop_iter 3 \ --optimistic_loop \ --msg "Morpho Blue" \ - --send_only \ "$@" From a0a1a2bb3d8c670805ce0d995790cc2283199dc4 Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Mon, 14 Aug 2023 19:32:29 +0200 Subject: [PATCH 013/204] Fix merge --- certora/specs/Blue.spec | 2 -- 1 file changed, 2 deletions(-) diff --git a/certora/specs/Blue.spec b/certora/specs/Blue.spec index 77e067480..5f0b8a414 100644 --- a/certora/specs/Blue.spec +++ b/certora/specs/Blue.spec @@ -20,8 +20,6 @@ methods { function mathLibMulDivUp(uint256, uint256, uint256) external returns uint256 envfree; function mathLibMulDivDown(uint256, uint256, uint256) external returns uint256 envfree; - function _.safeTransfer(address, uint256) internal => DISPATCHER(true); - function _.safeTransferFrom(address, address, uint256) internal => DISPATCHER(true); } ghost mapping(MorphoHarness.Id => mathint) sumSupplyShares From a486eafa703fffcbd3311134aa350d22949a62e1 Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Mon, 14 Aug 2023 22:44:20 +0200 Subject: [PATCH 014/204] Proof for liquidity invariant --- certora/specs/Blue.spec | 76 ++++++++++++++++++++++++++++++- src/libraries/SafeTransferLib.sol | 16 +++++-- 2 files changed, 86 insertions(+), 6 deletions(-) diff --git a/certora/specs/Blue.spec b/certora/specs/Blue.spec index 5f0b8a414..cc4659fd8 100644 --- a/certora/specs/Blue.spec +++ b/certora/specs/Blue.spec @@ -7,6 +7,9 @@ methods { function totalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; function totalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; function fee(MorphoHarness.Id) external returns uint256 envfree; + function getMarketId(MorphoHarness.Market) external returns MorphoHarness.Id envfree; + function idToMarket(MorphoHarness.Id) external returns (address, address, address, address, uint256) envfree; + function isAuthorized(address, address) external returns bool envfree; function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; function isLltvEnabled(uint256) external returns bool envfree; @@ -15,11 +18,12 @@ methods { function _.borrowRate(MorphoHarness.Market) external => HAVOC_ECF; function getMarketId(MorphoHarness.Market) external returns MorphoHarness.Id envfree; - // function _.safeTransfer(address, uint256) internal => DISPATCHER(true); - // function _.safeTransferFrom(address, address, uint256) internal => DISPATCHER(true); function mathLibMulDivUp(uint256, uint256, uint256) external returns uint256 envfree; function mathLibMulDivDown(uint256, uint256, uint256) external returns uint256 envfree; + + function SafeTransferLib.tmpSafeTransfer(address token, address to, uint256 value) internal => summarySafeTransferFrom(token, currentContract, to, value); + function SafeTransferLib.tmpSafeTransferFrom(address token, address from, address to, uint256 value) internal => summarySafeTransferFrom(token, from, to, value); } ghost mapping(MorphoHarness.Id => mathint) sumSupplyShares @@ -34,6 +38,17 @@ ghost mapping(MorphoHarness.Id => mathint) sumCollateral { init_state axiom (forall MorphoHarness.Id id. sumCollateral[id] == 0); } +ghost mapping(address => mathint) myBalances +{ + init_state axiom (forall address token. myBalances[token] == 0); +} +ghost mapping(address => mathint) expectedAmount +{ + init_state axiom (forall address token. expectedAmount[token] == 0); +} + +ghost idToBorrowable(MorphoHarness.Id) returns address; +ghost idToCollateral(MorphoHarness.Id) returns address; hook Sstore supplyShares[KEY MorphoHarness.Id id][KEY address owner] uint256 newShares (uint256 oldShares) STORAGE { sumSupplyShares[id] = sumSupplyShares[id] - oldShares + newShares; @@ -45,8 +60,30 @@ hook Sstore borrowShares[KEY MorphoHarness.Id id][KEY address owner] uint256 new hook Sstore collateral[KEY MorphoHarness.Id id][KEY address owner] uint256 newAmount (uint256 oldAmount) STORAGE { sumCollateral[id] = sumCollateral[id] - oldAmount + newAmount; + expectedAmount[idToCollateral(id)] = expectedAmount[idToCollateral(id)] - oldAmount + newAmount; +} + +hook Sstore totalSupply[KEY MorphoHarness.Id id] uint256 newAmount (uint256 oldAmount) STORAGE { + expectedAmount[idToBorrowable(id)] = expectedAmount[idToBorrowable(id)] - oldAmount + newAmount; } +hook Sstore totalBorrow[KEY MorphoHarness.Id id] uint256 newAmount (uint256 oldAmount) STORAGE { + expectedAmount[idToBorrowable(id)] = expectedAmount[idToBorrowable(id)] + oldAmount - newAmount; +} + +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); + } +} + +definition goodMarket(MorphoHarness.Market market, MorphoHarness.Id id) returns bool = + (idToBorrowable(id) == market.borrowableToken && + idToCollateral(id) == market.collateralToken); + definition VIRTUAL_ASSETS() returns mathint = 1; definition VIRTUAL_SHARES() returns mathint = 1000000000000000000; definition MAX_FEE() returns mathint = 250000000000000000; @@ -62,6 +99,41 @@ invariant sumBorrowSharesCorrect(MorphoHarness.Id id) invariant borrowLessSupply(MorphoHarness.Id id) totalBorrow(id) <= totalSupply(id); +invariant isLiquid(address token) + expectedAmount[token] <= myBalances[token] +{ + preserved supply(MorphoHarness.Market market, uint256 _a, uint256 _s, address _o, bytes _d) with (env _e) { + require goodMarket(market, getMarketId(market)); + require _e.msg.sender != currentContract; + } + preserved withdraw(MorphoHarness.Market market, uint256 _a, uint256 _s, address _o, address _r) with (env _e) { + require goodMarket(market, getMarketId(market)); + require _e.msg.sender != currentContract; + } + preserved borrow(MorphoHarness.Market market, uint256 _a, uint256 _s, address _o, address _r) with (env _e) { + require goodMarket(market, getMarketId(market)); + require _e.msg.sender != currentContract; + } + preserved repay(MorphoHarness.Market market, uint256 _a, uint256 _s, address _o, bytes _d) with (env _e) { + require goodMarket(market, getMarketId(market)); + require _e.msg.sender != currentContract; + } + preserved supplyCollateral(MorphoHarness.Market market, uint256 _a, address _o, bytes _d) with (env _e) { + require goodMarket(market, getMarketId(market)); + require _e.msg.sender != currentContract; + } + preserved withdrawCollateral(MorphoHarness.Market market, uint256 _a, address _o, address _r) with (env _e) { + require goodMarket(market, getMarketId(market)); + require _e.msg.sender != currentContract; + } + preserved liquidate(MorphoHarness.Market market, address _b, uint256 _s, bytes _d) with (env _e) { + require goodMarket(market, getMarketId(market)); + require _e.msg.sender != currentContract; + } +} +//invariant liquidOnCollateralToken(MorphoHarness.Market market) +// myBalances[market.collateralToken] <= collateral(getMarketId(market)); + rule supplyRevertZero(MorphoHarness.Market market) { env e; bytes b; diff --git a/src/libraries/SafeTransferLib.sol b/src/libraries/SafeTransferLib.sol index cfb7c2ab9..62015b1a1 100644 --- a/src/libraries/SafeTransferLib.sol +++ b/src/libraries/SafeTransferLib.sol @@ -11,20 +11,28 @@ import {IERC20} from "../interfaces/IERC20.sol"; /// @notice Library to manage tokens not fully ERC20 compliant: /// not returning a boolean for `transfer` and `transferFrom` functions. library SafeTransferLib { - function safeTransfer(IERC20 token, address to, uint256 value) internal { - (bool success, bytes memory returndata) = address(token).call(abi.encodeCall(token.transfer, (to, value))); + function tmpSafeTransfer(address token, address to, uint256 value) internal { + (bool success, bytes memory returndata) = address(token).call(abi.encodeCall(IERC20(token).transfer, (to, value))); require( success && address(token).code.length > 0 && (returndata.length == 0 || abi.decode(returndata, (bool))), ErrorsLib.TRANSFER_FAILED ); } - function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { + function tmpSafeTransferFrom(address token, address from, address to, uint256 value) internal { (bool success, bytes memory returndata) = - address(token).call(abi.encodeCall(token.transferFrom, (from, to, value))); + address(token).call(abi.encodeCall(IERC20(token).transferFrom, (from, to, value))); require( success && address(token).code.length > 0 && (returndata.length == 0 || abi.decode(returndata, (bool))), ErrorsLib.TRANSFER_FROM_FAILED ); } + + function safeTransfer(IERC20 token, address to, uint256 value) internal { + tmpSafeTransfer(address(token), to, value); + } + + function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { + tmpSafeTransferFrom(address(token), from, to, value); + } } From 294c1f509daf85f296e468cf1a4ee3ecb57dddee Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Tue, 15 Aug 2023 10:09:33 +0200 Subject: [PATCH 015/204] Use invariant for idToBorrowable and Collateral --- certora/specs/Blue.spec | 88 ++++++++++++++++++++++++++--------------- 1 file changed, 56 insertions(+), 32 deletions(-) diff --git a/certora/specs/Blue.spec b/certora/specs/Blue.spec index cc4659fd8..33773c4ea 100644 --- a/certora/specs/Blue.spec +++ b/certora/specs/Blue.spec @@ -47,8 +47,15 @@ ghost mapping(address => mathint) expectedAmount init_state axiom (forall address token. expectedAmount[token] == 0); } -ghost idToBorrowable(MorphoHarness.Id) returns address; -ghost idToCollateral(MorphoHarness.Id) returns address; +ghost mapping(MorphoHarness.Id => address) idToBorrowable; +ghost mapping(MorphoHarness.Id => address) idToCollateral; + +hook Sstore idToMarket[KEY MorphoHarness.Id id].borrowableToken address token STORAGE { + idToBorrowable[id] = token; +} +hook Sstore idToMarket[KEY MorphoHarness.Id id].collateralToken address token STORAGE { + idToCollateral[id] = token; +} hook Sstore supplyShares[KEY MorphoHarness.Id id][KEY address owner] uint256 newShares (uint256 oldShares) STORAGE { sumSupplyShares[id] = sumSupplyShares[id] - oldShares + newShares; @@ -60,15 +67,15 @@ hook Sstore borrowShares[KEY MorphoHarness.Id id][KEY address owner] uint256 new hook Sstore collateral[KEY MorphoHarness.Id id][KEY address owner] uint256 newAmount (uint256 oldAmount) STORAGE { sumCollateral[id] = sumCollateral[id] - oldAmount + newAmount; - expectedAmount[idToCollateral(id)] = expectedAmount[idToCollateral(id)] - oldAmount + newAmount; + expectedAmount[idToCollateral[id]] = expectedAmount[idToCollateral[id]] - oldAmount + newAmount; } hook Sstore totalSupply[KEY MorphoHarness.Id id] uint256 newAmount (uint256 oldAmount) STORAGE { - expectedAmount[idToBorrowable(id)] = expectedAmount[idToBorrowable(id)] - oldAmount + newAmount; + expectedAmount[idToBorrowable[id]] = expectedAmount[idToBorrowable[id]] - oldAmount + newAmount; } hook Sstore totalBorrow[KEY MorphoHarness.Id id] uint256 newAmount (uint256 oldAmount) STORAGE { - expectedAmount[idToBorrowable(id)] = expectedAmount[idToBorrowable(id)] + oldAmount - newAmount; + expectedAmount[idToBorrowable[id]] = expectedAmount[idToBorrowable[id]] + oldAmount - newAmount; } function summarySafeTransferFrom(address token, address from, address to, uint256 amount) { @@ -80,13 +87,12 @@ function summarySafeTransferFrom(address token, address from, address to, uint25 } } -definition goodMarket(MorphoHarness.Market market, MorphoHarness.Id id) returns bool = - (idToBorrowable(id) == market.borrowableToken && - idToCollateral(id) == market.collateralToken); - definition VIRTUAL_ASSETS() returns mathint = 1; definition VIRTUAL_SHARES() returns mathint = 1000000000000000000; definition MAX_FEE() returns mathint = 250000000000000000; +definition isInitialized(MorphoHarness.Id id) returns bool = + (lastUpdate(id) != 0); + invariant feeInRange(MorphoHarness.Id id) to_mathint(fee(id)) <= MAX_FEE(); @@ -99,36 +105,41 @@ invariant sumBorrowSharesCorrect(MorphoHarness.Id id) invariant borrowLessSupply(MorphoHarness.Id id) totalBorrow(id) <= totalSupply(id); +invariant marketInvariant(MorphoHarness.Market market) + isInitialized(getMarketId(market)) => + idToBorrowable[getMarketId(market)] == market.borrowableToken + && idToCollateral[getMarketId(market)] == market.collateralToken; + invariant isLiquid(address token) expectedAmount[token] <= myBalances[token] { - preserved supply(MorphoHarness.Market market, uint256 _a, uint256 _s, address _o, bytes _d) with (env _e) { - require goodMarket(market, getMarketId(market)); - require _e.msg.sender != currentContract; + preserved supply(MorphoHarness.Market market, uint256 _a, uint256 _s, address _o, bytes _d) with (env e) { + requireInvariant marketInvariant(market); + require e.msg.sender != currentContract; } - preserved withdraw(MorphoHarness.Market market, uint256 _a, uint256 _s, address _o, address _r) with (env _e) { - require goodMarket(market, getMarketId(market)); - require _e.msg.sender != currentContract; + preserved withdraw(MorphoHarness.Market market, uint256 _a, uint256 _s, address _o, address _r) with (env e) { + requireInvariant marketInvariant(market); + require e.msg.sender != currentContract; } - preserved borrow(MorphoHarness.Market market, uint256 _a, uint256 _s, address _o, address _r) with (env _e) { - require goodMarket(market, getMarketId(market)); - require _e.msg.sender != currentContract; + preserved borrow(MorphoHarness.Market market, uint256 _a, uint256 _s, address _o, address _r) with (env e) { + requireInvariant marketInvariant(market); + require e.msg.sender != currentContract; } - preserved repay(MorphoHarness.Market market, uint256 _a, uint256 _s, address _o, bytes _d) with (env _e) { - require goodMarket(market, getMarketId(market)); - require _e.msg.sender != currentContract; + preserved repay(MorphoHarness.Market market, uint256 _a, uint256 _s, address _o, bytes _d) with (env e) { + requireInvariant marketInvariant(market); + require e.msg.sender != currentContract; } - preserved supplyCollateral(MorphoHarness.Market market, uint256 _a, address _o, bytes _d) with (env _e) { - require goodMarket(market, getMarketId(market)); - require _e.msg.sender != currentContract; + preserved supplyCollateral(MorphoHarness.Market market, uint256 _a, address _o, bytes _d) with (env e) { + requireInvariant marketInvariant(market); + require e.msg.sender != currentContract; } - preserved withdrawCollateral(MorphoHarness.Market market, uint256 _a, address _o, address _r) with (env _e) { - require goodMarket(market, getMarketId(market)); - require _e.msg.sender != currentContract; + preserved withdrawCollateral(MorphoHarness.Market market, uint256 _a, address _o, address _r) with (env e) { + requireInvariant marketInvariant(market); + require e.msg.sender != currentContract; } - preserved liquidate(MorphoHarness.Market market, address _b, uint256 _s, bytes _d) with (env _e) { - require goodMarket(market, getMarketId(market)); - require _e.msg.sender != currentContract; + preserved liquidate(MorphoHarness.Market market, address _b, uint256 _s, bytes _d) with (env e) { + requireInvariant marketInvariant(market); + require e.msg.sender != currentContract; } } //invariant liquidOnCollateralToken(MorphoHarness.Market market) @@ -144,10 +155,10 @@ rule supplyRevertZero(MorphoHarness.Market market) { } invariant invOnlyEnabledLltv(MorphoHarness.Market market) - lastUpdate(getMarketId(market)) != 0 => isLltvEnabled(market.lltv); + isInitialized(getMarketId(market)) => isLltvEnabled(market.lltv); invariant invOnlyEnabledIrm(MorphoHarness.Market market) - lastUpdate(getMarketId(market)) != 0 => isIrmEnabled(market.irm); + isInitialized(getMarketId(market)) => isIrmEnabled(market.irm); /* Check the summaries required by BlueRatioMath.spec */ rule checkSummaryToAssetsUp(uint256 x, uint256 y, uint256 d) { @@ -159,3 +170,16 @@ rule checkSummaryToAssetsDown(uint256 x, uint256 y, uint256 d) { uint256 result = mathLibMulDivDown(x, y, d); assert result * d <= x * y; } + +rule marketIdUnique() { + MorphoHarness.Market market1; + MorphoHarness.Market market2; + + require getMarketId(market1) == getMarketId(market2); + + assert market1.borrowableToken == market2.borrowableToken; + assert market1.collateralToken == market2.collateralToken; + assert market1.oracle == market2.oracle; + assert market1.irm == market2.irm; + assert market1.lltv == market2.lltv; +} From 6161ffd8ae65b5c11ce9fd7708c5b4ac7100fa06 Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Tue, 15 Aug 2023 10:12:16 +0200 Subject: [PATCH 016/204] Ignore certora prover directories --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 98487917b..84536674b 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ docs/ # Certora .certora** +emv-*-certora* # Hardhat /types From 41d57c54e5d885dfa627d98c674cd625528d728d Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Tue, 15 Aug 2023 10:23:56 +0200 Subject: [PATCH 017/204] Fix formatting --- src/libraries/SafeTransferLib.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libraries/SafeTransferLib.sol b/src/libraries/SafeTransferLib.sol index 62015b1a1..8b968374d 100644 --- a/src/libraries/SafeTransferLib.sol +++ b/src/libraries/SafeTransferLib.sol @@ -12,7 +12,8 @@ import {IERC20} from "../interfaces/IERC20.sol"; /// not returning a boolean for `transfer` and `transferFrom` functions. library SafeTransferLib { function tmpSafeTransfer(address token, address to, uint256 value) internal { - (bool success, bytes memory returndata) = address(token).call(abi.encodeCall(IERC20(token).transfer, (to, value))); + (bool success, bytes memory returndata) = + address(token).call(abi.encodeCall(IERC20(token).transfer, (to, value))); require( success && address(token).code.length > 0 && (returndata.length == 0 || abi.decode(returndata, (bool))), ErrorsLib.TRANSFER_FAILED From 7f4158956d91838e1f99109077e24269289c3a46 Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Tue, 15 Aug 2023 10:59:12 +0200 Subject: [PATCH 018/204] Clean-ups as suggested by QGarchery --- certora/scripts/verifyBlueRatioMathSummary.sh | 7 +++++++ certora/specs/Blue.spec | 18 +++--------------- certora/specs/BlueRatioMath.spec | 8 ++------ certora/specs/BlueRatioMathSummary.spec | 15 +++++++++++++++ 4 files changed, 27 insertions(+), 21 deletions(-) create mode 100755 certora/scripts/verifyBlueRatioMathSummary.sh create mode 100644 certora/specs/BlueRatioMathSummary.spec diff --git a/certora/scripts/verifyBlueRatioMathSummary.sh b/certora/scripts/verifyBlueRatioMathSummary.sh new file mode 100755 index 000000000..f0a9305b9 --- /dev/null +++ b/certora/scripts/verifyBlueRatioMathSummary.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +certoraRun \ + certora/harness/MorphoHarness.sol \ + --verify MorphoHarness:certora/specs/BlueRatioMathSummary.spec \ + --msg "Morpho Ratio Math Summary" \ + "$@" diff --git a/certora/specs/Blue.spec b/certora/specs/Blue.spec index 33773c4ea..c1676db11 100644 --- a/certora/specs/Blue.spec +++ b/certora/specs/Blue.spec @@ -22,6 +22,7 @@ methods { function mathLibMulDivUp(uint256, uint256, uint256) external returns uint256 envfree; function mathLibMulDivDown(uint256, uint256, uint256) external returns uint256 envfree; + // Temporary workaround a bug that requires to have address instead of an interface in the signature function SafeTransferLib.tmpSafeTransfer(address token, address to, uint256 value) internal => summarySafeTransferFrom(token, currentContract, to, value); function SafeTransferLib.tmpSafeTransferFrom(address token, address from, address to, uint256 value) internal => summarySafeTransferFrom(token, from, to, value); } @@ -88,8 +89,8 @@ function summarySafeTransferFrom(address token, address from, address to, uint25 } definition VIRTUAL_ASSETS() returns mathint = 1; -definition VIRTUAL_SHARES() returns mathint = 1000000000000000000; -definition MAX_FEE() returns mathint = 250000000000000000; +definition VIRTUAL_SHARES() returns mathint = 10^18; +definition MAX_FEE() returns mathint = 10^18 * 25/100; definition isInitialized(MorphoHarness.Id id) returns bool = (lastUpdate(id) != 0); @@ -142,8 +143,6 @@ invariant isLiquid(address token) require e.msg.sender != currentContract; } } -//invariant liquidOnCollateralToken(MorphoHarness.Market market) -// myBalances[market.collateralToken] <= collateral(getMarketId(market)); rule supplyRevertZero(MorphoHarness.Market market) { env e; @@ -160,17 +159,6 @@ invariant invOnlyEnabledLltv(MorphoHarness.Market market) invariant invOnlyEnabledIrm(MorphoHarness.Market market) isInitialized(getMarketId(market)) => isIrmEnabled(market.irm); -/* Check the summaries required by BlueRatioMath.spec */ -rule checkSummaryToAssetsUp(uint256 x, uint256 y, uint256 d) { - uint256 result = mathLibMulDivUp(x, y, d); - assert result * d >= x * y; -} - -rule checkSummaryToAssetsDown(uint256 x, uint256 y, uint256 d) { - uint256 result = mathLibMulDivDown(x, y, d); - assert result * d <= x * y; -} - rule marketIdUnique() { MorphoHarness.Market market1; MorphoHarness.Market market2; diff --git a/certora/specs/BlueRatioMath.spec b/certora/specs/BlueRatioMath.spec index c9a0d768b..a7a4bf683 100644 --- a/certora/specs/BlueRatioMath.spec +++ b/certora/specs/BlueRatioMath.spec @@ -6,17 +6,13 @@ methods { function MathLib.mulDivDown(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivDown(a,b,c); function MathLib.mulDivUp(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivUp(a,b,c); function MathLib.wTaylorCompounded(uint256, uint256) internal returns uint256 => NONDET; - //function MathLib.wMulDown(uint256, uint256) internal returns uint256 => NONDET; - - //function SharesMathLib.toAssetsUp(uint256 shares, uint256 totalAssets, uint256 totalShares) internal returns uint256 => summaryToAssetsUp(shares, totalAssets, totalShares); - //function SharesMathLib.toAssetsDown(uint256 shares, uint256 totalAssets, uint256 totalShares) internal returns uint256 => summaryToAssetsDown(shares, totalAssets, totalShares); function _.borrowRate(MorphoHarness.Market) external => HAVOC_ECF; } definition VIRTUAL_ASSETS() returns mathint = 1; -definition VIRTUAL_SHARES() returns mathint = 1000000000000000000; -definition MAX_FEE() returns mathint = 250000000000000000; +definition VIRTUAL_SHARES() returns mathint = 10^18; +definition MAX_FEE() returns mathint = 10^18 * 25/100; invariant feeInRange(MorphoHarness.Id id) to_mathint(fee(id)) <= MAX_FEE(); diff --git a/certora/specs/BlueRatioMathSummary.spec b/certora/specs/BlueRatioMathSummary.spec new file mode 100644 index 000000000..419b67b1b --- /dev/null +++ b/certora/specs/BlueRatioMathSummary.spec @@ -0,0 +1,15 @@ +methods { + function mathLibMulDivUp(uint256, uint256, uint256) external returns uint256 envfree; + function mathLibMulDivDown(uint256, uint256, uint256) external returns uint256 envfree; +} + +/* Check the summaries required by BlueRatioMath.spec */ +rule checkSummaryToAssetsUp(uint256 x, uint256 y, uint256 d) { + uint256 result = mathLibMulDivUp(x, y, d); + assert result * d >= x * y; +} + +rule checkSummaryToAssetsDown(uint256 x, uint256 y, uint256 d) { + uint256 result = mathLibMulDivDown(x, y, d); + assert result * d <= x * y; +} From 1a81dfd936453aa1dc7031dbc5f9406593588e07 Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Tue, 15 Aug 2023 11:02:07 +0200 Subject: [PATCH 019/204] Remove unused method declaration --- certora/specs/Blue.spec | 3 --- 1 file changed, 3 deletions(-) diff --git a/certora/specs/Blue.spec b/certora/specs/Blue.spec index c1676db11..e45806fb1 100644 --- a/certora/specs/Blue.spec +++ b/certora/specs/Blue.spec @@ -19,9 +19,6 @@ methods { function getMarketId(MorphoHarness.Market) external returns MorphoHarness.Id envfree; - function mathLibMulDivUp(uint256, uint256, uint256) external returns uint256 envfree; - function mathLibMulDivDown(uint256, uint256, uint256) external returns uint256 envfree; - // Temporary workaround a bug that requires to have address instead of an interface in the signature function SafeTransferLib.tmpSafeTransfer(address token, address to, uint256 value) internal => summarySafeTransferFrom(token, currentContract, to, value); function SafeTransferLib.tmpSafeTransferFrom(address token, address from, address to, uint256 value) internal => summarySafeTransferFrom(token, from, to, value); From c38a3c01eec6341054e1b5c95df4b2bceb4c73b6 Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Tue, 15 Aug 2023 11:04:26 +0200 Subject: [PATCH 020/204] Update comment referencing new file --- certora/specs/BlueRatioMath.spec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certora/specs/BlueRatioMath.spec b/certora/specs/BlueRatioMath.spec index a7a4bf683..ea5a48cb1 100644 --- a/certora/specs/BlueRatioMath.spec +++ b/certora/specs/BlueRatioMath.spec @@ -18,7 +18,7 @@ invariant feeInRange(MorphoHarness.Id id) to_mathint(fee(id)) <= MAX_FEE(); /* This is a simple overapproximative summary, stating that it rounds in the right direction. - * The summary is checked by the specification in Blue.spec. + * The summary is checked by the specification in BlueRatioMathSummary.spec. */ function summaryMulDivUp(uint256 x, uint256 y, uint256 d) returns uint256 { uint256 result; @@ -27,7 +27,7 @@ function summaryMulDivUp(uint256 x, uint256 y, uint256 d) returns uint256 { } /* This is a simple overapproximative summary, stating that it rounds in the right direction. - * The summary is checked by the specification in Blue.spec. + * The summary is checked by the specification in BlueRatioMathSummary.spec. */ function summaryMulDivDown(uint256 x, uint256 y, uint256 d) returns uint256 { uint256 result; From d3d808620dc36740791c87ee075f59e68e807749 Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Tue, 15 Aug 2023 12:27:05 +0200 Subject: [PATCH 021/204] Update certora/specs/BlueRatioMathSummary.spec Co-authored-by: Quentin Garchery Signed-off-by: Jochen Hoenicke --- certora/specs/BlueRatioMathSummary.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certora/specs/BlueRatioMathSummary.spec b/certora/specs/BlueRatioMathSummary.spec index 419b67b1b..fb72c1cd1 100644 --- a/certora/specs/BlueRatioMathSummary.spec +++ b/certora/specs/BlueRatioMathSummary.spec @@ -4,7 +4,7 @@ methods { } /* Check the summaries required by BlueRatioMath.spec */ -rule checkSummaryToAssetsUp(uint256 x, uint256 y, uint256 d) { +rule checkSummaryMulDivUp(uint256 x, uint256 y, uint256 d) { uint256 result = mathLibMulDivUp(x, y, d); assert result * d >= x * y; } From 4c4e6cf141e3dab7b3d3dbb6e21297821c9a16b8 Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Tue, 15 Aug 2023 12:27:13 +0200 Subject: [PATCH 022/204] Update certora/specs/BlueRatioMathSummary.spec Co-authored-by: Quentin Garchery Signed-off-by: Jochen Hoenicke --- certora/specs/BlueRatioMathSummary.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certora/specs/BlueRatioMathSummary.spec b/certora/specs/BlueRatioMathSummary.spec index fb72c1cd1..d83492f03 100644 --- a/certora/specs/BlueRatioMathSummary.spec +++ b/certora/specs/BlueRatioMathSummary.spec @@ -9,7 +9,7 @@ rule checkSummaryMulDivUp(uint256 x, uint256 y, uint256 d) { assert result * d >= x * y; } -rule checkSummaryToAssetsDown(uint256 x, uint256 y, uint256 d) { +rule checkSummaryMulDivDown(uint256 x, uint256 y, uint256 d) { uint256 result = mathLibMulDivDown(x, y, d); assert result * d <= x * y; } From e46fd7392bb5b8c39833360cac65c8fe94e86d07 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 15 Aug 2023 13:31:52 +0200 Subject: [PATCH 023/204] chore: add blue ratio math summary to CI --- .github/workflows/certora.yml | 1 + certora/scripts/verifyBlueRatioMath.sh | 1 - certora/specs/Blue.spec | 4 ++-- certora/specs/BlueRatioMath.spec | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml index f43d9489f..ddced1d4d 100644 --- a/.github/workflows/certora.yml +++ b/.github/workflows/certora.yml @@ -42,3 +42,4 @@ jobs: matrix: script: - verifyBlue.sh + - verifyBlueRatioMathSummary.sh diff --git a/certora/scripts/verifyBlueRatioMath.sh b/certora/scripts/verifyBlueRatioMath.sh index c8cbf3987..088b34380 100755 --- a/certora/scripts/verifyBlueRatioMath.sh +++ b/certora/scripts/verifyBlueRatioMath.sh @@ -5,5 +5,4 @@ certoraRun \ --verify MorphoHarness:certora/specs/BlueRatioMath.spec \ --solc_allow_path src \ --msg "Morpho Ratio Math" \ - --send_only \ "$@" diff --git a/certora/specs/Blue.spec b/certora/specs/Blue.spec index e45806fb1..ca5681e5d 100644 --- a/certora/specs/Blue.spec +++ b/certora/specs/Blue.spec @@ -150,10 +150,10 @@ rule supplyRevertZero(MorphoHarness.Market market) { assert lastReverted; } -invariant invOnlyEnabledLltv(MorphoHarness.Market market) +invariant onlyEnabledLltv(MorphoHarness.Market market) isInitialized(getMarketId(market)) => isLltvEnabled(market.lltv); -invariant invOnlyEnabledIrm(MorphoHarness.Market market) +invariant onlyEnabledIrm(MorphoHarness.Market market) isInitialized(getMarketId(market)) => isIrmEnabled(market.irm); rule marketIdUnique() { diff --git a/certora/specs/BlueRatioMath.spec b/certora/specs/BlueRatioMath.spec index ea5a48cb1..8461f973e 100644 --- a/certora/specs/BlueRatioMath.spec +++ b/certora/specs/BlueRatioMath.spec @@ -16,7 +16,7 @@ definition MAX_FEE() returns mathint = 10^18 * 25/100; invariant feeInRange(MorphoHarness.Id id) to_mathint(fee(id)) <= MAX_FEE(); - + /* This is a simple overapproximative summary, stating that it rounds in the right direction. * The summary is checked by the specification in BlueRatioMathSummary.spec. */ @@ -35,7 +35,7 @@ function summaryMulDivDown(uint256 x, uint256 y, uint256 d) returns uint256 { return result; } -rule onlyLiquidateCanDecreasesRatio(method f) +rule onlyLiquidateCanDecreasesRatio(method f) filtered { f -> f.selector != sig:liquidate(MorphoHarness.Market, address, uint256, bytes).selector } From 898abf90e0ecabaf9fe0572c7e719f0cc58d0fad Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 16 Aug 2023 10:13:08 +0200 Subject: [PATCH 024/204] feat: add withdraw and withdrawCollateral exit liquidity --- .github/workflows/certora.yml | 1 + certora/scripts/verifyBlueExitLiquidity.sh | 12 ++++++ certora/specs/BlueExitLiquidity.spec | 43 ++++++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100755 certora/scripts/verifyBlueExitLiquidity.sh create mode 100644 certora/specs/BlueExitLiquidity.spec diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml index ddced1d4d..a7c2cbebd 100644 --- a/.github/workflows/certora.yml +++ b/.github/workflows/certora.yml @@ -43,3 +43,4 @@ jobs: script: - verifyBlue.sh - verifyBlueRatioMathSummary.sh + - verifyBlueExitLiquidity.sh diff --git a/certora/scripts/verifyBlueExitLiquidity.sh b/certora/scripts/verifyBlueExitLiquidity.sh new file mode 100755 index 000000000..4aa9877ad --- /dev/null +++ b/certora/scripts/verifyBlueExitLiquidity.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -euxo pipefail + +certoraRun \ + certora/harness/MorphoHarness.sol \ + --verify MorphoHarness:certora/specs/BlueExitLiquidity.spec \ + --solc_allow_path src \ + --loop_iter 3 \ + --optimistic_loop \ + --msg "Morpho Blue Exit Liquidity" \ + "$@" diff --git a/certora/specs/BlueExitLiquidity.spec b/certora/specs/BlueExitLiquidity.spec new file mode 100644 index 000000000..a6f1c478b --- /dev/null +++ b/certora/specs/BlueExitLiquidity.spec @@ -0,0 +1,43 @@ +methods { + function withdraw(MorphoHarness.Market, uint256, uint256, address, address) external returns (uint256, uint256); + function withdrawCollateral(MorphoHarness.Market, uint256, address, address) external returns (uint256, uint256); + + + function supplyShares(MorphoHarness.Id, address) external returns uint256 envfree; + function getVirtualTotalSupply(MorphoHarness.Id) external returns uint256 envfree; + function getVirtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; + function mathLibMulDivDown(uint256, uint256, uint256) external returns uint256 envfree; + function collateral(MorphoHarness.Id, address) external returns uint256 envfree; + + function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; + + function getMarketId(MorphoHarness.Market) external returns MorphoHarness.Id envfree; +} + +rule withdrawLiquidity(MorphoHarness.Market market, uint256 assets, uint256 shares, address onBehalf, address receiver) { + env e; + MorphoHarness.Id id = getMarketId(market); + + require lastUpdate(id) == e.block.timestamp; + + uint256 initialShares = supplyShares(id, onBehalf); + uint256 initialTotalSupply = getVirtualTotalSupply(id); + uint256 initialTotalSupplyShares = getVirtualTotalSupplyShares(id); + uint256 owedAssets = mathLibMulDivDown(initialShares, initialTotalSupply, initialTotalSupplyShares); + + uint256 withdrawnAssets; + withdrawnAssets, _ = withdraw(e, market, assets, shares, onBehalf, receiver); + + assert withdrawnAssets <= owedAssets; +} + +rule withdrawCollateralLiquidity(MorphoHarness.Market market, uint256 assets, address onBehalf, address receiver) { + env e; + MorphoHarness.Id id = getMarketId(market); + + uint256 initialCollateral = collateral(id, onBehalf); + + withdrawCollateral(e, market, assets, onBehalf, receiver); + + assert assets <= initialCollateral; +} From b69659679086373f8f54a15a1739f7de16cf463b Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 16 Aug 2023 10:38:16 +0200 Subject: [PATCH 025/204] feat: repay liquidity --- certora/harness/MorphoHarness.sol | 8 ++++++++ certora/specs/BlueExitLiquidity.spec | 25 ++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/certora/harness/MorphoHarness.sol b/certora/harness/MorphoHarness.sol index 3b40f10d0..1a0f1fe40 100644 --- a/certora/harness/MorphoHarness.sol +++ b/certora/harness/MorphoHarness.sol @@ -17,6 +17,14 @@ contract MorphoHarness is Morpho { return totalSupplyShares[id] + SharesMathLib.VIRTUAL_SHARES; } + function getVirtualTotalBorrow(Id id) external view returns (uint256) { + return totalBorrow[id] + SharesMathLib.VIRTUAL_ASSETS; + } + + function getVirtualTotalBorrowShares(Id id) external view returns (uint256) { + return totalBorrowShares[id] + SharesMathLib.VIRTUAL_SHARES; + } + function getMarketId(Market memory market) external pure returns (Id) { return market.id(); } diff --git a/certora/specs/BlueExitLiquidity.spec b/certora/specs/BlueExitLiquidity.spec index a6f1c478b..e6bad3375 100644 --- a/certora/specs/BlueExitLiquidity.spec +++ b/certora/specs/BlueExitLiquidity.spec @@ -6,11 +6,15 @@ methods { function supplyShares(MorphoHarness.Id, address) external returns uint256 envfree; function getVirtualTotalSupply(MorphoHarness.Id) external returns uint256 envfree; function getVirtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; - function mathLibMulDivDown(uint256, uint256, uint256) external returns uint256 envfree; + function borrowShares(MorphoHarness.Id, address) external returns uint256 envfree; + function getVirtualTotalBorrow(MorphoHarness.Id) external returns uint256 envfree; + function getVirtualTotalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; function collateral(MorphoHarness.Id, address) external returns uint256 envfree; function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; + function mathLibMulDivDown(uint256, uint256, uint256) external returns uint256 envfree; + function mathLibMulDivUp(uint256, uint256, uint256) external returns uint256 envfree; function getMarketId(MorphoHarness.Market) external returns MorphoHarness.Id envfree; } @@ -41,3 +45,22 @@ rule withdrawCollateralLiquidity(MorphoHarness.Market market, uint256 assets, ad assert assets <= initialCollateral; } + +rule repayLiquidity(MorphoHarness.Market market, uint256 assets, uint256 shares, address onBehalf, bytes data) { + env e; + MorphoHarness.Id id = getMarketId(market); + + require lastUpdate(id) == e.block.timestamp; + + uint256 initialShares = borrowShares(id, onBehalf); + uint256 initialTotalBorrow = getVirtualTotalBorrow(id); + uint256 initialTotalBorrowShares = getVirtualTotalBorrowShares(id); + uint256 assetsDue = mathLibMulDivUp(initialShares, initialTotalBorrow, initialTotalBorrowShares); + + uint256 repaidAssets; + repaidAssets, _ = repay(e, market, assets, shares, onBehalf, data); + + require borrowShares(id, onBehalf) == 0; + + assert repaidAssets >= assetsDue; +} From 80ee464dc374e2b3cb3be6cde464e554fb650595 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 16 Aug 2023 10:41:48 +0200 Subject: [PATCH 026/204] refactor: remove unnecessary method declaration --- certora/specs/BlueExitLiquidity.spec | 4 ---- 1 file changed, 4 deletions(-) diff --git a/certora/specs/BlueExitLiquidity.spec b/certora/specs/BlueExitLiquidity.spec index e6bad3375..bca86c35d 100644 --- a/certora/specs/BlueExitLiquidity.spec +++ b/certora/specs/BlueExitLiquidity.spec @@ -1,8 +1,4 @@ methods { - function withdraw(MorphoHarness.Market, uint256, uint256, address, address) external returns (uint256, uint256); - function withdrawCollateral(MorphoHarness.Market, uint256, address, address) external returns (uint256, uint256); - - function supplyShares(MorphoHarness.Id, address) external returns uint256 envfree; function getVirtualTotalSupply(MorphoHarness.Id) external returns uint256 envfree; function getVirtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; From b3478916f0c4c0d2bd7f9e970ccac8fccc957f28 Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Wed, 16 Aug 2023 10:31:48 +0200 Subject: [PATCH 027/204] Reentrancy specification. Don't consider borrowRate() for reentrancy --- certora/scripts/verifyReentrancy.sh | 12 ++++++ certora/specs/Reentrancy.spec | 64 +++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100755 certora/scripts/verifyReentrancy.sh create mode 100644 certora/specs/Reentrancy.spec diff --git a/certora/scripts/verifyReentrancy.sh b/certora/scripts/verifyReentrancy.sh new file mode 100755 index 000000000..7cca8c891 --- /dev/null +++ b/certora/scripts/verifyReentrancy.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -euxo pipefail + +certoraRun \ + certora/harness/MorphoHarness.sol \ + --verify MorphoHarness:certora/specs/Reentrancy.spec \ + --prover_args '-enableStorageSplitting false' \ + --loop_iter 3 \ + --optimistic_loop \ + --msg "Check Reentrancy" \ + "$@" diff --git a/certora/specs/Reentrancy.spec b/certora/specs/Reentrancy.spec new file mode 100644 index 000000000..8b1a518da --- /dev/null +++ b/certora/specs/Reentrancy.spec @@ -0,0 +1,64 @@ +methods { + function _.borrowRate(MorphoHarness.Market market) external => summaryBorrowRate(market) expect uint256; +} + +ghost bool hasAccessedStorage; +ghost bool hasCallAfterAccessingStorage; +ghost bool hasReentrancyUnsafeCall; +ghost bool delegate_call; +ghost bool static_call; +ghost bool callIsBorrowRate; + +function summaryBorrowRate(MorphoHarness.Market market) returns uint256 { + uint256 result; + callIsBorrowRate = true; + return result; +} + +hook ALL_SSTORE(uint loc, uint v) { + hasAccessedStorage = true; + hasReentrancyUnsafeCall = hasCallAfterAccessingStorage; +} + +hook ALL_SLOAD(uint loc) uint v { + hasAccessedStorage = true; + hasReentrancyUnsafeCall = hasCallAfterAccessingStorage; +} + +hook CALL(uint g, address addr, uint value, uint argsOffset, uint argsLength, uint retOffset, uint retLength) uint rc { + if (callIsBorrowRate) { + /* The calls to borrow rate are trusted and don't count */ + callIsBorrowRate = false; + } else { + hasCallAfterAccessingStorage = hasAccessedStorage; + } +} + +hook DELEGATECALL(uint g, address addr, uint argsOffset, uint argsLength, uint retOffset, uint retLength) uint rc { + delegate_call = true; +} + +hook STATICCALL(uint g, address addr, uint argsOffset, uint argsLength, uint retOffset, uint retLength) uint rc { + static_call = true; +} + +rule reentrancySafe(method f, calldataarg data, env e) { + require !callIsBorrowRate; + require !hasAccessedStorage && !hasCallAfterAccessingStorage && !hasReentrancyUnsafeCall; + f(e,data); + assert !hasReentrancyUnsafeCall, "Method is not safe for reentrancy."; +} + +/* Rules to check which methods have delegate or static calls +rule hasDelegateCalls(method f, calldataarg data, env e) { + require !delegate_call; + f(e,data); + satisfy delegate_call; +} + +rule hasStaticCalls(method f, calldataarg data, env e) { + require !static_call; + f(e,data); + satisfy static_call; +} +*/ From 520b1505efc1d92b80e2cc75e33e5a0bb8ca365b Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Wed, 16 Aug 2023 14:03:43 +0200 Subject: [PATCH 028/204] Check for no delegate calls, add workflow --- .github/workflows/certora.yml | 1 + certora/specs/Reentrancy.spec | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml index a7c2cbebd..5251b302a 100644 --- a/.github/workflows/certora.yml +++ b/.github/workflows/certora.yml @@ -44,3 +44,4 @@ jobs: - verifyBlue.sh - verifyBlueRatioMathSummary.sh - verifyBlueExitLiquidity.sh + - verifyReentrancy.sh diff --git a/certora/specs/Reentrancy.spec b/certora/specs/Reentrancy.spec index 8b1a518da..cba310b46 100644 --- a/certora/specs/Reentrancy.spec +++ b/certora/specs/Reentrancy.spec @@ -49,13 +49,13 @@ rule reentrancySafe(method f, calldataarg data, env e) { assert !hasReentrancyUnsafeCall, "Method is not safe for reentrancy."; } -/* Rules to check which methods have delegate or static calls -rule hasDelegateCalls(method f, calldataarg data, env e) { +rule noDelegateCalls(method f, calldataarg data, env e) { require !delegate_call; f(e,data); - satisfy delegate_call; + assert !delegate_call; } +/* This rule can be used to check which methods have static calls rule hasStaticCalls(method f, calldataarg data, env e) { require !static_call; f(e,data); From 80458ea2382aadea4bdffe1954f6e53394612106 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 16 Aug 2023 15:15:51 +0200 Subject: [PATCH 029/204] feat: add dispatch for tokens --- certora/dispatch/ERC20Bad.sol | 12 ++++++++ certora/dispatch/ERC20Good.sol | 8 +++++ certora/dispatch/ERC20USDT.sol | 54 ++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 certora/dispatch/ERC20Bad.sol create mode 100644 certora/dispatch/ERC20Good.sol create mode 100644 certora/dispatch/ERC20USDT.sol 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/ERC20USDT.sol b/certora/dispatch/ERC20USDT.sol new file mode 100644 index 000000000..f81240124 --- /dev/null +++ b/certora/dispatch/ERC20USDT.sol @@ -0,0 +1,54 @@ +// 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 _name, string _symbol, uint256 _decimals) { + name = _name; + symbol = _symbol; + decimals = _decimals; + owner = msg.sender; + } + + modifier onlyOwner() { + require(msg.sender == owner); + _; + } + + function transfer(address _to, uint256 _value) public { + balanceOf[msg.sender] -= _value; + balanceOf[_to] += _value; + Transfer(msg.sender, _to, _value); + } + + function transferFrom(address _from, address _to, uint256 _value) public { + if (allowed[_from][msg.sender] < MAX_UINT) { + allowed[_from][msg.sender] -= value; + } + balanceOf[_from] -= _value; + balanceOf[_to] -= _value; + } + + function approve(address _spender, uint256 _value) public { + require(!((_value != 0) && (allowed[msg.sender][_spender] != 0))); + + allowed[msg.sender][_spender] = _value; + } + + function allowance(address _owner, address _spender) public view returns (uint256 remaining) { + return allowed[_owner][_spender]; + } + + function mint(address _receiver, uint256 amount) public onlyOwner { + balanceOf[owner] += amount; + _totalSupply += amount; + } +} From 845984d50e28962ccb2dffe9a00d095a26273b2c Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Wed, 16 Aug 2023 16:48:41 +0200 Subject: [PATCH 030/204] workaround a bug in prover --- certora/specs/Reentrancy.spec | 1 + 1 file changed, 1 insertion(+) diff --git a/certora/specs/Reentrancy.spec b/certora/specs/Reentrancy.spec index cba310b46..cc5469e0b 100644 --- a/certora/specs/Reentrancy.spec +++ b/certora/specs/Reentrancy.spec @@ -29,6 +29,7 @@ hook CALL(uint g, address addr, uint value, uint argsOffset, uint argsLength, ui if (callIsBorrowRate) { /* The calls to borrow rate are trusted and don't count */ callIsBorrowRate = false; + hasCallAfterAccessingStorage = hasCallAfterAccessingStorage; } else { hasCallAfterAccessingStorage = hasAccessedStorage; } From e5021098012b91402a04f48ea6948fc1973dc8e9 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 16 Aug 2023 22:34:14 +0200 Subject: [PATCH 031/204] feat: add verification of summary for transfers --- certora/dispatch/ERC20USDT.sol | 8 ++-- certora/harness/TransferHarness.sol | 24 ++++++++++++ certora/scripts/verifyBlueTransferSummary.sh | 14 +++++++ certora/specs/BlueTransferSummary.spec | 39 ++++++++++++++++++++ 4 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 certora/harness/TransferHarness.sol create mode 100755 certora/scripts/verifyBlueTransferSummary.sh create mode 100644 certora/specs/BlueTransferSummary.spec diff --git a/certora/dispatch/ERC20USDT.sol b/certora/dispatch/ERC20USDT.sol index f81240124..9df9378ff 100644 --- a/certora/dispatch/ERC20USDT.sol +++ b/certora/dispatch/ERC20USDT.sol @@ -11,7 +11,7 @@ contract ERC20USDT { mapping(address => uint256) public balanceOf; mapping(address => mapping(address => uint256)) public allowed; - constructor(string _name, string _symbol, uint256 _decimals) { + constructor(string memory _name, string memory _symbol, uint256 _decimals) { name = _name; symbol = _symbol; decimals = _decimals; @@ -26,15 +26,14 @@ contract ERC20USDT { function transfer(address _to, uint256 _value) public { balanceOf[msg.sender] -= _value; balanceOf[_to] += _value; - Transfer(msg.sender, _to, _value); } function transferFrom(address _from, address _to, uint256 _value) public { if (allowed[_from][msg.sender] < MAX_UINT) { - allowed[_from][msg.sender] -= value; + allowed[_from][msg.sender] -= _value; } balanceOf[_from] -= _value; - balanceOf[_to] -= _value; + balanceOf[_to] += _value; } function approve(address _spender, uint256 _value) public { @@ -49,6 +48,5 @@ contract ERC20USDT { function mint(address _receiver, uint256 amount) public onlyOwner { balanceOf[owner] += amount; - _totalSupply += amount; } } diff --git a/certora/harness/TransferHarness.sol b/certora/harness/TransferHarness.sol new file mode 100644 index 000000000..fc435c919 --- /dev/null +++ b/certora/harness/TransferHarness.sol @@ -0,0 +1,24 @@ +// 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 returns (uint256); + function totalSupply() external returns (uint256); +} + +contract Transferer { + function doTransfer(address token, address from, address to, uint256 value) public { + IERC20Extended(token).transferFrom(from, to, value); + } + + function getBalance(address token, address user) public returns (uint256) { + return IERC20Extended(token).balanceOf(user); + } + + function getTotalSupply(address token) public returns (uint256) { + return IERC20Extended(token).totalSupply(); + } +} diff --git a/certora/scripts/verifyBlueTransferSummary.sh b/certora/scripts/verifyBlueTransferSummary.sh new file mode 100755 index 000000000..80a3be8da --- /dev/null +++ b/certora/scripts/verifyBlueTransferSummary.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +set -euxo pipefail + +certoraRun \ + certora/harness/TransferHarness.sol \ + certora/dispatch/ERC20Good.sol \ + certora/dispatch/ERC20USDT.sol \ + --verify Transferer:certora/specs/BlueTransferSummary.spec \ + --packages openzeppelin-contracts=lib/openzeppelin-contracts/contracts \ + --loop_iter 3 \ + --optimistic_loop \ + --msg "Morpho Transfer Summary" \ + "$@" diff --git a/certora/specs/BlueTransferSummary.spec b/certora/specs/BlueTransferSummary.spec new file mode 100644 index 000000000..acbcac4af --- /dev/null +++ b/certora/specs/BlueTransferSummary.spec @@ -0,0 +1,39 @@ +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 checkTransferSummary(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)); + + uint256 initialBalance = getBalance(token, currentContract); + doTransfer(token, from, to, amount); + uint256 finalBalance = getBalance(token, currentContract); + + mathint initialGhostBalance = myBalances[token]; + summarySafeTransferFrom(token, from, to, amount); + mathint finalGhostBalance = myBalances[token]; + + assert finalGhostBalance - initialGhostBalance == finalBalance - initialBalance; +} From 41aca5296f672210d45b71df083f60470a8d4559 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 16 Aug 2023 22:36:44 +0200 Subject: [PATCH 032/204] feat: add transfer summary to the CI --- .github/workflows/certora.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml index a7c2cbebd..c3c7b0807 100644 --- a/.github/workflows/certora.yml +++ b/.github/workflows/certora.yml @@ -44,3 +44,4 @@ jobs: - verifyBlue.sh - verifyBlueRatioMathSummary.sh - verifyBlueExitLiquidity.sh + - verifyBlueTransferSummary.sh From 9229f3c048b48946e763a7523260f78a61592ec8 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 16 Aug 2023 22:41:00 +0200 Subject: [PATCH 033/204] fix: install dependencies in CI --- .github/workflows/certora.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml index c001e6e25..87c24b282 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: From ffc7844cd2091b106b25056b974b6bb4b69f94d4 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 16 Aug 2023 22:43:43 +0200 Subject: [PATCH 034/204] fix: change name to TransferHarness --- certora/harness/TransferHarness.sol | 2 +- certora/scripts/verifyBlueTransferSummary.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/certora/harness/TransferHarness.sol b/certora/harness/TransferHarness.sol index fc435c919..b8382c921 100644 --- a/certora/harness/TransferHarness.sol +++ b/certora/harness/TransferHarness.sol @@ -9,7 +9,7 @@ interface IERC20Extended is IERC20 { function totalSupply() external returns (uint256); } -contract Transferer { +contract TransferHarness { function doTransfer(address token, address from, address to, uint256 value) public { IERC20Extended(token).transferFrom(from, to, value); } diff --git a/certora/scripts/verifyBlueTransferSummary.sh b/certora/scripts/verifyBlueTransferSummary.sh index 80a3be8da..1883d62fc 100755 --- a/certora/scripts/verifyBlueTransferSummary.sh +++ b/certora/scripts/verifyBlueTransferSummary.sh @@ -6,7 +6,7 @@ certoraRun \ certora/harness/TransferHarness.sol \ certora/dispatch/ERC20Good.sol \ certora/dispatch/ERC20USDT.sol \ - --verify Transferer:certora/specs/BlueTransferSummary.spec \ + --verify TransferHarness:certora/specs/BlueTransferSummary.spec \ --packages openzeppelin-contracts=lib/openzeppelin-contracts/contracts \ --loop_iter 3 \ --optimistic_loop \ From dcb6cbddb3db4051ef60a514fbb1c15ca5806e02 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 17 Aug 2023 09:38:35 +0200 Subject: [PATCH 035/204] chore: move to certora-cli-beta in the CI --- .github/workflows/certora.yml | 13 ++++++------- certora/harness/MorphoHarness.sol | 2 +- certora/specs/Blue.spec | 5 ++--- hardhat.config.ts | 2 +- src/Morpho.sol | 2 +- src/libraries/SafeTransferLib.sol | 17 ++++------------- 6 files changed, 15 insertions(+), 26 deletions(-) diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml index 87c24b282..ff5e30e51 100644 --- a/.github/workflows/certora.yml +++ b/.github/workflows/certora.yml @@ -13,9 +13,8 @@ jobs: steps: - uses: actions/checkout@v3 - - - name: Install submodules - run: git submodule update --init --recursive + with: + submodules: recursive - name: Install python uses: actions/setup-python@v4 @@ -23,18 +22,18 @@ jobs: python-version: "3.10" - name: Install certora - run: pip install certora-cli + run: pip install certora-cli-beta - name: Install solc run: | - wget https://github.com/ethereum/solidity/releases/download/v0.8.19/solc-static-linux + wget https://github.com/ethereum/solidity/releases/download/v0.8.21/solc-static-linux chmod +x solc-static-linux - sudo mv solc-static-linux /usr/local/bin/solc8.19 + sudo mv solc-static-linux /usr/local/bin/solc8.21 - name: Verify rule ${{ matrix.script }} run: | echo "key length" ${#CERTORAKEY} - bash certora/scripts/${{ matrix.script }} --solc solc8.19 + bash certora/scripts/${{ matrix.script }} --solc solc8.21 env: CERTORAKEY: ${{ secrets.CERTORAKEY }} diff --git a/certora/harness/MorphoHarness.sol b/certora/harness/MorphoHarness.sol index 1a0f1fe40..6114bcd7c 100644 --- a/certora/harness/MorphoHarness.sol +++ b/certora/harness/MorphoHarness.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.19; +pragma solidity 0.8.21; import "../../src/Morpho.sol"; import "../../src/libraries/SharesMathLib.sol"; diff --git a/certora/specs/Blue.spec b/certora/specs/Blue.spec index ca5681e5d..69e5408d5 100644 --- a/certora/specs/Blue.spec +++ b/certora/specs/Blue.spec @@ -19,9 +19,8 @@ methods { function getMarketId(MorphoHarness.Market) external returns MorphoHarness.Id envfree; - // Temporary workaround a bug that requires to have address instead of an interface in the signature - function SafeTransferLib.tmpSafeTransfer(address token, address to, uint256 value) internal => summarySafeTransferFrom(token, currentContract, to, value); - function SafeTransferLib.tmpSafeTransferFrom(address token, address from, address to, uint256 value) internal => summarySafeTransferFrom(token, from, to, value); + function SafeTransferLib.safeTransfer(address token, address to, uint256 value) internal => summarySafeTransferFrom(token, currentContract, to, value); + function SafeTransferLib.safeTransferFrom(address token, address from, address to, uint256 value) internal => summarySafeTransferFrom(token, from, to, value); } ghost mapping(MorphoHarness.Id => mathint) sumSupplyShares diff --git a/hardhat.config.ts b/hardhat.config.ts index c0136c1e2..d0e7619f0 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -32,7 +32,7 @@ const config: HardhatUserConfig = { solidity: { compilers: [ { - version: "0.8.19", + version: "0.8.21", settings: { optimizer: { enabled: true, diff --git a/src/Morpho.sol b/src/Morpho.sol index 92ff843a8..d1118f53d 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.19; +pragma solidity 0.8.21; import {Id, IMorpho, Market, Authorization, Signature} from "./interfaces/IMorpho.sol"; import {IFlashLender} from "./interfaces/IFlashLender.sol"; diff --git a/src/libraries/SafeTransferLib.sol b/src/libraries/SafeTransferLib.sol index 8b968374d..cfb7c2ab9 100644 --- a/src/libraries/SafeTransferLib.sol +++ b/src/libraries/SafeTransferLib.sol @@ -11,29 +11,20 @@ import {IERC20} from "../interfaces/IERC20.sol"; /// @notice Library to manage tokens not fully ERC20 compliant: /// not returning a boolean for `transfer` and `transferFrom` functions. library SafeTransferLib { - function tmpSafeTransfer(address token, address to, uint256 value) internal { - (bool success, bytes memory returndata) = - address(token).call(abi.encodeCall(IERC20(token).transfer, (to, value))); + function safeTransfer(IERC20 token, address to, uint256 value) internal { + (bool success, bytes memory returndata) = address(token).call(abi.encodeCall(token.transfer, (to, value))); require( success && address(token).code.length > 0 && (returndata.length == 0 || abi.decode(returndata, (bool))), ErrorsLib.TRANSFER_FAILED ); } - function tmpSafeTransferFrom(address token, address from, address to, uint256 value) internal { + function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { (bool success, bytes memory returndata) = - address(token).call(abi.encodeCall(IERC20(token).transferFrom, (from, to, value))); + address(token).call(abi.encodeCall(token.transferFrom, (from, to, value))); require( success && address(token).code.length > 0 && (returndata.length == 0 || abi.decode(returndata, (bool))), ErrorsLib.TRANSFER_FROM_FAILED ); } - - function safeTransfer(IERC20 token, address to, uint256 value) internal { - tmpSafeTransfer(address(token), to, value); - } - - function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { - tmpSafeTransferFrom(address(token), from, to, value); - } } From 7125c7d3a85ae413abf846ae533daf4692df05ea Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 17 Aug 2023 09:58:28 +0200 Subject: [PATCH 036/204] refactor: simplify transfer summary check Co-authored-by: Jochen Hoenicke Signed-off-by: Quentin Garchery --- certora/specs/BlueTransferSummary.spec | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/certora/specs/BlueTransferSummary.spec b/certora/specs/BlueTransferSummary.spec index acbcac4af..76a1fe70c 100644 --- a/certora/specs/BlueTransferSummary.spec +++ b/certora/specs/BlueTransferSummary.spec @@ -31,9 +31,7 @@ rule checkTransferSummary(address token, address from, address to, uint256 amoun doTransfer(token, from, to, amount); uint256 finalBalance = getBalance(token, currentContract); - mathint initialGhostBalance = myBalances[token]; + require myBalances[token] == initialBalance; summarySafeTransferFrom(token, from, to, amount); - mathint finalGhostBalance = myBalances[token]; - - assert finalGhostBalance - initialGhostBalance == finalBalance - initialBalance; + assert myBalances[token] == finalBalance; } From 9739986152f578e760771d63a0e3409e429db486 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 17 Aug 2023 10:18:02 +0200 Subject: [PATCH 037/204] fix: cast balance to mathint --- certora/specs/BlueTransferSummary.spec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certora/specs/BlueTransferSummary.spec b/certora/specs/BlueTransferSummary.spec index 76a1fe70c..3dc75591b 100644 --- a/certora/specs/BlueTransferSummary.spec +++ b/certora/specs/BlueTransferSummary.spec @@ -27,9 +27,9 @@ rule checkTransferSummary(address token, address from, address to, uint256 amoun require from != to => getBalance(token, from) + getBalance(token, to) <= to_mathint(getTotalSupply(token)); - uint256 initialBalance = getBalance(token, currentContract); + mathint initialBalance = getBalance(token, currentContract); doTransfer(token, from, to, amount); - uint256 finalBalance = getBalance(token, currentContract); + mathint finalBalance = getBalance(token, currentContract); require myBalances[token] == initialBalance; summarySafeTransferFrom(token, from, to, amount); From 5b9dc6379d1cd8819bf04663fc7ca716eed6e0bb Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 17 Aug 2023 10:19:23 +0200 Subject: [PATCH 038/204] feat: add dispatch token not reverting and returning a boolean --- certora/dispatch/ERC20NoRevert.sol | 55 ++++++++++++++++++++ certora/dispatch/ERC20USDT.sol | 27 +++++----- certora/scripts/verifyBlueTransferSummary.sh | 1 + 3 files changed, 71 insertions(+), 12 deletions(-) create mode 100644 certora/dispatch/ERC20NoRevert.sol 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 index 9df9378ff..a0400794d 100644 --- a/certora/dispatch/ERC20USDT.sol +++ b/certora/dispatch/ERC20USDT.sol @@ -23,30 +23,33 @@ contract ERC20USDT { _; } - function transfer(address _to, uint256 _value) public { - balanceOf[msg.sender] -= _value; - balanceOf[_to] += _value; + function _transfer(address _from, address _to, uint256 _amount) public { + balanceOf[_from] -= _amount; + balanceOf[_to] += _amount; } - function transferFrom(address _from, address _to, uint256 _value) public { + 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] -= _value; + allowed[_from][msg.sender] -= _amount; } - balanceOf[_from] -= _value; - balanceOf[_to] += _value; + _transfer(_from, _to, _amount); } - function approve(address _spender, uint256 _value) public { - require(!((_value != 0) && (allowed[msg.sender][_spender] != 0))); + function approve(address _spender, uint256 _amount) public { + require(!((_amount != 0) && (allowed[msg.sender][_spender] != 0))); - allowed[msg.sender][_spender] = _value; + 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[owner] += amount; + function mint(address _receiver, uint256 _amount) public onlyOwner { + balanceOf[_receiver] += _amount; } } diff --git a/certora/scripts/verifyBlueTransferSummary.sh b/certora/scripts/verifyBlueTransferSummary.sh index 1883d62fc..e9ae59ddb 100755 --- a/certora/scripts/verifyBlueTransferSummary.sh +++ b/certora/scripts/verifyBlueTransferSummary.sh @@ -6,6 +6,7 @@ certoraRun \ certora/harness/TransferHarness.sol \ certora/dispatch/ERC20Good.sol \ certora/dispatch/ERC20USDT.sol \ + certora/dispatch/ERC20NoRevert.sol \ --verify TransferHarness:certora/specs/BlueTransferSummary.spec \ --packages openzeppelin-contracts=lib/openzeppelin-contracts/contracts \ --loop_iter 3 \ From e0a9b50a5fe5526a1a4a0f609261b12d764e1038 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 17 Aug 2023 10:33:04 +0200 Subject: [PATCH 039/204] refactor: rename transfer to not only verify summary --- .github/workflows/certora.yml | 2 +- certora/harness/TransferHarness.sol | 12 +++++++----- ...yBlueTransferSummary.sh => verifyBlueTransfer.sh} | 2 +- .../{BlueTransferSummary.spec => BlueTransfer.spec} | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) rename certora/scripts/{verifyBlueTransferSummary.sh => verifyBlueTransfer.sh} (84%) rename certora/specs/{BlueTransferSummary.spec => BlueTransfer.spec} (94%) diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml index 87c24b282..c1c5523cd 100644 --- a/.github/workflows/certora.yml +++ b/.github/workflows/certora.yml @@ -47,5 +47,5 @@ jobs: - verifyBlue.sh - verifyBlueRatioMathSummary.sh - verifyBlueExitLiquidity.sh - - verifyBlueTransferSummary.sh + - verifyBlueTransfer.sh - verifyReentrancy.sh diff --git a/certora/harness/TransferHarness.sol b/certora/harness/TransferHarness.sol index b8382c921..e4f0d4f3c 100644 --- a/certora/harness/TransferHarness.sol +++ b/certora/harness/TransferHarness.sol @@ -5,20 +5,22 @@ import "../../src/libraries/SafeTransferLib.sol"; import "../../src/interfaces/IERC20.sol"; interface IERC20Extended is IERC20 { - function balanceOf(address) external returns (uint256); - function totalSupply() external returns (uint256); + 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 { - IERC20Extended(token).transferFrom(from, to, value); + IERC20(token).safeTransferFrom(from, to, value); } - function getBalance(address token, address user) public returns (uint256) { + function getBalance(address token, address user) public view returns (uint256) { return IERC20Extended(token).balanceOf(user); } - function getTotalSupply(address token) public returns (uint256) { + function getTotalSupply(address token) public view returns (uint256) { return IERC20Extended(token).totalSupply(); } } diff --git a/certora/scripts/verifyBlueTransferSummary.sh b/certora/scripts/verifyBlueTransfer.sh similarity index 84% rename from certora/scripts/verifyBlueTransferSummary.sh rename to certora/scripts/verifyBlueTransfer.sh index e9ae59ddb..7cddf3f51 100755 --- a/certora/scripts/verifyBlueTransferSummary.sh +++ b/certora/scripts/verifyBlueTransfer.sh @@ -7,7 +7,7 @@ certoraRun \ certora/dispatch/ERC20Good.sol \ certora/dispatch/ERC20USDT.sol \ certora/dispatch/ERC20NoRevert.sol \ - --verify TransferHarness:certora/specs/BlueTransferSummary.spec \ + --verify TransferHarness:certora/specs/BlueTransfer.spec \ --packages openzeppelin-contracts=lib/openzeppelin-contracts/contracts \ --loop_iter 3 \ --optimistic_loop \ diff --git a/certora/specs/BlueTransferSummary.spec b/certora/specs/BlueTransfer.spec similarity index 94% rename from certora/specs/BlueTransferSummary.spec rename to certora/specs/BlueTransfer.spec index 3dc75591b..8c435e88d 100644 --- a/certora/specs/BlueTransferSummary.spec +++ b/certora/specs/BlueTransfer.spec @@ -22,7 +22,7 @@ function summarySafeTransferFrom(address token, address from, address to, uint25 } } -rule checkTransferSummary(address token, address from, address to, uint256 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)); From 21fdad0579fce010534676a29112bf7c2fcedec9 Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Tue, 15 Aug 2023 18:22:35 +0200 Subject: [PATCH 040/204] Specification to check isHealthy --- certora/harness/MorphoHarness.sol | 4 +++ certora/scripts/verifyHealth.sh | 12 +++++++ certora/specs/Health.spec | 52 +++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100755 certora/scripts/verifyHealth.sh create mode 100644 certora/specs/Health.spec diff --git a/certora/harness/MorphoHarness.sol b/certora/harness/MorphoHarness.sol index 6114bcd7c..9ae163ccc 100644 --- a/certora/harness/MorphoHarness.sol +++ b/certora/harness/MorphoHarness.sol @@ -36,4 +36,8 @@ contract MorphoHarness is Morpho { function mathLibMulDivDown(uint256 x, uint256 y, uint256 d) public pure returns (uint256) { return MathLib.mulDivDown(x, y, d); } + + function isHealthy(Market memory market, address user) external view returns (bool) { + return _isHealthy(market, market.id(), user); + } } diff --git a/certora/scripts/verifyHealth.sh b/certora/scripts/verifyHealth.sh new file mode 100755 index 000000000..efcc770b8 --- /dev/null +++ b/certora/scripts/verifyHealth.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -euxo pipefail + +certoraRun \ + certora/harness/MorphoHarness.sol \ + src/mocks/OracleMock.sol \ + --verify MorphoHarness:certora/specs/Health.spec \ + --loop_iter 3 \ + --optimistic_loop \ + --msg "Morpho Health Check" \ + "$@" diff --git a/certora/specs/Health.spec b/certora/specs/Health.spec new file mode 100644 index 000000000..bf73fcb59 --- /dev/null +++ b/certora/specs/Health.spec @@ -0,0 +1,52 @@ +methods { + function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; + function isHealthy(MorphoHarness.Market, address user) external returns bool envfree; + function getMarketId(MorphoHarness.Market) external returns MorphoHarness.Id envfree; + function _.price() external => mockPrice() expect uint256; + function MathLib.mulDivDown(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivDown(a,b,c); + function MathLib.mulDivUp(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivUp(a,b,c); + function UtilsLib.min(uint256 a, uint256 b) internal returns uint256 => summaryMin(a,b); +} + +ghost uint256 lastPrice; +ghost bool priceChanged; + +function mockPrice() returns uint256 { + uint256 somePrice; + if (somePrice != lastPrice) { + priceChanged = true; + lastPrice = somePrice; + } + return somePrice; +} + +function summaryMulDivUp(uint256 x, uint256 y, uint256 d) returns uint256 { + return require_uint256((x * y + (d - 1)) / d); +} + +function summaryMulDivDown(uint256 x, uint256 y, uint256 d) returns uint256 { + return require_uint256((x * y) / d); +} + +function summaryMin(uint256 a, uint256 b) returns uint256 { + return a < b ? a : b; +} + +rule stayHealthy(method f, env e, calldataarg data) filtered { + f -> !f.isView +} { + MorphoHarness.Market market; + MorphoHarness.Id id = getMarketId(market); + address user; + + require isHealthy(market, user); + require market.lltv < 10^18; + require market.lltv > 0; + require lastUpdate(id) == e.block.timestamp; + priceChanged = false; + + f(e, data); + + bool stillHealthy = isHealthy(market, user); + assert !priceChanged => stillHealthy; +} From 08e0f30f6ea92597f4d53047da5213b540871802 Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Wed, 16 Aug 2023 17:24:16 +0200 Subject: [PATCH 041/204] Verify that Supply/Withdraw roundtrip is not profitable. --- certora/scripts/verifyDifficultMath.sh | 12 +++++ certora/specs/DifficultMath.spec | 75 ++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100755 certora/scripts/verifyDifficultMath.sh create mode 100644 certora/specs/DifficultMath.spec diff --git a/certora/scripts/verifyDifficultMath.sh b/certora/scripts/verifyDifficultMath.sh new file mode 100755 index 000000000..f775a585b --- /dev/null +++ b/certora/scripts/verifyDifficultMath.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -euxo pipefail + +certoraRun \ + certora/harness/MorphoHarness.sol \ + src/mocks/OracleMock.sol \ + --verify MorphoHarness:certora/specs/DifficultMath.spec \ + --loop_iter 3 \ + --optimistic_loop \ + --msg "Morpho Difficult Math" \ + "$@" diff --git a/certora/specs/DifficultMath.spec b/certora/specs/DifficultMath.spec new file mode 100644 index 000000000..4e448b6d3 --- /dev/null +++ b/certora/specs/DifficultMath.spec @@ -0,0 +1,75 @@ +methods { + function getMarketId(MorphoHarness.Market) external returns MorphoHarness.Id envfree; + function getVirtualTotalSupply(MorphoHarness.Id) external returns uint256 envfree; + function getVirtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; + function MathLib.mulDivDown(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivDown(a,b,c); + function MathLib.mulDivUp(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivUp(a,b,c); + function SafeTransferLib.safeTransfer(address token, address to, uint256 value) internal => NONDET; + function SafeTransferLib.safeTransferFrom(address token, address from, address to, uint256 value) internal => NONDET; + function _.onMorphoSupply(uint256 assets, bytes data) external => HAVOC_ECF; +} + +function summaryMulDivUp(uint256 x, uint256 y, uint256 d) returns uint256 { + return require_uint256((x * y + (d - 1)) / d); +} + +function summaryMulDivDown(uint256 x, uint256 y, uint256 d) returns uint256 { + return require_uint256((x * y) / d); +} + +/* There should be no profit from supply followed immediately by withdraw */ +rule supplyWithdraw() { + MorphoHarness.Market market; + uint256 assets; + uint256 shares; + uint256 suppliedAssets; + uint256 withdrawnAssets; + uint256 suppliedShares; + uint256 withdrawnShares; + address onbehalf; + address receiver; + bytes data; + env e1; + env e2; + + require e1.block.timestamp == e2.block.timestamp; + + suppliedAssets, suppliedShares = supply(e1, market, assets, shares, onbehalf, data); + + MorphoHarness.Id id = getMarketId(market); + assert suppliedAssets * (getVirtualTotalSupplyShares(id) - suppliedShares) >= suppliedShares * (getVirtualTotalSupply(id) - suppliedAssets); + assert suppliedAssets * getVirtualTotalSupplyShares(id) >= suppliedShares * getVirtualTotalSupply(id); + + withdrawnAssets, withdrawnShares = withdraw(e2, market, 0, suppliedShares, onbehalf, receiver); + + assert withdrawnShares == suppliedShares; + assert withdrawnAssets <= suppliedAssets; +} + +/* There should be no profit from withdraw followed immediately by supply */ +rule withdrawSupply() { + MorphoHarness.Market market; + uint256 assets; + uint256 shares; + uint256 suppliedAssets; + uint256 withdrawnAssets; + uint256 suppliedShares; + uint256 withdrawnShares; + address onbehalf; + address receiver; + bytes data; + env e1; + env e2; + + require e1.block.timestamp == e2.block.timestamp; + + withdrawnAssets, withdrawnShares = withdraw(e2, market, assets, shares, onbehalf, receiver); + + MorphoHarness.Id id = getMarketId(market); + assert withdrawnAssets * (getVirtualTotalSupplyShares(id) + withdrawnShares) <= withdrawnShares * (getVirtualTotalSupply(id) + withdrawnAssets); + assert withdrawnAssets * getVirtualTotalSupplyShares(id) <= withdrawnShares * getVirtualTotalSupply(id); + + suppliedAssets, suppliedShares = supply(e1, market, withdrawnAssets, 0, onbehalf, data); + + assert suppliedAssets == withdrawnAssets && withdrawnShares >= suppliedShares; +} From c0df5f7beaab8aa1e349887b052c1292e05b9b29 Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Thu, 17 Aug 2023 12:35:07 +0200 Subject: [PATCH 042/204] Added script to workflow --- .github/workflows/certora.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml index 092ae1a53..6775e9ad0 100644 --- a/.github/workflows/certora.yml +++ b/.github/workflows/certora.yml @@ -48,3 +48,4 @@ jobs: - verifyBlueExitLiquidity.sh - verifyBlueTransfer.sh - verifyReentrancy.sh + - verifyDifficultMath.sh From 43b0df35fa7fffd20a9a35b93cd624c47145b508 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 17 Aug 2023 15:29:30 +0200 Subject: [PATCH 043/204] feat: more thorough verification of the transfer lib --- certora/harness/TransferHarness.sol | 11 ++++++- certora/specs/BlueTransfer.spec | 49 +++++++++++++++++++++++++---- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/certora/harness/TransferHarness.sol b/certora/harness/TransferHarness.sol index e4f0d4f3c..1129191ef 100644 --- a/certora/harness/TransferHarness.sol +++ b/certora/harness/TransferHarness.sol @@ -6,20 +6,29 @@ import "../../src/interfaces/IERC20.sol"; interface IERC20Extended is IERC20 { function balanceOf(address) external view returns (uint256); + function allowance(address, 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 { + function doTransferFrom(address token, address from, address to, uint256 value) public { IERC20(token).safeTransferFrom(from, to, value); } + function doTransfer(address token, address to, uint256 value) public { + IERC20(token).safeTransfer(to, value); + } + function getBalance(address token, address user) public view returns (uint256) { return IERC20Extended(token).balanceOf(user); } + function getAllowance(address token, address owner, address spender) public view returns (uint256) { + return IERC20Extended(token).allowance(owner, spender); + } + function getTotalSupply(address token) public view returns (uint256) { return IERC20Extended(token).totalSupply(); } diff --git a/certora/specs/BlueTransfer.spec b/certora/specs/BlueTransfer.spec index 8c435e88d..79babb208 100644 --- a/certora/specs/BlueTransfer.spec +++ b/certora/specs/BlueTransfer.spec @@ -1,10 +1,14 @@ methods { - function doTransfer(address, address, address, uint256) external envfree; + function doTransfer(address, address, uint256) external envfree; + function doTransferFrom(address, address, address, uint256) external envfree; function getBalance(address, address) external returns (uint256) envfree; + function getAllowance(address, address, address) external returns (uint256) envfree; function getTotalSupply(address) external returns (uint256) envfree; + function _.transfer(address, uint256) external => DISPATCHER(true); function _.transferFrom(address, address, uint256) external => DISPATCHER(true); function _.balanceOf(address) external => DISPATCHER(true); + function _.allowance(address, address) external => DISPATCHER(true); function _.totalSupply() external => DISPATCHER(true); } @@ -22,16 +26,49 @@ function summarySafeTransferFrom(address token, address from, address to, uint25 } } -rule checkTransfer(address token, address from, address to, uint256 amount) { - require from == currentContract || to == currentContract; +rule checkTransferFromSummary(address token, address from, uint256 amount) { + mathint initialBalance = getBalance(token, currentContract); + require from != currentContract => initialBalance + getBalance(token, from) <= to_mathint(getTotalSupply(token)); + + doTransferFrom(token, from, currentContract, amount); + mathint finalBalance = getBalance(token, currentContract); - require from != to => getBalance(token, from) + getBalance(token, to) <= to_mathint(getTotalSupply(token)); + require myBalances[token] == initialBalance; + summarySafeTransferFrom(token, from, currentContract, amount); + assert myBalances[token] == finalBalance; +} +rule checkTransferSummary(address token, address to, uint256 amount) { mathint initialBalance = getBalance(token, currentContract); - doTransfer(token, from, to, amount); + require to != currentContract => initialBalance + getBalance(token, to) <= to_mathint(getTotalSupply(token)); + + doTransfer(token, to, amount); mathint finalBalance = getBalance(token, currentContract); require myBalances[token] == initialBalance; - summarySafeTransferFrom(token, from, to, amount); + summarySafeTransferFrom(token, currentContract, to, amount); assert myBalances[token] == finalBalance; } + +rule transferRevertCondition(address token, address to, uint256 amount) { + uint256 initialBalance = getBalance(token, currentContract); + uint256 toInitialBalance = getBalance(token, to); + require to != currentContract => initialBalance + toInitialBalance <= to_mathint(getTotalSupply(token)); + require currentContract != 0 && to != 0; + + doTransfer@withrevert(token, to, amount); + + assert lastReverted == (initialBalance < amount); +} + +rule transferFromRevertCondition(address token, address from, address to, uint256 amount) { + uint256 initialBalance = getBalance(token, from); + uint256 toInitialBalance = getBalance(token, to); + uint256 allowance = getAllowance(token, from, currentContract); + require to != from => initialBalance + toInitialBalance <= to_mathint(getTotalSupply(token)); + require from != 0 && to != 0; + + doTransferFrom@withrevert(token, from, to, amount); + + assert lastReverted == (initialBalance < amount) || allowance < amount; +} From e52b3ce574b0a282c89f0b810b865d99004b6aa2 Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Thu, 17 Aug 2023 16:48:25 +0200 Subject: [PATCH 044/204] Add missing totalSupply function. --- certora/dispatch/ERC20NoRevert.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/certora/dispatch/ERC20NoRevert.sol b/certora/dispatch/ERC20NoRevert.sol index ff93b0862..93690e715 100644 --- a/certora/dispatch/ERC20NoRevert.sol +++ b/certora/dispatch/ERC20NoRevert.sol @@ -6,6 +6,7 @@ contract ERC20NoRevert { string public symbol; uint256 public decimals; address owner; + uint256 public totalSupply; mapping(address => uint256) public balanceOf; mapping(address => mapping(address => uint256)) public allowed; @@ -35,6 +36,9 @@ contract ERC20NoRevert { } function transferFrom(address _from, address _to, uint256 _amount) public returns (bool) { + if (allowed[_from][msg.sender] < _amount) { + return false; + } allowed[_from][msg.sender] -= _amount; return _transfer(_from, _to, _amount); } @@ -51,5 +55,6 @@ contract ERC20NoRevert { function mint(address _receiver, uint256 _amount) public onlyOwner { balanceOf[_receiver] += _amount; + totalSupply += _amount; } } From c6f4614514eda540e226956255ebd97b9efc13f7 Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Thu, 17 Aug 2023 17:28:58 +0200 Subject: [PATCH 045/204] Add totalSupply to USDT, some linting. --- certora/dispatch/ERC20NoRevert.sol | 2 +- certora/dispatch/ERC20USDT.sol | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/certora/dispatch/ERC20NoRevert.sol b/certora/dispatch/ERC20NoRevert.sol index 93690e715..8904f782c 100644 --- a/certora/dispatch/ERC20NoRevert.sol +++ b/certora/dispatch/ERC20NoRevert.sol @@ -5,7 +5,7 @@ contract ERC20NoRevert { string public name; string public symbol; uint256 public decimals; - address owner; + address public owner; uint256 public totalSupply; mapping(address => uint256) public balanceOf; mapping(address => mapping(address => uint256)) public allowed; diff --git a/certora/dispatch/ERC20USDT.sol b/certora/dispatch/ERC20USDT.sol index a0400794d..95615e56e 100644 --- a/certora/dispatch/ERC20USDT.sol +++ b/certora/dispatch/ERC20USDT.sol @@ -7,7 +7,8 @@ contract ERC20USDT { string public name; string public symbol; uint256 public decimals; - address owner; + uint256 public totalSupply; + address public owner; mapping(address => uint256) public balanceOf; mapping(address => mapping(address => uint256)) public allowed; @@ -23,7 +24,7 @@ contract ERC20USDT { _; } - function _transfer(address _from, address _to, uint256 _amount) public { + function _transfer(address _from, address _to, uint256 _amount) internal { balanceOf[_from] -= _amount; balanceOf[_to] += _amount; } @@ -51,5 +52,6 @@ contract ERC20USDT { function mint(address _receiver, uint256 _amount) public onlyOwner { balanceOf[_receiver] += _amount; + totalSupply += _amount; } } From df8e27f65f9050f62d75bb06e8a3329fca088a10 Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Wed, 16 Aug 2023 10:32:28 +0200 Subject: [PATCH 046/204] More generic rules --- certora/specs/Blue.spec | 126 +++++++++++++++++++++++++++++++++++++- certora/specs/Health.spec | 36 ++++++++++- 2 files changed, 159 insertions(+), 3 deletions(-) diff --git a/certora/specs/Blue.spec b/certora/specs/Blue.spec index 69e5408d5..cc598dd25 100644 --- a/certora/specs/Blue.spec +++ b/certora/specs/Blue.spec @@ -1,9 +1,11 @@ methods { - function supply(MorphoHarness.Market, uint256, uint256, address, bytes) external; function getVirtualTotalSupply(MorphoHarness.Id) external returns uint256 envfree; function getVirtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; function totalSupply(MorphoHarness.Id) external returns uint256 envfree; function totalBorrow(MorphoHarness.Id) external returns uint256 envfree; + function supplyShares(MorphoHarness.Id, address user) external returns uint256 envfree; + function borrowShares(MorphoHarness.Id, address user) external returns uint256 envfree; + function collateral(MorphoHarness.Id, address user) external returns uint256 envfree; function totalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; function totalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; function fee(MorphoHarness.Id) external returns uint256 envfree; @@ -11,6 +13,7 @@ methods { function idToMarket(MorphoHarness.Id) external returns (address, address, address, address, uint256) envfree; function isAuthorized(address, address) external returns bool envfree; + function isHealthy(MorphoHarness.Market, address user) external returns bool envfree; function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; function isLltvEnabled(uint256) external returns bool envfree; function isIrmEnabled(address) external returns bool envfree; @@ -167,3 +170,124 @@ rule marketIdUnique() { assert market1.irm == market2.irm; assert market1.lltv == market2.lltv; } + +rule onlyUserCanAuthorizeWithoutSig(method f, calldataarg data) +filtered { + f -> !f.isView && f.selector != sig:setAuthorizationWithSig(MorphoHarness.Authorization memory, MorphoHarness.Signature calldata).selector +} +{ + address user; + address someone; + env e; + + require user != e.msg.sender; + bool authorizedBefore = isAuthorized(user, someone); + + f(e, data); + + assert isAuthorized(user, someone) == authorizedBefore; +} + +rule supplyMovesTokensAndIncreasesShares() { + MorphoHarness.Market market; + uint256 assets; + uint256 shares; + uint256 suppliedAssets; + uint256 suppliedShares; + address onbehalf; + bytes data; + MorphoHarness.Id id = getMarketId(market); + env e; + + require e.msg.sender != currentContract; + require lastUpdate(id) == e.block.timestamp; + + mathint sharesBefore = supplyShares(id, onbehalf); + mathint balanceBefore = myBalances[market.borrowableToken]; + + suppliedAssets, suppliedShares = supply(e, market, assets, shares, onbehalf, data); + assert assets != 0 => suppliedAssets == assets && shares == 0; + assert assets == 0 => suppliedShares == shares && shares != 0; + + mathint sharesAfter = supplyShares(id, onbehalf); + mathint balanceAfter = myBalances[market.borrowableToken]; + assert sharesAfter == sharesBefore + suppliedShares; + assert balanceAfter == balanceBefore + suppliedAssets; +} + +rule userCannotLoseSupplyShares(method f, calldataarg data) +filtered { + f -> !f.isView +} +{ + MorphoHarness.Market market; + uint256 assets; + uint256 shares; + uint256 suppliedAssets; + uint256 suppliedShares; + address user; + MorphoHarness.Id id = getMarketId(market); + env e; + + require !isAuthorized(user, e.msg.sender); + require user != e.msg.sender; + + mathint sharesBefore = supplyShares(id, user); + + f(e, data); + + mathint sharesAfter = supplyShares(id, user); + assert sharesAfter >= sharesBefore; +} + +rule userCannotGainBorrowShares(method f, calldataarg data) +filtered { + f -> !f.isView +} +{ + MorphoHarness.Market market; + uint256 assets; + uint256 shares; + uint256 suppliedAssets; + uint256 suppliedShares; + address user; + MorphoHarness.Id id = getMarketId(market); + env e; + + require !isAuthorized(user, e.msg.sender); + require user != e.msg.sender; + + mathint sharesBefore = borrowShares(id, user); + + f(e, data); + + mathint sharesAfter = borrowShares(id, user); + assert sharesAfter <= sharesBefore; +} + + +rule userWithoutBorrowCannotLoseCollateral(method f, calldataarg data) +filtered { + f -> !f.isView +} +{ + MorphoHarness.Market market; + uint256 assets; + uint256 shares; + uint256 suppliedAssets; + uint256 suppliedShares; + address user; + MorphoHarness.Id id = getMarketId(market); + env e; + + require !isAuthorized(user, e.msg.sender); + require user != e.msg.sender; + require borrowShares(id, user) == 0; + mathint collateralBefore = collateral(id, user); + + f(e, data); + + mathint collateralAfter = collateral(id, user); + assert borrowShares(id, user) == 0; + assert collateralAfter >= collateralBefore; +} diff --git a/certora/specs/Health.spec b/certora/specs/Health.spec index bf73fcb59..116e908ee 100644 --- a/certora/specs/Health.spec +++ b/certora/specs/Health.spec @@ -1,6 +1,8 @@ methods { function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; + function collateral(MorphoHarness.Id, address) external returns uint256 envfree; function isHealthy(MorphoHarness.Market, address user) external returns bool envfree; + function isAuthorized(address, address user) external returns bool envfree; function getMarketId(MorphoHarness.Market) external returns MorphoHarness.Id envfree; function _.price() external => mockPrice() expect uint256; function MathLib.mulDivDown(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivDown(a,b,c); @@ -32,9 +34,11 @@ function summaryMin(uint256 a, uint256 b) returns uint256 { return a < b ? a : b; } -rule stayHealthy(method f, env e, calldataarg data) filtered { +rule stayHealthy(method f, env e, calldataarg data) +filtered { f -> !f.isView -} { +} +{ MorphoHarness.Market market; MorphoHarness.Id id = getMarketId(market); address user; @@ -50,3 +54,31 @@ rule stayHealthy(method f, env e, calldataarg data) filtered { bool stillHealthy = isHealthy(market, user); assert !priceChanged => stillHealthy; } + +rule healthyUserCannotLoseCollateral(method f, calldataarg data) +filtered { + f -> !f.isView +} +{ + MorphoHarness.Market market; + uint256 assets; + uint256 shares; + uint256 suppliedAssets; + uint256 suppliedShares; + address user; + MorphoHarness.Id id = getMarketId(market); + env e; + + require !isAuthorized(user, e.msg.sender); + require user != e.msg.sender; + require lastUpdate(id) == e.block.timestamp; + require isHealthy(market, user); + mathint collateralBefore = collateral(id, user); + priceChanged = false; + + f(e, data); + + require !priceChanged; + mathint collateralAfter = collateral(id, user); + assert collateralAfter >= collateralBefore; +} From f3fc7079b9c0acf6d785bdc12f2e9a0e26b44a11 Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Thu, 17 Aug 2023 21:57:02 +0200 Subject: [PATCH 047/204] Rule for withdrawing everything --- certora/specs/Blue.spec | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/certora/specs/Blue.spec b/certora/specs/Blue.spec index cc598dd25..4c2a537d6 100644 --- a/certora/specs/Blue.spec +++ b/certora/specs/Blue.spec @@ -1,6 +1,7 @@ methods { function getVirtualTotalSupply(MorphoHarness.Id) external returns uint256 envfree; function getVirtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; + function supplyShares(MorphoHarness.Id, address user) external returns uint256 envfree; function totalSupply(MorphoHarness.Id) external returns uint256 envfree; function totalBorrow(MorphoHarness.Id) external returns uint256 envfree; function supplyShares(MorphoHarness.Id, address user) external returns uint256 envfree; @@ -291,3 +292,38 @@ filtered { assert borrowShares(id, user) == 0; assert collateralAfter >= collateralBefore; } + +rule noTimeTravel(method f, env e, calldataarg data) filtered { + f -> !f.isView +} { + MorphoHarness.Id id; + require lastUpdate(id) <= e.block.timestamp; + f(e, data); + assert lastUpdate(id) <= e.block.timestamp; +} + +rule canWithdrawAll() { + MorphoHarness.Market market; + uint256 withdrawnAssets; + uint256 withdrawnShares; + address receiver; + env e; + + MorphoHarness.Id id = getMarketId(market); + uint256 shares = supplyShares(id, e.msg.sender); + + require isInitialized(id); + require e.msg.sender != 0; + require receiver != 0; + require e.msg.value == 0; + require shares > 0; + require totalBorrow(id) == 0; + require lastUpdate(id) <= e.block.timestamp; + require shares < totalSupplyShares(id); + require totalSupplyShares(id) < 10^40 && totalSupply(id) < 10^30; + + withdrawnAssets, withdrawnShares = withdraw@withrevert(e, market, 0, shares, e.msg.sender, receiver); + + assert withdrawnShares == shares; + assert !lastReverted, "Can withdraw all assets if nobody borrows"; +} From 35531ea75da8ff8cefa7a88f3257dea623222633 Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Fri, 18 Aug 2023 10:49:27 +0200 Subject: [PATCH 048/204] Remove doubled line --- certora/specs/Blue.spec | 1 - 1 file changed, 1 deletion(-) diff --git a/certora/specs/Blue.spec b/certora/specs/Blue.spec index 4c2a537d6..199ec72f9 100644 --- a/certora/specs/Blue.spec +++ b/certora/specs/Blue.spec @@ -1,7 +1,6 @@ methods { function getVirtualTotalSupply(MorphoHarness.Id) external returns uint256 envfree; function getVirtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; - function supplyShares(MorphoHarness.Id, address user) external returns uint256 envfree; function totalSupply(MorphoHarness.Id) external returns uint256 envfree; function totalBorrow(MorphoHarness.Id) external returns uint256 envfree; function supplyShares(MorphoHarness.Id, address user) external returns uint256 envfree; From d35e97a59b25130b24d9cbde24de22aeef4bd748 Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Tue, 15 Aug 2023 18:20:28 +0200 Subject: [PATCH 049/204] Added rule for borrow ratio --- certora/specs/BlueRatioMath.spec | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/certora/specs/BlueRatioMath.spec b/certora/specs/BlueRatioMath.spec index 8461f973e..7684c1581 100644 --- a/certora/specs/BlueRatioMath.spec +++ b/certora/specs/BlueRatioMath.spec @@ -1,7 +1,10 @@ methods { function totalSupply(MorphoHarness.Id) external returns uint256 envfree; function totalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; + function totalBorrow(MorphoHarness.Id) external returns uint256 envfree; + function totalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; function fee(MorphoHarness.Id) external returns uint256 envfree; + function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; function MathLib.mulDivDown(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivDown(a,b,c); function MathLib.mulDivUp(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivUp(a,b,c); @@ -35,9 +38,9 @@ function summaryMulDivDown(uint256 x, uint256 y, uint256 d) returns uint256 { return result; } -rule onlyLiquidateCanDecreasesRatio(method f) +rule onlyLiquidateCanDecreaseRatio(method f) filtered { - f -> f.selector != sig:liquidate(MorphoHarness.Market, address, uint256, bytes).selector + f -> !f.isView && f.selector != sig:liquidate(MorphoHarness.Market, address, uint256, bytes).selector } { MorphoHarness.Id id; @@ -56,3 +59,26 @@ filtered { // check if ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter; assert assetsBefore * sharesAfter <= assetsAfter * sharesBefore; } + +rule onlyAccrueInterestsCanIncreaseBorrowRatio(method f) +filtered { + f -> !f.isView +} +{ + MorphoHarness.Id id; + requireInvariant feeInRange(id); + + mathint assetsBefore = totalBorrow(id) + VIRTUAL_ASSETS(); + mathint sharesBefore = totalBorrowShares(id) + VIRTUAL_SHARES(); + + env e; + calldataarg args; + require lastUpdate(id) == e.block.timestamp; + f(e,args); + + mathint assetsAfter = totalBorrow(id) + VIRTUAL_ASSETS(); + mathint sharesAfter = totalBorrowShares(id) + VIRTUAL_SHARES(); + + // check if ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter; + assert assetsBefore * sharesAfter >= assetsAfter * sharesBefore; +} From 01fa73cdade8c1395ef9769d8e466140e8138691 Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Thu, 17 Aug 2023 10:41:33 +0200 Subject: [PATCH 050/204] Add a summary for accrueInterests --- certora/harness/MorphoHarness.sol | 13 ++ .../scripts/verifyBlueRatioMathInterests.sh | 7 + certora/specs/BlueRatioMath.spec | 27 ++++ certora/specs/BlueRatioMathInterests.spec | 124 ++++++++++++++++++ 4 files changed, 171 insertions(+) create mode 100755 certora/scripts/verifyBlueRatioMathInterests.sh create mode 100644 certora/specs/BlueRatioMathInterests.spec diff --git a/certora/harness/MorphoHarness.sol b/certora/harness/MorphoHarness.sol index 9ae163ccc..ee9396651 100644 --- a/certora/harness/MorphoHarness.sol +++ b/certora/harness/MorphoHarness.sol @@ -25,6 +25,19 @@ contract MorphoHarness is Morpho { return totalBorrowShares[id] + SharesMathLib.VIRTUAL_SHARES; } + // Setter functions for summarizing accrueInterests + function setTotalSupply(Id id, uint256 newValue) external { + totalSupply[id] = newValue; + } + + function setTotalSupplyShares(Id id, uint256 newValue) external { + totalSupplyShares[id] = newValue; + } + + function setTotalBorrow(Id id, uint256 newValue) external { + totalBorrow[id] = newValue; + } + function getMarketId(Market memory market) external pure returns (Id) { return market.id(); } diff --git a/certora/scripts/verifyBlueRatioMathInterests.sh b/certora/scripts/verifyBlueRatioMathInterests.sh new file mode 100755 index 000000000..1ca3fd344 --- /dev/null +++ b/certora/scripts/verifyBlueRatioMathInterests.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +certoraRun \ + certora/harness/MorphoHarness.sol \ + --verify MorphoHarness:certora/specs/BlueRatioMathInterests.spec \ + --msg "Morpho accrueInterests properties" \ + "$@" diff --git a/certora/specs/BlueRatioMath.spec b/certora/specs/BlueRatioMath.spec index 7684c1581..2b29afd00 100644 --- a/certora/specs/BlueRatioMath.spec +++ b/certora/specs/BlueRatioMath.spec @@ -1,7 +1,11 @@ methods { + function getMarketId(MorphoHarness.Market) external returns MorphoHarness.Id envfree; function totalSupply(MorphoHarness.Id) external returns uint256 envfree; function totalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; function totalBorrow(MorphoHarness.Id) external returns uint256 envfree; + function setTotalSupply(MorphoHarness.Id, uint256) external envfree; + function setTotalSupplyShares(MorphoHarness.Id, uint256) external envfree; + function setTotalBorrow(MorphoHarness.Id, uint256) external envfree; function totalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; function fee(MorphoHarness.Id) external returns uint256 envfree; function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; @@ -11,6 +15,7 @@ methods { function MathLib.wTaylorCompounded(uint256, uint256) internal returns uint256 => NONDET; function _.borrowRate(MorphoHarness.Market) external => HAVOC_ECF; + function Morpho._accrueInterests(MorphoHarness.Market memory market, MorphoHarness.Id id) internal with (env e) => summaryAccrueInterests(id, e.block.timestamp); } definition VIRTUAL_ASSETS() returns mathint = 1; @@ -20,6 +25,28 @@ definition MAX_FEE() returns mathint = 10^18 * 25/100; invariant feeInRange(MorphoHarness.Id id) to_mathint(fee(id)) <= MAX_FEE(); +function summaryAccrueInterests(/*MorphoHarness.Market market, */MorphoHarness.Id id, uint256 timestamp) { + //assert id == getMarketId(market); + if (lastUpdate(id) < timestamp) { + uint256 interests; + + require interests >= 0; + uint256 oldSupply = totalSupply(id); + uint256 oldSupplyShares = totalSupplyShares(id); + uint256 newBorrow = require_uint256(totalBorrow(id) + interests); + uint256 newSupply = require_uint256(totalSupply(id) + interests); + uint256 newSupplyShares; + require newSupplyShares >= totalSupplyShares(id); + + require (VIRTUAL_ASSETS() + oldSupply) * (VIRTUAL_SHARES() + newSupplyShares) <= + (VIRTUAL_ASSETS() + newSupply) * (VIRTUAL_SHARES() + oldSupplyShares); + + setTotalSupplyShares(id, newSupplyShares); + setTotalSupply(id, newSupply); + setTotalBorrow(id, newBorrow); + } +} + /* This is a simple overapproximative summary, stating that it rounds in the right direction. * The summary is checked by the specification in BlueRatioMathSummary.spec. */ diff --git a/certora/specs/BlueRatioMathInterests.spec b/certora/specs/BlueRatioMathInterests.spec new file mode 100644 index 000000000..a83d74471 --- /dev/null +++ b/certora/specs/BlueRatioMathInterests.spec @@ -0,0 +1,124 @@ +methods { + function getMarketId(MorphoHarness.Market) external returns MorphoHarness.Id envfree; + function totalSupply(MorphoHarness.Id) external returns uint256 envfree; + function totalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; + function totalBorrow(MorphoHarness.Id) external returns uint256 envfree; + function totalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; + function fee(MorphoHarness.Id) external returns uint256 envfree; + function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; + + function MathLib.mulDivDown(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivDown(a,b,c); + function MathLib.mulDivUp(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivUp(a,b,c); + function MathLib.wTaylorCompounded(uint256, uint256) internal returns uint256 => NONDET; + + function _.borrowRate(MorphoHarness.Market) external => HAVOC_ECF; +} + +definition VIRTUAL_ASSETS() returns mathint = 1; +definition VIRTUAL_SHARES() returns mathint = 10^18; +definition MAX_FEE() returns mathint = 10^18 * 25/100; + +invariant feeInRange(MorphoHarness.Id id) + to_mathint(fee(id)) <= MAX_FEE(); + +/* This is a simple overapproximative summary, stating that it rounds in the right direction. + * The summary is checked by the specification in BlueRatioMathSummary.spec. + */ +function summaryMulDivUp(uint256 x, uint256 y, uint256 d) returns uint256 { + uint256 result; + require result * d >= x * y; + return result; +} + +/* This is a simple overapproximative summary, stating that it rounds in the right direction. + * The summary is checked by the specification in BlueRatioMathSummary.spec. + */ +function summaryMulDivDown(uint256 x, uint256 y, uint256 d) returns uint256 { + uint256 result; + require result * d <= x * y; + return result; +} + +rule checkAccrueInterestsSummary() +{ + MorphoHarness.Market market; + MorphoHarness.Id id = getMarketId(market); + MorphoHarness.Id otherId; + + requireInvariant feeInRange(id); + require otherId != id; + + uint256 oldSupply = totalSupply(id); + uint256 oldSupplyShares = totalSupplyShares(id); + uint256 oldBorrow = totalBorrow(id); + + uint256 oldSupplyOther = totalSupply(otherId); + uint256 oldSupplySharesOther = totalSupplyShares(otherId); + uint256 oldBorrowOther = totalBorrow(otherId); + + env e; + accrueInterests(e, market); + + uint256 newSupply = totalSupply(id); + uint256 newSupplyShares = totalSupplyShares(id); + uint256 newBorrow = totalBorrow(id); + + uint256 newSupplyOther = totalSupply(otherId); + uint256 newSupplySharesOther = totalSupplyShares(otherId); + uint256 newBorrowOther = totalBorrow(otherId); + + assert oldSupplyOther == newSupplyOther; + assert oldSupplySharesOther == newSupplySharesOther; + assert oldBorrowOther == newBorrowOther; + + mathint interests = newSupply - oldSupply; + + assert interests >= 0; + assert to_mathint(newBorrow) == oldBorrow + interests; + assert to_mathint(newSupply) == oldSupply + interests; + assert newSupplyShares >= oldSupplyShares; + + assert (VIRTUAL_ASSETS() + oldSupply) * (VIRTUAL_SHARES() + newSupplyShares) <= + (VIRTUAL_ASSETS() + newSupply) * (VIRTUAL_SHARES() + oldSupplyShares); +} + +rule accrueInterestsIncreasesRatio() +{ + MorphoHarness.Market market; + MorphoHarness.Id id; + requireInvariant feeInRange(id); + + mathint assetsBefore = totalSupply(id) + VIRTUAL_ASSETS(); + mathint sharesBefore = totalSupplyShares(id) + VIRTUAL_SHARES(); + + // we actually check it for every market, not just for id. + env e; + accrueInterests(e, market); + + mathint assetsAfter = totalSupply(id) + VIRTUAL_ASSETS(); + mathint sharesAfter = totalSupplyShares(id) + VIRTUAL_SHARES(); + + // check if ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter; + assert assetsBefore * sharesAfter <= assetsAfter * sharesBefore; +} + +rule accrueInterestsIncreasesBorrowRatio() +{ + MorphoHarness.Market market; + MorphoHarness.Id id; + requireInvariant feeInRange(id); + + mathint assetsBefore = totalBorrow(id) + VIRTUAL_ASSETS(); + mathint sharesBefore = totalBorrowShares(id) + VIRTUAL_SHARES(); + + // we actually check it for every market, not just for id. + env e; + accrueInterests(e, market); + + mathint assetsAfter = totalBorrow(id) + VIRTUAL_ASSETS(); + mathint sharesAfter = totalBorrowShares(id) + VIRTUAL_SHARES(); + + // check if ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter; + assert assetsBefore * sharesAfter <= assetsAfter * sharesBefore; +} + From 2f9caca8f952e05990560fcccd366d201dc07c21 Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Fri, 18 Aug 2023 10:53:41 +0200 Subject: [PATCH 051/204] Split verification of ratio increases in two parts - Revert the summary for accrueInterests - include the accrueInterests increases in BlueRatioMath.sh - change prover args, which is needed for the accrueInterest rules --- certora/harness/MorphoHarness.sol | 13 -- certora/scripts/verifyBlueRatioMath.sh | 2 +- .../scripts/verifyBlueRatioMathInterests.sh | 7 - certora/specs/BlueRatioMath.spec | 73 +++++++---- certora/specs/BlueRatioMathInterests.spec | 124 ------------------ 5 files changed, 47 insertions(+), 172 deletions(-) delete mode 100755 certora/scripts/verifyBlueRatioMathInterests.sh delete mode 100644 certora/specs/BlueRatioMathInterests.spec diff --git a/certora/harness/MorphoHarness.sol b/certora/harness/MorphoHarness.sol index ee9396651..9ae163ccc 100644 --- a/certora/harness/MorphoHarness.sol +++ b/certora/harness/MorphoHarness.sol @@ -25,19 +25,6 @@ contract MorphoHarness is Morpho { return totalBorrowShares[id] + SharesMathLib.VIRTUAL_SHARES; } - // Setter functions for summarizing accrueInterests - function setTotalSupply(Id id, uint256 newValue) external { - totalSupply[id] = newValue; - } - - function setTotalSupplyShares(Id id, uint256 newValue) external { - totalSupplyShares[id] = newValue; - } - - function setTotalBorrow(Id id, uint256 newValue) external { - totalBorrow[id] = newValue; - } - function getMarketId(Market memory market) external pure returns (Id) { return market.id(); } diff --git a/certora/scripts/verifyBlueRatioMath.sh b/certora/scripts/verifyBlueRatioMath.sh index 088b34380..a075e30a2 100755 --- a/certora/scripts/verifyBlueRatioMath.sh +++ b/certora/scripts/verifyBlueRatioMath.sh @@ -3,6 +3,6 @@ certoraRun \ certora/harness/MorphoHarness.sol \ --verify MorphoHarness:certora/specs/BlueRatioMath.spec \ - --solc_allow_path src \ --msg "Morpho Ratio Math" \ + --prover_args '-smt_hashingScheme plaininjectivity' \ "$@" diff --git a/certora/scripts/verifyBlueRatioMathInterests.sh b/certora/scripts/verifyBlueRatioMathInterests.sh deleted file mode 100755 index 1ca3fd344..000000000 --- a/certora/scripts/verifyBlueRatioMathInterests.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -certoraRun \ - certora/harness/MorphoHarness.sol \ - --verify MorphoHarness:certora/specs/BlueRatioMathInterests.spec \ - --msg "Morpho accrueInterests properties" \ - "$@" diff --git a/certora/specs/BlueRatioMath.spec b/certora/specs/BlueRatioMath.spec index 2b29afd00..3604a2bf4 100644 --- a/certora/specs/BlueRatioMath.spec +++ b/certora/specs/BlueRatioMath.spec @@ -3,9 +3,6 @@ methods { function totalSupply(MorphoHarness.Id) external returns uint256 envfree; function totalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; function totalBorrow(MorphoHarness.Id) external returns uint256 envfree; - function setTotalSupply(MorphoHarness.Id, uint256) external envfree; - function setTotalSupplyShares(MorphoHarness.Id, uint256) external envfree; - function setTotalBorrow(MorphoHarness.Id, uint256) external envfree; function totalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; function fee(MorphoHarness.Id) external returns uint256 envfree; function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; @@ -15,7 +12,6 @@ methods { function MathLib.wTaylorCompounded(uint256, uint256) internal returns uint256 => NONDET; function _.borrowRate(MorphoHarness.Market) external => HAVOC_ECF; - function Morpho._accrueInterests(MorphoHarness.Market memory market, MorphoHarness.Id id) internal with (env e) => summaryAccrueInterests(id, e.block.timestamp); } definition VIRTUAL_ASSETS() returns mathint = 1; @@ -25,28 +21,6 @@ definition MAX_FEE() returns mathint = 10^18 * 25/100; invariant feeInRange(MorphoHarness.Id id) to_mathint(fee(id)) <= MAX_FEE(); -function summaryAccrueInterests(/*MorphoHarness.Market market, */MorphoHarness.Id id, uint256 timestamp) { - //assert id == getMarketId(market); - if (lastUpdate(id) < timestamp) { - uint256 interests; - - require interests >= 0; - uint256 oldSupply = totalSupply(id); - uint256 oldSupplyShares = totalSupplyShares(id); - uint256 newBorrow = require_uint256(totalBorrow(id) + interests); - uint256 newSupply = require_uint256(totalSupply(id) + interests); - uint256 newSupplyShares; - require newSupplyShares >= totalSupplyShares(id); - - require (VIRTUAL_ASSETS() + oldSupply) * (VIRTUAL_SHARES() + newSupplyShares) <= - (VIRTUAL_ASSETS() + newSupply) * (VIRTUAL_SHARES() + oldSupplyShares); - - setTotalSupplyShares(id, newSupplyShares); - setTotalSupply(id, newSupply); - setTotalBorrow(id, newBorrow); - } -} - /* This is a simple overapproximative summary, stating that it rounds in the right direction. * The summary is checked by the specification in BlueRatioMathSummary.spec. */ @@ -65,6 +39,47 @@ function summaryMulDivDown(uint256 x, uint256 y, uint256 d) returns uint256 { return result; } +rule accrueInterestsIncreasesRatio() +{ + MorphoHarness.Market market; + MorphoHarness.Id id; + requireInvariant feeInRange(id); + + mathint assetsBefore = totalSupply(id) + VIRTUAL_ASSETS(); + mathint sharesBefore = totalSupplyShares(id) + VIRTUAL_SHARES(); + + // we actually check the property for every market, not just for id. + env e; + accrueInterests(e, market); + + mathint assetsAfter = totalSupply(id) + VIRTUAL_ASSETS(); + mathint sharesAfter = totalSupplyShares(id) + VIRTUAL_SHARES(); + + // check if ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter; + assert assetsBefore * sharesAfter <= assetsAfter * sharesBefore; +} + +rule accrueInterestsIncreasesBorrowRatio() +{ + MorphoHarness.Market market; + MorphoHarness.Id id; + requireInvariant feeInRange(id); + + mathint assetsBefore = totalBorrow(id) + VIRTUAL_ASSETS(); + mathint sharesBefore = totalBorrowShares(id) + VIRTUAL_SHARES(); + + // we actually check it for every market, not just for id. + env e; + accrueInterests(e, market); + + mathint assetsAfter = totalBorrow(id) + VIRTUAL_ASSETS(); + mathint sharesAfter = totalBorrowShares(id) + VIRTUAL_SHARES(); + + // check if ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter; + assert assetsBefore * sharesAfter <= assetsAfter * sharesBefore; +} + + rule onlyLiquidateCanDecreaseRatio(method f) filtered { f -> !f.isView && f.selector != sig:liquidate(MorphoHarness.Market, address, uint256, bytes).selector @@ -75,8 +90,11 @@ filtered { mathint assetsBefore = totalSupply(id) + VIRTUAL_ASSETS(); mathint sharesBefore = totalSupplyShares(id) + VIRTUAL_SHARES(); - env e; + // interest is checked separately by the rules above + // here we assume interest has already been accumulated for this block + require lastUpdate(id) == e.block.timestamp; + calldataarg args; f(e,args); @@ -100,6 +118,7 @@ filtered { env e; calldataarg args; + // interests would increase borrow ratio, so we need to assume no time passes. require lastUpdate(id) == e.block.timestamp; f(e,args); diff --git a/certora/specs/BlueRatioMathInterests.spec b/certora/specs/BlueRatioMathInterests.spec deleted file mode 100644 index a83d74471..000000000 --- a/certora/specs/BlueRatioMathInterests.spec +++ /dev/null @@ -1,124 +0,0 @@ -methods { - function getMarketId(MorphoHarness.Market) external returns MorphoHarness.Id envfree; - function totalSupply(MorphoHarness.Id) external returns uint256 envfree; - function totalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; - function totalBorrow(MorphoHarness.Id) external returns uint256 envfree; - function totalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; - function fee(MorphoHarness.Id) external returns uint256 envfree; - function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; - - function MathLib.mulDivDown(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivDown(a,b,c); - function MathLib.mulDivUp(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivUp(a,b,c); - function MathLib.wTaylorCompounded(uint256, uint256) internal returns uint256 => NONDET; - - function _.borrowRate(MorphoHarness.Market) external => HAVOC_ECF; -} - -definition VIRTUAL_ASSETS() returns mathint = 1; -definition VIRTUAL_SHARES() returns mathint = 10^18; -definition MAX_FEE() returns mathint = 10^18 * 25/100; - -invariant feeInRange(MorphoHarness.Id id) - to_mathint(fee(id)) <= MAX_FEE(); - -/* This is a simple overapproximative summary, stating that it rounds in the right direction. - * The summary is checked by the specification in BlueRatioMathSummary.spec. - */ -function summaryMulDivUp(uint256 x, uint256 y, uint256 d) returns uint256 { - uint256 result; - require result * d >= x * y; - return result; -} - -/* This is a simple overapproximative summary, stating that it rounds in the right direction. - * The summary is checked by the specification in BlueRatioMathSummary.spec. - */ -function summaryMulDivDown(uint256 x, uint256 y, uint256 d) returns uint256 { - uint256 result; - require result * d <= x * y; - return result; -} - -rule checkAccrueInterestsSummary() -{ - MorphoHarness.Market market; - MorphoHarness.Id id = getMarketId(market); - MorphoHarness.Id otherId; - - requireInvariant feeInRange(id); - require otherId != id; - - uint256 oldSupply = totalSupply(id); - uint256 oldSupplyShares = totalSupplyShares(id); - uint256 oldBorrow = totalBorrow(id); - - uint256 oldSupplyOther = totalSupply(otherId); - uint256 oldSupplySharesOther = totalSupplyShares(otherId); - uint256 oldBorrowOther = totalBorrow(otherId); - - env e; - accrueInterests(e, market); - - uint256 newSupply = totalSupply(id); - uint256 newSupplyShares = totalSupplyShares(id); - uint256 newBorrow = totalBorrow(id); - - uint256 newSupplyOther = totalSupply(otherId); - uint256 newSupplySharesOther = totalSupplyShares(otherId); - uint256 newBorrowOther = totalBorrow(otherId); - - assert oldSupplyOther == newSupplyOther; - assert oldSupplySharesOther == newSupplySharesOther; - assert oldBorrowOther == newBorrowOther; - - mathint interests = newSupply - oldSupply; - - assert interests >= 0; - assert to_mathint(newBorrow) == oldBorrow + interests; - assert to_mathint(newSupply) == oldSupply + interests; - assert newSupplyShares >= oldSupplyShares; - - assert (VIRTUAL_ASSETS() + oldSupply) * (VIRTUAL_SHARES() + newSupplyShares) <= - (VIRTUAL_ASSETS() + newSupply) * (VIRTUAL_SHARES() + oldSupplyShares); -} - -rule accrueInterestsIncreasesRatio() -{ - MorphoHarness.Market market; - MorphoHarness.Id id; - requireInvariant feeInRange(id); - - mathint assetsBefore = totalSupply(id) + VIRTUAL_ASSETS(); - mathint sharesBefore = totalSupplyShares(id) + VIRTUAL_SHARES(); - - // we actually check it for every market, not just for id. - env e; - accrueInterests(e, market); - - mathint assetsAfter = totalSupply(id) + VIRTUAL_ASSETS(); - mathint sharesAfter = totalSupplyShares(id) + VIRTUAL_SHARES(); - - // check if ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter; - assert assetsBefore * sharesAfter <= assetsAfter * sharesBefore; -} - -rule accrueInterestsIncreasesBorrowRatio() -{ - MorphoHarness.Market market; - MorphoHarness.Id id; - requireInvariant feeInRange(id); - - mathint assetsBefore = totalBorrow(id) + VIRTUAL_ASSETS(); - mathint sharesBefore = totalBorrowShares(id) + VIRTUAL_SHARES(); - - // we actually check it for every market, not just for id. - env e; - accrueInterests(e, market); - - mathint assetsAfter = totalBorrow(id) + VIRTUAL_ASSETS(); - mathint sharesAfter = totalBorrowShares(id) + VIRTUAL_SHARES(); - - // check if ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter; - assert assetsBefore * sharesAfter <= assetsAfter * sharesBefore; -} - From cfc6cf7676d981782a27c90c6dd3112cf7329012 Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Fri, 18 Aug 2023 11:51:05 +0200 Subject: [PATCH 052/204] Do not remove solc allowed path --- certora/scripts/verifyBlueRatioMath.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/certora/scripts/verifyBlueRatioMath.sh b/certora/scripts/verifyBlueRatioMath.sh index a075e30a2..6513bb805 100755 --- a/certora/scripts/verifyBlueRatioMath.sh +++ b/certora/scripts/verifyBlueRatioMath.sh @@ -3,6 +3,7 @@ certoraRun \ certora/harness/MorphoHarness.sol \ --verify MorphoHarness:certora/specs/BlueRatioMath.spec \ + --solc_allow_path src \ --msg "Morpho Ratio Math" \ --prover_args '-smt_hashingScheme plaininjectivity' \ "$@" From d451a3060458c97f8fc479cb55ffaaaf8362489c Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 18 Aug 2023 14:12:56 +0200 Subject: [PATCH 053/204] feat: add validation and revert conditions on all functions --- certora/harness/MorphoHarness.sol | 8 ++ certora/scripts/verifyBlueReverts.sh | 11 +++ certora/specs/Blue.spec | 17 +--- certora/specs/BlueReverts.spec | 127 +++++++++++++++++++++++++++ 4 files changed, 150 insertions(+), 13 deletions(-) create mode 100755 certora/scripts/verifyBlueReverts.sh create mode 100644 certora/specs/BlueReverts.spec diff --git a/certora/harness/MorphoHarness.sol b/certora/harness/MorphoHarness.sol index 6114bcd7c..54b279751 100644 --- a/certora/harness/MorphoHarness.sol +++ b/certora/harness/MorphoHarness.sol @@ -9,6 +9,14 @@ contract MorphoHarness is Morpho { constructor(address newOwner) Morpho(newOwner) {} + function MAX_FEE() external pure returns (uint256) { + return MAX_FEE; + } + + function WAD() external pure returns (uint256) { + return WAD; + } + function getVirtualTotalSupply(Id id) external view returns (uint256) { return totalSupply[id] + SharesMathLib.VIRTUAL_ASSETS; } diff --git a/certora/scripts/verifyBlueReverts.sh b/certora/scripts/verifyBlueReverts.sh new file mode 100755 index 000000000..4fe82cd14 --- /dev/null +++ b/certora/scripts/verifyBlueReverts.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -euxo pipefail + +certoraRun \ + certora/harness/MorphoHarness.sol \ + --verify MorphoHarness:certora/specs/BlueReverts.spec \ + --loop_iter 3 \ + --optimistic_loop \ + --msg "Morpho Reverts" \ + "$@" diff --git a/certora/specs/Blue.spec b/certora/specs/Blue.spec index 69e5408d5..4155b363d 100644 --- a/certora/specs/Blue.spec +++ b/certora/specs/Blue.spec @@ -87,7 +87,7 @@ function summarySafeTransferFrom(address token, address from, address to, uint25 definition VIRTUAL_ASSETS() returns mathint = 1; definition VIRTUAL_SHARES() returns mathint = 10^18; definition MAX_FEE() returns mathint = 10^18 * 25/100; -definition isInitialized(MorphoHarness.Id id) returns bool = +definition isCreated(MorphoHarness.Id id) returns bool = (lastUpdate(id) != 0); @@ -103,7 +103,7 @@ invariant borrowLessSupply(MorphoHarness.Id id) totalBorrow(id) <= totalSupply(id); invariant marketInvariant(MorphoHarness.Market market) - isInitialized(getMarketId(market)) => + isCreated(getMarketId(market)) => idToBorrowable[getMarketId(market)] == market.borrowableToken && idToCollateral[getMarketId(market)] == market.collateralToken; @@ -140,20 +140,11 @@ invariant isLiquid(address token) } } -rule supplyRevertZero(MorphoHarness.Market market) { - env e; - bytes b; - - supply@withrevert(e, market, 0, 0, e.msg.sender, b); - - assert lastReverted; -} - invariant onlyEnabledLltv(MorphoHarness.Market market) - isInitialized(getMarketId(market)) => isLltvEnabled(market.lltv); + isCreated(getMarketId(market)) => isLltvEnabled(market.lltv); invariant onlyEnabledIrm(MorphoHarness.Market market) - isInitialized(getMarketId(market)) => isIrmEnabled(market.irm); + isCreated(getMarketId(market)) => isIrmEnabled(market.irm); rule marketIdUnique() { MorphoHarness.Market market1; diff --git a/certora/specs/BlueReverts.spec b/certora/specs/BlueReverts.spec new file mode 100644 index 000000000..31564a44a --- /dev/null +++ b/certora/specs/BlueReverts.spec @@ -0,0 +1,127 @@ +methods { + function owner() external returns address envfree; + function totalSupply(MorphoHarness.Id) external returns uint256 envfree; + function totalBorrow(MorphoHarness.Id) external returns uint256 envfree; + function totalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; + function totalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; + function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; + function isAuthorized(address, address) external returns bool envfree; + function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; + function isLltvEnabled(uint256) external returns bool envfree; + function isIrmEnabled(address) external returns bool envfree; + + function getMarketId(MorphoHarness.Market) external returns MorphoHarness.Id envfree; + function MAX_FEE() external returns uint256 envfree; + function WAD() external returns uint256 envfree; +} + +definition isCreated(MorphoHarness.Id id) returns bool = + (lastUpdate(id) != 0); + +ghost mapping(MorphoHarness.Id => mathint) sumCollateral +{ + init_state axiom (forall MorphoHarness.Id id. sumCollateral[id] == 0); +} +hook Sstore collateral[KEY MorphoHarness.Id id][KEY address owner] uint256 newAmount (uint256 oldAmount) STORAGE { + sumCollateral[id] = sumCollateral[id] - oldAmount + newAmount; +} + +definition emptyMarket(MorphoHarness.Id id) returns bool = + totalSupply(id) == 0 && + totalSupplyShares(id) == 0 && + totalBorrow(id) == 0 && + totalBorrowShares(id) == 0 && + sumCollateral[id] == 0; + +definition exactlyOneZero(uint256 assets, uint256 shares) returns bool = + (assets == 0 && shares != 0) || (assets != 0 && shares == 0); + +// This invariant catches bugs when not checking that the market is created with lastUpdate. +invariant notInitializedEmpty(MorphoHarness.Id id) + !isCreated(id) => emptyMarket(id); + +invariant zeroDoesNotAuthorize(address authorized) + !isAuthorized(0, authorized) +{ + preserved setAuthorization(address _authorized, bool _newAuthorization) with (env e) { + require e.msg.sender != 0; + } +} + +rule setOwnerRevertCondition(env e, address newOwner) { + address oldOwner = owner(); + setOwner@withrevert(e, newOwner); + assert lastReverted <=> e.msg.value != 0 || e.msg.sender != oldOwner; +} + +rule enableIrmRevertCondition(env e, address irm) { + address oldOwner = owner(); + enableIrm@withrevert(e, irm); + assert lastReverted <=> e.msg.value != 0 || e.msg.sender != oldOwner; +} + +rule enableLltvRevertCondition(env e, uint256 lltv) { + address oldOwner = owner(); + enableLltv@withrevert(e, lltv); + assert lastReverted <=> e.msg.value != 0 || e.msg.sender != oldOwner || lltv > WAD(); +} + +rule setFeeRevertCondition(env e, MorphoHarness.Market market, uint256 newFee) { + address oldOwner = owner(); + MorphoHarness.Id id = getMarketId(market); + setFee@withrevert(e, market, newFee); + assert lastReverted <=> e.msg.value != 0 || e.msg.sender != oldOwner || !isCreated(id) || newFee > MAX_FEE(); +} + +rule setFeeRecipientRevertCondition(env e, address recipient) { + address oldOwner = owner(); + setFeeRecipient@withrevert(e, recipient); + assert lastReverted <=> e.msg.value != 0 || e.msg.sender != oldOwner; +} + +rule createMarketRevertCondition(env e, MorphoHarness.Market market) { + MorphoHarness.Id id = getMarketId(market); + createMarket@withrevert(e, market); + assert lastReverted <=> e.msg.value != 0 || !isIrmEnabled(market.irm) || !isLltvEnabled(market.lltv) || lastUpdate(id) != 0; +} + +rule supplyValidation(env e, MorphoHarness.Market market, uint256 assets, uint256 shares, address onBehalf, bytes b) { + supply@withrevert(e, market, assets, shares, onBehalf, b); + assert !exactlyOneZero(assets, shares) || onBehalf == 0 => lastReverted; +} + +rule withdrawValidation(env e, MorphoHarness.Market market, uint256 assets, uint256 shares, address onBehalf, address receiver) { + require e.msg.sender != 0; + requireInvariant zeroDoesNotAuthorize(e.msg.sender); + withdraw@withrevert(e, market, assets, shares, onBehalf, receiver); + assert !exactlyOneZero(assets, shares) || onBehalf == 0 => lastReverted; +} + +rule borrowValidation(env e, MorphoHarness.Market market, uint256 assets, uint256 shares, address onBehalf, address receiver) { + require e.msg.sender != 0; + requireInvariant zeroDoesNotAuthorize(e.msg.sender); + borrow@withrevert(e, market, assets, shares, onBehalf, receiver); + assert !exactlyOneZero(assets, shares) || onBehalf == 0 => lastReverted; +} + +rule repayValidation(env e, MorphoHarness.Market market, uint256 assets, uint256 shares, address onBehalf, bytes b) { + repay@withrevert(e, market, assets, shares, onBehalf, b); + assert !exactlyOneZero(assets, shares) || onBehalf == 0 => lastReverted; +} + +rule supplyCollateralValidation(env e, MorphoHarness.Market market, uint256 assets, address onBehalf, bytes b) { + supplyCollateral@withrevert(e, market, assets, onBehalf, b); + assert assets == 0 || onBehalf == 0 => lastReverted; +} + +rule withdrawCollateralValidation(env e, MorphoHarness.Market market, uint256 assets, address onBehalf, address receiver) { + require e.msg.sender != 0; + requireInvariant zeroDoesNotAuthorize(e.msg.sender); + withdrawCollateral@withrevert(e, market, assets, onBehalf, receiver); + assert assets == 0 || onBehalf == 0 => lastReverted; +} + +rule liquidateValidation(env e, MorphoHarness.Market market, address borrower, uint256 seized, bytes b) { + liquidate@withrevert(e, market, borrower, seized, b); + assert seized == 0 => lastReverted; +} From 5d5df6c82e98f350b3b2b1f839b04ffe98a15403 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 18 Aug 2023 14:59:39 +0200 Subject: [PATCH 054/204] fix: revert of enable IRM and set fee --- certora/specs/BlueReverts.spec | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/certora/specs/BlueReverts.spec b/certora/specs/BlueReverts.spec index 31564a44a..bffcbc6d7 100644 --- a/certora/specs/BlueReverts.spec +++ b/certora/specs/BlueReverts.spec @@ -63,14 +63,15 @@ rule enableIrmRevertCondition(env e, address irm) { rule enableLltvRevertCondition(env e, uint256 lltv) { address oldOwner = owner(); enableLltv@withrevert(e, lltv); - assert lastReverted <=> e.msg.value != 0 || e.msg.sender != oldOwner || lltv > WAD(); + assert lastReverted <=> e.msg.value != 0 || e.msg.sender != oldOwner || lltv >= WAD(); } -rule setFeeRevertCondition(env e, MorphoHarness.Market market, uint256 newFee) { +// setFee can also revert if the accrueInterests reverts. +rule setFeeInputValidation(env e, MorphoHarness.Market market, uint256 newFee) { address oldOwner = owner(); MorphoHarness.Id id = getMarketId(market); setFee@withrevert(e, market, newFee); - assert lastReverted <=> e.msg.value != 0 || e.msg.sender != oldOwner || !isCreated(id) || newFee > MAX_FEE(); + assert e.msg.value != 0 || e.msg.sender != oldOwner || !isCreated(id) || newFee > MAX_FEE() => lastReverted; } rule setFeeRecipientRevertCondition(env e, address recipient) { @@ -85,43 +86,43 @@ rule createMarketRevertCondition(env e, MorphoHarness.Market market) { assert lastReverted <=> e.msg.value != 0 || !isIrmEnabled(market.irm) || !isLltvEnabled(market.lltv) || lastUpdate(id) != 0; } -rule supplyValidation(env e, MorphoHarness.Market market, uint256 assets, uint256 shares, address onBehalf, bytes b) { +rule supplyInputValidation(env e, MorphoHarness.Market market, uint256 assets, uint256 shares, address onBehalf, bytes b) { supply@withrevert(e, market, assets, shares, onBehalf, b); assert !exactlyOneZero(assets, shares) || onBehalf == 0 => lastReverted; } -rule withdrawValidation(env e, MorphoHarness.Market market, uint256 assets, uint256 shares, address onBehalf, address receiver) { +rule withdrawInputValidation(env e, MorphoHarness.Market market, uint256 assets, uint256 shares, address onBehalf, address receiver) { require e.msg.sender != 0; requireInvariant zeroDoesNotAuthorize(e.msg.sender); withdraw@withrevert(e, market, assets, shares, onBehalf, receiver); assert !exactlyOneZero(assets, shares) || onBehalf == 0 => lastReverted; } -rule borrowValidation(env e, MorphoHarness.Market market, uint256 assets, uint256 shares, address onBehalf, address receiver) { +rule borrowInputValidation(env e, MorphoHarness.Market market, uint256 assets, uint256 shares, address onBehalf, address receiver) { require e.msg.sender != 0; requireInvariant zeroDoesNotAuthorize(e.msg.sender); borrow@withrevert(e, market, assets, shares, onBehalf, receiver); assert !exactlyOneZero(assets, shares) || onBehalf == 0 => lastReverted; } -rule repayValidation(env e, MorphoHarness.Market market, uint256 assets, uint256 shares, address onBehalf, bytes b) { +rule repayInputValidation(env e, MorphoHarness.Market market, uint256 assets, uint256 shares, address onBehalf, bytes b) { repay@withrevert(e, market, assets, shares, onBehalf, b); assert !exactlyOneZero(assets, shares) || onBehalf == 0 => lastReverted; } -rule supplyCollateralValidation(env e, MorphoHarness.Market market, uint256 assets, address onBehalf, bytes b) { +rule supplyCollateralInputValidation(env e, MorphoHarness.Market market, uint256 assets, address onBehalf, bytes b) { supplyCollateral@withrevert(e, market, assets, onBehalf, b); assert assets == 0 || onBehalf == 0 => lastReverted; } -rule withdrawCollateralValidation(env e, MorphoHarness.Market market, uint256 assets, address onBehalf, address receiver) { +rule withdrawCollateralInputValidation(env e, MorphoHarness.Market market, uint256 assets, address onBehalf, address receiver) { require e.msg.sender != 0; requireInvariant zeroDoesNotAuthorize(e.msg.sender); withdrawCollateral@withrevert(e, market, assets, onBehalf, receiver); assert assets == 0 || onBehalf == 0 => lastReverted; } -rule liquidateValidation(env e, MorphoHarness.Market market, address borrower, uint256 seized, bytes b) { +rule liquidateInputValidation(env e, MorphoHarness.Market market, address borrower, uint256 seized, bytes b) { liquidate@withrevert(e, market, borrower, seized, b); assert seized == 0 => lastReverted; } From 4d1ac12c161b08db698a26819dafb50c8651a436 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 18 Aug 2023 15:23:36 +0200 Subject: [PATCH 055/204] fix: set fee validation use last call to check revert --- certora/harness/MorphoHarness.sol | 16 ++++++++++++---- certora/specs/Blue.spec | 29 ++++++++++++++--------------- certora/specs/BlueRatioMath.spec | 8 ++++---- certora/specs/BlueReverts.spec | 3 ++- 4 files changed, 32 insertions(+), 24 deletions(-) diff --git a/certora/harness/MorphoHarness.sol b/certora/harness/MorphoHarness.sol index 54b279751..79ac48499 100644 --- a/certora/harness/MorphoHarness.sol +++ b/certora/harness/MorphoHarness.sol @@ -9,14 +9,22 @@ contract MorphoHarness is Morpho { constructor(address newOwner) Morpho(newOwner) {} - function MAX_FEE() external pure returns (uint256) { - return MAX_FEE; - } - function WAD() external pure returns (uint256) { return WAD; } + function VIRTUAL_SHARES() external pure returns (uint256) { + return SharesMathLib.VIRTUAL_SHARES; + } + + function VIRTUAL_ASSETS() external pure returns (uint256) { + return SharesMathLib.VIRTUAL_ASSETS; + } + + function MAX_FEE() external pure returns (uint256) { + return MAX_FEE; + } + function getVirtualTotalSupply(Id id) external view returns (uint256) { return totalSupply[id] + SharesMathLib.VIRTUAL_ASSETS; } diff --git a/certora/specs/Blue.spec b/certora/specs/Blue.spec index 4155b363d..e9ddeb98f 100644 --- a/certora/specs/Blue.spec +++ b/certora/specs/Blue.spec @@ -18,38 +18,40 @@ methods { function _.borrowRate(MorphoHarness.Market) external => HAVOC_ECF; function getMarketId(MorphoHarness.Market) external returns MorphoHarness.Id envfree; + function VIRTUAL_ASSETS() external returns uint256 envfree; + function VIRTUAL_SHARES() external returns uint256 envfree; + function MAX_FEE() external returns uint256 envfree; function SafeTransferLib.safeTransfer(address token, address to, uint256 value) internal => summarySafeTransferFrom(token, currentContract, to, value); function SafeTransferLib.safeTransferFrom(address token, address from, address to, uint256 value) internal => summarySafeTransferFrom(token, from, to, value); } -ghost mapping(MorphoHarness.Id => mathint) sumSupplyShares -{ +ghost mapping(MorphoHarness.Id => mathint) sumSupplyShares { init_state axiom (forall MorphoHarness.Id id. sumSupplyShares[id] == 0); } -ghost mapping(MorphoHarness.Id => mathint) sumBorrowShares -{ +ghost mapping(MorphoHarness.Id => mathint) sumBorrowShares { init_state axiom (forall MorphoHarness.Id id. sumBorrowShares[id] == 0); } -ghost mapping(MorphoHarness.Id => mathint) sumCollateral -{ +ghost mapping(MorphoHarness.Id => mathint) sumCollateral { init_state axiom (forall MorphoHarness.Id id. sumCollateral[id] == 0); } -ghost mapping(address => mathint) myBalances -{ + +ghost mapping(address => mathint) myBalances { init_state axiom (forall address token. myBalances[token] == 0); } -ghost mapping(address => mathint) expectedAmount -{ + +ghost mapping(address => mathint) expectedAmount { init_state axiom (forall address token. expectedAmount[token] == 0); } ghost mapping(MorphoHarness.Id => address) idToBorrowable; + ghost mapping(MorphoHarness.Id => address) idToCollateral; hook Sstore idToMarket[KEY MorphoHarness.Id id].borrowableToken address token STORAGE { idToBorrowable[id] = token; } + hook Sstore idToMarket[KEY MorphoHarness.Id id].collateralToken address token STORAGE { idToCollateral[id] = token; } @@ -84,18 +86,15 @@ function summarySafeTransferFrom(address token, address from, address to, uint25 } } -definition VIRTUAL_ASSETS() returns mathint = 1; -definition VIRTUAL_SHARES() returns mathint = 10^18; -definition MAX_FEE() returns mathint = 10^18 * 25/100; definition isCreated(MorphoHarness.Id id) returns bool = (lastUpdate(id) != 0); - invariant feeInRange(MorphoHarness.Id id) - to_mathint(fee(id)) <= MAX_FEE(); + fee(id) <= MAX_FEE(); invariant sumSupplySharesCorrect(MorphoHarness.Id id) to_mathint(totalSupplyShares(id)) == sumSupplyShares[id]; + invariant sumBorrowSharesCorrect(MorphoHarness.Id id) to_mathint(totalBorrowShares(id)) == sumBorrowShares[id]; diff --git a/certora/specs/BlueRatioMath.spec b/certora/specs/BlueRatioMath.spec index 8461f973e..8e3c9cae5 100644 --- a/certora/specs/BlueRatioMath.spec +++ b/certora/specs/BlueRatioMath.spec @@ -8,11 +8,11 @@ methods { function MathLib.wTaylorCompounded(uint256, uint256) internal returns uint256 => NONDET; function _.borrowRate(MorphoHarness.Market) external => HAVOC_ECF; -} -definition VIRTUAL_ASSETS() returns mathint = 1; -definition VIRTUAL_SHARES() returns mathint = 10^18; -definition MAX_FEE() returns mathint = 10^18 * 25/100; + function VIRTUAL_ASSETS() external returns uint256 envfree; + function VIRTUAL_SHARES() external returns uint256 envfree; + function MAX_FEE() external returns uint256 envfree; +} invariant feeInRange(MorphoHarness.Id id) to_mathint(fee(id)) <= MAX_FEE(); diff --git a/certora/specs/BlueReverts.spec b/certora/specs/BlueReverts.spec index bffcbc6d7..92215872b 100644 --- a/certora/specs/BlueReverts.spec +++ b/certora/specs/BlueReverts.spec @@ -71,7 +71,8 @@ rule setFeeInputValidation(env e, MorphoHarness.Market market, uint256 newFee) { address oldOwner = owner(); MorphoHarness.Id id = getMarketId(market); setFee@withrevert(e, market, newFee); - assert e.msg.value != 0 || e.msg.sender != oldOwner || !isCreated(id) || newFee > MAX_FEE() => lastReverted; + bool hasReverted = lastReverted; + assert e.msg.value != 0 || e.msg.sender != oldOwner || !isCreated(id) || newFee > MAX_FEE() => hasReverted; } rule setFeeRecipientRevertCondition(env e, address recipient) { From f2c0507ba4bd1239651bdee9740f1ceb1ea06aca Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 18 Aug 2023 17:00:02 +0200 Subject: [PATCH 056/204] style: start committing to one format --- certora/specs/BlueRatioMath.spec | 38 ++++++++++++++------------------ 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/certora/specs/BlueRatioMath.spec b/certora/specs/BlueRatioMath.spec index 3604a2bf4..52d3194fa 100644 --- a/certora/specs/BlueRatioMath.spec +++ b/certora/specs/BlueRatioMath.spec @@ -39,8 +39,7 @@ function summaryMulDivDown(uint256 x, uint256 y, uint256 d) returns uint256 { return result; } -rule accrueInterestsIncreasesRatio() -{ +rule accrueInterestsIncreasesSupplyRatio() { MorphoHarness.Market market; MorphoHarness.Id id; requireInvariant feeInRange(id); @@ -48,19 +47,18 @@ rule accrueInterestsIncreasesRatio() mathint assetsBefore = totalSupply(id) + VIRTUAL_ASSETS(); mathint sharesBefore = totalSupplyShares(id) + VIRTUAL_SHARES(); - // we actually check the property for every market, not just for id. + // The check is done for every market, not just for id. env e; accrueInterests(e, market); mathint assetsAfter = totalSupply(id) + VIRTUAL_ASSETS(); mathint sharesAfter = totalSupplyShares(id) + VIRTUAL_SHARES(); - // check if ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter; + // Check if ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter assert assetsBefore * sharesAfter <= assetsAfter * sharesBefore; } -rule accrueInterestsIncreasesBorrowRatio() -{ +rule accrueInterestsIncreasesBorrowRatio() { MorphoHarness.Market market; MorphoHarness.Id id; requireInvariant feeInRange(id); @@ -68,19 +66,19 @@ rule accrueInterestsIncreasesBorrowRatio() mathint assetsBefore = totalBorrow(id) + VIRTUAL_ASSETS(); mathint sharesBefore = totalBorrowShares(id) + VIRTUAL_SHARES(); - // we actually check it for every market, not just for id. + // The check is done for every market, not just for id. env e; accrueInterests(e, market); mathint assetsAfter = totalBorrow(id) + VIRTUAL_ASSETS(); mathint sharesAfter = totalBorrowShares(id) + VIRTUAL_SHARES(); - // check if ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter; + // Check if ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter assert assetsBefore * sharesAfter <= assetsAfter * sharesBefore; } -rule onlyLiquidateCanDecreaseRatio(method f) +rule onlyLiquidateCanDecreaseSupplyRatio(env e, method f, calldataarg args) filtered { f -> !f.isView && f.selector != sig:liquidate(MorphoHarness.Market, address, uint256, bytes).selector } @@ -90,25 +88,22 @@ filtered { mathint assetsBefore = totalSupply(id) + VIRTUAL_ASSETS(); mathint sharesBefore = totalSupplyShares(id) + VIRTUAL_SHARES(); - env e; - // interest is checked separately by the rules above - // here we assume interest has already been accumulated for this block + + // Interest is checked separately by the rules above. + // Here we assume interest has already been accumulated for this block. require lastUpdate(id) == e.block.timestamp; - calldataarg args; f(e,args); mathint assetsAfter = totalSupply(id) + VIRTUAL_ASSETS(); mathint sharesAfter = totalSupplyShares(id) + VIRTUAL_SHARES(); - // check if ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter; + // Check if ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter assert assetsBefore * sharesAfter <= assetsAfter * sharesBefore; } -rule onlyAccrueInterestsCanIncreaseBorrowRatio(method f) -filtered { - f -> !f.isView -} +rule onlyAccrueInterestsCanIncreaseBorrowRatio(env e, method f, calldataarg args) +filtered { f -> !f.isView } { MorphoHarness.Id id; requireInvariant feeInRange(id); @@ -116,15 +111,14 @@ filtered { mathint assetsBefore = totalBorrow(id) + VIRTUAL_ASSETS(); mathint sharesBefore = totalBorrowShares(id) + VIRTUAL_SHARES(); - env e; - calldataarg args; - // interests would increase borrow ratio, so we need to assume no time passes. + // Interest would increase borrow ratio, so we need to assume no time passes. require lastUpdate(id) == e.block.timestamp; + f(e,args); mathint assetsAfter = totalBorrow(id) + VIRTUAL_ASSETS(); mathint sharesAfter = totalBorrowShares(id) + VIRTUAL_SHARES(); - // check if ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter; + // Check if ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter assert assetsBefore * sharesAfter >= assetsAfter * sharesBefore; } From 1ae114e0da474e521078baf222bec0ac2fb60f5e Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 18 Aug 2023 17:07:43 +0200 Subject: [PATCH 057/204] fix: rename isCreated missed --- certora/specs/Blue.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certora/specs/Blue.spec b/certora/specs/Blue.spec index f5a61777e..9fc4fe5ff 100644 --- a/certora/specs/Blue.spec +++ b/certora/specs/Blue.spec @@ -301,7 +301,7 @@ rule canWithdrawAll() { MorphoHarness.Id id = getMarketId(market); uint256 shares = supplyShares(id, e.msg.sender); - require isInitialized(id); + require isCreated(id); require e.msg.sender != 0; require receiver != 0; require e.msg.value == 0; From c9bff6dd1ed25d027c378594cf0e809dac1f01f7 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Sat, 19 Aug 2023 12:41:45 +0200 Subject: [PATCH 058/204] feat: add every script to the CI --- .github/workflows/certora.yml | 7 +++++-- certora/specs/Blue.spec | 24 ++++++++++-------------- certora/specs/BlueRatioMath.spec | 10 ++++------ certora/specs/BlueRatioMathSummary.spec | 2 +- certora/specs/DifficultMath.spec | 4 ++-- certora/specs/Reentrancy.spec | 15 +++++++-------- 6 files changed, 29 insertions(+), 33 deletions(-) diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml index 6775e9ad0..79f4c1e8b 100644 --- a/.github/workflows/certora.yml +++ b/.github/workflows/certora.yml @@ -44,8 +44,11 @@ jobs: matrix: script: - verifyBlue.sh - - verifyBlueRatioMathSummary.sh - verifyBlueExitLiquidity.sh + - verifyBlueRatioMath.sh + - verifyBlueRatioMathSummary.sh + - verifyBlueReverts.sh - verifyBlueTransfer.sh - - verifyReentrancy.sh - verifyDifficultMath.sh + - verifyHealth.sh + - verifyReentrancy.sh diff --git a/certora/specs/Blue.spec b/certora/specs/Blue.spec index 9fc4fe5ff..e81aac0bf 100644 --- a/certora/specs/Blue.spec +++ b/certora/specs/Blue.spec @@ -32,9 +32,11 @@ methods { ghost mapping(MorphoHarness.Id => mathint) sumSupplyShares { init_state axiom (forall MorphoHarness.Id id. sumSupplyShares[id] == 0); } + ghost mapping(MorphoHarness.Id => mathint) sumBorrowShares { init_state axiom (forall MorphoHarness.Id id. sumBorrowShares[id] == 0); } + ghost mapping(MorphoHarness.Id => mathint) sumCollateral { init_state axiom (forall MorphoHarness.Id id. sumCollateral[id] == 0); } @@ -106,8 +108,8 @@ invariant borrowLessSupply(MorphoHarness.Id id) invariant marketInvariant(MorphoHarness.Market market) isCreated(getMarketId(market)) => - idToBorrowable[getMarketId(market)] == market.borrowableToken - && idToCollateral[getMarketId(market)] == market.collateralToken; + idToBorrowable[getMarketId(market)] == market.borrowableToken && + idToCollateral[getMarketId(market)] == market.collateralToken; invariant isLiquid(address token) expectedAmount[token] <= myBalances[token] @@ -206,9 +208,7 @@ rule supplyMovesTokensAndIncreasesShares() { } rule userCannotLoseSupplyShares(method f, calldataarg data) -filtered { - f -> !f.isView -} +filtered { f -> !f.isView } { MorphoHarness.Market market; uint256 assets; @@ -231,9 +231,7 @@ filtered { } rule userCannotGainBorrowShares(method f, calldataarg data) -filtered { - f -> !f.isView -} +filtered { f -> !f.isView } { MorphoHarness.Market market; uint256 assets; @@ -257,9 +255,7 @@ filtered { rule userWithoutBorrowCannotLoseCollateral(method f, calldataarg data) -filtered { - f -> !f.isView -} +filtered { f -> !f.isView } { MorphoHarness.Market market; uint256 assets; @@ -282,9 +278,9 @@ filtered { assert collateralAfter >= collateralBefore; } -rule noTimeTravel(method f, env e, calldataarg data) filtered { - f -> !f.isView -} { +rule noTimeTravel(method f, env e, calldataarg data) +filtered { f -> !f.isView } +{ MorphoHarness.Id id; require lastUpdate(id) <= e.block.timestamp; f(e, data); diff --git a/certora/specs/BlueRatioMath.spec b/certora/specs/BlueRatioMath.spec index ac573058c..d9cb70979 100644 --- a/certora/specs/BlueRatioMath.spec +++ b/certora/specs/BlueRatioMath.spec @@ -21,18 +21,16 @@ methods { invariant feeInRange(MorphoHarness.Id id) to_mathint(fee(id)) <= MAX_FEE(); -/* This is a simple overapproximative summary, stating that it rounds in the right direction. - * The summary is checked by the specification in BlueRatioMathSummary.spec. - */ +// This is a simple overapproximative summary, stating that it rounds in the right direction. +// The summary is checked by the specification in BlueRatioMathSummary.spec. function summaryMulDivUp(uint256 x, uint256 y, uint256 d) returns uint256 { uint256 result; require result * d >= x * y; return result; } -/* This is a simple overapproximative summary, stating that it rounds in the right direction. - * The summary is checked by the specification in BlueRatioMathSummary.spec. - */ +// This is a simple overapproximative summary, stating that it rounds in the right direction. +// The summary is checked by the specification in BlueRatioMathSummary.spec. function summaryMulDivDown(uint256 x, uint256 y, uint256 d) returns uint256 { uint256 result; require result * d <= x * y; diff --git a/certora/specs/BlueRatioMathSummary.spec b/certora/specs/BlueRatioMathSummary.spec index d83492f03..65f653283 100644 --- a/certora/specs/BlueRatioMathSummary.spec +++ b/certora/specs/BlueRatioMathSummary.spec @@ -3,7 +3,7 @@ methods { function mathLibMulDivDown(uint256, uint256, uint256) external returns uint256 envfree; } -/* Check the summaries required by BlueRatioMath.spec */ +// Check the summaries required by BlueRatioMath.spec rule checkSummaryMulDivUp(uint256 x, uint256 y, uint256 d) { uint256 result = mathLibMulDivUp(x, y, d); assert result * d >= x * y; diff --git a/certora/specs/DifficultMath.spec b/certora/specs/DifficultMath.spec index 4e448b6d3..df5699149 100644 --- a/certora/specs/DifficultMath.spec +++ b/certora/specs/DifficultMath.spec @@ -17,7 +17,7 @@ function summaryMulDivDown(uint256 x, uint256 y, uint256 d) returns uint256 { return require_uint256((x * y) / d); } -/* There should be no profit from supply followed immediately by withdraw */ +// There should be no profit from supply followed immediately by withdraw. rule supplyWithdraw() { MorphoHarness.Market market; uint256 assets; @@ -46,7 +46,7 @@ rule supplyWithdraw() { assert withdrawnAssets <= suppliedAssets; } -/* There should be no profit from withdraw followed immediately by supply */ +// There should be no profit from withdraw followed immediately by supply. rule withdrawSupply() { MorphoHarness.Market market; uint256 assets; diff --git a/certora/specs/Reentrancy.spec b/certora/specs/Reentrancy.spec index cc5469e0b..3bf0d9ecf 100644 --- a/certora/specs/Reentrancy.spec +++ b/certora/specs/Reentrancy.spec @@ -27,7 +27,7 @@ hook ALL_SLOAD(uint loc) uint v { hook CALL(uint g, address addr, uint value, uint argsOffset, uint argsLength, uint retOffset, uint retLength) uint rc { if (callIsBorrowRate) { - /* The calls to borrow rate are trusted and don't count */ + // The calls to borrow rate are trusted and don't count. callIsBorrowRate = false; hasCallAfterAccessingStorage = hasCallAfterAccessingStorage; } else { @@ -56,10 +56,9 @@ rule noDelegateCalls(method f, calldataarg data, env e) { assert !delegate_call; } -/* This rule can be used to check which methods have static calls -rule hasStaticCalls(method f, calldataarg data, env e) { - require !static_call; - f(e,data); - satisfy static_call; -} -*/ +// This rule can be used to check which methods have static calls +// rule hasStaticCalls(method f, calldataarg data, env e) { +// require !static_call; +// f(e,data); +// satisfy static_call; +// } From b4d7bf820c8d20796cd746dffbfaec2c257960e7 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Sat, 19 Aug 2023 22:56:44 +0200 Subject: [PATCH 059/204] fix: adapt to renaming and storage refactor --- certora/harness/MorphoHarness.sol | 58 +++++++-- certora/specs/Blue.spec | 179 +++++++++++++-------------- certora/specs/BlueExitLiquidity.spec | 48 +++---- certora/specs/BlueRatioMath.spec | 66 +++++----- certora/specs/BlueReverts.spec | 67 +++++----- certora/specs/DifficultMath.spec | 28 ++--- certora/specs/Health.spec | 34 ++--- 7 files changed, 254 insertions(+), 226 deletions(-) diff --git a/certora/harness/MorphoHarness.sol b/certora/harness/MorphoHarness.sol index 129e0bc42..482c95049 100644 --- a/certora/harness/MorphoHarness.sol +++ b/certora/harness/MorphoHarness.sol @@ -5,7 +5,7 @@ import "../../src/libraries/SharesMathLib.sol"; import "../../src/libraries/MarketLib.sol"; contract MorphoHarness is Morpho { - using MarketLib for Market; + using MarketLib for MarketParams; constructor(address newOwner) Morpho(newOwner) {} @@ -25,24 +25,60 @@ contract MorphoHarness is Morpho { return MAX_FEE; } - function getVirtualTotalSupply(Id id) external view returns (uint256) { - return totalSupply[id] + SharesMathLib.VIRTUAL_ASSETS; + function getTotalSupplyAssets(Id id) external view returns (uint256) { + return market[id].totalSupplyAssets; + } + + function getTotalSupplyShares(Id id) external view returns (uint256) { + return market[id].totalSupplyShares; + } + + function getTotalBorrowAssets(Id id) external view returns (uint256) { + return market[id].totalBorrowAssets; + } + + function getTotalBorrowShares(Id id) external view returns (uint256) { + return market[id].totalBorrowShares; + } + + function getSupplyShares(Id id, address account) external view returns (uint256) { + return user[id][account].supplyShares; + } + + function getBorrowShares(Id id, address account) external view returns (uint256) { + return user[id][account].borrowShares; + } + + function getCollateral(Id id, address account) external view returns (uint256) { + return user[id][account].collateral; + } + + function getLastUpdate(Id id) external view returns (uint256) { + return market[id].lastUpdate; + } + + function getFee(Id id) external view returns (uint256) { + return market[id].fee; + } + + function getVirtualTotalSupplyAssets(Id id) external view returns (uint256) { + return market[id].totalSupplyAssets + SharesMathLib.VIRTUAL_ASSETS; } function getVirtualTotalSupplyShares(Id id) external view returns (uint256) { - return totalSupplyShares[id] + SharesMathLib.VIRTUAL_SHARES; + return market[id].totalSupplyShares + SharesMathLib.VIRTUAL_SHARES; } - function getVirtualTotalBorrow(Id id) external view returns (uint256) { - return totalBorrow[id] + SharesMathLib.VIRTUAL_ASSETS; + function getVirtualTotalBorrowAssets(Id id) external view returns (uint256) { + return market[id].totalBorrowAssets + SharesMathLib.VIRTUAL_ASSETS; } function getVirtualTotalBorrowShares(Id id) external view returns (uint256) { - return totalBorrowShares[id] + SharesMathLib.VIRTUAL_SHARES; + return market[id].totalBorrowShares + SharesMathLib.VIRTUAL_SHARES; } - function getMarketId(Market memory market) external pure returns (Id) { - return market.id(); + function getMarketId(MarketParams memory marketParams) external pure returns (Id) { + return marketParams.id(); } function mathLibMulDivUp(uint256 x, uint256 y, uint256 d) public pure returns (uint256) { @@ -53,7 +89,7 @@ contract MorphoHarness is Morpho { return MathLib.mulDivDown(x, y, d); } - function isHealthy(Market memory market, address user) external view returns (bool) { - return _isHealthy(market, market.id(), user); + function isHealthy(MarketParams memory marketParams, address user) external view returns (bool) { + return _isHealthy(marketParams, marketParams.id(), user); } } diff --git a/certora/specs/Blue.spec b/certora/specs/Blue.spec index e81aac0bf..53a03f331 100644 --- a/certora/specs/Blue.spec +++ b/certora/specs/Blue.spec @@ -1,28 +1,21 @@ methods { - function getVirtualTotalSupply(MorphoHarness.Id) external returns uint256 envfree; - function getVirtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; - function totalSupply(MorphoHarness.Id) external returns uint256 envfree; - function totalBorrow(MorphoHarness.Id) external returns uint256 envfree; - function supplyShares(MorphoHarness.Id, address user) external returns uint256 envfree; - function borrowShares(MorphoHarness.Id, address user) external returns uint256 envfree; - function collateral(MorphoHarness.Id, address user) external returns uint256 envfree; - function totalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; - function totalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; - function fee(MorphoHarness.Id) external returns uint256 envfree; - function getMarketId(MorphoHarness.Market) external returns MorphoHarness.Id envfree; - function idToMarket(MorphoHarness.Id) external returns (address, address, address, address, uint256) envfree; - function isAuthorized(address, address) external returns bool envfree; + function getTotalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; + function getTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; + function getTotalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree; + function getTotalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; + function getSupplyShares(MorphoHarness.Id, address) external returns uint256 envfree; + function getBorrowShares(MorphoHarness.Id, address) external returns uint256 envfree; + function getCollateral(MorphoHarness.Id, address) external returns uint256 envfree; + function getFee(MorphoHarness.Id) external returns uint256 envfree; + function getLastUpdate(MorphoHarness.Id) external returns uint256 envfree; + function getMarketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; - function isHealthy(MorphoHarness.Market, address user) external returns bool envfree; - function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; + function isAuthorized(address, address) external returns bool envfree; function isLltvEnabled(uint256) external returns bool envfree; function isIrmEnabled(address) external returns bool envfree; - function _.borrowRate(MorphoHarness.Market) external => HAVOC_ECF; + function _.borrowRate(MorphoHarness.MarketParams) external => HAVOC_ECF; - function getMarketId(MorphoHarness.Market) external returns MorphoHarness.Id envfree; - function VIRTUAL_ASSETS() external returns uint256 envfree; - function VIRTUAL_SHARES() external returns uint256 envfree; function MAX_FEE() external returns uint256 envfree; function SafeTransferLib.safeTransfer(address token, address to, uint256 value) internal => summarySafeTransferFrom(token, currentContract, to, value); @@ -53,32 +46,32 @@ ghost mapping(MorphoHarness.Id => address) idToBorrowable; ghost mapping(MorphoHarness.Id => address) idToCollateral; -hook Sstore idToMarket[KEY MorphoHarness.Id id].borrowableToken address token STORAGE { +hook Sstore idToMarketParams[KEY MorphoHarness.Id id].borrowableToken address token STORAGE { idToBorrowable[id] = token; } -hook Sstore idToMarket[KEY MorphoHarness.Id id].collateralToken address token STORAGE { +hook Sstore idToMarketParams[KEY MorphoHarness.Id id].collateralToken address token STORAGE { idToCollateral[id] = token; } -hook Sstore supplyShares[KEY MorphoHarness.Id id][KEY address owner] uint256 newShares (uint256 oldShares) STORAGE { +hook Sstore user[KEY MorphoHarness.Id id][KEY address owner].supplyShares uint256 newShares (uint256 oldShares) STORAGE { sumSupplyShares[id] = sumSupplyShares[id] - oldShares + newShares; } -hook Sstore borrowShares[KEY MorphoHarness.Id id][KEY address owner] uint256 newShares (uint256 oldShares) STORAGE { +hook Sstore user[KEY MorphoHarness.Id id][KEY address owner].borrowShares uint128 newShares (uint128 oldShares) STORAGE { sumBorrowShares[id] = sumBorrowShares[id] - oldShares + newShares; } -hook Sstore collateral[KEY MorphoHarness.Id id][KEY address owner] uint256 newAmount (uint256 oldAmount) STORAGE { +hook Sstore user[KEY MorphoHarness.Id id][KEY address owner].collateral uint128 newAmount (uint128 oldAmount) STORAGE { sumCollateral[id] = sumCollateral[id] - oldAmount + newAmount; expectedAmount[idToCollateral[id]] = expectedAmount[idToCollateral[id]] - oldAmount + newAmount; } -hook Sstore totalSupply[KEY MorphoHarness.Id id] uint256 newAmount (uint256 oldAmount) STORAGE { +hook Sstore market[KEY MorphoHarness.Id id].totalSupplyAssets uint128 newAmount (uint128 oldAmount) STORAGE { expectedAmount[idToBorrowable[id]] = expectedAmount[idToBorrowable[id]] - oldAmount + newAmount; } -hook Sstore totalBorrow[KEY MorphoHarness.Id id] uint256 newAmount (uint256 oldAmount) STORAGE { +hook Sstore market[KEY MorphoHarness.Id id].totalBorrowAssets uint128 newAmount (uint128 oldAmount) STORAGE { expectedAmount[idToBorrowable[id]] = expectedAmount[idToBorrowable[id]] + oldAmount - newAmount; } @@ -92,75 +85,75 @@ function summarySafeTransferFrom(address token, address from, address to, uint25 } definition isCreated(MorphoHarness.Id id) returns bool = - (lastUpdate(id) != 0); + getLastUpdate(id) != 0; invariant feeInRange(MorphoHarness.Id id) - fee(id) <= MAX_FEE(); + getFee(id) <= MAX_FEE(); invariant sumSupplySharesCorrect(MorphoHarness.Id id) - to_mathint(totalSupplyShares(id)) == sumSupplyShares[id]; + to_mathint(getTotalSupplyShares(id)) == sumSupplyShares[id]; invariant sumBorrowSharesCorrect(MorphoHarness.Id id) - to_mathint(totalBorrowShares(id)) == sumBorrowShares[id]; + to_mathint(getTotalBorrowShares(id)) == sumBorrowShares[id]; invariant borrowLessSupply(MorphoHarness.Id id) - totalBorrow(id) <= totalSupply(id); + getTotalBorrowAssets(id) <= getTotalSupplyAssets(id); -invariant marketInvariant(MorphoHarness.Market market) - isCreated(getMarketId(market)) => - idToBorrowable[getMarketId(market)] == market.borrowableToken && - idToCollateral[getMarketId(market)] == market.collateralToken; +invariant marketInvariant(MorphoHarness.MarketParams marketParams) + isCreated(getMarketId(marketParams)) => + idToBorrowable[getMarketId(marketParams)] == marketParams.borrowableToken && + idToCollateral[getMarketId(marketParams)] == marketParams.collateralToken; invariant isLiquid(address token) expectedAmount[token] <= myBalances[token] { - preserved supply(MorphoHarness.Market market, uint256 _a, uint256 _s, address _o, bytes _d) with (env e) { - requireInvariant marketInvariant(market); + preserved supply(MorphoHarness.MarketParams marketParams, uint256 _a, uint256 _s, address _o, bytes _d) with (env e) { + requireInvariant marketInvariant(marketParams); require e.msg.sender != currentContract; } - preserved withdraw(MorphoHarness.Market market, uint256 _a, uint256 _s, address _o, address _r) with (env e) { - requireInvariant marketInvariant(market); + preserved withdraw(MorphoHarness.MarketParams marketParams, uint256 _a, uint256 _s, address _o, address _r) with (env e) { + requireInvariant marketInvariant(marketParams); require e.msg.sender != currentContract; } - preserved borrow(MorphoHarness.Market market, uint256 _a, uint256 _s, address _o, address _r) with (env e) { - requireInvariant marketInvariant(market); + preserved borrow(MorphoHarness.MarketParams marketParams, uint256 _a, uint256 _s, address _o, address _r) with (env e) { + requireInvariant marketInvariant(marketParams); require e.msg.sender != currentContract; } - preserved repay(MorphoHarness.Market market, uint256 _a, uint256 _s, address _o, bytes _d) with (env e) { - requireInvariant marketInvariant(market); + preserved repay(MorphoHarness.MarketParams marketParams, uint256 _a, uint256 _s, address _o, bytes _d) with (env e) { + requireInvariant marketInvariant(marketParams); require e.msg.sender != currentContract; } - preserved supplyCollateral(MorphoHarness.Market market, uint256 _a, address _o, bytes _d) with (env e) { - requireInvariant marketInvariant(market); + preserved supplyCollateral(MorphoHarness.MarketParams marketParams, uint256 _a, address _o, bytes _d) with (env e) { + requireInvariant marketInvariant(marketParams); require e.msg.sender != currentContract; } - preserved withdrawCollateral(MorphoHarness.Market market, uint256 _a, address _o, address _r) with (env e) { - requireInvariant marketInvariant(market); + preserved withdrawCollateral(MorphoHarness.MarketParams marketParams, uint256 _a, address _o, address _r) with (env e) { + requireInvariant marketInvariant(marketParams); require e.msg.sender != currentContract; } - preserved liquidate(MorphoHarness.Market market, address _b, uint256 _s, bytes _d) with (env e) { - requireInvariant marketInvariant(market); + preserved liquidate(MorphoHarness.MarketParams marketParams, address _b, uint256 _s, bytes _d) with (env e) { + requireInvariant marketInvariant(marketParams); require e.msg.sender != currentContract; } } -invariant onlyEnabledLltv(MorphoHarness.Market market) - isCreated(getMarketId(market)) => isLltvEnabled(market.lltv); +invariant onlyEnabledLltv(MorphoHarness.MarketParams marketParams) + isCreated(getMarketId(marketParams)) => isLltvEnabled(marketParams.lltv); -invariant onlyEnabledIrm(MorphoHarness.Market market) - isCreated(getMarketId(market)) => isIrmEnabled(market.irm); +invariant onlyEnabledIrm(MorphoHarness.MarketParams marketParams) + isCreated(getMarketId(marketParams)) => isIrmEnabled(marketParams.irm); rule marketIdUnique() { - MorphoHarness.Market market1; - MorphoHarness.Market market2; + MorphoHarness.MarketParams marketParams1; + MorphoHarness.MarketParams marketParams2; - require getMarketId(market1) == getMarketId(market2); + require getMarketId(marketParams1) == getMarketId(marketParams2); - assert market1.borrowableToken == market2.borrowableToken; - assert market1.collateralToken == market2.collateralToken; - assert market1.oracle == market2.oracle; - assert market1.irm == market2.irm; - assert market1.lltv == market2.lltv; + assert marketParams1.borrowableToken == marketParams2.borrowableToken; + assert marketParams1.collateralToken == marketParams2.collateralToken; + assert marketParams1.oracle == marketParams2.oracle; + assert marketParams1.irm == marketParams2.irm; + assert marketParams1.lltv == marketParams2.lltv; } rule onlyUserCanAuthorizeWithoutSig(method f, calldataarg data) @@ -181,28 +174,28 @@ filtered { } rule supplyMovesTokensAndIncreasesShares() { - MorphoHarness.Market market; + MorphoHarness.MarketParams marketParams; uint256 assets; uint256 shares; uint256 suppliedAssets; uint256 suppliedShares; address onbehalf; bytes data; - MorphoHarness.Id id = getMarketId(market); + MorphoHarness.Id id = getMarketId(marketParams); env e; require e.msg.sender != currentContract; - require lastUpdate(id) == e.block.timestamp; + require getLastUpdate(id) == e.block.timestamp; - mathint sharesBefore = supplyShares(id, onbehalf); - mathint balanceBefore = myBalances[market.borrowableToken]; + mathint sharesBefore = getSupplyShares(id, onbehalf); + mathint balanceBefore = myBalances[marketParams.borrowableToken]; - suppliedAssets, suppliedShares = supply(e, market, assets, shares, onbehalf, data); + suppliedAssets, suppliedShares = supply(e, marketParams, assets, shares, onbehalf, data); assert assets != 0 => suppliedAssets == assets && shares == 0; assert assets == 0 => suppliedShares == shares && shares != 0; - mathint sharesAfter = supplyShares(id, onbehalf); - mathint balanceAfter = myBalances[market.borrowableToken]; + mathint sharesAfter = getSupplyShares(id, onbehalf); + mathint balanceAfter = myBalances[marketParams.borrowableToken]; assert sharesAfter == sharesBefore + suppliedShares; assert balanceAfter == balanceBefore + suppliedAssets; } @@ -210,46 +203,46 @@ rule supplyMovesTokensAndIncreasesShares() { rule userCannotLoseSupplyShares(method f, calldataarg data) filtered { f -> !f.isView } { - MorphoHarness.Market market; + MorphoHarness.MarketParams marketParams; uint256 assets; uint256 shares; uint256 suppliedAssets; uint256 suppliedShares; address user; - MorphoHarness.Id id = getMarketId(market); + MorphoHarness.Id id = getMarketId(marketParams); env e; require !isAuthorized(user, e.msg.sender); require user != e.msg.sender; - mathint sharesBefore = supplyShares(id, user); + mathint sharesBefore = getSupplyShares(id, user); f(e, data); - mathint sharesAfter = supplyShares(id, user); + mathint sharesAfter = getSupplyShares(id, user); assert sharesAfter >= sharesBefore; } rule userCannotGainBorrowShares(method f, calldataarg data) filtered { f -> !f.isView } { - MorphoHarness.Market market; + MorphoHarness.MarketParams marketParams; uint256 assets; uint256 shares; uint256 suppliedAssets; uint256 suppliedShares; address user; - MorphoHarness.Id id = getMarketId(market); + MorphoHarness.Id id = getMarketId(marketParams); env e; require !isAuthorized(user, e.msg.sender); require user != e.msg.sender; - mathint sharesBefore = borrowShares(id, user); + mathint sharesBefore = getBorrowShares(id, user); f(e, data); - mathint sharesAfter = borrowShares(id, user); + mathint sharesAfter = getBorrowShares(id, user); assert sharesAfter <= sharesBefore; } @@ -257,24 +250,24 @@ filtered { f -> !f.isView } rule userWithoutBorrowCannotLoseCollateral(method f, calldataarg data) filtered { f -> !f.isView } { - MorphoHarness.Market market; + MorphoHarness.MarketParams marketParams; uint256 assets; uint256 shares; uint256 suppliedAssets; uint256 suppliedShares; address user; - MorphoHarness.Id id = getMarketId(market); + MorphoHarness.Id id = getMarketId(marketParams); env e; require !isAuthorized(user, e.msg.sender); require user != e.msg.sender; - require borrowShares(id, user) == 0; - mathint collateralBefore = collateral(id, user); + require getBorrowShares(id, user) == 0; + mathint collateralBefore = getCollateral(id, user); f(e, data); - mathint collateralAfter = collateral(id, user); - assert borrowShares(id, user) == 0; + mathint collateralAfter = getCollateral(id, user); + assert getBorrowShares(id, user) == 0; assert collateralAfter >= collateralBefore; } @@ -282,32 +275,32 @@ rule noTimeTravel(method f, env e, calldataarg data) filtered { f -> !f.isView } { MorphoHarness.Id id; - require lastUpdate(id) <= e.block.timestamp; + require getLastUpdate(id) <= e.block.timestamp; f(e, data); - assert lastUpdate(id) <= e.block.timestamp; + assert getLastUpdate(id) <= e.block.timestamp; } rule canWithdrawAll() { - MorphoHarness.Market market; + MorphoHarness.MarketParams marketParams; uint256 withdrawnAssets; uint256 withdrawnShares; address receiver; env e; - MorphoHarness.Id id = getMarketId(market); - uint256 shares = supplyShares(id, e.msg.sender); + MorphoHarness.Id id = getMarketId(marketParams); + uint256 shares = getSupplyShares(id, e.msg.sender); require isCreated(id); require e.msg.sender != 0; require receiver != 0; require e.msg.value == 0; require shares > 0; - require totalBorrow(id) == 0; - require lastUpdate(id) <= e.block.timestamp; - require shares < totalSupplyShares(id); - require totalSupplyShares(id) < 10^40 && totalSupply(id) < 10^30; + require getTotalBorrowAssets(id) == 0; + require getLastUpdate(id) <= e.block.timestamp; + require shares < getTotalSupplyShares(id); + require getTotalSupplyShares(id) < 10^40 && getTotalSupplyAssets(id) < 10^30; - withdrawnAssets, withdrawnShares = withdraw@withrevert(e, market, 0, shares, e.msg.sender, receiver); + withdrawnAssets, withdrawnShares = withdraw@withrevert(e, marketParams, 0, shares, e.msg.sender, receiver); assert withdrawnShares == shares; assert !lastReverted, "Can withdraw all assets if nobody borrows"; diff --git a/certora/specs/BlueExitLiquidity.spec b/certora/specs/BlueExitLiquidity.spec index bca86c35d..b8f1e6337 100644 --- a/certora/specs/BlueExitLiquidity.spec +++ b/certora/specs/BlueExitLiquidity.spec @@ -1,62 +1,62 @@ methods { - function supplyShares(MorphoHarness.Id, address) external returns uint256 envfree; - function getVirtualTotalSupply(MorphoHarness.Id) external returns uint256 envfree; + function getSupplyShares(MorphoHarness.Id, address) external returns uint256 envfree; + function getBorrowShares(MorphoHarness.Id, address) external returns uint256 envfree; + function getCollateral(MorphoHarness.Id, address) external returns uint256 envfree; + function getVirtualTotalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; function getVirtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; - function borrowShares(MorphoHarness.Id, address) external returns uint256 envfree; - function getVirtualTotalBorrow(MorphoHarness.Id) external returns uint256 envfree; + function getVirtualTotalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree; function getVirtualTotalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; - function collateral(MorphoHarness.Id, address) external returns uint256 envfree; - function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; + function getLastUpdate(MorphoHarness.Id) external returns uint256 envfree; function mathLibMulDivDown(uint256, uint256, uint256) external returns uint256 envfree; function mathLibMulDivUp(uint256, uint256, uint256) external returns uint256 envfree; - function getMarketId(MorphoHarness.Market) external returns MorphoHarness.Id envfree; + function getMarketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; } -rule withdrawLiquidity(MorphoHarness.Market market, uint256 assets, uint256 shares, address onBehalf, address receiver) { +rule withdrawLiquidity(MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) { env e; - MorphoHarness.Id id = getMarketId(market); + MorphoHarness.Id id = getMarketId(marketParams); - require lastUpdate(id) == e.block.timestamp; + require getLastUpdate(id) == e.block.timestamp; - uint256 initialShares = supplyShares(id, onBehalf); - uint256 initialTotalSupply = getVirtualTotalSupply(id); + uint256 initialShares = getSupplyShares(id, onBehalf); + uint256 initialTotalSupply = getVirtualTotalSupplyAssets(id); uint256 initialTotalSupplyShares = getVirtualTotalSupplyShares(id); uint256 owedAssets = mathLibMulDivDown(initialShares, initialTotalSupply, initialTotalSupplyShares); uint256 withdrawnAssets; - withdrawnAssets, _ = withdraw(e, market, assets, shares, onBehalf, receiver); + withdrawnAssets, _ = withdraw(e, marketParams, assets, shares, onBehalf, receiver); assert withdrawnAssets <= owedAssets; } -rule withdrawCollateralLiquidity(MorphoHarness.Market market, uint256 assets, address onBehalf, address receiver) { +rule withdrawCollateralLiquidity(MorphoHarness.MarketParams marketParams, uint256 assets, address onBehalf, address receiver) { env e; - MorphoHarness.Id id = getMarketId(market); + MorphoHarness.Id id = getMarketId(marketParams); - uint256 initialCollateral = collateral(id, onBehalf); + uint256 initialCollateral = getCollateral(id, onBehalf); - withdrawCollateral(e, market, assets, onBehalf, receiver); + withdrawCollateral(e, marketParams, assets, onBehalf, receiver); assert assets <= initialCollateral; } -rule repayLiquidity(MorphoHarness.Market market, uint256 assets, uint256 shares, address onBehalf, bytes data) { +rule repayLiquidity(MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { env e; - MorphoHarness.Id id = getMarketId(market); + MorphoHarness.Id id = getMarketId(marketParams); - require lastUpdate(id) == e.block.timestamp; + require getLastUpdate(id) == e.block.timestamp; - uint256 initialShares = borrowShares(id, onBehalf); - uint256 initialTotalBorrow = getVirtualTotalBorrow(id); + uint256 initialShares = getBorrowShares(id, onBehalf); + uint256 initialTotalBorrow = getVirtualTotalBorrowAssets(id); uint256 initialTotalBorrowShares = getVirtualTotalBorrowShares(id); uint256 assetsDue = mathLibMulDivUp(initialShares, initialTotalBorrow, initialTotalBorrowShares); uint256 repaidAssets; - repaidAssets, _ = repay(e, market, assets, shares, onBehalf, data); + repaidAssets, _ = repay(e, marketParams, assets, shares, onBehalf, data); - require borrowShares(id, onBehalf) == 0; + require getBorrowShares(id, onBehalf) == 0; assert repaidAssets >= assetsDue; } diff --git a/certora/specs/BlueRatioMath.spec b/certora/specs/BlueRatioMath.spec index d9cb70979..5c0996588 100644 --- a/certora/specs/BlueRatioMath.spec +++ b/certora/specs/BlueRatioMath.spec @@ -1,17 +1,17 @@ methods { - function getMarketId(MorphoHarness.Market) external returns MorphoHarness.Id envfree; - function totalSupply(MorphoHarness.Id) external returns uint256 envfree; - function totalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; - function totalBorrow(MorphoHarness.Id) external returns uint256 envfree; - function totalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; - function fee(MorphoHarness.Id) external returns uint256 envfree; - function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; + function getMarketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; + function getVirtualTotalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; + function getVirtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; + function getVirtualTotalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree; + function getVirtualTotalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; + function getFee(MorphoHarness.Id) external returns uint256 envfree; + function getLastUpdate(MorphoHarness.Id) external returns uint256 envfree; function MathLib.mulDivDown(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivDown(a,b,c); function MathLib.mulDivUp(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivUp(a,b,c); function MathLib.wTaylorCompounded(uint256, uint256) internal returns uint256 => NONDET; - function _.borrowRate(MorphoHarness.Market) external => HAVOC_ECF; + function _.borrowRate(MorphoHarness.MarketParams) external => HAVOC_ECF; function VIRTUAL_ASSETS() external returns uint256 envfree; function VIRTUAL_SHARES() external returns uint256 envfree; @@ -19,7 +19,7 @@ methods { } invariant feeInRange(MorphoHarness.Id id) - to_mathint(fee(id)) <= MAX_FEE(); + getFee(id) <= MAX_FEE(); // This is a simple overapproximative summary, stating that it rounds in the right direction. // The summary is checked by the specification in BlueRatioMathSummary.spec. @@ -38,38 +38,38 @@ function summaryMulDivDown(uint256 x, uint256 y, uint256 d) returns uint256 { } rule accrueInterestsIncreasesSupplyRatio() { - MorphoHarness.Market market; + MorphoHarness.MarketParams marketParams; MorphoHarness.Id id; requireInvariant feeInRange(id); - mathint assetsBefore = totalSupply(id) + VIRTUAL_ASSETS(); - mathint sharesBefore = totalSupplyShares(id) + VIRTUAL_SHARES(); + mathint assetsBefore = getVirtualTotalSupplyAssets(id); + mathint sharesBefore = getVirtualTotalSupplyShares(id); // The check is done for every market, not just for id. env e; - accrueInterests(e, market); + accrueInterest(e, marketParams); - mathint assetsAfter = totalSupply(id) + VIRTUAL_ASSETS(); - mathint sharesAfter = totalSupplyShares(id) + VIRTUAL_SHARES(); + mathint assetsAfter = getVirtualTotalSupplyAssets(id); + mathint sharesAfter = getVirtualTotalSupplyShares(id); // Check if ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter assert assetsBefore * sharesAfter <= assetsAfter * sharesBefore; } rule accrueInterestsIncreasesBorrowRatio() { - MorphoHarness.Market market; + MorphoHarness.MarketParams marketParams; MorphoHarness.Id id; requireInvariant feeInRange(id); - mathint assetsBefore = totalBorrow(id) + VIRTUAL_ASSETS(); - mathint sharesBefore = totalBorrowShares(id) + VIRTUAL_SHARES(); + mathint assetsBefore = getVirtualTotalBorrowAssets(id); + mathint sharesBefore = getVirtualTotalBorrowShares(id); - // The check is done for every market, not just for id. + // The check is done for every marketParams, not just for id. env e; - accrueInterests(e, market); + accrueInterest(e, marketParams); - mathint assetsAfter = totalBorrow(id) + VIRTUAL_ASSETS(); - mathint sharesAfter = totalBorrowShares(id) + VIRTUAL_SHARES(); + mathint assetsAfter = getVirtualTotalBorrowAssets(id); + mathint sharesAfter = getVirtualTotalBorrowShares(id); // Check if ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter assert assetsBefore * sharesAfter <= assetsAfter * sharesBefore; @@ -78,23 +78,23 @@ rule accrueInterestsIncreasesBorrowRatio() { rule onlyLiquidateCanDecreaseSupplyRatio(env e, method f, calldataarg args) filtered { - f -> !f.isView && f.selector != sig:liquidate(MorphoHarness.Market, address, uint256, bytes).selector + f -> !f.isView && f.selector != sig:liquidate(MorphoHarness.MarketParams, address, uint256, bytes).selector } { MorphoHarness.Id id; requireInvariant feeInRange(id); - mathint assetsBefore = totalSupply(id) + VIRTUAL_ASSETS(); - mathint sharesBefore = totalSupplyShares(id) + VIRTUAL_SHARES(); + mathint assetsBefore = getVirtualTotalSupplyAssets(id); + mathint sharesBefore = getVirtualTotalSupplyShares(id); // Interest is checked separately by the rules above. // Here we assume interest has already been accumulated for this block. - require lastUpdate(id) == e.block.timestamp; + require getLastUpdate(id) == e.block.timestamp; f(e,args); - mathint assetsAfter = totalSupply(id) + VIRTUAL_ASSETS(); - mathint sharesAfter = totalSupplyShares(id) + VIRTUAL_SHARES(); + mathint assetsAfter = getVirtualTotalSupplyAssets(id); + mathint sharesAfter = getVirtualTotalSupplyShares(id); // Check if ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter assert assetsBefore * sharesAfter <= assetsAfter * sharesBefore; @@ -106,16 +106,16 @@ filtered { f -> !f.isView } MorphoHarness.Id id; requireInvariant feeInRange(id); - mathint assetsBefore = totalBorrow(id) + VIRTUAL_ASSETS(); - mathint sharesBefore = totalBorrowShares(id) + VIRTUAL_SHARES(); + mathint assetsBefore = getVirtualTotalBorrowAssets(id); + mathint sharesBefore = getVirtualTotalBorrowShares(id); // Interest would increase borrow ratio, so we need to assume no time passes. - require lastUpdate(id) == e.block.timestamp; + require getLastUpdate(id) == e.block.timestamp; f(e,args); - mathint assetsAfter = totalBorrow(id) + VIRTUAL_ASSETS(); - mathint sharesAfter = totalBorrowShares(id) + VIRTUAL_SHARES(); + mathint assetsAfter = getVirtualTotalBorrowAssets(id); + mathint sharesAfter = getVirtualTotalBorrowShares(id); // Check if ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter assert assetsBefore * sharesAfter >= assetsAfter * sharesBefore; diff --git a/certora/specs/BlueReverts.spec b/certora/specs/BlueReverts.spec index 92215872b..804d2af00 100644 --- a/certora/specs/BlueReverts.spec +++ b/certora/specs/BlueReverts.spec @@ -1,36 +1,35 @@ methods { function owner() external returns address envfree; - function totalSupply(MorphoHarness.Id) external returns uint256 envfree; - function totalBorrow(MorphoHarness.Id) external returns uint256 envfree; - function totalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; - function totalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; - function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; + function getTotalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; + function getTotalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree; + function getTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; + function getTotalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; function isAuthorized(address, address) external returns bool envfree; - function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; + function getLastUpdate(MorphoHarness.Id) external returns uint256 envfree; function isLltvEnabled(uint256) external returns bool envfree; function isIrmEnabled(address) external returns bool envfree; - function getMarketId(MorphoHarness.Market) external returns MorphoHarness.Id envfree; + function getMarketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; function MAX_FEE() external returns uint256 envfree; function WAD() external returns uint256 envfree; } definition isCreated(MorphoHarness.Id id) returns bool = - (lastUpdate(id) != 0); + (getLastUpdate(id) != 0); ghost mapping(MorphoHarness.Id => mathint) sumCollateral { init_state axiom (forall MorphoHarness.Id id. sumCollateral[id] == 0); } -hook Sstore collateral[KEY MorphoHarness.Id id][KEY address owner] uint256 newAmount (uint256 oldAmount) STORAGE { +hook Sstore user[KEY MorphoHarness.Id id][KEY address owner].collateral uint128 newAmount (uint128 oldAmount) STORAGE { sumCollateral[id] = sumCollateral[id] - oldAmount + newAmount; } definition emptyMarket(MorphoHarness.Id id) returns bool = - totalSupply(id) == 0 && - totalSupplyShares(id) == 0 && - totalBorrow(id) == 0 && - totalBorrowShares(id) == 0 && + getTotalSupplyAssets(id) == 0 && + getTotalSupplyShares(id) == 0 && + getTotalBorrowAssets(id) == 0 && + getTotalBorrowShares(id) == 0 && sumCollateral[id] == 0; definition exactlyOneZero(uint256 assets, uint256 shares) returns bool = @@ -67,10 +66,10 @@ rule enableLltvRevertCondition(env e, uint256 lltv) { } // setFee can also revert if the accrueInterests reverts. -rule setFeeInputValidation(env e, MorphoHarness.Market market, uint256 newFee) { +rule setFeeInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 newFee) { address oldOwner = owner(); - MorphoHarness.Id id = getMarketId(market); - setFee@withrevert(e, market, newFee); + MorphoHarness.Id id = getMarketId(marketParams); + setFee@withrevert(e, marketParams, newFee); bool hasReverted = lastReverted; assert e.msg.value != 0 || e.msg.sender != oldOwner || !isCreated(id) || newFee > MAX_FEE() => hasReverted; } @@ -81,49 +80,49 @@ rule setFeeRecipientRevertCondition(env e, address recipient) { assert lastReverted <=> e.msg.value != 0 || e.msg.sender != oldOwner; } -rule createMarketRevertCondition(env e, MorphoHarness.Market market) { - MorphoHarness.Id id = getMarketId(market); - createMarket@withrevert(e, market); - assert lastReverted <=> e.msg.value != 0 || !isIrmEnabled(market.irm) || !isLltvEnabled(market.lltv) || lastUpdate(id) != 0; +rule createMarketRevertCondition(env e, MorphoHarness.MarketParams marketParams) { + MorphoHarness.Id id = getMarketId(marketParams); + createMarket@withrevert(e, marketParams); + assert lastReverted <=> e.msg.value != 0 || !isIrmEnabled(marketParams.irm) || !isLltvEnabled(marketParams.lltv) || getLastUpdate(id) != 0; } -rule supplyInputValidation(env e, MorphoHarness.Market market, uint256 assets, uint256 shares, address onBehalf, bytes b) { - supply@withrevert(e, market, assets, shares, onBehalf, b); +rule supplyInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes b) { + supply@withrevert(e, marketParams, assets, shares, onBehalf, b); assert !exactlyOneZero(assets, shares) || onBehalf == 0 => lastReverted; } -rule withdrawInputValidation(env e, MorphoHarness.Market market, uint256 assets, uint256 shares, address onBehalf, address receiver) { +rule withdrawInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) { require e.msg.sender != 0; requireInvariant zeroDoesNotAuthorize(e.msg.sender); - withdraw@withrevert(e, market, assets, shares, onBehalf, receiver); + withdraw@withrevert(e, marketParams, assets, shares, onBehalf, receiver); assert !exactlyOneZero(assets, shares) || onBehalf == 0 => lastReverted; } -rule borrowInputValidation(env e, MorphoHarness.Market market, uint256 assets, uint256 shares, address onBehalf, address receiver) { +rule borrowInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) { require e.msg.sender != 0; requireInvariant zeroDoesNotAuthorize(e.msg.sender); - borrow@withrevert(e, market, assets, shares, onBehalf, receiver); + borrow@withrevert(e, marketParams, assets, shares, onBehalf, receiver); assert !exactlyOneZero(assets, shares) || onBehalf == 0 => lastReverted; } -rule repayInputValidation(env e, MorphoHarness.Market market, uint256 assets, uint256 shares, address onBehalf, bytes b) { - repay@withrevert(e, market, assets, shares, onBehalf, b); +rule repayInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes b) { + repay@withrevert(e, marketParams, assets, shares, onBehalf, b); assert !exactlyOneZero(assets, shares) || onBehalf == 0 => lastReverted; } -rule supplyCollateralInputValidation(env e, MorphoHarness.Market market, uint256 assets, address onBehalf, bytes b) { - supplyCollateral@withrevert(e, market, assets, onBehalf, b); +rule supplyCollateralInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 assets, address onBehalf, bytes b) { + supplyCollateral@withrevert(e, marketParams, assets, onBehalf, b); assert assets == 0 || onBehalf == 0 => lastReverted; } -rule withdrawCollateralInputValidation(env e, MorphoHarness.Market market, uint256 assets, address onBehalf, address receiver) { +rule withdrawCollateralInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 assets, address onBehalf, address receiver) { require e.msg.sender != 0; requireInvariant zeroDoesNotAuthorize(e.msg.sender); - withdrawCollateral@withrevert(e, market, assets, onBehalf, receiver); + withdrawCollateral@withrevert(e, marketParams, assets, onBehalf, receiver); assert assets == 0 || onBehalf == 0 => lastReverted; } -rule liquidateInputValidation(env e, MorphoHarness.Market market, address borrower, uint256 seized, bytes b) { - liquidate@withrevert(e, market, borrower, seized, b); +rule liquidateInputValidation(env e, MorphoHarness.MarketParams marketParams, address borrower, uint256 seized, bytes b) { + liquidate@withrevert(e, marketParams, borrower, seized, b); assert seized == 0 => lastReverted; } diff --git a/certora/specs/DifficultMath.spec b/certora/specs/DifficultMath.spec index df5699149..0499394ef 100644 --- a/certora/specs/DifficultMath.spec +++ b/certora/specs/DifficultMath.spec @@ -1,6 +1,6 @@ methods { - function getMarketId(MorphoHarness.Market) external returns MorphoHarness.Id envfree; - function getVirtualTotalSupply(MorphoHarness.Id) external returns uint256 envfree; + function getMarketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; + function getVirtualTotalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; function getVirtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; function MathLib.mulDivDown(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivDown(a,b,c); function MathLib.mulDivUp(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivUp(a,b,c); @@ -19,7 +19,7 @@ function summaryMulDivDown(uint256 x, uint256 y, uint256 d) returns uint256 { // There should be no profit from supply followed immediately by withdraw. rule supplyWithdraw() { - MorphoHarness.Market market; + MorphoHarness.MarketParams marketParams; uint256 assets; uint256 shares; uint256 suppliedAssets; @@ -34,13 +34,13 @@ rule supplyWithdraw() { require e1.block.timestamp == e2.block.timestamp; - suppliedAssets, suppliedShares = supply(e1, market, assets, shares, onbehalf, data); + suppliedAssets, suppliedShares = supply(e1, marketParams, assets, shares, onbehalf, data); - MorphoHarness.Id id = getMarketId(market); - assert suppliedAssets * (getVirtualTotalSupplyShares(id) - suppliedShares) >= suppliedShares * (getVirtualTotalSupply(id) - suppliedAssets); - assert suppliedAssets * getVirtualTotalSupplyShares(id) >= suppliedShares * getVirtualTotalSupply(id); + MorphoHarness.Id id = getMarketId(marketParams); + assert suppliedAssets * (getVirtualTotalSupplyShares(id) - suppliedShares) >= suppliedShares * (getVirtualTotalSupplyAssets(id) - suppliedAssets); + assert suppliedAssets * getVirtualTotalSupplyShares(id) >= suppliedShares * getVirtualTotalSupplyAssets(id); - withdrawnAssets, withdrawnShares = withdraw(e2, market, 0, suppliedShares, onbehalf, receiver); + withdrawnAssets, withdrawnShares = withdraw(e2, marketParams, 0, suppliedShares, onbehalf, receiver); assert withdrawnShares == suppliedShares; assert withdrawnAssets <= suppliedAssets; @@ -48,7 +48,7 @@ rule supplyWithdraw() { // There should be no profit from withdraw followed immediately by supply. rule withdrawSupply() { - MorphoHarness.Market market; + MorphoHarness.MarketParams marketParams; uint256 assets; uint256 shares; uint256 suppliedAssets; @@ -63,13 +63,13 @@ rule withdrawSupply() { require e1.block.timestamp == e2.block.timestamp; - withdrawnAssets, withdrawnShares = withdraw(e2, market, assets, shares, onbehalf, receiver); + withdrawnAssets, withdrawnShares = withdraw(e2, marketParams, assets, shares, onbehalf, receiver); - MorphoHarness.Id id = getMarketId(market); - assert withdrawnAssets * (getVirtualTotalSupplyShares(id) + withdrawnShares) <= withdrawnShares * (getVirtualTotalSupply(id) + withdrawnAssets); - assert withdrawnAssets * getVirtualTotalSupplyShares(id) <= withdrawnShares * getVirtualTotalSupply(id); + MorphoHarness.Id id = getMarketId(marketParams); + assert withdrawnAssets * (getVirtualTotalSupplyShares(id) + withdrawnShares) <= withdrawnShares * (getVirtualTotalSupplyAssets(id) + withdrawnAssets); + assert withdrawnAssets * getVirtualTotalSupplyShares(id) <= withdrawnShares * getVirtualTotalSupplyAssets(id); - suppliedAssets, suppliedShares = supply(e1, market, withdrawnAssets, 0, onbehalf, data); + suppliedAssets, suppliedShares = supply(e1, marketParams, withdrawnAssets, 0, onbehalf, data); assert suppliedAssets == withdrawnAssets && withdrawnShares >= suppliedShares; } diff --git a/certora/specs/Health.spec b/certora/specs/Health.spec index 116e908ee..a42d1660f 100644 --- a/certora/specs/Health.spec +++ b/certora/specs/Health.spec @@ -1,9 +1,9 @@ methods { - function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; - function collateral(MorphoHarness.Id, address) external returns uint256 envfree; - function isHealthy(MorphoHarness.Market, address user) external returns bool envfree; + function getLastUpdate(MorphoHarness.Id) external returns uint256 envfree; + function getCollateral(MorphoHarness.Id, address) external returns uint256 envfree; + function isHealthy(MorphoHarness.MarketParams, address user) external returns bool envfree; function isAuthorized(address, address user) external returns bool envfree; - function getMarketId(MorphoHarness.Market) external returns MorphoHarness.Id envfree; + function getMarketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; function _.price() external => mockPrice() expect uint256; function MathLib.mulDivDown(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivDown(a,b,c); function MathLib.mulDivUp(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivUp(a,b,c); @@ -39,19 +39,19 @@ filtered { f -> !f.isView } { - MorphoHarness.Market market; - MorphoHarness.Id id = getMarketId(market); + MorphoHarness.MarketParams marketParams; + MorphoHarness.Id id = getMarketId(marketParams); address user; - require isHealthy(market, user); - require market.lltv < 10^18; - require market.lltv > 0; - require lastUpdate(id) == e.block.timestamp; + require isHealthy(marketParams, user); + require marketParams.lltv < 10^18; + require marketParams.lltv > 0; + require getLastUpdate(id) == e.block.timestamp; priceChanged = false; f(e, data); - bool stillHealthy = isHealthy(market, user); + bool stillHealthy = isHealthy(marketParams, user); assert !priceChanged => stillHealthy; } @@ -60,25 +60,25 @@ filtered { f -> !f.isView } { - MorphoHarness.Market market; + MorphoHarness.MarketParams marketParams; uint256 assets; uint256 shares; uint256 suppliedAssets; uint256 suppliedShares; address user; - MorphoHarness.Id id = getMarketId(market); + MorphoHarness.Id id = getMarketId(marketParams); env e; require !isAuthorized(user, e.msg.sender); require user != e.msg.sender; - require lastUpdate(id) == e.block.timestamp; - require isHealthy(market, user); - mathint collateralBefore = collateral(id, user); + require getLastUpdate(id) == e.block.timestamp; + require isHealthy(marketParams, user); + mathint collateralBefore = getCollateral(id, user); priceChanged = false; f(e, data); require !priceChanged; - mathint collateralAfter = collateral(id, user); + mathint collateralAfter = getCollateral(id, user); assert collateralAfter >= collateralBefore; } From daa6a3bf6cab41e33f8b1363b72a753005a3dd37 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Sun, 20 Aug 2023 09:53:40 +0200 Subject: [PATCH 060/204] fix: update in verifyReverts --- certora/specs/BlueReverts.spec | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/certora/specs/BlueReverts.spec b/certora/specs/BlueReverts.spec index 804d2af00..b1cf80589 100644 --- a/certora/specs/BlueReverts.spec +++ b/certora/specs/BlueReverts.spec @@ -37,7 +37,12 @@ definition exactlyOneZero(uint256 assets, uint256 shares) returns bool = // This invariant catches bugs when not checking that the market is created with lastUpdate. invariant notInitializedEmpty(MorphoHarness.Id id) - !isCreated(id) => emptyMarket(id); + !isCreated(id) => emptyMarket(id) +{ + preserved with (env e) { + require e.block.timestamp < 2^128; + } +} invariant zeroDoesNotAuthorize(address authorized) !isAuthorized(0, authorized) @@ -67,11 +72,12 @@ rule enableLltvRevertCondition(env e, uint256 lltv) { // setFee can also revert if the accrueInterests reverts. rule setFeeInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 newFee) { - address oldOwner = owner(); MorphoHarness.Id id = getMarketId(marketParams); + address oldOwner = owner(); + bool wasCreated = isCreated(id); setFee@withrevert(e, marketParams, newFee); bool hasReverted = lastReverted; - assert e.msg.value != 0 || e.msg.sender != oldOwner || !isCreated(id) || newFee > MAX_FEE() => hasReverted; + assert e.msg.value != 0 || e.msg.sender != oldOwner || !wasCreated || newFee > MAX_FEE() => hasReverted; } rule setFeeRecipientRevertCondition(env e, address recipient) { @@ -82,8 +88,11 @@ rule setFeeRecipientRevertCondition(env e, address recipient) { rule createMarketRevertCondition(env e, MorphoHarness.MarketParams marketParams) { MorphoHarness.Id id = getMarketId(marketParams); + bool irmEnabled = isIrmEnabled(marketParams.irm); + bool lltvEnabled = isLltvEnabled(marketParams.lltv); + uint256 lastUpdated = getLastUpdate(id); createMarket@withrevert(e, marketParams); - assert lastReverted <=> e.msg.value != 0 || !isIrmEnabled(marketParams.irm) || !isLltvEnabled(marketParams.lltv) || getLastUpdate(id) != 0; + assert lastReverted <=> e.msg.value != 0 || !irmEnabled || !lltvEnabled || lastUpdated != 0; } rule supplyInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes b) { From 13321409c67ec1bc5e6c6de98615d0fec2f15b4f Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Sun, 20 Aug 2023 10:21:13 +0200 Subject: [PATCH 061/204] fix: reentrancy check --- certora/specs/DifficultMath.spec | 4 ++++ certora/specs/Reentrancy.spec | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/certora/specs/DifficultMath.spec b/certora/specs/DifficultMath.spec index 0499394ef..a3927446b 100644 --- a/certora/specs/DifficultMath.spec +++ b/certora/specs/DifficultMath.spec @@ -33,6 +33,8 @@ rule supplyWithdraw() { env e2; require e1.block.timestamp == e2.block.timestamp; + require e1.block.timestamp < 2^128; + require e2.block.timestamp < 2^128; suppliedAssets, suppliedShares = supply(e1, marketParams, assets, shares, onbehalf, data); @@ -62,6 +64,8 @@ rule withdrawSupply() { env e2; require e1.block.timestamp == e2.block.timestamp; + require e1.block.timestamp < 2^128; + require e2.block.timestamp < 2^128; withdrawnAssets, withdrawnShares = withdraw(e2, marketParams, assets, shares, onbehalf, receiver); diff --git a/certora/specs/Reentrancy.spec b/certora/specs/Reentrancy.spec index 3bf0d9ecf..b3bd0ce9d 100644 --- a/certora/specs/Reentrancy.spec +++ b/certora/specs/Reentrancy.spec @@ -1,5 +1,5 @@ methods { - function _.borrowRate(MorphoHarness.Market market) external => summaryBorrowRate(market) expect uint256; + function _.borrowRate(MorphoHarness.MarketParams marketParams) external => summaryBorrowRate(marketParams) expect uint256; } ghost bool hasAccessedStorage; @@ -9,7 +9,7 @@ ghost bool delegate_call; ghost bool static_call; ghost bool callIsBorrowRate; -function summaryBorrowRate(MorphoHarness.Market market) returns uint256 { +function summaryBorrowRate(MorphoHarness.MarketParams marketParams) returns uint256 { uint256 result; callIsBorrowRate = true; return result; From 4abca55d29ce94dcc583d03c1bb973fb13a897e6 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 23 Aug 2023 07:56:53 +0200 Subject: [PATCH 062/204] chore: remove extSloads for now --- src/Morpho.sol | 18 ------------------ src/interfaces/IMorpho.sol | 3 --- 2 files changed, 21 deletions(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index 65c655cff..d2c19abfb 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -497,22 +497,4 @@ contract Morpho is IMorpho { return maxBorrow >= borrowed; } - - /* STORAGE VIEW */ - - /// @inheritdoc IMorpho - function extsload(bytes32[] calldata slots) external view returns (bytes32[] memory res) { - uint256 nSlots = slots.length; - - res = new bytes32[](nSlots); - - for (uint256 i; i < nSlots;) { - bytes32 slot = slots[i++]; - - /// @solidity memory-safe-assembly - assembly { - mstore(add(res, mul(i, 32)), sload(slot)) - } - } - } } diff --git a/src/interfaces/IMorpho.sol b/src/interfaces/IMorpho.sol index 112c84e50..3568f8e74 100644 --- a/src/interfaces/IMorpho.sol +++ b/src/interfaces/IMorpho.sol @@ -250,7 +250,4 @@ interface IMorpho { /// @notice Accrues interest for `market`. function accrueInterest(MarketParams memory marketParams) external; - - /// @notice Returns the data stored on the different `slots`. - function extsload(bytes32[] memory slots) external view returns (bytes32[] memory res); } From d425b7155c699331f6542e7535c7cd8c09fb3472 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 23 Aug 2023 08:14:48 +0200 Subject: [PATCH 063/204] fix: merge fix and remove extSloads --- certora/harness/MorphoHarness.sol | 6 +++--- src/Morpho.sol | 18 ------------------ 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/certora/harness/MorphoHarness.sol b/certora/harness/MorphoHarness.sol index 482c95049..c7134afe1 100644 --- a/certora/harness/MorphoHarness.sol +++ b/certora/harness/MorphoHarness.sol @@ -1,11 +1,11 @@ -pragma solidity 0.8.21; +pragma solidity 0.8.19; import "../../src/Morpho.sol"; import "../../src/libraries/SharesMathLib.sol"; -import "../../src/libraries/MarketLib.sol"; +import "../../src/libraries/MarketParamsLib.sol"; contract MorphoHarness is Morpho { - using MarketLib for MarketParams; + using MarketParamsLib for MarketParams; constructor(address newOwner) Morpho(newOwner) {} diff --git a/src/Morpho.sol b/src/Morpho.sol index 64cc623c7..01a87fe25 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -496,22 +496,4 @@ contract Morpho is IMorpho { return maxBorrow >= borrowed; } - - /* STORAGE VIEW */ - - /// @inheritdoc IMorpho - function extSloads(bytes32[] calldata slots) external view returns (bytes32[] memory res) { - uint256 nSlots = slots.length; - - res = new bytes32[](nSlots); - - for (uint256 i; i < nSlots;) { - bytes32 slot = slots[i++]; - - /// @solidity memory-safe-assembly - assembly { - mstore(add(res, mul(i, 32)), sload(slot)) - } - } - } } From af78edf57371416a95504578ba3cd2361d646ef6 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 23 Aug 2023 08:20:38 +0200 Subject: [PATCH 064/204] chore: certora ci to 0.8.19 --- .github/workflows/certora.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml index 79f4c1e8b..89ac5e0a7 100644 --- a/.github/workflows/certora.yml +++ b/.github/workflows/certora.yml @@ -26,14 +26,14 @@ jobs: - name: Install solc run: | - wget https://github.com/ethereum/solidity/releases/download/v0.8.21/solc-static-linux + wget https://github.com/ethereum/solidity/releases/download/v0.8.19/solc-static-linux chmod +x solc-static-linux - sudo mv solc-static-linux /usr/local/bin/solc8.21 + sudo mv solc-static-linux /usr/local/bin/solc8.19 - name: Verify rule ${{ matrix.script }} run: | echo "key length" ${#CERTORAKEY} - bash certora/scripts/${{ matrix.script }} --solc solc8.21 + bash certora/scripts/${{ matrix.script }} --solc solc8.19 env: CERTORAKEY: ${{ secrets.CERTORAKEY }} From bb3f0bc50f37bf110450792346b2ef785e85f19e Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 23 Aug 2023 08:34:45 +0200 Subject: [PATCH 065/204] fix: update reverts specs --- certora/specs/BlueReverts.spec | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/certora/specs/BlueReverts.spec b/certora/specs/BlueReverts.spec index b1cf80589..9655ed758 100644 --- a/certora/specs/BlueReverts.spec +++ b/certora/specs/BlueReverts.spec @@ -4,10 +4,12 @@ methods { function getTotalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree; function getTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; function getTotalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; - function isAuthorized(address, address) external returns bool envfree; function getLastUpdate(MorphoHarness.Id) external returns uint256 envfree; + + function feeRecipient() external returns address envfree; function isLltvEnabled(uint256) external returns bool envfree; function isIrmEnabled(address) external returns bool envfree; + function isAuthorized(address, address) external returns bool envfree; function getMarketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; function MAX_FEE() external returns uint256 envfree; @@ -55,19 +57,21 @@ invariant zeroDoesNotAuthorize(address authorized) rule setOwnerRevertCondition(env e, address newOwner) { address oldOwner = owner(); setOwner@withrevert(e, newOwner); - assert lastReverted <=> e.msg.value != 0 || e.msg.sender != oldOwner; + assert lastReverted <=> e.msg.value != 0 || e.msg.sender != oldOwner || newOwner == oldOwner; } rule enableIrmRevertCondition(env e, address irm) { address oldOwner = owner(); + bool oldIsIrmEnabled = isIrmEnabled(irm); enableIrm@withrevert(e, irm); - assert lastReverted <=> e.msg.value != 0 || e.msg.sender != oldOwner; + assert lastReverted <=> e.msg.value != 0 || e.msg.sender != oldOwner || oldIsIrmEnabled; } rule enableLltvRevertCondition(env e, uint256 lltv) { address oldOwner = owner(); + bool oldIsLltvEnabled = isLltvEnabled(lltv); enableLltv@withrevert(e, lltv); - assert lastReverted <=> e.msg.value != 0 || e.msg.sender != oldOwner || lltv >= WAD(); + assert lastReverted <=> e.msg.value != 0 || e.msg.sender != oldOwner || lltv >= WAD() || oldIsLltvEnabled; } // setFee can also revert if the accrueInterests reverts. @@ -80,10 +84,11 @@ rule setFeeInputValidation(env e, MorphoHarness.MarketParams marketParams, uint2 assert e.msg.value != 0 || e.msg.sender != oldOwner || !wasCreated || newFee > MAX_FEE() => hasReverted; } -rule setFeeRecipientRevertCondition(env e, address recipient) { +rule setFeeRecipientRevertCondition(env e, address newFeeRecipient) { address oldOwner = owner(); - setFeeRecipient@withrevert(e, recipient); - assert lastReverted <=> e.msg.value != 0 || e.msg.sender != oldOwner; + address oldFeeRecipient = feeRecipient(); + setFeeRecipient@withrevert(e, newFeeRecipient); + assert lastReverted <=> e.msg.value != 0 || e.msg.sender != oldOwner || newFeeRecipient == oldFeeRecipient; } rule createMarketRevertCondition(env e, MorphoHarness.MarketParams marketParams) { From fe3bbf9181f573d1645840ee50b010a124eb72b6 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 23 Aug 2023 08:47:16 +0200 Subject: [PATCH 066/204] fix: blue ratio math update --- certora/harness/MorphoHarness.sol | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/certora/harness/MorphoHarness.sol b/certora/harness/MorphoHarness.sol index c7134afe1..b4428b4b2 100644 --- a/certora/harness/MorphoHarness.sol +++ b/certora/harness/MorphoHarness.sol @@ -89,6 +89,13 @@ contract MorphoHarness is Morpho { return MathLib.mulDivDown(x, y, d); } + function accrueInterest(MarketParams memory marketParams) external { + Id id = marketParams.id(); + require(market[id].lastUpdate != 0, ErrorsLib.MARKET_NOT_CREATED); + + _accrueInterest(marketParams, id); + } + function isHealthy(MarketParams memory marketParams, address user) external view returns (bool) { return _isHealthy(marketParams, marketParams.id(), user); } From f2ea7d857745a41fac26f068e9d74068a2d6a676 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 23 Aug 2023 15:56:57 +0200 Subject: [PATCH 067/204] refactor: remove allowance of USDC in NoRevert token --- certora/dispatch/ERC20NoRevert.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/certora/dispatch/ERC20NoRevert.sol b/certora/dispatch/ERC20NoRevert.sol index 8904f782c..ab9a3f66a 100644 --- a/certora/dispatch/ERC20NoRevert.sol +++ b/certora/dispatch/ERC20NoRevert.sol @@ -44,8 +44,6 @@ contract ERC20NoRevert { } function approve(address _spender, uint256 _amount) public { - require(!((_amount != 0) && (allowed[msg.sender][_spender] != 0))); - allowed[msg.sender][_spender] = _amount; } From 7869482be8dfa85eff26ff18a9a5d8e13b0083ec Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Wed, 23 Aug 2023 15:57:07 +0200 Subject: [PATCH 068/204] New spec for checking commutativity of accrueInterests We show for supply/withdraw/borrow/repay that calling accrueInterests before has no effect as these function already do this. For all functions we show that calling accrueInterests before or after yields the same result, except for setFeeRecipient, for which it makes a difference (new fee recipient gets the interest if they are accrued afterwards). --- .github/workflows/certora.yml | 1 + certora/scripts/verifyBlueAccrueInterests.sh | 10 ++ certora/specs/BlueAccrueInterests.spec | 166 +++++++++++++++++++ 3 files changed, 177 insertions(+) create mode 100755 certora/scripts/verifyBlueAccrueInterests.sh create mode 100644 certora/specs/BlueAccrueInterests.spec diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml index 6775e9ad0..6ea3eb76d 100644 --- a/.github/workflows/certora.yml +++ b/.github/workflows/certora.yml @@ -44,6 +44,7 @@ jobs: matrix: script: - verifyBlue.sh + - verifyBlueAccrueInterests.sh - verifyBlueRatioMathSummary.sh - verifyBlueExitLiquidity.sh - verifyBlueTransfer.sh diff --git a/certora/scripts/verifyBlueAccrueInterests.sh b/certora/scripts/verifyBlueAccrueInterests.sh new file mode 100755 index 000000000..126128a4f --- /dev/null +++ b/certora/scripts/verifyBlueAccrueInterests.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -euxo pipefail + +certoraRun \ + certora/harness/MorphoHarness.sol \ + --verify MorphoHarness:certora/specs/BlueExitLiquidity.spec \ + --solc_allow_path src \ + --msg "Morpho Blue Commutativity of accrueInterests" \ + "$@" diff --git a/certora/specs/BlueAccrueInterests.spec b/certora/specs/BlueAccrueInterests.spec new file mode 100644 index 000000000..aa5f2f57d --- /dev/null +++ b/certora/specs/BlueAccrueInterests.spec @@ -0,0 +1,166 @@ +methods { + function MathLib.mulDivDown(uint256 a, uint256 b, uint256 c) internal returns uint256 => ghostMulDivDown(a,b,c); + function MathLib.mulDivUp(uint256 a, uint256 b, uint256 c) internal returns uint256 => ghostMulDivUp(a,b,c); + function MathLib.wTaylorCompounded(uint256 a, uint256 b) internal returns uint256 => ghostTaylorCompounded(a, b); + + // we assume here that all external functions will not access storage, since we cannot show + // commutativity otherwise. We also need to assume that the price and borrow rate return + // always the same value (and do not depend on msg.origin), so we use ghost functions for them. + function _.borrowRate(MorphoHarness.Market market) external with (env e) => ghostBorrowRate(market.irm, e.block.timestamp) expect uint256; + function _.price() external with (env e) => ghostOraclePrice(e.block.timestamp) expect uint256; + function _.transfer(address, uint256) external => NONDET; + function _.transferFrom(address, address, uint256) external => NONDET; + function _.onMorphoLiquidate(uint256, bytes) external => NONDET; + function _.onMorphoRepay(uint256, bytes) external => NONDET; + function _.onMorphoSupply(uint256, bytes) external => NONDET; + function _.onMorphoSupplyCollateral(uint256, bytes) external => NONDET; + function _.onMorphoFlashLoan(uint256, bytes) external => NONDET; + + function VIRTUAL_ASSETS() external returns uint256 envfree; + function VIRTUAL_SHARES() external returns uint256 envfree; + function MAX_FEE() external returns uint256 envfree; +} + +ghost ghostMulDivUp(uint256, uint256, uint256) returns uint256; +ghost ghostMulDivDown(uint256, uint256, uint256) returns uint256; +ghost ghostTaylorCompounded(uint256, uint256) returns uint256; +ghost ghostBorrowRate(address, uint256) returns uint256; +ghost ghostOraclePrice(uint256) returns uint256; + +rule supplyAccruesInterests() +{ + env e; + MorphoHarness.Market market; + uint256 assets; + uint256 shares; + address onbehalf; + bytes data; + + storage init = lastStorage; + + // check that calling accrueInterests first has no effect. + // this is because supply should call accrueInterests itself. + + accrueInterests(e, market); + supply(e, market, assets, shares, onbehalf, data); + storage afterBoth = lastStorage; + + supply(e, market, assets, shares, onbehalf, data) at init; + + storage afterOne = lastStorage; + + assert afterBoth == afterOne; +} + +rule withdrawAccruesInterests() +{ + env e; + MorphoHarness.Market market; + uint256 assets; + uint256 shares; + address onbehalf; + address receiver; + + storage init = lastStorage; + + // check that calling accrueInterests first has no effect. + // this is because withdraw should call accrueInterests itself. + + accrueInterests(e, market); + withdraw(e, market, assets, shares, onbehalf, receiver); + storage afterBoth = lastStorage; + + withdraw(e, market, assets, shares, onbehalf, receiver) at init; + + storage afterOne = lastStorage; + + assert afterBoth == afterOne; +} + +rule borrowAccruesInterests() +{ + env e; + MorphoHarness.Market market; + uint256 assets; + uint256 shares; + address onbehalf; + address receiver; + + storage init = lastStorage; + + // check that calling accrueInterests first has no effect. + // this is because borrow should call accrueInterests itself. + + accrueInterests(e, market); + borrow(e, market, assets, shares, onbehalf, receiver); + storage afterBoth = lastStorage; + + borrow(e, market, assets, shares, onbehalf, receiver) at init; + + storage afterOne = lastStorage; + + assert afterBoth == afterOne; +} + +rule repayAccruesInterests() +{ + env e; + MorphoHarness.Market market; + uint256 assets; + uint256 shares; + address onbehalf; + bytes data; + + storage init = lastStorage; + + // check that calling accrueInterests first has no effect. + // this is because repay should call accrueInterests itself. + + accrueInterests(e, market); + repay(e, market, assets, shares, onbehalf, data); + storage afterBoth = lastStorage; + + repay(e, market, assets, shares, onbehalf, data) at init; + + storage afterOne = lastStorage; + + assert afterBoth == afterOne; +} + + +/** + * Show that accrueInterests commutes with other state changing rules. + * We exclude view functions, because (a) we cannot check the return + * value and for storage commutativity is trivial and (b) most view + * functions, e.g. totalSupplyShares, are not commutative, i.e. they return + * a different value if called before accrueInterests is called. + * We also exclude setFeeRecipient, as it is known to be not commutative. + */ +rule accrueInterestsCommutesExceptForSetFeeRecipient(method f, env e, calldataarg args) +filtered { + f -> !f.isView && f.selector != sig:setFeeRecipient(address).selector +} +{ + env e1; + env e2; + MorphoHarness.Market market; + + require e1.block.timestamp == e2.block.timestamp; + + storage init = lastStorage; + + // check that accrueInterests commutes with every other function. + + accrueInterests(e1, market); + f(e2, args); + + storage store1 = lastStorage; + + + f(e2, args) at init; + accrueInterests(e1, market); + + storage store2 = lastStorage; + + assert store1 == store2; +} From 4e2062ae241c1ef275d164041907d30a3992fbd7 Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Wed, 23 Aug 2023 16:35:11 +0200 Subject: [PATCH 069/204] Check revert-equivalence This also requires that transfer/transferFrom behave the same, or otherwise safeTransfer differs in revert behaviour --- certora/specs/BlueAccrueInterests.spec | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/certora/specs/BlueAccrueInterests.spec b/certora/specs/BlueAccrueInterests.spec index aa5f2f57d..2217d44e4 100644 --- a/certora/specs/BlueAccrueInterests.spec +++ b/certora/specs/BlueAccrueInterests.spec @@ -8,8 +8,8 @@ methods { // always the same value (and do not depend on msg.origin), so we use ghost functions for them. function _.borrowRate(MorphoHarness.Market market) external with (env e) => ghostBorrowRate(market.irm, e.block.timestamp) expect uint256; function _.price() external with (env e) => ghostOraclePrice(e.block.timestamp) expect uint256; - function _.transfer(address, uint256) external => NONDET; - function _.transferFrom(address, address, uint256) external => NONDET; + function _.transfer(address to, uint256 amount) external => ghostTransfer(to, amount) expect bool; + function _.transferFrom(address from, address to, uint256 amount) external => ghostTransferFrom(from, to, amount) expect bool; function _.onMorphoLiquidate(uint256, bytes) external => NONDET; function _.onMorphoRepay(uint256, bytes) external => NONDET; function _.onMorphoSupply(uint256, bytes) external => NONDET; @@ -26,6 +26,8 @@ ghost ghostMulDivDown(uint256, uint256, uint256) returns uint256; ghost ghostTaylorCompounded(uint256, uint256) returns uint256; ghost ghostBorrowRate(address, uint256) returns uint256; ghost ghostOraclePrice(uint256) returns uint256; +ghost ghostTransfer(address, uint256) returns bool; +ghost ghostTransferFrom(address, address, uint256) returns bool; rule supplyAccruesInterests() { @@ -152,15 +154,18 @@ filtered { // check that accrueInterests commutes with every other function. accrueInterests(e1, market); - f(e2, args); + f@withrevert(e2, args); + bool revert1 = lastReverted; storage store1 = lastStorage; - f(e2, args) at init; + f@withrevert(e2, args) at init; + bool revert2 = lastReverted; accrueInterests(e1, market); storage store2 = lastStorage; + assert revert1 <=> revert2; assert store1 == store2; } From ebacb120131e3f5323516505ac353d48f13fcde7 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 23 Aug 2023 16:47:13 +0200 Subject: [PATCH 070/204] feat: adding always collateralized invariant --- certora/specs/Health.spec | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/certora/specs/Health.spec b/certora/specs/Health.spec index a42d1660f..b539bba70 100644 --- a/certora/specs/Health.spec +++ b/certora/specs/Health.spec @@ -1,5 +1,6 @@ methods { function getLastUpdate(MorphoHarness.Id) external returns uint256 envfree; + function getBorrowShares(MorphoHarness.Id, address) external returns uint256 envfree; function getCollateral(MorphoHarness.Id, address) external returns uint256 envfree; function isHealthy(MorphoHarness.MarketParams, address user) external returns bool envfree; function isAuthorized(address, address user) external returns bool envfree; @@ -82,3 +83,6 @@ filtered { mathint collateralAfter = getCollateral(id, user); assert collateralAfter >= collateralBefore; } + +invariant alwaysCollateralized(MorphoHarness.Id id, address borrower) + getBorrowShares(id, borrower) != 0 => getCollateral(id, borrower) != 0; From 740266b615b3a2e5e34d0e04edbb96a403728e49 Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Wed, 23 Aug 2023 16:51:48 +0200 Subject: [PATCH 071/204] Fix spec file name --- certora/scripts/verifyBlueAccrueInterests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certora/scripts/verifyBlueAccrueInterests.sh b/certora/scripts/verifyBlueAccrueInterests.sh index 126128a4f..8c6027925 100755 --- a/certora/scripts/verifyBlueAccrueInterests.sh +++ b/certora/scripts/verifyBlueAccrueInterests.sh @@ -4,7 +4,7 @@ set -euxo pipefail certoraRun \ certora/harness/MorphoHarness.sol \ - --verify MorphoHarness:certora/specs/BlueExitLiquidity.spec \ + --verify MorphoHarness:certora/specs/BlueAccrueInterests.spec \ --solc_allow_path src \ --msg "Morpho Blue Commutativity of accrueInterests" \ "$@" From 9281c478a94d76af2944e5621e96ec3a53c0dff4 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 23 Aug 2023 17:17:56 +0200 Subject: [PATCH 072/204] chore: remove extSloads --- certora/scripts/verifyBlueReverts.sh | 1 + src/Morpho.sol | 7 +++++++ src/interfaces/IMorpho.sol | 3 +++ 3 files changed, 11 insertions(+) diff --git a/certora/scripts/verifyBlueReverts.sh b/certora/scripts/verifyBlueReverts.sh index 4fe82cd14..ae50d84ce 100755 --- a/certora/scripts/verifyBlueReverts.sh +++ b/certora/scripts/verifyBlueReverts.sh @@ -4,6 +4,7 @@ set -euxo pipefail certoraRun \ certora/harness/MorphoHarness.sol \ + --solc_via_ir \ --verify MorphoHarness:certora/specs/BlueReverts.spec \ --loop_iter 3 \ --optimistic_loop \ diff --git a/src/Morpho.sol b/src/Morpho.sol index dd048f2a6..b7b81a858 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -483,4 +483,11 @@ contract Morpho is IMorpho { return maxBorrow >= borrowed; } + + /* STORAGE VIEW */ + + /// @inheritdoc IMorpho + function extSloads(bytes32[] calldata slots) external pure returns (bytes32[] memory res) { + return slots; + } } diff --git a/src/interfaces/IMorpho.sol b/src/interfaces/IMorpho.sol index 7f08abd28..21ecbe4f7 100644 --- a/src/interfaces/IMorpho.sol +++ b/src/interfaces/IMorpho.sol @@ -245,4 +245,7 @@ interface IMorpho { /// @param authorization The `Authorization` struct. /// @param signature The signature. function setAuthorizationWithSig(Authorization calldata authorization, Signature calldata signature) external; + + /// @notice Returns the data stored on the different `slots`. + function extSloads(bytes32[] memory slots) external pure returns (bytes32[] memory res); } From 205955501d74e04ceb0a1df3b3f88554dc65bc70 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 24 Aug 2023 08:45:02 +0200 Subject: [PATCH 073/204] fix: update to new liquidate signature --- certora/specs/Blue.spec | 2 +- certora/specs/BlueRatioMath.spec | 2 +- certora/specs/BlueReverts.spec | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/certora/specs/Blue.spec b/certora/specs/Blue.spec index 53a03f331..a7793e829 100644 --- a/certora/specs/Blue.spec +++ b/certora/specs/Blue.spec @@ -131,7 +131,7 @@ invariant isLiquid(address token) requireInvariant marketInvariant(marketParams); require e.msg.sender != currentContract; } - preserved liquidate(MorphoHarness.MarketParams marketParams, address _b, uint256 _s, bytes _d) with (env e) { + preserved liquidate(MorphoHarness.MarketParams marketParams, address _b, uint256 _s, uint256 _r, bytes _d) with (env e) { requireInvariant marketInvariant(marketParams); require e.msg.sender != currentContract; } diff --git a/certora/specs/BlueRatioMath.spec b/certora/specs/BlueRatioMath.spec index 5c0996588..13902fac8 100644 --- a/certora/specs/BlueRatioMath.spec +++ b/certora/specs/BlueRatioMath.spec @@ -78,7 +78,7 @@ rule accrueInterestsIncreasesBorrowRatio() { rule onlyLiquidateCanDecreaseSupplyRatio(env e, method f, calldataarg args) filtered { - f -> !f.isView && f.selector != sig:liquidate(MorphoHarness.MarketParams, address, uint256, bytes).selector + f -> !f.isView && f.selector != sig:liquidate(MorphoHarness.MarketParams, address, uint256, uint256, bytes).selector } { MorphoHarness.Id id; diff --git a/certora/specs/BlueReverts.spec b/certora/specs/BlueReverts.spec index 9655ed758..f8e561c11 100644 --- a/certora/specs/BlueReverts.spec +++ b/certora/specs/BlueReverts.spec @@ -100,8 +100,8 @@ rule createMarketRevertCondition(env e, MorphoHarness.MarketParams marketParams) assert lastReverted <=> e.msg.value != 0 || !irmEnabled || !lltvEnabled || lastUpdated != 0; } -rule supplyInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes b) { - supply@withrevert(e, marketParams, assets, shares, onBehalf, b); +rule supplyInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { + supply@withrevert(e, marketParams, assets, shares, onBehalf, data); assert !exactlyOneZero(assets, shares) || onBehalf == 0 => lastReverted; } @@ -119,13 +119,13 @@ rule borrowInputValidation(env e, MorphoHarness.MarketParams marketParams, uint2 assert !exactlyOneZero(assets, shares) || onBehalf == 0 => lastReverted; } -rule repayInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes b) { - repay@withrevert(e, marketParams, assets, shares, onBehalf, b); +rule repayInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { + repay@withrevert(e, marketParams, assets, shares, onBehalf, data); assert !exactlyOneZero(assets, shares) || onBehalf == 0 => lastReverted; } -rule supplyCollateralInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 assets, address onBehalf, bytes b) { - supplyCollateral@withrevert(e, marketParams, assets, onBehalf, b); +rule supplyCollateralInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 assets, address onBehalf, bytes data) { + supplyCollateral@withrevert(e, marketParams, assets, onBehalf, data); assert assets == 0 || onBehalf == 0 => lastReverted; } @@ -136,7 +136,7 @@ rule withdrawCollateralInputValidation(env e, MorphoHarness.MarketParams marketP assert assets == 0 || onBehalf == 0 => lastReverted; } -rule liquidateInputValidation(env e, MorphoHarness.MarketParams marketParams, address borrower, uint256 seized, bytes b) { - liquidate@withrevert(e, marketParams, borrower, seized, b); +rule liquidateInputValidation(env e, MorphoHarness.MarketParams marketParams, address borrower, uint256 seizedAssets, uint256 repaidShares, bytes data) { + liquidate@withrevert(e, marketParams, borrower, seizedAssets, repaidShares, data); assert seized == 0 => lastReverted; } From e742a36cd6b8840c4c8e1bf89dca2a38344de32f Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 24 Aug 2023 09:52:57 +0200 Subject: [PATCH 074/204] fix: remove via IR compilation for now --- certora/scripts/verifyBlueReverts.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/certora/scripts/verifyBlueReverts.sh b/certora/scripts/verifyBlueReverts.sh index ae50d84ce..4fe82cd14 100755 --- a/certora/scripts/verifyBlueReverts.sh +++ b/certora/scripts/verifyBlueReverts.sh @@ -4,7 +4,6 @@ set -euxo pipefail certoraRun \ certora/harness/MorphoHarness.sol \ - --solc_via_ir \ --verify MorphoHarness:certora/specs/BlueReverts.spec \ --loop_iter 3 \ --optimistic_loop \ From e52ecddfbf0f597f0296d76220c395371ae9bf99 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 24 Aug 2023 10:12:20 +0200 Subject: [PATCH 075/204] fix: require denominator 0 --- certora/specs/Health.spec | 2 ++ 1 file changed, 2 insertions(+) diff --git a/certora/specs/Health.spec b/certora/specs/Health.spec index b539bba70..2cde2c795 100644 --- a/certora/specs/Health.spec +++ b/certora/specs/Health.spec @@ -24,10 +24,12 @@ function mockPrice() returns uint256 { } function summaryMulDivUp(uint256 x, uint256 y, uint256 d) returns uint256 { + require d != 0; return require_uint256((x * y + (d - 1)) / d); } function summaryMulDivDown(uint256 x, uint256 y, uint256 d) returns uint256 { + require d != 0; return require_uint256((x * y) / d); } From 90392ed1488be6a2bf5c333dc2341be73335b319 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 24 Aug 2023 10:14:34 +0200 Subject: [PATCH 076/204] fix: seized renaming --- certora/specs/BlueReverts.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certora/specs/BlueReverts.spec b/certora/specs/BlueReverts.spec index f8e561c11..60f8184a9 100644 --- a/certora/specs/BlueReverts.spec +++ b/certora/specs/BlueReverts.spec @@ -138,5 +138,5 @@ rule withdrawCollateralInputValidation(env e, MorphoHarness.MarketParams marketP rule liquidateInputValidation(env e, MorphoHarness.MarketParams marketParams, address borrower, uint256 seizedAssets, uint256 repaidShares, bytes data) { liquidate@withrevert(e, marketParams, borrower, seizedAssets, repaidShares, data); - assert seized == 0 => lastReverted; + assert seizedAssets == 0 => lastReverted; } From 8aebfd4dd7c2bd5b7925ab88da93e9378528a456 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 24 Aug 2023 10:57:51 +0200 Subject: [PATCH 077/204] fix: liquidate input validation --- certora/specs/BlueReverts.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certora/specs/BlueReverts.spec b/certora/specs/BlueReverts.spec index 60f8184a9..b7a67301c 100644 --- a/certora/specs/BlueReverts.spec +++ b/certora/specs/BlueReverts.spec @@ -138,5 +138,5 @@ rule withdrawCollateralInputValidation(env e, MorphoHarness.MarketParams marketP rule liquidateInputValidation(env e, MorphoHarness.MarketParams marketParams, address borrower, uint256 seizedAssets, uint256 repaidShares, bytes data) { liquidate@withrevert(e, marketParams, borrower, seizedAssets, repaidShares, data); - assert seizedAssets == 0 => lastReverted; + assert !exactlyOneZero(seizedAssets, repaidShares) => lastReverted; } From 2733de7de8a5d5d28a3864e800f4606b63f06dfc Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 24 Aug 2023 11:03:50 +0200 Subject: [PATCH 078/204] fix: adapt borrowRate signature --- certora/specs/Blue.spec | 2 +- certora/specs/BlueRatioMath.spec | 2 +- certora/specs/Reentrancy.spec | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/certora/specs/Blue.spec b/certora/specs/Blue.spec index a7793e829..962ea0bb5 100644 --- a/certora/specs/Blue.spec +++ b/certora/specs/Blue.spec @@ -14,7 +14,7 @@ methods { function isLltvEnabled(uint256) external returns bool envfree; function isIrmEnabled(address) external returns bool envfree; - function _.borrowRate(MorphoHarness.MarketParams) external => HAVOC_ECF; + function _.borrowRate(MorphoHarness.MarketParams, MorphoHarness.Market) external => HAVOC_ECF; function MAX_FEE() external returns uint256 envfree; diff --git a/certora/specs/BlueRatioMath.spec b/certora/specs/BlueRatioMath.spec index 13902fac8..f96baea7c 100644 --- a/certora/specs/BlueRatioMath.spec +++ b/certora/specs/BlueRatioMath.spec @@ -11,7 +11,7 @@ methods { function MathLib.mulDivUp(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivUp(a,b,c); function MathLib.wTaylorCompounded(uint256, uint256) internal returns uint256 => NONDET; - function _.borrowRate(MorphoHarness.MarketParams) external => HAVOC_ECF; + function _.borrowRate(MorphoHarness.MarketParams, MorphoHarness.Market) external => HAVOC_ECF; function VIRTUAL_ASSETS() external returns uint256 envfree; function VIRTUAL_SHARES() external returns uint256 envfree; diff --git a/certora/specs/Reentrancy.spec b/certora/specs/Reentrancy.spec index b3bd0ce9d..86ad76679 100644 --- a/certora/specs/Reentrancy.spec +++ b/certora/specs/Reentrancy.spec @@ -1,5 +1,5 @@ methods { - function _.borrowRate(MorphoHarness.MarketParams marketParams) external => summaryBorrowRate(marketParams) expect uint256; + function _.borrowRate(MorphoHarness.MarketParams marketParams, MorphoHarness.Market) external => summaryBorrowRate() expect uint256; } ghost bool hasAccessedStorage; @@ -9,7 +9,7 @@ ghost bool delegate_call; ghost bool static_call; ghost bool callIsBorrowRate; -function summaryBorrowRate(MorphoHarness.MarketParams marketParams) returns uint256 { +function summaryBorrowRate() returns uint256 { uint256 result; callIsBorrowRate = true; return result; From 755b01282bcb954db6b0047c37fe42e6f6a88283 Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Thu, 24 Aug 2023 15:37:49 +0200 Subject: [PATCH 079/204] Added option plaininjectivity --- certora/scripts/verifyDifficultMath.sh | 1 + certora/scripts/verifyHealth.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/certora/scripts/verifyDifficultMath.sh b/certora/scripts/verifyDifficultMath.sh index f775a585b..b5a6618d6 100755 --- a/certora/scripts/verifyDifficultMath.sh +++ b/certora/scripts/verifyDifficultMath.sh @@ -8,5 +8,6 @@ certoraRun \ --verify MorphoHarness:certora/specs/DifficultMath.spec \ --loop_iter 3 \ --optimistic_loop \ + --prover_args '-smt_hashingScheme plaininjectivity' \ --msg "Morpho Difficult Math" \ "$@" diff --git a/certora/scripts/verifyHealth.sh b/certora/scripts/verifyHealth.sh index efcc770b8..e41ca696b 100755 --- a/certora/scripts/verifyHealth.sh +++ b/certora/scripts/verifyHealth.sh @@ -8,5 +8,6 @@ certoraRun \ --verify MorphoHarness:certora/specs/Health.spec \ --loop_iter 3 \ --optimistic_loop \ + --prover_args '-smt_hashingScheme plaininjectivity' \ --msg "Morpho Health Check" \ "$@" From ecf5226231f568e1a189e2a444fc3d8a12162505 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 24 Aug 2023 17:13:27 +0200 Subject: [PATCH 080/204] fix: rename user to position --- certora/harness/MorphoHarness.sol | 6 +++--- certora/specs/Blue.spec | 6 +++--- certora/specs/BlueReverts.spec | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/certora/harness/MorphoHarness.sol b/certora/harness/MorphoHarness.sol index b4428b4b2..24f4e4eb8 100644 --- a/certora/harness/MorphoHarness.sol +++ b/certora/harness/MorphoHarness.sol @@ -42,15 +42,15 @@ contract MorphoHarness is Morpho { } function getSupplyShares(Id id, address account) external view returns (uint256) { - return user[id][account].supplyShares; + return position[id][account].supplyShares; } function getBorrowShares(Id id, address account) external view returns (uint256) { - return user[id][account].borrowShares; + return position[id][account].borrowShares; } function getCollateral(Id id, address account) external view returns (uint256) { - return user[id][account].collateral; + return position[id][account].collateral; } function getLastUpdate(Id id) external view returns (uint256) { diff --git a/certora/specs/Blue.spec b/certora/specs/Blue.spec index 962ea0bb5..ed9f7928a 100644 --- a/certora/specs/Blue.spec +++ b/certora/specs/Blue.spec @@ -54,15 +54,15 @@ hook Sstore idToMarketParams[KEY MorphoHarness.Id id].collateralToken address to idToCollateral[id] = token; } -hook Sstore user[KEY MorphoHarness.Id id][KEY address owner].supplyShares uint256 newShares (uint256 oldShares) STORAGE { +hook Sstore position[KEY MorphoHarness.Id id][KEY address owner].supplyShares uint256 newShares (uint256 oldShares) STORAGE { sumSupplyShares[id] = sumSupplyShares[id] - oldShares + newShares; } -hook Sstore user[KEY MorphoHarness.Id id][KEY address owner].borrowShares uint128 newShares (uint128 oldShares) STORAGE { +hook Sstore position[KEY MorphoHarness.Id id][KEY address owner].borrowShares uint128 newShares (uint128 oldShares) STORAGE { sumBorrowShares[id] = sumBorrowShares[id] - oldShares + newShares; } -hook Sstore user[KEY MorphoHarness.Id id][KEY address owner].collateral uint128 newAmount (uint128 oldAmount) STORAGE { +hook Sstore position[KEY MorphoHarness.Id id][KEY address owner].collateral uint128 newAmount (uint128 oldAmount) STORAGE { sumCollateral[id] = sumCollateral[id] - oldAmount + newAmount; expectedAmount[idToCollateral[id]] = expectedAmount[idToCollateral[id]] - oldAmount + newAmount; } diff --git a/certora/specs/BlueReverts.spec b/certora/specs/BlueReverts.spec index b7a67301c..751c1c452 100644 --- a/certora/specs/BlueReverts.spec +++ b/certora/specs/BlueReverts.spec @@ -23,7 +23,7 @@ ghost mapping(MorphoHarness.Id => mathint) sumCollateral { init_state axiom (forall MorphoHarness.Id id. sumCollateral[id] == 0); } -hook Sstore user[KEY MorphoHarness.Id id][KEY address owner].collateral uint128 newAmount (uint128 oldAmount) STORAGE { +hook Sstore position[KEY MorphoHarness.Id id][KEY address owner].collateral uint128 newAmount (uint128 oldAmount) STORAGE { sumCollateral[id] = sumCollateral[id] - oldAmount + newAmount; } From 8fb896b693c4af03b021ce59262f460d72ec4f2e Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 24 Aug 2023 17:16:04 +0200 Subject: [PATCH 081/204] fix: marketParams renaming for accrue interest --- certora/specs/BlueAccrueInterests.spec | 40 +++++++++++++------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/certora/specs/BlueAccrueInterests.spec b/certora/specs/BlueAccrueInterests.spec index 2217d44e4..a98e8d9b1 100644 --- a/certora/specs/BlueAccrueInterests.spec +++ b/certora/specs/BlueAccrueInterests.spec @@ -6,7 +6,7 @@ methods { // we assume here that all external functions will not access storage, since we cannot show // commutativity otherwise. We also need to assume that the price and borrow rate return // always the same value (and do not depend on msg.origin), so we use ghost functions for them. - function _.borrowRate(MorphoHarness.Market market) external with (env e) => ghostBorrowRate(market.irm, e.block.timestamp) expect uint256; + function _.borrowRate(MorphoHarness.MarketParams marketParams) external with (env e) => ghostBorrowRate(marketParams.irm, e.block.timestamp) expect uint256; function _.price() external with (env e) => ghostOraclePrice(e.block.timestamp) expect uint256; function _.transfer(address to, uint256 amount) external => ghostTransfer(to, amount) expect bool; function _.transferFrom(address from, address to, uint256 amount) external => ghostTransferFrom(from, to, amount) expect bool; @@ -32,7 +32,7 @@ ghost ghostTransferFrom(address, address, uint256) returns bool; rule supplyAccruesInterests() { env e; - MorphoHarness.Market market; + MorphoHarness.MarketParams marketParams; uint256 assets; uint256 shares; address onbehalf; @@ -43,11 +43,11 @@ rule supplyAccruesInterests() // check that calling accrueInterests first has no effect. // this is because supply should call accrueInterests itself. - accrueInterests(e, market); - supply(e, market, assets, shares, onbehalf, data); + accrueInterests(e, marketParams); + supply(e, marketParams, assets, shares, onbehalf, data); storage afterBoth = lastStorage; - supply(e, market, assets, shares, onbehalf, data) at init; + supply(e, marketParams, assets, shares, onbehalf, data) at init; storage afterOne = lastStorage; @@ -57,7 +57,7 @@ rule supplyAccruesInterests() rule withdrawAccruesInterests() { env e; - MorphoHarness.Market market; + MorphoHarness.MarketParams marketParams; uint256 assets; uint256 shares; address onbehalf; @@ -68,11 +68,11 @@ rule withdrawAccruesInterests() // check that calling accrueInterests first has no effect. // this is because withdraw should call accrueInterests itself. - accrueInterests(e, market); - withdraw(e, market, assets, shares, onbehalf, receiver); + accrueInterests(e, marketParams); + withdraw(e, marketParams, assets, shares, onbehalf, receiver); storage afterBoth = lastStorage; - withdraw(e, market, assets, shares, onbehalf, receiver) at init; + withdraw(e, marketParams, assets, shares, onbehalf, receiver) at init; storage afterOne = lastStorage; @@ -82,7 +82,7 @@ rule withdrawAccruesInterests() rule borrowAccruesInterests() { env e; - MorphoHarness.Market market; + MorphoHarness.MarketParams marketParams; uint256 assets; uint256 shares; address onbehalf; @@ -93,11 +93,11 @@ rule borrowAccruesInterests() // check that calling accrueInterests first has no effect. // this is because borrow should call accrueInterests itself. - accrueInterests(e, market); - borrow(e, market, assets, shares, onbehalf, receiver); + accrueInterests(e, marketParams); + borrow(e, marketParams, assets, shares, onbehalf, receiver); storage afterBoth = lastStorage; - borrow(e, market, assets, shares, onbehalf, receiver) at init; + borrow(e, marketParams, assets, shares, onbehalf, receiver) at init; storage afterOne = lastStorage; @@ -107,7 +107,7 @@ rule borrowAccruesInterests() rule repayAccruesInterests() { env e; - MorphoHarness.Market market; + MorphoHarness.MarketParams marketParams; uint256 assets; uint256 shares; address onbehalf; @@ -118,11 +118,11 @@ rule repayAccruesInterests() // check that calling accrueInterests first has no effect. // this is because repay should call accrueInterests itself. - accrueInterests(e, market); - repay(e, market, assets, shares, onbehalf, data); + accrueInterests(e, marketParams); + repay(e, marketParams, assets, shares, onbehalf, data); storage afterBoth = lastStorage; - repay(e, market, assets, shares, onbehalf, data) at init; + repay(e, marketParams, assets, shares, onbehalf, data) at init; storage afterOne = lastStorage; @@ -145,7 +145,7 @@ filtered { { env e1; env e2; - MorphoHarness.Market market; + MorphoHarness.MarketParams marketParams; require e1.block.timestamp == e2.block.timestamp; @@ -153,7 +153,7 @@ filtered { // check that accrueInterests commutes with every other function. - accrueInterests(e1, market); + accrueInterests(e1, marketParams); f@withrevert(e2, args); bool revert1 = lastReverted; @@ -162,7 +162,7 @@ filtered { f@withrevert(e2, args) at init; bool revert2 = lastReverted; - accrueInterests(e1, market); + accrueInterests(e1, marketParams); storage store2 = lastStorage; From 00e24154985da73500af68e1a9fff4b09bdce49b Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 24 Aug 2023 17:59:56 +0200 Subject: [PATCH 082/204] fix: rename accrueInterest --- certora/specs/BlueAccrueInterests.spec | 34 +++++++++++++------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/certora/specs/BlueAccrueInterests.spec b/certora/specs/BlueAccrueInterests.spec index a98e8d9b1..208a97d31 100644 --- a/certora/specs/BlueAccrueInterests.spec +++ b/certora/specs/BlueAccrueInterests.spec @@ -40,10 +40,10 @@ rule supplyAccruesInterests() storage init = lastStorage; - // check that calling accrueInterests first has no effect. - // this is because supply should call accrueInterests itself. + // check that calling accrueInterest first has no effect. + // this is because supply should call accrueInterest itself. - accrueInterests(e, marketParams); + accrueInterest(e, marketParams); supply(e, marketParams, assets, shares, onbehalf, data); storage afterBoth = lastStorage; @@ -65,10 +65,10 @@ rule withdrawAccruesInterests() storage init = lastStorage; - // check that calling accrueInterests first has no effect. - // this is because withdraw should call accrueInterests itself. + // check that calling accrueInterest first has no effect. + // this is because withdraw should call accrueInterest itself. - accrueInterests(e, marketParams); + accrueInterest(e, marketParams); withdraw(e, marketParams, assets, shares, onbehalf, receiver); storage afterBoth = lastStorage; @@ -90,10 +90,10 @@ rule borrowAccruesInterests() storage init = lastStorage; - // check that calling accrueInterests first has no effect. - // this is because borrow should call accrueInterests itself. + // check that calling accrueInterest first has no effect. + // this is because borrow should call accrueInterest itself. - accrueInterests(e, marketParams); + accrueInterest(e, marketParams); borrow(e, marketParams, assets, shares, onbehalf, receiver); storage afterBoth = lastStorage; @@ -115,10 +115,10 @@ rule repayAccruesInterests() storage init = lastStorage; - // check that calling accrueInterests first has no effect. - // this is because repay should call accrueInterests itself. + // check that calling accrueInterest first has no effect. + // this is because repay should call accrueInterest itself. - accrueInterests(e, marketParams); + accrueInterest(e, marketParams); repay(e, marketParams, assets, shares, onbehalf, data); storage afterBoth = lastStorage; @@ -131,11 +131,11 @@ rule repayAccruesInterests() /** - * Show that accrueInterests commutes with other state changing rules. + * Show that accrueInterest commutes with other state changing rules. * We exclude view functions, because (a) we cannot check the return * value and for storage commutativity is trivial and (b) most view * functions, e.g. totalSupplyShares, are not commutative, i.e. they return - * a different value if called before accrueInterests is called. + * a different value if called before accrueInterest is called. * We also exclude setFeeRecipient, as it is known to be not commutative. */ rule accrueInterestsCommutesExceptForSetFeeRecipient(method f, env e, calldataarg args) @@ -151,9 +151,9 @@ filtered { storage init = lastStorage; - // check that accrueInterests commutes with every other function. + // check that accrueInterest commutes with every other function. - accrueInterests(e1, marketParams); + accrueInterest(e1, marketParams); f@withrevert(e2, args); bool revert1 = lastReverted; @@ -162,7 +162,7 @@ filtered { f@withrevert(e2, args) at init; bool revert2 = lastReverted; - accrueInterests(e1, marketParams); + accrueInterest(e1, marketParams); storage store2 = lastStorage; From b33db9226e2a31fbc49c56898bdccba060182cdb Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 25 Aug 2023 19:02:14 +0200 Subject: [PATCH 083/204] chore: munging of market params lib --- .github/workflows/certora.yml | 3 +-- certora/harness/MorphoHarness.sol | 6 ++++++ ...erifyBlueRatioMathSummary.sh => verifyBlueLibSummary.sh} | 2 +- .../{BlueRatioMathSummary.spec => BlueLibSummary.spec} | 6 ++++++ src/libraries/MarketParamsLib.sol | 6 ++---- 5 files changed, 16 insertions(+), 7 deletions(-) rename certora/scripts/{verifyBlueRatioMathSummary.sh => verifyBlueLibSummary.sh} (62%) rename certora/specs/{BlueRatioMathSummary.spec => BlueLibSummary.spec} (62%) diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml index fd24d5fb1..acc163e53 100644 --- a/.github/workflows/certora.yml +++ b/.github/workflows/certora.yml @@ -45,10 +45,9 @@ jobs: script: - verifyBlue.sh - verifyBlueAccrueInterests.sh - - verifyBlueRatioMathSummary.sh - verifyBlueExitLiquidity.sh + - verifyBlueLibSummary.sh - verifyBlueRatioMath.sh - - verifyBlueRatioMathSummary.sh - verifyBlueReverts.sh - verifyBlueTransfer.sh - verifyDifficultMath.sh diff --git a/certora/harness/MorphoHarness.sol b/certora/harness/MorphoHarness.sol index 24f4e4eb8..33ae528f6 100644 --- a/certora/harness/MorphoHarness.sol +++ b/certora/harness/MorphoHarness.sol @@ -81,6 +81,12 @@ contract MorphoHarness is Morpho { return marketParams.id(); } + function marketLibId(MarketParams memory marketParams) external pure returns (Id marketParamsId) { + assembly ("memory-safe") { + marketParamsId := keccak256(marketParams, mul(5, 32)) + } + } + function mathLibMulDivUp(uint256 x, uint256 y, uint256 d) public pure returns (uint256) { return MathLib.mulDivUp(x, y, d); } diff --git a/certora/scripts/verifyBlueRatioMathSummary.sh b/certora/scripts/verifyBlueLibSummary.sh similarity index 62% rename from certora/scripts/verifyBlueRatioMathSummary.sh rename to certora/scripts/verifyBlueLibSummary.sh index f0a9305b9..64f02dcb4 100755 --- a/certora/scripts/verifyBlueRatioMathSummary.sh +++ b/certora/scripts/verifyBlueLibSummary.sh @@ -2,6 +2,6 @@ certoraRun \ certora/harness/MorphoHarness.sol \ - --verify MorphoHarness:certora/specs/BlueRatioMathSummary.spec \ + --verify MorphoHarness:certora/specs/BlueLibSummary.spec \ --msg "Morpho Ratio Math Summary" \ "$@" diff --git a/certora/specs/BlueRatioMathSummary.spec b/certora/specs/BlueLibSummary.spec similarity index 62% rename from certora/specs/BlueRatioMathSummary.spec rename to certora/specs/BlueLibSummary.spec index 65f653283..793491c9f 100644 --- a/certora/specs/BlueRatioMathSummary.spec +++ b/certora/specs/BlueLibSummary.spec @@ -1,6 +1,8 @@ methods { function mathLibMulDivUp(uint256, uint256, uint256) external returns uint256 envfree; function mathLibMulDivDown(uint256, uint256, uint256) external returns uint256 envfree; + function getMarketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; + function marketLibId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; } // Check the summaries required by BlueRatioMath.spec @@ -13,3 +15,7 @@ rule checkSummaryMulDivDown(uint256 x, uint256 y, uint256 d) { uint256 result = mathLibMulDivDown(x, y, d); assert result * d <= x * y; } + +rule checkSummaryId(MorphoHarness.MarketParams martketParams) { + assert marketLibId(marketParams) == getMarketId(marketParams); +} diff --git a/src/libraries/MarketParamsLib.sol b/src/libraries/MarketParamsLib.sol index eea778c5c..d4fdfd33f 100644 --- a/src/libraries/MarketParamsLib.sol +++ b/src/libraries/MarketParamsLib.sol @@ -9,9 +9,7 @@ import {Id, MarketParams} from "../interfaces/IMorpho.sol"; /// @notice Library to convert a market to its id. library MarketParamsLib { /// @notice Returns the id of the market `marketParams`. - function id(MarketParams memory marketParams) internal pure returns (Id marketParamsId) { - assembly ("memory-safe") { - marketParamsId := keccak256(marketParams, mul(5, 32)) - } + function id(MarketParams memory marketParams) internal pure returns (Id) { + return Id.wrap(keccak256(abi.encode(marketParams))); } } From a49d137e299956b226844b00d0ea627b6ef3c3db Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 25 Aug 2023 19:12:17 +0200 Subject: [PATCH 084/204] fix: spelling marketParams --- certora/specs/BlueLibSummary.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certora/specs/BlueLibSummary.spec b/certora/specs/BlueLibSummary.spec index 793491c9f..9c95eeeac 100644 --- a/certora/specs/BlueLibSummary.spec +++ b/certora/specs/BlueLibSummary.spec @@ -16,6 +16,6 @@ rule checkSummaryMulDivDown(uint256 x, uint256 y, uint256 d) { assert result * d <= x * y; } -rule checkSummaryId(MorphoHarness.MarketParams martketParams) { +rule checkSummaryId(MorphoHarness.MarketParams marketParams) { assert marketLibId(marketParams) == getMarketId(marketParams); } From 7e833313b62317c1ae0bfdb487b759b74c7f3fd7 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 25 Aug 2023 19:14:43 +0200 Subject: [PATCH 085/204] fix: borrow rate parameters --- certora/specs/BlueAccrueInterests.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certora/specs/BlueAccrueInterests.spec b/certora/specs/BlueAccrueInterests.spec index 208a97d31..f4cfa837e 100644 --- a/certora/specs/BlueAccrueInterests.spec +++ b/certora/specs/BlueAccrueInterests.spec @@ -6,7 +6,7 @@ methods { // we assume here that all external functions will not access storage, since we cannot show // commutativity otherwise. We also need to assume that the price and borrow rate return // always the same value (and do not depend on msg.origin), so we use ghost functions for them. - function _.borrowRate(MorphoHarness.MarketParams marketParams) external with (env e) => ghostBorrowRate(marketParams.irm, e.block.timestamp) expect uint256; + function _.borrowRate(MorphoHarness.MarketParams marketParams, MorphoHarness.Market market) external with (env e) => ghostBorrowRate(marketParams.irm, e.block.timestamp) expect uint256; function _.price() external with (env e) => ghostOraclePrice(e.block.timestamp) expect uint256; function _.transfer(address to, uint256 amount) external => ghostTransfer(to, amount) expect bool; function _.transferFrom(address from, address to, uint256 amount) external => ghostTransferFrom(from, to, amount) expect bool; From b8538fee642c8fadfca405c5d8304376198f0020 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 25 Aug 2023 23:04:55 +0200 Subject: [PATCH 086/204] fix: timestamp overflow --- certora/specs/BlueAccrueInterests.spec | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/certora/specs/BlueAccrueInterests.spec b/certora/specs/BlueAccrueInterests.spec index f4cfa837e..0ac324321 100644 --- a/certora/specs/BlueAccrueInterests.spec +++ b/certora/specs/BlueAccrueInterests.spec @@ -38,6 +38,8 @@ rule supplyAccruesInterests() address onbehalf; bytes data; + require e.block.timestamp < 2^128; + storage init = lastStorage; // check that calling accrueInterest first has no effect. @@ -63,6 +65,8 @@ rule withdrawAccruesInterests() address onbehalf; address receiver; + require e.block.timestamp < 2^128; + storage init = lastStorage; // check that calling accrueInterest first has no effect. @@ -88,6 +92,8 @@ rule borrowAccruesInterests() address onbehalf; address receiver; + require e.block.timestamp < 2^128; + storage init = lastStorage; // check that calling accrueInterest first has no effect. @@ -113,6 +119,8 @@ rule repayAccruesInterests() address onbehalf; bytes data; + require e.block.timestamp < 2^128; + storage init = lastStorage; // check that calling accrueInterest first has no effect. @@ -148,6 +156,7 @@ filtered { MorphoHarness.MarketParams marketParams; require e1.block.timestamp == e2.block.timestamp; + require e1.block.timestamp < 2^128; storage init = lastStorage; From 4fe8cce9cd68748b21eb4ca4c857e2471a11094c Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Mon, 28 Aug 2023 16:57:43 +0200 Subject: [PATCH 087/204] fix: adapt borrow ratio spec to the underflow change --- certora/specs/BlueRatioMath.spec | 28 +++++++++++++++++++++++++--- certora/specs/DifficultMath.spec | 29 +++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/certora/specs/BlueRatioMath.spec b/certora/specs/BlueRatioMath.spec index f96baea7c..7fbd229dc 100644 --- a/certora/specs/BlueRatioMath.spec +++ b/certora/specs/BlueRatioMath.spec @@ -13,8 +13,6 @@ methods { function _.borrowRate(MorphoHarness.MarketParams, MorphoHarness.Market) external => HAVOC_ECF; - function VIRTUAL_ASSETS() external returns uint256 envfree; - function VIRTUAL_SHARES() external returns uint256 envfree; function MAX_FEE() external returns uint256 envfree; } @@ -101,7 +99,9 @@ filtered { } rule onlyAccrueInterestsCanIncreaseBorrowRatio(env e, method f, calldataarg args) -filtered { f -> !f.isView } +filtered { + f -> !f.isView && f.selector != sig:repay(MorphoHarness.MarketParams, uint256, uint256, address, bytes).selector +} { MorphoHarness.Id id; requireInvariant feeInRange(id); @@ -120,3 +120,25 @@ filtered { f -> !f.isView } // Check if ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter assert assetsBefore * sharesAfter >= assetsAfter * sharesBefore; } + +rule repayIncreasesBorrowRatio(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onbehalf, bytes data) +{ + MorphoHarness.Id id = getMarketId(marketParams); + requireInvariant feeInRange(id); + + mathint assetsBefore = getVirtualTotalBorrowAssets(id); + mathint sharesBefore = getVirtualTotalBorrowShares(id); + + require getLastUpdate(id) == e.block.timestamp; + + mathint repaidAssets; + repaidAssets, _ = repay(e, marketParams, assets, shares, onbehalf, data); + + require repaidAssets < assetsBefore; + + mathint assetsAfter = getVirtualTotalBorrowAssets(id); + mathint sharesAfter = getVirtualTotalBorrowShares(id); + + assert assetsAfter == assetsBefore - repaidAssets; + assert assetsBefore * sharesAfter >= assetsAfter * sharesBefore; +} diff --git a/certora/specs/DifficultMath.spec b/certora/specs/DifficultMath.spec index a3927446b..382626c34 100644 --- a/certora/specs/DifficultMath.spec +++ b/certora/specs/DifficultMath.spec @@ -2,11 +2,18 @@ methods { function getMarketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; function getVirtualTotalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; function getVirtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; + function getVirtualTotalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree; + function getVirtualTotalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; + function getFee(MorphoHarness.Id) external returns uint256 envfree; + function getLastUpdate(MorphoHarness.Id) external returns uint256 envfree; + function MathLib.mulDivDown(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivDown(a,b,c); function MathLib.mulDivUp(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivUp(a,b,c); function SafeTransferLib.safeTransfer(address token, address to, uint256 value) internal => NONDET; function SafeTransferLib.safeTransferFrom(address token, address from, address to, uint256 value) internal => NONDET; function _.onMorphoSupply(uint256 assets, bytes data) external => HAVOC_ECF; + + function MAX_FEE() external returns uint256 envfree; } function summaryMulDivUp(uint256 x, uint256 y, uint256 d) returns uint256 { @@ -17,6 +24,28 @@ function summaryMulDivDown(uint256 x, uint256 y, uint256 d) returns uint256 { return require_uint256((x * y) / d); } +rule repayAllResetsBorrowRatio(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onbehalf, bytes data) +{ + MorphoHarness.Id id = getMarketId(marketParams); + require getFee(id) <= MAX_FEE(); + + mathint assetsBefore = getVirtualTotalBorrowAssets(id); + mathint sharesBefore = getVirtualTotalBorrowShares(id); + + require getLastUpdate(id) == e.block.timestamp; + + mathint repaidAssets; + repaidAssets, _ = repay(e, marketParams, assets, shares, onbehalf, data); + + require repaidAssets >= assetsBefore; + + mathint assetsAfter = getVirtualTotalBorrowAssets(id); + mathint sharesAfter = getVirtualTotalBorrowShares(id); + + assert assetsAfter == 1; +} + + // There should be no profit from supply followed immediately by withdraw. rule supplyWithdraw() { MorphoHarness.MarketParams marketParams; From ca63f5d4c0f12a98442ee1721bee17af99a8a83f Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Mon, 28 Aug 2023 19:46:18 +0200 Subject: [PATCH 088/204] fix: repay stay healthy (times out) --- certora/specs/Health.spec | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/certora/specs/Health.spec b/certora/specs/Health.spec index 2cde2c795..d9c1daccf 100644 --- a/certora/specs/Health.spec +++ b/certora/specs/Health.spec @@ -1,5 +1,6 @@ methods { function getLastUpdate(MorphoHarness.Id) external returns uint256 envfree; + function getTotalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; function getBorrowShares(MorphoHarness.Id, address) external returns uint256 envfree; function getCollateral(MorphoHarness.Id, address) external returns uint256 envfree; function isHealthy(MorphoHarness.MarketParams, address user) external returns bool envfree; @@ -37,7 +38,7 @@ function summaryMin(uint256 a, uint256 b) returns uint256 { return a < b ? a : b; } -rule stayHealthy(method f, env e, calldataarg data) +rule stayHealthy(env e, method f, calldataarg data) filtered { f -> !f.isView } @@ -58,6 +59,25 @@ filtered { assert !priceChanged => stillHealthy; } +rule repayStayHealthy(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) +{ + MorphoHarness.Id id = getMarketId(marketParams); + address user; + + require isHealthy(marketParams, user); + require marketParams.lltv < 10^18; + require marketParams.lltv > 0; + require getLastUpdate(id) == e.block.timestamp; + priceChanged = false; + + repay(e, marketParams, assets, shares, onBehalf, data); + + require getBorrowShares(id, user) <= getTotalBorrowShares(id); + + bool stillHealthy = isHealthy(marketParams, user); + assert !priceChanged => stillHealthy; +} + rule healthyUserCannotLoseCollateral(method f, calldataarg data) filtered { f -> !f.isView From 6b20024557bcbf4d6bb2b6e840a6ddcd98c86f77 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 29 Aug 2023 09:41:25 +0200 Subject: [PATCH 089/204] fix: remove time-outs --- certora/specs/BlueLibSummary.spec | 6 +++--- certora/specs/BlueRatioMath.spec | 4 +++- certora/specs/Health.spec | 22 ++++------------------ 3 files changed, 10 insertions(+), 22 deletions(-) diff --git a/certora/specs/BlueLibSummary.spec b/certora/specs/BlueLibSummary.spec index 9c95eeeac..9f41296a0 100644 --- a/certora/specs/BlueLibSummary.spec +++ b/certora/specs/BlueLibSummary.spec @@ -16,6 +16,6 @@ rule checkSummaryMulDivDown(uint256 x, uint256 y, uint256 d) { assert result * d <= x * y; } -rule checkSummaryId(MorphoHarness.MarketParams marketParams) { - assert marketLibId(marketParams) == getMarketId(marketParams); -} +// rule checkSummaryId(MorphoHarness.MarketParams marketParams) { +// assert marketLibId(marketParams) == getMarketId(marketParams); +// } diff --git a/certora/specs/BlueRatioMath.spec b/certora/specs/BlueRatioMath.spec index 7fbd229dc..3c192c4c1 100644 --- a/certora/specs/BlueRatioMath.spec +++ b/certora/specs/BlueRatioMath.spec @@ -100,7 +100,9 @@ filtered { rule onlyAccrueInterestsCanIncreaseBorrowRatio(env e, method f, calldataarg args) filtered { - f -> !f.isView && f.selector != sig:repay(MorphoHarness.MarketParams, uint256, uint256, address, bytes).selector + f -> !f.isView && + f.selector != sig:repay(MorphoHarness.MarketParams, uint256, uint256, address, bytes).selector && + f.selector != sig:liquidate(MorphoHarness.MarketParams, address, uint256, uint256, bytes).selector } { MorphoHarness.Id id; diff --git a/certora/specs/Health.spec b/certora/specs/Health.spec index d9c1daccf..42bcb4db2 100644 --- a/certora/specs/Health.spec +++ b/certora/specs/Health.spec @@ -40,7 +40,10 @@ function summaryMin(uint256 a, uint256 b) returns uint256 { rule stayHealthy(env e, method f, calldataarg data) filtered { - f -> !f.isView + f -> !f.isView && + f.selector != sig:liquidate(MorphoHarness.MarketParams, address, uint256, uint256, bytes).selector && + f.selector != sig:repay(MorphoHarness.MarketParams, uint256, uint256, address, bytes).selector && + f.selector != sig:borrow(MorphoHarness.MarketParams, uint256, uint256, address, address).selector } { MorphoHarness.MarketParams marketParams; @@ -55,23 +58,6 @@ filtered { f(e, data); - bool stillHealthy = isHealthy(marketParams, user); - assert !priceChanged => stillHealthy; -} - -rule repayStayHealthy(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) -{ - MorphoHarness.Id id = getMarketId(marketParams); - address user; - - require isHealthy(marketParams, user); - require marketParams.lltv < 10^18; - require marketParams.lltv > 0; - require getLastUpdate(id) == e.block.timestamp; - priceChanged = false; - - repay(e, marketParams, assets, shares, onBehalf, data); - require getBorrowShares(id, user) <= getTotalBorrowShares(id); bool stillHealthy = isHealthy(marketParams, user); From 9593236ef67038e46c90a2b24c0a09a5c37738b1 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 29 Aug 2023 10:18:00 +0200 Subject: [PATCH 090/204] feat: use munging --- .gitignore | 1 + certora/applyMunging.patch | 49 ++++++++++++++++++++ certora/harness/MorphoHarness.sol | 6 +-- certora/makefile | 12 +++++ certora/scripts/verifyBlue.sh | 2 + certora/scripts/verifyBlueAccrueInterests.sh | 2 + certora/scripts/verifyBlueExitLiquidity.sh | 2 + certora/scripts/verifyBlueLibSummary.sh | 6 ++- certora/scripts/verifyBlueRatioMath.sh | 6 ++- certora/scripts/verifyBlueReverts.sh | 2 + certora/scripts/verifyBlueTransfer.sh | 2 + certora/scripts/verifyDifficultMath.sh | 2 + certora/scripts/verifyHealth.sh | 2 + certora/scripts/verifyReentrancy.sh | 2 + src/Morpho.sol | 14 +++++- src/interfaces/IMorpho.sol | 2 +- src/libraries/MarketParamsLib.sol | 6 ++- 17 files changed, 108 insertions(+), 10 deletions(-) create mode 100644 certora/applyMunging.patch create mode 100644 certora/makefile diff --git a/.gitignore b/.gitignore index 84536674b..718ea1943 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ docs/ # Certora .certora** emv-*-certora* +certora/munged # Hardhat /types diff --git a/certora/applyMunging.patch b/certora/applyMunging.patch new file mode 100644 index 000000000..6b7d547fd --- /dev/null +++ b/certora/applyMunging.patch @@ -0,0 +1,49 @@ +diff -ruN interfaces/IMorpho.sol interfaces/IMorpho.sol +--- interfaces/IMorpho.sol 2023-08-29 09:58:51.628147127 +0200 ++++ interfaces/IMorpho.sol 2023-08-29 10:15:36.946593577 +0200 +@@ -292,7 +292,4 @@ + /// @param authorization The `Authorization` struct. + /// @param signature The signature. + function setAuthorizationWithSig(Authorization calldata authorization, Signature calldata signature) external; +- +- /// @notice Returns the data stored on the different `slots`. +- function extSloads(bytes32[] memory slots) external view returns (bytes32[] memory); + } +diff -ruN libraries/MarketParamsLib.sol libraries/MarketParamsLib.sol +--- libraries/MarketParamsLib.sol 2023-08-29 09:59:37.937583556 +0200 ++++ libraries/MarketParamsLib.sol 2023-08-29 10:16:10.519752188 +0200 +@@ -10,8 +10,6 @@ + library MarketParamsLib { + /// @notice Returns the id of the market `marketParams`. + function id(MarketParams memory marketParams) internal pure returns (Id marketParamsId) { +- assembly ("memory-safe") { +- marketParamsId := keccak256(marketParams, mul(5, 32)) +- } ++ marketParamsId = Id.wrap(keccak256(abi.encode(marketParams))); + } + } +diff -ruN Morpho.sol Morpho.sol +--- Morpho.sol 2023-08-29 09:58:43.158169812 +0200 ++++ Morpho.sol 2023-08-29 10:15:20.650012827 +0200 +@@ -502,21 +502,4 @@ + + return maxBorrow >= borrowed; + } +- +- /* STORAGE VIEW */ +- +- /// @inheritdoc IMorpho +- function extSloads(bytes32[] calldata slots) external view returns (bytes32[] memory res) { +- uint256 nSlots = slots.length; +- +- res = new bytes32[](nSlots); +- +- for (uint256 i; i < nSlots;) { +- bytes32 slot = slots[i++]; +- +- assembly ("memory-safe") { +- mstore(add(res, mul(i, 32)), sload(slot)) +- } +- } +- } + } diff --git a/certora/harness/MorphoHarness.sol b/certora/harness/MorphoHarness.sol index 33ae528f6..c22d1f289 100644 --- a/certora/harness/MorphoHarness.sol +++ b/certora/harness/MorphoHarness.sol @@ -1,8 +1,8 @@ pragma solidity 0.8.19; -import "../../src/Morpho.sol"; -import "../../src/libraries/SharesMathLib.sol"; -import "../../src/libraries/MarketParamsLib.sol"; +import "../munged/Morpho.sol"; +import "../munged/libraries/SharesMathLib.sol"; +import "../munged/libraries/MarketParamsLib.sol"; contract MorphoHarness is Morpho { using MarketParamsLib for MarketParams; diff --git a/certora/makefile b/certora/makefile new file mode 100644 index 000000000..36c1a0542 --- /dev/null +++ b/certora/makefile @@ -0,0 +1,12 @@ +munged: $(wildcard ../src/*.sol) applyMunging.patch + @rm -rf munged + @cp -r ../src munged + @patch -p0 -d munged < applyMunging.patch + +record: + diff -ruN ../src munged | sed 's+\.\./src/++g' | sed 's+munged/++g' > applyMunging.patch + +clean: + rm -rf munged + +.PHONY: record clean # do not add munged here, as it is useful to protect munged edits diff --git a/certora/scripts/verifyBlue.sh b/certora/scripts/verifyBlue.sh index bffdad175..9e6545d5b 100755 --- a/certora/scripts/verifyBlue.sh +++ b/certora/scripts/verifyBlue.sh @@ -2,6 +2,8 @@ set -euxo pipefail +make -C certora munged + certoraRun \ certora/harness/MorphoHarness.sol \ --verify MorphoHarness:certora/specs/Blue.spec \ diff --git a/certora/scripts/verifyBlueAccrueInterests.sh b/certora/scripts/verifyBlueAccrueInterests.sh index 8c6027925..a43e36427 100755 --- a/certora/scripts/verifyBlueAccrueInterests.sh +++ b/certora/scripts/verifyBlueAccrueInterests.sh @@ -2,6 +2,8 @@ set -euxo pipefail +make -C certora munged + certoraRun \ certora/harness/MorphoHarness.sol \ --verify MorphoHarness:certora/specs/BlueAccrueInterests.spec \ diff --git a/certora/scripts/verifyBlueExitLiquidity.sh b/certora/scripts/verifyBlueExitLiquidity.sh index 4aa9877ad..e23ee0bbb 100755 --- a/certora/scripts/verifyBlueExitLiquidity.sh +++ b/certora/scripts/verifyBlueExitLiquidity.sh @@ -2,6 +2,8 @@ set -euxo pipefail +make -C certora munged + certoraRun \ certora/harness/MorphoHarness.sol \ --verify MorphoHarness:certora/specs/BlueExitLiquidity.spec \ diff --git a/certora/scripts/verifyBlueLibSummary.sh b/certora/scripts/verifyBlueLibSummary.sh index 64f02dcb4..eb4391809 100755 --- a/certora/scripts/verifyBlueLibSummary.sh +++ b/certora/scripts/verifyBlueLibSummary.sh @@ -1,4 +1,8 @@ -#!/bin/sh +#!/bin/bash + +set -euxo pipefail + +make -C certora munged certoraRun \ certora/harness/MorphoHarness.sol \ diff --git a/certora/scripts/verifyBlueRatioMath.sh b/certora/scripts/verifyBlueRatioMath.sh index 6513bb805..1b5cacbdc 100755 --- a/certora/scripts/verifyBlueRatioMath.sh +++ b/certora/scripts/verifyBlueRatioMath.sh @@ -1,4 +1,8 @@ -#!/bin/sh +#!/bin/bash + +set -euxo pipefail + +make -C certora munged certoraRun \ certora/harness/MorphoHarness.sol \ diff --git a/certora/scripts/verifyBlueReverts.sh b/certora/scripts/verifyBlueReverts.sh index 4fe82cd14..4d30e8d26 100755 --- a/certora/scripts/verifyBlueReverts.sh +++ b/certora/scripts/verifyBlueReverts.sh @@ -2,6 +2,8 @@ set -euxo pipefail +make -C certora munged + certoraRun \ certora/harness/MorphoHarness.sol \ --verify MorphoHarness:certora/specs/BlueReverts.spec \ diff --git a/certora/scripts/verifyBlueTransfer.sh b/certora/scripts/verifyBlueTransfer.sh index 7cddf3f51..0e92538fc 100755 --- a/certora/scripts/verifyBlueTransfer.sh +++ b/certora/scripts/verifyBlueTransfer.sh @@ -2,6 +2,8 @@ set -euxo pipefail +make -C certora munged + certoraRun \ certora/harness/TransferHarness.sol \ certora/dispatch/ERC20Good.sol \ diff --git a/certora/scripts/verifyDifficultMath.sh b/certora/scripts/verifyDifficultMath.sh index b5a6618d6..cb7114d96 100755 --- a/certora/scripts/verifyDifficultMath.sh +++ b/certora/scripts/verifyDifficultMath.sh @@ -2,6 +2,8 @@ set -euxo pipefail +make -C certora munged + certoraRun \ certora/harness/MorphoHarness.sol \ src/mocks/OracleMock.sol \ diff --git a/certora/scripts/verifyHealth.sh b/certora/scripts/verifyHealth.sh index e41ca696b..d46f97a3a 100755 --- a/certora/scripts/verifyHealth.sh +++ b/certora/scripts/verifyHealth.sh @@ -2,6 +2,8 @@ set -euxo pipefail +make -C certora munged + certoraRun \ certora/harness/MorphoHarness.sol \ src/mocks/OracleMock.sol \ diff --git a/certora/scripts/verifyReentrancy.sh b/certora/scripts/verifyReentrancy.sh index 7cca8c891..0336e3f9f 100755 --- a/certora/scripts/verifyReentrancy.sh +++ b/certora/scripts/verifyReentrancy.sh @@ -2,6 +2,8 @@ set -euxo pipefail +make -C certora munged + certoraRun \ certora/harness/MorphoHarness.sol \ --verify MorphoHarness:certora/specs/Reentrancy.spec \ diff --git a/src/Morpho.sol b/src/Morpho.sol index 900a49b5f..c0ad87915 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -506,7 +506,17 @@ contract Morpho is IMorpho { /* STORAGE VIEW */ /// @inheritdoc IMorpho - function extSloads(bytes32[] calldata slots) external pure returns (bytes32[] memory res) { - return slots; + function extSloads(bytes32[] calldata slots) external view returns (bytes32[] memory res) { + uint256 nSlots = slots.length; + + res = new bytes32[](nSlots); + + for (uint256 i; i < nSlots;) { + bytes32 slot = slots[i++]; + + assembly ("memory-safe") { + mstore(add(res, mul(i, 32)), sload(slot)) + } + } } } diff --git a/src/interfaces/IMorpho.sol b/src/interfaces/IMorpho.sol index e0d27a07b..0f5c52de2 100644 --- a/src/interfaces/IMorpho.sol +++ b/src/interfaces/IMorpho.sol @@ -294,5 +294,5 @@ interface IMorpho { function setAuthorizationWithSig(Authorization calldata authorization, Signature calldata signature) external; /// @notice Returns the data stored on the different `slots`. - function extSloads(bytes32[] memory slots) external pure returns (bytes32[] memory); + function extSloads(bytes32[] memory slots) external view returns (bytes32[] memory); } diff --git a/src/libraries/MarketParamsLib.sol b/src/libraries/MarketParamsLib.sol index cd3b26f9e..b6106e22e 100644 --- a/src/libraries/MarketParamsLib.sol +++ b/src/libraries/MarketParamsLib.sol @@ -9,7 +9,9 @@ import {Id, MarketParams} from "../interfaces/IMorpho.sol"; /// @notice Library to convert a market to its id. library MarketParamsLib { /// @notice Returns the id of the market `marketParams`. - function id(MarketParams memory marketParams) internal pure returns (Id) { - return Id.wrap(keccak256(abi.encode(marketParams))); + function id(MarketParams memory marketParams) internal pure returns (Id marketParamsId) { + assembly ("memory-safe") { + marketParamsId := keccak256(marketParams, mul(5, 32)) + } } } From e066c47836fa1889741ff44c3230ae531c533ae1 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 29 Aug 2023 17:34:48 +0200 Subject: [PATCH 091/204] feat: exit liveness --- certora/dispatch/ERC20Bad.sol | 12 -- .../{ERC20Good.sol => ERC20Standard.sol} | 2 +- certora/harness/MorphoInternalAccess.sol | 22 +++ certora/scripts/verifyBlueLiveness.sh | 14 ++ certora/scripts/verifyBlueTransfer.sh | 2 +- certora/specs/Blue.spec | 53 -------- certora/specs/BlueLiveness.spec | 126 ++++++++++++++++++ 7 files changed, 164 insertions(+), 67 deletions(-) delete mode 100644 certora/dispatch/ERC20Bad.sol rename certora/dispatch/{ERC20Good.sol => ERC20Standard.sol} (86%) create mode 100644 certora/harness/MorphoInternalAccess.sol create mode 100755 certora/scripts/verifyBlueLiveness.sh create mode 100644 certora/specs/BlueLiveness.spec diff --git a/certora/dispatch/ERC20Bad.sol b/certora/dispatch/ERC20Bad.sol deleted file mode 100644 index 96c8e5a48..000000000 --- a/certora/dispatch/ERC20Bad.sol +++ /dev/null @@ -1,12 +0,0 @@ -// 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/ERC20Standard.sol similarity index 86% rename from certora/dispatch/ERC20Good.sol rename to certora/dispatch/ERC20Standard.sol index 38fcc8e6c..2e9312532 100644 --- a/certora/dispatch/ERC20Good.sol +++ b/certora/dispatch/ERC20Standard.sol @@ -3,6 +3,6 @@ pragma solidity ^0.8.0; import {ERC20} from "openzeppelin-contracts/token/ERC20/ERC20.sol"; -contract ERC20Good is ERC20 { +contract ERC20Standard is ERC20 { constructor(string memory name, string memory symbol) ERC20(name, symbol) {} } diff --git a/certora/harness/MorphoInternalAccess.sol b/certora/harness/MorphoInternalAccess.sol new file mode 100644 index 000000000..b294b45dd --- /dev/null +++ b/certora/harness/MorphoInternalAccess.sol @@ -0,0 +1,22 @@ +pragma solidity 0.8.19; + +import "./MorphoHarness.sol"; + +contract MorphoInternalAccess is MorphoHarness { + constructor(address newOwner) MorphoHarness(newOwner) {} + + uint128 internal interest; + + function nonDetInterest() external view returns (uint128) { + return interest; + } + + function update(Id id, uint256 timestamp) external { + market[id].lastUpdate = uint128(timestamp); + } + + function increaseInterest(Id id, uint128 interest) external { + market[id].totalBorrowAssets += interest; + market[id].totalSupplyAssets += interest; + } +} diff --git a/certora/scripts/verifyBlueLiveness.sh b/certora/scripts/verifyBlueLiveness.sh new file mode 100755 index 000000000..c138b238c --- /dev/null +++ b/certora/scripts/verifyBlueLiveness.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +set -euxo pipefail + +make -C certora munged + +certoraRun \ + certora/harness/MorphoInternalAccess.sol \ + --verify MorphoInternalAccess:certora/specs/BlueLiveness.spec \ + --solc_allow_path src \ + --loop_iter 3 \ + --optimistic_loop \ + --msg "Morpho Blue" \ + "$@" diff --git a/certora/scripts/verifyBlueTransfer.sh b/certora/scripts/verifyBlueTransfer.sh index 0e92538fc..a84744b3e 100755 --- a/certora/scripts/verifyBlueTransfer.sh +++ b/certora/scripts/verifyBlueTransfer.sh @@ -6,7 +6,7 @@ make -C certora munged certoraRun \ certora/harness/TransferHarness.sol \ - certora/dispatch/ERC20Good.sol \ + certora/dispatch/ERC20Standard.sol \ certora/dispatch/ERC20USDT.sol \ certora/dispatch/ERC20NoRevert.sol \ --verify TransferHarness:certora/specs/BlueTransfer.spec \ diff --git a/certora/specs/Blue.spec b/certora/specs/Blue.spec index ed9f7928a..55a56b668 100644 --- a/certora/specs/Blue.spec +++ b/certora/specs/Blue.spec @@ -173,33 +173,6 @@ filtered { assert isAuthorized(user, someone) == authorizedBefore; } -rule supplyMovesTokensAndIncreasesShares() { - MorphoHarness.MarketParams marketParams; - uint256 assets; - uint256 shares; - uint256 suppliedAssets; - uint256 suppliedShares; - address onbehalf; - bytes data; - MorphoHarness.Id id = getMarketId(marketParams); - env e; - - require e.msg.sender != currentContract; - require getLastUpdate(id) == e.block.timestamp; - - mathint sharesBefore = getSupplyShares(id, onbehalf); - mathint balanceBefore = myBalances[marketParams.borrowableToken]; - - suppliedAssets, suppliedShares = supply(e, marketParams, assets, shares, onbehalf, data); - assert assets != 0 => suppliedAssets == assets && shares == 0; - assert assets == 0 => suppliedShares == shares && shares != 0; - - mathint sharesAfter = getSupplyShares(id, onbehalf); - mathint balanceAfter = myBalances[marketParams.borrowableToken]; - assert sharesAfter == sharesBefore + suppliedShares; - assert balanceAfter == balanceBefore + suppliedAssets; -} - rule userCannotLoseSupplyShares(method f, calldataarg data) filtered { f -> !f.isView } { @@ -279,29 +252,3 @@ filtered { f -> !f.isView } f(e, data); assert getLastUpdate(id) <= e.block.timestamp; } - -rule canWithdrawAll() { - MorphoHarness.MarketParams marketParams; - uint256 withdrawnAssets; - uint256 withdrawnShares; - address receiver; - env e; - - MorphoHarness.Id id = getMarketId(marketParams); - uint256 shares = getSupplyShares(id, e.msg.sender); - - require isCreated(id); - require e.msg.sender != 0; - require receiver != 0; - require e.msg.value == 0; - require shares > 0; - require getTotalBorrowAssets(id) == 0; - require getLastUpdate(id) <= e.block.timestamp; - require shares < getTotalSupplyShares(id); - require getTotalSupplyShares(id) < 10^40 && getTotalSupplyAssets(id) < 10^30; - - withdrawnAssets, withdrawnShares = withdraw@withrevert(e, marketParams, 0, shares, e.msg.sender, receiver); - - assert withdrawnShares == shares; - assert !lastReverted, "Can withdraw all assets if nobody borrows"; -} diff --git a/certora/specs/BlueLiveness.spec b/certora/specs/BlueLiveness.spec new file mode 100644 index 000000000..5ee69064f --- /dev/null +++ b/certora/specs/BlueLiveness.spec @@ -0,0 +1,126 @@ +methods { + function getTotalSupplyAssets(MorphoInternalAccess.Id) external returns uint256 envfree; + function getTotalSupplyShares(MorphoInternalAccess.Id) external returns uint256 envfree; + function getTotalBorrowAssets(MorphoInternalAccess.Id) external returns uint256 envfree; + function getTotalBorrowShares(MorphoInternalAccess.Id) external returns uint256 envfree; + function getSupplyShares(MorphoInternalAccess.Id, address) external returns uint256 envfree; + function getBorrowShares(MorphoInternalAccess.Id, address) external returns uint256 envfree; + function getCollateral(MorphoInternalAccess.Id, address) external returns uint256 envfree; + function getFee(MorphoInternalAccess.Id) external returns uint256 envfree; + function getLastUpdate(MorphoInternalAccess.Id) external returns uint256 envfree; + function getMarketId(MorphoInternalAccess.MarketParams) external returns MorphoInternalAccess.Id envfree; + + function _._accrueInterest(MorphoInternalAccess.MarketParams memory marketParams, MorphoInternalAccess.Id id) internal with (env e) => summaryAccrueInterest(e, marketParams, id) expect bool; + function _.nonDetInterest() external => NONDET; + + function SafeTransferLib.safeTransfer(address token, address to, uint256 value) internal => summarySafeTransferFrom(token, currentContract, to, value); + function SafeTransferLib.safeTransferFrom(address token, address from, address to, uint256 value) internal => summarySafeTransferFrom(token, from, to, value); +} + +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); + } +} + +// Assume no fee. +function summaryAccrueInterest(env e, MorphoInternalAccess.MarketParams marketParams, MorphoInternalAccess.Id id) returns bool { + require e.block.timestamp < 2^128; + if (e.block.timestamp != getLastUpdate(id) && getTotalBorrowAssets(id) != 0) { + uint128 interest = nonDetInterest(e); + uint256 borrow = getTotalBorrowAssets(id); + uint256 supply = getTotalSupplyAssets(id); + require interest + borrow < 2^256; + require interest + supply < 2^256; + increaseInterest(e, id, interest); + } + + update(e, id, e.block.timestamp); + + return true; +} + +definition isCreated(MorphoInternalAccess.Id id) returns bool = + getLastUpdate(id) != 0; + +rule supplyMovesTokensAndIncreasesShares(env e, MorphoInternalAccess.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { + MorphoInternalAccess.Id id = getMarketId(marketParams); + + require e.msg.sender != currentContract; + require getLastUpdate(id) == e.block.timestamp; + + mathint sharesBefore = getSupplyShares(id, onBehalf); + mathint balanceBefore = myBalances[marketParams.borrowableToken]; + + uint256 suppliedAssets; + uint256 suppliedShares; + suppliedAssets, suppliedShares = supply(e, marketParams, assets, shares, onBehalf, data); + + mathint sharesAfter = getSupplyShares(id, onBehalf); + mathint balanceAfter = myBalances[marketParams.borrowableToken]; + + assert assets != 0 => suppliedAssets == assets; + assert assets == 0 => suppliedShares == shares; + assert sharesAfter == sharesBefore + suppliedShares; + assert balanceAfter == balanceBefore + suppliedAssets; +} + +rule canRepayAll(env e, MorphoInternalAccess.MarketParams marketParams, uint256 shares, bytes data) { + MorphoInternalAccess.Id id = getMarketId(marketParams); + + require data.length == 0; + + require shares == getBorrowShares(id, e.msg.sender); + require isCreated(id); + require e.msg.sender != 0; + require e.msg.value == 0; + require shares > 0; + require getLastUpdate(id) <= e.block.timestamp; + require shares <= getTotalBorrowShares(id); + require getTotalBorrowAssets(id) < 10^35; + + repay@withrevert(e, marketParams, 0, shares, e.msg.sender, data); + + assert !lastReverted; +} + +rule canWithdrawAll(env e, MorphoInternalAccess.MarketParams marketParams, uint256 shares, address receiver) { + MorphoInternalAccess.Id id = getMarketId(marketParams); + + require shares == getSupplyShares(id, e.msg.sender); + require isCreated(id); + require e.msg.sender != 0; + require receiver != 0; + require e.msg.value == 0; + require shares > 0; + require getTotalBorrowAssets(id) == 0; + require getLastUpdate(id) <= e.block.timestamp; + require shares <= getTotalSupplyShares(id); + + withdraw@withrevert(e, marketParams, 0, shares, e.msg.sender, receiver); + + assert !lastReverted; +} + +rule canWithdrawCollateralAll(env e, MorphoInternalAccess.MarketParams marketParams, uint256 assets, address receiver) { + MorphoInternalAccess.Id id = getMarketId(marketParams); + + require assets == getCollateral(id, e.msg.sender); + require isCreated(id); + require receiver != 0; + require e.msg.value == 0; + require assets > 0; + require getLastUpdate(id) <= e.block.timestamp; + require getBorrowShares(id, e.msg.sender) == 0; + + withdrawCollateral@withrevert(e, marketParams, assets, e.msg.sender, receiver); + + assert !lastReverted; +} From f76a90c8b989a7c9dcd4fb4ac815ecee1cdae2a5 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 29 Aug 2023 17:36:28 +0200 Subject: [PATCH 092/204] chore: add liveness to the CI --- .github/workflows/certora.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml index acc163e53..72a3df433 100644 --- a/.github/workflows/certora.yml +++ b/.github/workflows/certora.yml @@ -47,6 +47,7 @@ jobs: - verifyBlueAccrueInterests.sh - verifyBlueExitLiquidity.sh - verifyBlueLibSummary.sh + - verifyBlueLiveness.sh - verifyBlueRatioMath.sh - verifyBlueReverts.sh - verifyBlueTransfer.sh From 272ee064847ccb69031018e88594d15f16452490 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 30 Aug 2023 18:59:58 +0200 Subject: [PATCH 093/204] fix: remove useless return value, simplify non det interest --- certora/harness/MorphoInternalAccess.sol | 6 ------ certora/specs/BlueLiveness.spec | 9 +++------ 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/certora/harness/MorphoInternalAccess.sol b/certora/harness/MorphoInternalAccess.sol index b294b45dd..cf46bf451 100644 --- a/certora/harness/MorphoInternalAccess.sol +++ b/certora/harness/MorphoInternalAccess.sol @@ -5,12 +5,6 @@ import "./MorphoHarness.sol"; contract MorphoInternalAccess is MorphoHarness { constructor(address newOwner) MorphoHarness(newOwner) {} - uint128 internal interest; - - function nonDetInterest() external view returns (uint128) { - return interest; - } - function update(Id id, uint256 timestamp) external { market[id].lastUpdate = uint128(timestamp); } diff --git a/certora/specs/BlueLiveness.spec b/certora/specs/BlueLiveness.spec index 5ee69064f..c3b631978 100644 --- a/certora/specs/BlueLiveness.spec +++ b/certora/specs/BlueLiveness.spec @@ -10,8 +10,7 @@ methods { function getLastUpdate(MorphoInternalAccess.Id) external returns uint256 envfree; function getMarketId(MorphoInternalAccess.MarketParams) external returns MorphoInternalAccess.Id envfree; - function _._accrueInterest(MorphoInternalAccess.MarketParams memory marketParams, MorphoInternalAccess.Id id) internal with (env e) => summaryAccrueInterest(e, marketParams, id) expect bool; - function _.nonDetInterest() external => NONDET; + function _._accrueInterest(MorphoInternalAccess.MarketParams memory marketParams, MorphoInternalAccess.Id id) internal with (env e) => summaryAccrueInterest(e, marketParams, id) expect void; function SafeTransferLib.safeTransfer(address token, address to, uint256 value) internal => summarySafeTransferFrom(token, currentContract, to, value); function SafeTransferLib.safeTransferFrom(address token, address from, address to, uint256 value) internal => summarySafeTransferFrom(token, from, to, value); @@ -31,10 +30,10 @@ function summarySafeTransferFrom(address token, address from, address to, uint25 } // Assume no fee. -function summaryAccrueInterest(env e, MorphoInternalAccess.MarketParams marketParams, MorphoInternalAccess.Id id) returns bool { +function summaryAccrueInterest(env e, MorphoInternalAccess.MarketParams marketParams, MorphoInternalAccess.Id id) { require e.block.timestamp < 2^128; if (e.block.timestamp != getLastUpdate(id) && getTotalBorrowAssets(id) != 0) { - uint128 interest = nonDetInterest(e); + uint128 interest; uint256 borrow = getTotalBorrowAssets(id); uint256 supply = getTotalSupplyAssets(id); require interest + borrow < 2^256; @@ -43,8 +42,6 @@ function summaryAccrueInterest(env e, MorphoInternalAccess.MarketParams marketPa } update(e, id, e.block.timestamp); - - return true; } definition isCreated(MorphoInternalAccess.Id id) returns bool = From 43397a3a45478cb1d2fa40e684ae059b80144fa1 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 30 Aug 2023 19:02:59 +0200 Subject: [PATCH 094/204] fix: remove extSloads munging --- certora/applyMunging.patch | 37 +------------------------- certora/specs/Blue.spec | 1 + certora/specs/BlueAccrueInterests.spec | 1 + certora/specs/BlueExitLiquidity.spec | 1 + certora/specs/BlueLibSummary.spec | 1 + certora/specs/BlueLiveness.spec | 1 + certora/specs/BlueRatioMath.spec | 1 + certora/specs/BlueReverts.spec | 1 + certora/specs/DifficultMath.spec | 1 + certora/specs/Health.spec | 1 + certora/specs/Reentrancy.spec | 1 + 11 files changed, 11 insertions(+), 36 deletions(-) diff --git a/certora/applyMunging.patch b/certora/applyMunging.patch index 6b7d547fd..8b490be5c 100644 --- a/certora/applyMunging.patch +++ b/certora/applyMunging.patch @@ -1,14 +1,4 @@ -diff -ruN interfaces/IMorpho.sol interfaces/IMorpho.sol ---- interfaces/IMorpho.sol 2023-08-29 09:58:51.628147127 +0200 -+++ interfaces/IMorpho.sol 2023-08-29 10:15:36.946593577 +0200 -@@ -292,7 +292,4 @@ - /// @param authorization The `Authorization` struct. - /// @param signature The signature. - function setAuthorizationWithSig(Authorization calldata authorization, Signature calldata signature) external; -- -- /// @notice Returns the data stored on the different `slots`. -- function extSloads(bytes32[] memory slots) external view returns (bytes32[] memory); - } + diff -ruN libraries/MarketParamsLib.sol libraries/MarketParamsLib.sol --- libraries/MarketParamsLib.sol 2023-08-29 09:59:37.937583556 +0200 +++ libraries/MarketParamsLib.sol 2023-08-29 10:16:10.519752188 +0200 @@ -22,28 +12,3 @@ diff -ruN libraries/MarketParamsLib.sol libraries/MarketParamsLib.sol + marketParamsId = Id.wrap(keccak256(abi.encode(marketParams))); } } -diff -ruN Morpho.sol Morpho.sol ---- Morpho.sol 2023-08-29 09:58:43.158169812 +0200 -+++ Morpho.sol 2023-08-29 10:15:20.650012827 +0200 -@@ -502,21 +502,4 @@ - - return maxBorrow >= borrowed; - } -- -- /* STORAGE VIEW */ -- -- /// @inheritdoc IMorpho -- function extSloads(bytes32[] calldata slots) external view returns (bytes32[] memory res) { -- uint256 nSlots = slots.length; -- -- res = new bytes32[](nSlots); -- -- for (uint256 i; i < nSlots;) { -- bytes32 slot = slots[i++]; -- -- assembly ("memory-safe") { -- mstore(add(res, mul(i, 32)), sload(slot)) -- } -- } -- } - } diff --git a/certora/specs/Blue.spec b/certora/specs/Blue.spec index 55a56b668..6655a094f 100644 --- a/certora/specs/Blue.spec +++ b/certora/specs/Blue.spec @@ -1,4 +1,5 @@ methods { + function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); function getTotalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; function getTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; function getTotalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree; diff --git a/certora/specs/BlueAccrueInterests.spec b/certora/specs/BlueAccrueInterests.spec index 0ac324321..fb8aa5075 100644 --- a/certora/specs/BlueAccrueInterests.spec +++ b/certora/specs/BlueAccrueInterests.spec @@ -1,4 +1,5 @@ methods { + function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); function MathLib.mulDivDown(uint256 a, uint256 b, uint256 c) internal returns uint256 => ghostMulDivDown(a,b,c); function MathLib.mulDivUp(uint256 a, uint256 b, uint256 c) internal returns uint256 => ghostMulDivUp(a,b,c); function MathLib.wTaylorCompounded(uint256 a, uint256 b) internal returns uint256 => ghostTaylorCompounded(a, b); diff --git a/certora/specs/BlueExitLiquidity.spec b/certora/specs/BlueExitLiquidity.spec index b8f1e6337..4d925c11b 100644 --- a/certora/specs/BlueExitLiquidity.spec +++ b/certora/specs/BlueExitLiquidity.spec @@ -1,4 +1,5 @@ methods { + function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); function getSupplyShares(MorphoHarness.Id, address) external returns uint256 envfree; function getBorrowShares(MorphoHarness.Id, address) external returns uint256 envfree; function getCollateral(MorphoHarness.Id, address) external returns uint256 envfree; diff --git a/certora/specs/BlueLibSummary.spec b/certora/specs/BlueLibSummary.spec index 9f41296a0..827304a37 100644 --- a/certora/specs/BlueLibSummary.spec +++ b/certora/specs/BlueLibSummary.spec @@ -1,4 +1,5 @@ methods { + function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); function mathLibMulDivUp(uint256, uint256, uint256) external returns uint256 envfree; function mathLibMulDivDown(uint256, uint256, uint256) external returns uint256 envfree; function getMarketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; diff --git a/certora/specs/BlueLiveness.spec b/certora/specs/BlueLiveness.spec index c3b631978..a2725210a 100644 --- a/certora/specs/BlueLiveness.spec +++ b/certora/specs/BlueLiveness.spec @@ -1,4 +1,5 @@ methods { + function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); function getTotalSupplyAssets(MorphoInternalAccess.Id) external returns uint256 envfree; function getTotalSupplyShares(MorphoInternalAccess.Id) external returns uint256 envfree; function getTotalBorrowAssets(MorphoInternalAccess.Id) external returns uint256 envfree; diff --git a/certora/specs/BlueRatioMath.spec b/certora/specs/BlueRatioMath.spec index 3c192c4c1..8281d7208 100644 --- a/certora/specs/BlueRatioMath.spec +++ b/certora/specs/BlueRatioMath.spec @@ -1,4 +1,5 @@ methods { + function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); function getMarketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; function getVirtualTotalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; function getVirtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; diff --git a/certora/specs/BlueReverts.spec b/certora/specs/BlueReverts.spec index 751c1c452..f501fe82f 100644 --- a/certora/specs/BlueReverts.spec +++ b/certora/specs/BlueReverts.spec @@ -1,4 +1,5 @@ methods { + function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); function owner() external returns address envfree; function getTotalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; function getTotalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree; diff --git a/certora/specs/DifficultMath.spec b/certora/specs/DifficultMath.spec index 382626c34..a7b18a52a 100644 --- a/certora/specs/DifficultMath.spec +++ b/certora/specs/DifficultMath.spec @@ -1,4 +1,5 @@ methods { + function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); function getMarketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; function getVirtualTotalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; function getVirtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; diff --git a/certora/specs/Health.spec b/certora/specs/Health.spec index 42bcb4db2..99403f8a6 100644 --- a/certora/specs/Health.spec +++ b/certora/specs/Health.spec @@ -1,4 +1,5 @@ methods { + function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); function getLastUpdate(MorphoHarness.Id) external returns uint256 envfree; function getTotalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; function getBorrowShares(MorphoHarness.Id, address) external returns uint256 envfree; diff --git a/certora/specs/Reentrancy.spec b/certora/specs/Reentrancy.spec index 86ad76679..3f7bfdf1f 100644 --- a/certora/specs/Reentrancy.spec +++ b/certora/specs/Reentrancy.spec @@ -1,4 +1,5 @@ methods { + function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); function _.borrowRate(MorphoHarness.MarketParams marketParams, MorphoHarness.Market) external => summaryBorrowRate() expect uint256; } From 029b974d528f70ab9c675b3bf52bfa15b9b44212 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 30 Aug 2023 22:25:08 +0200 Subject: [PATCH 095/204] chore: remove canRepayAll rule for now --- certora/specs/BlueLiveness.spec | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/certora/specs/BlueLiveness.spec b/certora/specs/BlueLiveness.spec index c3b631978..4a77b573d 100644 --- a/certora/specs/BlueLiveness.spec +++ b/certora/specs/BlueLiveness.spec @@ -69,24 +69,24 @@ rule supplyMovesTokensAndIncreasesShares(env e, MorphoInternalAccess.MarketParam assert balanceAfter == balanceBefore + suppliedAssets; } -rule canRepayAll(env e, MorphoInternalAccess.MarketParams marketParams, uint256 shares, bytes data) { - MorphoInternalAccess.Id id = getMarketId(marketParams); +// rule canRepayAll(env e, MorphoInternalAccess.MarketParams marketParams, uint256 shares, bytes data) { +// MorphoInternalAccess.Id id = getMarketId(marketParams); - require data.length == 0; +// require data.length == 0; - require shares == getBorrowShares(id, e.msg.sender); - require isCreated(id); - require e.msg.sender != 0; - require e.msg.value == 0; - require shares > 0; - require getLastUpdate(id) <= e.block.timestamp; - require shares <= getTotalBorrowShares(id); - require getTotalBorrowAssets(id) < 10^35; +// require shares == getBorrowShares(id, e.msg.sender); +// require isCreated(id); +// require e.msg.sender != 0; +// require e.msg.value == 0; +// require shares > 0; +// require getLastUpdate(id) <= e.block.timestamp; +// require shares <= getTotalBorrowShares(id); +// require getTotalBorrowAssets(id) < 10^35; - repay@withrevert(e, marketParams, 0, shares, e.msg.sender, data); +// repay@withrevert(e, marketParams, 0, shares, e.msg.sender, data); - assert !lastReverted; -} +// assert !lastReverted; +// } rule canWithdrawAll(env e, MorphoInternalAccess.MarketParams marketParams, uint256 shares, address receiver) { MorphoInternalAccess.Id id = getMarketId(marketParams); From fbea3894549e898d0749d1de7ea116a13a263a25 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 31 Aug 2023 18:29:18 +0200 Subject: [PATCH 096/204] docs: renaming and documentation of the rules --- certora/makefile | 6 +- certora/{applyMunging.patch => munging.patch} | 0 ...ueInterests.sh => verifyAccrueInterest.sh} | 4 +- ...verifyBlue.sh => verifyConsistentState.sh} | 4 +- ...ifyDifficultMath.sh => verifyExactMath.sh} | 4 +- ...xitLiquidity.sh => verifyExitLiquidity.sh} | 2 +- certora/scripts/verifyHealth.sh | 2 +- ...yBlueLibSummary.sh => verifyLibSummary.sh} | 4 +- ...erifyBlueLiveness.sh => verifyLiveness.sh} | 4 +- ...ifyBlueRatioMath.sh => verifyRatioMath.sh} | 4 +- certora/scripts/verifyReentrancy.sh | 2 +- ...{verifyBlueReverts.sh => verifyReverts.sh} | 4 +- ...erifyBlueTransfer.sh => verifyTransfer.sh} | 4 +- ...crueInterests.spec => AccrueInterest.spec} | 106 +++++----------- .../specs/{Blue.spec => ConsistentState.spec} | 117 +++++++++++------- .../{DifficultMath.spec => ExactMath.spec} | 32 ++--- ...eExitLiquidity.spec => ExitLiquidity.spec} | 3 + certora/specs/Health.spec | 35 +++--- .../{BlueLibSummary.spec => LibSummary.spec} | 5 +- .../{BlueLiveness.spec => Liveness.spec} | 17 +++ .../{BlueRatioMath.spec => RatioMath.spec} | 48 ++++--- certora/specs/Reentrancy.spec | 1 + .../specs/{BlueReverts.spec => Reverts.spec} | 16 ++- .../{BlueTransfer.spec => Transfer.spec} | 20 +-- 24 files changed, 242 insertions(+), 202 deletions(-) rename certora/{applyMunging.patch => munging.patch} (100%) rename certora/scripts/{verifyBlueAccrueInterests.sh => verifyAccrueInterest.sh} (53%) rename certora/scripts/{verifyBlue.sh => verifyConsistentState.sh} (64%) rename certora/scripts/{verifyDifficultMath.sh => verifyExactMath.sh} (72%) rename certora/scripts/{verifyBlueExitLiquidity.sh => verifyExitLiquidity.sh} (77%) rename certora/scripts/{verifyBlueLibSummary.sh => verifyLibSummary.sh} (53%) rename certora/scripts/{verifyBlueLiveness.sh => verifyLiveness.sh} (66%) rename certora/scripts/{verifyBlueRatioMath.sh => verifyRatioMath.sh} (68%) rename certora/scripts/{verifyBlueReverts.sh => verifyReverts.sh} (64%) rename certora/scripts/{verifyBlueTransfer.sh => verifyTransfer.sh} (78%) rename certora/specs/{BlueAccrueInterests.spec => AccrueInterest.spec} (57%) rename certora/specs/{Blue.spec => ConsistentState.spec} (67%) rename certora/specs/{DifficultMath.spec => ExactMath.spec} (93%) rename certora/specs/{BlueExitLiquidity.spec => ExitLiquidity.spec} (91%) rename certora/specs/{BlueLibSummary.spec => LibSummary.spec} (76%) rename certora/specs/{BlueLiveness.spec => Liveness.spec} (83%) rename certora/specs/{BlueRatioMath.spec => RatioMath.spec} (74%) rename certora/specs/{BlueReverts.spec => Reverts.spec} (86%) rename certora/specs/{BlueTransfer.spec => Transfer.spec} (92%) diff --git a/certora/makefile b/certora/makefile index 36c1a0542..664ff4fb9 100644 --- a/certora/makefile +++ b/certora/makefile @@ -1,10 +1,10 @@ -munged: $(wildcard ../src/*.sol) applyMunging.patch +munged: $(wildcard ../src/*.sol) munging.patch @rm -rf munged @cp -r ../src munged - @patch -p0 -d munged < applyMunging.patch + @patch -p0 -d munged < munging.patch record: - diff -ruN ../src munged | sed 's+\.\./src/++g' | sed 's+munged/++g' > applyMunging.patch + diff -ruN ../src munged | sed 's+\.\./src/++g' | sed 's+munged/++g' > munging.patch clean: rm -rf munged diff --git a/certora/applyMunging.patch b/certora/munging.patch similarity index 100% rename from certora/applyMunging.patch rename to certora/munging.patch diff --git a/certora/scripts/verifyBlueAccrueInterests.sh b/certora/scripts/verifyAccrueInterest.sh similarity index 53% rename from certora/scripts/verifyBlueAccrueInterests.sh rename to certora/scripts/verifyAccrueInterest.sh index a43e36427..d97b89c0e 100755 --- a/certora/scripts/verifyBlueAccrueInterests.sh +++ b/certora/scripts/verifyAccrueInterest.sh @@ -6,7 +6,7 @@ make -C certora munged certoraRun \ certora/harness/MorphoHarness.sol \ - --verify MorphoHarness:certora/specs/BlueAccrueInterests.spec \ + --verify MorphoHarness:certora/specs/AccrueInterest.spec \ --solc_allow_path src \ - --msg "Morpho Blue Commutativity of accrueInterests" \ + --msg "Morpho Blue Accrue Interest" \ "$@" diff --git a/certora/scripts/verifyBlue.sh b/certora/scripts/verifyConsistentState.sh similarity index 64% rename from certora/scripts/verifyBlue.sh rename to certora/scripts/verifyConsistentState.sh index 9e6545d5b..d549cef7f 100755 --- a/certora/scripts/verifyBlue.sh +++ b/certora/scripts/verifyConsistentState.sh @@ -6,9 +6,9 @@ make -C certora munged certoraRun \ certora/harness/MorphoHarness.sol \ - --verify MorphoHarness:certora/specs/Blue.spec \ + --verify MorphoHarness:certora/specs/ConsistentState.spec \ --solc_allow_path src \ --loop_iter 3 \ --optimistic_loop \ - --msg "Morpho Blue" \ + --msg "Morpho Blue Consistent State" \ "$@" diff --git a/certora/scripts/verifyDifficultMath.sh b/certora/scripts/verifyExactMath.sh similarity index 72% rename from certora/scripts/verifyDifficultMath.sh rename to certora/scripts/verifyExactMath.sh index cb7114d96..adeb34d75 100755 --- a/certora/scripts/verifyDifficultMath.sh +++ b/certora/scripts/verifyExactMath.sh @@ -7,9 +7,9 @@ make -C certora munged certoraRun \ certora/harness/MorphoHarness.sol \ src/mocks/OracleMock.sol \ - --verify MorphoHarness:certora/specs/DifficultMath.spec \ + --verify MorphoHarness:certora/specs/ExactMath.spec \ --loop_iter 3 \ --optimistic_loop \ --prover_args '-smt_hashingScheme plaininjectivity' \ - --msg "Morpho Difficult Math" \ + --msg "Morpho Blue Exact Math" \ "$@" diff --git a/certora/scripts/verifyBlueExitLiquidity.sh b/certora/scripts/verifyExitLiquidity.sh similarity index 77% rename from certora/scripts/verifyBlueExitLiquidity.sh rename to certora/scripts/verifyExitLiquidity.sh index e23ee0bbb..d83843fd6 100755 --- a/certora/scripts/verifyBlueExitLiquidity.sh +++ b/certora/scripts/verifyExitLiquidity.sh @@ -6,7 +6,7 @@ make -C certora munged certoraRun \ certora/harness/MorphoHarness.sol \ - --verify MorphoHarness:certora/specs/BlueExitLiquidity.spec \ + --verify MorphoHarness:certora/specs/ExitLiquidity.spec \ --solc_allow_path src \ --loop_iter 3 \ --optimistic_loop \ diff --git a/certora/scripts/verifyHealth.sh b/certora/scripts/verifyHealth.sh index d46f97a3a..beaed1600 100755 --- a/certora/scripts/verifyHealth.sh +++ b/certora/scripts/verifyHealth.sh @@ -11,5 +11,5 @@ certoraRun \ --loop_iter 3 \ --optimistic_loop \ --prover_args '-smt_hashingScheme plaininjectivity' \ - --msg "Morpho Health Check" \ + --msg "Morpho Blue Health Check" \ "$@" diff --git a/certora/scripts/verifyBlueLibSummary.sh b/certora/scripts/verifyLibSummary.sh similarity index 53% rename from certora/scripts/verifyBlueLibSummary.sh rename to certora/scripts/verifyLibSummary.sh index eb4391809..85b78dd6e 100755 --- a/certora/scripts/verifyBlueLibSummary.sh +++ b/certora/scripts/verifyLibSummary.sh @@ -6,6 +6,6 @@ make -C certora munged certoraRun \ certora/harness/MorphoHarness.sol \ - --verify MorphoHarness:certora/specs/BlueLibSummary.spec \ - --msg "Morpho Ratio Math Summary" \ + --verify MorphoHarness:certora/specs/LibSummary.spec \ + --msg "Morpho Blue Lib Summary" \ "$@" diff --git a/certora/scripts/verifyBlueLiveness.sh b/certora/scripts/verifyLiveness.sh similarity index 66% rename from certora/scripts/verifyBlueLiveness.sh rename to certora/scripts/verifyLiveness.sh index c138b238c..cdaacf79b 100755 --- a/certora/scripts/verifyBlueLiveness.sh +++ b/certora/scripts/verifyLiveness.sh @@ -6,9 +6,9 @@ make -C certora munged certoraRun \ certora/harness/MorphoInternalAccess.sol \ - --verify MorphoInternalAccess:certora/specs/BlueLiveness.spec \ + --verify MorphoInternalAccess:certora/specs/Liveness.spec \ --solc_allow_path src \ --loop_iter 3 \ --optimistic_loop \ - --msg "Morpho Blue" \ + --msg "Morpho Blue Liveness" \ "$@" diff --git a/certora/scripts/verifyBlueRatioMath.sh b/certora/scripts/verifyRatioMath.sh similarity index 68% rename from certora/scripts/verifyBlueRatioMath.sh rename to certora/scripts/verifyRatioMath.sh index 1b5cacbdc..14b53c6b8 100755 --- a/certora/scripts/verifyBlueRatioMath.sh +++ b/certora/scripts/verifyRatioMath.sh @@ -6,8 +6,8 @@ make -C certora munged certoraRun \ certora/harness/MorphoHarness.sol \ - --verify MorphoHarness:certora/specs/BlueRatioMath.spec \ + --verify MorphoHarness:certora/specs/RatioMath.spec \ --solc_allow_path src \ - --msg "Morpho Ratio Math" \ --prover_args '-smt_hashingScheme plaininjectivity' \ + --msg "Morpho Blue Ratio Math" \ "$@" diff --git a/certora/scripts/verifyReentrancy.sh b/certora/scripts/verifyReentrancy.sh index 0336e3f9f..570c2d986 100755 --- a/certora/scripts/verifyReentrancy.sh +++ b/certora/scripts/verifyReentrancy.sh @@ -10,5 +10,5 @@ certoraRun \ --prover_args '-enableStorageSplitting false' \ --loop_iter 3 \ --optimistic_loop \ - --msg "Check Reentrancy" \ + --msg "Morpho Blue Reentrancy" \ "$@" diff --git a/certora/scripts/verifyBlueReverts.sh b/certora/scripts/verifyReverts.sh similarity index 64% rename from certora/scripts/verifyBlueReverts.sh rename to certora/scripts/verifyReverts.sh index 4d30e8d26..3e340ceb9 100755 --- a/certora/scripts/verifyBlueReverts.sh +++ b/certora/scripts/verifyReverts.sh @@ -6,8 +6,8 @@ make -C certora munged certoraRun \ certora/harness/MorphoHarness.sol \ - --verify MorphoHarness:certora/specs/BlueReverts.spec \ + --verify MorphoHarness:certora/specs/Reverts.spec \ --loop_iter 3 \ --optimistic_loop \ - --msg "Morpho Reverts" \ + --msg "Morpho Blue Reverts" \ "$@" diff --git a/certora/scripts/verifyBlueTransfer.sh b/certora/scripts/verifyTransfer.sh similarity index 78% rename from certora/scripts/verifyBlueTransfer.sh rename to certora/scripts/verifyTransfer.sh index a84744b3e..1fee34c92 100755 --- a/certora/scripts/verifyBlueTransfer.sh +++ b/certora/scripts/verifyTransfer.sh @@ -9,9 +9,9 @@ certoraRun \ certora/dispatch/ERC20Standard.sol \ certora/dispatch/ERC20USDT.sol \ certora/dispatch/ERC20NoRevert.sol \ - --verify TransferHarness:certora/specs/BlueTransfer.spec \ + --verify TransferHarness:certora/specs/Transfer.spec \ --packages openzeppelin-contracts=lib/openzeppelin-contracts/contracts \ --loop_iter 3 \ --optimistic_loop \ - --msg "Morpho Transfer Summary" \ + --msg "Morpho Blue Transfer" \ "$@" diff --git a/certora/specs/BlueAccrueInterests.spec b/certora/specs/AccrueInterest.spec similarity index 57% rename from certora/specs/BlueAccrueInterests.spec rename to certora/specs/AccrueInterest.spec index fb8aa5075..24990229a 100644 --- a/certora/specs/BlueAccrueInterests.spec +++ b/certora/specs/AccrueInterest.spec @@ -4,9 +4,8 @@ methods { function MathLib.mulDivUp(uint256 a, uint256 b, uint256 c) internal returns uint256 => ghostMulDivUp(a,b,c); function MathLib.wTaylorCompounded(uint256 a, uint256 b) internal returns uint256 => ghostTaylorCompounded(a, b); - // we assume here that all external functions will not access storage, since we cannot show - // commutativity otherwise. We also need to assume that the price and borrow rate return - // always the same value (and do not depend on msg.origin), so we use ghost functions for them. + // We assume here that all external functions will not access storage, since we cannot show commutativity otherwise. + // We also need to assume that the price and borrow rate return always the same value (and do not depend on msg.origin), so we use ghost functions for them. function _.borrowRate(MorphoHarness.MarketParams marketParams, MorphoHarness.Market market) external with (env e) => ghostBorrowRate(marketParams.irm, e.block.timestamp) expect uint256; function _.price() external with (env e) => ghostOraclePrice(e.block.timestamp) expect uint256; function _.transfer(address to, uint256 amount) external => ghostTransfer(to, amount) expect bool; @@ -30,124 +29,81 @@ ghost ghostOraclePrice(uint256) returns uint256; ghost ghostTransfer(address, uint256) returns bool; ghost ghostTransferFrom(address, address, uint256) returns bool; -rule supplyAccruesInterests() -{ - env e; - MorphoHarness.MarketParams marketParams; - uint256 assets; - uint256 shares; - address onbehalf; - bytes data; +// Check that calling accrueInterest first has no effect. +// This is because supply should call accrueInterest itself. +rule supplyAccruesInterest(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { require e.block.timestamp < 2^128; storage init = lastStorage; - // check that calling accrueInterest first has no effect. - // this is because supply should call accrueInterest itself. - accrueInterest(e, marketParams); - supply(e, marketParams, assets, shares, onbehalf, data); + supply(e, marketParams, assets, shares, onBehalf, data); storage afterBoth = lastStorage; - supply(e, marketParams, assets, shares, onbehalf, data) at init; - + supply(e, marketParams, assets, shares, onBehalf, data) at init; storage afterOne = lastStorage; assert afterBoth == afterOne; } -rule withdrawAccruesInterests() -{ - env e; - MorphoHarness.MarketParams marketParams; - uint256 assets; - uint256 shares; - address onbehalf; - address receiver; - +// Check that calling accrueInterest first has no effect. +// This is because withdraw should call accrueInterest itself. +rule withdrawAccruesInterest(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) { require e.block.timestamp < 2^128; storage init = lastStorage; - // check that calling accrueInterest first has no effect. - // this is because withdraw should call accrueInterest itself. - accrueInterest(e, marketParams); - withdraw(e, marketParams, assets, shares, onbehalf, receiver); + withdraw(e, marketParams, assets, shares, onBehalf, receiver); storage afterBoth = lastStorage; - withdraw(e, marketParams, assets, shares, onbehalf, receiver) at init; - + withdraw(e, marketParams, assets, shares, onBehalf, receiver) at init; storage afterOne = lastStorage; assert afterBoth == afterOne; } -rule borrowAccruesInterests() -{ - env e; - MorphoHarness.MarketParams marketParams; - uint256 assets; - uint256 shares; - address onbehalf; - address receiver; - +// Check that calling accrueInterest first has no effect. +// This is because borrow should call accrueInterest itself. +rule borrowAccruesInterest(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) { require e.block.timestamp < 2^128; storage init = lastStorage; - // check that calling accrueInterest first has no effect. - // this is because borrow should call accrueInterest itself. - accrueInterest(e, marketParams); - borrow(e, marketParams, assets, shares, onbehalf, receiver); + borrow(e, marketParams, assets, shares, onBehalf, receiver); storage afterBoth = lastStorage; - borrow(e, marketParams, assets, shares, onbehalf, receiver) at init; - + borrow(e, marketParams, assets, shares, onBehalf, receiver) at init; storage afterOne = lastStorage; assert afterBoth == afterOne; } -rule repayAccruesInterests() -{ - env e; - MorphoHarness.MarketParams marketParams; - uint256 assets; - uint256 shares; - address onbehalf; - bytes data; - +// Check that calling accrueInterest first has no effect. +// This is because repay should call accrueInterest itself. +rule repayAccruesInterest(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { require e.block.timestamp < 2^128; storage init = lastStorage; - // check that calling accrueInterest first has no effect. - // this is because repay should call accrueInterest itself. - accrueInterest(e, marketParams); - repay(e, marketParams, assets, shares, onbehalf, data); + repay(e, marketParams, assets, shares, onBehalf, data); storage afterBoth = lastStorage; - repay(e, marketParams, assets, shares, onbehalf, data) at init; - + repay(e, marketParams, assets, shares, onBehalf, data) at init; storage afterOne = lastStorage; assert afterBoth == afterOne; } - -/** - * Show that accrueInterest commutes with other state changing rules. - * We exclude view functions, because (a) we cannot check the return - * value and for storage commutativity is trivial and (b) most view - * functions, e.g. totalSupplyShares, are not commutative, i.e. they return - * a different value if called before accrueInterest is called. - * We also exclude setFeeRecipient, as it is known to be not commutative. - */ -rule accrueInterestsCommutesExceptForSetFeeRecipient(method f, env e, calldataarg args) +// Show that accrueInterest commutes with other state changing rules. +// We exclude view functions, because: +// (a) we cannot check the return value and for storage commutativity is trivial, and +// (b) most view functions, e.g. totalSupplyShares, are not commutative, i.e. they return a different value if called before accrueInterest is called. +// We also exclude setFeeRecipient, as it is known to be not commutative. +rule accrueInterestCommutesExceptForSetFeeRecipient(method f, calldataarg args) filtered { f -> !f.isView && f.selector != sig:setFeeRecipient(address).selector } @@ -156,24 +112,22 @@ filtered { env e2; MorphoHarness.MarketParams marketParams; + // Require interactions to happen at the same block. require e1.block.timestamp == e2.block.timestamp; + // Assumption required to cast a timestamp to uint128. require e1.block.timestamp < 2^128; storage init = lastStorage; - // check that accrueInterest commutes with every other function. - accrueInterest(e1, marketParams); f@withrevert(e2, args); bool revert1 = lastReverted; - storage store1 = lastStorage; f@withrevert(e2, args) at init; bool revert2 = lastReverted; accrueInterest(e1, marketParams); - storage store2 = lastStorage; assert revert1 <=> revert2; diff --git a/certora/specs/Blue.spec b/certora/specs/ConsistentState.spec similarity index 67% rename from certora/specs/Blue.spec rename to certora/specs/ConsistentState.spec index 6655a094f..1982eb09b 100644 --- a/certora/specs/Blue.spec +++ b/certora/specs/ConsistentState.spec @@ -39,8 +39,8 @@ ghost mapping(address => mathint) myBalances { init_state axiom (forall address token. myBalances[token] == 0); } -ghost mapping(address => mathint) expectedAmount { - init_state axiom (forall address token. expectedAmount[token] == 0); +ghost mapping(address => mathint) sumAmount { + init_state axiom (forall address token. sumAmount[token] == 0); } ghost mapping(MorphoHarness.Id => address) idToBorrowable; @@ -65,15 +65,15 @@ hook Sstore position[KEY MorphoHarness.Id id][KEY address owner].borrowShares ui hook Sstore position[KEY MorphoHarness.Id id][KEY address owner].collateral uint128 newAmount (uint128 oldAmount) STORAGE { sumCollateral[id] = sumCollateral[id] - oldAmount + newAmount; - expectedAmount[idToCollateral[id]] = expectedAmount[idToCollateral[id]] - oldAmount + newAmount; + sumAmount[idToCollateral[id]] = sumAmount[idToCollateral[id]] - oldAmount + newAmount; } hook Sstore market[KEY MorphoHarness.Id id].totalSupplyAssets uint128 newAmount (uint128 oldAmount) STORAGE { - expectedAmount[idToBorrowable[id]] = expectedAmount[idToBorrowable[id]] - oldAmount + newAmount; + sumAmount[idToBorrowable[id]] = sumAmount[idToBorrowable[id]] - oldAmount + newAmount; } hook Sstore market[KEY MorphoHarness.Id id].totalBorrowAssets uint128 newAmount (uint128 oldAmount) STORAGE { - expectedAmount[idToBorrowable[id]] = expectedAmount[idToBorrowable[id]] + oldAmount - newAmount; + sumAmount[idToBorrowable[id]] = sumAmount[idToBorrowable[id]] + oldAmount - newAmount; } function summarySafeTransferFrom(address token, address from, address to, uint256 amount) { @@ -88,66 +88,77 @@ function summarySafeTransferFrom(address token, address from, address to, uint25 definition isCreated(MorphoHarness.Id id) returns bool = getLastUpdate(id) != 0; +// Check that the fee is always lower than the max fee constant. invariant feeInRange(MorphoHarness.Id id) getFee(id) <= MAX_FEE(); +// Check that the accounting of totalSupplyShares is correct. invariant sumSupplySharesCorrect(MorphoHarness.Id id) to_mathint(getTotalSupplyShares(id)) == sumSupplyShares[id]; +// Check that the accounting of totalBorrowShares is correct. invariant sumBorrowSharesCorrect(MorphoHarness.Id id) to_mathint(getTotalBorrowShares(id)) == sumBorrowShares[id]; +// Check that a market only allows borrows up to the total supply. +// This invariant shows that markets are independent, tokens from one market cannot be taken by interacting with another market. invariant borrowLessSupply(MorphoHarness.Id id) getTotalBorrowAssets(id) <= getTotalSupplyAssets(id); +// This invariant is useful in the following rule, to link an id back to a market. invariant marketInvariant(MorphoHarness.MarketParams marketParams) isCreated(getMarketId(marketParams)) => idToBorrowable[getMarketId(marketParams)] == marketParams.borrowableToken && idToCollateral[getMarketId(marketParams)] == marketParams.collateralToken; +// Check that the idle amount on the singleton is greater to the sum amount, that is the sum over all the markets of the total supply plus the total collateral minus the total borrow. invariant isLiquid(address token) - expectedAmount[token] <= myBalances[token] + sumAmount[token] <= myBalances[token] { - preserved supply(MorphoHarness.MarketParams marketParams, uint256 _a, uint256 _s, address _o, bytes _d) with (env e) { + preserved supply(MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) with (env e) { requireInvariant marketInvariant(marketParams); require e.msg.sender != currentContract; } - preserved withdraw(MorphoHarness.MarketParams marketParams, uint256 _a, uint256 _s, address _o, address _r) with (env e) { + preserved withdraw(MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) with (env e) { requireInvariant marketInvariant(marketParams); require e.msg.sender != currentContract; } - preserved borrow(MorphoHarness.MarketParams marketParams, uint256 _a, uint256 _s, address _o, address _r) with (env e) { + preserved borrow(MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) with (env e) { requireInvariant marketInvariant(marketParams); require e.msg.sender != currentContract; } - preserved repay(MorphoHarness.MarketParams marketParams, uint256 _a, uint256 _s, address _o, bytes _d) with (env e) { + preserved repay(MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) with (env e) { requireInvariant marketInvariant(marketParams); require e.msg.sender != currentContract; } - preserved supplyCollateral(MorphoHarness.MarketParams marketParams, uint256 _a, address _o, bytes _d) with (env e) { + preserved supplyCollateral(MorphoHarness.MarketParams marketParams, uint256 assets, address onBehalf, bytes data) with (env e) { requireInvariant marketInvariant(marketParams); require e.msg.sender != currentContract; } - preserved withdrawCollateral(MorphoHarness.MarketParams marketParams, uint256 _a, address _o, address _r) with (env e) { + preserved withdrawCollateral(MorphoHarness.MarketParams marketParams, uint256 assets, address onBehalf, address receiver) with (env e) { requireInvariant marketInvariant(marketParams); require e.msg.sender != currentContract; } - preserved liquidate(MorphoHarness.MarketParams marketParams, address _b, uint256 _s, uint256 _r, bytes _d) with (env e) { + preserved liquidate(MorphoHarness.MarketParams marketParams, address _b, uint256 shares, uint256 receiver, bytes data) with (env e) { requireInvariant marketInvariant(marketParams); require e.msg.sender != currentContract; } } +// Check that a market can only exist if its LLTV is enabled. invariant onlyEnabledLltv(MorphoHarness.MarketParams marketParams) isCreated(getMarketId(marketParams)) => isLltvEnabled(marketParams.lltv); +// Check that a market can only exist if its IRM is enabled. invariant onlyEnabledIrm(MorphoHarness.MarketParams marketParams) isCreated(getMarketId(marketParams)) => isIrmEnabled(marketParams.irm); +// Check the pseudo-injectivity of the hashing function id(). rule marketIdUnique() { MorphoHarness.MarketParams marketParams1; MorphoHarness.MarketParams marketParams2; + // Require the same arguments. require getMarketId(marketParams1) == getMarketId(marketParams2); assert marketParams1.borrowableToken == marketParams2.borrowableToken; @@ -157,35 +168,35 @@ rule marketIdUnique() { assert marketParams1.lltv == marketParams2.lltv; } -rule onlyUserCanAuthorizeWithoutSig(method f, calldataarg data) +// Check that only the user is able to change who is authorized to manage his position. +rule onlyUserCanAuthorizeWithoutSig(env e, method f, calldataarg data) filtered { f -> !f.isView && f.selector != sig:setAuthorizationWithSig(MorphoHarness.Authorization memory, MorphoHarness.Signature calldata).selector } { address user; address someone; - env e; + // Require a different user to interact with Morpho. require user != e.msg.sender; + bool authorizedBefore = isAuthorized(user, someone); f(e, data); - assert isAuthorized(user, someone) == authorizedBefore; + bool authorizedAfter = isAuthorized(user, someone); + + assert authorizedAfter == authorizedBefore; } -rule userCannotLoseSupplyShares(method f, calldataarg data) +// Check that only authorized users are able to decrease supply shares of a position. +rule userCannotLoseSupplyShares(env e, method f, calldataarg data) filtered { f -> !f.isView } { - MorphoHarness.MarketParams marketParams; - uint256 assets; - uint256 shares; - uint256 suppliedAssets; - uint256 suppliedShares; + MorphoHarness.Id id; address user; - MorphoHarness.Id id = getMarketId(marketParams); - env e; + // Require that the e.msg.sender is not authorized. require !isAuthorized(user, e.msg.sender); require user != e.msg.sender; @@ -194,62 +205,78 @@ filtered { f -> !f.isView } f(e, data); mathint sharesAfter = getSupplyShares(id, user); + assert sharesAfter >= sharesBefore; } -rule userCannotGainBorrowShares(method f, calldataarg data) +// Check that only authorized users are able to increase the borrow shares of a position. +rule userCannotGainBorrowShares(env e, method f, calldataarg args) filtered { f -> !f.isView } { - MorphoHarness.MarketParams marketParams; - uint256 assets; - uint256 shares; - uint256 suppliedAssets; - uint256 suppliedShares; + MorphoHarness.Id id; address user; - MorphoHarness.Id id = getMarketId(marketParams); - env e; + // Require that the e.msg.sender is not authorized. require !isAuthorized(user, e.msg.sender); require user != e.msg.sender; mathint sharesBefore = getBorrowShares(id, user); - f(e, data); + f(e, args); mathint sharesAfter = getBorrowShares(id, user); + assert sharesAfter <= sharesBefore; } +// Check that users cannot lose collateral by unauthorized parties except in case of a liquidation. +rule userCannotLoseCollateralExceptLiquidate(env e, method f, calldataarg args) +filtered { + f -> !f.isView && + f.selector != sig:liquidate(MorphoHarness.MarketParams, address, uint256, uint256, bytes).selector +{ + MorphoHarness.Id id; + address user; -rule userWithoutBorrowCannotLoseCollateral(method f, calldataarg data) + // Require that the e.msg.sender is not authorized. + require !isAuthorized(user, e.msg.sender); + require user != e.msg.sender; + + f(e, args); + + mathint collateralAfter = getCollateral(id, user); + + assert collateralAfter >= collateralBefore; +} + +// Check that users cannot lose collateral by unauthorized parties if they have no outstanding debt. +rule userWithoutBorrowCannotLoseCollateral(env e, method f, calldataarg args) filtered { f -> !f.isView } { - MorphoHarness.MarketParams marketParams; - uint256 assets; - uint256 shares; - uint256 suppliedAssets; - uint256 suppliedShares; + MorphoHarness.Id id; address user; - MorphoHarness.Id id = getMarketId(marketParams); - env e; + // Require that the e.msg.sender is not authorized. require !isAuthorized(user, e.msg.sender); require user != e.msg.sender; + // Require that the user has no outstanding debt. require getBorrowShares(id, user) == 0; + mathint collateralBefore = getCollateral(id, user); - f(e, data); + f(e, args); mathint collateralAfter = getCollateral(id, user); - assert getBorrowShares(id, user) == 0; + assert collateralAfter >= collateralBefore; } -rule noTimeTravel(method f, env e, calldataarg data) +// Invariant checking that the last updated time is never greater than the current time. +rule noTimeTravel(method f, env e, calldataarg args) filtered { f -> !f.isView } { MorphoHarness.Id id; require getLastUpdate(id) <= e.block.timestamp; - f(e, data); + f(e, args); assert getLastUpdate(id) <= e.block.timestamp; } diff --git a/certora/specs/DifficultMath.spec b/certora/specs/ExactMath.spec similarity index 93% rename from certora/specs/DifficultMath.spec rename to certora/specs/ExactMath.spec index a7b18a52a..ddf83dc0a 100644 --- a/certora/specs/DifficultMath.spec +++ b/certora/specs/ExactMath.spec @@ -25,8 +25,7 @@ function summaryMulDivDown(uint256 x, uint256 y, uint256 d) returns uint256 { return require_uint256((x * y) / d); } -rule repayAllResetsBorrowRatio(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onbehalf, bytes data) -{ +rule repayAllResetsBorrowRatio(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onbehalf, bytes data) { MorphoHarness.Id id = getMarketId(marketParams); require getFee(id) <= MAX_FEE(); @@ -46,32 +45,33 @@ rule repayAllResetsBorrowRatio(env e, MorphoHarness.MarketParams marketParams, u assert assetsAfter == 1; } - // There should be no profit from supply followed immediately by withdraw. rule supplyWithdraw() { MorphoHarness.MarketParams marketParams; + MorphoHarness.Id id = getMarketId(marketParams); uint256 assets; uint256 shares; - uint256 suppliedAssets; - uint256 withdrawnAssets; - uint256 suppliedShares; - uint256 withdrawnShares; address onbehalf; address receiver; bytes data; env e1; env e2; + // Require interactions to happen at the same block. require e1.block.timestamp == e2.block.timestamp; + // Assumption required to cast timestamps to uint128. require e1.block.timestamp < 2^128; - require e2.block.timestamp < 2^128; + uint256 suppliedAssets; + uint256 suppliedShares; suppliedAssets, suppliedShares = supply(e1, marketParams, assets, shares, onbehalf, data); - MorphoHarness.Id id = getMarketId(marketParams); + // Hints for the prover. assert suppliedAssets * (getVirtualTotalSupplyShares(id) - suppliedShares) >= suppliedShares * (getVirtualTotalSupplyAssets(id) - suppliedAssets); assert suppliedAssets * getVirtualTotalSupplyShares(id) >= suppliedShares * getVirtualTotalSupplyAssets(id); + uint256 withdrawnAssets; + uint256 withdrawnShares; withdrawnAssets, withdrawnShares = withdraw(e2, marketParams, 0, suppliedShares, onbehalf, receiver); assert withdrawnShares == suppliedShares; @@ -81,28 +81,30 @@ rule supplyWithdraw() { // There should be no profit from withdraw followed immediately by supply. rule withdrawSupply() { MorphoHarness.MarketParams marketParams; + MorphoHarness.Id id = getMarketId(marketParams); uint256 assets; uint256 shares; - uint256 suppliedAssets; - uint256 withdrawnAssets; - uint256 suppliedShares; - uint256 withdrawnShares; address onbehalf; address receiver; bytes data; env e1; env e2; + // Require interactions to happen at the same block. require e1.block.timestamp == e2.block.timestamp; + // Assumption required to cast timestamps to uint128. require e1.block.timestamp < 2^128; - require e2.block.timestamp < 2^128; + uint256 withdrawnAssets; + uint256 withdrawnShares; withdrawnAssets, withdrawnShares = withdraw(e2, marketParams, assets, shares, onbehalf, receiver); - MorphoHarness.Id id = getMarketId(marketParams); + // Hints for the prover. assert withdrawnAssets * (getVirtualTotalSupplyShares(id) + withdrawnShares) <= withdrawnShares * (getVirtualTotalSupplyAssets(id) + withdrawnAssets); assert withdrawnAssets * getVirtualTotalSupplyShares(id) <= withdrawnShares * getVirtualTotalSupplyAssets(id); + uint256 suppliedAssets; + uint256 suppliedShares; suppliedAssets, suppliedShares = supply(e1, marketParams, withdrawnAssets, 0, onbehalf, data); assert suppliedAssets == withdrawnAssets && withdrawnShares >= suppliedShares; diff --git a/certora/specs/BlueExitLiquidity.spec b/certora/specs/ExitLiquidity.spec similarity index 91% rename from certora/specs/BlueExitLiquidity.spec rename to certora/specs/ExitLiquidity.spec index 4d925c11b..20568c161 100644 --- a/certora/specs/BlueExitLiquidity.spec +++ b/certora/specs/ExitLiquidity.spec @@ -15,6 +15,7 @@ methods { function getMarketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; } +// Check that it's not possible to withdraw more assets than what the user has supplied. rule withdrawLiquidity(MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) { env e; MorphoHarness.Id id = getMarketId(marketParams); @@ -32,6 +33,7 @@ rule withdrawLiquidity(MorphoHarness.MarketParams marketParams, uint256 assets, assert withdrawnAssets <= owedAssets; } +// Check that it's not possible to withdraw more collateral than what the user has supplied. rule withdrawCollateralLiquidity(MorphoHarness.MarketParams marketParams, uint256 assets, address onBehalf, address receiver) { env e; MorphoHarness.Id id = getMarketId(marketParams); @@ -43,6 +45,7 @@ rule withdrawCollateralLiquidity(MorphoHarness.MarketParams marketParams, uint25 assert assets <= initialCollateral; } +// Check than when repaying the full outstanding debt requires more assets than what was borrowed. rule repayLiquidity(MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { env e; MorphoHarness.Id id = getMarketId(marketParams); diff --git a/certora/specs/Health.spec b/certora/specs/Health.spec index 99403f8a6..54e00eb78 100644 --- a/certora/specs/Health.spec +++ b/certora/specs/Health.spec @@ -7,6 +7,7 @@ methods { function isHealthy(MorphoHarness.MarketParams, address user) external returns bool envfree; function isAuthorized(address, address user) external returns bool envfree; function getMarketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; + function _.price() external => mockPrice() expect uint256; function MathLib.mulDivDown(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivDown(a,b,c); function MathLib.mulDivUp(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivUp(a,b,c); @@ -39,6 +40,8 @@ function summaryMin(uint256 a, uint256 b) returns uint256 { return a < b ? a : b; } +// Check that without accruing interest, no interaction can put an healthy account into an unhealthy one. +// This rule times out for liquidate, repay and borrow. rule stayHealthy(env e, method f, calldataarg data) filtered { f -> !f.isView && @@ -51,47 +54,51 @@ filtered { MorphoHarness.Id id = getMarketId(marketParams); address user; + // Require that the position is healthy before the interaction. require isHealthy(marketParams, user); + // Require that the LLTV takes coherent values. require marketParams.lltv < 10^18; require marketParams.lltv > 0; + // Ensure that no interest is accumulated. require getLastUpdate(id) == e.block.timestamp; - priceChanged = false; + priceChanged = false; f(e, data); + // Safe require because of the invariant sumBorrowSharesCorrect. require getBorrowShares(id, user) <= getTotalBorrowShares(id); bool stillHealthy = isHealthy(marketParams, user); assert !priceChanged => stillHealthy; } -rule healthyUserCannotLoseCollateral(method f, calldataarg data) -filtered { - f -> !f.isView -} +// Check that users cannot lose collateral by unauthorized parties except in case of an unhealthy position. +rule healthyUserCannotLoseCollateral(env e, method f, calldataarg data) +filtered { f -> !f.isView } { MorphoHarness.MarketParams marketParams; - uint256 assets; - uint256 shares; - uint256 suppliedAssets; - uint256 suppliedShares; - address user; MorphoHarness.Id id = getMarketId(marketParams); - env e; + address user; + // Require that the e.msg.sender is not authorized. require !isAuthorized(user, e.msg.sender); require user != e.msg.sender; + // Ensure that no interest is accumulated. require getLastUpdate(id) == e.block.timestamp; + // Require that the user is healthy. require isHealthy(marketParams, user); + mathint collateralBefore = getCollateral(id, user); - priceChanged = false; + priceChanged = false; f(e, data); - require !priceChanged; mathint collateralAfter = getCollateral(id, user); - assert collateralAfter >= collateralBefore; + + assert !priceChanged => collateralAfter >= collateralBefore; } +// Check that users without collateral also have no debt. +// This invariant ensures that bad debt is always accounted. invariant alwaysCollateralized(MorphoHarness.Id id, address borrower) getBorrowShares(id, borrower) != 0 => getCollateral(id, borrower) != 0; diff --git a/certora/specs/BlueLibSummary.spec b/certora/specs/LibSummary.spec similarity index 76% rename from certora/specs/BlueLibSummary.spec rename to certora/specs/LibSummary.spec index 827304a37..0471e8a04 100644 --- a/certora/specs/BlueLibSummary.spec +++ b/certora/specs/LibSummary.spec @@ -6,17 +6,20 @@ methods { function marketLibId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; } -// Check the summaries required by BlueRatioMath.spec +// Check the summary of mulDivUp required by RatioMath.spec rule checkSummaryMulDivUp(uint256 x, uint256 y, uint256 d) { uint256 result = mathLibMulDivUp(x, y, d); assert result * d >= x * y; } +// Check the summary of mulDivDown required by RatioMath.spec rule checkSummaryMulDivDown(uint256 x, uint256 y, uint256 d) { uint256 result = mathLibMulDivDown(x, y, d); assert result * d <= x * y; } +// Check the munging of the MarketParams.id function. +// This rule cannot be checked because it is not possible disable the keccak256 summary for the moment. // rule checkSummaryId(MorphoHarness.MarketParams marketParams) { // assert marketLibId(marketParams) == getMarketId(marketParams); // } diff --git a/certora/specs/BlueLiveness.spec b/certora/specs/Liveness.spec similarity index 83% rename from certora/specs/BlueLiveness.spec rename to certora/specs/Liveness.spec index 09a224a3e..51a3792ce 100644 --- a/certora/specs/BlueLiveness.spec +++ b/certora/specs/Liveness.spec @@ -48,10 +48,13 @@ function summaryAccrueInterest(env e, MorphoInternalAccess.MarketParams marketPa definition isCreated(MorphoInternalAccess.Id id) returns bool = getLastUpdate(id) != 0; +// Check that tokens and shares are properly accounted following a supply. rule supplyMovesTokensAndIncreasesShares(env e, MorphoInternalAccess.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { MorphoInternalAccess.Id id = getMarketId(marketParams); + // Safe require that Morpho is not the sender. require e.msg.sender != currentContract; + // Ensure that no interest is accumulated. require getLastUpdate(id) == e.block.timestamp; mathint sharesBefore = getSupplyShares(id, onBehalf); @@ -70,6 +73,8 @@ rule supplyMovesTokensAndIncreasesShares(env e, MorphoInternalAccess.MarketParam assert balanceAfter == balanceBefore + suppliedAssets; } +// This rule is commented out for the moment because of a bug in CVL where market IDs are not consistent accross a run. +// Check that one can always repay the debt in full. // rule canRepayAll(env e, MorphoInternalAccess.MarketParams marketParams, uint256 shares, bytes data) { // MorphoInternalAccess.Id id = getMarketId(marketParams); @@ -89,17 +94,23 @@ rule supplyMovesTokensAndIncreasesShares(env e, MorphoInternalAccess.MarketParam // assert !lastReverted; // } +// Check the one can always withdraw all, under the condition that there are no outstanding debt on the market. rule canWithdrawAll(env e, MorphoInternalAccess.MarketParams marketParams, uint256 shares, address receiver) { MorphoInternalAccess.Id id = getMarketId(marketParams); + // Require to ensure a withdraw all. require shares == getSupplyShares(id, e.msg.sender); + // Omit sanity checks. require isCreated(id); require e.msg.sender != 0; require receiver != 0; require e.msg.value == 0; require shares > 0; + // Require no outstanding debt on the market. require getTotalBorrowAssets(id) == 0; + // Safe require because of the noTimeTravel rule. require getLastUpdate(id) <= e.block.timestamp; + // Safe require because of the sumSupplySharesCorrect invariant. require shares <= getTotalSupplyShares(id); withdraw@withrevert(e, marketParams, 0, shares, e.msg.sender, receiver); @@ -107,15 +118,21 @@ rule canWithdrawAll(env e, MorphoInternalAccess.MarketParams marketParams, uint2 assert !lastReverted; } +// Check that a user can always withdraw all, under the condition that this user does not have an outstanding debt. +// Combined with the canRepayAll rule, this ensures that a borrower can always fully exit a market. rule canWithdrawCollateralAll(env e, MorphoInternalAccess.MarketParams marketParams, uint256 assets, address receiver) { MorphoInternalAccess.Id id = getMarketId(marketParams); + // Ensure a withdrawCollateral all. require assets == getCollateral(id, e.msg.sender); + // Omit sanity checks. require isCreated(id); require receiver != 0; require e.msg.value == 0; require assets > 0; + // Safe require because of the noTimeTravel rule. require getLastUpdate(id) <= e.block.timestamp; + // Require that the user does not have an outstanding debt. require getBorrowShares(id, e.msg.sender) == 0; withdrawCollateral@withrevert(e, marketParams, assets, e.msg.sender, receiver); diff --git a/certora/specs/BlueRatioMath.spec b/certora/specs/RatioMath.spec similarity index 74% rename from certora/specs/BlueRatioMath.spec rename to certora/specs/RatioMath.spec index 8281d7208..b30628794 100644 --- a/certora/specs/BlueRatioMath.spec +++ b/certora/specs/RatioMath.spec @@ -21,7 +21,7 @@ invariant feeInRange(MorphoHarness.Id id) getFee(id) <= MAX_FEE(); // This is a simple overapproximative summary, stating that it rounds in the right direction. -// The summary is checked by the specification in BlueRatioMathSummary.spec. +// The summary is checked by the specification in LibSummary.spec. function summaryMulDivUp(uint256 x, uint256 y, uint256 d) returns uint256 { uint256 result; require result * d >= x * y; @@ -29,15 +29,15 @@ function summaryMulDivUp(uint256 x, uint256 y, uint256 d) returns uint256 { } // This is a simple overapproximative summary, stating that it rounds in the right direction. -// The summary is checked by the specification in BlueRatioMathSummary.spec. +// The summary is checked by the specification in LibSummary.spec. function summaryMulDivDown(uint256 x, uint256 y, uint256 d) returns uint256 { uint256 result; require result * d <= x * y; return result; } -rule accrueInterestsIncreasesSupplyRatio() { - MorphoHarness.MarketParams marketParams; +// Check that accrueInterest increases the value of supply shares. +rule accrueInterestIncreasesSupplyRatio(env e, MorphoHarness.MarketParams marketParams) { MorphoHarness.Id id; requireInvariant feeInRange(id); @@ -45,18 +45,17 @@ rule accrueInterestsIncreasesSupplyRatio() { mathint sharesBefore = getVirtualTotalSupplyShares(id); // The check is done for every market, not just for id. - env e; accrueInterest(e, marketParams); mathint assetsAfter = getVirtualTotalSupplyAssets(id); mathint sharesAfter = getVirtualTotalSupplyShares(id); - // Check if ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter + // Check that ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter. assert assetsBefore * sharesAfter <= assetsAfter * sharesBefore; } -rule accrueInterestsIncreasesBorrowRatio() { - MorphoHarness.MarketParams marketParams; +// Check that accrueInterest increases the value of borrow shares. +rule accrueInterestIncreasesBorrowRatio(env e, MorphoHarness.MarketParams marketParams) { MorphoHarness.Id id; requireInvariant feeInRange(id); @@ -64,17 +63,17 @@ rule accrueInterestsIncreasesBorrowRatio() { mathint sharesBefore = getVirtualTotalBorrowShares(id); // The check is done for every marketParams, not just for id. - env e; accrueInterest(e, marketParams); mathint assetsAfter = getVirtualTotalBorrowAssets(id); mathint sharesAfter = getVirtualTotalBorrowShares(id); - // Check if ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter + // Check that ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter. assert assetsBefore * sharesAfter <= assetsAfter * sharesBefore; } +// Check that excepti for liquidate, every function increases the value of supply shares. rule onlyLiquidateCanDecreaseSupplyRatio(env e, method f, calldataarg args) filtered { f -> !f.isView && f.selector != sig:liquidate(MorphoHarness.MarketParams, address, uint256, uint256, bytes).selector @@ -95,11 +94,14 @@ filtered { mathint assetsAfter = getVirtualTotalSupplyAssets(id); mathint sharesAfter = getVirtualTotalSupplyShares(id); - // Check if ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter + // Check that ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter assert assetsBefore * sharesAfter <= assetsAfter * sharesBefore; } -rule onlyAccrueInterestsCanIncreaseBorrowRatio(env e, method f, calldataarg args) +// Check that except when not accruing interest, every function is decreasing the value of borrow shares. +// The repay function is checked separately, see below. +// The liquidate function is not checked. +rule onlyAccrueInterestCanIncreaseBorrowRatio(env e, method f, calldataarg args) filtered { f -> !f.isView && f.selector != sig:repay(MorphoHarness.MarketParams, uint256, uint256, address, bytes).selector && @@ -109,39 +111,45 @@ filtered { MorphoHarness.Id id; requireInvariant feeInRange(id); + // In;erest would increase borrow ratio, so we need to assume that no time passes. + require getLastUpdate(id) == e.block.timestamp; + mathint assetsBefore = getVirtualTotalBorrowAssets(id); mathint sharesBefore = getVirtualTotalBorrowShares(id); - // Interest would increase borrow ratio, so we need to assume no time passes. - require getLastUpdate(id) == e.block.timestamp; - f(e,args); mathint assetsAfter = getVirtualTotalBorrowAssets(id); mathint sharesAfter = getVirtualTotalBorrowShares(id); - // Check if ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter + // Check that ratio decreases: assetsBefore/sharesBefore >= assetsAfter / sharesAfter assert assetsBefore * sharesAfter >= assetsAfter * sharesBefore; } +// Check that when not accruing interest, repay is decreasing the value of borrow shares. +// Check the case where the market is not repaid fully. +// The other case requires exact math (ie not summarizing mulDivUp and mulDivDown), so it is checked separately in ExactMath.spec rule repayIncreasesBorrowRatio(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onbehalf, bytes data) { MorphoHarness.Id id = getMarketId(marketParams); requireInvariant feeInRange(id); + // Interest would increase borrow ratio, so we need to assume that no time passes. + require getLastUpdate(id) == e.block.timestamp; + mathint assetsBefore = getVirtualTotalBorrowAssets(id); mathint sharesBefore = getVirtualTotalBorrowShares(id); - require getLastUpdate(id) == e.block.timestamp; - mathint repaidAssets; repaidAssets, _ = repay(e, marketParams, assets, shares, onbehalf, data); - require repaidAssets < assetsBefore; - mathint assetsAfter = getVirtualTotalBorrowAssets(id); mathint sharesAfter = getVirtualTotalBorrowShares(id); + // Check the case where the market is not repaid fully. + require repaidAssets < assetsBefore; + assert assetsAfter == assetsBefore - repaidAssets; + // Check that ratio decreases: assetsBefore/sharesBefore >= assetsAfter / sharesAfter assert assetsBefore * sharesAfter >= assetsAfter * sharesBefore; } diff --git a/certora/specs/Reentrancy.spec b/certora/specs/Reentrancy.spec index 3f7bfdf1f..f7307fb7f 100644 --- a/certora/specs/Reentrancy.spec +++ b/certora/specs/Reentrancy.spec @@ -44,6 +44,7 @@ hook STATICCALL(uint g, address addr, uint argsOffset, uint argsLength, uint ret static_call = true; } +// Check that no function is accessing storage, then making an external call other than to the IRM, and accessing storage again. rule reentrancySafe(method f, calldataarg data, env e) { require !callIsBorrowRate; require !hasAccessedStorage && !hasCallAfterAccessingStorage && !hasReentrancyUnsafeCall; diff --git a/certora/specs/BlueReverts.spec b/certora/specs/Reverts.spec similarity index 86% rename from certora/specs/BlueReverts.spec rename to certora/specs/Reverts.spec index f501fe82f..fd1d44a99 100644 --- a/certora/specs/BlueReverts.spec +++ b/certora/specs/Reverts.spec @@ -47,6 +47,7 @@ invariant notInitializedEmpty(MorphoHarness.Id id) } } +// Useful to ensure that authorized parties are not the zero address and so we can omit the sanity check in this case. invariant zeroDoesNotAuthorize(address authorized) !isAuthorized(0, authorized) { @@ -55,12 +56,14 @@ invariant zeroDoesNotAuthorize(address authorized) } } +// Check the revert condition for the setOwner function. rule setOwnerRevertCondition(env e, address newOwner) { address oldOwner = owner(); setOwner@withrevert(e, newOwner); assert lastReverted <=> e.msg.value != 0 || e.msg.sender != oldOwner || newOwner == oldOwner; } +// Check the revert condition for the setOwner function. rule enableIrmRevertCondition(env e, address irm) { address oldOwner = owner(); bool oldIsIrmEnabled = isIrmEnabled(irm); @@ -68,6 +71,7 @@ rule enableIrmRevertCondition(env e, address irm) { assert lastReverted <=> e.msg.value != 0 || e.msg.sender != oldOwner || oldIsIrmEnabled; } +// Check the revert condition for the enableLltv function. rule enableLltvRevertCondition(env e, uint256 lltv) { address oldOwner = owner(); bool oldIsLltvEnabled = isLltvEnabled(lltv); @@ -75,7 +79,8 @@ rule enableLltvRevertCondition(env e, uint256 lltv) { assert lastReverted <=> e.msg.value != 0 || e.msg.sender != oldOwner || lltv >= WAD() || oldIsLltvEnabled; } -// setFee can also revert if the accrueInterests reverts. +// Check that setFee reverts when its inputs are not validated. +// setFee can also revert if the accrueInterest reverts. rule setFeeInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 newFee) { MorphoHarness.Id id = getMarketId(marketParams); address oldOwner = owner(); @@ -85,6 +90,7 @@ rule setFeeInputValidation(env e, MorphoHarness.MarketParams marketParams, uint2 assert e.msg.value != 0 || e.msg.sender != oldOwner || !wasCreated || newFee > MAX_FEE() => hasReverted; } +// Check the revert condition for the setFeeRecipient function. rule setFeeRecipientRevertCondition(env e, address newFeeRecipient) { address oldOwner = owner(); address oldFeeRecipient = feeRecipient(); @@ -92,6 +98,7 @@ rule setFeeRecipientRevertCondition(env e, address newFeeRecipient) { assert lastReverted <=> e.msg.value != 0 || e.msg.sender != oldOwner || newFeeRecipient == oldFeeRecipient; } +// Check the revert condition for the createMarket function. rule createMarketRevertCondition(env e, MorphoHarness.MarketParams marketParams) { MorphoHarness.Id id = getMarketId(marketParams); bool irmEnabled = isIrmEnabled(marketParams.irm); @@ -101,11 +108,13 @@ rule createMarketRevertCondition(env e, MorphoHarness.MarketParams marketParams) assert lastReverted <=> e.msg.value != 0 || !irmEnabled || !lltvEnabled || lastUpdated != 0; } +// Check that supply reverts when its input are not validated. rule supplyInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { supply@withrevert(e, marketParams, assets, shares, onBehalf, data); assert !exactlyOneZero(assets, shares) || onBehalf == 0 => lastReverted; } +// Check that withdraw reverts when its inputs are not validated. rule withdrawInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) { require e.msg.sender != 0; requireInvariant zeroDoesNotAuthorize(e.msg.sender); @@ -113,6 +122,7 @@ rule withdrawInputValidation(env e, MorphoHarness.MarketParams marketParams, uin assert !exactlyOneZero(assets, shares) || onBehalf == 0 => lastReverted; } +// Check that borrow reverts when its inputs are not validated. rule borrowInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) { require e.msg.sender != 0; requireInvariant zeroDoesNotAuthorize(e.msg.sender); @@ -120,16 +130,19 @@ rule borrowInputValidation(env e, MorphoHarness.MarketParams marketParams, uint2 assert !exactlyOneZero(assets, shares) || onBehalf == 0 => lastReverted; } +// Check that repay reverts when its inputs are not validated. rule repayInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { repay@withrevert(e, marketParams, assets, shares, onBehalf, data); assert !exactlyOneZero(assets, shares) || onBehalf == 0 => lastReverted; } +// Check that supplyCollateral reverts when its inputs are not validated. rule supplyCollateralInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 assets, address onBehalf, bytes data) { supplyCollateral@withrevert(e, marketParams, assets, onBehalf, data); assert assets == 0 || onBehalf == 0 => lastReverted; } +// Check that withdrawCollateral reverts when its inputs are not validated. rule withdrawCollateralInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 assets, address onBehalf, address receiver) { require e.msg.sender != 0; requireInvariant zeroDoesNotAuthorize(e.msg.sender); @@ -137,6 +150,7 @@ rule withdrawCollateralInputValidation(env e, MorphoHarness.MarketParams marketP assert assets == 0 || onBehalf == 0 => lastReverted; } +// Check that liqudiate reverts when its inputs are not validated. rule liquidateInputValidation(env e, MorphoHarness.MarketParams marketParams, address borrower, uint256 seizedAssets, uint256 repaidShares, bytes data) { liquidate@withrevert(e, marketParams, borrower, seizedAssets, repaidShares, data); assert !exactlyOneZero(seizedAssets, repaidShares) => lastReverted; diff --git a/certora/specs/BlueTransfer.spec b/certora/specs/Transfer.spec similarity index 92% rename from certora/specs/BlueTransfer.spec rename to certora/specs/Transfer.spec index 79babb208..16fa13795 100644 --- a/certora/specs/BlueTransfer.spec +++ b/certora/specs/Transfer.spec @@ -26,30 +26,33 @@ function summarySafeTransferFrom(address token, address from, address to, uint25 } } -rule checkTransferFromSummary(address token, address from, uint256 amount) { +// Check the functional correctness of the summary of safeTransfer. +rule checkTransferSummary(address token, address to, uint256 amount) { mathint initialBalance = getBalance(token, currentContract); - require from != currentContract => initialBalance + getBalance(token, from) <= to_mathint(getTotalSupply(token)); + require to != currentContract => initialBalance + getBalance(token, to) <= to_mathint(getTotalSupply(token)); - doTransferFrom(token, from, currentContract, amount); + doTransfer(token, to, amount); mathint finalBalance = getBalance(token, currentContract); require myBalances[token] == initialBalance; - summarySafeTransferFrom(token, from, currentContract, amount); + summarySafeTransferFrom(token, currentContract, to, amount); assert myBalances[token] == finalBalance; } -rule checkTransferSummary(address token, address to, uint256 amount) { +// Check the functional correctness of the summary of safeTransferFrom. +rule checkTransferFromSummary(address token, address from, uint256 amount) { mathint initialBalance = getBalance(token, currentContract); - require to != currentContract => initialBalance + getBalance(token, to) <= to_mathint(getTotalSupply(token)); + require from != currentContract => initialBalance + getBalance(token, from) <= to_mathint(getTotalSupply(token)); - doTransfer(token, to, amount); + doTransferFrom(token, from, currentContract, amount); mathint finalBalance = getBalance(token, currentContract); require myBalances[token] == initialBalance; - summarySafeTransferFrom(token, currentContract, to, amount); + summarySafeTransferFrom(token, from, currentContract, amount); assert myBalances[token] == finalBalance; } +// Check the revert condition of the summary of safeTransfer. rule transferRevertCondition(address token, address to, uint256 amount) { uint256 initialBalance = getBalance(token, currentContract); uint256 toInitialBalance = getBalance(token, to); @@ -61,6 +64,7 @@ rule transferRevertCondition(address token, address to, uint256 amount) { assert lastReverted == (initialBalance < amount); } +// Check the revert condition of the summary of safeTransferFrom. rule transferFromRevertCondition(address token, address from, address to, uint256 amount) { uint256 initialBalance = getBalance(token, from); uint256 toInitialBalance = getBalance(token, to); From 2f489aa96375e3360d994827964eb4694225dba5 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 1 Sep 2023 00:06:27 +0200 Subject: [PATCH 097/204] chore: update scripts run by the CI --- .github/workflows/certora.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml index 72a3df433..3f810daf0 100644 --- a/.github/workflows/certora.yml +++ b/.github/workflows/certora.yml @@ -43,14 +43,14 @@ jobs: matrix: script: - - verifyBlue.sh - - verifyBlueAccrueInterests.sh - - verifyBlueExitLiquidity.sh - - verifyBlueLibSummary.sh - - verifyBlueLiveness.sh - - verifyBlueRatioMath.sh - - verifyBlueReverts.sh - - verifyBlueTransfer.sh - - verifyDifficultMath.sh + - verifyAccrueInterests.sh + - verifyConsistentState.sh + - verifyExactMath.sh + - verifyExitLiquidity.sh - verifyHealth.sh + - verifyLibSummary.sh + - verifyLiveness.sh + - verifyRatioMath.sh - verifyReentrancy.sh + - verifyReverts.sh + - verifyTransfer.sh From d7476a260b9814eb0cb45326b8c1a316bc4074fd Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 1 Sep 2023 00:08:32 +0200 Subject: [PATCH 098/204] fix: typos --- .github/workflows/certora.yml | 2 +- certora/specs/ConsistentState.spec | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml index 3f810daf0..976f2058b 100644 --- a/.github/workflows/certora.yml +++ b/.github/workflows/certora.yml @@ -43,7 +43,7 @@ jobs: matrix: script: - - verifyAccrueInterests.sh + - verifyAccrueInterest.sh - verifyConsistentState.sh - verifyExactMath.sh - verifyExitLiquidity.sh diff --git a/certora/specs/ConsistentState.spec b/certora/specs/ConsistentState.spec index 1982eb09b..dcdb958c1 100644 --- a/certora/specs/ConsistentState.spec +++ b/certora/specs/ConsistentState.spec @@ -234,6 +234,7 @@ rule userCannotLoseCollateralExceptLiquidate(env e, method f, calldataarg args) filtered { f -> !f.isView && f.selector != sig:liquidate(MorphoHarness.MarketParams, address, uint256, uint256, bytes).selector +} { MorphoHarness.Id id; address user; From 6b7ea5811952e2447759ac61129ae37b71e16ae9 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 1 Sep 2023 00:30:35 +0200 Subject: [PATCH 099/204] style: rename harness functions, add license --- README.md | 1 + certora/harness/LICENSE | 389 +++++++++++++++++++++++ certora/harness/MorphoHarness.sol | 43 ++- certora/harness/MorphoInternalAccess.sol | 1 + certora/harness/TransferHarness.sol | 14 +- certora/specs/AccrueInterest.spec | 4 +- certora/specs/ConsistentState.spec | 66 ++-- certora/specs/ExactMath.spec | 42 +-- certora/specs/ExitLiquidity.spec | 44 +-- certora/specs/Health.spec | 26 +- certora/specs/LibSummary.spec | 4 +- certora/specs/Liveness.spec | 64 ++-- certora/specs/RatioMath.spec | 66 ++-- certora/specs/Reverts.spec | 36 +-- certora/specs/Transfer.spec | 44 +-- 15 files changed, 614 insertions(+), 230 deletions(-) create mode 100644 certora/harness/LICENSE diff --git a/README.md b/README.md index 01737eec6..c850a356f 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,4 @@ All files in the following folders can also be licensed under `GPL-2.0-or-later` - `src/libraries`, see [`src/libraries/LICENSE`](./src/libaries/LICENSE) - `src/mocks`, see [`src/mocks/LICENSE`](./src/mocks/LICENSE) - `test`, see [`test/LICENSE`](./test/LICENSE) +- `certora/harness`, see [`certora/harness/LICENSE`](./certora/harness/LICENSE) diff --git a/certora/harness/LICENSE b/certora/harness/LICENSE new file mode 100644 index 000000000..aec4e2aca --- /dev/null +++ b/certora/harness/LICENSE @@ -0,0 +1,389 @@ +This software is available under your choice of the GNU General Public +License, version 2 or later, or the Business Source License, as set +forth below. + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + +Business Source License 1.1 + +License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved. +"Business Source License" is a trademark of MariaDB Corporation Ab. + +----------------------------------------------------------------------------- + +Parameters + +Licensor: Morpho Association + +Licensed Work: Morpho Blue Core + The Licensed Work is (c) 2023 Morpho Association + +Additional Use Grant: Any uses listed and defined at + morpho-blue-core-license-grants.morpho.eth + +Change Date: The earlier of (i) 2026-01-01, or (ii) a date specified + at morpho-blue-core-license-date.morpho.eth, or (iii) + upon the activation of the setFee function of the + Licensed Work’s applicable protocol smart contracts + deployed for production use. + +Change License: GNU General Public License v2.0 or later + +----------------------------------------------------------------------------- + +Terms + +The Licensor hereby grants you the right to copy, modify, create derivative +works, redistribute, and make non-production use of the Licensed Work. The +Licensor may make an Additional Use Grant, above, permitting limited +production use. + +Effective on the Change Date, or the fourth anniversary of the first publicly +available distribution of a specific version of the Licensed Work under this +License, whichever comes first, the Licensor hereby grants you rights under +the terms of the Change License, and the rights granted in the paragraph +above terminate. + +If your use of the Licensed Work does not comply with the requirements +currently in effect as described in this License, you must purchase a +commercial license from the Licensor, its affiliated entities, or authorized +resellers, or you must refrain from using the Licensed Work. + +All copies of the original and modified Licensed Work, and derivative works +of the Licensed Work, are subject to this License. This License applies +separately for each version of the Licensed Work and the Change Date may vary +for each version of the Licensed Work released by Licensor. + +You must conspicuously display this License on each original or modified copy +of the Licensed Work. If you receive the Licensed Work in original or +modified form from a third party, the terms and conditions set forth in this +License apply to your use of that work. + +Any use of the Licensed Work in violation of this License will automatically +terminate your rights under this License for the current and all other +versions of the Licensed Work. + +This License does not grant you any right in any trademark or logo of +Licensor or its affiliates (provided that you may use a trademark or logo of +Licensor as expressly required by this License). + +TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON +AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, +EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND +TITLE. + +MariaDB hereby grants you permission to use this License’s text to license +your works, and to refer to it using the trademark "Business Source License", +as long as you comply with the Covenants of Licensor below. + +----------------------------------------------------------------------------- + +Covenants of Licensor + +In consideration of the right to use this License’s text and the "Business +Source License" name and trademark, Licensor covenants to MariaDB, and to all +other recipients of the licensed work to be provided by Licensor: + +1. To specify as the Change License the GPL Version 2.0 or any later version, + or a license that is compatible with GPL Version 2.0 or a later version, + where "compatible" means that software provided under the Change License can + be included in a program with software provided under GPL Version 2.0 or a + later version. Licensor may specify additional Change Licenses without + limitation. + +2. To either: (a) specify an additional grant of rights to use that does not + impose any additional restriction on the right granted in this License, as + the Additional Use Grant; or (b) insert the text "None". + +3. To specify a Change Date. + +4. Not to modify this License in any other way. + +----------------------------------------------------------------------------- + +Notice + +The Business Source License (this document, or the "License") is not an Open +Source license. However, the Licensed Work will eventually be made available +under an Open Source License, as stated in this License. diff --git a/certora/harness/MorphoHarness.sol b/certora/harness/MorphoHarness.sol index c22d1f289..d94feb92d 100644 --- a/certora/harness/MorphoHarness.sol +++ b/certora/harness/MorphoHarness.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity 0.8.19; import "../munged/Morpho.sol"; @@ -9,75 +10,67 @@ contract MorphoHarness is Morpho { constructor(address newOwner) Morpho(newOwner) {} - function WAD() external pure returns (uint256) { + function wad() external pure returns (uint256) { return WAD; } - function VIRTUAL_SHARES() external pure returns (uint256) { - return SharesMathLib.VIRTUAL_SHARES; + function maxFee() external pure returns (uint256) { + return maxFee(); } - function VIRTUAL_ASSETS() external pure returns (uint256) { - return SharesMathLib.VIRTUAL_ASSETS; - } - - function MAX_FEE() external pure returns (uint256) { - return MAX_FEE; - } - - function getTotalSupplyAssets(Id id) external view returns (uint256) { + function totalSupplyAssets(Id id) external view returns (uint256) { return market[id].totalSupplyAssets; } - function getTotalSupplyShares(Id id) external view returns (uint256) { + function totalSupplyShares(Id id) external view returns (uint256) { return market[id].totalSupplyShares; } - function getTotalBorrowAssets(Id id) external view returns (uint256) { + function totalBorrowAssets(Id id) external view returns (uint256) { return market[id].totalBorrowAssets; } - function getTotalBorrowShares(Id id) external view returns (uint256) { + function totalBorrowShares(Id id) external view returns (uint256) { return market[id].totalBorrowShares; } - function getSupplyShares(Id id, address account) external view returns (uint256) { + function supplyShares(Id id, address account) external view returns (uint256) { return position[id][account].supplyShares; } - function getBorrowShares(Id id, address account) external view returns (uint256) { + function borrowShares(Id id, address account) external view returns (uint256) { return position[id][account].borrowShares; } - function getCollateral(Id id, address account) external view returns (uint256) { + function collateral(Id id, address account) external view returns (uint256) { return position[id][account].collateral; } - function getLastUpdate(Id id) external view returns (uint256) { + function lastUpdate(Id id) external view returns (uint256) { return market[id].lastUpdate; } - function getFee(Id id) external view returns (uint256) { + function fee(Id id) external view returns (uint256) { return market[id].fee; } - function getVirtualTotalSupplyAssets(Id id) external view returns (uint256) { + function virtualTotalSupplyAssets(Id id) external view returns (uint256) { return market[id].totalSupplyAssets + SharesMathLib.VIRTUAL_ASSETS; } - function getVirtualTotalSupplyShares(Id id) external view returns (uint256) { + function virtualTotalSupplyShares(Id id) external view returns (uint256) { return market[id].totalSupplyShares + SharesMathLib.VIRTUAL_SHARES; } - function getVirtualTotalBorrowAssets(Id id) external view returns (uint256) { + function virtualTotalBorrowAssets(Id id) external view returns (uint256) { return market[id].totalBorrowAssets + SharesMathLib.VIRTUAL_ASSETS; } - function getVirtualTotalBorrowShares(Id id) external view returns (uint256) { + function virtualTotalBorrowShares(Id id) external view returns (uint256) { return market[id].totalBorrowShares + SharesMathLib.VIRTUAL_SHARES; } - function getMarketId(MarketParams memory marketParams) external pure returns (Id) { + function marketId(MarketParams memory marketParams) external pure returns (Id) { return marketParams.id(); } diff --git a/certora/harness/MorphoInternalAccess.sol b/certora/harness/MorphoInternalAccess.sol index cf46bf451..a4f7ecfa1 100644 --- a/certora/harness/MorphoInternalAccess.sol +++ b/certora/harness/MorphoInternalAccess.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity 0.8.19; import "./MorphoHarness.sol"; diff --git a/certora/harness/TransferHarness.sol b/certora/harness/TransferHarness.sol index 1129191ef..011027e79 100644 --- a/certora/harness/TransferHarness.sol +++ b/certora/harness/TransferHarness.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.0; +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.12; import "../../src/libraries/SafeTransferLib.sol"; import "../../src/interfaces/IERC20.sol"; @@ -13,23 +13,23 @@ interface IERC20Extended is IERC20 { contract TransferHarness { using SafeTransferLib for IERC20; - function doTransferFrom(address token, address from, address to, uint256 value) public { + function safeTransferFrom(address token, address from, address to, uint256 value) public { IERC20(token).safeTransferFrom(from, to, value); } - function doTransfer(address token, address to, uint256 value) public { + function safeTransfer(address token, address to, uint256 value) public { IERC20(token).safeTransfer(to, value); } - function getBalance(address token, address user) public view returns (uint256) { + function balanceOf(address token, address user) public view returns (uint256) { return IERC20Extended(token).balanceOf(user); } - function getAllowance(address token, address owner, address spender) public view returns (uint256) { + function allowance(address token, address owner, address spender) public view returns (uint256) { return IERC20Extended(token).allowance(owner, spender); } - function getTotalSupply(address token) public view returns (uint256) { + function totalSupply(address token) public view returns (uint256) { return IERC20Extended(token).totalSupply(); } } diff --git a/certora/specs/AccrueInterest.spec b/certora/specs/AccrueInterest.spec index 24990229a..9470448d1 100644 --- a/certora/specs/AccrueInterest.spec +++ b/certora/specs/AccrueInterest.spec @@ -16,9 +16,7 @@ methods { function _.onMorphoSupplyCollateral(uint256, bytes) external => NONDET; function _.onMorphoFlashLoan(uint256, bytes) external => NONDET; - function VIRTUAL_ASSETS() external returns uint256 envfree; - function VIRTUAL_SHARES() external returns uint256 envfree; - function MAX_FEE() external returns uint256 envfree; + function maxFee() external returns uint256 envfree; } ghost ghostMulDivUp(uint256, uint256, uint256) returns uint256; diff --git a/certora/specs/ConsistentState.spec b/certora/specs/ConsistentState.spec index dcdb958c1..2a393f910 100644 --- a/certora/specs/ConsistentState.spec +++ b/certora/specs/ConsistentState.spec @@ -1,15 +1,15 @@ methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); - function getTotalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; - function getTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; - function getTotalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree; - function getTotalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; - function getSupplyShares(MorphoHarness.Id, address) external returns uint256 envfree; - function getBorrowShares(MorphoHarness.Id, address) external returns uint256 envfree; - function getCollateral(MorphoHarness.Id, address) external returns uint256 envfree; - function getFee(MorphoHarness.Id) external returns uint256 envfree; - function getLastUpdate(MorphoHarness.Id) external returns uint256 envfree; - function getMarketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; + function totalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; + function totalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; + function totalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree; + function totalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; + function supplyShares(MorphoHarness.Id, address) external returns uint256 envfree; + function borrowShares(MorphoHarness.Id, address) external returns uint256 envfree; + function collateral(MorphoHarness.Id, address) external returns uint256 envfree; + function fee(MorphoHarness.Id) external returns uint256 envfree; + function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; + function marketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; function isAuthorized(address, address) external returns bool envfree; function isLltvEnabled(uint256) external returns bool envfree; @@ -17,7 +17,7 @@ methods { function _.borrowRate(MorphoHarness.MarketParams, MorphoHarness.Market) external => HAVOC_ECF; - function MAX_FEE() external returns uint256 envfree; + function maxFee() external returns uint256 envfree; function SafeTransferLib.safeTransfer(address token, address to, uint256 value) internal => summarySafeTransferFrom(token, currentContract, to, value); function SafeTransferLib.safeTransferFrom(address token, address from, address to, uint256 value) internal => summarySafeTransferFrom(token, from, to, value); @@ -86,30 +86,30 @@ function summarySafeTransferFrom(address token, address from, address to, uint25 } definition isCreated(MorphoHarness.Id id) returns bool = - getLastUpdate(id) != 0; + lastUpdate(id) != 0; // Check that the fee is always lower than the max fee constant. invariant feeInRange(MorphoHarness.Id id) - getFee(id) <= MAX_FEE(); + fee(id) <= maxFee(); // Check that the accounting of totalSupplyShares is correct. invariant sumSupplySharesCorrect(MorphoHarness.Id id) - to_mathint(getTotalSupplyShares(id)) == sumSupplyShares[id]; + to_mathint(totalSupplyShares(id)) == sumSupplyShares[id]; // Check that the accounting of totalBorrowShares is correct. invariant sumBorrowSharesCorrect(MorphoHarness.Id id) - to_mathint(getTotalBorrowShares(id)) == sumBorrowShares[id]; + to_mathint(totalBorrowShares(id)) == sumBorrowShares[id]; // Check that a market only allows borrows up to the total supply. // This invariant shows that markets are independent, tokens from one market cannot be taken by interacting with another market. invariant borrowLessSupply(MorphoHarness.Id id) - getTotalBorrowAssets(id) <= getTotalSupplyAssets(id); + totalBorrowAssets(id) <= totalSupplyAssets(id); // This invariant is useful in the following rule, to link an id back to a market. invariant marketInvariant(MorphoHarness.MarketParams marketParams) - isCreated(getMarketId(marketParams)) => - idToBorrowable[getMarketId(marketParams)] == marketParams.borrowableToken && - idToCollateral[getMarketId(marketParams)] == marketParams.collateralToken; + isCreated(marketId(marketParams)) => + idToBorrowable[marketId(marketParams)] == marketParams.borrowableToken && + idToCollateral[marketId(marketParams)] == marketParams.collateralToken; // Check that the idle amount on the singleton is greater to the sum amount, that is the sum over all the markets of the total supply plus the total collateral minus the total borrow. invariant isLiquid(address token) @@ -147,11 +147,11 @@ invariant isLiquid(address token) // Check that a market can only exist if its LLTV is enabled. invariant onlyEnabledLltv(MorphoHarness.MarketParams marketParams) - isCreated(getMarketId(marketParams)) => isLltvEnabled(marketParams.lltv); + isCreated(marketId(marketParams)) => isLltvEnabled(marketParams.lltv); // Check that a market can only exist if its IRM is enabled. invariant onlyEnabledIrm(MorphoHarness.MarketParams marketParams) - isCreated(getMarketId(marketParams)) => isIrmEnabled(marketParams.irm); + isCreated(marketId(marketParams)) => isIrmEnabled(marketParams.irm); // Check the pseudo-injectivity of the hashing function id(). rule marketIdUnique() { @@ -159,7 +159,7 @@ rule marketIdUnique() { MorphoHarness.MarketParams marketParams2; // Require the same arguments. - require getMarketId(marketParams1) == getMarketId(marketParams2); + require marketId(marketParams1) == marketId(marketParams2); assert marketParams1.borrowableToken == marketParams2.borrowableToken; assert marketParams1.collateralToken == marketParams2.collateralToken; @@ -200,11 +200,11 @@ filtered { f -> !f.isView } require !isAuthorized(user, e.msg.sender); require user != e.msg.sender; - mathint sharesBefore = getSupplyShares(id, user); + mathint sharesBefore = supplyShares(id, user); f(e, data); - mathint sharesAfter = getSupplyShares(id, user); + mathint sharesAfter = supplyShares(id, user); assert sharesAfter >= sharesBefore; } @@ -220,11 +220,11 @@ filtered { f -> !f.isView } require !isAuthorized(user, e.msg.sender); require user != e.msg.sender; - mathint sharesBefore = getBorrowShares(id, user); + mathint sharesBefore = borrowShares(id, user); f(e, args); - mathint sharesAfter = getBorrowShares(id, user); + mathint sharesAfter = borrowShares(id, user); assert sharesAfter <= sharesBefore; } @@ -243,9 +243,11 @@ filtered { require !isAuthorized(user, e.msg.sender); require user != e.msg.sender; + mathint collateralBefore = collateral(id, user); + f(e, args); - mathint collateralAfter = getCollateral(id, user); + mathint collateralAfter = collateral(id, user); assert collateralAfter >= collateralBefore; } @@ -261,13 +263,13 @@ filtered { f -> !f.isView } require !isAuthorized(user, e.msg.sender); require user != e.msg.sender; // Require that the user has no outstanding debt. - require getBorrowShares(id, user) == 0; + require borrowShares(id, user) == 0; - mathint collateralBefore = getCollateral(id, user); + mathint collateralBefore = collateral(id, user); f(e, args); - mathint collateralAfter = getCollateral(id, user); + mathint collateralAfter = collateral(id, user); assert collateralAfter >= collateralBefore; } @@ -277,7 +279,7 @@ rule noTimeTravel(method f, env e, calldataarg args) filtered { f -> !f.isView } { MorphoHarness.Id id; - require getLastUpdate(id) <= e.block.timestamp; + require lastUpdate(id) <= e.block.timestamp; f(e, args); - assert getLastUpdate(id) <= e.block.timestamp; + assert lastUpdate(id) <= e.block.timestamp; } diff --git a/certora/specs/ExactMath.spec b/certora/specs/ExactMath.spec index ddf83dc0a..e536fc02e 100644 --- a/certora/specs/ExactMath.spec +++ b/certora/specs/ExactMath.spec @@ -1,12 +1,12 @@ methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); - function getMarketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; - function getVirtualTotalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; - function getVirtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; - function getVirtualTotalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree; - function getVirtualTotalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; - function getFee(MorphoHarness.Id) external returns uint256 envfree; - function getLastUpdate(MorphoHarness.Id) external returns uint256 envfree; + function marketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; + function virtualTotalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; + function virtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; + function virtualTotalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree; + function virtualTotalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; + function fee(MorphoHarness.Id) external returns uint256 envfree; + function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; function MathLib.mulDivDown(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivDown(a,b,c); function MathLib.mulDivUp(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivUp(a,b,c); @@ -14,7 +14,7 @@ methods { function SafeTransferLib.safeTransferFrom(address token, address from, address to, uint256 value) internal => NONDET; function _.onMorphoSupply(uint256 assets, bytes data) external => HAVOC_ECF; - function MAX_FEE() external returns uint256 envfree; + function maxFee() external returns uint256 envfree; } function summaryMulDivUp(uint256 x, uint256 y, uint256 d) returns uint256 { @@ -26,21 +26,21 @@ function summaryMulDivDown(uint256 x, uint256 y, uint256 d) returns uint256 { } rule repayAllResetsBorrowRatio(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onbehalf, bytes data) { - MorphoHarness.Id id = getMarketId(marketParams); - require getFee(id) <= MAX_FEE(); + MorphoHarness.Id id = marketId(marketParams); + require fee(id) <= maxFee(); - mathint assetsBefore = getVirtualTotalBorrowAssets(id); - mathint sharesBefore = getVirtualTotalBorrowShares(id); + mathint assetsBefore = virtualTotalBorrowAssets(id); + mathint sharesBefore = virtualTotalBorrowShares(id); - require getLastUpdate(id) == e.block.timestamp; + require lastUpdate(id) == e.block.timestamp; mathint repaidAssets; repaidAssets, _ = repay(e, marketParams, assets, shares, onbehalf, data); require repaidAssets >= assetsBefore; - mathint assetsAfter = getVirtualTotalBorrowAssets(id); - mathint sharesAfter = getVirtualTotalBorrowShares(id); + mathint assetsAfter = virtualTotalBorrowAssets(id); + mathint sharesAfter = virtualTotalBorrowShares(id); assert assetsAfter == 1; } @@ -48,7 +48,7 @@ rule repayAllResetsBorrowRatio(env e, MorphoHarness.MarketParams marketParams, u // There should be no profit from supply followed immediately by withdraw. rule supplyWithdraw() { MorphoHarness.MarketParams marketParams; - MorphoHarness.Id id = getMarketId(marketParams); + MorphoHarness.Id id = marketId(marketParams); uint256 assets; uint256 shares; address onbehalf; @@ -67,8 +67,8 @@ rule supplyWithdraw() { suppliedAssets, suppliedShares = supply(e1, marketParams, assets, shares, onbehalf, data); // Hints for the prover. - assert suppliedAssets * (getVirtualTotalSupplyShares(id) - suppliedShares) >= suppliedShares * (getVirtualTotalSupplyAssets(id) - suppliedAssets); - assert suppliedAssets * getVirtualTotalSupplyShares(id) >= suppliedShares * getVirtualTotalSupplyAssets(id); + assert suppliedAssets * (virtualTotalSupplyShares(id) - suppliedShares) >= suppliedShares * (virtualTotalSupplyAssets(id) - suppliedAssets); + assert suppliedAssets * virtualTotalSupplyShares(id) >= suppliedShares * virtualTotalSupplyAssets(id); uint256 withdrawnAssets; uint256 withdrawnShares; @@ -81,7 +81,7 @@ rule supplyWithdraw() { // There should be no profit from withdraw followed immediately by supply. rule withdrawSupply() { MorphoHarness.MarketParams marketParams; - MorphoHarness.Id id = getMarketId(marketParams); + MorphoHarness.Id id = marketId(marketParams); uint256 assets; uint256 shares; address onbehalf; @@ -100,8 +100,8 @@ rule withdrawSupply() { withdrawnAssets, withdrawnShares = withdraw(e2, marketParams, assets, shares, onbehalf, receiver); // Hints for the prover. - assert withdrawnAssets * (getVirtualTotalSupplyShares(id) + withdrawnShares) <= withdrawnShares * (getVirtualTotalSupplyAssets(id) + withdrawnAssets); - assert withdrawnAssets * getVirtualTotalSupplyShares(id) <= withdrawnShares * getVirtualTotalSupplyAssets(id); + assert withdrawnAssets * (virtualTotalSupplyShares(id) + withdrawnShares) <= withdrawnShares * (virtualTotalSupplyAssets(id) + withdrawnAssets); + assert withdrawnAssets * virtualTotalSupplyShares(id) <= withdrawnShares * virtualTotalSupplyAssets(id); uint256 suppliedAssets; uint256 suppliedShares; diff --git a/certora/specs/ExitLiquidity.spec b/certora/specs/ExitLiquidity.spec index 20568c161..2a5636f6e 100644 --- a/certora/specs/ExitLiquidity.spec +++ b/certora/specs/ExitLiquidity.spec @@ -1,30 +1,30 @@ methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); - function getSupplyShares(MorphoHarness.Id, address) external returns uint256 envfree; - function getBorrowShares(MorphoHarness.Id, address) external returns uint256 envfree; - function getCollateral(MorphoHarness.Id, address) external returns uint256 envfree; - function getVirtualTotalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; - function getVirtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; - function getVirtualTotalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree; - function getVirtualTotalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; + function supplyShares(MorphoHarness.Id, address) external returns uint256 envfree; + function borrowShares(MorphoHarness.Id, address) external returns uint256 envfree; + function collateral(MorphoHarness.Id, address) external returns uint256 envfree; + function virtualTotalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; + function virtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; + function virtualTotalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree; + function virtualTotalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; - function getLastUpdate(MorphoHarness.Id) external returns uint256 envfree; + function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; function mathLibMulDivDown(uint256, uint256, uint256) external returns uint256 envfree; function mathLibMulDivUp(uint256, uint256, uint256) external returns uint256 envfree; - function getMarketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; + function marketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; } // Check that it's not possible to withdraw more assets than what the user has supplied. rule withdrawLiquidity(MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) { env e; - MorphoHarness.Id id = getMarketId(marketParams); + MorphoHarness.Id id = marketId(marketParams); - require getLastUpdate(id) == e.block.timestamp; + require lastUpdate(id) == e.block.timestamp; - uint256 initialShares = getSupplyShares(id, onBehalf); - uint256 initialTotalSupply = getVirtualTotalSupplyAssets(id); - uint256 initialTotalSupplyShares = getVirtualTotalSupplyShares(id); + uint256 initialShares = supplyShares(id, onBehalf); + uint256 initialTotalSupply = virtualTotalSupplyAssets(id); + uint256 initialTotalSupplyShares = virtualTotalSupplyShares(id); uint256 owedAssets = mathLibMulDivDown(initialShares, initialTotalSupply, initialTotalSupplyShares); uint256 withdrawnAssets; @@ -36,9 +36,9 @@ rule withdrawLiquidity(MorphoHarness.MarketParams marketParams, uint256 assets, // Check that it's not possible to withdraw more collateral than what the user has supplied. rule withdrawCollateralLiquidity(MorphoHarness.MarketParams marketParams, uint256 assets, address onBehalf, address receiver) { env e; - MorphoHarness.Id id = getMarketId(marketParams); + MorphoHarness.Id id = marketId(marketParams); - uint256 initialCollateral = getCollateral(id, onBehalf); + uint256 initialCollateral = collateral(id, onBehalf); withdrawCollateral(e, marketParams, assets, onBehalf, receiver); @@ -48,19 +48,19 @@ rule withdrawCollateralLiquidity(MorphoHarness.MarketParams marketParams, uint25 // Check than when repaying the full outstanding debt requires more assets than what was borrowed. rule repayLiquidity(MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { env e; - MorphoHarness.Id id = getMarketId(marketParams); + MorphoHarness.Id id = marketId(marketParams); - require getLastUpdate(id) == e.block.timestamp; + require lastUpdate(id) == e.block.timestamp; - uint256 initialShares = getBorrowShares(id, onBehalf); - uint256 initialTotalBorrow = getVirtualTotalBorrowAssets(id); - uint256 initialTotalBorrowShares = getVirtualTotalBorrowShares(id); + uint256 initialShares = borrowShares(id, onBehalf); + uint256 initialTotalBorrow = virtualTotalBorrowAssets(id); + uint256 initialTotalBorrowShares = virtualTotalBorrowShares(id); uint256 assetsDue = mathLibMulDivUp(initialShares, initialTotalBorrow, initialTotalBorrowShares); uint256 repaidAssets; repaidAssets, _ = repay(e, marketParams, assets, shares, onBehalf, data); - require getBorrowShares(id, onBehalf) == 0; + require borrowShares(id, onBehalf) == 0; assert repaidAssets >= assetsDue; } diff --git a/certora/specs/Health.spec b/certora/specs/Health.spec index 54e00eb78..3204fbb62 100644 --- a/certora/specs/Health.spec +++ b/certora/specs/Health.spec @@ -1,12 +1,12 @@ methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); - function getLastUpdate(MorphoHarness.Id) external returns uint256 envfree; - function getTotalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; - function getBorrowShares(MorphoHarness.Id, address) external returns uint256 envfree; - function getCollateral(MorphoHarness.Id, address) external returns uint256 envfree; + function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; + function totalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; + function borrowShares(MorphoHarness.Id, address) external returns uint256 envfree; + function collateral(MorphoHarness.Id, address) external returns uint256 envfree; function isHealthy(MorphoHarness.MarketParams, address user) external returns bool envfree; function isAuthorized(address, address user) external returns bool envfree; - function getMarketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; + function marketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; function _.price() external => mockPrice() expect uint256; function MathLib.mulDivDown(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivDown(a,b,c); @@ -51,7 +51,7 @@ filtered { } { MorphoHarness.MarketParams marketParams; - MorphoHarness.Id id = getMarketId(marketParams); + MorphoHarness.Id id = marketId(marketParams); address user; // Require that the position is healthy before the interaction. @@ -60,13 +60,13 @@ filtered { require marketParams.lltv < 10^18; require marketParams.lltv > 0; // Ensure that no interest is accumulated. - require getLastUpdate(id) == e.block.timestamp; + require lastUpdate(id) == e.block.timestamp; priceChanged = false; f(e, data); // Safe require because of the invariant sumBorrowSharesCorrect. - require getBorrowShares(id, user) <= getTotalBorrowShares(id); + require borrowShares(id, user) <= totalBorrowShares(id); bool stillHealthy = isHealthy(marketParams, user); assert !priceChanged => stillHealthy; @@ -77,23 +77,23 @@ rule healthyUserCannotLoseCollateral(env e, method f, calldataarg data) filtered { f -> !f.isView } { MorphoHarness.MarketParams marketParams; - MorphoHarness.Id id = getMarketId(marketParams); + MorphoHarness.Id id = marketId(marketParams); address user; // Require that the e.msg.sender is not authorized. require !isAuthorized(user, e.msg.sender); require user != e.msg.sender; // Ensure that no interest is accumulated. - require getLastUpdate(id) == e.block.timestamp; + require lastUpdate(id) == e.block.timestamp; // Require that the user is healthy. require isHealthy(marketParams, user); - mathint collateralBefore = getCollateral(id, user); + mathint collateralBefore = collateral(id, user); priceChanged = false; f(e, data); - mathint collateralAfter = getCollateral(id, user); + mathint collateralAfter = collateral(id, user); assert !priceChanged => collateralAfter >= collateralBefore; } @@ -101,4 +101,4 @@ filtered { f -> !f.isView } // Check that users without collateral also have no debt. // This invariant ensures that bad debt is always accounted. invariant alwaysCollateralized(MorphoHarness.Id id, address borrower) - getBorrowShares(id, borrower) != 0 => getCollateral(id, borrower) != 0; + borrowShares(id, borrower) != 0 => collateral(id, borrower) != 0; diff --git a/certora/specs/LibSummary.spec b/certora/specs/LibSummary.spec index 0471e8a04..100a17159 100644 --- a/certora/specs/LibSummary.spec +++ b/certora/specs/LibSummary.spec @@ -2,7 +2,7 @@ methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); function mathLibMulDivUp(uint256, uint256, uint256) external returns uint256 envfree; function mathLibMulDivDown(uint256, uint256, uint256) external returns uint256 envfree; - function getMarketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; + function marketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; function marketLibId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; } @@ -21,5 +21,5 @@ rule checkSummaryMulDivDown(uint256 x, uint256 y, uint256 d) { // Check the munging of the MarketParams.id function. // This rule cannot be checked because it is not possible disable the keccak256 summary for the moment. // rule checkSummaryId(MorphoHarness.MarketParams marketParams) { -// assert marketLibId(marketParams) == getMarketId(marketParams); +// assert marketLibId(marketParams) == marketId(marketParams); // } diff --git a/certora/specs/Liveness.spec b/certora/specs/Liveness.spec index 51a3792ce..acbe7b2a7 100644 --- a/certora/specs/Liveness.spec +++ b/certora/specs/Liveness.spec @@ -1,15 +1,15 @@ methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); - function getTotalSupplyAssets(MorphoInternalAccess.Id) external returns uint256 envfree; - function getTotalSupplyShares(MorphoInternalAccess.Id) external returns uint256 envfree; - function getTotalBorrowAssets(MorphoInternalAccess.Id) external returns uint256 envfree; - function getTotalBorrowShares(MorphoInternalAccess.Id) external returns uint256 envfree; - function getSupplyShares(MorphoInternalAccess.Id, address) external returns uint256 envfree; - function getBorrowShares(MorphoInternalAccess.Id, address) external returns uint256 envfree; - function getCollateral(MorphoInternalAccess.Id, address) external returns uint256 envfree; - function getFee(MorphoInternalAccess.Id) external returns uint256 envfree; - function getLastUpdate(MorphoInternalAccess.Id) external returns uint256 envfree; - function getMarketId(MorphoInternalAccess.MarketParams) external returns MorphoInternalAccess.Id envfree; + function totalSupplyAssets(MorphoInternalAccess.Id) external returns uint256 envfree; + function totalSupplyShares(MorphoInternalAccess.Id) external returns uint256 envfree; + function totalBorrowAssets(MorphoInternalAccess.Id) external returns uint256 envfree; + function totalBorrowShares(MorphoInternalAccess.Id) external returns uint256 envfree; + function supplyShares(MorphoInternalAccess.Id, address) external returns uint256 envfree; + function borrowShares(MorphoInternalAccess.Id, address) external returns uint256 envfree; + function collateral(MorphoInternalAccess.Id, address) external returns uint256 envfree; + function fee(MorphoInternalAccess.Id) external returns uint256 envfree; + function lastUpdate(MorphoInternalAccess.Id) external returns uint256 envfree; + function marketId(MorphoInternalAccess.MarketParams) external returns MorphoInternalAccess.Id envfree; function _._accrueInterest(MorphoInternalAccess.MarketParams memory marketParams, MorphoInternalAccess.Id id) internal with (env e) => summaryAccrueInterest(e, marketParams, id) expect void; @@ -33,10 +33,10 @@ function summarySafeTransferFrom(address token, address from, address to, uint25 // Assume no fee. function summaryAccrueInterest(env e, MorphoInternalAccess.MarketParams marketParams, MorphoInternalAccess.Id id) { require e.block.timestamp < 2^128; - if (e.block.timestamp != getLastUpdate(id) && getTotalBorrowAssets(id) != 0) { + if (e.block.timestamp != lastUpdate(id) && totalBorrowAssets(id) != 0) { uint128 interest; - uint256 borrow = getTotalBorrowAssets(id); - uint256 supply = getTotalSupplyAssets(id); + uint256 borrow = totalBorrowAssets(id); + uint256 supply = totalSupplyAssets(id); require interest + borrow < 2^256; require interest + supply < 2^256; increaseInterest(e, id, interest); @@ -46,25 +46,25 @@ function summaryAccrueInterest(env e, MorphoInternalAccess.MarketParams marketPa } definition isCreated(MorphoInternalAccess.Id id) returns bool = - getLastUpdate(id) != 0; + lastUpdate(id) != 0; // Check that tokens and shares are properly accounted following a supply. rule supplyMovesTokensAndIncreasesShares(env e, MorphoInternalAccess.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { - MorphoInternalAccess.Id id = getMarketId(marketParams); + MorphoInternalAccess.Id id = marketId(marketParams); // Safe require that Morpho is not the sender. require e.msg.sender != currentContract; // Ensure that no interest is accumulated. - require getLastUpdate(id) == e.block.timestamp; + require lastUpdate(id) == e.block.timestamp; - mathint sharesBefore = getSupplyShares(id, onBehalf); + mathint sharesBefore = supplyShares(id, onBehalf); mathint balanceBefore = myBalances[marketParams.borrowableToken]; uint256 suppliedAssets; uint256 suppliedShares; suppliedAssets, suppliedShares = supply(e, marketParams, assets, shares, onBehalf, data); - mathint sharesAfter = getSupplyShares(id, onBehalf); + mathint sharesAfter = supplyShares(id, onBehalf); mathint balanceAfter = myBalances[marketParams.borrowableToken]; assert assets != 0 => suppliedAssets == assets; @@ -76,18 +76,18 @@ rule supplyMovesTokensAndIncreasesShares(env e, MorphoInternalAccess.MarketParam // This rule is commented out for the moment because of a bug in CVL where market IDs are not consistent accross a run. // Check that one can always repay the debt in full. // rule canRepayAll(env e, MorphoInternalAccess.MarketParams marketParams, uint256 shares, bytes data) { -// MorphoInternalAccess.Id id = getMarketId(marketParams); +// MorphoInternalAccess.Id id = marketId(marketParams); // require data.length == 0; -// require shares == getBorrowShares(id, e.msg.sender); +// require shares == borrowShares(id, e.msg.sender); // require isCreated(id); // require e.msg.sender != 0; // require e.msg.value == 0; // require shares > 0; -// require getLastUpdate(id) <= e.block.timestamp; -// require shares <= getTotalBorrowShares(id); -// require getTotalBorrowAssets(id) < 10^35; +// require lastUpdate(id) <= e.block.timestamp; +// require shares <= totalBorrowShares(id); +// require totalBorrowAssets(id) < 10^35; // repay@withrevert(e, marketParams, 0, shares, e.msg.sender, data); @@ -96,10 +96,10 @@ rule supplyMovesTokensAndIncreasesShares(env e, MorphoInternalAccess.MarketParam // Check the one can always withdraw all, under the condition that there are no outstanding debt on the market. rule canWithdrawAll(env e, MorphoInternalAccess.MarketParams marketParams, uint256 shares, address receiver) { - MorphoInternalAccess.Id id = getMarketId(marketParams); + MorphoInternalAccess.Id id = marketId(marketParams); // Require to ensure a withdraw all. - require shares == getSupplyShares(id, e.msg.sender); + require shares == supplyShares(id, e.msg.sender); // Omit sanity checks. require isCreated(id); require e.msg.sender != 0; @@ -107,11 +107,11 @@ rule canWithdrawAll(env e, MorphoInternalAccess.MarketParams marketParams, uint2 require e.msg.value == 0; require shares > 0; // Require no outstanding debt on the market. - require getTotalBorrowAssets(id) == 0; + require totalBorrowAssets(id) == 0; // Safe require because of the noTimeTravel rule. - require getLastUpdate(id) <= e.block.timestamp; + require lastUpdate(id) <= e.block.timestamp; // Safe require because of the sumSupplySharesCorrect invariant. - require shares <= getTotalSupplyShares(id); + require shares <= totalSupplyShares(id); withdraw@withrevert(e, marketParams, 0, shares, e.msg.sender, receiver); @@ -121,19 +121,19 @@ rule canWithdrawAll(env e, MorphoInternalAccess.MarketParams marketParams, uint2 // Check that a user can always withdraw all, under the condition that this user does not have an outstanding debt. // Combined with the canRepayAll rule, this ensures that a borrower can always fully exit a market. rule canWithdrawCollateralAll(env e, MorphoInternalAccess.MarketParams marketParams, uint256 assets, address receiver) { - MorphoInternalAccess.Id id = getMarketId(marketParams); + MorphoInternalAccess.Id id = marketId(marketParams); // Ensure a withdrawCollateral all. - require assets == getCollateral(id, e.msg.sender); + require assets == collateral(id, e.msg.sender); // Omit sanity checks. require isCreated(id); require receiver != 0; require e.msg.value == 0; require assets > 0; // Safe require because of the noTimeTravel rule. - require getLastUpdate(id) <= e.block.timestamp; + require lastUpdate(id) <= e.block.timestamp; // Require that the user does not have an outstanding debt. - require getBorrowShares(id, e.msg.sender) == 0; + require borrowShares(id, e.msg.sender) == 0; withdrawCollateral@withrevert(e, marketParams, assets, e.msg.sender, receiver); diff --git a/certora/specs/RatioMath.spec b/certora/specs/RatioMath.spec index b30628794..8be42cba5 100644 --- a/certora/specs/RatioMath.spec +++ b/certora/specs/RatioMath.spec @@ -1,12 +1,12 @@ methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); - function getMarketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; - function getVirtualTotalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; - function getVirtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; - function getVirtualTotalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree; - function getVirtualTotalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; - function getFee(MorphoHarness.Id) external returns uint256 envfree; - function getLastUpdate(MorphoHarness.Id) external returns uint256 envfree; + function marketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; + function virtualTotalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; + function virtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; + function virtualTotalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree; + function virtualTotalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; + function fee(MorphoHarness.Id) external returns uint256 envfree; + function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; function MathLib.mulDivDown(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivDown(a,b,c); function MathLib.mulDivUp(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivUp(a,b,c); @@ -14,11 +14,11 @@ methods { function _.borrowRate(MorphoHarness.MarketParams, MorphoHarness.Market) external => HAVOC_ECF; - function MAX_FEE() external returns uint256 envfree; + function maxFee() external returns uint256 envfree; } invariant feeInRange(MorphoHarness.Id id) - getFee(id) <= MAX_FEE(); + fee(id) <= maxFee(); // This is a simple overapproximative summary, stating that it rounds in the right direction. // The summary is checked by the specification in LibSummary.spec. @@ -41,14 +41,14 @@ rule accrueInterestIncreasesSupplyRatio(env e, MorphoHarness.MarketParams market MorphoHarness.Id id; requireInvariant feeInRange(id); - mathint assetsBefore = getVirtualTotalSupplyAssets(id); - mathint sharesBefore = getVirtualTotalSupplyShares(id); + mathint assetsBefore = virtualTotalSupplyAssets(id); + mathint sharesBefore = virtualTotalSupplyShares(id); // The check is done for every market, not just for id. accrueInterest(e, marketParams); - mathint assetsAfter = getVirtualTotalSupplyAssets(id); - mathint sharesAfter = getVirtualTotalSupplyShares(id); + mathint assetsAfter = virtualTotalSupplyAssets(id); + mathint sharesAfter = virtualTotalSupplyShares(id); // Check that ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter. assert assetsBefore * sharesAfter <= assetsAfter * sharesBefore; @@ -59,14 +59,14 @@ rule accrueInterestIncreasesBorrowRatio(env e, MorphoHarness.MarketParams market MorphoHarness.Id id; requireInvariant feeInRange(id); - mathint assetsBefore = getVirtualTotalBorrowAssets(id); - mathint sharesBefore = getVirtualTotalBorrowShares(id); + mathint assetsBefore = virtualTotalBorrowAssets(id); + mathint sharesBefore = virtualTotalBorrowShares(id); // The check is done for every marketParams, not just for id. accrueInterest(e, marketParams); - mathint assetsAfter = getVirtualTotalBorrowAssets(id); - mathint sharesAfter = getVirtualTotalBorrowShares(id); + mathint assetsAfter = virtualTotalBorrowAssets(id); + mathint sharesAfter = virtualTotalBorrowShares(id); // Check that ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter. assert assetsBefore * sharesAfter <= assetsAfter * sharesBefore; @@ -82,17 +82,17 @@ filtered { MorphoHarness.Id id; requireInvariant feeInRange(id); - mathint assetsBefore = getVirtualTotalSupplyAssets(id); - mathint sharesBefore = getVirtualTotalSupplyShares(id); + mathint assetsBefore = virtualTotalSupplyAssets(id); + mathint sharesBefore = virtualTotalSupplyShares(id); // Interest is checked separately by the rules above. // Here we assume interest has already been accumulated for this block. - require getLastUpdate(id) == e.block.timestamp; + require lastUpdate(id) == e.block.timestamp; f(e,args); - mathint assetsAfter = getVirtualTotalSupplyAssets(id); - mathint sharesAfter = getVirtualTotalSupplyShares(id); + mathint assetsAfter = virtualTotalSupplyAssets(id); + mathint sharesAfter = virtualTotalSupplyShares(id); // Check that ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter assert assetsBefore * sharesAfter <= assetsAfter * sharesBefore; @@ -112,15 +112,15 @@ filtered { requireInvariant feeInRange(id); // In;erest would increase borrow ratio, so we need to assume that no time passes. - require getLastUpdate(id) == e.block.timestamp; + require lastUpdate(id) == e.block.timestamp; - mathint assetsBefore = getVirtualTotalBorrowAssets(id); - mathint sharesBefore = getVirtualTotalBorrowShares(id); + mathint assetsBefore = virtualTotalBorrowAssets(id); + mathint sharesBefore = virtualTotalBorrowShares(id); f(e,args); - mathint assetsAfter = getVirtualTotalBorrowAssets(id); - mathint sharesAfter = getVirtualTotalBorrowShares(id); + mathint assetsAfter = virtualTotalBorrowAssets(id); + mathint sharesAfter = virtualTotalBorrowShares(id); // Check that ratio decreases: assetsBefore/sharesBefore >= assetsAfter / sharesAfter assert assetsBefore * sharesAfter >= assetsAfter * sharesBefore; @@ -131,20 +131,20 @@ filtered { // The other case requires exact math (ie not summarizing mulDivUp and mulDivDown), so it is checked separately in ExactMath.spec rule repayIncreasesBorrowRatio(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onbehalf, bytes data) { - MorphoHarness.Id id = getMarketId(marketParams); + MorphoHarness.Id id = marketId(marketParams); requireInvariant feeInRange(id); // Interest would increase borrow ratio, so we need to assume that no time passes. - require getLastUpdate(id) == e.block.timestamp; + require lastUpdate(id) == e.block.timestamp; - mathint assetsBefore = getVirtualTotalBorrowAssets(id); - mathint sharesBefore = getVirtualTotalBorrowShares(id); + mathint assetsBefore = virtualTotalBorrowAssets(id); + mathint sharesBefore = virtualTotalBorrowShares(id); mathint repaidAssets; repaidAssets, _ = repay(e, marketParams, assets, shares, onbehalf, data); - mathint assetsAfter = getVirtualTotalBorrowAssets(id); - mathint sharesAfter = getVirtualTotalBorrowShares(id); + mathint assetsAfter = virtualTotalBorrowAssets(id); + mathint sharesAfter = virtualTotalBorrowShares(id); // Check the case where the market is not repaid fully. require repaidAssets < assetsBefore; diff --git a/certora/specs/Reverts.spec b/certora/specs/Reverts.spec index fd1d44a99..021bb3cd1 100644 --- a/certora/specs/Reverts.spec +++ b/certora/specs/Reverts.spec @@ -1,24 +1,24 @@ methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); function owner() external returns address envfree; - function getTotalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; - function getTotalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree; - function getTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; - function getTotalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; - function getLastUpdate(MorphoHarness.Id) external returns uint256 envfree; + function totalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; + function totalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree; + function totalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; + function totalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; + function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; function feeRecipient() external returns address envfree; function isLltvEnabled(uint256) external returns bool envfree; function isIrmEnabled(address) external returns bool envfree; function isAuthorized(address, address) external returns bool envfree; - function getMarketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; - function MAX_FEE() external returns uint256 envfree; - function WAD() external returns uint256 envfree; + function marketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; + function maxFee() external returns uint256 envfree; + function wad() external returns uint256 envfree; } definition isCreated(MorphoHarness.Id id) returns bool = - (getLastUpdate(id) != 0); + (lastUpdate(id) != 0); ghost mapping(MorphoHarness.Id => mathint) sumCollateral { @@ -29,10 +29,10 @@ hook Sstore position[KEY MorphoHarness.Id id][KEY address owner].collateral uint } definition emptyMarket(MorphoHarness.Id id) returns bool = - getTotalSupplyAssets(id) == 0 && - getTotalSupplyShares(id) == 0 && - getTotalBorrowAssets(id) == 0 && - getTotalBorrowShares(id) == 0 && + totalSupplyAssets(id) == 0 && + totalSupplyShares(id) == 0 && + totalBorrowAssets(id) == 0 && + totalBorrowShares(id) == 0 && sumCollateral[id] == 0; definition exactlyOneZero(uint256 assets, uint256 shares) returns bool = @@ -76,18 +76,18 @@ rule enableLltvRevertCondition(env e, uint256 lltv) { address oldOwner = owner(); bool oldIsLltvEnabled = isLltvEnabled(lltv); enableLltv@withrevert(e, lltv); - assert lastReverted <=> e.msg.value != 0 || e.msg.sender != oldOwner || lltv >= WAD() || oldIsLltvEnabled; + assert lastReverted <=> e.msg.value != 0 || e.msg.sender != oldOwner || lltv >= wad() || oldIsLltvEnabled; } // Check that setFee reverts when its inputs are not validated. // setFee can also revert if the accrueInterest reverts. rule setFeeInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 newFee) { - MorphoHarness.Id id = getMarketId(marketParams); + MorphoHarness.Id id = marketId(marketParams); address oldOwner = owner(); bool wasCreated = isCreated(id); setFee@withrevert(e, marketParams, newFee); bool hasReverted = lastReverted; - assert e.msg.value != 0 || e.msg.sender != oldOwner || !wasCreated || newFee > MAX_FEE() => hasReverted; + assert e.msg.value != 0 || e.msg.sender != oldOwner || !wasCreated || newFee > maxFee() => hasReverted; } // Check the revert condition for the setFeeRecipient function. @@ -100,10 +100,10 @@ rule setFeeRecipientRevertCondition(env e, address newFeeRecipient) { // Check the revert condition for the createMarket function. rule createMarketRevertCondition(env e, MorphoHarness.MarketParams marketParams) { - MorphoHarness.Id id = getMarketId(marketParams); + MorphoHarness.Id id = marketId(marketParams); bool irmEnabled = isIrmEnabled(marketParams.irm); bool lltvEnabled = isLltvEnabled(marketParams.lltv); - uint256 lastUpdated = getLastUpdate(id); + uint256 lastUpdated = lastUpdate(id); createMarket@withrevert(e, marketParams); assert lastReverted <=> e.msg.value != 0 || !irmEnabled || !lltvEnabled || lastUpdated != 0; } diff --git a/certora/specs/Transfer.spec b/certora/specs/Transfer.spec index 16fa13795..1ce406f11 100644 --- a/certora/specs/Transfer.spec +++ b/certora/specs/Transfer.spec @@ -1,9 +1,9 @@ methods { - function doTransfer(address, address, uint256) external envfree; - function doTransferFrom(address, address, address, uint256) external envfree; - function getBalance(address, address) external returns (uint256) envfree; - function getAllowance(address, address, address) external returns (uint256) envfree; - function getTotalSupply(address) external returns (uint256) envfree; + function safeTransfer(address, address, uint256) external envfree; + function safeTransferFrom(address, address, address, uint256) external envfree; + function balanceOf(address, address) external returns (uint256) envfree; + function allowance(address, address, address) external returns (uint256) envfree; + function totalSupply(address) external returns (uint256) envfree; function _.transfer(address, uint256) external => DISPATCHER(true); function _.transferFrom(address, address, uint256) external => DISPATCHER(true); @@ -28,11 +28,11 @@ function summarySafeTransferFrom(address token, address from, address to, uint25 // Check the functional correctness of the summary of safeTransfer. rule checkTransferSummary(address token, address to, uint256 amount) { - mathint initialBalance = getBalance(token, currentContract); - require to != currentContract => initialBalance + getBalance(token, to) <= to_mathint(getTotalSupply(token)); + mathint initialBalance = balanceOf(token, currentContract); + require to != currentContract => initialBalance + balanceOf(token, to) <= to_mathint(totalSupply(token)); - doTransfer(token, to, amount); - mathint finalBalance = getBalance(token, currentContract); + safeTransfer(token, to, amount); + mathint finalBalance = balanceOf(token, currentContract); require myBalances[token] == initialBalance; summarySafeTransferFrom(token, currentContract, to, amount); @@ -41,11 +41,11 @@ rule checkTransferSummary(address token, address to, uint256 amount) { // Check the functional correctness of the summary of safeTransferFrom. rule checkTransferFromSummary(address token, address from, uint256 amount) { - mathint initialBalance = getBalance(token, currentContract); - require from != currentContract => initialBalance + getBalance(token, from) <= to_mathint(getTotalSupply(token)); + mathint initialBalance = balanceOf(token, currentContract); + require from != currentContract => initialBalance + balanceOf(token, from) <= to_mathint(totalSupply(token)); - doTransferFrom(token, from, currentContract, amount); - mathint finalBalance = getBalance(token, currentContract); + safeTransferFrom(token, from, currentContract, amount); + mathint finalBalance = balanceOf(token, currentContract); require myBalances[token] == initialBalance; summarySafeTransferFrom(token, from, currentContract, amount); @@ -54,25 +54,25 @@ rule checkTransferFromSummary(address token, address from, uint256 amount) { // Check the revert condition of the summary of safeTransfer. rule transferRevertCondition(address token, address to, uint256 amount) { - uint256 initialBalance = getBalance(token, currentContract); - uint256 toInitialBalance = getBalance(token, to); - require to != currentContract => initialBalance + toInitialBalance <= to_mathint(getTotalSupply(token)); + uint256 initialBalance = balanceOf(token, currentContract); + uint256 toInitialBalance = balanceOf(token, to); + require to != currentContract => initialBalance + toInitialBalance <= to_mathint(totalSupply(token)); require currentContract != 0 && to != 0; - doTransfer@withrevert(token, to, amount); + safeTransfe@withrevert(token, to, amount); assert lastReverted == (initialBalance < amount); } // Check the revert condition of the summary of safeTransferFrom. rule transferFromRevertCondition(address token, address from, address to, uint256 amount) { - uint256 initialBalance = getBalance(token, from); - uint256 toInitialBalance = getBalance(token, to); - uint256 allowance = getAllowance(token, from, currentContract); - require to != from => initialBalance + toInitialBalance <= to_mathint(getTotalSupply(token)); + uint256 initialBalance = balanceOf(token, from); + uint256 toInitialBalance = balanceOf(token, to); + uint256 allowance = allowance(token, from, currentContract); + require to != from => initialBalance + toInitialBalance <= to_mathint(totalSupply(token)); require from != 0 && to != 0; - doTransferFrom@withrevert(token, from, to, amount); + safeTransferFrom@withrevert(token, from, to, amount); assert lastReverted == (initialBalance < amount) || allowance < amount; } From 4063fd2ed579286d8468b6578fce8079c596335a Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 1 Sep 2023 00:35:42 +0200 Subject: [PATCH 100/204] fix: renaming typos --- certora/harness/MorphoHarness.sol | 2 +- certora/specs/Transfer.spec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/certora/harness/MorphoHarness.sol b/certora/harness/MorphoHarness.sol index d94feb92d..2e48332aa 100644 --- a/certora/harness/MorphoHarness.sol +++ b/certora/harness/MorphoHarness.sol @@ -15,7 +15,7 @@ contract MorphoHarness is Morpho { } function maxFee() external pure returns (uint256) { - return maxFee(); + return MAX_FEE; } function totalSupplyAssets(Id id) external view returns (uint256) { diff --git a/certora/specs/Transfer.spec b/certora/specs/Transfer.spec index 1ce406f11..6484dd076 100644 --- a/certora/specs/Transfer.spec +++ b/certora/specs/Transfer.spec @@ -59,7 +59,7 @@ rule transferRevertCondition(address token, address to, uint256 amount) { require to != currentContract => initialBalance + toInitialBalance <= to_mathint(totalSupply(token)); require currentContract != 0 && to != 0; - safeTransfe@withrevert(token, to, amount); + safeTransfer@withrevert(token, to, amount); assert lastReverted == (initialBalance < amount); } From b1e88f1a5ea41961e35518f0401ff7182b5194bb Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 1 Sep 2023 00:38:18 +0200 Subject: [PATCH 101/204] chore: send all jobs at the start --- .github/workflows/certora.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml index 976f2058b..b27fd301f 100644 --- a/.github/workflows/certora.yml +++ b/.github/workflows/certora.yml @@ -39,7 +39,6 @@ jobs: strategy: fail-fast: false - max-parallel: 4 matrix: script: From b6c2b0ac33837ec7f3a134eb8c99e968aef904d9 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 1 Sep 2023 00:48:04 +0200 Subject: [PATCH 102/204] fix: repay decreases borrow ratio --- certora/specs/RatioMath.spec | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/certora/specs/RatioMath.spec b/certora/specs/RatioMath.spec index 8be42cba5..2fbe2e7e8 100644 --- a/certora/specs/RatioMath.spec +++ b/certora/specs/RatioMath.spec @@ -129,7 +129,7 @@ filtered { // Check that when not accruing interest, repay is decreasing the value of borrow shares. // Check the case where the market is not repaid fully. // The other case requires exact math (ie not summarizing mulDivUp and mulDivDown), so it is checked separately in ExactMath.spec -rule repayIncreasesBorrowRatio(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onbehalf, bytes data) +rule repayDecreasesBorrowRatio(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { MorphoHarness.Id id = marketId(marketParams); requireInvariant feeInRange(id); @@ -141,14 +141,14 @@ rule repayIncreasesBorrowRatio(env e, MorphoHarness.MarketParams marketParams, u mathint sharesBefore = virtualTotalBorrowShares(id); mathint repaidAssets; - repaidAssets, _ = repay(e, marketParams, assets, shares, onbehalf, data); - - mathint assetsAfter = virtualTotalBorrowAssets(id); - mathint sharesAfter = virtualTotalBorrowShares(id); + repaidAssets, _ = repay(e, marketParams, assets, shares, onBehalf, data); // Check the case where the market is not repaid fully. require repaidAssets < assetsBefore; + mathint assetsAfter = virtualTotalBorrowAssets(id); + mathint sharesAfter = virtualTotalBorrowShares(id); + assert assetsAfter == assetsBefore - repaidAssets; // Check that ratio decreases: assetsBefore/sharesBefore >= assetsAfter / sharesAfter assert assetsBefore * sharesAfter >= assetsAfter * sharesBefore; From eaaeb4b95eaef4d5ab40b0b763f8cd6669cbc944 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 1 Sep 2023 09:26:49 +0200 Subject: [PATCH 103/204] fix: timeout on repayDecreasesBorrowRatio --- certora/specs/RatioMath.spec | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/certora/specs/RatioMath.spec b/certora/specs/RatioMath.spec index 2fbe2e7e8..123273ea3 100644 --- a/certora/specs/RatioMath.spec +++ b/certora/specs/RatioMath.spec @@ -134,12 +134,11 @@ rule repayDecreasesBorrowRatio(env e, MorphoHarness.MarketParams marketParams, u MorphoHarness.Id id = marketId(marketParams); requireInvariant feeInRange(id); - // Interest would increase borrow ratio, so we need to assume that no time passes. - require lastUpdate(id) == e.block.timestamp; - mathint assetsBefore = virtualTotalBorrowAssets(id); mathint sharesBefore = virtualTotalBorrowShares(id); + require lastUpdate(id) == e.block.timestamp; + mathint repaidAssets; repaidAssets, _ = repay(e, marketParams, assets, shares, onBehalf, data); From 96a33911a846ae21c6f8a3dfe226762b45416c89 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 1 Sep 2023 09:48:32 +0200 Subject: [PATCH 104/204] docs: add comment back --- certora/specs/RatioMath.spec | 1 + 1 file changed, 1 insertion(+) diff --git a/certora/specs/RatioMath.spec b/certora/specs/RatioMath.spec index 123273ea3..3c7e189dd 100644 --- a/certora/specs/RatioMath.spec +++ b/certora/specs/RatioMath.spec @@ -137,6 +137,7 @@ rule repayDecreasesBorrowRatio(env e, MorphoHarness.MarketParams marketParams, u mathint assetsBefore = virtualTotalBorrowAssets(id); mathint sharesBefore = virtualTotalBorrowShares(id); + // Interest would increase borrow ratio, so we need to assume that no time passes. require lastUpdate(id) == e.block.timestamp; mathint repaidAssets; From c8bfa564d8fd63e3d5ca4a34be8cf17580f49a45 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 1 Sep 2023 11:57:32 +0200 Subject: [PATCH 105/204] chore: license for the whole certora folder --- README.md | 2 +- certora/{harness => }/LICENSE | 0 certora/dispatch/ERC20NoRevert.sol | 2 +- certora/dispatch/ERC20Standard.sol | 2 +- certora/dispatch/ERC20USDT.sol | 2 +- certora/specs/AccrueInterest.spec | 1 + certora/specs/ConsistentState.spec | 1 + certora/specs/ExactMath.spec | 1 + certora/specs/ExitLiquidity.spec | 1 + certora/specs/Health.spec | 1 + certora/specs/LibSummary.spec | 1 + certora/specs/Liveness.spec | 1 + certora/specs/RatioMath.spec | 1 + certora/specs/Reentrancy.spec | 1 + certora/specs/Reverts.spec | 1 + certora/specs/Transfer.spec | 1 + 16 files changed, 15 insertions(+), 4 deletions(-) rename certora/{harness => }/LICENSE (100%) diff --git a/README.md b/README.md index c850a356f..df831d1f4 100644 --- a/README.md +++ b/README.md @@ -9,4 +9,4 @@ All files in the following folders can also be licensed under `GPL-2.0-or-later` - `src/libraries`, see [`src/libraries/LICENSE`](./src/libaries/LICENSE) - `src/mocks`, see [`src/mocks/LICENSE`](./src/mocks/LICENSE) - `test`, see [`test/LICENSE`](./test/LICENSE) -- `certora/harness`, see [`certora/harness/LICENSE`](./certora/harness/LICENSE) +- `certora`, see [`certora/LICENSE`](./certora/LICENSE) diff --git a/certora/harness/LICENSE b/certora/LICENSE similarity index 100% rename from certora/harness/LICENSE rename to certora/LICENSE diff --git a/certora/dispatch/ERC20NoRevert.sol b/certora/dispatch/ERC20NoRevert.sol index ab9a3f66a..6a9ab6efc 100644 --- a/certora/dispatch/ERC20NoRevert.sol +++ b/certora/dispatch/ERC20NoRevert.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.0; contract ERC20NoRevert { diff --git a/certora/dispatch/ERC20Standard.sol b/certora/dispatch/ERC20Standard.sol index 2e9312532..eb76f146e 100644 --- a/certora/dispatch/ERC20Standard.sol +++ b/certora/dispatch/ERC20Standard.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.0; import {ERC20} from "openzeppelin-contracts/token/ERC20/ERC20.sol"; diff --git a/certora/dispatch/ERC20USDT.sol b/certora/dispatch/ERC20USDT.sol index 95615e56e..789372041 100644 --- a/certora/dispatch/ERC20USDT.sol +++ b/certora/dispatch/ERC20USDT.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.0; contract ERC20USDT { diff --git a/certora/specs/AccrueInterest.spec b/certora/specs/AccrueInterest.spec index 9470448d1..3d578caa8 100644 --- a/certora/specs/AccrueInterest.spec +++ b/certora/specs/AccrueInterest.spec @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); function MathLib.mulDivDown(uint256 a, uint256 b, uint256 c) internal returns uint256 => ghostMulDivDown(a,b,c); diff --git a/certora/specs/ConsistentState.spec b/certora/specs/ConsistentState.spec index 2a393f910..9cb819b76 100644 --- a/certora/specs/ConsistentState.spec +++ b/certora/specs/ConsistentState.spec @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); function totalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; diff --git a/certora/specs/ExactMath.spec b/certora/specs/ExactMath.spec index e536fc02e..8d94adfbc 100644 --- a/certora/specs/ExactMath.spec +++ b/certora/specs/ExactMath.spec @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); function marketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; diff --git a/certora/specs/ExitLiquidity.spec b/certora/specs/ExitLiquidity.spec index 2a5636f6e..a54a8e582 100644 --- a/certora/specs/ExitLiquidity.spec +++ b/certora/specs/ExitLiquidity.spec @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); function supplyShares(MorphoHarness.Id, address) external returns uint256 envfree; diff --git a/certora/specs/Health.spec b/certora/specs/Health.spec index 3204fbb62..c68e3c43e 100644 --- a/certora/specs/Health.spec +++ b/certora/specs/Health.spec @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; diff --git a/certora/specs/LibSummary.spec b/certora/specs/LibSummary.spec index 100a17159..9adec259a 100644 --- a/certora/specs/LibSummary.spec +++ b/certora/specs/LibSummary.spec @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); function mathLibMulDivUp(uint256, uint256, uint256) external returns uint256 envfree; diff --git a/certora/specs/Liveness.spec b/certora/specs/Liveness.spec index acbe7b2a7..334a65fa4 100644 --- a/certora/specs/Liveness.spec +++ b/certora/specs/Liveness.spec @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); function totalSupplyAssets(MorphoInternalAccess.Id) external returns uint256 envfree; diff --git a/certora/specs/RatioMath.spec b/certora/specs/RatioMath.spec index 3c7e189dd..36d4520ca 100644 --- a/certora/specs/RatioMath.spec +++ b/certora/specs/RatioMath.spec @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); function marketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; diff --git a/certora/specs/Reentrancy.spec b/certora/specs/Reentrancy.spec index f7307fb7f..0d009a898 100644 --- a/certora/specs/Reentrancy.spec +++ b/certora/specs/Reentrancy.spec @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); function _.borrowRate(MorphoHarness.MarketParams marketParams, MorphoHarness.Market) external => summaryBorrowRate() expect uint256; diff --git a/certora/specs/Reverts.spec b/certora/specs/Reverts.spec index 021bb3cd1..5b9ef4d6f 100644 --- a/certora/specs/Reverts.spec +++ b/certora/specs/Reverts.spec @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); function owner() external returns address envfree; diff --git a/certora/specs/Transfer.spec b/certora/specs/Transfer.spec index 6484dd076..25c744e4c 100644 --- a/certora/specs/Transfer.spec +++ b/certora/specs/Transfer.spec @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later methods { function safeTransfer(address, address, uint256) external envfree; function safeTransferFrom(address, address, address, uint256) external envfree; From 66db2701e7bbd5ac5148fe96ab38a07c043ea47f Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 1 Sep 2023 16:43:18 +0200 Subject: [PATCH 106/204] chore: explicit import in ERC20Standard --- certora/dispatch/ERC20Standard.sol | 2 +- certora/scripts/verifyTransfer.sh | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/certora/dispatch/ERC20Standard.sol b/certora/dispatch/ERC20Standard.sol index eb76f146e..817c487df 100644 --- a/certora/dispatch/ERC20Standard.sol +++ b/certora/dispatch/ERC20Standard.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.0; -import {ERC20} from "openzeppelin-contracts/token/ERC20/ERC20.sol"; +import {ERC20} from "../../lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; contract ERC20Standard is ERC20 { constructor(string memory name, string memory symbol) ERC20(name, symbol) {} diff --git a/certora/scripts/verifyTransfer.sh b/certora/scripts/verifyTransfer.sh index 1fee34c92..3f0adcd49 100755 --- a/certora/scripts/verifyTransfer.sh +++ b/certora/scripts/verifyTransfer.sh @@ -10,7 +10,6 @@ certoraRun \ certora/dispatch/ERC20USDT.sol \ certora/dispatch/ERC20NoRevert.sol \ --verify TransferHarness:certora/specs/Transfer.spec \ - --packages openzeppelin-contracts=lib/openzeppelin-contracts/contracts \ --loop_iter 3 \ --optimistic_loop \ --msg "Morpho Blue Transfer" \ From 770e1c203e9c80e120fe876dbe62602a1489a48e Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 1 Sep 2023 17:24:15 +0200 Subject: [PATCH 107/204] refactor: use munged files consistently --- certora/harness/TransferHarness.sol | 4 ++-- certora/scripts/verifyHealth.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/certora/harness/TransferHarness.sol b/certora/harness/TransferHarness.sol index 011027e79..1cf77f9e7 100644 --- a/certora/harness/TransferHarness.sol +++ b/certora/harness/TransferHarness.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.12; -import "../../src/libraries/SafeTransferLib.sol"; -import "../../src/interfaces/IERC20.sol"; +import "../munged/libraries/SafeTransferLib.sol"; +import "../munged/interfaces/IERC20.sol"; interface IERC20Extended is IERC20 { function balanceOf(address) external view returns (uint256); diff --git a/certora/scripts/verifyHealth.sh b/certora/scripts/verifyHealth.sh index beaed1600..8c5b830f0 100755 --- a/certora/scripts/verifyHealth.sh +++ b/certora/scripts/verifyHealth.sh @@ -6,7 +6,7 @@ make -C certora munged certoraRun \ certora/harness/MorphoHarness.sol \ - src/mocks/OracleMock.sol \ + certora/munged/mocks/OracleMock.sol \ --verify MorphoHarness:certora/specs/Health.spec \ --loop_iter 3 \ --optimistic_loop \ From c98b4fb641dc2dd1dd2c2568e24d6bcef9da8ffd Mon Sep 17 00:00:00 2001 From: Jochen Hoenicke Date: Sun, 3 Sep 2023 10:36:51 +0200 Subject: [PATCH 108/204] remove optimistic_loop option This makes the verification results more trusted. The only loop was in extSloads and it's no longer included in verification. --- certora/scripts/verifyConsistentState.sh | 2 -- certora/scripts/verifyExactMath.sh | 2 -- certora/scripts/verifyExitLiquidity.sh | 2 -- certora/scripts/verifyHealth.sh | 2 -- certora/scripts/verifyLiveness.sh | 2 -- certora/scripts/verifyReentrancy.sh | 2 -- certora/scripts/verifyReverts.sh | 2 -- certora/scripts/verifyTransfer.sh | 2 -- 8 files changed, 16 deletions(-) diff --git a/certora/scripts/verifyConsistentState.sh b/certora/scripts/verifyConsistentState.sh index d549cef7f..a49f74bc7 100755 --- a/certora/scripts/verifyConsistentState.sh +++ b/certora/scripts/verifyConsistentState.sh @@ -8,7 +8,5 @@ certoraRun \ certora/harness/MorphoHarness.sol \ --verify MorphoHarness:certora/specs/ConsistentState.spec \ --solc_allow_path src \ - --loop_iter 3 \ - --optimistic_loop \ --msg "Morpho Blue Consistent State" \ "$@" diff --git a/certora/scripts/verifyExactMath.sh b/certora/scripts/verifyExactMath.sh index adeb34d75..fc9f801fa 100755 --- a/certora/scripts/verifyExactMath.sh +++ b/certora/scripts/verifyExactMath.sh @@ -8,8 +8,6 @@ certoraRun \ certora/harness/MorphoHarness.sol \ src/mocks/OracleMock.sol \ --verify MorphoHarness:certora/specs/ExactMath.spec \ - --loop_iter 3 \ - --optimistic_loop \ --prover_args '-smt_hashingScheme plaininjectivity' \ --msg "Morpho Blue Exact Math" \ "$@" diff --git a/certora/scripts/verifyExitLiquidity.sh b/certora/scripts/verifyExitLiquidity.sh index d83843fd6..9896353c7 100755 --- a/certora/scripts/verifyExitLiquidity.sh +++ b/certora/scripts/verifyExitLiquidity.sh @@ -8,7 +8,5 @@ certoraRun \ certora/harness/MorphoHarness.sol \ --verify MorphoHarness:certora/specs/ExitLiquidity.spec \ --solc_allow_path src \ - --loop_iter 3 \ - --optimistic_loop \ --msg "Morpho Blue Exit Liquidity" \ "$@" diff --git a/certora/scripts/verifyHealth.sh b/certora/scripts/verifyHealth.sh index 8c5b830f0..9d63978af 100755 --- a/certora/scripts/verifyHealth.sh +++ b/certora/scripts/verifyHealth.sh @@ -8,8 +8,6 @@ certoraRun \ certora/harness/MorphoHarness.sol \ certora/munged/mocks/OracleMock.sol \ --verify MorphoHarness:certora/specs/Health.spec \ - --loop_iter 3 \ - --optimistic_loop \ --prover_args '-smt_hashingScheme plaininjectivity' \ --msg "Morpho Blue Health Check" \ "$@" diff --git a/certora/scripts/verifyLiveness.sh b/certora/scripts/verifyLiveness.sh index cdaacf79b..c0034c362 100755 --- a/certora/scripts/verifyLiveness.sh +++ b/certora/scripts/verifyLiveness.sh @@ -8,7 +8,5 @@ certoraRun \ certora/harness/MorphoInternalAccess.sol \ --verify MorphoInternalAccess:certora/specs/Liveness.spec \ --solc_allow_path src \ - --loop_iter 3 \ - --optimistic_loop \ --msg "Morpho Blue Liveness" \ "$@" diff --git a/certora/scripts/verifyReentrancy.sh b/certora/scripts/verifyReentrancy.sh index 570c2d986..cc9d7dbfc 100755 --- a/certora/scripts/verifyReentrancy.sh +++ b/certora/scripts/verifyReentrancy.sh @@ -8,7 +8,5 @@ certoraRun \ certora/harness/MorphoHarness.sol \ --verify MorphoHarness:certora/specs/Reentrancy.spec \ --prover_args '-enableStorageSplitting false' \ - --loop_iter 3 \ - --optimistic_loop \ --msg "Morpho Blue Reentrancy" \ "$@" diff --git a/certora/scripts/verifyReverts.sh b/certora/scripts/verifyReverts.sh index 3e340ceb9..e89bc970d 100755 --- a/certora/scripts/verifyReverts.sh +++ b/certora/scripts/verifyReverts.sh @@ -7,7 +7,5 @@ make -C certora munged certoraRun \ certora/harness/MorphoHarness.sol \ --verify MorphoHarness:certora/specs/Reverts.spec \ - --loop_iter 3 \ - --optimistic_loop \ --msg "Morpho Blue Reverts" \ "$@" diff --git a/certora/scripts/verifyTransfer.sh b/certora/scripts/verifyTransfer.sh index 3f0adcd49..633880f35 100755 --- a/certora/scripts/verifyTransfer.sh +++ b/certora/scripts/verifyTransfer.sh @@ -10,7 +10,5 @@ certoraRun \ certora/dispatch/ERC20USDT.sol \ certora/dispatch/ERC20NoRevert.sol \ --verify TransferHarness:certora/specs/Transfer.spec \ - --loop_iter 3 \ - --optimistic_loop \ --msg "Morpho Blue Transfer" \ "$@" From 197257caf37c61cf68b80d3379568027c9a94a9f Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 5 Sep 2023 09:06:02 +0200 Subject: [PATCH 109/204] feat: start documentation --- certora/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 certora/README.md diff --git a/certora/README.md b/certora/README.md new file mode 100644 index 000000000..ed8373d63 --- /dev/null +++ b/certora/README.md @@ -0,0 +1,14 @@ +# Certora verification + +This folder defines the verification of the Morpho Blue protocol using CVL, Certora's specification language. + +## High level description + + + +## Folder and files structure + +The [`certora/specs`](./specs) folder contains the following files: +- [AccrueInterest.spec](./specs/AccrueInterest.spec), checking that the main functions accrue interest at the start of the interaction. This is done by making sure that accruing interest before calling the function does not change the outcome. View functions do not necessarily respect this property (for example `totalSupplyShares`), and are filtered out. +- [ConsistentState.spec](./specs/ConsistentState.spec), checking that the state (storage) of the Morpho contract is consistent. This includes checking that the accounting of the total amount and shares is correct, that markets are independent from each other, that only enabled IRMs and LLTVs can be used, and that users cannot have their position made worse by an unauthorized account. +- [ExactMath.spec](./specs/ExactMath.spec), checking properties about From 75dcf22bc758eabefb7bc0a0b335cccf1b1c1cef Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 5 Sep 2023 10:51:48 +0200 Subject: [PATCH 110/204] docs: complete files description --- certora/README.md | 15 ++++++++++----- certora/harness/MorphoHarness.sol | 8 ++++---- certora/harness/TransferHarness.sol | 4 ++-- certora/specs/ConsistentState.spec | 16 ++++++++-------- certora/specs/ExactMath.spec | 10 ++++++---- certora/specs/ExitLiquidity.spec | 16 ++++++++-------- certora/specs/Health.spec | 6 +++--- certora/specs/LibSummary.spec | 14 +++++++------- certora/specs/Liveness.spec | 10 +++++----- certora/specs/RatioMath.spec | 6 +++--- certora/specs/Reentrancy.spec | 2 +- certora/specs/Reverts.spec | 6 +++--- certora/specs/Transfer.spec | 8 ++++---- 13 files changed, 64 insertions(+), 57 deletions(-) diff --git a/certora/README.md b/certora/README.md index ed8373d63..7dabd675a 100644 --- a/certora/README.md +++ b/certora/README.md @@ -1,14 +1,19 @@ -# Certora verification - This folder defines the verification of the Morpho Blue protocol using CVL, Certora's specification language. ## High level description - - ## Folder and files structure The [`certora/specs`](./specs) folder contains the following files: + - [AccrueInterest.spec](./specs/AccrueInterest.spec), checking that the main functions accrue interest at the start of the interaction. This is done by making sure that accruing interest before calling the function does not change the outcome. View functions do not necessarily respect this property (for example `totalSupplyShares`), and are filtered out. - [ConsistentState.spec](./specs/ConsistentState.spec), checking that the state (storage) of the Morpho contract is consistent. This includes checking that the accounting of the total amount and shares is correct, that markets are independent from each other, that only enabled IRMs and LLTVs can be used, and that users cannot have their position made worse by an unauthorized account. -- [ExactMath.spec](./specs/ExactMath.spec), checking properties about +- [ExactMath.spec](./specs/ExactMath.spec), checking precise properties when taking into account the exact multiplication and division. Notably, this file specifies that doing using supply and withdraw in the same block cannot yield more funds than at the start. +- [ExitLiquidity.spec](./specs/ExitLiquidity.spec), checking that when exiting a position with witdraw, withdrawCollateral or repay, the user cannot get more than what was owed. +- [Health.spec](./specs/Health.spec), checking properties about the health of the positions. Notably, functions cannot render an account unhealthy, and debt positions at least have some collateral. +- [LibSummary.spec](./specs/LibSummary.spec), checking the summarization of the library functions that are used in other specification files. +- [Liveness.spec](./specs/Liveness.spec), checking that main functions change the owner of funds and the amount of shares as expected, and that it's always possible to exit a position. +- [RatioMath.spec](./specs/RatioMath.spec), checking that the ratio between shares and assets evolves predictably over time. +- [Reentrancy.spec](./specs/Reentrancy.spec), checking that the contract is immune to a particular class of reentrancy issues. +- [Reverts.spec](./specs/Reverts.spec), checking the condition for reverts and that inputs are correctly validated. +- [Transfer.spec](./specs/Transfer.spec), checking the summarization of the safe transfer library functions that are used in other specification files. diff --git a/certora/harness/MorphoHarness.sol b/certora/harness/MorphoHarness.sol index 2e48332aa..600515f4b 100644 --- a/certora/harness/MorphoHarness.sol +++ b/certora/harness/MorphoHarness.sol @@ -70,21 +70,21 @@ contract MorphoHarness is Morpho { return market[id].totalBorrowShares + SharesMathLib.VIRTUAL_SHARES; } - function marketId(MarketParams memory marketParams) external pure returns (Id) { + function libId(MarketParams memory marketParams) external pure returns (Id) { return marketParams.id(); } - function marketLibId(MarketParams memory marketParams) external pure returns (Id marketParamsId) { + function optimizedId(MarketParams memory marketParams) external pure returns (Id marketParamsId) { assembly ("memory-safe") { marketParamsId := keccak256(marketParams, mul(5, 32)) } } - function mathLibMulDivUp(uint256 x, uint256 y, uint256 d) public pure returns (uint256) { + function libMulDivUp(uint256 x, uint256 y, uint256 d) public pure returns (uint256) { return MathLib.mulDivUp(x, y, d); } - function mathLibMulDivDown(uint256 x, uint256 y, uint256 d) public pure returns (uint256) { + function libMulDivDown(uint256 x, uint256 y, uint256 d) public pure returns (uint256) { return MathLib.mulDivDown(x, y, d); } diff --git a/certora/harness/TransferHarness.sol b/certora/harness/TransferHarness.sol index 1cf77f9e7..1227e3fc7 100644 --- a/certora/harness/TransferHarness.sol +++ b/certora/harness/TransferHarness.sol @@ -13,11 +13,11 @@ interface IERC20Extended is IERC20 { contract TransferHarness { using SafeTransferLib for IERC20; - function safeTransferFrom(address token, address from, address to, uint256 value) public { + function libSafeTransferFrom(address token, address from, address to, uint256 value) public { IERC20(token).safeTransferFrom(from, to, value); } - function safeTransfer(address token, address to, uint256 value) public { + function libSafeTransfer(address token, address to, uint256 value) public { IERC20(token).safeTransfer(to, value); } diff --git a/certora/specs/ConsistentState.spec b/certora/specs/ConsistentState.spec index 9cb819b76..8915e0301 100644 --- a/certora/specs/ConsistentState.spec +++ b/certora/specs/ConsistentState.spec @@ -10,7 +10,7 @@ methods { function collateral(MorphoHarness.Id, address) external returns uint256 envfree; function fee(MorphoHarness.Id) external returns uint256 envfree; function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; - function marketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; + function libId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; function isAuthorized(address, address) external returns bool envfree; function isLltvEnabled(uint256) external returns bool envfree; @@ -108,9 +108,9 @@ invariant borrowLessSupply(MorphoHarness.Id id) // This invariant is useful in the following rule, to link an id back to a market. invariant marketInvariant(MorphoHarness.MarketParams marketParams) - isCreated(marketId(marketParams)) => - idToBorrowable[marketId(marketParams)] == marketParams.borrowableToken && - idToCollateral[marketId(marketParams)] == marketParams.collateralToken; + isCreated(libId(marketParams)) => + idToBorrowable[libId(marketParams)] == marketParams.borrowableToken && + idToCollateral[libId(marketParams)] == marketParams.collateralToken; // Check that the idle amount on the singleton is greater to the sum amount, that is the sum over all the markets of the total supply plus the total collateral minus the total borrow. invariant isLiquid(address token) @@ -148,19 +148,19 @@ invariant isLiquid(address token) // Check that a market can only exist if its LLTV is enabled. invariant onlyEnabledLltv(MorphoHarness.MarketParams marketParams) - isCreated(marketId(marketParams)) => isLltvEnabled(marketParams.lltv); + isCreated(libId(marketParams)) => isLltvEnabled(marketParams.lltv); // Check that a market can only exist if its IRM is enabled. invariant onlyEnabledIrm(MorphoHarness.MarketParams marketParams) - isCreated(marketId(marketParams)) => isIrmEnabled(marketParams.irm); + isCreated(libId(marketParams)) => isIrmEnabled(marketParams.irm); // Check the pseudo-injectivity of the hashing function id(). -rule marketIdUnique() { +rule libIdUnique() { MorphoHarness.MarketParams marketParams1; MorphoHarness.MarketParams marketParams2; // Require the same arguments. - require marketId(marketParams1) == marketId(marketParams2); + require libId(marketParams1) == libId(marketParams2); assert marketParams1.borrowableToken == marketParams2.borrowableToken; assert marketParams1.collateralToken == marketParams2.collateralToken; diff --git a/certora/specs/ExactMath.spec b/certora/specs/ExactMath.spec index 8d94adfbc..d55385975 100644 --- a/certora/specs/ExactMath.spec +++ b/certora/specs/ExactMath.spec @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); - function marketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; + function libId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; function virtualTotalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; function virtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; function virtualTotalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree; @@ -26,8 +26,9 @@ function summaryMulDivDown(uint256 x, uint256 y, uint256 d) returns uint256 { return require_uint256((x * y) / d); } +// Check that when not accruing interest, and when repaying all, the borrow ratio is at least reset to the initial ratio. rule repayAllResetsBorrowRatio(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onbehalf, bytes data) { - MorphoHarness.Id id = marketId(marketParams); + MorphoHarness.Id id = libId(marketParams); require fee(id) <= maxFee(); mathint assetsBefore = virtualTotalBorrowAssets(id); @@ -44,12 +45,13 @@ rule repayAllResetsBorrowRatio(env e, MorphoHarness.MarketParams marketParams, u mathint sharesAfter = virtualTotalBorrowShares(id); assert assetsAfter == 1; + // There are at least as many shares as virtual shares. } // There should be no profit from supply followed immediately by withdraw. rule supplyWithdraw() { MorphoHarness.MarketParams marketParams; - MorphoHarness.Id id = marketId(marketParams); + MorphoHarness.Id id = libId(marketParams); uint256 assets; uint256 shares; address onbehalf; @@ -82,7 +84,7 @@ rule supplyWithdraw() { // There should be no profit from withdraw followed immediately by supply. rule withdrawSupply() { MorphoHarness.MarketParams marketParams; - MorphoHarness.Id id = marketId(marketParams); + MorphoHarness.Id id = libId(marketParams); uint256 assets; uint256 shares; address onbehalf; diff --git a/certora/specs/ExitLiquidity.spec b/certora/specs/ExitLiquidity.spec index a54a8e582..9f3fdd7bd 100644 --- a/certora/specs/ExitLiquidity.spec +++ b/certora/specs/ExitLiquidity.spec @@ -11,22 +11,22 @@ methods { function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; - function mathLibMulDivDown(uint256, uint256, uint256) external returns uint256 envfree; - function mathLibMulDivUp(uint256, uint256, uint256) external returns uint256 envfree; - function marketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; + function libMulDivDown(uint256, uint256, uint256) external returns uint256 envfree; + function libMulDivUp(uint256, uint256, uint256) external returns uint256 envfree; + function libId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; } // Check that it's not possible to withdraw more assets than what the user has supplied. rule withdrawLiquidity(MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) { env e; - MorphoHarness.Id id = marketId(marketParams); + MorphoHarness.Id id = libId(marketParams); require lastUpdate(id) == e.block.timestamp; uint256 initialShares = supplyShares(id, onBehalf); uint256 initialTotalSupply = virtualTotalSupplyAssets(id); uint256 initialTotalSupplyShares = virtualTotalSupplyShares(id); - uint256 owedAssets = mathLibMulDivDown(initialShares, initialTotalSupply, initialTotalSupplyShares); + uint256 owedAssets = libMulDivDown(initialShares, initialTotalSupply, initialTotalSupplyShares); uint256 withdrawnAssets; withdrawnAssets, _ = withdraw(e, marketParams, assets, shares, onBehalf, receiver); @@ -37,7 +37,7 @@ rule withdrawLiquidity(MorphoHarness.MarketParams marketParams, uint256 assets, // Check that it's not possible to withdraw more collateral than what the user has supplied. rule withdrawCollateralLiquidity(MorphoHarness.MarketParams marketParams, uint256 assets, address onBehalf, address receiver) { env e; - MorphoHarness.Id id = marketId(marketParams); + MorphoHarness.Id id = libId(marketParams); uint256 initialCollateral = collateral(id, onBehalf); @@ -49,14 +49,14 @@ rule withdrawCollateralLiquidity(MorphoHarness.MarketParams marketParams, uint25 // Check than when repaying the full outstanding debt requires more assets than what was borrowed. rule repayLiquidity(MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { env e; - MorphoHarness.Id id = marketId(marketParams); + MorphoHarness.Id id = libId(marketParams); require lastUpdate(id) == e.block.timestamp; uint256 initialShares = borrowShares(id, onBehalf); uint256 initialTotalBorrow = virtualTotalBorrowAssets(id); uint256 initialTotalBorrowShares = virtualTotalBorrowShares(id); - uint256 assetsDue = mathLibMulDivUp(initialShares, initialTotalBorrow, initialTotalBorrowShares); + uint256 assetsDue = libMulDivUp(initialShares, initialTotalBorrow, initialTotalBorrowShares); uint256 repaidAssets; repaidAssets, _ = repay(e, marketParams, assets, shares, onBehalf, data); diff --git a/certora/specs/Health.spec b/certora/specs/Health.spec index c68e3c43e..5db3c3724 100644 --- a/certora/specs/Health.spec +++ b/certora/specs/Health.spec @@ -7,7 +7,7 @@ methods { function collateral(MorphoHarness.Id, address) external returns uint256 envfree; function isHealthy(MorphoHarness.MarketParams, address user) external returns bool envfree; function isAuthorized(address, address user) external returns bool envfree; - function marketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; + function libId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; function _.price() external => mockPrice() expect uint256; function MathLib.mulDivDown(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivDown(a,b,c); @@ -52,7 +52,7 @@ filtered { } { MorphoHarness.MarketParams marketParams; - MorphoHarness.Id id = marketId(marketParams); + MorphoHarness.Id id = libId(marketParams); address user; // Require that the position is healthy before the interaction. @@ -78,7 +78,7 @@ rule healthyUserCannotLoseCollateral(env e, method f, calldataarg data) filtered { f -> !f.isView } { MorphoHarness.MarketParams marketParams; - MorphoHarness.Id id = marketId(marketParams); + MorphoHarness.Id id = libId(marketParams); address user; // Require that the e.msg.sender is not authorized. diff --git a/certora/specs/LibSummary.spec b/certora/specs/LibSummary.spec index 9adec259a..6b88f842d 100644 --- a/certora/specs/LibSummary.spec +++ b/certora/specs/LibSummary.spec @@ -1,26 +1,26 @@ // SPDX-License-Identifier: GPL-2.0-or-later methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); - function mathLibMulDivUp(uint256, uint256, uint256) external returns uint256 envfree; - function mathLibMulDivDown(uint256, uint256, uint256) external returns uint256 envfree; - function marketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; - function marketLibId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; + function libMulDivUp(uint256, uint256, uint256) external returns uint256 envfree; + function libMulDivDown(uint256, uint256, uint256) external returns uint256 envfree; + function libId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; + function optimizedId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; } // Check the summary of mulDivUp required by RatioMath.spec rule checkSummaryMulDivUp(uint256 x, uint256 y, uint256 d) { - uint256 result = mathLibMulDivUp(x, y, d); + uint256 result = libMulDivUp(x, y, d); assert result * d >= x * y; } // Check the summary of mulDivDown required by RatioMath.spec rule checkSummaryMulDivDown(uint256 x, uint256 y, uint256 d) { - uint256 result = mathLibMulDivDown(x, y, d); + uint256 result = libMulDivDown(x, y, d); assert result * d <= x * y; } // Check the munging of the MarketParams.id function. // This rule cannot be checked because it is not possible disable the keccak256 summary for the moment. // rule checkSummaryId(MorphoHarness.MarketParams marketParams) { -// assert marketLibId(marketParams) == marketId(marketParams); +// assert optimizedId(marketParams) == libId(marketParams); // } diff --git a/certora/specs/Liveness.spec b/certora/specs/Liveness.spec index 334a65fa4..d97bfedb2 100644 --- a/certora/specs/Liveness.spec +++ b/certora/specs/Liveness.spec @@ -10,7 +10,7 @@ methods { function collateral(MorphoInternalAccess.Id, address) external returns uint256 envfree; function fee(MorphoInternalAccess.Id) external returns uint256 envfree; function lastUpdate(MorphoInternalAccess.Id) external returns uint256 envfree; - function marketId(MorphoInternalAccess.MarketParams) external returns MorphoInternalAccess.Id envfree; + function libId(MorphoInternalAccess.MarketParams) external returns MorphoInternalAccess.Id envfree; function _._accrueInterest(MorphoInternalAccess.MarketParams memory marketParams, MorphoInternalAccess.Id id) internal with (env e) => summaryAccrueInterest(e, marketParams, id) expect void; @@ -51,7 +51,7 @@ definition isCreated(MorphoInternalAccess.Id id) returns bool = // Check that tokens and shares are properly accounted following a supply. rule supplyMovesTokensAndIncreasesShares(env e, MorphoInternalAccess.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { - MorphoInternalAccess.Id id = marketId(marketParams); + MorphoInternalAccess.Id id = libId(marketParams); // Safe require that Morpho is not the sender. require e.msg.sender != currentContract; @@ -77,7 +77,7 @@ rule supplyMovesTokensAndIncreasesShares(env e, MorphoInternalAccess.MarketParam // This rule is commented out for the moment because of a bug in CVL where market IDs are not consistent accross a run. // Check that one can always repay the debt in full. // rule canRepayAll(env e, MorphoInternalAccess.MarketParams marketParams, uint256 shares, bytes data) { -// MorphoInternalAccess.Id id = marketId(marketParams); +// MorphoInternalAccess.Id id = libId(marketParams); // require data.length == 0; @@ -97,7 +97,7 @@ rule supplyMovesTokensAndIncreasesShares(env e, MorphoInternalAccess.MarketParam // Check the one can always withdraw all, under the condition that there are no outstanding debt on the market. rule canWithdrawAll(env e, MorphoInternalAccess.MarketParams marketParams, uint256 shares, address receiver) { - MorphoInternalAccess.Id id = marketId(marketParams); + MorphoInternalAccess.Id id = libId(marketParams); // Require to ensure a withdraw all. require shares == supplyShares(id, e.msg.sender); @@ -122,7 +122,7 @@ rule canWithdrawAll(env e, MorphoInternalAccess.MarketParams marketParams, uint2 // Check that a user can always withdraw all, under the condition that this user does not have an outstanding debt. // Combined with the canRepayAll rule, this ensures that a borrower can always fully exit a market. rule canWithdrawCollateralAll(env e, MorphoInternalAccess.MarketParams marketParams, uint256 assets, address receiver) { - MorphoInternalAccess.Id id = marketId(marketParams); + MorphoInternalAccess.Id id = libId(marketParams); // Ensure a withdrawCollateral all. require assets == collateral(id, e.msg.sender); diff --git a/certora/specs/RatioMath.spec b/certora/specs/RatioMath.spec index 36d4520ca..0844e834f 100644 --- a/certora/specs/RatioMath.spec +++ b/certora/specs/RatioMath.spec @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); - function marketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; + function libId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; function virtualTotalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; function virtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; function virtualTotalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree; @@ -129,10 +129,10 @@ filtered { // Check that when not accruing interest, repay is decreasing the value of borrow shares. // Check the case where the market is not repaid fully. -// The other case requires exact math (ie not summarizing mulDivUp and mulDivDown), so it is checked separately in ExactMath.spec +// The other case requires exact math (ie not over-approximating mulDivUp and mulDivDown), so it is checked separately in ExactMath.spec rule repayDecreasesBorrowRatio(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { - MorphoHarness.Id id = marketId(marketParams); + MorphoHarness.Id id = libId(marketParams); requireInvariant feeInRange(id); mathint assetsBefore = virtualTotalBorrowAssets(id); diff --git a/certora/specs/Reentrancy.spec b/certora/specs/Reentrancy.spec index 0d009a898..82ebeabde 100644 --- a/certora/specs/Reentrancy.spec +++ b/certora/specs/Reentrancy.spec @@ -50,7 +50,7 @@ rule reentrancySafe(method f, calldataarg data, env e) { require !callIsBorrowRate; require !hasAccessedStorage && !hasCallAfterAccessingStorage && !hasReentrancyUnsafeCall; f(e,data); - assert !hasReentrancyUnsafeCall, "Method is not safe for reentrancy."; + assert !hasReentrancyUnsafeCall; } rule noDelegateCalls(method f, calldataarg data, env e) { diff --git a/certora/specs/Reverts.spec b/certora/specs/Reverts.spec index 5b9ef4d6f..7edcc6dce 100644 --- a/certora/specs/Reverts.spec +++ b/certora/specs/Reverts.spec @@ -13,7 +13,7 @@ methods { function isIrmEnabled(address) external returns bool envfree; function isAuthorized(address, address) external returns bool envfree; - function marketId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; + function libId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; function maxFee() external returns uint256 envfree; function wad() external returns uint256 envfree; } @@ -83,7 +83,7 @@ rule enableLltvRevertCondition(env e, uint256 lltv) { // Check that setFee reverts when its inputs are not validated. // setFee can also revert if the accrueInterest reverts. rule setFeeInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 newFee) { - MorphoHarness.Id id = marketId(marketParams); + MorphoHarness.Id id = libId(marketParams); address oldOwner = owner(); bool wasCreated = isCreated(id); setFee@withrevert(e, marketParams, newFee); @@ -101,7 +101,7 @@ rule setFeeRecipientRevertCondition(env e, address newFeeRecipient) { // Check the revert condition for the createMarket function. rule createMarketRevertCondition(env e, MorphoHarness.MarketParams marketParams) { - MorphoHarness.Id id = marketId(marketParams); + MorphoHarness.Id id = libId(marketParams); bool irmEnabled = isIrmEnabled(marketParams.irm); bool lltvEnabled = isLltvEnabled(marketParams.lltv); uint256 lastUpdated = lastUpdate(id); diff --git a/certora/specs/Transfer.spec b/certora/specs/Transfer.spec index 25c744e4c..c95da0999 100644 --- a/certora/specs/Transfer.spec +++ b/certora/specs/Transfer.spec @@ -32,7 +32,7 @@ rule checkTransferSummary(address token, address to, uint256 amount) { mathint initialBalance = balanceOf(token, currentContract); require to != currentContract => initialBalance + balanceOf(token, to) <= to_mathint(totalSupply(token)); - safeTransfer(token, to, amount); + libSafeTransfer(token, to, amount); mathint finalBalance = balanceOf(token, currentContract); require myBalances[token] == initialBalance; @@ -45,7 +45,7 @@ rule checkTransferFromSummary(address token, address from, uint256 amount) { mathint initialBalance = balanceOf(token, currentContract); require from != currentContract => initialBalance + balanceOf(token, from) <= to_mathint(totalSupply(token)); - safeTransferFrom(token, from, currentContract, amount); + libSafeTransferFrom(token, from, currentContract, amount); mathint finalBalance = balanceOf(token, currentContract); require myBalances[token] == initialBalance; @@ -60,7 +60,7 @@ rule transferRevertCondition(address token, address to, uint256 amount) { require to != currentContract => initialBalance + toInitialBalance <= to_mathint(totalSupply(token)); require currentContract != 0 && to != 0; - safeTransfer@withrevert(token, to, amount); + libSafeTransfer@withrevert(token, to, amount); assert lastReverted == (initialBalance < amount); } @@ -73,7 +73,7 @@ rule transferFromRevertCondition(address token, address from, address to, uint25 require to != from => initialBalance + toInitialBalance <= to_mathint(totalSupply(token)); require from != 0 && to != 0; - safeTransferFrom@withrevert(token, from, to, amount); + libSafeTransferFrom@withrevert(token, from, to, amount); assert lastReverted == (initialBalance < amount) || allowance < amount; } From f138fe33c55f59f4eb2697abaeb0139840d5fea2 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 5 Sep 2023 10:54:04 +0200 Subject: [PATCH 111/204] fix: safe transfer library functions names --- certora/specs/Transfer.spec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certora/specs/Transfer.spec b/certora/specs/Transfer.spec index c95da0999..665acc248 100644 --- a/certora/specs/Transfer.spec +++ b/certora/specs/Transfer.spec @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later methods { - function safeTransfer(address, address, uint256) external envfree; - function safeTransferFrom(address, address, address, uint256) external envfree; + function libSafeTransfer(address, address, uint256) external envfree; + function libSafeTransferFrom(address, address, address, uint256) external envfree; function balanceOf(address, address) external returns (uint256) envfree; function allowance(address, address, address) external returns (uint256) envfree; function totalSupply(address) external returns (uint256) envfree; From 662904d3f4d7806f3b1107280097f00002df3e1f Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 5 Sep 2023 12:47:15 +0200 Subject: [PATCH 112/204] feat: finish high level description --- certora/README.md | 49 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/certora/README.md b/certora/README.md index 7dabd675a..4a07cad15 100644 --- a/certora/README.md +++ b/certora/README.md @@ -1,19 +1,44 @@ -This folder defines the verification of the Morpho Blue protocol using CVL, Certora's specification language. +This folder defines the verification of the Morpho Blue protocol using CVL, Certora's verification language. ## High level description +The Morpho Blue protocol relies on a few different concepts that are described below. Those concepts have been verified using CVL, see the description of the specification files (or those files directly) for more details. + +The Morpho Blue protocol allows to take collateralized loans on ERC20 tokens. Transfers of tokens are verified to behave as expected, notably for the most common implementations.\ +Supplying on Morpho Blue entails to some interest, and this is implemented using the share mechanism. Notably, shares can increase in value when accruing interest. What is not borrowed stays on the contract.\ +Borrowing on Morpho Blue requires to deposit collateral. This collateral stays idle, and participates to the liquidity of the contract.\ +Markets on Morpho Blue depend on a pair of assets: the borrowable asset that is supplied and borrowed, and the collateral asset that is needed to be able to take a borrow position. Markets are sound, independent, and positions of users are also independent. In particular, loans cannot be impacted by loans from other markets.\ +To ensure proper collateralization, a liquidation system is put in place. It is verified that no unhealthy position can be created in a given block.\ +Morpho Blue also defines an authorization system that is sound: a user cannot modify the position of another user without the proper authorization (except when liquidating).\ +Other safety properties are verified, notably about reentrancy attacks and about input validation and revert conditions.\ +Other liveness properties are verified, notably it is always possible to exit a position, without concern for the external contracts such as the oracle. + ## Folder and files structure The [`certora/specs`](./specs) folder contains the following files: -- [AccrueInterest.spec](./specs/AccrueInterest.spec), checking that the main functions accrue interest at the start of the interaction. This is done by making sure that accruing interest before calling the function does not change the outcome. View functions do not necessarily respect this property (for example `totalSupplyShares`), and are filtered out. -- [ConsistentState.spec](./specs/ConsistentState.spec), checking that the state (storage) of the Morpho contract is consistent. This includes checking that the accounting of the total amount and shares is correct, that markets are independent from each other, that only enabled IRMs and LLTVs can be used, and that users cannot have their position made worse by an unauthorized account. -- [ExactMath.spec](./specs/ExactMath.spec), checking precise properties when taking into account the exact multiplication and division. Notably, this file specifies that doing using supply and withdraw in the same block cannot yield more funds than at the start. -- [ExitLiquidity.spec](./specs/ExitLiquidity.spec), checking that when exiting a position with witdraw, withdrawCollateral or repay, the user cannot get more than what was owed. -- [Health.spec](./specs/Health.spec), checking properties about the health of the positions. Notably, functions cannot render an account unhealthy, and debt positions at least have some collateral. -- [LibSummary.spec](./specs/LibSummary.spec), checking the summarization of the library functions that are used in other specification files. -- [Liveness.spec](./specs/Liveness.spec), checking that main functions change the owner of funds and the amount of shares as expected, and that it's always possible to exit a position. -- [RatioMath.spec](./specs/RatioMath.spec), checking that the ratio between shares and assets evolves predictably over time. -- [Reentrancy.spec](./specs/Reentrancy.spec), checking that the contract is immune to a particular class of reentrancy issues. -- [Reverts.spec](./specs/Reverts.spec), checking the condition for reverts and that inputs are correctly validated. -- [Transfer.spec](./specs/Transfer.spec), checking the summarization of the safe transfer library functions that are used in other specification files. +- [`AccrueInterest.spec`](./specs/AccrueInterest.spec), checking that the main functions accrue interest at the start of the interaction. This is done by making sure that accruing interest before calling the function does not change the outcome. View functions do not necessarily respect this property (for example `totalSupplyShares`), and are filtered out. +- [`ConsistentState.spec`](./specs/ConsistentState.spec), checking that the state (storage) of the Morpho contract is consistent. This includes checking that the accounting of the total amount and shares is correct, that markets are independent from each other, that only enabled IRMs and LLTVs can be used, and that users cannot have their position made worse by an unauthorized account. +- [`ExactMath.spec`](./specs/ExactMath.spec), checking precise properties when taking into account the exact multiplication and division. Notably, this file specifies that doing using supply and withdraw in the same block cannot yield more funds than at the start. +- [`ExitLiquidity.spec`](./specs/ExitLiquidity.spec), checking that when exiting a position with witdraw, withdrawCollateral or repay, the user cannot get more than what was owed. +- [`Health.spec`](./specs/Health.spec), checking properties about the health of the positions. Notably, functions cannot render an account unhealthy, and debt positions at least have some collateral. +- [`LibSummary.spec`](./specs/LibSummary.spec), checking the summarization of the library functions that are used in other specification files. +- [`Liveness.spec`](./specs/Liveness.spec), checking that main functions change the owner of funds and the amount of shares as expected, and that it's always possible to exit a position. +- [`RatioMath.spec`](./specs/RatioMath.spec), checking that the ratio between shares and assets evolves predictably over time. +- [`Reentrancy.spec`](./specs/Reentrancy.spec), checking that the contract is immune to a particular class of reentrancy issues. +- [`Reverts.spec`](./specs/Reverts.spec), checking the condition for reverts and that inputs are correctly validated. +- [`Transfer.spec`](./specs/Transfer.spec), checking the summarization of the safe transfer library functions that are used in other specification files. + +The [`certora/scripts`](./scripts/) folder contains a script for each corresponding specification file. + +The [`certora/harness`](./harness/) folder contains contracts that enable the verification of Morpho Blue. Notably, this allows to handle the fact that library functions should be called from a contract to be verified independently, and it allows to define needed getters. + +The [`certora/dispatch`](./dispatch/) folder contains different contracts similar to contracts that are expected to be called from Morpho Blue. + +## Usage + +Verify specification files by running the corresponding script in the [`certora/scripts`](./scripts/) folder. You can pass arguments to the script, which notable allows you to verify specific properties. For example, at the root of the repository: + +``` +./certora/scripts/verifyConsistentState.sh --rule borrowLessSupply +``` From 6981f8edc2fb72fb81b829f31e1bcb676d341861 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 5 Sep 2023 15:22:16 +0200 Subject: [PATCH 113/204] fix: grammar check on README --- certora/README.md | 87 +++++++++++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 37 deletions(-) diff --git a/certora/README.md b/certora/README.md index 4a07cad15..a7f3bcd4a 100644 --- a/certora/README.md +++ b/certora/README.md @@ -1,43 +1,56 @@ -This folder defines the verification of the Morpho Blue protocol using CVL, Certora's verification language. - -## High level description - -The Morpho Blue protocol relies on a few different concepts that are described below. Those concepts have been verified using CVL, see the description of the specification files (or those files directly) for more details. - -The Morpho Blue protocol allows to take collateralized loans on ERC20 tokens. Transfers of tokens are verified to behave as expected, notably for the most common implementations.\ -Supplying on Morpho Blue entails to some interest, and this is implemented using the share mechanism. Notably, shares can increase in value when accruing interest. What is not borrowed stays on the contract.\ -Borrowing on Morpho Blue requires to deposit collateral. This collateral stays idle, and participates to the liquidity of the contract.\ -Markets on Morpho Blue depend on a pair of assets: the borrowable asset that is supplied and borrowed, and the collateral asset that is needed to be able to take a borrow position. Markets are sound, independent, and positions of users are also independent. In particular, loans cannot be impacted by loans from other markets.\ -To ensure proper collateralization, a liquidation system is put in place. It is verified that no unhealthy position can be created in a given block.\ -Morpho Blue also defines an authorization system that is sound: a user cannot modify the position of another user without the proper authorization (except when liquidating).\ -Other safety properties are verified, notably about reentrancy attacks and about input validation and revert conditions.\ -Other liveness properties are verified, notably it is always possible to exit a position, without concern for the external contracts such as the oracle. - -## Folder and files structure - -The [`certora/specs`](./specs) folder contains the following files: - -- [`AccrueInterest.spec`](./specs/AccrueInterest.spec), checking that the main functions accrue interest at the start of the interaction. This is done by making sure that accruing interest before calling the function does not change the outcome. View functions do not necessarily respect this property (for example `totalSupplyShares`), and are filtered out. -- [`ConsistentState.spec`](./specs/ConsistentState.spec), checking that the state (storage) of the Morpho contract is consistent. This includes checking that the accounting of the total amount and shares is correct, that markets are independent from each other, that only enabled IRMs and LLTVs can be used, and that users cannot have their position made worse by an unauthorized account. -- [`ExactMath.spec`](./specs/ExactMath.spec), checking precise properties when taking into account the exact multiplication and division. Notably, this file specifies that doing using supply and withdraw in the same block cannot yield more funds than at the start. -- [`ExitLiquidity.spec`](./specs/ExitLiquidity.spec), checking that when exiting a position with witdraw, withdrawCollateral or repay, the user cannot get more than what was owed. -- [`Health.spec`](./specs/Health.spec), checking properties about the health of the positions. Notably, functions cannot render an account unhealthy, and debt positions at least have some collateral. -- [`LibSummary.spec`](./specs/LibSummary.spec), checking the summarization of the library functions that are used in other specification files. -- [`Liveness.spec`](./specs/Liveness.spec), checking that main functions change the owner of funds and the amount of shares as expected, and that it's always possible to exit a position. -- [`RatioMath.spec`](./specs/RatioMath.spec), checking that the ratio between shares and assets evolves predictably over time. -- [`Reentrancy.spec`](./specs/Reentrancy.spec), checking that the contract is immune to a particular class of reentrancy issues. -- [`Reverts.spec`](./specs/Reverts.spec), checking the condition for reverts and that inputs are correctly validated. -- [`Transfer.spec`](./specs/Transfer.spec), checking the summarization of the safe transfer library functions that are used in other specification files. - -The [`certora/scripts`](./scripts/) folder contains a script for each corresponding specification file. - -The [`certora/harness`](./harness/) folder contains contracts that enable the verification of Morpho Blue. Notably, this allows to handle the fact that library functions should be called from a contract to be verified independently, and it allows to define needed getters. - -The [`certora/dispatch`](./dispatch/) folder contains different contracts similar to contracts that are expected to be called from Morpho Blue. +This folder contains the verification of the Morpho Blue protocol using CVL, Certora's Verification Language. + +## High-Level Description + +The Morpho Blue protocol relies on several different concepts, which are described below. +These concepts have been verified using CVL. See the specification files (or those files directly) for more details.\ +The Morpho Blue protocol allows users to take out collateralized loans on ERC20 tokens. +Token transfers are verified to behave as expected, notably for the most common implementations.\ +Markets on Morpho Blue depend on a pair of assets: the borrowable asset that is supplied and borrowed, and the collateral asset that is needed to take a borrow position. +Markets are sound, independent, and positions of users are also independent. +In particular, loans cannot be impacted by loans from other markets.\ +When supplying on Morpho Blue, interest is earned over time, and implemented through a share mechanism. +Shares increase in value as interest is accrued.\ +To borrow on Morpho Blue, collateral must be deposited. +Collateral tokens remain idle and contribute to the liquidity of the contract, as well as any borrowable token that has not been borrowed.\ +To ensure proper collateralization, a liquidation system is put in place. +It is verified that no unhealthy position can be created in a given block.\ +Morpho Blue also defines a sound authorization system: users cannot modify positions of other users without proper authorization (except when liquidating).\ +Other safety properties are verified, particularly regarding reentrancy attacks, input validation and revert conditions.\ +Other liveness properties are verified as well, in particular it is always possible to exit a position without concern for external contracts such as the oracle. + +## Folder and File Structure + +The [`certora/specs`](./specs) folder contains the following files: + +- [`AccrueInterest.spec`](./specs/AccrueInterest.spec) checks that the main functions accrue interest at the start of the interaction. + This is done by ensuring that accruing interest before calling the function does not change the outcome. + View functions do not necessarily respect this property (for example, `totalSupplyShares`), and are filtered out. +- [`ConsistentState.spec`](./specs/ConsistentState.spec) checks that the state (storage) of the Morpho contract is consistent. + This includes checking that the accounting of the total amount and shares is correct, that markets are independent from each other, that only enabled IRMs and LLTVs can be used, and that users cannot have their position made worse by an unauthorized account. +- [`ExactMath.spec`](./specs/ExactMath.spec) checks precise properties when taking into account exact multiplication and division. + Notably, this file specifies that using supply and withdraw in the same block cannot yield more funds than at the start. +- [`ExitLiquidity.spec`](./specs/ExitLiquidity.spec) checks that when exiting a position with withdraw, withdrawCollateral, or repay, the user cannot get more than what was owed. +- [`Health.spec`](./specs/Health.spec) checks properties about the health of the positions. + Notably, functions cannot render an account unhealthy, and debt positions necessarily have some collateral. +- [`LibSummary.spec`](./specs/LibSummary.spec) checks the summarization of the library functions that are used in other specification files. +- [`Liveness.spec`](./specs/Liveness.spec) checks that main functions change the owner of funds and the amount of shares as expected, and that it's always possible to exit a position. +- [`RatioMath.spec`](./specs/RatioMath.spec) checks that the ratio between shares and assets evolves predictably over time. +- [`Reentrancy.spec`](./specs/Reentrancy.spec) checks that the contract is immune to a particular class of reentrancy issues. +- [`Reverts.spec`](./specs/Reverts.spec) checks the condition for reverts and that inputs are correctly validated. +- [`Transfer.spec`](./specs/Transfer.spec) checks the summarization of the safe transfer library functions that are used in other specification files. + +The [`certora/scripts`](./scripts) folder contains a script for each corresponding specification file. + +The [`certora/harness`](./harness) folder contains contracts that enable the verification of Morpho Blue. +Notably, this allows handling the fact that library functions should be called from a contract to be verified independently, and it allows defining needed getters. + +The [`certora/dispatch`](./dispatch) folder contains different contracts similar to contracts that are expected to be called from Morpho Blue. ## Usage -Verify specification files by running the corresponding script in the [`certora/scripts`](./scripts/) folder. You can pass arguments to the script, which notable allows you to verify specific properties. For example, at the root of the repository: +To verify specification files, run the corresponding script in the [`certora/scripts`](./scripts) folder. +You can pass arguments to the script, which allows you to verify specific properties. For example, at the root of the repository: ``` ./certora/scripts/verifyConsistentState.sh --rule borrowLessSupply From 6b4107ac85110aaa8a5e486a15f91c075e84f6f0 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 5 Sep 2023 15:56:37 +0200 Subject: [PATCH 114/204] docs: minor tweaks --- certora/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/certora/README.md b/certora/README.md index a7f3bcd4a..17e9f8ee1 100644 --- a/certora/README.md +++ b/certora/README.md @@ -3,7 +3,8 @@ This folder contains the verification of the Morpho Blue protocol using CVL, Cer ## High-Level Description The Morpho Blue protocol relies on several different concepts, which are described below. -These concepts have been verified using CVL. See the specification files (or those files directly) for more details.\ +These concepts have been verified using CVL. See the specification files (or those files directly) for more details. + The Morpho Blue protocol allows users to take out collateralized loans on ERC20 tokens. Token transfers are verified to behave as expected, notably for the most common implementations.\ Markets on Morpho Blue depend on a pair of assets: the borrowable asset that is supplied and borrowed, and the collateral asset that is needed to take a borrow position. @@ -50,6 +51,7 @@ The [`certora/dispatch`](./dispatch) folder contains different contracts simil ## Usage To verify specification files, run the corresponding script in the [`certora/scripts`](./scripts) folder. +It requires having set the `CERTORAKEY` environment variable to a valid Certora key. You can pass arguments to the script, which allows you to verify specific properties. For example, at the root of the repository: ``` From 5c6911c762323aa12f74dfd5c323f72f29c312d2 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 5 Sep 2023 17:02:10 +0200 Subject: [PATCH 115/204] fix: from review --- certora/README.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/certora/README.md b/certora/README.md index 17e9f8ee1..ae7ad5f14 100644 --- a/certora/README.md +++ b/certora/README.md @@ -6,26 +6,27 @@ The Morpho Blue protocol relies on several different concepts, which are describ These concepts have been verified using CVL. See the specification files (or those files directly) for more details. The Morpho Blue protocol allows users to take out collateralized loans on ERC20 tokens. -Token transfers are verified to behave as expected, notably for the most common implementations.\ -Markets on Morpho Blue depend on a pair of assets: the borrowable asset that is supplied and borrowed, and the collateral asset that is needed to take a borrow position. -Markets are sound, independent, and positions of users are also independent. -In particular, loans cannot be impacted by loans from other markets.\ +Token transfers are verified to behave as expected for the most common implementations, in particular the transferred amount is the amount passed as input.\ +Markets on Morpho Blue depend on a pair of assets: the borrowable asset that is supplied and borrowed, and the collateral asset. +Markets are independent: loans cannot be impacted by loans from other markets. +Positions of users are also independent: loans cannot be impacted by loans from other users. +The accounting of the markets has been verified (such as the total amounts), as well as the fact that only market with enabled parameters are created.\ When supplying on Morpho Blue, interest is earned over time, and implemented through a share mechanism. Shares increase in value as interest is accrued.\ To borrow on Morpho Blue, collateral must be deposited. -Collateral tokens remain idle and contribute to the liquidity of the contract, as well as any borrowable token that has not been borrowed.\ +Collateral tokens remain idle, as well as any borrowable token that has not been borrowed.\ To ensure proper collateralization, a liquidation system is put in place. It is verified that no unhealthy position can be created in a given block.\ Morpho Blue also defines a sound authorization system: users cannot modify positions of other users without proper authorization (except when liquidating).\ -Other safety properties are verified, particularly regarding reentrancy attacks, input validation and revert conditions.\ -Other liveness properties are verified as well, in particular it is always possible to exit a position without concern for external contracts such as the oracle. +Other safety properties are verified, particularly regarding reentrancy attacks and about input validation and revert conditions.\ +Other liveness properties are verified as well, in particular it is always possible to exit a position without concern for the oracle. ## Folder and File Structure The [`certora/specs`](./specs) folder contains the following files: - [`AccrueInterest.spec`](./specs/AccrueInterest.spec) checks that the main functions accrue interest at the start of the interaction. - This is done by ensuring that accruing interest before calling the function does not change the outcome. + This is done by ensuring that accruing interest before calling the function does not change the outcome compared to just calling the function. View functions do not necessarily respect this property (for example, `totalSupplyShares`), and are filtered out. - [`ConsistentState.spec`](./specs/ConsistentState.spec) checks that the state (storage) of the Morpho contract is consistent. This includes checking that the accounting of the total amount and shares is correct, that markets are independent from each other, that only enabled IRMs and LLTVs can be used, and that users cannot have their position made worse by an unauthorized account. @@ -33,7 +34,7 @@ The [`certora/specs`](./specs) folder contains the following files: Notably, this file specifies that using supply and withdraw in the same block cannot yield more funds than at the start. - [`ExitLiquidity.spec`](./specs/ExitLiquidity.spec) checks that when exiting a position with withdraw, withdrawCollateral, or repay, the user cannot get more than what was owed. - [`Health.spec`](./specs/Health.spec) checks properties about the health of the positions. - Notably, functions cannot render an account unhealthy, and debt positions necessarily have some collateral. + Notably, functions cannot render an account unhealthy, and debt positions always have some collateral (thanks to the bad debt realization mechanism). - [`LibSummary.spec`](./specs/LibSummary.spec) checks the summarization of the library functions that are used in other specification files. - [`Liveness.spec`](./specs/Liveness.spec) checks that main functions change the owner of funds and the amount of shares as expected, and that it's always possible to exit a position. - [`RatioMath.spec`](./specs/RatioMath.spec) checks that the ratio between shares and assets evolves predictably over time. @@ -46,7 +47,7 @@ The [`certora/scripts`](./scripts) folder contains a script for each correspon The [`certora/harness`](./harness) folder contains contracts that enable the verification of Morpho Blue. Notably, this allows handling the fact that library functions should be called from a contract to be verified independently, and it allows defining needed getters. -The [`certora/dispatch`](./dispatch) folder contains different contracts similar to contracts that are expected to be called from Morpho Blue. +The [`certora/dispatch`](./dispatch) folder contains different contracts similar to the ones that are expected to be called from Morpho Blue. ## Usage From 5325441a49e7beb6f4735c94209e2580de648139 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 5 Sep 2023 17:08:41 +0200 Subject: [PATCH 116/204] docs: add title to the high level description --- certora/README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/certora/README.md b/certora/README.md index ae7ad5f14..18d922fde 100644 --- a/certora/README.md +++ b/certora/README.md @@ -5,21 +5,21 @@ This folder contains the verification of the Morpho Blue protocol using CVL, Cer The Morpho Blue protocol relies on several different concepts, which are described below. These concepts have been verified using CVL. See the specification files (or those files directly) for more details. -The Morpho Blue protocol allows users to take out collateralized loans on ERC20 tokens. -Token transfers are verified to behave as expected for the most common implementations, in particular the transferred amount is the amount passed as input.\ -Markets on Morpho Blue depend on a pair of assets: the borrowable asset that is supplied and borrowed, and the collateral asset. -Markets are independent: loans cannot be impacted by loans from other markets. -Positions of users are also independent: loans cannot be impacted by loans from other users. +The Morpho Blue protocol allows users to take out collateralized loans on ERC20 tokens.\ +**Transfers.** Token transfers are verified to behave as expected for the most common implementations, in particular the transferred amount is the amount passed as input.\ +**Markets**. Markets on Morpho Blue depend on a pair of assets, the borrowable asset that is supplied and borrowed, and the collateral asset. +Markets are independent, which means that loans cannot be impacted by loans from other markets. +Positions of users are also independent, so loans cannot be impacted by loans from other users. The accounting of the markets has been verified (such as the total amounts), as well as the fact that only market with enabled parameters are created.\ -When supplying on Morpho Blue, interest is earned over time, and implemented through a share mechanism. +**Supply.** When supplying on Morpho Blue, interest is earned over time, and implemented through a share mechanism. Shares increase in value as interest is accrued.\ -To borrow on Morpho Blue, collateral must be deposited. +**Borrow.** To borrow on Morpho Blue, collateral must be deposited. Collateral tokens remain idle, as well as any borrowable token that has not been borrowed.\ -To ensure proper collateralization, a liquidation system is put in place. +**Liquidation.** To ensure proper collateralization, a liquidation system is put in place. It is verified that no unhealthy position can be created in a given block.\ -Morpho Blue also defines a sound authorization system: users cannot modify positions of other users without proper authorization (except when liquidating).\ -Other safety properties are verified, particularly regarding reentrancy attacks and about input validation and revert conditions.\ -Other liveness properties are verified as well, in particular it is always possible to exit a position without concern for the oracle. +**Authorization.** Morpho Blue also defines a sound authorization system where users cannot modify positions of other users without proper authorization (except when liquidating).\ +**Safety.** Other safety properties are verified, particularly regarding reentrancy attacks and about input validation and revert conditions.\ +**Liveness.** Other liveness properties are verified as well, in particular it is always possible to exit a position without concern for the oracle. ## Folder and File Structure From c04151a525cd55a28b704aa1010302c67aeb907f Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 5 Sep 2023 17:19:46 +0200 Subject: [PATCH 117/204] fix: small rewording --- certora/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/certora/README.md b/certora/README.md index 18d922fde..edae44543 100644 --- a/certora/README.md +++ b/certora/README.md @@ -3,7 +3,8 @@ This folder contains the verification of the Morpho Blue protocol using CVL, Cer ## High-Level Description The Morpho Blue protocol relies on several different concepts, which are described below. -These concepts have been verified using CVL. See the specification files (or those files directly) for more details. +These concepts have been verified using CVL. +See the description of the specification files below (or those files directly) for more details. The Morpho Blue protocol allows users to take out collateralized loans on ERC20 tokens.\ **Transfers.** Token transfers are verified to behave as expected for the most common implementations, in particular the transferred amount is the amount passed as input.\ @@ -11,7 +12,7 @@ The Morpho Blue protocol allows users to take out collateralized loans on ERC20 Markets are independent, which means that loans cannot be impacted by loans from other markets. Positions of users are also independent, so loans cannot be impacted by loans from other users. The accounting of the markets has been verified (such as the total amounts), as well as the fact that only market with enabled parameters are created.\ -**Supply.** When supplying on Morpho Blue, interest is earned over time, and implemented through a share mechanism. +**Supply.** When supplying on Morpho Blue, interest is earned over time, and the distribution is implemented through a shares mechanism. Shares increase in value as interest is accrued.\ **Borrow.** To borrow on Morpho Blue, collateral must be deposited. Collateral tokens remain idle, as well as any borrowable token that has not been borrowed.\ From 7d0d5f603c34c3298b4b213cc80034a0c50ec4c2 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 5 Sep 2023 17:58:18 +0200 Subject: [PATCH 118/204] docs: more explicity comment about health after an interaction --- certora/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certora/README.md b/certora/README.md index edae44543..48e987103 100644 --- a/certora/README.md +++ b/certora/README.md @@ -17,7 +17,7 @@ Shares increase in value as interest is accrued.\ **Borrow.** To borrow on Morpho Blue, collateral must be deposited. Collateral tokens remain idle, as well as any borrowable token that has not been borrowed.\ **Liquidation.** To ensure proper collateralization, a liquidation system is put in place. -It is verified that no unhealthy position can be created in a given block.\ +In the absence of accrued interest, for example when creating a new position or when interacting multiple times in the same block, a position cannot be made unhealthy.\ **Authorization.** Morpho Blue also defines a sound authorization system where users cannot modify positions of other users without proper authorization (except when liquidating).\ **Safety.** Other safety properties are verified, particularly regarding reentrancy attacks and about input validation and revert conditions.\ **Liveness.** Other liveness properties are verified as well, in particular it is always possible to exit a position without concern for the oracle. From c9285476fc99bcea7a3f66171ae55e4bacbb798c Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 7 Sep 2023 14:51:37 +0200 Subject: [PATCH 119/204] feat: complete liveness for all the main functions --- certora/specs/Liveness.spec | 121 +++++++++++++++++++++++++++++++++++- 1 file changed, 118 insertions(+), 3 deletions(-) diff --git a/certora/specs/Liveness.spec b/certora/specs/Liveness.spec index d97bfedb2..9126b1fd0 100644 --- a/certora/specs/Liveness.spec +++ b/certora/specs/Liveness.spec @@ -50,11 +50,11 @@ definition isCreated(MorphoInternalAccess.Id id) returns bool = lastUpdate(id) != 0; // Check that tokens and shares are properly accounted following a supply. -rule supplyMovesTokensAndIncreasesShares(env e, MorphoInternalAccess.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { +rule supplyChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { MorphoInternalAccess.Id id = libId(marketParams); - // Safe require that Morpho is not the sender. - require e.msg.sender != currentContract; + // Require that Morpho is not the sender. + require currentContract != e.msg.sender; // Ensure that no interest is accumulated. require lastUpdate(id) == e.block.timestamp; @@ -74,6 +74,121 @@ rule supplyMovesTokensAndIncreasesShares(env e, MorphoInternalAccess.MarketParam assert balanceAfter == balanceBefore + suppliedAssets; } +// Check that tokens and shares are properly accounted following a withdraw. +rule withdrawChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) { + MorphoInternalAccess.Id id = libId(marketParams); + + // Require that Morpho is not the receiver. + require currentContract != receiver; + // Ensure that no interest is accumulated. + require lastUpdate(id) == e.block.timestamp; + + mathint sharesBefore = supplyShares(id, onBehalf); + mathint balanceBefore = myBalances[marketParams.borrowableToken]; + + uint256 withdrawnAssets; + uint256 withdrawnShares; + withdrawnAssets, withdrawnShares = withdraw(e, marketParams, assets, shares, onBehalf, receiver); + + mathint sharesAfter = supplyShares(id, onBehalf); + mathint balanceAfter = myBalances[marketParams.borrowableToken]; + + assert assets != 0 => withdrawnAssets == assets; + assert assets == 0 => withdrawnShares == shares; + assert sharesAfter == sharesBefore - withdrawnShares; + assert balanceAfter == balanceBefore - withdrawnAssets; +} + +// Check that tokens and shares are properly accounted following a borrow. +rule borrowChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) { + MorphoInternalAccess.Id id = libId(marketParams); + + // Require that Morpho is not the receiver. + require currentContract != receiver; + // Ensure that no interest is accumulated. + require lastUpdate(id) == e.block.timestamp; + + mathint sharesBefore = borrowShares(id, onBehalf); + mathint balanceBefore = myBalances[marketParams.borrowableToken]; + + uint256 borrowedAssets; + uint256 borrowedShares; + borrowedAssets, borrowedShares = borrow(e, marketParams, assets, shares, onBehalf, receiver); + + mathint sharesAfter = borrowShares(id, onBehalf); + mathint balanceAfter = myBalances[marketParams.borrowableToken]; + + assert assets != 0 => borrowedAssets == assets; + assert assets == 0 => borrowedShares == shares; + assert sharesAfter == sharesBefore + borrowedShares; + assert balanceAfter == balanceBefore - borrowedAssets; +} + +// Check that tokens and shares are properly accounted following a repay. +rule repayChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { + MorphoInternalAccess.Id id = libId(marketParams); + + // Require that Morpho is not the sender. + require currentContract != e.msg.sender; + // Ensure that no interest is accumulated. + require lastUpdate(id) == e.block.timestamp; + + mathint sharesBefore = borrowShares(id, onBehalf); + mathint balanceBefore = myBalances[marketParams.borrowableToken]; + + uint256 repaidAssets; + uint256 repaidShares; + repaidAssets, repaidShares = repay(e, marketParams, assets, shares, onBehalf, data); + + mathint sharesAfter = borrowShares(id, onBehalf); + mathint balanceAfter = myBalances[marketParams.borrowableToken]; + + assert assets != 0 => repaidAssets == assets; + assert assets == 0 => repaidShares == shares; + assert sharesAfter == sharesBefore - repaidShares; + assert balanceAfter == balanceBefore + repaidAssets; +} + +// Check that tokens and balances are properly accounted following a supplyCollateral. +rule supplyCollateralChangesTokensAndBalance(env e, MorphoInternalAccess.MarketParams marketParams, uint256 assets, address onBehalf, bytes data) { + MorphoInternalAccess.Id id = libId(marketParams); + + // Require that Morpho is not the sender. + require currentContract != e.msg.sender; + + mathint collateralBefore = collateral(id, onBehalf); + mathint balanceBefore = myBalances[marketParams.collateralToken]; + + supplyCollateral(e, marketParams, assets, onBehalf, data); + + mathint collateralAfter = collateral(id, onBehalf); + mathint balanceAfter = myBalances[marketParams.collateralToken]; + + assert collateralAfter == collateralBefore + assets; + assert balanceAfter == balanceBefore + assets; +} + +// Check that tokens and balances are properly accounted following a withdrawCollateral. +rule withdrawCollateralChangesTokensAndBalance(env e, MorphoInternalAccess.MarketParams marketParams, uint256 assets, address onBehalf, address receiver) { + MorphoInternalAccess.Id id = libId(marketParams); + + // Require that Morpho is not the receiver. + require currentContract != receiver; + // Ensure that no interest is accumulated. + require lastUpdate(id) == e.block.timestamp; + + mathint collateralBefore = collateral(id, onBehalf); + mathint balanceBefore = myBalances[marketParams.collateralToken]; + + withdrawCollateral(e, marketParams, assets, onBehalf, receiver); + + mathint collateralAfter = collateral(id, onBehalf); + mathint balanceAfter = myBalances[marketParams.collateralToken]; + + assert collateralAfter == collateralBefore - assets; + assert balanceAfter == balanceBefore - assets; +} + // This rule is commented out for the moment because of a bug in CVL where market IDs are not consistent accross a run. // Check that one can always repay the debt in full. // rule canRepayAll(env e, MorphoInternalAccess.MarketParams marketParams, uint256 shares, bytes data) { From 2f64b480a3b8e585b845b7155ad5e23c632a4c67 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 7 Sep 2023 18:31:05 +0200 Subject: [PATCH 120/204] docs: improve revert documentation --- certora/specs/AccrueInterest.spec | 8 +++++-- certora/specs/ConsistentState.spec | 22 +++++++++++++------ certora/specs/ExactMath.spec | 14 ++++++++---- certora/specs/ExitLiquidity.spec | 3 +++ certora/specs/Health.spec | 14 ++++++------ certora/specs/Liveness.spec | 34 +++++++++++++++++------------- certora/specs/RatioMath.spec | 8 +++---- certora/specs/Reentrancy.spec | 3 +++ certora/specs/Reverts.spec | 5 +++++ certora/specs/Transfer.spec | 8 +++++++ 10 files changed, 81 insertions(+), 38 deletions(-) diff --git a/certora/specs/AccrueInterest.spec b/certora/specs/AccrueInterest.spec index 3d578caa8..3af214790 100644 --- a/certora/specs/AccrueInterest.spec +++ b/certora/specs/AccrueInterest.spec @@ -32,6 +32,7 @@ ghost ghostTransferFrom(address, address, uint256) returns bool; // Check that calling accrueInterest first has no effect. // This is because supply should call accrueInterest itself. rule supplyAccruesInterest(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { + // Safe require because timestamps cannot realistically be that large. require e.block.timestamp < 2^128; storage init = lastStorage; @@ -49,6 +50,7 @@ rule supplyAccruesInterest(env e, MorphoHarness.MarketParams marketParams, uint2 // Check that calling accrueInterest first has no effect. // This is because withdraw should call accrueInterest itself. rule withdrawAccruesInterest(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) { + // Safe require because timestamps cannot realistically be that large. require e.block.timestamp < 2^128; storage init = lastStorage; @@ -66,6 +68,7 @@ rule withdrawAccruesInterest(env e, MorphoHarness.MarketParams marketParams, uin // Check that calling accrueInterest first has no effect. // This is because borrow should call accrueInterest itself. rule borrowAccruesInterest(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) { + // Safe require because timestamps cannot realistically be that large. require e.block.timestamp < 2^128; storage init = lastStorage; @@ -83,6 +86,7 @@ rule borrowAccruesInterest(env e, MorphoHarness.MarketParams marketParams, uint2 // Check that calling accrueInterest first has no effect. // This is because repay should call accrueInterest itself. rule repayAccruesInterest(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { + // Safe require because timestamps cannot realistically be that large. require e.block.timestamp < 2^128; storage init = lastStorage; @@ -111,9 +115,9 @@ filtered { env e2; MorphoHarness.MarketParams marketParams; - // Require interactions to happen at the same block. + // Assume interactions to happen at the same block. require e1.block.timestamp == e2.block.timestamp; - // Assumption required to cast a timestamp to uint128. + // Safe require because timestamps cannot realistically be that large. require e1.block.timestamp < 2^128; storage init = lastStorage; diff --git a/certora/specs/ConsistentState.spec b/certora/specs/ConsistentState.spec index 8915e0301..a0d91965d 100644 --- a/certora/specs/ConsistentState.spec +++ b/certora/specs/ConsistentState.spec @@ -19,6 +19,7 @@ methods { function _.borrowRate(MorphoHarness.MarketParams, MorphoHarness.Market) external => HAVOC_ECF; function maxFee() external returns uint256 envfree; + function wad() external returns uint256 envfree; function SafeTransferLib.safeTransfer(address token, address to, uint256 value) internal => summarySafeTransferFrom(token, currentContract, to, value); function SafeTransferLib.safeTransferFrom(address token, address from, address to, uint256 value) internal => summarySafeTransferFrom(token, from, to, value); @@ -79,9 +80,11 @@ hook Sstore market[KEY MorphoHarness.Id id].totalBorrowAssets uint128 newAmount function summarySafeTransferFrom(address token, address from, address to, uint256 amount) { if (from == currentContract) { + // Safe require because the reference implementation would revert. myBalances[token] = require_uint256(myBalances[token] - amount); } if (to == currentContract) { + // Safe require because the reference implementation would revert. myBalances[token] = require_uint256(myBalances[token] + amount); } } @@ -116,6 +119,7 @@ invariant marketInvariant(MorphoHarness.MarketParams marketParams) invariant isLiquid(address token) sumAmount[token] <= myBalances[token] { + // Safe requires on the sender because the contract cannot call the function itself. preserved supply(MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) with (env e) { requireInvariant marketInvariant(marketParams); require e.msg.sender != currentContract; @@ -150,6 +154,9 @@ invariant isLiquid(address token) invariant onlyEnabledLltv(MorphoHarness.MarketParams marketParams) isCreated(libId(marketParams)) => isLltvEnabled(marketParams.lltv); +invariant lltvSmallerThanWad(uint256 lltv) + isLltvEnabled(lltv) => lltv < wad(); + // Check that a market can only exist if its IRM is enabled. invariant onlyEnabledIrm(MorphoHarness.MarketParams marketParams) isCreated(libId(marketParams)) => isIrmEnabled(marketParams.irm); @@ -159,7 +166,7 @@ rule libIdUnique() { MorphoHarness.MarketParams marketParams1; MorphoHarness.MarketParams marketParams2; - // Require the same arguments. + // Assume that arguments are the same. require libId(marketParams1) == libId(marketParams2); assert marketParams1.borrowableToken == marketParams2.borrowableToken; @@ -178,7 +185,7 @@ filtered { address user; address someone; - // Require a different user to interact with Morpho. + // Assume that it is another user that is interacting with Morpho. require user != e.msg.sender; bool authorizedBefore = isAuthorized(user, someone); @@ -197,7 +204,7 @@ filtered { f -> !f.isView } MorphoHarness.Id id; address user; - // Require that the e.msg.sender is not authorized. + // Assume that the e.msg.sender is not authorized. require !isAuthorized(user, e.msg.sender); require user != e.msg.sender; @@ -217,7 +224,7 @@ filtered { f -> !f.isView } MorphoHarness.Id id; address user; - // Require that the e.msg.sender is not authorized. + // Assume that the e.msg.sender is not authorized. require !isAuthorized(user, e.msg.sender); require user != e.msg.sender; @@ -240,7 +247,7 @@ filtered { MorphoHarness.Id id; address user; - // Require that the e.msg.sender is not authorized. + // Assume that the e.msg.sender is not authorized. require !isAuthorized(user, e.msg.sender); require user != e.msg.sender; @@ -260,10 +267,10 @@ filtered { f -> !f.isView } MorphoHarness.Id id; address user; - // Require that the e.msg.sender is not authorized. + // Assume that the e.msg.sender is not authorized. require !isAuthorized(user, e.msg.sender); require user != e.msg.sender; - // Require that the user has no outstanding debt. + // Assume that the user has no outstanding debt. require borrowShares(id, user) == 0; mathint collateralBefore = collateral(id, user); @@ -280,6 +287,7 @@ rule noTimeTravel(method f, env e, calldataarg args) filtered { f -> !f.isView } { MorphoHarness.Id id; + // Assume the property before the interaction. require lastUpdate(id) <= e.block.timestamp; f(e, args); assert lastUpdate(id) <= e.block.timestamp; diff --git a/certora/specs/ExactMath.spec b/certora/specs/ExactMath.spec index d55385975..3ca2c3736 100644 --- a/certora/specs/ExactMath.spec +++ b/certora/specs/ExactMath.spec @@ -19,26 +19,32 @@ methods { } function summaryMulDivUp(uint256 x, uint256 y, uint256 d) returns uint256 { + // Safe require because the reference implementation would revert. return require_uint256((x * y + (d - 1)) / d); } function summaryMulDivDown(uint256 x, uint256 y, uint256 d) returns uint256 { + // Safe require because the reference implementation would revert. return require_uint256((x * y) / d); } // Check that when not accruing interest, and when repaying all, the borrow ratio is at least reset to the initial ratio. +// More details on the purpose of this rule in RatioMath.spec. rule repayAllResetsBorrowRatio(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onbehalf, bytes data) { MorphoHarness.Id id = libId(marketParams); + // Safe require because this invariant is checked in ConsistentState.spec require fee(id) <= maxFee(); mathint assetsBefore = virtualTotalBorrowAssets(id); mathint sharesBefore = virtualTotalBorrowShares(id); + // Assume no interest as it would increase the borrowed assets. require lastUpdate(id) == e.block.timestamp; mathint repaidAssets; repaidAssets, _ = repay(e, marketParams, assets, shares, onbehalf, data); + // Check the case where the market is fully repaid. require repaidAssets >= assetsBefore; mathint assetsAfter = virtualTotalBorrowAssets(id); @@ -60,9 +66,9 @@ rule supplyWithdraw() { env e1; env e2; - // Require interactions to happen at the same block. + // Assume that interactions to happen at the same block. require e1.block.timestamp == e2.block.timestamp; - // Assumption required to cast timestamps to uint128. + // Safe require because timestamps cannot realistically be that large. require e1.block.timestamp < 2^128; uint256 suppliedAssets; @@ -93,9 +99,9 @@ rule withdrawSupply() { env e1; env e2; - // Require interactions to happen at the same block. + // Assume interactions to happen at the same block. require e1.block.timestamp == e2.block.timestamp; - // Assumption required to cast timestamps to uint128. + // Safe require because timestamps cannot realistically be that large. require e1.block.timestamp < 2^128; uint256 withdrawnAssets; diff --git a/certora/specs/ExitLiquidity.spec b/certora/specs/ExitLiquidity.spec index 9f3fdd7bd..30036108b 100644 --- a/certora/specs/ExitLiquidity.spec +++ b/certora/specs/ExitLiquidity.spec @@ -21,6 +21,7 @@ rule withdrawLiquidity(MorphoHarness.MarketParams marketParams, uint256 assets, env e; MorphoHarness.Id id = libId(marketParams); + // Assume no interest as it would increase the total supply assets. require lastUpdate(id) == e.block.timestamp; uint256 initialShares = supplyShares(id, onBehalf); @@ -51,6 +52,7 @@ rule repayLiquidity(MorphoHarness.MarketParams marketParams, uint256 assets, uin env e; MorphoHarness.Id id = libId(marketParams); + // Assume no interest as it would increase the total borrowed assets. require lastUpdate(id) == e.block.timestamp; uint256 initialShares = borrowShares(id, onBehalf); @@ -61,6 +63,7 @@ rule repayLiquidity(MorphoHarness.MarketParams marketParams, uint256 assets, uin uint256 repaidAssets; repaidAssets, _ = repay(e, marketParams, assets, shares, onBehalf, data); + // Assume a full repay. require borrowShares(id, onBehalf) == 0; assert repaidAssets >= assetsDue; diff --git a/certora/specs/Health.spec b/certora/specs/Health.spec index 5db3c3724..2d9a9858e 100644 --- a/certora/specs/Health.spec +++ b/certora/specs/Health.spec @@ -28,11 +28,13 @@ function mockPrice() returns uint256 { } function summaryMulDivUp(uint256 x, uint256 y, uint256 d) returns uint256 { + // Safe requires because the reference implementation would revert. require d != 0; return require_uint256((x * y + (d - 1)) / d); } function summaryMulDivDown(uint256 x, uint256 y, uint256 d) returns uint256 { + // Safe requires because the reference implementation would revert. require d != 0; return require_uint256((x * y) / d); } @@ -55,12 +57,12 @@ filtered { MorphoHarness.Id id = libId(marketParams); address user; - // Require that the position is healthy before the interaction. + // Assume that the position is healthy before the interaction. require isHealthy(marketParams, user); - // Require that the LLTV takes coherent values. + // Safe require because of the invariants onlyEnabledLltv and lltvSmallerThanWad in ConsistentState.spec. require marketParams.lltv < 10^18; require marketParams.lltv > 0; - // Ensure that no interest is accumulated. + // Assumption to ensure that no interest is accumulated. require lastUpdate(id) == e.block.timestamp; priceChanged = false; @@ -81,12 +83,12 @@ filtered { f -> !f.isView } MorphoHarness.Id id = libId(marketParams); address user; - // Require that the e.msg.sender is not authorized. + // Assume that the e.msg.sender is not authorized. require !isAuthorized(user, e.msg.sender); require user != e.msg.sender; - // Ensure that no interest is accumulated. + // Assumption to ensure that no interest is accumulated. require lastUpdate(id) == e.block.timestamp; - // Require that the user is healthy. + // Assume that the user is healthy. require isHealthy(marketParams, user); mathint collateralBefore = collateral(id, user); diff --git a/certora/specs/Liveness.spec b/certora/specs/Liveness.spec index 9126b1fd0..02698bcdc 100644 --- a/certora/specs/Liveness.spec +++ b/certora/specs/Liveness.spec @@ -24,20 +24,24 @@ ghost mapping(address => mathint) myBalances { function summarySafeTransferFrom(address token, address from, address to, uint256 amount) { if (from == currentContract) { + // Safe require because the reference implementation would revert. myBalances[token] = require_uint256(myBalances[token] - amount); } if (to == currentContract) { + // Safe require because the reference implementation would revert. myBalances[token] = require_uint256(myBalances[token] + amount); } } // Assume no fee. function summaryAccrueInterest(env e, MorphoInternalAccess.MarketParams marketParams, MorphoInternalAccess.Id id) { + // Safe require because timestamps cannot realistically be that large. require e.block.timestamp < 2^128; if (e.block.timestamp != lastUpdate(id) && totalBorrowAssets(id) != 0) { uint128 interest; uint256 borrow = totalBorrowAssets(id); uint256 supply = totalSupplyAssets(id); + // Safe requires because the reference implementation would revert. require interest + borrow < 2^256; require interest + supply < 2^256; increaseInterest(e, id, interest); @@ -53,9 +57,9 @@ definition isCreated(MorphoInternalAccess.Id id) returns bool = rule supplyChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { MorphoInternalAccess.Id id = libId(marketParams); - // Require that Morpho is not the sender. + // Safe require because Morpho cannot call such functions by itself. require currentContract != e.msg.sender; - // Ensure that no interest is accumulated. + // Assumption to ensure that no interest is accumulated. require lastUpdate(id) == e.block.timestamp; mathint sharesBefore = supplyShares(id, onBehalf); @@ -78,9 +82,9 @@ rule supplyChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams marke rule withdrawChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) { MorphoInternalAccess.Id id = libId(marketParams); - // Require that Morpho is not the receiver. + // Assume that Morpho is not the receiver. require currentContract != receiver; - // Ensure that no interest is accumulated. + // Assumption to ensure that no interest is accumulated. require lastUpdate(id) == e.block.timestamp; mathint sharesBefore = supplyShares(id, onBehalf); @@ -103,9 +107,9 @@ rule withdrawChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams mar rule borrowChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) { MorphoInternalAccess.Id id = libId(marketParams); - // Require that Morpho is not the receiver. + // Assume that Morpho is not the receiver. require currentContract != receiver; - // Ensure that no interest is accumulated. + // Assumption to ensure that no interest is accumulated. require lastUpdate(id) == e.block.timestamp; mathint sharesBefore = borrowShares(id, onBehalf); @@ -128,9 +132,9 @@ rule borrowChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams marke rule repayChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { MorphoInternalAccess.Id id = libId(marketParams); - // Require that Morpho is not the sender. + // Safe require because Morpho cannot call such functions by itself. require currentContract != e.msg.sender; - // Ensure that no interest is accumulated. + // Assumption to ensure that no interest is accumulated. require lastUpdate(id) == e.block.timestamp; mathint sharesBefore = borrowShares(id, onBehalf); @@ -153,7 +157,7 @@ rule repayChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams market rule supplyCollateralChangesTokensAndBalance(env e, MorphoInternalAccess.MarketParams marketParams, uint256 assets, address onBehalf, bytes data) { MorphoInternalAccess.Id id = libId(marketParams); - // Require that Morpho is not the sender. + // Safe require because Morpho cannot call such functions by itself. require currentContract != e.msg.sender; mathint collateralBefore = collateral(id, onBehalf); @@ -172,9 +176,9 @@ rule supplyCollateralChangesTokensAndBalance(env e, MorphoInternalAccess.MarketP rule withdrawCollateralChangesTokensAndBalance(env e, MorphoInternalAccess.MarketParams marketParams, uint256 assets, address onBehalf, address receiver) { MorphoInternalAccess.Id id = libId(marketParams); - // Require that Morpho is not the receiver. + // Assume that Morpho is not the receiver. require currentContract != receiver; - // Ensure that no interest is accumulated. + // Assumption to ensure that no interest is accumulated. require lastUpdate(id) == e.block.timestamp; mathint collateralBefore = collateral(id, onBehalf); @@ -214,7 +218,7 @@ rule withdrawCollateralChangesTokensAndBalance(env e, MorphoInternalAccess.Marke rule canWithdrawAll(env e, MorphoInternalAccess.MarketParams marketParams, uint256 shares, address receiver) { MorphoInternalAccess.Id id = libId(marketParams); - // Require to ensure a withdraw all. + // Assume a full withdraw. require shares == supplyShares(id, e.msg.sender); // Omit sanity checks. require isCreated(id); @@ -222,7 +226,7 @@ rule canWithdrawAll(env e, MorphoInternalAccess.MarketParams marketParams, uint2 require receiver != 0; require e.msg.value == 0; require shares > 0; - // Require no outstanding debt on the market. + // Assume no outstanding debt on the market. require totalBorrowAssets(id) == 0; // Safe require because of the noTimeTravel rule. require lastUpdate(id) <= e.block.timestamp; @@ -239,7 +243,7 @@ rule canWithdrawAll(env e, MorphoInternalAccess.MarketParams marketParams, uint2 rule canWithdrawCollateralAll(env e, MorphoInternalAccess.MarketParams marketParams, uint256 assets, address receiver) { MorphoInternalAccess.Id id = libId(marketParams); - // Ensure a withdrawCollateral all. + // Ensure a full withdrawCollateral. require assets == collateral(id, e.msg.sender); // Omit sanity checks. require isCreated(id); @@ -248,7 +252,7 @@ rule canWithdrawCollateralAll(env e, MorphoInternalAccess.MarketParams marketPar require assets > 0; // Safe require because of the noTimeTravel rule. require lastUpdate(id) <= e.block.timestamp; - // Require that the user does not have an outstanding debt. + // Assume that the user does not have an outstanding debt. require borrowShares(id, e.msg.sender) == 0; withdrawCollateral@withrevert(e, marketParams, assets, e.msg.sender, receiver); diff --git a/certora/specs/RatioMath.spec b/certora/specs/RatioMath.spec index 0844e834f..21dc1050a 100644 --- a/certora/specs/RatioMath.spec +++ b/certora/specs/RatioMath.spec @@ -22,17 +22,17 @@ invariant feeInRange(MorphoHarness.Id id) fee(id) <= maxFee(); // This is a simple overapproximative summary, stating that it rounds in the right direction. -// The summary is checked by the specification in LibSummary.spec. function summaryMulDivUp(uint256 x, uint256 y, uint256 d) returns uint256 { uint256 result; + // Safe require that is checked by the specification in LibSummary.spec. require result * d >= x * y; return result; } // This is a simple overapproximative summary, stating that it rounds in the right direction. -// The summary is checked by the specification in LibSummary.spec. function summaryMulDivDown(uint256 x, uint256 y, uint256 d) returns uint256 { uint256 result; + // Safe require that is checked by the specification in LibSummary.spec. require result * d <= x * y; return result; } @@ -112,7 +112,7 @@ filtered { MorphoHarness.Id id; requireInvariant feeInRange(id); - // In;erest would increase borrow ratio, so we need to assume that no time passes. + // Interest would increase borrow ratio, so we need to assume that no time passes. require lastUpdate(id) == e.block.timestamp; mathint assetsBefore = virtualTotalBorrowAssets(id); @@ -129,7 +129,7 @@ filtered { // Check that when not accruing interest, repay is decreasing the value of borrow shares. // Check the case where the market is not repaid fully. -// The other case requires exact math (ie not over-approximating mulDivUp and mulDivDown), so it is checked separately in ExactMath.spec +// The other case requires exact math (ie not over-approximating mulDivUp and mulDivDown), so it is checked separately in ExactMath.spec. rule repayDecreasesBorrowRatio(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { MorphoHarness.Id id = libId(marketParams); diff --git a/certora/specs/Reentrancy.spec b/certora/specs/Reentrancy.spec index 82ebeabde..db47a8a95 100644 --- a/certora/specs/Reentrancy.spec +++ b/certora/specs/Reentrancy.spec @@ -47,6 +47,7 @@ hook STATICCALL(uint g, address addr, uint argsOffset, uint argsLength, uint ret // Check that no function is accessing storage, then making an external call other than to the IRM, and accessing storage again. rule reentrancySafe(method f, calldataarg data, env e) { + // Set up the initial state. require !callIsBorrowRate; require !hasAccessedStorage && !hasCallAfterAccessingStorage && !hasReentrancyUnsafeCall; f(e,data); @@ -54,6 +55,7 @@ rule reentrancySafe(method f, calldataarg data, env e) { } rule noDelegateCalls(method f, calldataarg data, env e) { + // Set up the initial state. require !delegate_call; f(e,data); assert !delegate_call; @@ -61,6 +63,7 @@ rule noDelegateCalls(method f, calldataarg data, env e) { // This rule can be used to check which methods have static calls // rule hasStaticCalls(method f, calldataarg data, env e) { +// // Set up the initial state. // require !static_call; // f(e,data); // satisfy static_call; diff --git a/certora/specs/Reverts.spec b/certora/specs/Reverts.spec index 7edcc6dce..c54c06a21 100644 --- a/certora/specs/Reverts.spec +++ b/certora/specs/Reverts.spec @@ -44,6 +44,7 @@ invariant notInitializedEmpty(MorphoHarness.Id id) !isCreated(id) => emptyMarket(id) { preserved with (env e) { + // Safe require because timestamps cannot realistically be that large. require e.block.timestamp < 2^128; } } @@ -53,6 +54,7 @@ invariant zeroDoesNotAuthorize(address authorized) !isAuthorized(0, authorized) { preserved setAuthorization(address _authorized, bool _newAuthorization) with (env e) { + // Safe require because no one controls the zero address. require e.msg.sender != 0; } } @@ -117,6 +119,7 @@ rule supplyInputValidation(env e, MorphoHarness.MarketParams marketParams, uint2 // Check that withdraw reverts when its inputs are not validated. rule withdrawInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) { + // Safe require because no one controls the zero address. require e.msg.sender != 0; requireInvariant zeroDoesNotAuthorize(e.msg.sender); withdraw@withrevert(e, marketParams, assets, shares, onBehalf, receiver); @@ -125,6 +128,7 @@ rule withdrawInputValidation(env e, MorphoHarness.MarketParams marketParams, uin // Check that borrow reverts when its inputs are not validated. rule borrowInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) { + // Safe require because no one controls the zero address. require e.msg.sender != 0; requireInvariant zeroDoesNotAuthorize(e.msg.sender); borrow@withrevert(e, marketParams, assets, shares, onBehalf, receiver); @@ -145,6 +149,7 @@ rule supplyCollateralInputValidation(env e, MorphoHarness.MarketParams marketPar // Check that withdrawCollateral reverts when its inputs are not validated. rule withdrawCollateralInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 assets, address onBehalf, address receiver) { + // Safe require because no one controls the zero address. require e.msg.sender != 0; requireInvariant zeroDoesNotAuthorize(e.msg.sender); withdrawCollateral@withrevert(e, marketParams, assets, onBehalf, receiver); diff --git a/certora/specs/Transfer.spec b/certora/specs/Transfer.spec index 665acc248..5a92cfec0 100644 --- a/certora/specs/Transfer.spec +++ b/certora/specs/Transfer.spec @@ -20,9 +20,11 @@ ghost mapping(address => mathint) myBalances function summarySafeTransferFrom(address token, address from, address to, uint256 amount) { if (from == currentContract) { + // Safe require because the reference implementation would revert. myBalances[token] = require_uint256(myBalances[token] - amount); } if (to == currentContract) { + // Safe require because the reference implementation would revert. myBalances[token] = require_uint256(myBalances[token] + amount); } } @@ -30,6 +32,7 @@ function summarySafeTransferFrom(address token, address from, address to, uint25 // Check the functional correctness of the summary of safeTransfer. rule checkTransferSummary(address token, address to, uint256 amount) { mathint initialBalance = balanceOf(token, currentContract); + // Safe require because the total supply is greater than the sum of the balance of any two accounts. require to != currentContract => initialBalance + balanceOf(token, to) <= to_mathint(totalSupply(token)); libSafeTransfer(token, to, amount); @@ -43,6 +46,7 @@ rule checkTransferSummary(address token, address to, uint256 amount) { // Check the functional correctness of the summary of safeTransferFrom. rule checkTransferFromSummary(address token, address from, uint256 amount) { mathint initialBalance = balanceOf(token, currentContract); + // Safe require because the total supply is greater than the sum of the balance of any two accounts. require from != currentContract => initialBalance + balanceOf(token, from) <= to_mathint(totalSupply(token)); libSafeTransferFrom(token, from, currentContract, amount); @@ -57,7 +61,9 @@ rule checkTransferFromSummary(address token, address from, uint256 amount) { rule transferRevertCondition(address token, address to, uint256 amount) { uint256 initialBalance = balanceOf(token, currentContract); uint256 toInitialBalance = balanceOf(token, to); + // Safe require because the total supply is greater than the sum of the balance of any two accounts. require to != currentContract => initialBalance + toInitialBalance <= to_mathint(totalSupply(token)); + // Some tokens revert when either the sender or the receiver is the zero address. require currentContract != 0 && to != 0; libSafeTransfer@withrevert(token, to, amount); @@ -70,7 +76,9 @@ rule transferFromRevertCondition(address token, address from, address to, uint25 uint256 initialBalance = balanceOf(token, from); uint256 toInitialBalance = balanceOf(token, to); uint256 allowance = allowance(token, from, currentContract); + // Safe require because the total supply is greater than the sum of the balance of any two accounts. require to != from => initialBalance + toInitialBalance <= to_mathint(totalSupply(token)); + // Some tokens revert when either the sender or the receiver is the zero address. require from != 0 && to != 0; libSafeTransferFrom@withrevert(token, from, to, amount); From 3e5e78587f72764d381d3a4396cd817102bc2439 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 7 Sep 2023 18:39:03 +0200 Subject: [PATCH 121/204] feat: remove unnecessary require in stayHealthy --- certora/specs/Health.spec | 1 - 1 file changed, 1 deletion(-) diff --git a/certora/specs/Health.spec b/certora/specs/Health.spec index 2d9a9858e..3602de29c 100644 --- a/certora/specs/Health.spec +++ b/certora/specs/Health.spec @@ -61,7 +61,6 @@ filtered { require isHealthy(marketParams, user); // Safe require because of the invariants onlyEnabledLltv and lltvSmallerThanWad in ConsistentState.spec. require marketParams.lltv < 10^18; - require marketParams.lltv > 0; // Assumption to ensure that no interest is accumulated. require lastUpdate(id) == e.block.timestamp; From fa020767fb41a27a63918c5288d70a2c2ed799d6 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Mon, 11 Sep 2023 17:58:37 +0200 Subject: [PATCH 122/204] refactor: remove unnecessary storage variables in tokens --- certora/dispatch/ERC20NoRevert.sol | 20 +++++--------------- certora/dispatch/ERC20USDT.sol | 22 ++++++---------------- 2 files changed, 11 insertions(+), 31 deletions(-) diff --git a/certora/dispatch/ERC20NoRevert.sol b/certora/dispatch/ERC20NoRevert.sol index 6a9ab6efc..b8dfccd1e 100644 --- a/certora/dispatch/ERC20NoRevert.sol +++ b/certora/dispatch/ERC20NoRevert.sol @@ -2,18 +2,12 @@ pragma solidity ^0.8.0; contract ERC20NoRevert { - string public name; - string public symbol; - uint256 public decimals; address public owner; uint256 public totalSupply; mapping(address => uint256) public balanceOf; - mapping(address => mapping(address => uint256)) public allowed; + mapping(address => mapping(address => uint256)) public allowance; - constructor(string memory _name, string memory _symbol, uint256 _decimals) { - name = _name; - symbol = _symbol; - decimals = _decimals; + constructor() { owner = msg.sender; } @@ -36,19 +30,15 @@ contract ERC20NoRevert { } function transferFrom(address _from, address _to, uint256 _amount) public returns (bool) { - if (allowed[_from][msg.sender] < _amount) { + if (allowance[_from][msg.sender] < _amount) { return false; } - allowed[_from][msg.sender] -= _amount; + allowance[_from][msg.sender] -= _amount; return _transfer(_from, _to, _amount); } function approve(address _spender, uint256 _amount) public { - allowed[msg.sender][_spender] = _amount; - } - - function allowance(address _owner, address _spender) public view returns (uint256 remaining) { - return allowed[_owner][_spender]; + allowance[msg.sender][_spender] = _amount; } function mint(address _receiver, uint256 _amount) public onlyOwner { diff --git a/certora/dispatch/ERC20USDT.sol b/certora/dispatch/ERC20USDT.sol index 789372041..9962e9d6f 100644 --- a/certora/dispatch/ERC20USDT.sol +++ b/certora/dispatch/ERC20USDT.sol @@ -4,18 +4,12 @@ pragma solidity ^0.8.0; contract ERC20USDT { uint256 public constant MAX_UINT = 2 ** 256 - 1; - string public name; - string public symbol; - uint256 public decimals; uint256 public totalSupply; address public owner; mapping(address => uint256) public balanceOf; - mapping(address => mapping(address => uint256)) public allowed; + mapping(address => mapping(address => uint256)) public allowance; - constructor(string memory _name, string memory _symbol, uint256 _decimals) { - name = _name; - symbol = _symbol; - decimals = _decimals; + constructor() { owner = msg.sender; } @@ -34,20 +28,16 @@ contract ERC20USDT { } function transferFrom(address _from, address _to, uint256 _amount) public { - if (allowed[_from][msg.sender] < MAX_UINT) { - allowed[_from][msg.sender] -= _amount; + if (allowance[_from][msg.sender] < MAX_UINT) { + allowance[_from][msg.sender] -= _amount; } _transfer(_from, _to, _amount); } function approve(address _spender, uint256 _amount) public { - require(!((_amount != 0) && (allowed[msg.sender][_spender] != 0))); + require(!((_amount != 0) && (allowance[msg.sender][_spender] != 0))); - allowed[msg.sender][_spender] = _amount; - } - - function allowance(address _owner, address _spender) public view returns (uint256 remaining) { - return allowed[_owner][_spender]; + allowance[msg.sender][_spender] = _amount; } function mint(address _receiver, uint256 _amount) public onlyOwner { From 3a3b75c6ce29264a09724033575cdf7c80909ae5 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Mon, 11 Sep 2023 18:07:02 +0200 Subject: [PATCH 123/204] refactor: remove unecessary allow path --- certora/scripts/verifyAccrueInterest.sh | 1 - certora/scripts/verifyConsistentState.sh | 1 - certora/scripts/verifyExitLiquidity.sh | 1 - certora/scripts/verifyLiveness.sh | 1 - certora/scripts/verifyRatioMath.sh | 1 - 5 files changed, 5 deletions(-) diff --git a/certora/scripts/verifyAccrueInterest.sh b/certora/scripts/verifyAccrueInterest.sh index d97b89c0e..cf075e77a 100755 --- a/certora/scripts/verifyAccrueInterest.sh +++ b/certora/scripts/verifyAccrueInterest.sh @@ -7,6 +7,5 @@ make -C certora munged certoraRun \ certora/harness/MorphoHarness.sol \ --verify MorphoHarness:certora/specs/AccrueInterest.spec \ - --solc_allow_path src \ --msg "Morpho Blue Accrue Interest" \ "$@" diff --git a/certora/scripts/verifyConsistentState.sh b/certora/scripts/verifyConsistentState.sh index a49f74bc7..cd833fab3 100755 --- a/certora/scripts/verifyConsistentState.sh +++ b/certora/scripts/verifyConsistentState.sh @@ -7,6 +7,5 @@ make -C certora munged certoraRun \ certora/harness/MorphoHarness.sol \ --verify MorphoHarness:certora/specs/ConsistentState.spec \ - --solc_allow_path src \ --msg "Morpho Blue Consistent State" \ "$@" diff --git a/certora/scripts/verifyExitLiquidity.sh b/certora/scripts/verifyExitLiquidity.sh index 9896353c7..63c847707 100755 --- a/certora/scripts/verifyExitLiquidity.sh +++ b/certora/scripts/verifyExitLiquidity.sh @@ -7,6 +7,5 @@ make -C certora munged certoraRun \ certora/harness/MorphoHarness.sol \ --verify MorphoHarness:certora/specs/ExitLiquidity.spec \ - --solc_allow_path src \ --msg "Morpho Blue Exit Liquidity" \ "$@" diff --git a/certora/scripts/verifyLiveness.sh b/certora/scripts/verifyLiveness.sh index c0034c362..396a94383 100755 --- a/certora/scripts/verifyLiveness.sh +++ b/certora/scripts/verifyLiveness.sh @@ -7,6 +7,5 @@ make -C certora munged certoraRun \ certora/harness/MorphoInternalAccess.sol \ --verify MorphoInternalAccess:certora/specs/Liveness.spec \ - --solc_allow_path src \ --msg "Morpho Blue Liveness" \ "$@" diff --git a/certora/scripts/verifyRatioMath.sh b/certora/scripts/verifyRatioMath.sh index 14b53c6b8..4faa6516f 100755 --- a/certora/scripts/verifyRatioMath.sh +++ b/certora/scripts/verifyRatioMath.sh @@ -7,7 +7,6 @@ make -C certora munged certoraRun \ certora/harness/MorphoHarness.sol \ --verify MorphoHarness:certora/specs/RatioMath.spec \ - --solc_allow_path src \ --prover_args '-smt_hashingScheme plaininjectivity' \ --msg "Morpho Blue Ratio Math" \ "$@" From 3b180c4b727571530b96ae597014c1cb1f82caa7 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 13 Sep 2023 12:10:57 +0200 Subject: [PATCH 124/204] chore: increase timeout on split to 12 sec --- certora/scripts/verifyExactMath.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certora/scripts/verifyExactMath.sh b/certora/scripts/verifyExactMath.sh index fc9f801fa..87a6d9e47 100755 --- a/certora/scripts/verifyExactMath.sh +++ b/certora/scripts/verifyExactMath.sh @@ -8,6 +8,6 @@ certoraRun \ certora/harness/MorphoHarness.sol \ src/mocks/OracleMock.sol \ --verify MorphoHarness:certora/specs/ExactMath.spec \ - --prover_args '-smt_hashingScheme plaininjectivity' \ + --prover_args '-smt_hashingScheme plaininjectivity -mediumTimeout 12' \ --msg "Morpho Blue Exact Math" \ "$@" From 5a611ffa68e86c6635c1554780024bbe96bf0aeb Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 13 Sep 2023 15:12:13 +0200 Subject: [PATCH 125/204] refactor: remove unused oracle in Certora script --- certora/scripts/verifyExactMath.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/certora/scripts/verifyExactMath.sh b/certora/scripts/verifyExactMath.sh index 87a6d9e47..b617213c8 100755 --- a/certora/scripts/verifyExactMath.sh +++ b/certora/scripts/verifyExactMath.sh @@ -6,7 +6,6 @@ make -C certora munged certoraRun \ certora/harness/MorphoHarness.sol \ - src/mocks/OracleMock.sol \ --verify MorphoHarness:certora/specs/ExactMath.spec \ --prover_args '-smt_hashingScheme plaininjectivity -mediumTimeout 12' \ --msg "Morpho Blue Exact Math" \ From 2a9ca7940af14f257ba27413cbe160cd625671e3 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 15 Sep 2023 14:25:20 +0200 Subject: [PATCH 126/204] feat: verify the id function summary --- certora/specs/LibSummary.spec | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/certora/specs/LibSummary.spec b/certora/specs/LibSummary.spec index 6b88f842d..72f7f15fc 100644 --- a/certora/specs/LibSummary.spec +++ b/certora/specs/LibSummary.spec @@ -21,6 +21,6 @@ rule checkSummaryMulDivDown(uint256 x, uint256 y, uint256 d) { // Check the munging of the MarketParams.id function. // This rule cannot be checked because it is not possible disable the keccak256 summary for the moment. -// rule checkSummaryId(MorphoHarness.MarketParams marketParams) { -// assert optimizedId(marketParams) == libId(marketParams); -// } +rule checkSummaryId(MorphoHarness.MarketParams marketParams) { + assert optimizedId(marketParams) == libId(marketParams); +} From 3e1e21352f1e06ad7edf9740144f5f8e5eb070a1 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 15 Sep 2023 14:58:59 +0200 Subject: [PATCH 127/204] feat: enable verification of the canRepayAll rule --- certora/specs/Liveness.spec | 40 ++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/certora/specs/Liveness.spec b/certora/specs/Liveness.spec index 02698bcdc..e86fa5f61 100644 --- a/certora/specs/Liveness.spec +++ b/certora/specs/Liveness.spec @@ -34,6 +34,7 @@ function summarySafeTransferFrom(address token, address from, address to, uint25 } // Assume no fee. +// Summarize the accrue interest to avoid having to deal with reverts with absurdly high borrow rates. function summaryAccrueInterest(env e, MorphoInternalAccess.MarketParams marketParams, MorphoInternalAccess.Id id) { // Safe require because timestamps cannot realistically be that large. require e.block.timestamp < 2^128; @@ -195,24 +196,35 @@ rule withdrawCollateralChangesTokensAndBalance(env e, MorphoInternalAccess.Marke // This rule is commented out for the moment because of a bug in CVL where market IDs are not consistent accross a run. // Check that one can always repay the debt in full. -// rule canRepayAll(env e, MorphoInternalAccess.MarketParams marketParams, uint256 shares, bytes data) { -// MorphoInternalAccess.Id id = libId(marketParams); +rule canRepayAll(env e, MorphoInternalAccess.MarketParams marketParams, uint256 shares, bytes data) { + MorphoInternalAccess.Id id = libId(marketParams); + + // Assume no callback, which still allows to repay all. + require data.length == 0; + + // Assume a full repay. + require shares == borrowShares(id, e.msg.sender); + // Omit sanity checks. + require isCreated(id); + require e.msg.sender != 0; + require e.msg.value == 0; + require shares > 0; + // Safe require because of the noTimeTravel rule. + require lastUpdate(id) <= e.block.timestamp; + // Safe require because of the sumBorrowSharesCorrect invariant. + require shares <= totalBorrowShares(id); -// require data.length == 0; + // Accrue interest first to ensure that the accrued interest is reasonable (next require). + // Safe because of the AccrueInterest.repayAccruesInterest rule + summaryAccrueInterest(e, marketParams, id); -// require shares == borrowShares(id, e.msg.sender); -// require isCreated(id); -// require e.msg.sender != 0; -// require e.msg.value == 0; -// require shares > 0; -// require lastUpdate(id) <= e.block.timestamp; -// require shares <= totalBorrowShares(id); -// require totalBorrowAssets(id) < 10^35; + // Assume that the invariant about tokens total supply is respected. + require totalBorrowAssets(id) < 10^35; -// repay@withrevert(e, marketParams, 0, shares, e.msg.sender, data); + repay@withrevert(e, marketParams, 0, shares, e.msg.sender, data); -// assert !lastReverted; -// } + assert !lastReverted; +} // Check the one can always withdraw all, under the condition that there are no outstanding debt on the market. rule canWithdrawAll(env e, MorphoInternalAccess.MarketParams marketParams, uint256 shares, address receiver) { From c548f84e38f80dbf90e85c641f19920131743ffa Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 15 Sep 2023 14:59:18 +0200 Subject: [PATCH 128/204] refactor: document and clean the Reentrancy file --- certora/specs/Reentrancy.spec | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/certora/specs/Reentrancy.spec b/certora/specs/Reentrancy.spec index db47a8a95..d17dfa91b 100644 --- a/certora/specs/Reentrancy.spec +++ b/certora/specs/Reentrancy.spec @@ -8,7 +8,6 @@ ghost bool hasAccessedStorage; ghost bool hasCallAfterAccessingStorage; ghost bool hasReentrancyUnsafeCall; ghost bool delegate_call; -ghost bool static_call; ghost bool callIsBorrowRate; function summaryBorrowRate() returns uint256 { @@ -41,11 +40,7 @@ hook DELEGATECALL(uint g, address addr, uint argsOffset, uint argsLength, uint r delegate_call = true; } -hook STATICCALL(uint g, address addr, uint argsOffset, uint argsLength, uint retOffset, uint retLength) uint rc { - static_call = true; -} - -// Check that no function is accessing storage, then making an external call other than to the IRM, and accessing storage again. +// Check that no function is accessing storage, then making an external CALL other than to the IRM, and accessing storage again. rule reentrancySafe(method f, calldataarg data, env e) { // Set up the initial state. require !callIsBorrowRate; @@ -54,17 +49,10 @@ rule reentrancySafe(method f, calldataarg data, env e) { assert !hasReentrancyUnsafeCall; } +// Check that the contract is truly immutable. rule noDelegateCalls(method f, calldataarg data, env e) { // Set up the initial state. require !delegate_call; f(e,data); assert !delegate_call; } - -// This rule can be used to check which methods have static calls -// rule hasStaticCalls(method f, calldataarg data, env e) { -// // Set up the initial state. -// require !static_call; -// f(e,data); -// satisfy static_call; -// } From 9b3f24e8fa92e1f7aeeda123e709541c1dcde907 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 15 Sep 2023 15:18:56 +0200 Subject: [PATCH 129/204] refactor: remove outdated comments --- certora/specs/LibSummary.spec | 1 - certora/specs/Liveness.spec | 1 - certora/specs/Reentrancy.spec | 4 ++-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/certora/specs/LibSummary.spec b/certora/specs/LibSummary.spec index 72f7f15fc..784ca38d7 100644 --- a/certora/specs/LibSummary.spec +++ b/certora/specs/LibSummary.spec @@ -20,7 +20,6 @@ rule checkSummaryMulDivDown(uint256 x, uint256 y, uint256 d) { } // Check the munging of the MarketParams.id function. -// This rule cannot be checked because it is not possible disable the keccak256 summary for the moment. rule checkSummaryId(MorphoHarness.MarketParams marketParams) { assert optimizedId(marketParams) == libId(marketParams); } diff --git a/certora/specs/Liveness.spec b/certora/specs/Liveness.spec index e86fa5f61..cc4afba3d 100644 --- a/certora/specs/Liveness.spec +++ b/certora/specs/Liveness.spec @@ -194,7 +194,6 @@ rule withdrawCollateralChangesTokensAndBalance(env e, MorphoInternalAccess.Marke assert balanceAfter == balanceBefore - assets; } -// This rule is commented out for the moment because of a bug in CVL where market IDs are not consistent accross a run. // Check that one can always repay the debt in full. rule canRepayAll(env e, MorphoInternalAccess.MarketParams marketParams, uint256 shares, bytes data) { MorphoInternalAccess.Id id = libId(marketParams); diff --git a/certora/specs/Reentrancy.spec b/certora/specs/Reentrancy.spec index d17dfa91b..0a55276ea 100644 --- a/certora/specs/Reentrancy.spec +++ b/certora/specs/Reentrancy.spec @@ -41,7 +41,7 @@ hook DELEGATECALL(uint g, address addr, uint argsOffset, uint argsLength, uint r } // Check that no function is accessing storage, then making an external CALL other than to the IRM, and accessing storage again. -rule reentrancySafe(method f, calldataarg data, env e) { +rule reentrancySafe(method f, env e, calldataarg data) { // Set up the initial state. require !callIsBorrowRate; require !hasAccessedStorage && !hasCallAfterAccessingStorage && !hasReentrancyUnsafeCall; @@ -50,7 +50,7 @@ rule reentrancySafe(method f, calldataarg data, env e) { } // Check that the contract is truly immutable. -rule noDelegateCalls(method f, calldataarg data, env e) { +rule noDelegateCalls(method f, env e, calldataarg data) { // Set up the initial state. require !delegate_call; f(e,data); From b3c7038f95defbeb82486574e84a0506fabb3a7f Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 15 Sep 2023 15:28:57 +0200 Subject: [PATCH 130/204] feat: remove munging --- certora/harness/MorphoHarness.sol | 6 ++---- certora/munging.patch | 14 -------------- certora/specs/LibSummary.spec | 6 +++--- 3 files changed, 5 insertions(+), 21 deletions(-) diff --git a/certora/harness/MorphoHarness.sol b/certora/harness/MorphoHarness.sol index 600515f4b..8101fc251 100644 --- a/certora/harness/MorphoHarness.sol +++ b/certora/harness/MorphoHarness.sol @@ -74,10 +74,8 @@ contract MorphoHarness is Morpho { return marketParams.id(); } - function optimizedId(MarketParams memory marketParams) external pure returns (Id marketParamsId) { - assembly ("memory-safe") { - marketParamsId := keccak256(marketParams, mul(5, 32)) - } + function refId(MarketParams memory marketParams) external pure returns (Id marketParamsId) { + marketParamsId = Id.wrap(keccak256(abi.encode(marketParams))); } function libMulDivUp(uint256 x, uint256 y, uint256 d) public pure returns (uint256) { diff --git a/certora/munging.patch b/certora/munging.patch index 8b490be5c..e69de29bb 100644 --- a/certora/munging.patch +++ b/certora/munging.patch @@ -1,14 +0,0 @@ - -diff -ruN libraries/MarketParamsLib.sol libraries/MarketParamsLib.sol ---- libraries/MarketParamsLib.sol 2023-08-29 09:59:37.937583556 +0200 -+++ libraries/MarketParamsLib.sol 2023-08-29 10:16:10.519752188 +0200 -@@ -10,8 +10,6 @@ - library MarketParamsLib { - /// @notice Returns the id of the market `marketParams`. - function id(MarketParams memory marketParams) internal pure returns (Id marketParamsId) { -- assembly ("memory-safe") { -- marketParamsId := keccak256(marketParams, mul(5, 32)) -- } -+ marketParamsId = Id.wrap(keccak256(abi.encode(marketParams))); - } - } diff --git a/certora/specs/LibSummary.spec b/certora/specs/LibSummary.spec index 784ca38d7..bfa47b7a7 100644 --- a/certora/specs/LibSummary.spec +++ b/certora/specs/LibSummary.spec @@ -4,7 +4,7 @@ methods { function libMulDivUp(uint256, uint256, uint256) external returns uint256 envfree; function libMulDivDown(uint256, uint256, uint256) external returns uint256 envfree; function libId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; - function optimizedId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; + function refId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; } // Check the summary of mulDivUp required by RatioMath.spec @@ -19,7 +19,7 @@ rule checkSummaryMulDivDown(uint256 x, uint256 y, uint256 d) { assert result * d <= x * y; } -// Check the munging of the MarketParams.id function. +// Check the optimization of the MarketParams.id function. rule checkSummaryId(MorphoHarness.MarketParams marketParams) { - assert optimizedId(marketParams) == libId(marketParams); + assert libId(marketParams) == refId(marketParams); } From 73ad72d0bccb0fecbf6f2c8bfb7e08892f32da15 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 15 Sep 2023 16:28:11 +0200 Subject: [PATCH 131/204] feat: use summary of id in liveness --- certora/specs/LibSummary.spec | 6 +++--- certora/specs/Liveness.spec | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/certora/specs/LibSummary.spec b/certora/specs/LibSummary.spec index bfa47b7a7..19251e0d6 100644 --- a/certora/specs/LibSummary.spec +++ b/certora/specs/LibSummary.spec @@ -7,19 +7,19 @@ methods { function refId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; } -// Check the summary of mulDivUp required by RatioMath.spec +// Check the summary of MathLib.mulDivUp required by RatioMath.spec rule checkSummaryMulDivUp(uint256 x, uint256 y, uint256 d) { uint256 result = libMulDivUp(x, y, d); assert result * d >= x * y; } -// Check the summary of mulDivDown required by RatioMath.spec +// Check the summary of MathLib.mulDivDown required by RatioMath.spec rule checkSummaryMulDivDown(uint256 x, uint256 y, uint256 d) { uint256 result = libMulDivDown(x, y, d); assert result * d <= x * y; } -// Check the optimization of the MarketParams.id function. +// Check the summary of MarketParams.id required by Liveness.spec rule checkSummaryId(MorphoHarness.MarketParams marketParams) { assert libId(marketParams) == refId(marketParams); } diff --git a/certora/specs/Liveness.spec b/certora/specs/Liveness.spec index cc4afba3d..c00456242 100644 --- a/certora/specs/Liveness.spec +++ b/certora/specs/Liveness.spec @@ -11,9 +11,11 @@ methods { function fee(MorphoInternalAccess.Id) external returns uint256 envfree; function lastUpdate(MorphoInternalAccess.Id) external returns uint256 envfree; function libId(MorphoInternalAccess.MarketParams) external returns MorphoInternalAccess.Id envfree; + function refId(MorphoInternalAccess.MarketParams) external returns MorphoInternalAccess.Id envfree; function _._accrueInterest(MorphoInternalAccess.MarketParams memory marketParams, MorphoInternalAccess.Id id) internal with (env e) => summaryAccrueInterest(e, marketParams, id) expect void; + function MarketParamsLib.id(MorphoInternalAccess.MarketParams memory marketParams) internal returns MorphoInternalAccess.Id => summaryId(marketParams); function SafeTransferLib.safeTransfer(address token, address to, uint256 value) internal => summarySafeTransferFrom(token, currentContract, to, value); function SafeTransferLib.safeTransferFrom(address token, address from, address to, uint256 value) internal => summarySafeTransferFrom(token, from, to, value); } @@ -22,6 +24,10 @@ ghost mapping(address => mathint) myBalances { init_state axiom (forall address token. myBalances[token] == 0); } +function summaryId(MorphoInternalAccess.MarketParams marketParams) returns MorphoInternalAccess.Id { + return refId(marketParams); +} + function summarySafeTransferFrom(address token, address from, address to, uint256 amount) { if (from == currentContract) { // Safe require because the reference implementation would revert. From 602804d524e61178e1f7a0fc836d5183e3f43a56 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Sat, 16 Sep 2023 09:12:56 +0200 Subject: [PATCH 132/204] refactor: actual removal of munging files --- .gitignore | 1 - certora/harness/MorphoHarness.sol | 6 +++--- certora/harness/TransferHarness.sol | 4 ++-- certora/makefile | 12 ------------ certora/munging.patch | 0 certora/scripts/verifyAccrueInterest.sh | 2 -- certora/scripts/verifyConsistentState.sh | 2 -- certora/scripts/verifyExactMath.sh | 2 -- certora/scripts/verifyExitLiquidity.sh | 2 -- certora/scripts/verifyHealth.sh | 4 +--- certora/scripts/verifyLibSummary.sh | 2 -- certora/scripts/verifyLiveness.sh | 2 -- certora/scripts/verifyRatioMath.sh | 2 -- certora/scripts/verifyReentrancy.sh | 2 -- certora/scripts/verifyReverts.sh | 2 -- certora/scripts/verifyTransfer.sh | 2 -- certora/specs/ExactMath.spec | 6 ++++++ certora/specs/LibSummary.spec | 2 +- 18 files changed, 13 insertions(+), 42 deletions(-) delete mode 100644 certora/makefile delete mode 100644 certora/munging.patch diff --git a/.gitignore b/.gitignore index 718ea1943..84536674b 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,6 @@ docs/ # Certora .certora** emv-*-certora* -certora/munged # Hardhat /types diff --git a/certora/harness/MorphoHarness.sol b/certora/harness/MorphoHarness.sol index 8101fc251..0f4ac6ed6 100644 --- a/certora/harness/MorphoHarness.sol +++ b/certora/harness/MorphoHarness.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity 0.8.19; -import "../munged/Morpho.sol"; -import "../munged/libraries/SharesMathLib.sol"; -import "../munged/libraries/MarketParamsLib.sol"; +import "../../src/Morpho.sol"; +import "../../src/libraries/SharesMathLib.sol"; +import "../../src/libraries/MarketParamsLib.sol"; contract MorphoHarness is Morpho { using MarketParamsLib for MarketParams; diff --git a/certora/harness/TransferHarness.sol b/certora/harness/TransferHarness.sol index 1227e3fc7..5e6954a46 100644 --- a/certora/harness/TransferHarness.sol +++ b/certora/harness/TransferHarness.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.12; -import "../munged/libraries/SafeTransferLib.sol"; -import "../munged/interfaces/IERC20.sol"; +import "../../src/libraries/SafeTransferLib.sol"; +import "../../src/interfaces/IERC20.sol"; interface IERC20Extended is IERC20 { function balanceOf(address) external view returns (uint256); diff --git a/certora/makefile b/certora/makefile deleted file mode 100644 index 664ff4fb9..000000000 --- a/certora/makefile +++ /dev/null @@ -1,12 +0,0 @@ -munged: $(wildcard ../src/*.sol) munging.patch - @rm -rf munged - @cp -r ../src munged - @patch -p0 -d munged < munging.patch - -record: - diff -ruN ../src munged | sed 's+\.\./src/++g' | sed 's+munged/++g' > munging.patch - -clean: - rm -rf munged - -.PHONY: record clean # do not add munged here, as it is useful to protect munged edits diff --git a/certora/munging.patch b/certora/munging.patch deleted file mode 100644 index e69de29bb..000000000 diff --git a/certora/scripts/verifyAccrueInterest.sh b/certora/scripts/verifyAccrueInterest.sh index cf075e77a..7311c9e9c 100755 --- a/certora/scripts/verifyAccrueInterest.sh +++ b/certora/scripts/verifyAccrueInterest.sh @@ -2,8 +2,6 @@ set -euxo pipefail -make -C certora munged - certoraRun \ certora/harness/MorphoHarness.sol \ --verify MorphoHarness:certora/specs/AccrueInterest.spec \ diff --git a/certora/scripts/verifyConsistentState.sh b/certora/scripts/verifyConsistentState.sh index cd833fab3..47dd684f2 100755 --- a/certora/scripts/verifyConsistentState.sh +++ b/certora/scripts/verifyConsistentState.sh @@ -2,8 +2,6 @@ set -euxo pipefail -make -C certora munged - certoraRun \ certora/harness/MorphoHarness.sol \ --verify MorphoHarness:certora/specs/ConsistentState.spec \ diff --git a/certora/scripts/verifyExactMath.sh b/certora/scripts/verifyExactMath.sh index b617213c8..f7c9bbe30 100755 --- a/certora/scripts/verifyExactMath.sh +++ b/certora/scripts/verifyExactMath.sh @@ -2,8 +2,6 @@ set -euxo pipefail -make -C certora munged - certoraRun \ certora/harness/MorphoHarness.sol \ --verify MorphoHarness:certora/specs/ExactMath.spec \ diff --git a/certora/scripts/verifyExitLiquidity.sh b/certora/scripts/verifyExitLiquidity.sh index 63c847707..afdd603e6 100755 --- a/certora/scripts/verifyExitLiquidity.sh +++ b/certora/scripts/verifyExitLiquidity.sh @@ -2,8 +2,6 @@ set -euxo pipefail -make -C certora munged - certoraRun \ certora/harness/MorphoHarness.sol \ --verify MorphoHarness:certora/specs/ExitLiquidity.spec \ diff --git a/certora/scripts/verifyHealth.sh b/certora/scripts/verifyHealth.sh index 9d63978af..0bd7a7c2b 100755 --- a/certora/scripts/verifyHealth.sh +++ b/certora/scripts/verifyHealth.sh @@ -2,11 +2,9 @@ set -euxo pipefail -make -C certora munged - certoraRun \ certora/harness/MorphoHarness.sol \ - certora/munged/mocks/OracleMock.sol \ + src/mocks/OracleMock.sol \ --verify MorphoHarness:certora/specs/Health.spec \ --prover_args '-smt_hashingScheme plaininjectivity' \ --msg "Morpho Blue Health Check" \ diff --git a/certora/scripts/verifyLibSummary.sh b/certora/scripts/verifyLibSummary.sh index 85b78dd6e..41dcf9ca0 100755 --- a/certora/scripts/verifyLibSummary.sh +++ b/certora/scripts/verifyLibSummary.sh @@ -2,8 +2,6 @@ set -euxo pipefail -make -C certora munged - certoraRun \ certora/harness/MorphoHarness.sol \ --verify MorphoHarness:certora/specs/LibSummary.spec \ diff --git a/certora/scripts/verifyLiveness.sh b/certora/scripts/verifyLiveness.sh index 396a94383..260bc3757 100755 --- a/certora/scripts/verifyLiveness.sh +++ b/certora/scripts/verifyLiveness.sh @@ -2,8 +2,6 @@ set -euxo pipefail -make -C certora munged - certoraRun \ certora/harness/MorphoInternalAccess.sol \ --verify MorphoInternalAccess:certora/specs/Liveness.spec \ diff --git a/certora/scripts/verifyRatioMath.sh b/certora/scripts/verifyRatioMath.sh index 4faa6516f..c17eb1fc9 100755 --- a/certora/scripts/verifyRatioMath.sh +++ b/certora/scripts/verifyRatioMath.sh @@ -2,8 +2,6 @@ set -euxo pipefail -make -C certora munged - certoraRun \ certora/harness/MorphoHarness.sol \ --verify MorphoHarness:certora/specs/RatioMath.spec \ diff --git a/certora/scripts/verifyReentrancy.sh b/certora/scripts/verifyReentrancy.sh index cc9d7dbfc..0ffa2d570 100755 --- a/certora/scripts/verifyReentrancy.sh +++ b/certora/scripts/verifyReentrancy.sh @@ -2,8 +2,6 @@ set -euxo pipefail -make -C certora munged - certoraRun \ certora/harness/MorphoHarness.sol \ --verify MorphoHarness:certora/specs/Reentrancy.spec \ diff --git a/certora/scripts/verifyReverts.sh b/certora/scripts/verifyReverts.sh index e89bc970d..ae2675499 100755 --- a/certora/scripts/verifyReverts.sh +++ b/certora/scripts/verifyReverts.sh @@ -2,8 +2,6 @@ set -euxo pipefail -make -C certora munged - certoraRun \ certora/harness/MorphoHarness.sol \ --verify MorphoHarness:certora/specs/Reverts.spec \ diff --git a/certora/scripts/verifyTransfer.sh b/certora/scripts/verifyTransfer.sh index 633880f35..396585896 100755 --- a/certora/scripts/verifyTransfer.sh +++ b/certora/scripts/verifyTransfer.sh @@ -2,8 +2,6 @@ set -euxo pipefail -make -C certora munged - certoraRun \ certora/harness/TransferHarness.sol \ certora/dispatch/ERC20Standard.sol \ diff --git a/certora/specs/ExactMath.spec b/certora/specs/ExactMath.spec index 3ca2c3736..309ab7bdf 100644 --- a/certora/specs/ExactMath.spec +++ b/certora/specs/ExactMath.spec @@ -8,7 +8,9 @@ methods { function virtualTotalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; function fee(MorphoHarness.Id) external returns uint256 envfree; function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; + function refId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; + function MarketParamsLib.id(MorphoHarness.MarketParams memory marketParams) internal returns MorphoHarness.Id => summaryId(marketParams); function MathLib.mulDivDown(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivDown(a,b,c); function MathLib.mulDivUp(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivUp(a,b,c); function SafeTransferLib.safeTransfer(address token, address to, uint256 value) internal => NONDET; @@ -18,6 +20,10 @@ methods { function maxFee() external returns uint256 envfree; } +function summaryId(MorphoHarness.MarketParams marketParams) returns MorphoHarness.Id { + return refId(marketParams); +} + function summaryMulDivUp(uint256 x, uint256 y, uint256 d) returns uint256 { // Safe require because the reference implementation would revert. return require_uint256((x * y + (d - 1)) / d); diff --git a/certora/specs/LibSummary.spec b/certora/specs/LibSummary.spec index 19251e0d6..1f145d9ea 100644 --- a/certora/specs/LibSummary.spec +++ b/certora/specs/LibSummary.spec @@ -19,7 +19,7 @@ rule checkSummaryMulDivDown(uint256 x, uint256 y, uint256 d) { assert result * d <= x * y; } -// Check the summary of MarketParams.id required by Liveness.spec +// Check the summary of MarketParamsLib.id required by Liveness.spec and ExactMath.spec rule checkSummaryId(MorphoHarness.MarketParams marketParams) { assert libId(marketParams) == refId(marketParams); } From c9142fdfb3e12d56590dd826a8fbd3e1040a2f98 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Sat, 16 Sep 2023 13:07:47 +0200 Subject: [PATCH 133/204] refactor: remove summary id on ExactMath --- certora/specs/ExactMath.spec | 6 ------ certora/specs/LibSummary.spec | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/certora/specs/ExactMath.spec b/certora/specs/ExactMath.spec index 309ab7bdf..3ca2c3736 100644 --- a/certora/specs/ExactMath.spec +++ b/certora/specs/ExactMath.spec @@ -8,9 +8,7 @@ methods { function virtualTotalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; function fee(MorphoHarness.Id) external returns uint256 envfree; function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; - function refId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; - function MarketParamsLib.id(MorphoHarness.MarketParams memory marketParams) internal returns MorphoHarness.Id => summaryId(marketParams); function MathLib.mulDivDown(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivDown(a,b,c); function MathLib.mulDivUp(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivUp(a,b,c); function SafeTransferLib.safeTransfer(address token, address to, uint256 value) internal => NONDET; @@ -20,10 +18,6 @@ methods { function maxFee() external returns uint256 envfree; } -function summaryId(MorphoHarness.MarketParams marketParams) returns MorphoHarness.Id { - return refId(marketParams); -} - function summaryMulDivUp(uint256 x, uint256 y, uint256 d) returns uint256 { // Safe require because the reference implementation would revert. return require_uint256((x * y + (d - 1)) / d); diff --git a/certora/specs/LibSummary.spec b/certora/specs/LibSummary.spec index 1f145d9ea..21cd67538 100644 --- a/certora/specs/LibSummary.spec +++ b/certora/specs/LibSummary.spec @@ -19,7 +19,7 @@ rule checkSummaryMulDivDown(uint256 x, uint256 y, uint256 d) { assert result * d <= x * y; } -// Check the summary of MarketParamsLib.id required by Liveness.spec and ExactMath.spec +// Check the summary of MarketParamsLib.id required by Liveness.spec rule checkSummaryId(MorphoHarness.MarketParams marketParams) { assert libId(marketParams) == refId(marketParams); } From b8680aeba206f1998666f1525d8082d0545f5314 Mon Sep 17 00:00:00 2001 From: ALEX JOSEPH Date: Wed, 20 Sep 2023 08:32:38 +0100 Subject: [PATCH 134/204] increase medium timeout to 30 --- certora/scripts/verifyExactMath.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certora/scripts/verifyExactMath.sh b/certora/scripts/verifyExactMath.sh index f7c9bbe30..0effcb7bd 100755 --- a/certora/scripts/verifyExactMath.sh +++ b/certora/scripts/verifyExactMath.sh @@ -5,6 +5,6 @@ set -euxo pipefail certoraRun \ certora/harness/MorphoHarness.sol \ --verify MorphoHarness:certora/specs/ExactMath.spec \ - --prover_args '-smt_hashingScheme plaininjectivity -mediumTimeout 12' \ + --prover_args '-smt_hashingScheme plaininjectivity -mediumTimeout 30' \ --msg "Morpho Blue Exact Math" \ "$@" From 69ee16048b4b88288caa6f1f7a38a87354c40c32 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 21 Sep 2023 11:09:43 +0200 Subject: [PATCH 135/204] perf: plain injectivity setting for health script --- certora/scripts/verifyAccrueInterest.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/certora/scripts/verifyAccrueInterest.sh b/certora/scripts/verifyAccrueInterest.sh index 7311c9e9c..555a40d85 100755 --- a/certora/scripts/verifyAccrueInterest.sh +++ b/certora/scripts/verifyAccrueInterest.sh @@ -5,5 +5,6 @@ set -euxo pipefail certoraRun \ certora/harness/MorphoHarness.sol \ --verify MorphoHarness:certora/specs/AccrueInterest.spec \ + --prover_args '-smt_hashingScheme plaininjectivity -mediumTimeout 30' \ --msg "Morpho Blue Accrue Interest" \ "$@" From 1dc07bd9017c0997cc848c41a8d502aa5681e674 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 12 Oct 2023 14:55:02 +0200 Subject: [PATCH 136/204] fix: rename borrowableToken into loanToken --- certora/specs/ConsistentState.spec | 6 +++--- certora/specs/Liveness.spec | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/certora/specs/ConsistentState.spec b/certora/specs/ConsistentState.spec index a0d91965d..3b78703fc 100644 --- a/certora/specs/ConsistentState.spec +++ b/certora/specs/ConsistentState.spec @@ -49,7 +49,7 @@ ghost mapping(MorphoHarness.Id => address) idToBorrowable; ghost mapping(MorphoHarness.Id => address) idToCollateral; -hook Sstore idToMarketParams[KEY MorphoHarness.Id id].borrowableToken address token STORAGE { +hook Sstore idToMarketParams[KEY MorphoHarness.Id id].loanToken address token STORAGE { idToBorrowable[id] = token; } @@ -112,7 +112,7 @@ invariant borrowLessSupply(MorphoHarness.Id id) // This invariant is useful in the following rule, to link an id back to a market. invariant marketInvariant(MorphoHarness.MarketParams marketParams) isCreated(libId(marketParams)) => - idToBorrowable[libId(marketParams)] == marketParams.borrowableToken && + idToBorrowable[libId(marketParams)] == marketParams.loanToken && idToCollateral[libId(marketParams)] == marketParams.collateralToken; // Check that the idle amount on the singleton is greater to the sum amount, that is the sum over all the markets of the total supply plus the total collateral minus the total borrow. @@ -169,7 +169,7 @@ rule libIdUnique() { // Assume that arguments are the same. require libId(marketParams1) == libId(marketParams2); - assert marketParams1.borrowableToken == marketParams2.borrowableToken; + assert marketParams1.loanToken == marketParams2.loanToken; assert marketParams1.collateralToken == marketParams2.collateralToken; assert marketParams1.oracle == marketParams2.oracle; assert marketParams1.irm == marketParams2.irm; diff --git a/certora/specs/Liveness.spec b/certora/specs/Liveness.spec index c00456242..5e7f46b1c 100644 --- a/certora/specs/Liveness.spec +++ b/certora/specs/Liveness.spec @@ -70,14 +70,14 @@ rule supplyChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams marke require lastUpdate(id) == e.block.timestamp; mathint sharesBefore = supplyShares(id, onBehalf); - mathint balanceBefore = myBalances[marketParams.borrowableToken]; + mathint balanceBefore = myBalances[marketParams.loanToken]; uint256 suppliedAssets; uint256 suppliedShares; suppliedAssets, suppliedShares = supply(e, marketParams, assets, shares, onBehalf, data); mathint sharesAfter = supplyShares(id, onBehalf); - mathint balanceAfter = myBalances[marketParams.borrowableToken]; + mathint balanceAfter = myBalances[marketParams.loanToken]; assert assets != 0 => suppliedAssets == assets; assert assets == 0 => suppliedShares == shares; @@ -95,14 +95,14 @@ rule withdrawChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams mar require lastUpdate(id) == e.block.timestamp; mathint sharesBefore = supplyShares(id, onBehalf); - mathint balanceBefore = myBalances[marketParams.borrowableToken]; + mathint balanceBefore = myBalances[marketParams.loanToken]; uint256 withdrawnAssets; uint256 withdrawnShares; withdrawnAssets, withdrawnShares = withdraw(e, marketParams, assets, shares, onBehalf, receiver); mathint sharesAfter = supplyShares(id, onBehalf); - mathint balanceAfter = myBalances[marketParams.borrowableToken]; + mathint balanceAfter = myBalances[marketParams.loanToken]; assert assets != 0 => withdrawnAssets == assets; assert assets == 0 => withdrawnShares == shares; @@ -120,14 +120,14 @@ rule borrowChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams marke require lastUpdate(id) == e.block.timestamp; mathint sharesBefore = borrowShares(id, onBehalf); - mathint balanceBefore = myBalances[marketParams.borrowableToken]; + mathint balanceBefore = myBalances[marketParams.loanToken]; uint256 borrowedAssets; uint256 borrowedShares; borrowedAssets, borrowedShares = borrow(e, marketParams, assets, shares, onBehalf, receiver); mathint sharesAfter = borrowShares(id, onBehalf); - mathint balanceAfter = myBalances[marketParams.borrowableToken]; + mathint balanceAfter = myBalances[marketParams.loanToken]; assert assets != 0 => borrowedAssets == assets; assert assets == 0 => borrowedShares == shares; @@ -145,14 +145,14 @@ rule repayChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams market require lastUpdate(id) == e.block.timestamp; mathint sharesBefore = borrowShares(id, onBehalf); - mathint balanceBefore = myBalances[marketParams.borrowableToken]; + mathint balanceBefore = myBalances[marketParams.loanToken]; uint256 repaidAssets; uint256 repaidShares; repaidAssets, repaidShares = repay(e, marketParams, assets, shares, onBehalf, data); mathint sharesAfter = borrowShares(id, onBehalf); - mathint balanceAfter = myBalances[marketParams.borrowableToken]; + mathint balanceAfter = myBalances[marketParams.loanToken]; assert assets != 0 => repaidAssets == assets; assert assets == 0 => repaidShares == shares; From 0783ddba1d0d4927f3d91dfcf4dc84b8a150fc65 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 12 Oct 2023 15:04:21 +0200 Subject: [PATCH 137/204] refactor: remove OZ dependency in dispatch --- certora/dispatch/ERC20Standard.sol | 40 +++++++++++++++++++++++++++--- certora/dispatch/ERC20USDT.sol | 6 ++--- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/certora/dispatch/ERC20Standard.sol b/certora/dispatch/ERC20Standard.sol index 817c487df..71af6d9ca 100644 --- a/certora/dispatch/ERC20Standard.sol +++ b/certora/dispatch/ERC20Standard.sol @@ -1,8 +1,42 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.0; -import {ERC20} from "../../lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +contract ERC20Standard { + address public owner; + uint256 public totalSupply; + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; -contract ERC20Standard is ERC20 { - constructor(string memory name, string memory symbol) ERC20(name, symbol) {} + constructor() { + owner = msg.sender; + } + + modifier onlyOwner() { + require(msg.sender == owner); + _; + } + + function _transfer(address _from, address _to, uint256 _amount) internal returns (bool) { + 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) { + allowance[_from][msg.sender] -= _amount; + return _transfer(_from, _to, _amount); + } + + function approve(address _spender, uint256 _amount) public { + allowance[msg.sender][_spender] = _amount; + } + + function mint(address _receiver, uint256 _amount) public onlyOwner { + balanceOf[_receiver] += _amount; + totalSupply += _amount; + } } diff --git a/certora/dispatch/ERC20USDT.sol b/certora/dispatch/ERC20USDT.sol index 9962e9d6f..cce99353a 100644 --- a/certora/dispatch/ERC20USDT.sol +++ b/certora/dispatch/ERC20USDT.sol @@ -2,10 +2,8 @@ pragma solidity ^0.8.0; contract ERC20USDT { - uint256 public constant MAX_UINT = 2 ** 256 - 1; - - uint256 public totalSupply; address public owner; + uint256 public totalSupply; mapping(address => uint256) public balanceOf; mapping(address => mapping(address => uint256)) public allowance; @@ -28,7 +26,7 @@ contract ERC20USDT { } function transferFrom(address _from, address _to, uint256 _amount) public { - if (allowance[_from][msg.sender] < MAX_UINT) { + if (allowance[_from][msg.sender] < type(uint256).max) { allowance[_from][msg.sender] -= _amount; } _transfer(_from, _to, _amount); From a6b46673c4e975f4b401b04fe2d24ed87be8dccc Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 6 Oct 2023 10:27:54 +0200 Subject: [PATCH 138/204] docs: certora documentation layout --- certora/README.md | 73 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 15 deletions(-) diff --git a/certora/README.md b/certora/README.md index 48e987103..11a128e14 100644 --- a/certora/README.md +++ b/certora/README.md @@ -2,25 +2,68 @@ This folder contains the verification of the Morpho Blue protocol using CVL, Cer ## High-Level Description -The Morpho Blue protocol relies on several different concepts, which are described below. +The Morpho Blue protocol relies on several different concepts, which are described in the Whitepaper. These concepts have been verified using CVL. -See the description of the specification files below (or those files directly) for more details. +For more details, see the description of the specification below or the description of the specification files in the next section. -The Morpho Blue protocol allows users to take out collateralized loans on ERC20 tokens.\ -**Transfers.** Token transfers are verified to behave as expected for the most common implementations, in particular the transferred amount is the amount passed as input.\ -**Markets**. Markets on Morpho Blue depend on a pair of assets, the borrowable asset that is supplied and borrowed, and the collateral asset. +The Morpho Blue protocol allows users to take out collateralized loans on ERC20 tokens. + +```solidity +rule supplyAccruesInterest(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { + // Safe require because timestamps cannot realistically be that large. + require e.block.timestamp < 2^128; + + storage init = lastStorage; + + accrueInterest(e, marketParams); + supply(e, marketParams, assets, shares, onBehalf, data); + storage afterBoth = lastStorage; + + supply(e, marketParams, assets, shares, onBehalf, data) at init; + storage afterOne = lastStorage; + + assert afterBoth == afterOne; +} +``` + +### Transfers + +Token transfers are verified to behave as expected for the most common implementations, in particular the transferred amount is the amount passed as input. +Morpho Blue uses a transfer library to handle different tokens, including tokens that do not strictly respect the standard, in particular, when the return value on transfer and transferFrom function are missing, such as for the USDT token. This is difficult for the prover, so a summary is used to ease the verification of rules that rely on the transfer of tokens. This summary is verified to behave as expected in the Transfer.spec file. + +### Markets + +Markets on Morpho Blue depend on a pair of assets, the borrowable asset that is supplied and borrowed, and the collateral asset. Markets are independent, which means that loans cannot be impacted by loans from other markets. Positions of users are also independent, so loans cannot be impacted by loans from other users. -The accounting of the markets has been verified (such as the total amounts), as well as the fact that only market with enabled parameters are created.\ -**Supply.** When supplying on Morpho Blue, interest is earned over time, and the distribution is implemented through a shares mechanism. -Shares increase in value as interest is accrued.\ -**Borrow.** To borrow on Morpho Blue, collateral must be deposited. -Collateral tokens remain idle, as well as any borrowable token that has not been borrowed.\ -**Liquidation.** To ensure proper collateralization, a liquidation system is put in place. -In the absence of accrued interest, for example when creating a new position or when interacting multiple times in the same block, a position cannot be made unhealthy.\ -**Authorization.** Morpho Blue also defines a sound authorization system where users cannot modify positions of other users without proper authorization (except when liquidating).\ -**Safety.** Other safety properties are verified, particularly regarding reentrancy attacks and about input validation and revert conditions.\ -**Liveness.** Other liveness properties are verified as well, in particular it is always possible to exit a position without concern for the oracle. +The accounting of the markets has been verified (such as the total amounts), as well as the fact that only market with enabled parameters are created. + +### Supply + +When supplying on Morpho Blue, interest is earned over time, and the distribution is implemented through a shares mechanism. +Shares increase in value as interest is accrued. + +### Borrow + +To borrow on Morpho Blue, collateral must be deposited. +Collateral tokens remain idle, as well as any borrowable token that has not been borrowed. + +### Liquidation + +To ensure proper collateralization, a liquidation system is put in place. +In the absence of accrued interest, for example when creating a new position or when interacting multiple times in the same block, a position cannot be made unhealthy. + +### Authorization + +Morpho Blue also defines a sound authorization system where users cannot modify positions of other users without proper authorization (except when liquidating). + +### Safety + +Other safety properties are verified, particularly regarding reentrancy attacks and about input validation and revert conditions. + +### Liveness + +Other liveness properties are verified as well, in particular it is always possible to exit a position without concern for the oracle. ## Folder and File Structure From b4271a2ef369c66ee3a2731a6a6a8885672f81f9 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 12 Oct 2023 16:03:30 +0200 Subject: [PATCH 139/204] docs: certora documentation of transfers --- certora/README.md | 48 ++++++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/certora/README.md b/certora/README.md index 11a128e14..ad1a68dfa 100644 --- a/certora/README.md +++ b/certora/README.md @@ -1,35 +1,41 @@ This folder contains the verification of the Morpho Blue protocol using CVL, Certora's Verification Language. -## High-Level Description - -The Morpho Blue protocol relies on several different concepts, which are described in the Whitepaper. +The core concepts of the the Morpho Blue protocol are described in the [Whitepaper](../morpho-blue-whitepaper.pdf). These concepts have been verified using CVL. -For more details, see the description of the specification below or the description of the specification files in the next section. +We first give a [high-level description](#high-level-description) of the verification and then describe the [folder and file structure](#folder-and-file-structure) of the specification files. -The Morpho Blue protocol allows users to take out collateralized loans on ERC20 tokens. +## High-Level description -```solidity -rule supplyAccruesInterest(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { - // Safe require because timestamps cannot realistically be that large. - require e.block.timestamp < 2^128; +The Morpho Blue protocol allows users to take out collateralized loans on ERC20 tokens. - storage init = lastStorage; +### Transfers - accrueInterest(e, marketParams); - supply(e, marketParams, assets, shares, onBehalf, data); - storage afterBoth = lastStorage; +For a given market, Morpho Blue relies on the fact that the tokens involved respect the ERC20 standard. +In particular, in case of a transfer, it is assumed that the balance of Morpho Blue increases or decreases (depending if its the recipient or the sender) of the amount transferred. - supply(e, marketParams, assets, shares, onBehalf, data) at init; - storage afterOne = lastStorage; +The file [Transfer.spec](./specs/Transfer.spec) defines a summary of the transfer functions. +This summary is taken as the reference implementation to check that the balance of the Morpho Blue contract changes as expected. - assert afterBoth == afterOne; +```solidity +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); + } } ``` -### Transfers +The verification is done for the most common implementations of the ERC20 standard, for which we distinguish three different implementations: + +- [ERC20Standard](./dispatch/ERC20Standard.sol) which respects the standard and reverts in case of insufficient funds or in case of insufficient allowance. +- [ERC20NoRevert](./dispatch/ERC20NoRevert.sol) which respects the standard but does not revert (and returns false instead). +- [ERC20USDT](./dispatch/ERC20USDT.sol) which does not strictly respects the standard because it omits the return value of the `transfer` and `transferFrom` functions. -Token transfers are verified to behave as expected for the most common implementations, in particular the transferred amount is the amount passed as input. -Morpho Blue uses a transfer library to handle different tokens, including tokens that do not strictly respect the standard, in particular, when the return value on transfer and transferFrom function are missing, such as for the USDT token. This is difficult for the prover, so a summary is used to ease the verification of rules that rely on the transfer of tokens. This summary is verified to behave as expected in the Transfer.spec file. +Additionally, Morpho Blue always goes through a custom transfer library to handle tokens in all the above cases. +This library reverts when the transfer is not successful, and this is checked for the case of insufficient funds or insufficient allowance. +The use of the library can make it difficult for the provers, so the summary is sometimes used in other specification files to ease the verification of rules that rely on the transfer of tokens. ### Markets @@ -65,7 +71,7 @@ Other safety properties are verified, particularly regarding reentrancy attacks Other liveness properties are verified as well, in particular it is always possible to exit a position without concern for the oracle. -## Folder and File Structure +## Folder and file structure The [`certora/specs`](./specs) folder contains the following files: @@ -93,7 +99,7 @@ Notably, this allows handling the fact that library functions should be called f The [`certora/dispatch`](./dispatch) folder contains different contracts similar to the ones that are expected to be called from Morpho Blue. -## Usage +## Getting started To verify specification files, run the corresponding script in the [`certora/scripts`](./scripts) folder. It requires having set the `CERTORAKEY` environment variable to a valid Certora key. From f75498074b307e942e5a54e719e1b72735cea5b1 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 12 Oct 2023 17:20:13 +0200 Subject: [PATCH 140/204] docs: certora documentation of markets --- certora/README.md | 60 ++++++++++++++++++++---------- certora/specs/ConsistentState.spec | 22 +++++------ 2 files changed, 52 insertions(+), 30 deletions(-) diff --git a/certora/README.md b/certora/README.md index ad1a68dfa..3e6d61111 100644 --- a/certora/README.md +++ b/certora/README.md @@ -4,11 +4,11 @@ The core concepts of the the Morpho Blue protocol are described in the [Whitepap These concepts have been verified using CVL. We first give a [high-level description](#high-level-description) of the verification and then describe the [folder and file structure](#folder-and-file-structure) of the specification files. -## High-Level description +# High-level description The Morpho Blue protocol allows users to take out collateralized loans on ERC20 tokens. -### Transfers +## ERC20 tokens and transfers For a given market, Morpho Blue relies on the fact that the tokens involved respect the ERC20 standard. In particular, in case of a transfer, it is assumed that the balance of Morpho Blue increases or decreases (depending if its the recipient or the sender) of the amount transferred. @@ -19,10 +19,10 @@ This summary is taken as the reference implementation to check that the balance ```solidity function summarySafeTransferFrom(address token, address from, address to, uint256 amount) { if (from == currentContract) { - myBalances[token] = require_uint256(myBalances[token] - amount); + balance[token] = require_uint256(balance[token] - amount); } if (to == currentContract) { - myBalances[token] = require_uint256(myBalances[token] + amount); + balance[token] = require_uint256(balance[token] + amount); } } ``` @@ -37,41 +37,63 @@ Additionally, Morpho Blue always goes through a custom transfer library to handl This library reverts when the transfer is not successful, and this is checked for the case of insufficient funds or insufficient allowance. The use of the library can make it difficult for the provers, so the summary is sometimes used in other specification files to ease the verification of rules that rely on the transfer of tokens. -### Markets +## Markets -Markets on Morpho Blue depend on a pair of assets, the borrowable asset that is supplied and borrowed, and the collateral asset. -Markets are independent, which means that loans cannot be impacted by loans from other markets. -Positions of users are also independent, so loans cannot be impacted by loans from other users. -The accounting of the markets has been verified (such as the total amounts), as well as the fact that only market with enabled parameters are created. +Morpho Blue is a singleton contract that defines different markets. +Markets on Morpho Blue depend on a pair of assets, the loan token that is supplied and borrowed, and the collateral token. +Taking out a loan requires to deposit some collateral, which stays idle in the contract. +Additionally, every loan token that is not borrowed also stays idle in the contract. +This is verified by the following property: + +```solidity +invariant idleAmountLessBalance(address token) + idleAmount[token] <= balance[token] +``` -### Supply +where `balance` is the ERC20 balance of the singleton, and where `idleAmount` is the sum over all the markets of: the collateral amounts plus the supplied amounts minus the borrowed amounts. +In effect, this means that funds can only leave the contract through borrows and withdrawals. + +Additionally, it is checked that on a given market the borrowed amounts cannot exceed the supplied amounts. + +```solidity +invariant borrowLessSupply(MorphoHarness.Id id) + totalBorrowAssets(id) <= totalSupplyAssets(id); +``` + +This property, along with the previous one ensures that other markets can only impact the balance positively. +Said otherwise, markets are independent: tokens from a given market cannot be impacted by operations done in another market. + +## Shares When supplying on Morpho Blue, interest is earned over time, and the distribution is implemented through a shares mechanism. Shares increase in value as interest is accrued. -### Borrow +The accounting of the markets has been verified (such as the total shares), . -To borrow on Morpho Blue, collateral must be deposited. -Collateral tokens remain idle, as well as any borrowable token that has not been borrowed. - -### Liquidation +## Health To ensure proper collateralization, a liquidation system is put in place. In the absence of accrued interest, for example when creating a new position or when interacting multiple times in the same block, a position cannot be made unhealthy. +## Safety + ### Authorization Morpho Blue also defines a sound authorization system where users cannot modify positions of other users without proper authorization (except when liquidating). -### Safety +Positions of users are also independent, so loans cannot be impacted by loans from other users. + +### Others Other safety properties are verified, particularly regarding reentrancy attacks and about input validation and revert conditions. -### Liveness +as well as the fact that only market with enabled parameters are created + +## Liveness Other liveness properties are verified as well, in particular it is always possible to exit a position without concern for the oracle. -## Folder and file structure +# Folder and file structure The [`certora/specs`](./specs) folder contains the following files: @@ -99,7 +121,7 @@ Notably, this allows handling the fact that library functions should be called f The [`certora/dispatch`](./dispatch) folder contains different contracts similar to the ones that are expected to be called from Morpho Blue. -## Getting started +# Getting started To verify specification files, run the corresponding script in the [`certora/scripts`](./scripts) folder. It requires having set the `CERTORAKEY` environment variable to a valid Certora key. diff --git a/certora/specs/ConsistentState.spec b/certora/specs/ConsistentState.spec index 3b78703fc..1f2fd51c5 100644 --- a/certora/specs/ConsistentState.spec +++ b/certora/specs/ConsistentState.spec @@ -37,12 +37,12 @@ ghost mapping(MorphoHarness.Id => mathint) sumCollateral { init_state axiom (forall MorphoHarness.Id id. sumCollateral[id] == 0); } -ghost mapping(address => mathint) myBalances { - init_state axiom (forall address token. myBalances[token] == 0); +ghost mapping(address => mathint) balance { + init_state axiom (forall address token. balance[token] == 0); } -ghost mapping(address => mathint) sumAmount { - init_state axiom (forall address token. sumAmount[token] == 0); +ghost mapping(address => mathint) idleAmount { + init_state axiom (forall address token. idleAmount[token] == 0); } ghost mapping(MorphoHarness.Id => address) idToBorrowable; @@ -67,25 +67,25 @@ hook Sstore position[KEY MorphoHarness.Id id][KEY address owner].borrowShares ui hook Sstore position[KEY MorphoHarness.Id id][KEY address owner].collateral uint128 newAmount (uint128 oldAmount) STORAGE { sumCollateral[id] = sumCollateral[id] - oldAmount + newAmount; - sumAmount[idToCollateral[id]] = sumAmount[idToCollateral[id]] - oldAmount + newAmount; + idleAmount[idToCollateral[id]] = idleAmount[idToCollateral[id]] - oldAmount + newAmount; } hook Sstore market[KEY MorphoHarness.Id id].totalSupplyAssets uint128 newAmount (uint128 oldAmount) STORAGE { - sumAmount[idToBorrowable[id]] = sumAmount[idToBorrowable[id]] - oldAmount + newAmount; + idleAmount[idToBorrowable[id]] = idleAmount[idToBorrowable[id]] - oldAmount + newAmount; } hook Sstore market[KEY MorphoHarness.Id id].totalBorrowAssets uint128 newAmount (uint128 oldAmount) STORAGE { - sumAmount[idToBorrowable[id]] = sumAmount[idToBorrowable[id]] + oldAmount - newAmount; + idleAmount[idToBorrowable[id]] = idleAmount[idToBorrowable[id]] + oldAmount - newAmount; } function summarySafeTransferFrom(address token, address from, address to, uint256 amount) { if (from == currentContract) { // Safe require because the reference implementation would revert. - myBalances[token] = require_uint256(myBalances[token] - amount); + balance[token] = require_uint256(balance[token] - amount); } if (to == currentContract) { // Safe require because the reference implementation would revert. - myBalances[token] = require_uint256(myBalances[token] + amount); + balance[token] = require_uint256(balance[token] + amount); } } @@ -116,8 +116,8 @@ invariant marketInvariant(MorphoHarness.MarketParams marketParams) idToCollateral[libId(marketParams)] == marketParams.collateralToken; // Check that the idle amount on the singleton is greater to the sum amount, that is the sum over all the markets of the total supply plus the total collateral minus the total borrow. -invariant isLiquid(address token) - sumAmount[token] <= myBalances[token] +invariant idleAmountLessBalance(address token) + idleAmount[token] <= balance[token] { // Safe requires on the sender because the contract cannot call the function itself. preserved supply(MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) with (env e) { From 375a86b4543ac99b2c3d6c83eb38ce3208deef04 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 12 Oct 2023 17:45:23 +0200 Subject: [PATCH 141/204] docs: certora documentation of shares --- certora/README.md | 30 +++++++++++++++++++++++++----- certora/specs/ConsistentState.spec | 4 ++-- certora/specs/RatioMath.spec | 10 +++++----- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/certora/README.md b/certora/README.md index 3e6d61111..c1e3edd31 100644 --- a/certora/README.md +++ b/certora/README.md @@ -27,6 +27,8 @@ function summarySafeTransferFrom(address token, address from, address to, uint25 } ``` +where `balance` is the ERC20 balance of the Morpho Blue contract. + The verification is done for the most common implementations of the ERC20 standard, for which we distinguish three different implementations: - [ERC20Standard](./dispatch/ERC20Standard.sol) which respects the standard and reverts in case of insufficient funds or in case of insufficient allowance. @@ -39,24 +41,24 @@ The use of the library can make it difficult for the provers, so the summary is ## Markets -Morpho Blue is a singleton contract that defines different markets. +The Morpho Blue contract is a singleton contract that defines different markets. Markets on Morpho Blue depend on a pair of assets, the loan token that is supplied and borrowed, and the collateral token. Taking out a loan requires to deposit some collateral, which stays idle in the contract. Additionally, every loan token that is not borrowed also stays idle in the contract. This is verified by the following property: ```solidity -invariant idleAmountLessBalance(address token) +invariant idleAmountLessThanBalance(address token) idleAmount[token] <= balance[token] ``` -where `balance` is the ERC20 balance of the singleton, and where `idleAmount` is the sum over all the markets of: the collateral amounts plus the supplied amounts minus the borrowed amounts. +where `idleAmount` is the sum over all the markets of: the collateral amounts plus the supplied amounts minus the borrowed amounts. In effect, this means that funds can only leave the contract through borrows and withdrawals. Additionally, it is checked that on a given market the borrowed amounts cannot exceed the supplied amounts. ```solidity -invariant borrowLessSupply(MorphoHarness.Id id) +invariant borrowLessThanSupply(MorphoHarness.Id id) totalBorrowAssets(id) <= totalSupplyAssets(id); ``` @@ -67,8 +69,26 @@ Said otherwise, markets are independent: tokens from a given market cannot be im When supplying on Morpho Blue, interest is earned over time, and the distribution is implemented through a shares mechanism. Shares increase in value as interest is accrued. +The share mechanism is implemented symetrically for the borrow side: a share of borrow increasing in value over time represents additional owed interest. +The rule `accrueInterestIncreasesSupplyRatio` checks this property for the supply side with the following statement. + +```soldidity + // Check that the ratio increases: assetsBefore/sharesBefore <= assetsAfter/sharesAfter. + assert assetsBefore * sharesAfter <= assetsAfter * sharesBefore; +``` + +where `assetsBefore` and `sharesBefore` represents respectively the supplied assets and the supplied shares before accruing the interest. Similarly, `assetsAfter` and `sharesAfter` represent the supplied assets and shares after an interest accrual. + +The accounting of the shares mechanism relies on another variable to store the total number of shares, in order to compute what is the relative part of each user. +This variable needs to be kept up to date at each corresponding interaction, and it is checked that this accounting is done properly. +For example, for the supply side, this is done by the following invariant. + +```solidity +invariant sumSupplySharesCorrect(MorphoHarness.Id id) + to_mathint(totalSupplyShares(id)) == sumSupplyShares[id]; +``` -The accounting of the markets has been verified (such as the total shares), . +where `sumSupplyShares` only exists in the specification, and is defined to be automatically updated whenever any of the shares of the users are modified. ## Health diff --git a/certora/specs/ConsistentState.spec b/certora/specs/ConsistentState.spec index 1f2fd51c5..5324bd5b4 100644 --- a/certora/specs/ConsistentState.spec +++ b/certora/specs/ConsistentState.spec @@ -106,7 +106,7 @@ invariant sumBorrowSharesCorrect(MorphoHarness.Id id) // Check that a market only allows borrows up to the total supply. // This invariant shows that markets are independent, tokens from one market cannot be taken by interacting with another market. -invariant borrowLessSupply(MorphoHarness.Id id) +invariant borrowLessThanSupply(MorphoHarness.Id id) totalBorrowAssets(id) <= totalSupplyAssets(id); // This invariant is useful in the following rule, to link an id back to a market. @@ -116,7 +116,7 @@ invariant marketInvariant(MorphoHarness.MarketParams marketParams) idToCollateral[libId(marketParams)] == marketParams.collateralToken; // Check that the idle amount on the singleton is greater to the sum amount, that is the sum over all the markets of the total supply plus the total collateral minus the total borrow. -invariant idleAmountLessBalance(address token) +invariant idleAmountLessThanBalance(address token) idleAmount[token] <= balance[token] { // Safe requires on the sender because the contract cannot call the function itself. diff --git a/certora/specs/RatioMath.spec b/certora/specs/RatioMath.spec index 21dc1050a..af690462e 100644 --- a/certora/specs/RatioMath.spec +++ b/certora/specs/RatioMath.spec @@ -51,7 +51,7 @@ rule accrueInterestIncreasesSupplyRatio(env e, MorphoHarness.MarketParams market mathint assetsAfter = virtualTotalSupplyAssets(id); mathint sharesAfter = virtualTotalSupplyShares(id); - // Check that ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter. + // Check that the ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter. assert assetsBefore * sharesAfter <= assetsAfter * sharesBefore; } @@ -69,7 +69,7 @@ rule accrueInterestIncreasesBorrowRatio(env e, MorphoHarness.MarketParams market mathint assetsAfter = virtualTotalBorrowAssets(id); mathint sharesAfter = virtualTotalBorrowShares(id); - // Check that ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter. + // Check that the ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter. assert assetsBefore * sharesAfter <= assetsAfter * sharesBefore; } @@ -95,7 +95,7 @@ filtered { mathint assetsAfter = virtualTotalSupplyAssets(id); mathint sharesAfter = virtualTotalSupplyShares(id); - // Check that ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter + // Check that the ratio increases: assetsBefore/sharesBefore <= assetsAfter / sharesAfter assert assetsBefore * sharesAfter <= assetsAfter * sharesBefore; } @@ -123,7 +123,7 @@ filtered { mathint assetsAfter = virtualTotalBorrowAssets(id); mathint sharesAfter = virtualTotalBorrowShares(id); - // Check that ratio decreases: assetsBefore/sharesBefore >= assetsAfter / sharesAfter + // Check that the ratio decreases: assetsBefore/sharesBefore >= assetsAfter / sharesAfter assert assetsBefore * sharesAfter >= assetsAfter * sharesBefore; } @@ -151,6 +151,6 @@ rule repayDecreasesBorrowRatio(env e, MorphoHarness.MarketParams marketParams, u mathint sharesAfter = virtualTotalBorrowShares(id); assert assetsAfter == assetsBefore - repaidAssets; - // Check that ratio decreases: assetsBefore/sharesBefore >= assetsAfter / sharesAfter + // Check that the ratio decreases: assetsBefore/sharesBefore >= assetsAfter / sharesAfter assert assetsBefore * sharesAfter >= assetsAfter * sharesBefore; } From 8a4d786a4d09516f388c7714590efe6ba432d941 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 13 Oct 2023 10:40:22 +0200 Subject: [PATCH 142/204] docs: certora documentation of health --- certora/README.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/certora/README.md b/certora/README.md index c1e3edd31..af2067f8c 100644 --- a/certora/README.md +++ b/certora/README.md @@ -35,7 +35,7 @@ The verification is done for the most common implementations of the ERC20 standa - [ERC20NoRevert](./dispatch/ERC20NoRevert.sol) which respects the standard but does not revert (and returns false instead). - [ERC20USDT](./dispatch/ERC20USDT.sol) which does not strictly respects the standard because it omits the return value of the `transfer` and `transferFrom` functions. -Additionally, Morpho Blue always goes through a custom transfer library to handle tokens in all the above cases. +Additionally, Morpho Blue always goes through a custom transfer library to handle ERC20 tokens, notably in all the above cases. This library reverts when the transfer is not successful, and this is checked for the case of insufficient funds or insufficient allowance. The use of the library can make it difficult for the provers, so the summary is sometimes used in other specification files to ease the verification of rules that rely on the transfer of tokens. @@ -92,8 +92,22 @@ where `sumSupplyShares` only exists in the specification, and is defined to be a ## Health -To ensure proper collateralization, a liquidation system is put in place. -In the absence of accrued interest, for example when creating a new position or when interacting multiple times in the same block, a position cannot be made unhealthy. +To ensure proper collateralization, a liquidation system is put in place, where unhealthy positions can be liquidated. +A position is said to be healthy if the ratio of the borrowed value over collateral value is smaller than the LLTV of that market. +This leaves a safety buffer before the position can be insolvent, where the aforementioned ratio is above 1. +To ensure that liquidators have the time to interact with unhealthy positions, it is formally verified that this buffer is respected. +Notably, it is verified that in the absence of accrued interest, which is the case when creating a new position or when interacting multiple times in the same block, a position cannot be made unhealthy. + +Let's define bad debt of a position as the amount borrowed when it is backed by no collateral. +Morpho Blue automatically realizes the bad debt when liquidating a position, by transferring it to the lenders. +In effect, this means that there is no bad debt on Morpho Blue, which is verified by the following invariant. + +```solidity +invariant alwaysCollateralized(MorphoHarness.Id id, address borrower) + borrowShares(id, borrower) != 0 => collateral(id, borrower) != 0; +``` + +More generally, this means that the result of liquidating a position multiple times eventually lead to a healthy position (possibly empty). ## Safety From b90cdb2200eaa31f42947a289a7f3c1675f1d435 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 13 Oct 2023 11:06:15 +0200 Subject: [PATCH 143/204] docs: certora documentation of authorization --- certora/README.md | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/certora/README.md b/certora/README.md index af2067f8c..c4f8738ac 100644 --- a/certora/README.md +++ b/certora/README.md @@ -109,13 +109,40 @@ invariant alwaysCollateralized(MorphoHarness.Id id, address borrower) More generally, this means that the result of liquidating a position multiple times eventually lead to a healthy position (possibly empty). -## Safety +## Authorization + +Morpho Blue also defines primitive authorization system, where users can authorize an account to fully manage their position. +This allows to rebuild more granular control of the position on top by authorizing an immutable contract with limited capabilities. +The authorization is verified to be sound in the sense that no user can modify the position of another user without proper authorization (except when liquidating). + +Let's detail the rule that makes sure that the supply side stays consistent. + +```solidity +rule userCannotLoseSupplyShares(env e, method f, calldataarg data) +filtered { f -> !f.isView } +{ + MorphoHarness.Id id; + address user; + + // Assume that the e.msg.sender is not authorized. + require !isAuthorized(user, e.msg.sender); + require user != e.msg.sender; -### Authorization + mathint sharesBefore = supplyShares(id, user); -Morpho Blue also defines a sound authorization system where users cannot modify positions of other users without proper authorization (except when liquidating). + f(e, data); -Positions of users are also independent, so loans cannot be impacted by loans from other users. + mathint sharesAfter = supplyShares(id, user); + + assert sharesAfter >= sharesBefore; +} +``` + +In the previous rule, an arbitrary function of Morpho Blue `f` is called with arbitrary `data`. +Shares of `user` on the market identified by `id` are recorded before and after this call. +In this way, it is checked that the supply shares are increasing when the caller of the function is neither the owner of those shares (`user != e.msg.sender`) nor authorized (`!isAuthorized(user, e.msg.sender)`). + +## Safety ### Others From a32acdba306cf9618a4f4fed1494a9e1b7a227c2 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 13 Oct 2023 11:57:22 +0200 Subject: [PATCH 144/204] docs: certora documentation of other safety properties --- certora/README.md | 64 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/certora/README.md b/certora/README.md index c4f8738ac..cec52be1d 100644 --- a/certora/README.md +++ b/certora/README.md @@ -93,7 +93,7 @@ where `sumSupplyShares` only exists in the specification, and is defined to be a ## Health To ensure proper collateralization, a liquidation system is put in place, where unhealthy positions can be liquidated. -A position is said to be healthy if the ratio of the borrowed value over collateral value is smaller than the LLTV of that market. +A position is said to be healthy if the ratio of the borrowed value over collateral value is smaller than the liquidation loan-to-value (LLTV) of that market. This leaves a safety buffer before the position can be insolvent, where the aforementioned ratio is above 1. To ensure that liquidators have the time to interact with unhealthy positions, it is formally verified that this buffer is respected. Notably, it is verified that in the absence of accrued interest, which is the case when creating a new position or when interacting multiple times in the same block, a position cannot be made unhealthy. @@ -142,18 +142,70 @@ In the previous rule, an arbitrary function of Morpho Blue `f` is called with ar Shares of `user` on the market identified by `id` are recorded before and after this call. In this way, it is checked that the supply shares are increasing when the caller of the function is neither the owner of those shares (`user != e.msg.sender`) nor authorized (`!isAuthorized(user, e.msg.sender)`). -## Safety +## Other safety properties -### Others +### Enabled LLTV and IRM -Other safety properties are verified, particularly regarding reentrancy attacks and about input validation and revert conditions. +Creating a market is permissionless on Morpho Blue, but some parameters should fall into the range of admitted values. +Notably, the LLTV value should be enabled beforehand. +The following rule checks that no market can ever exist with a LLTV that had not been previously approved. -as well as the fact that only market with enabled parameters are created +```solidity +invariant onlyEnabledLltv(MorphoHarness.MarketParams marketParams) + isCreated(libId(marketParams)) => isLltvEnabled(marketParams.lltv); +``` + +Similarly, the interest rate model (IRM) used for the market must have been previously whitelisted. + +### Range of the fee + +The governance can choose to set a fee to a given market. +Fees are guaranteed to never exceed 25% of the interest accrued, and this is verified by the following rule. + +```solidity +invariant feeInRange(MorphoHarness.Id id) + fee(id) <= maxFee(); +``` + +### Sanity checks and input validation + +The formal verification is also taking care of other sanity checks, some of which are needed properties to verify other rules. +For example, the following rule checks that the variable storing the last update time is no more than the current time. +This is a sanity check, but it is also useful to ensure that there will be no underflow when computing the time elapsed since the last update. + +```solidity +rule noTimeTravel(method f, env e, calldataarg args) +filtered { f -> !f.isView } +{ + MorphoHarness.Id id; + // Assume the property before the interaction. + require lastUpdate(id) <= e.block.timestamp; + f(e, args); + assert lastUpdate(id) <= e.block.timestamp; +} +``` -## Liveness +Additional rules are verified to ensure that the sanitization of inputs is done correctly. + +```solidity +rule supplyInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { + supply@withrevert(e, marketParams, assets, shares, onBehalf, data); + assert !exactlyOneZero(assets, shares) || onBehalf == 0 => lastReverted; +} +``` + +The previous rule checks that the `supply` function reverts whenever the `onBehalf` parameter is the address zero, or when either both `assets` and `shares` are zero or both are non-zero. + +## Liveness properties Other liveness properties are verified as well, in particular it is always possible to exit a position without concern for the oracle. +## Protection against common attack vectors + +### Reentrancy + +### Extraction of value + # Folder and file structure The [`certora/specs`](./specs) folder contains the following files: From 35de6cf5771b0bf7d272ca3a2f2445aadd38ea20 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 13 Oct 2023 13:12:24 +0200 Subject: [PATCH 145/204] docs: certora documentation of liveness properties --- certora/README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/certora/README.md b/certora/README.md index cec52be1d..aa9ebc41e 100644 --- a/certora/README.md +++ b/certora/README.md @@ -90,7 +90,7 @@ invariant sumSupplySharesCorrect(MorphoHarness.Id id) where `sumSupplyShares` only exists in the specification, and is defined to be automatically updated whenever any of the shares of the users are modified. -## Health +## Positions health and liquidations To ensure proper collateralization, a liquidation system is put in place, where unhealthy positions can be liquidated. A position is said to be healthy if the ratio of the borrowed value over collateral value is smaller than the liquidation loan-to-value (LLTV) of that market. @@ -198,7 +198,15 @@ The previous rule checks that the `supply` function reverts whenever the `onBeha ## Liveness properties -Other liveness properties are verified as well, in particular it is always possible to exit a position without concern for the oracle. +On top of verifying that the protocol is secured, the verification also proves that it is usable. +Such properties are called liveness properties, and it is notably checked that the accounting is done when an interaction goes through. +As an example, the `withdrawChangesTokensAndShares` rule checks that calling the `withdraw` function successfully will decrease the shares of the concerned account and increase the balance of the receiver. + +Other liveness properties are verified as well. +Notably, it's also verified that it is always possible to exit a position without concern for the oracle. +This is done through the verification of two rules: the `canRepayAll` rule and the `canWithdrawCollateralAll` rule. +The `canRepayAll` rule ensures that it is always possible to repay the full debt of a position, leaving the account without any oustanding debt. +The `canWithdrawCollateralAll` rule ensures that in the case where the account has no outstanding debt, then it is possible to withdraw the full collateral. ## Protection against common attack vectors From e4aca4ce5ec01fe919c9c4067ee17c12fd11991b Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 13 Oct 2023 14:08:42 +0200 Subject: [PATCH 146/204] docs: certora documentation of protection against common attack vectors --- certora/README.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/certora/README.md b/certora/README.md index aa9ebc41e..df41364b3 100644 --- a/certora/README.md +++ b/certora/README.md @@ -210,10 +210,32 @@ The `canWithdrawCollateralAll` rule ensures that in the case where the account h ## Protection against common attack vectors +Other common and known attack vectors are verified to not be possible on the Morpho Blue protocol. + ### Reentrancy +Reentrancy is a common attack vector that happen when a call to a contract allows, when in a temporary state, to call the same contract again. +The state of the contract usually refers to the storage variables, which can typically hold values that are meant to be used only after the full execution of the current function. +The Morpho Blue contract is verified to not be vulnerable to this kind of reentrancy attack thanks to the rule `reentrancySafe`. + ### Extraction of value +The Morpho Blue protocol uses a conservative approach to handle arithmetic operations. +Rounding is done such that potential (small) errors are in favor of the protocol, which ensures that it is not possible to extract value from other users. + +The rule `supplyWithdraw` handles the simple scenario of a supply followed by a withdraw, and has the following check. + +```solidity +assert withdrawnAssets <= suppliedAssets; +``` + +The rule `withdrawLiquidity` is more general and defines `owedAssets` as the assets that are owed to the user, rounding in favor of the protocol. +This rule has the following check to ensure that no more than the owed assets can be withdrawn. + +```solidity +assert withdrawnAssets <= owedAssets; +``` + # Folder and file structure The [`certora/specs`](./specs) folder contains the following files: @@ -249,5 +271,5 @@ It requires having set the `CERTORAKEY` environment variable to a valid Certora You can pass arguments to the script, which allows you to verify specific properties. For example, at the root of the repository: ``` -./certora/scripts/verifyConsistentState.sh --rule borrowLessSupply +./certora/scripts/verifyConsistentState.sh --rule borrowLessThanSupply ``` From d7f4f9556cf968303e3bccfdbc092b0b64a42912 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 17 Oct 2023 12:06:36 +0200 Subject: [PATCH 147/204] feat: use configuration file instead of script --- .github/workflows/certora.yml | 2 +- certora/configuration/ConsistentState.conf | 8 ++++++++ certora/scripts/verifyConsistentState.sh | 9 --------- 3 files changed, 9 insertions(+), 10 deletions(-) create mode 100644 certora/configuration/ConsistentState.conf delete mode 100755 certora/scripts/verifyConsistentState.sh diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml index b27fd301f..60720fcfe 100644 --- a/.github/workflows/certora.yml +++ b/.github/workflows/certora.yml @@ -43,7 +43,7 @@ jobs: matrix: script: - verifyAccrueInterest.sh - - verifyConsistentState.sh + - certoraRun certora/configuration/ConsistentState.conf - verifyExactMath.sh - verifyExitLiquidity.sh - verifyHealth.sh diff --git a/certora/configuration/ConsistentState.conf b/certora/configuration/ConsistentState.conf new file mode 100644 index 000000000..5b507bf4b --- /dev/null +++ b/certora/configuration/ConsistentState.conf @@ -0,0 +1,8 @@ +{ + "files": [ + "certora/harness/MorphoHarness.sol" + ], + "msg": "Morpho Blue Consistent State", + "process": "emv", + "verify": "MorphoHarness:certora/specs/ConsistentState.spec" +} diff --git a/certora/scripts/verifyConsistentState.sh b/certora/scripts/verifyConsistentState.sh deleted file mode 100755 index 47dd684f2..000000000 --- a/certora/scripts/verifyConsistentState.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -set -euxo pipefail - -certoraRun \ - certora/harness/MorphoHarness.sol \ - --verify MorphoHarness:certora/specs/ConsistentState.spec \ - --msg "Morpho Blue Consistent State" \ - "$@" From ac88f34941ee31fc707fd5084b33cadf17ceed55 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 17 Oct 2023 12:15:19 +0200 Subject: [PATCH 148/204] fix: use conf matrix --- .github/workflows/certora.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml index 60720fcfe..fc567c38f 100644 --- a/.github/workflows/certora.yml +++ b/.github/workflows/certora.yml @@ -34,6 +34,7 @@ jobs: run: | echo "key length" ${#CERTORAKEY} bash certora/scripts/${{ matrix.script }} --solc solc8.19 + certoraRun certora/configuration/${{ matrix.conf }} --solc solc8.19 env: CERTORAKEY: ${{ secrets.CERTORAKEY }} @@ -41,9 +42,10 @@ jobs: fail-fast: false matrix: + conf: + - ConsistentState.conf script: - verifyAccrueInterest.sh - - certoraRun certora/configuration/ConsistentState.conf - verifyExactMath.sh - verifyExitLiquidity.sh - verifyHealth.sh From 2f8d60123a2593b75d083373d5bef8d9c67e9d0f Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 17 Oct 2023 12:16:46 +0200 Subject: [PATCH 149/204] fix: remove unused script row --- .github/workflows/certora.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml index fc567c38f..9dba61ddc 100644 --- a/.github/workflows/certora.yml +++ b/.github/workflows/certora.yml @@ -33,7 +33,6 @@ jobs: - name: Verify rule ${{ matrix.script }} run: | echo "key length" ${#CERTORAKEY} - bash certora/scripts/${{ matrix.script }} --solc solc8.19 certoraRun certora/configuration/${{ matrix.conf }} --solc solc8.19 env: CERTORAKEY: ${{ secrets.CERTORAKEY }} @@ -44,14 +43,3 @@ jobs: matrix: conf: - ConsistentState.conf - script: - - verifyAccrueInterest.sh - - verifyExactMath.sh - - verifyExitLiquidity.sh - - verifyHealth.sh - - verifyLibSummary.sh - - verifyLiveness.sh - - verifyRatioMath.sh - - verifyReentrancy.sh - - verifyReverts.sh - - verifyTransfer.sh From defa1e756d26a52079ce7eb532a52fc4117cee64 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 17 Oct 2023 12:17:45 +0200 Subject: [PATCH 150/204] chore: switch to certora-cli --- .github/workflows/certora.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml index 9dba61ddc..e0211d5cc 100644 --- a/.github/workflows/certora.yml +++ b/.github/workflows/certora.yml @@ -22,7 +22,7 @@ jobs: python-version: "3.10" - name: Install certora - run: pip install certora-cli-beta + run: pip install certora-cli - name: Install solc run: | From 473e750e55ce3516f56a2b180df3caad84a98c01 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 17 Oct 2023 13:18:06 +0200 Subject: [PATCH 151/204] feat: add every script as a configuration file --- .github/workflows/certora.yml | 16 +++++++++++++--- certora/configuration/AccrueInterest.conf | 12 ++++++++++++ certora/configuration/ExactMath.conf | 12 ++++++++++++ certora/configuration/ExitLiquidity.conf | 8 ++++++++ certora/configuration/Health.conf | 12 ++++++++++++ certora/configuration/LibSummary.conf | 8 ++++++++ certora/configuration/Liveness.conf | 8 ++++++++ certora/configuration/RatioMath.conf | 11 +++++++++++ certora/configuration/Reentrancy.conf | 11 +++++++++++ certora/configuration/Reverts.conf | 8 ++++++++ certora/configuration/Transfer.conf | 11 +++++++++++ certora/scripts/verifyAccrueInterest.sh | 10 ---------- certora/scripts/verifyExactMath.sh | 10 ---------- certora/scripts/verifyExitLiquidity.sh | 9 --------- certora/scripts/verifyHealth.sh | 11 ----------- certora/scripts/verifyLibSummary.sh | 9 --------- certora/scripts/verifyLiveness.sh | 9 --------- certora/scripts/verifyRatioMath.sh | 10 ---------- certora/scripts/verifyReentrancy.sh | 10 ---------- certora/scripts/verifyReverts.sh | 9 --------- certora/scripts/verifyTransfer.sh | 12 ------------ 21 files changed, 114 insertions(+), 102 deletions(-) create mode 100644 certora/configuration/AccrueInterest.conf create mode 100644 certora/configuration/ExactMath.conf create mode 100644 certora/configuration/ExitLiquidity.conf create mode 100644 certora/configuration/Health.conf create mode 100644 certora/configuration/LibSummary.conf create mode 100644 certora/configuration/Liveness.conf create mode 100644 certora/configuration/RatioMath.conf create mode 100644 certora/configuration/Reentrancy.conf create mode 100644 certora/configuration/Reverts.conf create mode 100644 certora/configuration/Transfer.conf delete mode 100755 certora/scripts/verifyAccrueInterest.sh delete mode 100755 certora/scripts/verifyExactMath.sh delete mode 100755 certora/scripts/verifyExitLiquidity.sh delete mode 100755 certora/scripts/verifyHealth.sh delete mode 100755 certora/scripts/verifyLibSummary.sh delete mode 100755 certora/scripts/verifyLiveness.sh delete mode 100755 certora/scripts/verifyRatioMath.sh delete mode 100755 certora/scripts/verifyReentrancy.sh delete mode 100755 certora/scripts/verifyReverts.sh delete mode 100755 certora/scripts/verifyTransfer.sh diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml index e0211d5cc..ec1780b07 100644 --- a/.github/workflows/certora.yml +++ b/.github/workflows/certora.yml @@ -28,12 +28,12 @@ jobs: run: | wget https://github.com/ethereum/solidity/releases/download/v0.8.19/solc-static-linux chmod +x solc-static-linux - sudo mv solc-static-linux /usr/local/bin/solc8.19 + sudo mv solc-static-linux /usr/local/bin/solc - name: Verify rule ${{ matrix.script }} run: | echo "key length" ${#CERTORAKEY} - certoraRun certora/configuration/${{ matrix.conf }} --solc solc8.19 + certoraRun certora/configuration/${{ matrix.conf }} env: CERTORAKEY: ${{ secrets.CERTORAKEY }} @@ -42,4 +42,14 @@ jobs: matrix: conf: - - ConsistentState.conf + - AccrueInterest + - ConsistentState + - ExactMath + - ExitLiquidity + - Health + - LibSummary + - Liveness + - RatioMath + - Reentrancy + - Reverts + - Transfer diff --git a/certora/configuration/AccrueInterest.conf b/certora/configuration/AccrueInterest.conf new file mode 100644 index 000000000..33696fef8 --- /dev/null +++ b/certora/configuration/AccrueInterest.conf @@ -0,0 +1,12 @@ +{ + "files": [ + "certora/harness/MorphoHarness.sol" + ], + "msg": "Morpho Blue Accrue Interest", + "process": "emv", + "prover_args": [ + "-smt_hashingScheme plaininjectivity", + "-mediumTimeout 30" + ], + "verify": "MorphoHarness:certora/specs/AccrueInterest.spec" +} diff --git a/certora/configuration/ExactMath.conf b/certora/configuration/ExactMath.conf new file mode 100644 index 000000000..ee362c157 --- /dev/null +++ b/certora/configuration/ExactMath.conf @@ -0,0 +1,12 @@ +{ + "files": [ + "certora/harness/MorphoHarness.sol" + ], + "msg": "Morpho Blue Exact Math", + "process": "emv", + "prover_args": [ + "-smt_hashingScheme plaininjectivity", + "-mediumTimeout 30" + ], + "verify": "MorphoHarness:certora/specs/ExactMath.spec" +} diff --git a/certora/configuration/ExitLiquidity.conf b/certora/configuration/ExitLiquidity.conf new file mode 100644 index 000000000..de28e803b --- /dev/null +++ b/certora/configuration/ExitLiquidity.conf @@ -0,0 +1,8 @@ +{ + "files": [ + "certora/harness/MorphoHarness.sol" + ], + "msg": "Morpho Blue Exit Liquidity", + "process": "emv", + "verify": "MorphoHarness:certora/specs/ExitLiquidity.spec" +} diff --git a/certora/configuration/Health.conf b/certora/configuration/Health.conf new file mode 100644 index 000000000..bdea9ff2a --- /dev/null +++ b/certora/configuration/Health.conf @@ -0,0 +1,12 @@ +{ + "files": [ + "certora/harness/MorphoHarness.sol", + "src/mocks/OracleMock.sol" + ], + "msg": "Morpho Blue Health", + "process": "emv", + "prover_args": [ + "-smt_hashingScheme plaininjectivity" + ], + "verify": "MorphoHarness:certora/specs/Health.spec" +} diff --git a/certora/configuration/LibSummary.conf b/certora/configuration/LibSummary.conf new file mode 100644 index 000000000..ecff125e1 --- /dev/null +++ b/certora/configuration/LibSummary.conf @@ -0,0 +1,8 @@ +{ + "files": [ + "certora/harness/MorphoHarness.sol" + ], + "msg": "Morpho Blue Lib Summary", + "process": "emv", + "verify": "MorphoHarness:certora/specs/LibSummary.spec" +} diff --git a/certora/configuration/Liveness.conf b/certora/configuration/Liveness.conf new file mode 100644 index 000000000..8481d19dd --- /dev/null +++ b/certora/configuration/Liveness.conf @@ -0,0 +1,8 @@ +{ + "files": [ + "certora/harness/MorphoInternalAccess.sol" + ], + "msg": "Morpho Blue Liveness", + "process": "emv", + "verify": "MorphoInternalAccess:certora/specs/Liveness.spec" +} diff --git a/certora/configuration/RatioMath.conf b/certora/configuration/RatioMath.conf new file mode 100644 index 000000000..c770cba68 --- /dev/null +++ b/certora/configuration/RatioMath.conf @@ -0,0 +1,11 @@ +{ + "files": [ + "certora/harness/MorphoHarness.sol" + ], + "msg": "Morpho Blue Ratio Math", + "process": "emv", + "prover_args": [ + "-smt_hashingScheme plaininjectivity" + ], + "verify": "MorphoHarness:certora/specs/RatioMath.spec" +} diff --git a/certora/configuration/Reentrancy.conf b/certora/configuration/Reentrancy.conf new file mode 100644 index 000000000..c093b7dcb --- /dev/null +++ b/certora/configuration/Reentrancy.conf @@ -0,0 +1,11 @@ +{ + "files": [ + "certora/harness/MorphoHarness.sol" + ], + "msg": "Morpho Blue Reentrancy", + "process": "emv", + "prover_args": [ + "-enableStorageSplitting false" + ], + "verify": "MorphoHarness:certora/specs/Reentrancy.spec" +} diff --git a/certora/configuration/Reverts.conf b/certora/configuration/Reverts.conf new file mode 100644 index 000000000..34c3bc958 --- /dev/null +++ b/certora/configuration/Reverts.conf @@ -0,0 +1,8 @@ +{ + "files": [ + "certora/harness/MorphoHarness.sol" + ], + "msg": "Morpho Blue Reverts", + "process": "emv", + "verify": "MorphoHarness:certora/specs/Reverts.spec" +} diff --git a/certora/configuration/Transfer.conf b/certora/configuration/Transfer.conf new file mode 100644 index 000000000..c732629ef --- /dev/null +++ b/certora/configuration/Transfer.conf @@ -0,0 +1,11 @@ +{ + "files": [ + "certora/harness/TransferHarness.sol", + "certora/dispatch/ERC20Standard.sol", + "certora/dispatch/ERC20USDT.sol", + "certora/dispatch/ERC20NoRevert.sol" + ], + "msg": "Morpho Blue Transfer", + "process": "emv", + "verify": "TransferHarness:certora/specs/Transfer.spec" +} \ No newline at end of file diff --git a/certora/scripts/verifyAccrueInterest.sh b/certora/scripts/verifyAccrueInterest.sh deleted file mode 100755 index 555a40d85..000000000 --- a/certora/scripts/verifyAccrueInterest.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -set -euxo pipefail - -certoraRun \ - certora/harness/MorphoHarness.sol \ - --verify MorphoHarness:certora/specs/AccrueInterest.spec \ - --prover_args '-smt_hashingScheme plaininjectivity -mediumTimeout 30' \ - --msg "Morpho Blue Accrue Interest" \ - "$@" diff --git a/certora/scripts/verifyExactMath.sh b/certora/scripts/verifyExactMath.sh deleted file mode 100755 index 0effcb7bd..000000000 --- a/certora/scripts/verifyExactMath.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -set -euxo pipefail - -certoraRun \ - certora/harness/MorphoHarness.sol \ - --verify MorphoHarness:certora/specs/ExactMath.spec \ - --prover_args '-smt_hashingScheme plaininjectivity -mediumTimeout 30' \ - --msg "Morpho Blue Exact Math" \ - "$@" diff --git a/certora/scripts/verifyExitLiquidity.sh b/certora/scripts/verifyExitLiquidity.sh deleted file mode 100755 index afdd603e6..000000000 --- a/certora/scripts/verifyExitLiquidity.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -set -euxo pipefail - -certoraRun \ - certora/harness/MorphoHarness.sol \ - --verify MorphoHarness:certora/specs/ExitLiquidity.spec \ - --msg "Morpho Blue Exit Liquidity" \ - "$@" diff --git a/certora/scripts/verifyHealth.sh b/certora/scripts/verifyHealth.sh deleted file mode 100755 index 0bd7a7c2b..000000000 --- a/certora/scripts/verifyHealth.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -set -euxo pipefail - -certoraRun \ - certora/harness/MorphoHarness.sol \ - src/mocks/OracleMock.sol \ - --verify MorphoHarness:certora/specs/Health.spec \ - --prover_args '-smt_hashingScheme plaininjectivity' \ - --msg "Morpho Blue Health Check" \ - "$@" diff --git a/certora/scripts/verifyLibSummary.sh b/certora/scripts/verifyLibSummary.sh deleted file mode 100755 index 41dcf9ca0..000000000 --- a/certora/scripts/verifyLibSummary.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -set -euxo pipefail - -certoraRun \ - certora/harness/MorphoHarness.sol \ - --verify MorphoHarness:certora/specs/LibSummary.spec \ - --msg "Morpho Blue Lib Summary" \ - "$@" diff --git a/certora/scripts/verifyLiveness.sh b/certora/scripts/verifyLiveness.sh deleted file mode 100755 index 260bc3757..000000000 --- a/certora/scripts/verifyLiveness.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -set -euxo pipefail - -certoraRun \ - certora/harness/MorphoInternalAccess.sol \ - --verify MorphoInternalAccess:certora/specs/Liveness.spec \ - --msg "Morpho Blue Liveness" \ - "$@" diff --git a/certora/scripts/verifyRatioMath.sh b/certora/scripts/verifyRatioMath.sh deleted file mode 100755 index c17eb1fc9..000000000 --- a/certora/scripts/verifyRatioMath.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -set -euxo pipefail - -certoraRun \ - certora/harness/MorphoHarness.sol \ - --verify MorphoHarness:certora/specs/RatioMath.spec \ - --prover_args '-smt_hashingScheme plaininjectivity' \ - --msg "Morpho Blue Ratio Math" \ - "$@" diff --git a/certora/scripts/verifyReentrancy.sh b/certora/scripts/verifyReentrancy.sh deleted file mode 100755 index 0ffa2d570..000000000 --- a/certora/scripts/verifyReentrancy.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -set -euxo pipefail - -certoraRun \ - certora/harness/MorphoHarness.sol \ - --verify MorphoHarness:certora/specs/Reentrancy.spec \ - --prover_args '-enableStorageSplitting false' \ - --msg "Morpho Blue Reentrancy" \ - "$@" diff --git a/certora/scripts/verifyReverts.sh b/certora/scripts/verifyReverts.sh deleted file mode 100755 index ae2675499..000000000 --- a/certora/scripts/verifyReverts.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -set -euxo pipefail - -certoraRun \ - certora/harness/MorphoHarness.sol \ - --verify MorphoHarness:certora/specs/Reverts.spec \ - --msg "Morpho Blue Reverts" \ - "$@" diff --git a/certora/scripts/verifyTransfer.sh b/certora/scripts/verifyTransfer.sh deleted file mode 100755 index 396585896..000000000 --- a/certora/scripts/verifyTransfer.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -set -euxo pipefail - -certoraRun \ - certora/harness/TransferHarness.sol \ - certora/dispatch/ERC20Standard.sol \ - certora/dispatch/ERC20USDT.sol \ - certora/dispatch/ERC20NoRevert.sol \ - --verify TransferHarness:certora/specs/Transfer.spec \ - --msg "Morpho Blue Transfer" \ - "$@" From 88c173958b18c663a4e8afe765307b5b57a4e73d Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 17 Oct 2023 13:20:11 +0200 Subject: [PATCH 152/204] fix: add missing extension on configuration files --- .github/workflows/certora.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml index ec1780b07..34c0930ba 100644 --- a/.github/workflows/certora.yml +++ b/.github/workflows/certora.yml @@ -33,7 +33,7 @@ jobs: - name: Verify rule ${{ matrix.script }} run: | echo "key length" ${#CERTORAKEY} - certoraRun certora/configuration/${{ matrix.conf }} + certoraRun certora/configuration/${{ matrix.conf }}.conf env: CERTORAKEY: ${{ secrets.CERTORAKEY }} From 093bcd2c5229b715cde6c2baeea41e8186b66574 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 17 Oct 2023 13:31:31 +0200 Subject: [PATCH 153/204] refactor: order parameters in configuration files --- certora/configuration/AccrueInterest.conf | 5 ++--- certora/configuration/ConsistentState.conf | 5 ++--- certora/configuration/ExactMath.conf | 5 ++--- certora/configuration/ExitLiquidity.conf | 3 +-- certora/configuration/Health.conf | 5 ++--- certora/configuration/Liveness.conf | 5 ++--- certora/configuration/RatioMath.conf | 5 ++--- certora/configuration/Reentrancy.conf | 5 ++--- certora/configuration/Reverts.conf | 5 ++--- certora/configuration/Transfer.conf | 7 +++---- 10 files changed, 20 insertions(+), 30 deletions(-) diff --git a/certora/configuration/AccrueInterest.conf b/certora/configuration/AccrueInterest.conf index 33696fef8..fbc772df3 100644 --- a/certora/configuration/AccrueInterest.conf +++ b/certora/configuration/AccrueInterest.conf @@ -2,11 +2,10 @@ "files": [ "certora/harness/MorphoHarness.sol" ], - "msg": "Morpho Blue Accrue Interest", - "process": "emv", + "verify": "MorphoHarness:certora/specs/AccrueInterest.spec", "prover_args": [ "-smt_hashingScheme plaininjectivity", "-mediumTimeout 30" ], - "verify": "MorphoHarness:certora/specs/AccrueInterest.spec" + "msg": "Morpho Blue Accrue Interest" } diff --git a/certora/configuration/ConsistentState.conf b/certora/configuration/ConsistentState.conf index 5b507bf4b..b9e774bc7 100644 --- a/certora/configuration/ConsistentState.conf +++ b/certora/configuration/ConsistentState.conf @@ -2,7 +2,6 @@ "files": [ "certora/harness/MorphoHarness.sol" ], - "msg": "Morpho Blue Consistent State", - "process": "emv", - "verify": "MorphoHarness:certora/specs/ConsistentState.spec" + "verify": "MorphoHarness:certora/specs/ConsistentState.spec", + "msg": "Morpho Blue Consistent State" } diff --git a/certora/configuration/ExactMath.conf b/certora/configuration/ExactMath.conf index ee362c157..01c7f3f22 100644 --- a/certora/configuration/ExactMath.conf +++ b/certora/configuration/ExactMath.conf @@ -2,11 +2,10 @@ "files": [ "certora/harness/MorphoHarness.sol" ], - "msg": "Morpho Blue Exact Math", - "process": "emv", + "verify": "MorphoHarness:certora/specs/ExactMath.spec", "prover_args": [ "-smt_hashingScheme plaininjectivity", "-mediumTimeout 30" ], - "verify": "MorphoHarness:certora/specs/ExactMath.spec" + "msg": "Morpho Blue Exact Math" } diff --git a/certora/configuration/ExitLiquidity.conf b/certora/configuration/ExitLiquidity.conf index de28e803b..5e31571b1 100644 --- a/certora/configuration/ExitLiquidity.conf +++ b/certora/configuration/ExitLiquidity.conf @@ -2,7 +2,6 @@ "files": [ "certora/harness/MorphoHarness.sol" ], - "msg": "Morpho Blue Exit Liquidity", - "process": "emv", "verify": "MorphoHarness:certora/specs/ExitLiquidity.spec" + "msg": "Morpho Blue Exit Liquidity" } diff --git a/certora/configuration/Health.conf b/certora/configuration/Health.conf index bdea9ff2a..a84180e29 100644 --- a/certora/configuration/Health.conf +++ b/certora/configuration/Health.conf @@ -3,10 +3,9 @@ "certora/harness/MorphoHarness.sol", "src/mocks/OracleMock.sol" ], - "msg": "Morpho Blue Health", - "process": "emv", + "verify": "MorphoHarness:certora/specs/Health.spec", "prover_args": [ "-smt_hashingScheme plaininjectivity" ], - "verify": "MorphoHarness:certora/specs/Health.spec" + "msg": "Morpho Blue Health" } diff --git a/certora/configuration/Liveness.conf b/certora/configuration/Liveness.conf index 8481d19dd..44cddd6d3 100644 --- a/certora/configuration/Liveness.conf +++ b/certora/configuration/Liveness.conf @@ -2,7 +2,6 @@ "files": [ "certora/harness/MorphoInternalAccess.sol" ], - "msg": "Morpho Blue Liveness", - "process": "emv", - "verify": "MorphoInternalAccess:certora/specs/Liveness.spec" + "verify": "MorphoInternalAccess:certora/specs/Liveness.spec", + "msg": "Morpho Blue Liveness" } diff --git a/certora/configuration/RatioMath.conf b/certora/configuration/RatioMath.conf index c770cba68..ee100b758 100644 --- a/certora/configuration/RatioMath.conf +++ b/certora/configuration/RatioMath.conf @@ -2,10 +2,9 @@ "files": [ "certora/harness/MorphoHarness.sol" ], - "msg": "Morpho Blue Ratio Math", - "process": "emv", + "verify": "MorphoHarness:certora/specs/RatioMath.spec", "prover_args": [ "-smt_hashingScheme plaininjectivity" ], - "verify": "MorphoHarness:certora/specs/RatioMath.spec" + "msg": "Morpho Blue Ratio Math" } diff --git a/certora/configuration/Reentrancy.conf b/certora/configuration/Reentrancy.conf index c093b7dcb..f2e26e264 100644 --- a/certora/configuration/Reentrancy.conf +++ b/certora/configuration/Reentrancy.conf @@ -2,10 +2,9 @@ "files": [ "certora/harness/MorphoHarness.sol" ], - "msg": "Morpho Blue Reentrancy", - "process": "emv", + "verify": "MorphoHarness:certora/specs/Reentrancy.spec", "prover_args": [ "-enableStorageSplitting false" ], - "verify": "MorphoHarness:certora/specs/Reentrancy.spec" + "msg": "Morpho Blue Reentrancy" } diff --git a/certora/configuration/Reverts.conf b/certora/configuration/Reverts.conf index 34c3bc958..c414253b2 100644 --- a/certora/configuration/Reverts.conf +++ b/certora/configuration/Reverts.conf @@ -2,7 +2,6 @@ "files": [ "certora/harness/MorphoHarness.sol" ], - "msg": "Morpho Blue Reverts", - "process": "emv", - "verify": "MorphoHarness:certora/specs/Reverts.spec" + "verify": "MorphoHarness:certora/specs/Reverts.spec", + "msg": "Morpho Blue Reverts" } diff --git a/certora/configuration/Transfer.conf b/certora/configuration/Transfer.conf index c732629ef..ae018d869 100644 --- a/certora/configuration/Transfer.conf +++ b/certora/configuration/Transfer.conf @@ -5,7 +5,6 @@ "certora/dispatch/ERC20USDT.sol", "certora/dispatch/ERC20NoRevert.sol" ], - "msg": "Morpho Blue Transfer", - "process": "emv", - "verify": "TransferHarness:certora/specs/Transfer.spec" -} \ No newline at end of file + "verify": "TransferHarness:certora/specs/Transfer.spec", + "msg": "Morpho Blue Transfer" +} From 80995c80207b023e1c359f0154b3a721884fe8e6 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 17 Oct 2023 13:33:06 +0200 Subject: [PATCH 154/204] fix: missing comma --- certora/configuration/ExitLiquidity.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certora/configuration/ExitLiquidity.conf b/certora/configuration/ExitLiquidity.conf index 5e31571b1..b58c24657 100644 --- a/certora/configuration/ExitLiquidity.conf +++ b/certora/configuration/ExitLiquidity.conf @@ -2,6 +2,6 @@ "files": [ "certora/harness/MorphoHarness.sol" ], - "verify": "MorphoHarness:certora/specs/ExitLiquidity.spec" + "verify": "MorphoHarness:certora/specs/ExitLiquidity.spec", "msg": "Morpho Blue Exit Liquidity" } From dfe4834904621fa28190da0490ea9b98c2f10664 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 18 Oct 2023 14:15:00 +0200 Subject: [PATCH 155/204] docs: update README to configuration files --- certora/README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/certora/README.md b/certora/README.md index df41364b3..153e21334 100644 --- a/certora/README.md +++ b/certora/README.md @@ -266,10 +266,12 @@ The [`certora/dispatch`](./dispatch) folder contains different contracts simil # Getting started -To verify specification files, run the corresponding script in the [`certora/scripts`](./scripts) folder. +Install `certoraRun` with `pip install certora-cli`. +To verify specification files, pass the corresponding configuration file in the [`certora/configuration`](./configuration) folder. It requires having set the `CERTORAKEY` environment variable to a valid Certora key. -You can pass arguments to the script, which allows you to verify specific properties. For example, at the root of the repository: +You can also pass additional arguments, notably to verify a specific rule. +For example, at the root of the repository: ``` -./certora/scripts/verifyConsistentState.sh --rule borrowLessThanSupply +certoraRun certora/configuration/ConsistentState.conf --rule borrowLessThanSupply ``` From 0ff92f8ee5aadb703dbc7f0073efd7cc47ffcabd Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 19 Oct 2023 10:55:12 +0200 Subject: [PATCH 156/204] refactor: missing refactor of LibSummary.conf --- certora/configuration/LibSummary.conf | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/certora/configuration/LibSummary.conf b/certora/configuration/LibSummary.conf index ecff125e1..cc89d89ca 100644 --- a/certora/configuration/LibSummary.conf +++ b/certora/configuration/LibSummary.conf @@ -2,7 +2,6 @@ "files": [ "certora/harness/MorphoHarness.sol" ], - "msg": "Morpho Blue Lib Summary", - "process": "emv", - "verify": "MorphoHarness:certora/specs/LibSummary.spec" + "verify": "MorphoHarness:certora/specs/LibSummary.spec", + "msg": "Morpho Blue Lib Summary" } From f914b28b4d4894700b02dcee6f48dc6972d89012 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 21 Nov 2023 10:03:51 +0100 Subject: [PATCH 157/204] fix: remove unnecessary accrueInterest harness function --- certora/harness/MorphoHarness.sol | 7 ------- 1 file changed, 7 deletions(-) diff --git a/certora/harness/MorphoHarness.sol b/certora/harness/MorphoHarness.sol index 0f4ac6ed6..8eac2f190 100644 --- a/certora/harness/MorphoHarness.sol +++ b/certora/harness/MorphoHarness.sol @@ -86,13 +86,6 @@ contract MorphoHarness is Morpho { return MathLib.mulDivDown(x, y, d); } - function accrueInterest(MarketParams memory marketParams) external { - Id id = marketParams.id(); - require(market[id].lastUpdate != 0, ErrorsLib.MARKET_NOT_CREATED); - - _accrueInterest(marketParams, id); - } - function isHealthy(MarketParams memory marketParams, address user) external view returns (bool) { return _isHealthy(marketParams, marketParams.id(), user); } From b7872868333704fdf80d23c824ed8043f42644e6 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 21 Nov 2023 10:08:20 +0100 Subject: [PATCH 158/204] refactor: harness visibility to external --- certora/harness/MorphoHarness.sol | 4 ++-- certora/harness/TransferHarness.sol | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/certora/harness/MorphoHarness.sol b/certora/harness/MorphoHarness.sol index 8eac2f190..1a1912aeb 100644 --- a/certora/harness/MorphoHarness.sol +++ b/certora/harness/MorphoHarness.sol @@ -78,11 +78,11 @@ contract MorphoHarness is Morpho { marketParamsId = Id.wrap(keccak256(abi.encode(marketParams))); } - function libMulDivUp(uint256 x, uint256 y, uint256 d) public pure returns (uint256) { + function libMulDivUp(uint256 x, uint256 y, uint256 d) external pure returns (uint256) { return MathLib.mulDivUp(x, y, d); } - function libMulDivDown(uint256 x, uint256 y, uint256 d) public pure returns (uint256) { + function libMulDivDown(uint256 x, uint256 y, uint256 d) external pure returns (uint256) { return MathLib.mulDivDown(x, y, d); } diff --git a/certora/harness/TransferHarness.sol b/certora/harness/TransferHarness.sol index 5e6954a46..67a6d88c8 100644 --- a/certora/harness/TransferHarness.sol +++ b/certora/harness/TransferHarness.sol @@ -13,23 +13,23 @@ interface IERC20Extended is IERC20 { contract TransferHarness { using SafeTransferLib for IERC20; - function libSafeTransferFrom(address token, address from, address to, uint256 value) public { + function libSafeTransferFrom(address token, address from, address to, uint256 value) external { IERC20(token).safeTransferFrom(from, to, value); } - function libSafeTransfer(address token, address to, uint256 value) public { + function libSafeTransfer(address token, address to, uint256 value) external { IERC20(token).safeTransfer(to, value); } - function balanceOf(address token, address user) public view returns (uint256) { + function balanceOf(address token, address user) external view returns (uint256) { return IERC20Extended(token).balanceOf(user); } - function allowance(address token, address owner, address spender) public view returns (uint256) { + function allowance(address token, address owner, address spender) external view returns (uint256) { return IERC20Extended(token).allowance(owner, spender); } - function totalSupply(address token) public view returns (uint256) { + function totalSupply(address token) external view returns (uint256) { return IERC20Extended(token).totalSupply(); } } From c11af7c2e981945014cdc4d1f796b362c094da39 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 21 Nov 2023 11:39:23 +0100 Subject: [PATCH 159/204] fix: precendence of equality and equivalence --- certora/specs/Transfer.spec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certora/specs/Transfer.spec b/certora/specs/Transfer.spec index 5a92cfec0..e9861d323 100644 --- a/certora/specs/Transfer.spec +++ b/certora/specs/Transfer.spec @@ -68,7 +68,7 @@ rule transferRevertCondition(address token, address to, uint256 amount) { libSafeTransfer@withrevert(token, to, amount); - assert lastReverted == (initialBalance < amount); + assert lastReverted <=> initialBalance < amount; } // Check the revert condition of the summary of safeTransferFrom. @@ -83,5 +83,5 @@ rule transferFromRevertCondition(address token, address from, address to, uint25 libSafeTransferFrom@withrevert(token, from, to, amount); - assert lastReverted == (initialBalance < amount) || allowance < amount; + assert lastReverted <=> initialBalance < amount || allowance < amount; } From 526b2de58981ea5226d1323c8b761f674cbfc016 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 21 Nov 2023 11:40:26 +0100 Subject: [PATCH 160/204] chore: increase timeout for ratio math --- certora/configuration/RatioMath.conf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/certora/configuration/RatioMath.conf b/certora/configuration/RatioMath.conf index ee100b758..5eaf797ec 100644 --- a/certora/configuration/RatioMath.conf +++ b/certora/configuration/RatioMath.conf @@ -4,7 +4,8 @@ ], "verify": "MorphoHarness:certora/specs/RatioMath.spec", "prover_args": [ - "-smt_hashingScheme plaininjectivity" + "-smt_hashingScheme plaininjectivity", + "-mediumTimeout 30" ], "msg": "Morpho Blue Ratio Math" } From f5dd19449ebdaa77e0e4a849ca147335c4d33dad Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 21 Nov 2023 14:27:49 +0100 Subject: [PATCH 161/204] fix: increase timeout for RatioMath --- certora/configuration/RatioMath.conf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/certora/configuration/RatioMath.conf b/certora/configuration/RatioMath.conf index 5eaf797ec..400061d5b 100644 --- a/certora/configuration/RatioMath.conf +++ b/certora/configuration/RatioMath.conf @@ -5,7 +5,8 @@ "verify": "MorphoHarness:certora/specs/RatioMath.spec", "prover_args": [ "-smt_hashingScheme plaininjectivity", - "-mediumTimeout 30" + "-mediumTimeout 30", + "-timeout 3600" ], "msg": "Morpho Blue Ratio Math" } From 6c99282d30fe8ffe9b81f061d57af70615119730 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 21 Nov 2023 15:16:25 +0100 Subject: [PATCH 162/204] chore: update to v5 DELETE syntax --- certora/specs/AccrueInterest.spec | 2 +- certora/specs/ConsistentState.spec | 2 +- certora/specs/ExactMath.spec | 2 +- certora/specs/ExitLiquidity.spec | 2 +- certora/specs/Health.spec | 2 +- certora/specs/LibSummary.spec | 2 +- certora/specs/Liveness.spec | 2 +- certora/specs/RatioMath.spec | 2 +- certora/specs/Reentrancy.spec | 2 +- certora/specs/Reverts.spec | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/certora/specs/AccrueInterest.spec b/certora/specs/AccrueInterest.spec index 3af214790..824cce07e 100644 --- a/certora/specs/AccrueInterest.spec +++ b/certora/specs/AccrueInterest.spec @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later methods { - function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); + function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE; function MathLib.mulDivDown(uint256 a, uint256 b, uint256 c) internal returns uint256 => ghostMulDivDown(a,b,c); function MathLib.mulDivUp(uint256 a, uint256 b, uint256 c) internal returns uint256 => ghostMulDivUp(a,b,c); function MathLib.wTaylorCompounded(uint256 a, uint256 b) internal returns uint256 => ghostTaylorCompounded(a, b); diff --git a/certora/specs/ConsistentState.spec b/certora/specs/ConsistentState.spec index 5324bd5b4..49e0c1df8 100644 --- a/certora/specs/ConsistentState.spec +++ b/certora/specs/ConsistentState.spec @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later methods { - function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); + function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE; function totalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; function totalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; function totalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree; diff --git a/certora/specs/ExactMath.spec b/certora/specs/ExactMath.spec index 3ca2c3736..6e08543c8 100644 --- a/certora/specs/ExactMath.spec +++ b/certora/specs/ExactMath.spec @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later methods { - function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); + function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE; function libId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; function virtualTotalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; function virtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; diff --git a/certora/specs/ExitLiquidity.spec b/certora/specs/ExitLiquidity.spec index 30036108b..bf90ec31c 100644 --- a/certora/specs/ExitLiquidity.spec +++ b/certora/specs/ExitLiquidity.spec @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later methods { - function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); + function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE; function supplyShares(MorphoHarness.Id, address) external returns uint256 envfree; function borrowShares(MorphoHarness.Id, address) external returns uint256 envfree; function collateral(MorphoHarness.Id, address) external returns uint256 envfree; diff --git a/certora/specs/Health.spec b/certora/specs/Health.spec index 3602de29c..bac2aeba1 100644 --- a/certora/specs/Health.spec +++ b/certora/specs/Health.spec @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later methods { - function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); + function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE; function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; function totalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; function borrowShares(MorphoHarness.Id, address) external returns uint256 envfree; diff --git a/certora/specs/LibSummary.spec b/certora/specs/LibSummary.spec index 21cd67538..64322f7dc 100644 --- a/certora/specs/LibSummary.spec +++ b/certora/specs/LibSummary.spec @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later methods { - function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); + function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE; function libMulDivUp(uint256, uint256, uint256) external returns uint256 envfree; function libMulDivDown(uint256, uint256, uint256) external returns uint256 envfree; function libId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; diff --git a/certora/specs/Liveness.spec b/certora/specs/Liveness.spec index 5e7f46b1c..e5b96af82 100644 --- a/certora/specs/Liveness.spec +++ b/certora/specs/Liveness.spec @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later methods { - function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); + function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE; function totalSupplyAssets(MorphoInternalAccess.Id) external returns uint256 envfree; function totalSupplyShares(MorphoInternalAccess.Id) external returns uint256 envfree; function totalBorrowAssets(MorphoInternalAccess.Id) external returns uint256 envfree; diff --git a/certora/specs/RatioMath.spec b/certora/specs/RatioMath.spec index af690462e..2d138ad13 100644 --- a/certora/specs/RatioMath.spec +++ b/certora/specs/RatioMath.spec @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later methods { - function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); + function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE; function libId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; function virtualTotalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; function virtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; diff --git a/certora/specs/Reentrancy.spec b/certora/specs/Reentrancy.spec index 0a55276ea..5cb2b1639 100644 --- a/certora/specs/Reentrancy.spec +++ b/certora/specs/Reentrancy.spec @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later methods { - function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); + function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE; function _.borrowRate(MorphoHarness.MarketParams marketParams, MorphoHarness.Market) external => summaryBorrowRate() expect uint256; } diff --git a/certora/specs/Reverts.spec b/certora/specs/Reverts.spec index c54c06a21..7ecb5be1c 100644 --- a/certora/specs/Reverts.spec +++ b/certora/specs/Reverts.spec @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later methods { - function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE(true); + function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE; function owner() external returns address envfree; function totalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; function totalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree; From f196ce0070fc1be8e079bb6e4f7580979ee6a5b9 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 21 Nov 2023 15:11:45 +0100 Subject: [PATCH 163/204] feat: add basic sanity checks --- certora/configuration/AccrueInterest.conf | 1 + certora/configuration/ConsistentState.conf | 1 + certora/configuration/ExactMath.conf | 1 + certora/configuration/ExitLiquidity.conf | 1 + certora/configuration/Health.conf | 1 + certora/configuration/LibSummary.conf | 1 + certora/configuration/Liveness.conf | 1 + certora/configuration/RatioMath.conf | 1 + certora/configuration/Reentrancy.conf | 1 + certora/configuration/Reverts.conf | 1 + certora/configuration/Transfer.conf | 1 + 11 files changed, 11 insertions(+) diff --git a/certora/configuration/AccrueInterest.conf b/certora/configuration/AccrueInterest.conf index fbc772df3..a923c628e 100644 --- a/certora/configuration/AccrueInterest.conf +++ b/certora/configuration/AccrueInterest.conf @@ -7,5 +7,6 @@ "-smt_hashingScheme plaininjectivity", "-mediumTimeout 30" ], + "rule_sanity": "basic", "msg": "Morpho Blue Accrue Interest" } diff --git a/certora/configuration/ConsistentState.conf b/certora/configuration/ConsistentState.conf index b9e774bc7..9691cfcb1 100644 --- a/certora/configuration/ConsistentState.conf +++ b/certora/configuration/ConsistentState.conf @@ -3,5 +3,6 @@ "certora/harness/MorphoHarness.sol" ], "verify": "MorphoHarness:certora/specs/ConsistentState.spec", + "rule_sanity": "basic", "msg": "Morpho Blue Consistent State" } diff --git a/certora/configuration/ExactMath.conf b/certora/configuration/ExactMath.conf index 01c7f3f22..26a0d6d2b 100644 --- a/certora/configuration/ExactMath.conf +++ b/certora/configuration/ExactMath.conf @@ -3,6 +3,7 @@ "certora/harness/MorphoHarness.sol" ], "verify": "MorphoHarness:certora/specs/ExactMath.spec", + "rule_sanity": "basic", "prover_args": [ "-smt_hashingScheme plaininjectivity", "-mediumTimeout 30" diff --git a/certora/configuration/ExitLiquidity.conf b/certora/configuration/ExitLiquidity.conf index b58c24657..336359251 100644 --- a/certora/configuration/ExitLiquidity.conf +++ b/certora/configuration/ExitLiquidity.conf @@ -3,5 +3,6 @@ "certora/harness/MorphoHarness.sol" ], "verify": "MorphoHarness:certora/specs/ExitLiquidity.spec", + "rule_sanity": "basic", "msg": "Morpho Blue Exit Liquidity" } diff --git a/certora/configuration/Health.conf b/certora/configuration/Health.conf index a84180e29..a75fa1bb6 100644 --- a/certora/configuration/Health.conf +++ b/certora/configuration/Health.conf @@ -4,6 +4,7 @@ "src/mocks/OracleMock.sol" ], "verify": "MorphoHarness:certora/specs/Health.spec", + "rule_sanity": "basic", "prover_args": [ "-smt_hashingScheme plaininjectivity" ], diff --git a/certora/configuration/LibSummary.conf b/certora/configuration/LibSummary.conf index cc89d89ca..4aebed846 100644 --- a/certora/configuration/LibSummary.conf +++ b/certora/configuration/LibSummary.conf @@ -3,5 +3,6 @@ "certora/harness/MorphoHarness.sol" ], "verify": "MorphoHarness:certora/specs/LibSummary.spec", + "rule_sanity": "basic", "msg": "Morpho Blue Lib Summary" } diff --git a/certora/configuration/Liveness.conf b/certora/configuration/Liveness.conf index 44cddd6d3..2659b17b1 100644 --- a/certora/configuration/Liveness.conf +++ b/certora/configuration/Liveness.conf @@ -3,5 +3,6 @@ "certora/harness/MorphoInternalAccess.sol" ], "verify": "MorphoInternalAccess:certora/specs/Liveness.spec", + "rule_sanity": "basic", "msg": "Morpho Blue Liveness" } diff --git a/certora/configuration/RatioMath.conf b/certora/configuration/RatioMath.conf index 400061d5b..700c436e2 100644 --- a/certora/configuration/RatioMath.conf +++ b/certora/configuration/RatioMath.conf @@ -3,6 +3,7 @@ "certora/harness/MorphoHarness.sol" ], "verify": "MorphoHarness:certora/specs/RatioMath.spec", + "rule_sanity": "basic", "prover_args": [ "-smt_hashingScheme plaininjectivity", "-mediumTimeout 30", diff --git a/certora/configuration/Reentrancy.conf b/certora/configuration/Reentrancy.conf index f2e26e264..0fe902cfb 100644 --- a/certora/configuration/Reentrancy.conf +++ b/certora/configuration/Reentrancy.conf @@ -3,6 +3,7 @@ "certora/harness/MorphoHarness.sol" ], "verify": "MorphoHarness:certora/specs/Reentrancy.spec", + "rule_sanity": "basic", "prover_args": [ "-enableStorageSplitting false" ], diff --git a/certora/configuration/Reverts.conf b/certora/configuration/Reverts.conf index c414253b2..92ce70bff 100644 --- a/certora/configuration/Reverts.conf +++ b/certora/configuration/Reverts.conf @@ -3,5 +3,6 @@ "certora/harness/MorphoHarness.sol" ], "verify": "MorphoHarness:certora/specs/Reverts.spec", + "rule_sanity": "basic", "msg": "Morpho Blue Reverts" } diff --git a/certora/configuration/Transfer.conf b/certora/configuration/Transfer.conf index ae018d869..ac1bdeee1 100644 --- a/certora/configuration/Transfer.conf +++ b/certora/configuration/Transfer.conf @@ -6,5 +6,6 @@ "certora/dispatch/ERC20NoRevert.sol" ], "verify": "TransferHarness:certora/specs/Transfer.spec", + "rule_sanity": "basic", "msg": "Morpho Blue Transfer" } From d6524d9ac62fffa4213e5bd479219a0c370a93df Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 21 Nov 2023 16:50:50 +0100 Subject: [PATCH 164/204] refactor: rename configuration folder to confs --- certora/README.md | 6 +++--- certora/{configuration => confs}/AccrueInterest.conf | 0 certora/{configuration => confs}/ConsistentState.conf | 0 certora/{configuration => confs}/ExactMath.conf | 0 certora/{configuration => confs}/ExitLiquidity.conf | 0 certora/{configuration => confs}/Health.conf | 0 certora/{configuration => confs}/LibSummary.conf | 0 certora/{configuration => confs}/Liveness.conf | 0 certora/{configuration => confs}/RatioMath.conf | 0 certora/{configuration => confs}/Reentrancy.conf | 0 certora/{configuration => confs}/Reverts.conf | 0 certora/{configuration => confs}/Transfer.conf | 0 12 files changed, 3 insertions(+), 3 deletions(-) rename certora/{configuration => confs}/AccrueInterest.conf (100%) rename certora/{configuration => confs}/ConsistentState.conf (100%) rename certora/{configuration => confs}/ExactMath.conf (100%) rename certora/{configuration => confs}/ExitLiquidity.conf (100%) rename certora/{configuration => confs}/Health.conf (100%) rename certora/{configuration => confs}/LibSummary.conf (100%) rename certora/{configuration => confs}/Liveness.conf (100%) rename certora/{configuration => confs}/RatioMath.conf (100%) rename certora/{configuration => confs}/Reentrancy.conf (100%) rename certora/{configuration => confs}/Reverts.conf (100%) rename certora/{configuration => confs}/Transfer.conf (100%) diff --git a/certora/README.md b/certora/README.md index 153e21334..b9dda26aa 100644 --- a/certora/README.md +++ b/certora/README.md @@ -257,7 +257,7 @@ The [`certora/specs`](./specs) folder contains the following files: - [`Reverts.spec`](./specs/Reverts.spec) checks the condition for reverts and that inputs are correctly validated. - [`Transfer.spec`](./specs/Transfer.spec) checks the summarization of the safe transfer library functions that are used in other specification files. -The [`certora/scripts`](./scripts) folder contains a script for each corresponding specification file. +The [`certora/confs`](./confs) folder contains a configuration file for each corresponding specification file. The [`certora/harness`](./harness) folder contains contracts that enable the verification of Morpho Blue. Notably, this allows handling the fact that library functions should be called from a contract to be verified independently, and it allows defining needed getters. @@ -267,11 +267,11 @@ The [`certora/dispatch`](./dispatch) folder contains different contracts simil # Getting started Install `certoraRun` with `pip install certora-cli`. -To verify specification files, pass the corresponding configuration file in the [`certora/configuration`](./configuration) folder. +To verify specification files, pass the corresponding configuration file in the [`certora/confs`](./confs) folder. It requires having set the `CERTORAKEY` environment variable to a valid Certora key. You can also pass additional arguments, notably to verify a specific rule. For example, at the root of the repository: ``` -certoraRun certora/configuration/ConsistentState.conf --rule borrowLessThanSupply +certoraRun certora/confs/ConsistentState.conf --rule borrowLessThanSupply ``` diff --git a/certora/configuration/AccrueInterest.conf b/certora/confs/AccrueInterest.conf similarity index 100% rename from certora/configuration/AccrueInterest.conf rename to certora/confs/AccrueInterest.conf diff --git a/certora/configuration/ConsistentState.conf b/certora/confs/ConsistentState.conf similarity index 100% rename from certora/configuration/ConsistentState.conf rename to certora/confs/ConsistentState.conf diff --git a/certora/configuration/ExactMath.conf b/certora/confs/ExactMath.conf similarity index 100% rename from certora/configuration/ExactMath.conf rename to certora/confs/ExactMath.conf diff --git a/certora/configuration/ExitLiquidity.conf b/certora/confs/ExitLiquidity.conf similarity index 100% rename from certora/configuration/ExitLiquidity.conf rename to certora/confs/ExitLiquidity.conf diff --git a/certora/configuration/Health.conf b/certora/confs/Health.conf similarity index 100% rename from certora/configuration/Health.conf rename to certora/confs/Health.conf diff --git a/certora/configuration/LibSummary.conf b/certora/confs/LibSummary.conf similarity index 100% rename from certora/configuration/LibSummary.conf rename to certora/confs/LibSummary.conf diff --git a/certora/configuration/Liveness.conf b/certora/confs/Liveness.conf similarity index 100% rename from certora/configuration/Liveness.conf rename to certora/confs/Liveness.conf diff --git a/certora/configuration/RatioMath.conf b/certora/confs/RatioMath.conf similarity index 100% rename from certora/configuration/RatioMath.conf rename to certora/confs/RatioMath.conf diff --git a/certora/configuration/Reentrancy.conf b/certora/confs/Reentrancy.conf similarity index 100% rename from certora/configuration/Reentrancy.conf rename to certora/confs/Reentrancy.conf diff --git a/certora/configuration/Reverts.conf b/certora/confs/Reverts.conf similarity index 100% rename from certora/configuration/Reverts.conf rename to certora/confs/Reverts.conf diff --git a/certora/configuration/Transfer.conf b/certora/confs/Transfer.conf similarity index 100% rename from certora/configuration/Transfer.conf rename to certora/confs/Transfer.conf From e58b8a2b4b57f4a952764c85e8675496c5b647e7 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 22 Nov 2023 11:54:28 +0100 Subject: [PATCH 165/204] fix: update CI path to CVL conf files --- .github/workflows/certora.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml index 34c0930ba..bd08ca33f 100644 --- a/.github/workflows/certora.yml +++ b/.github/workflows/certora.yml @@ -33,7 +33,7 @@ jobs: - name: Verify rule ${{ matrix.script }} run: | echo "key length" ${#CERTORAKEY} - certoraRun certora/configuration/${{ matrix.conf }}.conf + certoraRun certora/confs/${{ matrix.conf }}.conf env: CERTORAKEY: ${{ secrets.CERTORAKEY }} From 810662488a6859057aeb4bc0dd7f0e79717ccd91 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 21 Nov 2023 14:42:21 +0100 Subject: [PATCH 166/204] feat: add gambit initial configuration --- certora/gambit.conf | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 certora/gambit.conf diff --git a/certora/gambit.conf b/certora/gambit.conf new file mode 100644 index 000000000..fe0dd6f3f --- /dev/null +++ b/certora/gambit.conf @@ -0,0 +1,7 @@ + { + "filename" : "src/Morpho.sol", + "solc": "solc8.19", + "sourceroot": "..", + "num_mutants": 15, + "solc_remappings": [] +} From d5c7303ee4f1f566c3f4a215be40683137e9f68e Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 31 Oct 2023 12:15:11 +0100 Subject: [PATCH 167/204] fix: remove breaking solc version --- certora/gambit.conf | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/certora/gambit.conf b/certora/gambit.conf index fe0dd6f3f..0dcba742c 100644 --- a/certora/gambit.conf +++ b/certora/gambit.conf @@ -1,6 +1,5 @@ { - "filename" : "src/Morpho.sol", - "solc": "solc8.19", + "filename" : "../src/Morpho.sol", "sourceroot": "..", "num_mutants": 15, "solc_remappings": [] From 766667f101c48525459dac56fe9dce5c0ddbd137 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 22 Nov 2023 19:35:44 +0100 Subject: [PATCH 168/204] test: mutant 2 --- src/Morpho.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index f755904b6..913ada68d 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -172,7 +172,9 @@ contract Morpho is IMorphoStaticTyping { ) external returns (uint256, uint256) { Id id = marketParams.id(); require(market[id].lastUpdate != 0, ErrorsLib.MARKET_NOT_CREATED); - require(UtilsLib.exactlyOneZero(assets, shares), ErrorsLib.INCONSISTENT_INPUT); + /// DeleteExpressionMutation(`require(UtilsLib.exactlyOneZero(assets, shares), ErrorsLib.INCONSISTENT_INPUT)` + /// |==> `assert(true)`) of: `require(UtilsLib.exactlyOneZero(assets, shares), ErrorsLib.INCONSISTENT_INPUT);` + assert(true); require(onBehalf != address(0), ErrorsLib.ZERO_ADDRESS); _accrueInterest(marketParams, id); From 744d0ffe2cb587e0802195f77bfdd6417d355830 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 22 Nov 2023 19:35:49 +0100 Subject: [PATCH 169/204] test: mutant 3 --- src/Morpho.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index f755904b6..4491e7c86 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -246,7 +246,10 @@ contract Morpho is IMorphoStaticTyping { _accrueInterest(marketParams, id); if (assets > 0) shares = assets.toSharesUp(market[id].totalBorrowAssets, market[id].totalBorrowShares); - else assets = shares.toAssetsDown(market[id].totalBorrowAssets, market[id].totalBorrowShares); + /// DeleteExpressionMutation(`assets = shares.toAssetsDown(market[id].totalBorrowAssets, + /// market[id].totalBorrowShares)` |==> `assert(true)`) of: `else assets = + /// shares.toAssetsDown(market[id].totalBorrowAssets, market[id].totalBorrowShares);` + else assert(true); position[id][onBehalf].borrowShares += shares.toUint128(); market[id].totalBorrowShares += shares.toUint128(); From e445aaf47da01f679595d82fe999f9729840e6db Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 22 Nov 2023 19:36:06 +0100 Subject: [PATCH 170/204] test: mutant 6 --- src/Morpho.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index f755904b6..78f4da5c3 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -282,7 +282,10 @@ contract Morpho is IMorphoStaticTyping { position[id][onBehalf].borrowShares -= shares.toUint128(); market[id].totalBorrowShares -= shares.toUint128(); - market[id].totalBorrowAssets = UtilsLib.zeroFloorSub(market[id].totalBorrowAssets, assets).toUint128(); + /// DeleteExpressionMutation(`market[id].totalBorrowAssets = UtilsLib.zeroFloorSub(market[id].totalBorrowAssets, + /// assets).toUint128()` |==> `assert(true)`) of: `market[id].totalBorrowAssets = + /// UtilsLib.zeroFloorSub(market[id].totalBorrowAssets, assets).toUint128();` + assert(true); // `assets` may be greater than `totalBorrowAssets` by 1. emit EventsLib.Repay(id, msg.sender, onBehalf, assets, shares); From 9eba54d9bf9eda6246bcf04355dafeba0ecebef3 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 22 Nov 2023 19:36:27 +0100 Subject: [PATCH 171/204] test: mutant 10 --- src/Morpho.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index f755904b6..ad8c037e6 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -391,7 +391,9 @@ contract Morpho is IMorphoStaticTyping { badDebtShares.toAssetsUp(market[id].totalBorrowAssets, market[id].totalBorrowShares) ); - market[id].totalBorrowAssets -= badDebt.toUint128(); + /// AssignmentMutation(`badDebt.toUint128()` |==> `0`) of: `market[id].totalBorrowAssets -= + /// badDebt.toUint128();` + market[id].totalBorrowAssets -= 0; market[id].totalSupplyAssets -= badDebt.toUint128(); market[id].totalBorrowShares -= badDebtShares.toUint128(); position[id][borrower].borrowShares = 0; From d21a86f291aad386dc3383ca7e16a790c213d7b6 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 22 Nov 2023 19:36:43 +0100 Subject: [PATCH 172/204] test: mutant 13 --- src/Morpho.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index f755904b6..0cf98947a 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -461,7 +461,9 @@ contract Morpho is IMorphoStaticTyping { /// @inheritdoc IMorphoBase function accrueInterest(MarketParams memory marketParams) external { Id id = marketParams.id(); - require(market[id].lastUpdate != 0, ErrorsLib.MARKET_NOT_CREATED); + /// RequireMutation(`market[id].lastUpdate != 0` |==> `false`) of: `require(market[id].lastUpdate != 0, + /// ErrorsLib.MARKET_NOT_CREATED);` + require(false, ErrorsLib.MARKET_NOT_CREATED); _accrueInterest(marketParams, id); } From de3147cce6146e3af4447139814173a6d363b5a7 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 23 Nov 2023 09:53:10 +0100 Subject: [PATCH 173/204] feat: respecting non-zero input --- certora/specs/Liveness.spec | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/certora/specs/Liveness.spec b/certora/specs/Liveness.spec index e5b96af82..275a0c235 100644 --- a/certora/specs/Liveness.spec +++ b/certora/specs/Liveness.spec @@ -80,7 +80,7 @@ rule supplyChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams marke mathint balanceAfter = myBalances[marketParams.loanToken]; assert assets != 0 => suppliedAssets == assets; - assert assets == 0 => suppliedShares == shares; + assert shares != 0 => suppliedShares == shares; assert sharesAfter == sharesBefore + suppliedShares; assert balanceAfter == balanceBefore + suppliedAssets; } @@ -105,7 +105,7 @@ rule withdrawChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams mar mathint balanceAfter = myBalances[marketParams.loanToken]; assert assets != 0 => withdrawnAssets == assets; - assert assets == 0 => withdrawnShares == shares; + assert shares != 0 => withdrawnShares == shares; assert sharesAfter == sharesBefore - withdrawnShares; assert balanceAfter == balanceBefore - withdrawnAssets; } @@ -130,7 +130,7 @@ rule borrowChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams marke mathint balanceAfter = myBalances[marketParams.loanToken]; assert assets != 0 => borrowedAssets == assets; - assert assets == 0 => borrowedShares == shares; + assert shares != 0 => borrowedShares == shares; assert sharesAfter == sharesBefore + borrowedShares; assert balanceAfter == balanceBefore - borrowedAssets; } @@ -155,7 +155,7 @@ rule repayChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams market mathint balanceAfter = myBalances[marketParams.loanToken]; assert assets != 0 => repaidAssets == assets; - assert assets == 0 => repaidShares == shares; + assert shares != 0 => repaidShares == shares; assert sharesAfter == sharesBefore - repaidShares; assert balanceAfter == balanceBefore + repaidAssets; } From 0ef14b5853bba07c8292af369fb9af910b2154d5 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 23 Nov 2023 10:19:14 +0100 Subject: [PATCH 174/204] Revert "test: mutant 2" This reverts commit 766667f101c48525459dac56fe9dce5c0ddbd137. --- src/Morpho.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index 913ada68d..f755904b6 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -172,9 +172,7 @@ contract Morpho is IMorphoStaticTyping { ) external returns (uint256, uint256) { Id id = marketParams.id(); require(market[id].lastUpdate != 0, ErrorsLib.MARKET_NOT_CREATED); - /// DeleteExpressionMutation(`require(UtilsLib.exactlyOneZero(assets, shares), ErrorsLib.INCONSISTENT_INPUT)` - /// |==> `assert(true)`) of: `require(UtilsLib.exactlyOneZero(assets, shares), ErrorsLib.INCONSISTENT_INPUT);` - assert(true); + require(UtilsLib.exactlyOneZero(assets, shares), ErrorsLib.INCONSISTENT_INPUT); require(onBehalf != address(0), ErrorsLib.ZERO_ADDRESS); _accrueInterest(marketParams, id); From 957d61cd8a980293e4ffded7645b5d2c50266adb Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 23 Nov 2023 12:17:41 +0100 Subject: [PATCH 175/204] feat: test liquidity in liveness --- certora/specs/Liveness.spec | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/certora/specs/Liveness.spec b/certora/specs/Liveness.spec index e5b96af82..f1f83a4f0 100644 --- a/certora/specs/Liveness.spec +++ b/certora/specs/Liveness.spec @@ -71,6 +71,7 @@ rule supplyChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams marke mathint sharesBefore = supplyShares(id, onBehalf); mathint balanceBefore = myBalances[marketParams.loanToken]; + mathint liquidityBefore = totalSupplyAssets(id) - totalBorrowAssets(id); uint256 suppliedAssets; uint256 suppliedShares; @@ -78,11 +79,13 @@ rule supplyChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams marke mathint sharesAfter = supplyShares(id, onBehalf); mathint balanceAfter = myBalances[marketParams.loanToken]; + mathint liquidityAfter = totalSupplyAssets(id) - totalBorrowAssets(id); assert assets != 0 => suppliedAssets == assets; assert assets == 0 => suppliedShares == shares; assert sharesAfter == sharesBefore + suppliedShares; assert balanceAfter == balanceBefore + suppliedAssets; + assert liquidityAfter == liquidityBefore + suppliedAssets; } // Check that tokens and shares are properly accounted following a withdraw. @@ -96,6 +99,7 @@ rule withdrawChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams mar mathint sharesBefore = supplyShares(id, onBehalf); mathint balanceBefore = myBalances[marketParams.loanToken]; + mathint liquidityBefore = totalSupplyAssets(id) - totalBorrowAssets(id); uint256 withdrawnAssets; uint256 withdrawnShares; @@ -103,11 +107,13 @@ rule withdrawChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams mar mathint sharesAfter = supplyShares(id, onBehalf); mathint balanceAfter = myBalances[marketParams.loanToken]; + mathint liquidityAfter = totalSupplyAssets(id) - totalBorrowAssets(id); assert assets != 0 => withdrawnAssets == assets; assert assets == 0 => withdrawnShares == shares; assert sharesAfter == sharesBefore - withdrawnShares; assert balanceAfter == balanceBefore - withdrawnAssets; + assert liquidityAfter == liquidityBefore - withdrawnAssets; } // Check that tokens and shares are properly accounted following a borrow. @@ -121,6 +127,7 @@ rule borrowChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams marke mathint sharesBefore = borrowShares(id, onBehalf); mathint balanceBefore = myBalances[marketParams.loanToken]; + mathint liquidityBefore = totalSupplyAssets(id) - totalBorrowAssets(id); uint256 borrowedAssets; uint256 borrowedShares; @@ -128,11 +135,13 @@ rule borrowChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams marke mathint sharesAfter = borrowShares(id, onBehalf); mathint balanceAfter = myBalances[marketParams.loanToken]; + mathint liquidityAfter = totalSupplyAssets(id) - totalBorrowAssets(id); assert assets != 0 => borrowedAssets == assets; assert assets == 0 => borrowedShares == shares; assert sharesAfter == sharesBefore + borrowedShares; assert balanceAfter == balanceBefore - borrowedAssets; + assert liquidityAfter == liquidityBefore - borrowedAssets; } // Check that tokens and shares are properly accounted following a repay. @@ -146,6 +155,7 @@ rule repayChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams market mathint sharesBefore = borrowShares(id, onBehalf); mathint balanceBefore = myBalances[marketParams.loanToken]; + mathint liquidityBefore = totalSupplyAssets(id) - totalBorrowAssets(id); uint256 repaidAssets; uint256 repaidShares; @@ -153,11 +163,14 @@ rule repayChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams market mathint sharesAfter = borrowShares(id, onBehalf); mathint balanceAfter = myBalances[marketParams.loanToken]; + mathint liquidityAfter = totalSupplyAssets(id) - totalBorrowAssets(id); assert assets != 0 => repaidAssets == assets; assert assets == 0 => repaidShares == shares; assert sharesAfter == sharesBefore - repaidShares; assert balanceAfter == balanceBefore + repaidAssets; + // Added condition to omit the floor case of the subtraction in totaBorrowAssets. + assert totalBorrowAssets(id) != 0 => liquidityAfter == liquidityBefore + repaidAssets; } // Check that tokens and balances are properly accounted following a supplyCollateral. From 04f0c4b3953180aaf1cfdadbf6a170a10f504772 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 23 Nov 2023 12:50:55 +0100 Subject: [PATCH 176/204] refactor: improving mutant to get significant result --- src/Morpho.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index 0cf98947a..226386cee 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -463,7 +463,7 @@ contract Morpho is IMorphoStaticTyping { Id id = marketParams.id(); /// RequireMutation(`market[id].lastUpdate != 0` |==> `false`) of: `require(market[id].lastUpdate != 0, /// ErrorsLib.MARKET_NOT_CREATED);` - require(false, ErrorsLib.MARKET_NOT_CREATED); + require(true, ErrorsLib.MARKET_NOT_CREATED); _accrueInterest(marketParams, id); } From c4306f1c72fb981832e1697b85aad8001b39d30d Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 23 Nov 2023 13:26:43 +0100 Subject: [PATCH 177/204] feat: accrue interest input validation --- certora/specs/Reverts.spec | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/certora/specs/Reverts.spec b/certora/specs/Reverts.spec index 7ecb5be1c..11bdabddf 100644 --- a/certora/specs/Reverts.spec +++ b/certora/specs/Reverts.spec @@ -161,3 +161,10 @@ rule liquidateInputValidation(env e, MorphoHarness.MarketParams marketParams, ad liquidate@withrevert(e, marketParams, borrower, seizedAssets, repaidShares, data); assert !exactlyOneZero(seizedAssets, repaidShares) => lastReverted; } + +// Check that accrueInterest reverts when its inputs are not validated. +rule accrueInterestInputValidation(env e, MorphoHarness.MarketParams marketParams) { + uint256 lastUpdate = lastUpdate(libId(marketParams)); + accrueInterest@withrevert(e, marketParams); + assert lastUpdate == 0 => lastReverted; +} From c25947a795a8fdea18211c0d5642c1b1cf5176bc Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 23 Nov 2023 14:03:12 +0100 Subject: [PATCH 178/204] revert: test mutant 13 --- src/Morpho.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index 226386cee..f755904b6 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -461,9 +461,7 @@ contract Morpho is IMorphoStaticTyping { /// @inheritdoc IMorphoBase function accrueInterest(MarketParams memory marketParams) external { Id id = marketParams.id(); - /// RequireMutation(`market[id].lastUpdate != 0` |==> `false`) of: `require(market[id].lastUpdate != 0, - /// ErrorsLib.MARKET_NOT_CREATED);` - require(true, ErrorsLib.MARKET_NOT_CREATED); + require(market[id].lastUpdate != 0, ErrorsLib.MARKET_NOT_CREATED); _accrueInterest(marketParams, id); } From 7d1a1c2f29742a4d2b2ea9335ef9418fd3c30720 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 23 Nov 2023 14:36:01 +0100 Subject: [PATCH 179/204] feat: improving coverage of liquidity check in repay liveness --- certora/specs/Liveness.spec | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/certora/specs/Liveness.spec b/certora/specs/Liveness.spec index f1f83a4f0..d47ff8f37 100644 --- a/certora/specs/Liveness.spec +++ b/certora/specs/Liveness.spec @@ -39,6 +39,10 @@ function summarySafeTransferFrom(address token, address from, address to, uint25 } } +function min(mathint a, mathint b) returns mathint { + return a < b ? a : b; +} + // Assume no fee. // Summarize the accrue interest to avoid having to deal with reverts with absurdly high borrow rates. function summaryAccrueInterest(env e, MorphoInternalAccess.MarketParams marketParams, MorphoInternalAccess.Id id) { @@ -157,6 +161,8 @@ rule repayChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams market mathint balanceBefore = myBalances[marketParams.loanToken]; mathint liquidityBefore = totalSupplyAssets(id) - totalBorrowAssets(id); + mathint borrowAssetsBefore = totalBorrowAssets(id); + uint256 repaidAssets; uint256 repaidShares; repaidAssets, repaidShares = repay(e, marketParams, assets, shares, onBehalf, data); @@ -169,8 +175,8 @@ rule repayChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams market assert assets == 0 => repaidShares == shares; assert sharesAfter == sharesBefore - repaidShares; assert balanceAfter == balanceBefore + repaidAssets; - // Added condition to omit the floor case of the subtraction in totaBorrowAssets. - assert totalBorrowAssets(id) != 0 => liquidityAfter == liquidityBefore + repaidAssets; + // Taking the min to handle the zeroFloorSub in the code. + assert liquidityAfter == liquidityBefore + min(repaidAssets, borrowAssetsBefore); } // Check that tokens and balances are properly accounted following a supplyCollateral. From bb6d9bb8b1949fbe66046d0d25a3ae9a9d54151a Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 23 Nov 2023 14:52:14 +0100 Subject: [PATCH 180/204] revert: mutant 6 --- src/Morpho.sol | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index 78f4da5c3..f755904b6 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -282,10 +282,7 @@ contract Morpho is IMorphoStaticTyping { position[id][onBehalf].borrowShares -= shares.toUint128(); market[id].totalBorrowShares -= shares.toUint128(); - /// DeleteExpressionMutation(`market[id].totalBorrowAssets = UtilsLib.zeroFloorSub(market[id].totalBorrowAssets, - /// assets).toUint128()` |==> `assert(true)`) of: `market[id].totalBorrowAssets = - /// UtilsLib.zeroFloorSub(market[id].totalBorrowAssets, assets).toUint128();` - assert(true); + market[id].totalBorrowAssets = UtilsLib.zeroFloorSub(market[id].totalBorrowAssets, assets).toUint128(); // `assets` may be greater than `totalBorrowAssets` by 1. emit EventsLib.Repay(id, msg.sender, onBehalf, assets, shares); From 3e22f7c22c70e1c569e73a5ca2c06870638c8a02 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 23 Nov 2023 16:38:15 +0100 Subject: [PATCH 181/204] feat: added liveness for liquidate --- certora/specs/Liveness.spec | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/certora/specs/Liveness.spec b/certora/specs/Liveness.spec index 5e5cce8c4..2841c53b3 100644 --- a/certora/specs/Liveness.spec +++ b/certora/specs/Liveness.spec @@ -219,6 +219,41 @@ rule withdrawCollateralChangesTokensAndBalance(env e, MorphoInternalAccess.Marke assert balanceAfter == balanceBefore - assets; } +// Check that tokens are properly accounted following a liquidate. +rule liquidateChangesTokens(env e, MorphoInternalAccess.MarketParams marketParams, address borrower, uint256 seized, uint256 repaid, bytes data) { + MorphoInternalAccess.Id id = libId(marketParams); + + // Safe require because Morpho cannot call such functions by itself. + require currentContract != e.msg.sender; + // Assumption to ensure that no interest is accumulated. + require lastUpdate(id) == e.block.timestamp; + + mathint collateralBefore = collateral(id, borrower); + mathint balanceLoanBefore = myBalances[marketParams.loanToken]; + mathint balanceCollateralBefore = myBalances[marketParams.collateralToken]; + mathint liquidityBefore = totalSupplyAssets(id) - totalBorrowAssets(id); + + mathint borrowLoanAssetsBefore = totalBorrowAssets(id); + + uint256 seizedAssets; + uint256 repaidAssets; + seizedAssets, repaidAssets = liquidate(e, marketParams, borrower, seized, repaid, data); + + mathint collateralAfter = collateral(id, borrower); + mathint balanceLoanAfter = myBalances[marketParams.loanToken]; + mathint balanceCollateralAfter = myBalances[marketParams.collateralToken]; + mathint liquidityAfter = totalSupplyAssets(id) - totalBorrowAssets(id); + + assert seized != 0 => seizedAssets == seized; + assert repaid != 0 => repaidAssets == repaid; + // Added collateralbefore > seizedAssets condition to omit bad debt scenarios. + assert collateralBefore > to_mathint(seizedAssets) => collateralAfter == collateralBefore - seizedAssets; + assert balanceLoanAfter == balanceLoanBefore + repaidAssets; + assert balanceCollateralAfter == balanceCollateralBefore - seizedAssets; + // Taking the min to handle the zeroFloorSub in the code. + assert liquidityAfter == liquidityBefore + min(repaidAssets, borrowLoanAssetsBefore); +} + // Check that one can always repay the debt in full. rule canRepayAll(env e, MorphoInternalAccess.MarketParams marketParams, uint256 shares, bytes data) { MorphoInternalAccess.Id id = libId(marketParams); From 641a6f164c03f990dd5f7bcb155e794e67f3e048 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 22 Nov 2023 19:36:38 +0100 Subject: [PATCH 182/204] test: mutant 12 --- src/Morpho.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index f755904b6..d72314d72 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -434,7 +434,9 @@ contract Morpho is IMorphoStaticTyping { /// @inheritdoc IMorphoBase function setAuthorizationWithSig(Authorization memory authorization, Signature calldata signature) external { require(block.timestamp <= authorization.deadline, ErrorsLib.SIGNATURE_EXPIRED); - require(authorization.nonce == nonce[authorization.authorizer]++, ErrorsLib.INVALID_NONCE); + /// UnaryOperatorMutation(`++` |==> `--`) of: `require(authorization.nonce == nonce[authorization.authorizer]++, + /// ErrorsLib.INVALID_NONCE);` + require(authorization.nonce == nonce[authorization.authorizer]--, ErrorsLib.INVALID_NONCE); bytes32 hashStruct = keccak256(abi.encode(AUTHORIZATION_TYPEHASH, authorization)); bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, hashStruct)); From 75025b008af82576f70d399f69cbb6eedfefc37f Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 23 Nov 2023 12:32:32 +0100 Subject: [PATCH 183/204] feat: add setAuthorizationWithSig liveness and input validation --- certora/specs/AccrueInterest.spec | 6 +++--- certora/specs/ConsistentState.spec | 18 +++++++++--------- certora/specs/ExactMath.spec | 7 ++++--- certora/specs/ExitLiquidity.spec | 2 +- certora/specs/Health.spec | 6 ++++-- certora/specs/LibSummary.spec | 1 + certora/specs/Liveness.spec | 21 ++++++++++++++++++--- certora/specs/RatioMath.spec | 5 +++-- certora/specs/Reentrancy.spec | 1 + certora/specs/Reverts.spec | 17 +++++++++++++---- 10 files changed, 57 insertions(+), 27 deletions(-) diff --git a/certora/specs/AccrueInterest.spec b/certora/specs/AccrueInterest.spec index 824cce07e..7babf38de 100644 --- a/certora/specs/AccrueInterest.spec +++ b/certora/specs/AccrueInterest.spec @@ -1,10 +1,12 @@ // SPDX-License-Identifier: GPL-2.0-or-later methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE; + + function maxFee() external returns uint256 envfree; + function MathLib.mulDivDown(uint256 a, uint256 b, uint256 c) internal returns uint256 => ghostMulDivDown(a,b,c); function MathLib.mulDivUp(uint256 a, uint256 b, uint256 c) internal returns uint256 => ghostMulDivUp(a,b,c); function MathLib.wTaylorCompounded(uint256 a, uint256 b) internal returns uint256 => ghostTaylorCompounded(a, b); - // We assume here that all external functions will not access storage, since we cannot show commutativity otherwise. // We also need to assume that the price and borrow rate return always the same value (and do not depend on msg.origin), so we use ghost functions for them. function _.borrowRate(MorphoHarness.MarketParams marketParams, MorphoHarness.Market market) external with (env e) => ghostBorrowRate(marketParams.irm, e.block.timestamp) expect uint256; @@ -16,8 +18,6 @@ methods { function _.onMorphoSupply(uint256, bytes) external => NONDET; function _.onMorphoSupplyCollateral(uint256, bytes) external => NONDET; function _.onMorphoFlashLoan(uint256, bytes) external => NONDET; - - function maxFee() external returns uint256 envfree; } ghost ghostMulDivUp(uint256, uint256, uint256) returns uint256; diff --git a/certora/specs/ConsistentState.spec b/certora/specs/ConsistentState.spec index 49e0c1df8..97083a02d 100644 --- a/certora/specs/ConsistentState.spec +++ b/certora/specs/ConsistentState.spec @@ -1,25 +1,25 @@ // SPDX-License-Identifier: GPL-2.0-or-later methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE; + + function supplyShares(MorphoHarness.Id, address) external returns uint256 envfree; + function borrowShares(MorphoHarness.Id, address) external returns uint256 envfree; + function collateral(MorphoHarness.Id, address) external returns uint256 envfree; function totalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; function totalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; function totalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree; function totalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; - function supplyShares(MorphoHarness.Id, address) external returns uint256 envfree; - function borrowShares(MorphoHarness.Id, address) external returns uint256 envfree; - function collateral(MorphoHarness.Id, address) external returns uint256 envfree; function fee(MorphoHarness.Id) external returns uint256 envfree; function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; - function libId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; - - function isAuthorized(address, address) external returns bool envfree; - function isLltvEnabled(uint256) external returns bool envfree; function isIrmEnabled(address) external returns bool envfree; - - function _.borrowRate(MorphoHarness.MarketParams, MorphoHarness.Market) external => HAVOC_ECF; + function isLltvEnabled(uint256) external returns bool envfree; + function isAuthorized(address, address) external returns bool envfree; function maxFee() external returns uint256 envfree; function wad() external returns uint256 envfree; + function libId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; + + function _.borrowRate(MorphoHarness.MarketParams, MorphoHarness.Market) external => HAVOC_ECF; function SafeTransferLib.safeTransfer(address token, address to, uint256 value) internal => summarySafeTransferFrom(token, currentContract, to, value); function SafeTransferLib.safeTransferFrom(address token, address from, address to, uint256 value) internal => summarySafeTransferFrom(token, from, to, value); diff --git a/certora/specs/ExactMath.spec b/certora/specs/ExactMath.spec index 6e08543c8..5f92dac3d 100644 --- a/certora/specs/ExactMath.spec +++ b/certora/specs/ExactMath.spec @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE; - function libId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; + function virtualTotalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; function virtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; function virtualTotalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree; @@ -9,13 +9,14 @@ methods { function fee(MorphoHarness.Id) external returns uint256 envfree; function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; + function maxFee() external returns uint256 envfree; + function libId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; + function MathLib.mulDivDown(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivDown(a,b,c); function MathLib.mulDivUp(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivUp(a,b,c); function SafeTransferLib.safeTransfer(address token, address to, uint256 value) internal => NONDET; function SafeTransferLib.safeTransferFrom(address token, address from, address to, uint256 value) internal => NONDET; function _.onMorphoSupply(uint256 assets, bytes data) external => HAVOC_ECF; - - function maxFee() external returns uint256 envfree; } function summaryMulDivUp(uint256 x, uint256 y, uint256 d) returns uint256 { diff --git a/certora/specs/ExitLiquidity.spec b/certora/specs/ExitLiquidity.spec index bf90ec31c..d967a7974 100644 --- a/certora/specs/ExitLiquidity.spec +++ b/certora/specs/ExitLiquidity.spec @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE; + function supplyShares(MorphoHarness.Id, address) external returns uint256 envfree; function borrowShares(MorphoHarness.Id, address) external returns uint256 envfree; function collateral(MorphoHarness.Id, address) external returns uint256 envfree; @@ -8,7 +9,6 @@ methods { function virtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; function virtualTotalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree; function virtualTotalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; - function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; function libMulDivDown(uint256, uint256, uint256) external returns uint256 envfree; diff --git a/certora/specs/Health.spec b/certora/specs/Health.spec index bac2aeba1..cdc689135 100644 --- a/certora/specs/Health.spec +++ b/certora/specs/Health.spec @@ -1,13 +1,15 @@ // SPDX-License-Identifier: GPL-2.0-or-later methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE; - function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; + function totalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; + function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; function borrowShares(MorphoHarness.Id, address) external returns uint256 envfree; function collateral(MorphoHarness.Id, address) external returns uint256 envfree; - function isHealthy(MorphoHarness.MarketParams, address user) external returns bool envfree; function isAuthorized(address, address user) external returns bool envfree; + function libId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; + function isHealthy(MorphoHarness.MarketParams, address user) external returns bool envfree; function _.price() external => mockPrice() expect uint256; function MathLib.mulDivDown(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivDown(a,b,c); diff --git a/certora/specs/LibSummary.spec b/certora/specs/LibSummary.spec index 64322f7dc..030e89140 100644 --- a/certora/specs/LibSummary.spec +++ b/certora/specs/LibSummary.spec @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE; + function libMulDivUp(uint256, uint256, uint256) external returns uint256 envfree; function libMulDivDown(uint256, uint256, uint256) external returns uint256 envfree; function libId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; diff --git a/certora/specs/Liveness.spec b/certora/specs/Liveness.spec index 5e5cce8c4..25419af3a 100644 --- a/certora/specs/Liveness.spec +++ b/certora/specs/Liveness.spec @@ -1,15 +1,18 @@ // SPDX-License-Identifier: GPL-2.0-or-later methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE; + function supplyShares(MorphoInternalAccess.Id, address) external returns uint256 envfree; + function borrowShares(MorphoInternalAccess.Id, address) external returns uint256 envfree; + function collateral(MorphoInternalAccess.Id, address) external returns uint256 envfree; function totalSupplyAssets(MorphoInternalAccess.Id) external returns uint256 envfree; function totalSupplyShares(MorphoInternalAccess.Id) external returns uint256 envfree; function totalBorrowAssets(MorphoInternalAccess.Id) external returns uint256 envfree; function totalBorrowShares(MorphoInternalAccess.Id) external returns uint256 envfree; - function supplyShares(MorphoInternalAccess.Id, address) external returns uint256 envfree; - function borrowShares(MorphoInternalAccess.Id, address) external returns uint256 envfree; - function collateral(MorphoInternalAccess.Id, address) external returns uint256 envfree; function fee(MorphoInternalAccess.Id) external returns uint256 envfree; function lastUpdate(MorphoInternalAccess.Id) external returns uint256 envfree; + function nonce(address) external returns uint256 envfree; + function isAuthorized(address, address) external returns bool envfree; + function libId(MorphoInternalAccess.MarketParams) external returns MorphoInternalAccess.Id envfree; function refId(MorphoInternalAccess.MarketParams) external returns MorphoInternalAccess.Id envfree; @@ -219,6 +222,18 @@ rule withdrawCollateralChangesTokensAndBalance(env e, MorphoInternalAccess.Marke assert balanceAfter == balanceBefore - assets; } +// Check that nonce and authorization are properly updated with calling setAuthorizationWithSig. +rule setAuthorizationWithSigChangesNonceAndAuthorizes(env e, MorphoInternalAccess.Authorization authorization, MorphoInternalAccess.Signature signature) { + mathint nonceBefore = nonce(authorization.authorizer); + + setAuthorizationWithSig(e, authorization, signature); + + mathint nonceAfter = nonce(authorization.authorizer); + + assert nonceAfter == nonceBefore + 1; + assert isAuthorized(authorization.authorizer, authorization.authorized) == authorization.isAuthorized; +} + // Check that one can always repay the debt in full. rule canRepayAll(env e, MorphoInternalAccess.MarketParams marketParams, uint256 shares, bytes data) { MorphoInternalAccess.Id id = libId(marketParams); diff --git a/certora/specs/RatioMath.spec b/certora/specs/RatioMath.spec index 2d138ad13..e07ee77b9 100644 --- a/certora/specs/RatioMath.spec +++ b/certora/specs/RatioMath.spec @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE; - function libId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; function virtualTotalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; function virtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; function virtualTotalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree; @@ -9,13 +8,15 @@ methods { function fee(MorphoHarness.Id) external returns uint256 envfree; function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; + function maxFee() external returns uint256 envfree; + function libId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; + function MathLib.mulDivDown(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivDown(a,b,c); function MathLib.mulDivUp(uint256 a, uint256 b, uint256 c) internal returns uint256 => summaryMulDivUp(a,b,c); function MathLib.wTaylorCompounded(uint256, uint256) internal returns uint256 => NONDET; function _.borrowRate(MorphoHarness.MarketParams, MorphoHarness.Market) external => HAVOC_ECF; - function maxFee() external returns uint256 envfree; } invariant feeInRange(MorphoHarness.Id id) diff --git a/certora/specs/Reentrancy.spec b/certora/specs/Reentrancy.spec index 5cb2b1639..e66336305 100644 --- a/certora/specs/Reentrancy.spec +++ b/certora/specs/Reentrancy.spec @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE; + function _.borrowRate(MorphoHarness.MarketParams marketParams, MorphoHarness.Market) external => summaryBorrowRate() expect uint256; } diff --git a/certora/specs/Reverts.spec b/certora/specs/Reverts.spec index 11bdabddf..38d0cd4e9 100644 --- a/certora/specs/Reverts.spec +++ b/certora/specs/Reverts.spec @@ -1,19 +1,21 @@ // SPDX-License-Identifier: GPL-2.0-or-later methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE; + function owner() external returns address envfree; + function feeRecipient() external returns address envfree; function totalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; function totalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree; function totalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; function totalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; - - function feeRecipient() external returns address envfree; - function isLltvEnabled(uint256) external returns bool envfree; function isIrmEnabled(address) external returns bool envfree; + function isLltvEnabled(uint256) external returns bool envfree; function isAuthorized(address, address) external returns bool envfree; + function nonce(address) external returns uint256 envfree; function libId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; + function maxFee() external returns uint256 envfree; function wad() external returns uint256 envfree; } @@ -156,12 +158,19 @@ rule withdrawCollateralInputValidation(env e, MorphoHarness.MarketParams marketP assert assets == 0 || onBehalf == 0 => lastReverted; } -// Check that liqudiate reverts when its inputs are not validated. +// Check that liquidate reverts when its inputs are not validated. rule liquidateInputValidation(env e, MorphoHarness.MarketParams marketParams, address borrower, uint256 seizedAssets, uint256 repaidShares, bytes data) { liquidate@withrevert(e, marketParams, borrower, seizedAssets, repaidShares, data); assert !exactlyOneZero(seizedAssets, repaidShares) => lastReverted; } +// Check that setAuthorizationWithSig reverts when its inputs are not validated. +rule setAuthorizationWithSigInputValidation(env e, MorphoHarness.Authorization authorization, MorphoHarness.Signature signature) { + uint256 nonceBefore = nonce(authorization.authorizer); + setAuthorizationWithSig@withrevert(e, authorization, signature); + assert e.block.timestamp <= authorization.deadline || authorization.nonce != nonceBefore => lastReverted; +} + // Check that accrueInterest reverts when its inputs are not validated. rule accrueInterestInputValidation(env e, MorphoHarness.MarketParams marketParams) { uint256 lastUpdate = lastUpdate(libId(marketParams)); From 033670c6648ec2b3e9157e9491251f3730c594b1 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 23 Nov 2023 17:03:34 +0100 Subject: [PATCH 184/204] fix: repaidShares input in liquidation --- certora/specs/Liveness.spec | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/certora/specs/Liveness.spec b/certora/specs/Liveness.spec index 2841c53b3..9d9b0d1c1 100644 --- a/certora/specs/Liveness.spec +++ b/certora/specs/Liveness.spec @@ -220,7 +220,7 @@ rule withdrawCollateralChangesTokensAndBalance(env e, MorphoInternalAccess.Marke } // Check that tokens are properly accounted following a liquidate. -rule liquidateChangesTokens(env e, MorphoInternalAccess.MarketParams marketParams, address borrower, uint256 seized, uint256 repaid, bytes data) { +rule liquidateChangesTokens(env e, MorphoInternalAccess.MarketParams marketParams, address borrower, uint256 seized, uint256 repaidShares, bytes data) { MorphoInternalAccess.Id id = libId(marketParams); // Safe require because Morpho cannot call such functions by itself. @@ -237,7 +237,7 @@ rule liquidateChangesTokens(env e, MorphoInternalAccess.MarketParams marketParam uint256 seizedAssets; uint256 repaidAssets; - seizedAssets, repaidAssets = liquidate(e, marketParams, borrower, seized, repaid, data); + seizedAssets, repaidAssets = liquidate(e, marketParams, borrower, seized, repaidShares, data); mathint collateralAfter = collateral(id, borrower); mathint balanceLoanAfter = myBalances[marketParams.loanToken]; @@ -245,8 +245,6 @@ rule liquidateChangesTokens(env e, MorphoInternalAccess.MarketParams marketParam mathint liquidityAfter = totalSupplyAssets(id) - totalBorrowAssets(id); assert seized != 0 => seizedAssets == seized; - assert repaid != 0 => repaidAssets == repaid; - // Added collateralbefore > seizedAssets condition to omit bad debt scenarios. assert collateralBefore > to_mathint(seizedAssets) => collateralAfter == collateralBefore - seizedAssets; assert balanceLoanAfter == balanceLoanBefore + repaidAssets; assert balanceCollateralAfter == balanceCollateralBefore - seizedAssets; From d76c9f4c137095c1ea890bfd5a5b6eeeb67f2473 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 23 Nov 2023 17:11:50 +0100 Subject: [PATCH 185/204] fix: revert condition setAuthorizationWithSig --- certora/specs/Reverts.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certora/specs/Reverts.spec b/certora/specs/Reverts.spec index 38d0cd4e9..59554fef8 100644 --- a/certora/specs/Reverts.spec +++ b/certora/specs/Reverts.spec @@ -168,7 +168,7 @@ rule liquidateInputValidation(env e, MorphoHarness.MarketParams marketParams, ad rule setAuthorizationWithSigInputValidation(env e, MorphoHarness.Authorization authorization, MorphoHarness.Signature signature) { uint256 nonceBefore = nonce(authorization.authorizer); setAuthorizationWithSig@withrevert(e, authorization, signature); - assert e.block.timestamp <= authorization.deadline || authorization.nonce != nonceBefore => lastReverted; + assert e.block.timestamp > authorization.deadline || authorization.nonce != nonceBefore => lastReverted; } // Check that accrueInterest reverts when its inputs are not validated. From 2aac61f4ef6ba6eeb05cff7418dd52f2e80640cc Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 23 Nov 2023 17:18:55 +0100 Subject: [PATCH 186/204] fix: liquidate liveness only in case loan and collateral are different --- certora/specs/Liveness.spec | 2 ++ 1 file changed, 2 insertions(+) diff --git a/certora/specs/Liveness.spec b/certora/specs/Liveness.spec index 9d9b0d1c1..b6008c320 100644 --- a/certora/specs/Liveness.spec +++ b/certora/specs/Liveness.spec @@ -225,6 +225,8 @@ rule liquidateChangesTokens(env e, MorphoInternalAccess.MarketParams marketParam // Safe require because Morpho cannot call such functions by itself. require currentContract != e.msg.sender; + // Assumption to simplify the balance specification in the rest of this rule. + require marketParams.loanToken != marketParams.collateralToken; // Assumption to ensure that no interest is accumulated. require lastUpdate(id) == e.block.timestamp; From 35eb09ac234cfcd944b9c526c67fa773cf549a40 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 23 Nov 2023 17:28:00 +0100 Subject: [PATCH 187/204] Revert "test: mutant 10" This reverts commit 9eba54d9bf9eda6246bcf04355dafeba0ecebef3. --- src/Morpho.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index ad8c037e6..f755904b6 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -391,9 +391,7 @@ contract Morpho is IMorphoStaticTyping { badDebtShares.toAssetsUp(market[id].totalBorrowAssets, market[id].totalBorrowShares) ); - /// AssignmentMutation(`badDebt.toUint128()` |==> `0`) of: `market[id].totalBorrowAssets -= - /// badDebt.toUint128();` - market[id].totalBorrowAssets -= 0; + market[id].totalBorrowAssets -= badDebt.toUint128(); market[id].totalSupplyAssets -= badDebt.toUint128(); market[id].totalBorrowShares -= badDebtShares.toUint128(); position[id][borrower].borrowShares = 0; From f5c34bd6ff91a761c82cdaace4a29f18446021bb Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 23 Nov 2023 17:29:35 +0100 Subject: [PATCH 188/204] Revert "test: mutant 12" This reverts commit 641a6f164c03f990dd5f7bcb155e794e67f3e048. --- src/Morpho.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index d72314d72..f755904b6 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -434,9 +434,7 @@ contract Morpho is IMorphoStaticTyping { /// @inheritdoc IMorphoBase function setAuthorizationWithSig(Authorization memory authorization, Signature calldata signature) external { require(block.timestamp <= authorization.deadline, ErrorsLib.SIGNATURE_EXPIRED); - /// UnaryOperatorMutation(`++` |==> `--`) of: `require(authorization.nonce == nonce[authorization.authorizer]++, - /// ErrorsLib.INVALID_NONCE);` - require(authorization.nonce == nonce[authorization.authorizer]--, ErrorsLib.INVALID_NONCE); + require(authorization.nonce == nonce[authorization.authorizer]++, ErrorsLib.INVALID_NONCE); bytes32 hashStruct = keccak256(abi.encode(AUTHORIZATION_TYPEHASH, authorization)); bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, hashStruct)); From 5753c9f777defab6ef725d90644503befc57924e Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 23 Nov 2023 16:05:08 +0100 Subject: [PATCH 189/204] feat: can use shares properties --- certora/specs/Liveness.spec | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/certora/specs/Liveness.spec b/certora/specs/Liveness.spec index e5b96af82..ed16cc96b 100644 --- a/certora/specs/Liveness.spec +++ b/certora/specs/Liveness.spec @@ -85,6 +85,15 @@ rule supplyChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams marke assert balanceAfter == balanceBefore + suppliedAssets; } +// Check that you can supply non-zero tokens by passing shares. +rule canSupplyByPassingShares(env e, MorphoInternalAccess.MarketParams marketParams, uint256 shares, address onBehalf, bytes data) { + uint256 suppliedAssets; + uint256 suppliedShares; + suppliedAssets, suppliedShares = supply(e, marketParams, 0, shares, onBehalf, data); + + satisfy suppliedAssets != 0; +} + // Check that tokens and shares are properly accounted following a withdraw. rule withdrawChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) { MorphoInternalAccess.Id id = libId(marketParams); @@ -110,6 +119,15 @@ rule withdrawChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams mar assert balanceAfter == balanceBefore - withdrawnAssets; } +// Check that you can withdraw non-zero tokens by passing shares. +rule canWithdrawByPassingShares(env e, MorphoInternalAccess.MarketParams marketParams, uint256 shares, address onBehalf, address receiver) { + uint256 withdrawnAssets; + uint256 withdrawnShares; + withdrawnAssets, withdrawnShares = withdraw(e, marketParams, 0, shares, onBehalf, receiver); + + satisfy withdrawnAssets != 0; +} + // Check that tokens and shares are properly accounted following a borrow. rule borrowChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) { MorphoInternalAccess.Id id = libId(marketParams); @@ -135,6 +153,15 @@ rule borrowChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams marke assert balanceAfter == balanceBefore - borrowedAssets; } +// Check that you can borrow non-zero tokens by passing shares. +rule canBorrowByPassingShares(env e, MorphoInternalAccess.MarketParams marketParams, uint256 shares, address onBehalf, address receiver) { + uint256 borrowedAssets; + uint256 borrowedShares; + borrowedAssets, borrowedShares = borrow(e, marketParams, 0, shares, onBehalf, receiver); + + satisfy borrowedAssets != 0; +} + // Check that tokens and shares are properly accounted following a repay. rule repayChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { MorphoInternalAccess.Id id = libId(marketParams); @@ -160,6 +187,15 @@ rule repayChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams market assert balanceAfter == balanceBefore + repaidAssets; } +// Check that you can repay non-zero tokens by passing shares. +rule canRepayByPassingShares(env e, MorphoInternalAccess.MarketParams marketParams, uint256 shares, address onBehalf, bytes data) { + uint256 repaidAssets; + uint256 repaidShares; + repaidAssets, repaidShares = repay(e, marketParams, 0, shares, onBehalf, data); + + satisfy repaidAssets != 0; +} + // Check that tokens and balances are properly accounted following a supplyCollateral. rule supplyCollateralChangesTokensAndBalance(env e, MorphoInternalAccess.MarketParams marketParams, uint256 assets, address onBehalf, bytes data) { MorphoInternalAccess.Id id = libId(marketParams); From 87d55a603e85bfebae47adabf1157340de8ac631 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 24 Nov 2023 09:12:24 +0100 Subject: [PATCH 190/204] Revert "test: mutant 3" This reverts commit 744d0ffe2cb587e0802195f77bfdd6417d355830. --- src/Morpho.sol | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index 4491e7c86..f755904b6 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -246,10 +246,7 @@ contract Morpho is IMorphoStaticTyping { _accrueInterest(marketParams, id); if (assets > 0) shares = assets.toSharesUp(market[id].totalBorrowAssets, market[id].totalBorrowShares); - /// DeleteExpressionMutation(`assets = shares.toAssetsDown(market[id].totalBorrowAssets, - /// market[id].totalBorrowShares)` |==> `assert(true)`) of: `else assets = - /// shares.toAssetsDown(market[id].totalBorrowAssets, market[id].totalBorrowShares);` - else assert(true); + else assets = shares.toAssetsDown(market[id].totalBorrowAssets, market[id].totalBorrowShares); position[id][onBehalf].borrowShares += shares.toUint128(); market[id].totalBorrowShares += shares.toUint128(); From dfc58ad5ca844d8f0511ac3088126877fe41fa36 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 24 Nov 2023 11:50:36 +0100 Subject: [PATCH 191/204] docs: update certora/README.md with mutations --- certora/README.md | 51 ++++++++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/certora/README.md b/certora/README.md index b9dda26aa..29f44afbb 100644 --- a/certora/README.md +++ b/certora/README.md @@ -13,7 +13,7 @@ The Morpho Blue protocol allows users to take out collateralized loans on ERC20 For a given market, Morpho Blue relies on the fact that the tokens involved respect the ERC20 standard. In particular, in case of a transfer, it is assumed that the balance of Morpho Blue increases or decreases (depending if its the recipient or the sender) of the amount transferred. -The file [Transfer.spec](./specs/Transfer.spec) defines a summary of the transfer functions. +The file [Transfer.spec](specs/Transfer.spec) defines a summary of the transfer functions. This summary is taken as the reference implementation to check that the balance of the Morpho Blue contract changes as expected. ```solidity @@ -31,9 +31,9 @@ where `balance` is the ERC20 balance of the Morpho Blue contract. The verification is done for the most common implementations of the ERC20 standard, for which we distinguish three different implementations: -- [ERC20Standard](./dispatch/ERC20Standard.sol) which respects the standard and reverts in case of insufficient funds or in case of insufficient allowance. -- [ERC20NoRevert](./dispatch/ERC20NoRevert.sol) which respects the standard but does not revert (and returns false instead). -- [ERC20USDT](./dispatch/ERC20USDT.sol) which does not strictly respects the standard because it omits the return value of the `transfer` and `transferFrom` functions. +- [ERC20Standard](dispatch/ERC20Standard.sol) which respects the standard and reverts in case of insufficient funds or in case of insufficient allowance. +- [ERC20NoRevert](dispatch/ERC20NoRevert.sol) which respects the standard but does not revert (and returns false instead). +- [ERC20USDT](dispatch/ERC20USDT.sol) which does not strictly respects the standard because it omits the return value of the `transfer` and `transferFrom` functions. Additionally, Morpho Blue always goes through a custom transfer library to handle ERC20 tokens, notably in all the above cases. This library reverts when the transfer is not successful, and this is checked for the case of insufficient funds or insufficient allowance. @@ -238,36 +238,36 @@ assert withdrawnAssets <= owedAssets; # Folder and file structure -The [`certora/specs`](./specs) folder contains the following files: +The [`certora/specs`](specs) folder contains the following files: -- [`AccrueInterest.spec`](./specs/AccrueInterest.spec) checks that the main functions accrue interest at the start of the interaction. +- [`AccrueInterest.spec`](specs/AccrueInterest.spec) checks that the main functions accrue interest at the start of the interaction. This is done by ensuring that accruing interest before calling the function does not change the outcome compared to just calling the function. View functions do not necessarily respect this property (for example, `totalSupplyShares`), and are filtered out. -- [`ConsistentState.spec`](./specs/ConsistentState.spec) checks that the state (storage) of the Morpho contract is consistent. +- [`ConsistentState.spec`](specs/ConsistentState.spec) checks that the state (storage) of the Morpho contract is consistent. This includes checking that the accounting of the total amount and shares is correct, that markets are independent from each other, that only enabled IRMs and LLTVs can be used, and that users cannot have their position made worse by an unauthorized account. -- [`ExactMath.spec`](./specs/ExactMath.spec) checks precise properties when taking into account exact multiplication and division. +- [`ExactMath.spec`](specs/ExactMath.spec) checks precise properties when taking into account exact multiplication and division. Notably, this file specifies that using supply and withdraw in the same block cannot yield more funds than at the start. -- [`ExitLiquidity.spec`](./specs/ExitLiquidity.spec) checks that when exiting a position with withdraw, withdrawCollateral, or repay, the user cannot get more than what was owed. -- [`Health.spec`](./specs/Health.spec) checks properties about the health of the positions. +- [`ExitLiquidity.spec`](specs/ExitLiquidity.spec) checks that when exiting a position with withdraw, withdrawCollateral, or repay, the user cannot get more than what was owed. +- [`Health.spec`](specs/Health.spec) checks properties about the health of the positions. Notably, functions cannot render an account unhealthy, and debt positions always have some collateral (thanks to the bad debt realization mechanism). -- [`LibSummary.spec`](./specs/LibSummary.spec) checks the summarization of the library functions that are used in other specification files. -- [`Liveness.spec`](./specs/Liveness.spec) checks that main functions change the owner of funds and the amount of shares as expected, and that it's always possible to exit a position. -- [`RatioMath.spec`](./specs/RatioMath.spec) checks that the ratio between shares and assets evolves predictably over time. -- [`Reentrancy.spec`](./specs/Reentrancy.spec) checks that the contract is immune to a particular class of reentrancy issues. -- [`Reverts.spec`](./specs/Reverts.spec) checks the condition for reverts and that inputs are correctly validated. -- [`Transfer.spec`](./specs/Transfer.spec) checks the summarization of the safe transfer library functions that are used in other specification files. +- [`LibSummary.spec`](specs/LibSummary.spec) checks the summarization of the library functions that are used in other specification files. +- [`Liveness.spec`](specs/Liveness.spec) checks that main functions change the owner of funds and the amount of shares as expected, and that it's always possible to exit a position. +- [`RatioMath.spec`](specs/RatioMath.spec) checks that the ratio between shares and assets evolves predictably over time. +- [`Reentrancy.spec`](specs/Reentrancy.spec) checks that the contract is immune to a particular class of reentrancy issues. +- [`Reverts.spec`](specs/Reverts.spec) checks the condition for reverts and that inputs are correctly validated. +- [`Transfer.spec`](specs/Transfer.spec) checks the summarization of the safe transfer library functions that are used in other specification files. -The [`certora/confs`](./confs) folder contains a configuration file for each corresponding specification file. +The [`certora/confs`](confs) folder contains a configuration file for each corresponding specification file. -The [`certora/harness`](./harness) folder contains contracts that enable the verification of Morpho Blue. +The [`certora/harness`](harness) folder contains contracts that enable the verification of Morpho Blue. Notably, this allows handling the fact that library functions should be called from a contract to be verified independently, and it allows defining needed getters. -The [`certora/dispatch`](./dispatch) folder contains different contracts similar to the ones that are expected to be called from Morpho Blue. +The [`certora/dispatch`](dispatch) folder contains different contracts similar to the ones that are expected to be called from Morpho Blue. # Getting started -Install `certoraRun` with `pip install certora-cli`. -To verify specification files, pass the corresponding configuration file in the [`certora/confs`](./confs) folder. +Install `certora-cli` package with `pip install certora-cli`. +To verify specification files, pass to `certoraRun` the corresponding configuration file in the [`certora/confs`](confs) folder. It requires having set the `CERTORAKEY` environment variable to a valid Certora key. You can also pass additional arguments, notably to verify a specific rule. For example, at the root of the repository: @@ -275,3 +275,12 @@ For example, at the root of the repository: ``` certoraRun certora/confs/ConsistentState.conf --rule borrowLessThanSupply ``` + +The `certora-cli` package also includes a `certoraMutate` binary. +The file [`gambit.conf`](gambit.conf) provides a default configuration of the mutations. +You can test to mutate the code and check it against a particular specification. +For example, at the root of the repository: + +``` +certoraMutate --prover_conf certora/confs/ConsistentState.conf --mutation_conf certora/gambit.conf +``` From 3e4f205bd468f42907c2557b3ba1d2eff9051347 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 22 Nov 2023 17:45:52 +0100 Subject: [PATCH 192/204] refactor: roundtrip with arbitrary amounts --- certora/specs/ExactMath.spec | 57 ++++++++++++++++++------------------ certora/specs/Liveness.spec | 12 +++----- 2 files changed, 32 insertions(+), 37 deletions(-) diff --git a/certora/specs/ExactMath.spec b/certora/specs/ExactMath.spec index 5f92dac3d..e0e00369d 100644 --- a/certora/specs/ExactMath.spec +++ b/certora/specs/ExactMath.spec @@ -2,6 +2,8 @@ methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE; + function supplyShares(MorphoHarness.Id, address) external returns uint256 envfree; + function borrowShares(MorphoHarness.Id, address) external returns uint256 envfree; function virtualTotalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; function virtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; function virtualTotalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree; @@ -31,7 +33,7 @@ function summaryMulDivDown(uint256 x, uint256 y, uint256 d) returns uint256 { // Check that when not accruing interest, and when repaying all, the borrow ratio is at least reset to the initial ratio. // More details on the purpose of this rule in RatioMath.spec. -rule repayAllResetsBorrowRatio(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onbehalf, bytes data) { +rule repayAllResetsBorrowRatio(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { MorphoHarness.Id id = libId(marketParams); // Safe require because this invariant is checked in ConsistentState.spec require fee(id) <= maxFee(); @@ -43,7 +45,7 @@ rule repayAllResetsBorrowRatio(env e, MorphoHarness.MarketParams marketParams, u require lastUpdate(id) == e.block.timestamp; mathint repaidAssets; - repaidAssets, _ = repay(e, marketParams, assets, shares, onbehalf, data); + repaidAssets, _ = repay(e, marketParams, assets, shares, onBehalf, data); // Check the case where the market is fully repaid. require repaidAssets >= assetsBefore; @@ -59,63 +61,60 @@ rule repayAllResetsBorrowRatio(env e, MorphoHarness.MarketParams marketParams, u rule supplyWithdraw() { MorphoHarness.MarketParams marketParams; MorphoHarness.Id id = libId(marketParams); - uint256 assets; - uint256 shares; - address onbehalf; - address receiver; - bytes data; env e1; env e2; + address onBehalf; - // Assume that interactions to happen at the same block. + // Assume that interactions happen at the same block. require e1.block.timestamp == e2.block.timestamp; + // Assume that the user starts without any supply position. + require supplyShares(id, onBehalf) == 0; // Safe require because timestamps cannot realistically be that large. require e1.block.timestamp < 2^128; + uint256 supplyAssets; uint256 supplyShares; bytes data; uint256 suppliedAssets; uint256 suppliedShares; - suppliedAssets, suppliedShares = supply(e1, marketParams, assets, shares, onbehalf, data); + suppliedAssets, suppliedShares = supply(e1, marketParams, supplyAssets, supplyShares, onBehalf, data); // Hints for the prover. assert suppliedAssets * (virtualTotalSupplyShares(id) - suppliedShares) >= suppliedShares * (virtualTotalSupplyAssets(id) - suppliedAssets); assert suppliedAssets * virtualTotalSupplyShares(id) >= suppliedShares * virtualTotalSupplyAssets(id); + uint256 withdrawAssets; uint256 withdrawShares; address receiver; uint256 withdrawnAssets; - uint256 withdrawnShares; - withdrawnAssets, withdrawnShares = withdraw(e2, marketParams, 0, suppliedShares, onbehalf, receiver); + withdrawnAssets, _ = withdraw(e2, marketParams, withdrawAssets, withdrawShares, onBehalf, receiver); - assert withdrawnShares == suppliedShares; assert withdrawnAssets <= suppliedAssets; } -// There should be no profit from withdraw followed immediately by supply. -rule withdrawSupply() { +// There should be no profit from borrow followed immediately by repay. +rule borrowRepay() { MorphoHarness.MarketParams marketParams; MorphoHarness.Id id = libId(marketParams); - uint256 assets; - uint256 shares; - address onbehalf; - address receiver; - bytes data; + address onBehalf; env e1; env e2; - // Assume interactions to happen at the same block. + // Assume interactions happen at the same block. require e1.block.timestamp == e2.block.timestamp; + // Assume that the user starts without any borrow position. + require borrowShares(id, onBehalf) == 0; // Safe require because timestamps cannot realistically be that large. require e1.block.timestamp < 2^128; - uint256 withdrawnAssets; - uint256 withdrawnShares; - withdrawnAssets, withdrawnShares = withdraw(e2, marketParams, assets, shares, onbehalf, receiver); + uint256 borrowAssets; uint256 borrowShares; address receiver; + uint256 borrowedAssets; + uint256 borrowedShares; + borrowedAssets, borrowedShares = borrow(e2, marketParams, borrowAssets, borrowShares, onBehalf, receiver); // Hints for the prover. - assert withdrawnAssets * (virtualTotalSupplyShares(id) + withdrawnShares) <= withdrawnShares * (virtualTotalSupplyAssets(id) + withdrawnAssets); - assert withdrawnAssets * virtualTotalSupplyShares(id) <= withdrawnShares * virtualTotalSupplyAssets(id); + assert borrowedAssets * (virtualTotalBorrowShares(id) - borrowedShares) <= borrowedShares * (virtualTotalBorrowAssets(id) - borrowedAssets); + assert borrowedAssets * virtualTotalBorrowShares(id) <= borrowedShares * virtualTotalBorrowAssets(id); - uint256 suppliedAssets; - uint256 suppliedShares; - suppliedAssets, suppliedShares = supply(e1, marketParams, withdrawnAssets, 0, onbehalf, data); + uint256 repayAssets; uint256 repayShares; bytes data; + uint256 repaidAssets; + repaidAssets, _ = repay(e1, marketParams, repayAssets, repayShares, onBehalf, data); - assert suppliedAssets == withdrawnAssets && withdrawnShares >= suppliedShares; + assert borrowedAssets <= repaidAssets; } diff --git a/certora/specs/Liveness.spec b/certora/specs/Liveness.spec index 0c3fb3130..7d66662ad 100644 --- a/certora/specs/Liveness.spec +++ b/certora/specs/Liveness.spec @@ -98,8 +98,7 @@ rule supplyChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams marke // Check that you can supply non-zero tokens by passing shares. rule canSupplyByPassingShares(env e, MorphoInternalAccess.MarketParams marketParams, uint256 shares, address onBehalf, bytes data) { uint256 suppliedAssets; - uint256 suppliedShares; - suppliedAssets, suppliedShares = supply(e, marketParams, 0, shares, onBehalf, data); + suppliedAssets, _ = supply(e, marketParams, 0, shares, onBehalf, data); satisfy suppliedAssets != 0; } @@ -135,8 +134,7 @@ rule withdrawChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams mar // Check that you can withdraw non-zero tokens by passing shares. rule canWithdrawByPassingShares(env e, MorphoInternalAccess.MarketParams marketParams, uint256 shares, address onBehalf, address receiver) { uint256 withdrawnAssets; - uint256 withdrawnShares; - withdrawnAssets, withdrawnShares = withdraw(e, marketParams, 0, shares, onBehalf, receiver); + withdrawnAssets, _ = withdraw(e, marketParams, 0, shares, onBehalf, receiver); satisfy withdrawnAssets != 0; } @@ -172,8 +170,7 @@ rule borrowChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams marke // Check that you can borrow non-zero tokens by passing shares. rule canBorrowByPassingShares(env e, MorphoInternalAccess.MarketParams marketParams, uint256 shares, address onBehalf, address receiver) { uint256 borrowedAssets; - uint256 borrowedShares; - borrowedAssets, borrowedShares = borrow(e, marketParams, 0, shares, onBehalf, receiver); + borrowedAssets, _ = borrow(e, marketParams, 0, shares, onBehalf, receiver); satisfy borrowedAssets != 0; } @@ -212,8 +209,7 @@ rule repayChangesTokensAndShares(env e, MorphoInternalAccess.MarketParams market // Check that you can repay non-zero tokens by passing shares. rule canRepayByPassingShares(env e, MorphoInternalAccess.MarketParams marketParams, uint256 shares, address onBehalf, bytes data) { uint256 repaidAssets; - uint256 repaidShares; - repaidAssets, repaidShares = repay(e, marketParams, 0, shares, onBehalf, data); + repaidAssets, _ = repay(e, marketParams, 0, shares, onBehalf, data); satisfy repaidAssets != 0; } From c2522faad9c64a6f6c36af57c91927c5913f1a33 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 30 Nov 2023 10:14:26 +0100 Subject: [PATCH 193/204] refactor: remove Transfer address 0 case --- certora/specs/Transfer.spec | 4 ---- 1 file changed, 4 deletions(-) diff --git a/certora/specs/Transfer.spec b/certora/specs/Transfer.spec index e9861d323..a560f0658 100644 --- a/certora/specs/Transfer.spec +++ b/certora/specs/Transfer.spec @@ -63,8 +63,6 @@ rule transferRevertCondition(address token, address to, uint256 amount) { uint256 toInitialBalance = balanceOf(token, to); // Safe require because the total supply is greater than the sum of the balance of any two accounts. require to != currentContract => initialBalance + toInitialBalance <= to_mathint(totalSupply(token)); - // Some tokens revert when either the sender or the receiver is the zero address. - require currentContract != 0 && to != 0; libSafeTransfer@withrevert(token, to, amount); @@ -78,8 +76,6 @@ rule transferFromRevertCondition(address token, address from, address to, uint25 uint256 allowance = allowance(token, from, currentContract); // Safe require because the total supply is greater than the sum of the balance of any two accounts. require to != from => initialBalance + toInitialBalance <= to_mathint(totalSupply(token)); - // Some tokens revert when either the sender or the receiver is the zero address. - require from != 0 && to != 0; libSafeTransferFrom@withrevert(token, from, to, amount); From 193c8c954018557da9652fa8cbab5749dd52146b Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 30 Nov 2023 10:27:23 +0100 Subject: [PATCH 194/204] refactor: use isCreated function --- certora/specs/Reverts.spec | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/certora/specs/Reverts.spec b/certora/specs/Reverts.spec index 59554fef8..fec8b5771 100644 --- a/certora/specs/Reverts.spec +++ b/certora/specs/Reverts.spec @@ -42,7 +42,7 @@ definition exactlyOneZero(uint256 assets, uint256 shares) returns bool = (assets == 0 && shares != 0) || (assets != 0 && shares == 0); // This invariant catches bugs when not checking that the market is created with lastUpdate. -invariant notInitializedEmpty(MorphoHarness.Id id) +invariant notCreatedIsEmpty(MorphoHarness.Id id) !isCreated(id) => emptyMarket(id) { preserved with (env e) { @@ -108,9 +108,9 @@ rule createMarketRevertCondition(env e, MorphoHarness.MarketParams marketParams) MorphoHarness.Id id = libId(marketParams); bool irmEnabled = isIrmEnabled(marketParams.irm); bool lltvEnabled = isLltvEnabled(marketParams.lltv); - uint256 lastUpdated = lastUpdate(id); + bool wasCreated = isCreated(id); createMarket@withrevert(e, marketParams); - assert lastReverted <=> e.msg.value != 0 || !irmEnabled || !lltvEnabled || lastUpdated != 0; + assert lastReverted <=> e.msg.value != 0 || !irmEnabled || !lltvEnabled || wasCreated; } // Check that supply reverts when its input are not validated. @@ -173,7 +173,7 @@ rule setAuthorizationWithSigInputValidation(env e, MorphoHarness.Authorization a // Check that accrueInterest reverts when its inputs are not validated. rule accrueInterestInputValidation(env e, MorphoHarness.MarketParams marketParams) { - uint256 lastUpdate = lastUpdate(libId(marketParams)); + bool wasCreated = isCreated(libId(marketParams)); accrueInterest@withrevert(e, marketParams); - assert lastUpdate == 0 => lastReverted; + assert !wasCreated => lastReverted; } From 66998549e94a028a0ebe5dbc315460cd6fe28388 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 30 Nov 2023 10:29:15 +0100 Subject: [PATCH 195/204] feat: receiver 0 input validation --- certora/specs/Reverts.spec | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/certora/specs/Reverts.spec b/certora/specs/Reverts.spec index fec8b5771..8981107ff 100644 --- a/certora/specs/Reverts.spec +++ b/certora/specs/Reverts.spec @@ -125,7 +125,7 @@ rule withdrawInputValidation(env e, MorphoHarness.MarketParams marketParams, uin require e.msg.sender != 0; requireInvariant zeroDoesNotAuthorize(e.msg.sender); withdraw@withrevert(e, marketParams, assets, shares, onBehalf, receiver); - assert !exactlyOneZero(assets, shares) || onBehalf == 0 => lastReverted; + assert !exactlyOneZero(assets, shares) || onBehalf == 0 || receiver == 0 => lastReverted; } // Check that borrow reverts when its inputs are not validated. @@ -140,7 +140,7 @@ rule borrowInputValidation(env e, MorphoHarness.MarketParams marketParams, uint2 // Check that repay reverts when its inputs are not validated. rule repayInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { repay@withrevert(e, marketParams, assets, shares, onBehalf, data); - assert !exactlyOneZero(assets, shares) || onBehalf == 0 => lastReverted; + assert !exactlyOneZero(assets, shares) || onBehalf == 0 || receiver == 0 => lastReverted; } // Check that supplyCollateral reverts when its inputs are not validated. @@ -155,7 +155,7 @@ rule withdrawCollateralInputValidation(env e, MorphoHarness.MarketParams marketP require e.msg.sender != 0; requireInvariant zeroDoesNotAuthorize(e.msg.sender); withdrawCollateral@withrevert(e, marketParams, assets, onBehalf, receiver); - assert assets == 0 || onBehalf == 0 => lastReverted; + assert assets == 0 || onBehalf == 0 || receiver == 0 => lastReverted; } // Check that liquidate reverts when its inputs are not validated. From 9449ba77b281de9b2b67bb33654a1f7889c2d402 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 30 Nov 2023 10:43:40 +0100 Subject: [PATCH 196/204] docs: clarify Reentrancy --- certora/specs/Reentrancy.spec | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/certora/specs/Reentrancy.spec b/certora/specs/Reentrancy.spec index e66336305..726509bcf 100644 --- a/certora/specs/Reentrancy.spec +++ b/certora/specs/Reentrancy.spec @@ -5,11 +5,14 @@ methods { function _.borrowRate(MorphoHarness.MarketParams marketParams, MorphoHarness.Market) external => summaryBorrowRate() expect uint256; } +ghost bool delegateCall; +ghost bool callIsBorrowRate; +// True if storage has been accessed with either a SSTORE or a SLOAD. ghost bool hasAccessedStorage; +// True when a CALL has been done after storage has been accessed. ghost bool hasCallAfterAccessingStorage; +// True when storage has been accessed, after which an external call is made, followed by accessing storage again. ghost bool hasReentrancyUnsafeCall; -ghost bool delegate_call; -ghost bool callIsBorrowRate; function summaryBorrowRate() returns uint256 { uint256 result; @@ -31,14 +34,13 @@ hook CALL(uint g, address addr, uint value, uint argsOffset, uint argsLength, ui if (callIsBorrowRate) { // The calls to borrow rate are trusted and don't count. callIsBorrowRate = false; - hasCallAfterAccessingStorage = hasCallAfterAccessingStorage; } else { hasCallAfterAccessingStorage = hasAccessedStorage; } } hook DELEGATECALL(uint g, address addr, uint argsOffset, uint argsLength, uint retOffset, uint retLength) uint rc { - delegate_call = true; + delegateCall = true; } // Check that no function is accessing storage, then making an external CALL other than to the IRM, and accessing storage again. @@ -53,7 +55,7 @@ rule reentrancySafe(method f, env e, calldataarg data) { // Check that the contract is truly immutable. rule noDelegateCalls(method f, env e, calldataarg data) { // Set up the initial state. - require !delegate_call; + require !delegateCall; f(e,data); - assert !delegate_call; + assert !delegateCall; } From 46c66c989680b56be011c9adaebdb51236109e53 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 30 Nov 2023 10:58:03 +0100 Subject: [PATCH 197/204] docs: clarify ExitLiquidity owned/owed --- certora/specs/ExitLiquidity.spec | 22 +++++++++++----------- certora/specs/RatioMath.spec | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/certora/specs/ExitLiquidity.spec b/certora/specs/ExitLiquidity.spec index d967a7974..73c5af58c 100644 --- a/certora/specs/ExitLiquidity.spec +++ b/certora/specs/ExitLiquidity.spec @@ -16,7 +16,7 @@ methods { function libId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; } -// Check that it's not possible to withdraw more assets than what the user has supplied. +// Check that it's not possible to withdraw more assets than what the user owns. rule withdrawLiquidity(MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) { env e; MorphoHarness.Id id = libId(marketParams); @@ -27,27 +27,27 @@ rule withdrawLiquidity(MorphoHarness.MarketParams marketParams, uint256 assets, uint256 initialShares = supplyShares(id, onBehalf); uint256 initialTotalSupply = virtualTotalSupplyAssets(id); uint256 initialTotalSupplyShares = virtualTotalSupplyShares(id); - uint256 owedAssets = libMulDivDown(initialShares, initialTotalSupply, initialTotalSupplyShares); + uint256 ownedAssets = libMulDivDown(initialShares, initialTotalSupply, initialTotalSupplyShares); uint256 withdrawnAssets; withdrawnAssets, _ = withdraw(e, marketParams, assets, shares, onBehalf, receiver); - assert withdrawnAssets <= owedAssets; + assert withdrawnAssets <= ownedAssets; } -// Check that it's not possible to withdraw more collateral than what the user has supplied. -rule withdrawCollateralLiquidity(MorphoHarness.MarketParams marketParams, uint256 assets, address onBehalf, address receiver) { +// Check that it's not possible to withdraw more collateral than what the user owns. +rule withdrawCollateralLiquidity(MorphoHarness.MarketParams marketParams, uint256 withdrawnAssets, address onBehalf, address receiver) { env e; MorphoHarness.Id id = libId(marketParams); - uint256 initialCollateral = collateral(id, onBehalf); + uint256 ownedAssets = collateral(id, onBehalf); - withdrawCollateral(e, marketParams, assets, onBehalf, receiver); + withdrawCollateral(e, marketParams, withdrawnAssets, onBehalf, receiver); - assert assets <= initialCollateral; + assert withdrawnAssets <= ownedAssets; } -// Check than when repaying the full outstanding debt requires more assets than what was borrowed. +// Check than when repaying the full outstanding debt requires more assets than what the user owes. rule repayLiquidity(MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { env e; MorphoHarness.Id id = libId(marketParams); @@ -58,7 +58,7 @@ rule repayLiquidity(MorphoHarness.MarketParams marketParams, uint256 assets, uin uint256 initialShares = borrowShares(id, onBehalf); uint256 initialTotalBorrow = virtualTotalBorrowAssets(id); uint256 initialTotalBorrowShares = virtualTotalBorrowShares(id); - uint256 assetsDue = libMulDivUp(initialShares, initialTotalBorrow, initialTotalBorrowShares); + uint256 owedAssets = libMulDivUp(initialShares, initialTotalBorrow, initialTotalBorrowShares); uint256 repaidAssets; repaidAssets, _ = repay(e, marketParams, assets, shares, onBehalf, data); @@ -66,5 +66,5 @@ rule repayLiquidity(MorphoHarness.MarketParams marketParams, uint256 assets, uin // Assume a full repay. require borrowShares(id, onBehalf) == 0; - assert repaidAssets >= assetsDue; + assert repaidAssets >= owedAssets; } diff --git a/certora/specs/RatioMath.spec b/certora/specs/RatioMath.spec index e07ee77b9..2ae82a8bc 100644 --- a/certora/specs/RatioMath.spec +++ b/certora/specs/RatioMath.spec @@ -75,7 +75,7 @@ rule accrueInterestIncreasesBorrowRatio(env e, MorphoHarness.MarketParams market } -// Check that excepti for liquidate, every function increases the value of supply shares. +// Check that except when not accruing interest and except for liquidate, every function increases the value of supply shares. rule onlyLiquidateCanDecreaseSupplyRatio(env e, method f, calldataarg args) filtered { f -> !f.isView && f.selector != sig:liquidate(MorphoHarness.MarketParams, address, uint256, uint256, bytes).selector From 5870024105d40ba88371c974fdf31960fe622691 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 30 Nov 2023 11:08:06 +0100 Subject: [PATCH 198/204] docs: alwaysCollateralized comment --- certora/specs/Health.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certora/specs/Health.spec b/certora/specs/Health.spec index cdc689135..7f13eedee 100644 --- a/certora/specs/Health.spec +++ b/certora/specs/Health.spec @@ -103,6 +103,6 @@ filtered { f -> !f.isView } } // Check that users without collateral also have no debt. -// This invariant ensures that bad debt is always accounted. +// This invariant ensures that bad debt realization cannot be bypassed. invariant alwaysCollateralized(MorphoHarness.Id id, address borrower) borrowShares(id, borrower) != 0 => collateral(id, borrower) != 0; From ff354797851e74ccd3c8960b9dc19d25171f9d55 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 30 Nov 2023 11:11:03 +0100 Subject: [PATCH 199/204] refactor: only check interest overflow on the supply --- certora/specs/Health.spec | 4 ++-- certora/specs/Liveness.spec | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/certora/specs/Health.spec b/certora/specs/Health.spec index 7f13eedee..dc565b909 100644 --- a/certora/specs/Health.spec +++ b/certora/specs/Health.spec @@ -30,13 +30,13 @@ function mockPrice() returns uint256 { } function summaryMulDivUp(uint256 x, uint256 y, uint256 d) returns uint256 { - // Safe requires because the reference implementation would revert. + // Safe require because the reference implementation would revert. require d != 0; return require_uint256((x * y + (d - 1)) / d); } function summaryMulDivDown(uint256 x, uint256 y, uint256 d) returns uint256 { - // Safe requires because the reference implementation would revert. + // Safe require because the reference implementation would revert. require d != 0; return require_uint256((x * y) / d); } diff --git a/certora/specs/Liveness.spec b/certora/specs/Liveness.spec index 0c3fb3130..78d5d63b4 100644 --- a/certora/specs/Liveness.spec +++ b/certora/specs/Liveness.spec @@ -53,10 +53,8 @@ function summaryAccrueInterest(env e, MorphoInternalAccess.MarketParams marketPa require e.block.timestamp < 2^128; if (e.block.timestamp != lastUpdate(id) && totalBorrowAssets(id) != 0) { uint128 interest; - uint256 borrow = totalBorrowAssets(id); uint256 supply = totalSupplyAssets(id); - // Safe requires because the reference implementation would revert. - require interest + borrow < 2^256; + // Safe require because the reference implementation would revert. require interest + supply < 2^256; increaseInterest(e, id, interest); } From d590bbede1a3ffdd005185c75eef554fc607b491 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 30 Nov 2023 11:15:36 +0100 Subject: [PATCH 200/204] fix: receiver parameter wrong place --- certora/specs/Reverts.spec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certora/specs/Reverts.spec b/certora/specs/Reverts.spec index 8981107ff..9ae422c51 100644 --- a/certora/specs/Reverts.spec +++ b/certora/specs/Reverts.spec @@ -134,13 +134,13 @@ rule borrowInputValidation(env e, MorphoHarness.MarketParams marketParams, uint2 require e.msg.sender != 0; requireInvariant zeroDoesNotAuthorize(e.msg.sender); borrow@withrevert(e, marketParams, assets, shares, onBehalf, receiver); - assert !exactlyOneZero(assets, shares) || onBehalf == 0 => lastReverted; + assert !exactlyOneZero(assets, shares) || onBehalf == 0 || receiver == 0 => lastReverted; } // Check that repay reverts when its inputs are not validated. rule repayInputValidation(env e, MorphoHarness.MarketParams marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) { repay@withrevert(e, marketParams, assets, shares, onBehalf, data); - assert !exactlyOneZero(assets, shares) || onBehalf == 0 || receiver == 0 => lastReverted; + assert !exactlyOneZero(assets, shares) || onBehalf == 0 => lastReverted; } // Check that supplyCollateral reverts when its inputs are not validated. From 7e9a1b8b7fcecbb71b9ee391faee7f3a280249ae Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 30 Nov 2023 11:26:30 +0100 Subject: [PATCH 201/204] docs: small comment fix --- certora/specs/Reentrancy.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certora/specs/Reentrancy.spec b/certora/specs/Reentrancy.spec index 726509bcf..6ec26dc4e 100644 --- a/certora/specs/Reentrancy.spec +++ b/certora/specs/Reentrancy.spec @@ -7,7 +7,7 @@ methods { ghost bool delegateCall; ghost bool callIsBorrowRate; -// True if storage has been accessed with either a SSTORE or a SLOAD. +// True when storage has been accessed with either a SSTORE or a SLOAD. ghost bool hasAccessedStorage; // True when a CALL has been done after storage has been accessed. ghost bool hasCallAfterAccessingStorage; From 17d221d150fd94cf83cb9df013c45068e6a3621f Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 30 Nov 2023 11:46:29 +0100 Subject: [PATCH 202/204] feat: liveness liquidate by passing shares --- certora/specs/Liveness.spec | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/certora/specs/Liveness.spec b/certora/specs/Liveness.spec index 0c3fb3130..198cb8075 100644 --- a/certora/specs/Liveness.spec +++ b/certora/specs/Liveness.spec @@ -293,6 +293,15 @@ rule liquidateChangesTokens(env e, MorphoInternalAccess.MarketParams marketParam assert liquidityAfter == liquidityBefore + min(repaidAssets, borrowLoanAssetsBefore); } +// Check that you can liquidate non-zero tokens by passing shares. +rule canLiquidateByPassingShares(env e, MorphoInternalAccess.MarketParams marketParams, address borrower, uint256 repaidShares, bytes data) { + uint256 seizedAssets; + uint256 repaidAssets; + seizedAssets, repaidAssets = liquidate(e, marketParams, borrower, 0, repaidShares, data); + + satisfy seizedAssets != 0 && repaidAssets != 0; +} + // Check that nonce and authorization are properly updated with calling setAuthorizationWithSig. rule setAuthorizationWithSigChangesNonceAndAuthorizes(env e, MorphoInternalAccess.Authorization authorization, MorphoInternalAccess.Signature signature) { mathint nonceBefore = nonce(authorization.authorizer); From 83f692f3092957906f2608e55d62b98d75a7c711 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 30 Nov 2023 15:46:52 +0100 Subject: [PATCH 203/204] fix: rules specification --- certora/specs/ExactMath.spec | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/certora/specs/ExactMath.spec b/certora/specs/ExactMath.spec index e0e00369d..d25465c25 100644 --- a/certora/specs/ExactMath.spec +++ b/certora/specs/ExactMath.spec @@ -2,14 +2,15 @@ methods { function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE; + function feeRecipient() external returns address envfree; function supplyShares(MorphoHarness.Id, address) external returns uint256 envfree; function borrowShares(MorphoHarness.Id, address) external returns uint256 envfree; function virtualTotalSupplyAssets(MorphoHarness.Id) external returns uint256 envfree; function virtualTotalSupplyShares(MorphoHarness.Id) external returns uint256 envfree; function virtualTotalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree; function virtualTotalBorrowShares(MorphoHarness.Id) external returns uint256 envfree; - function fee(MorphoHarness.Id) external returns uint256 envfree; function lastUpdate(MorphoHarness.Id) external returns uint256 envfree; + function fee(MorphoHarness.Id) external returns uint256 envfree; function maxFee() external returns uint256 envfree; function libId(MorphoHarness.MarketParams) external returns MorphoHarness.Id envfree; @@ -69,6 +70,8 @@ rule supplyWithdraw() { require e1.block.timestamp == e2.block.timestamp; // Assume that the user starts without any supply position. require supplyShares(id, onBehalf) == 0; + // Assume that the user is not the fee recipient, otherwise the gain can come from the fee. + require onBehalf != feeRecipient(); // Safe require because timestamps cannot realistically be that large. require e1.block.timestamp < 2^128; @@ -88,7 +91,7 @@ rule supplyWithdraw() { assert withdrawnAssets <= suppliedAssets; } -// There should be no profit from borrow followed immediately by repay. +// There should be no profit from borrow followed immediately by repaying all. rule borrowRepay() { MorphoHarness.MarketParams marketParams; MorphoHarness.Id id = libId(marketParams); @@ -112,9 +115,9 @@ rule borrowRepay() { assert borrowedAssets * (virtualTotalBorrowShares(id) - borrowedShares) <= borrowedShares * (virtualTotalBorrowAssets(id) - borrowedAssets); assert borrowedAssets * virtualTotalBorrowShares(id) <= borrowedShares * virtualTotalBorrowAssets(id); - uint256 repayAssets; uint256 repayShares; bytes data; + bytes data; uint256 repaidAssets; - repaidAssets, _ = repay(e1, marketParams, repayAssets, repayShares, onBehalf, data); + repaidAssets, _ = repay(e1, marketParams, 0, borrowedShares, onBehalf, data); assert borrowedAssets <= repaidAssets; } From c265b461a21160926c00639f391e394e5ebdc506 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 1 Dec 2023 14:13:24 +0100 Subject: [PATCH 204/204] refactor: clean Certora CI --- .github/workflows/certora.yml | 42 ++++++++++++++++------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml index bd08ca33f..402149e56 100644 --- a/.github/workflows/certora.yml +++ b/.github/workflows/certora.yml @@ -11,6 +11,23 @@ jobs: verify: runs-on: ubuntu-latest + strategy: + fail-fast: false + + matrix: + conf: + - AccrueInterest + - ConsistentState + - ExactMath + - ExitLiquidity + - Health + - LibSummary + - Liveness + - RatioMath + - Reentrancy + - Reverts + - Transfer + steps: - uses: actions/checkout@v3 with: @@ -18,8 +35,6 @@ jobs: - name: Install python uses: actions/setup-python@v4 - with: - python-version: "3.10" - name: Install certora run: pip install certora-cli @@ -30,26 +45,7 @@ jobs: chmod +x solc-static-linux sudo mv solc-static-linux /usr/local/bin/solc - - name: Verify rule ${{ matrix.script }} - run: | - echo "key length" ${#CERTORAKEY} - certoraRun certora/confs/${{ matrix.conf }}.conf + - name: Verify ${{ matrix.conf }} + run: certoraRun certora/confs/${{ matrix.conf }}.conf env: CERTORAKEY: ${{ secrets.CERTORAKEY }} - - strategy: - fail-fast: false - - matrix: - conf: - - AccrueInterest - - ConsistentState - - ExactMath - - ExitLiquidity - - Health - - LibSummary - - Liveness - - RatioMath - - Reentrancy - - Reverts - - Transfer