Skip to content

Commit

Permalink
Merge pull request #633 from morpho-org/post-cantina
Browse files Browse the repository at this point in the history
post-cantina
  • Loading branch information
MerlinEgalite authored Dec 23, 2023
2 parents 39f5b8a + 24b4d02 commit 55d2d99
Show file tree
Hide file tree
Showing 41 changed files with 290 additions and 406 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/foundry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
uses: foundry-rs/foundry-toolchain@v1

- name: Run Forge tests in ${{ matrix.type }} mode
run: forge test -vvv
run: yarn test:forge -vvv
env:
FOUNDRY_FUZZ_RUNS: ${{ matrix.fuzz-runs }}
FOUNDRY_FUZZ_MAX_TEST_REJECTS: ${{ matrix.max-test-rejects }}
Expand Down
2 changes: 2 additions & 0 deletions certora/confs/AccrueInterest.conf
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
],
"verify": "MorphoHarness:certora/specs/AccrueInterest.spec",
"prover_args": [
"-depth 3",
"-smt_hashingScheme plaininjectivity",
"-mediumTimeout 30"
],
"rule_sanity": "basic",
"server": "production",
"msg": "Morpho Blue Accrue Interest"
}
1 change: 1 addition & 0 deletions certora/confs/AssetsAccounting.conf
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
],
"verify": "MorphoHarness:certora/specs/AssetsAccounting.spec",
"rule_sanity": "basic",
"server": "production",
"msg": "Morpho Blue Assets Accounting"
}
1 change: 1 addition & 0 deletions certora/confs/ConsistentState.conf
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
],
"verify": "MorphoHarness:certora/specs/ConsistentState.spec",
"rule_sanity": "basic",
"server": "production",
"msg": "Morpho Blue Consistent State"
}
1 change: 1 addition & 0 deletions certora/confs/ExactMath.conf
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
"-smt_hashingScheme plaininjectivity",
"-mediumTimeout 30"
],
"server": "production",
"msg": "Morpho Blue Exact Math"
}
1 change: 1 addition & 0 deletions certora/confs/Health.conf
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
"prover_args": [
"-smt_hashingScheme plaininjectivity"
],
"server": "production",
"msg": "Morpho Blue Health"
}
1 change: 1 addition & 0 deletions certora/confs/LibSummary.conf
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
],
"verify": "MorphoHarness:certora/specs/LibSummary.spec",
"rule_sanity": "basic",
"server": "production",
"msg": "Morpho Blue Lib Summary"
}
1 change: 1 addition & 0 deletions certora/confs/Liveness.conf
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
],
"verify": "MorphoInternalAccess:certora/specs/Liveness.spec",
"rule_sanity": "basic",
"server": "production",
"msg": "Morpho Blue Liveness"
}
1 change: 1 addition & 0 deletions certora/confs/RatioMath.conf
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@
"-mediumTimeout 30",
"-timeout 3600"
],
"server": "production",
"msg": "Morpho Blue Ratio Math"
}
1 change: 1 addition & 0 deletions certora/confs/Reentrancy.conf
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
"prover_args": [
"-enableStorageSplitting false"
],
"server": "production",
"msg": "Morpho Blue Reentrancy"
}
1 change: 1 addition & 0 deletions certora/confs/Reverts.conf
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
],
"verify": "MorphoHarness:certora/specs/Reverts.spec",
"rule_sanity": "basic",
"server": "production",
"msg": "Morpho Blue Reverts"
}
1 change: 1 addition & 0 deletions certora/confs/Transfer.conf
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
],
"verify": "TransferHarness:certora/specs/Transfer.spec",
"rule_sanity": "basic",
"server": "production",
"msg": "Morpho Blue Transfer"
}
6 changes: 3 additions & 3 deletions certora/specs/Reverts.spec
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,14 @@ 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) {
// Check that createMarket reverts when its input are not validated.
rule createMarketInputValidation(env e, MorphoHarness.MarketParams marketParams) {
MorphoHarness.Id id = libId(marketParams);
bool irmEnabled = isIrmEnabled(marketParams.irm);
bool lltvEnabled = isLltvEnabled(marketParams.lltv);
bool wasCreated = isCreated(id);
createMarket@withrevert(e, marketParams);
assert lastReverted <=> e.msg.value != 0 || !irmEnabled || !lltvEnabled || wasCreated;
assert e.msg.value != 0 || !irmEnabled || !lltvEnabled || wasCreated => lastReverted;
}

// Check that supply reverts when its input are not validated.
Expand Down
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
names = true
sizes = true
via-ir = true
optimizer_runs = 4294967295
optimizer_runs = 999999 # Etherscan does not support verifying contracts with more optimization runs.

[profile.default.invariant]
runs = 8
Expand Down
82 changes: 48 additions & 34 deletions src/Morpho.sol
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ contract Morpho is IMorphoStaticTyping {
idToMarketParams[id] = marketParams;

emit EventsLib.CreateMarket(id, marketParams);

// Call to initialize the IRM in case it is stateful.
if (marketParams.irm != address(0)) IIrm(marketParams.irm).borrowRate(marketParams, market[id]);
}

/* SUPPLY MANAGEMENT */
Expand Down Expand Up @@ -354,28 +357,29 @@ contract Morpho is IMorphoStaticTyping {

_accrueInterest(marketParams, id);

uint256 collateralPrice = IOracle(marketParams.oracle).price();
{
uint256 collateralPrice = IOracle(marketParams.oracle).price();

require(!_isHealthy(marketParams, id, borrower, collateralPrice), ErrorsLib.HEALTHY_POSITION);
require(!_isHealthy(marketParams, id, borrower, collateralPrice), ErrorsLib.HEALTHY_POSITION);

uint256 repaidAssets;
{
// The liquidation incentive factor is min(maxLiquidationIncentiveFactor, 1/(1 - cursor*(1 - lltv))).
uint256 liquidationIncentiveFactor = UtilsLib.min(
MAX_LIQUIDATION_INCENTIVE_FACTOR,
WAD.wDivDown(WAD - LIQUIDATION_CURSOR.wMulDown(WAD - marketParams.lltv))
);

if (seizedAssets > 0) {
repaidAssets =
seizedAssets.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE).wDivUp(liquidationIncentiveFactor);
repaidShares = repaidAssets.toSharesDown(market[id].totalBorrowAssets, market[id].totalBorrowShares);
uint256 seizedAssetsQuoted = seizedAssets.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE);

repaidShares = seizedAssetsQuoted.wDivUp(liquidationIncentiveFactor).toSharesUp(
market[id].totalBorrowAssets, market[id].totalBorrowShares
);
} else {
repaidAssets = repaidShares.toAssetsUp(market[id].totalBorrowAssets, market[id].totalBorrowShares);
seizedAssets =
repaidAssets.wMulDown(liquidationIncentiveFactor).mulDivDown(ORACLE_PRICE_SCALE, collateralPrice);
seizedAssets = repaidShares.toAssetsDown(market[id].totalBorrowAssets, market[id].totalBorrowShares)
.wMulDown(liquidationIncentiveFactor).mulDivDown(ORACLE_PRICE_SCALE, collateralPrice);
}
}
uint256 repaidAssets = repaidShares.toAssetsUp(market[id].totalBorrowAssets, market[id].totalBorrowShares);

position[id][borrower].borrowShares -= repaidShares.toUint128();
market[id].totalBorrowShares -= repaidShares.toUint128();
Expand All @@ -384,23 +388,26 @@ contract Morpho is IMorphoStaticTyping {
position[id][borrower].collateral -= seizedAssets.toUint128();

uint256 badDebtShares;
uint256 badDebtAssets;
if (position[id][borrower].collateral == 0) {
badDebtShares = position[id][borrower].borrowShares;
uint256 badDebt = UtilsLib.min(
badDebtAssets = UtilsLib.min(
market[id].totalBorrowAssets,
badDebtShares.toAssetsUp(market[id].totalBorrowAssets, market[id].totalBorrowShares)
);

market[id].totalBorrowAssets -= badDebt.toUint128();
market[id].totalSupplyAssets -= badDebt.toUint128();
market[id].totalBorrowAssets -= badDebtAssets.toUint128();
market[id].totalSupplyAssets -= badDebtAssets.toUint128();
market[id].totalBorrowShares -= badDebtShares.toUint128();
position[id][borrower].borrowShares = 0;
}

IERC20(marketParams.collateralToken).safeTransfer(msg.sender, seizedAssets);

// `repaidAssets` may be greater than `totalBorrowAssets` by 1.
emit EventsLib.Liquidate(id, msg.sender, borrower, repaidAssets, repaidShares, seizedAssets, badDebtShares);
emit EventsLib.Liquidate(
id, msg.sender, borrower, repaidAssets, repaidShares, seizedAssets, badDebtAssets, badDebtShares
);

IERC20(marketParams.collateralToken).safeTransfer(msg.sender, seizedAssets);

if (data.length > 0) IMorphoLiquidateCallback(msg.sender).onMorphoLiquidate(repaidAssets, data);

Expand All @@ -413,10 +420,12 @@ contract Morpho is IMorphoStaticTyping {

/// @inheritdoc IMorphoBase
function flashLoan(address token, uint256 assets, bytes calldata data) external {
IERC20(token).safeTransfer(msg.sender, assets);
require(assets != 0, ErrorsLib.ZERO_ASSETS);

emit EventsLib.FlashLoan(msg.sender, token, assets);

IERC20(token).safeTransfer(msg.sender, assets);

IMorphoFlashLoanCallback(msg.sender).onMorphoFlashLoan(assets, data);

IERC20(token).safeTransferFrom(msg.sender, address(this), assets);
Expand All @@ -426,18 +435,21 @@ contract Morpho is IMorphoStaticTyping {

/// @inheritdoc IMorphoBase
function setAuthorization(address authorized, bool newIsAuthorized) external {
require(newIsAuthorized != isAuthorized[msg.sender][authorized], ErrorsLib.ALREADY_SET);

isAuthorized[msg.sender][authorized] = newIsAuthorized;

emit EventsLib.SetAuthorization(msg.sender, msg.sender, authorized, newIsAuthorized);
}

/// @inheritdoc IMorphoBase
function setAuthorizationWithSig(Authorization memory authorization, Signature calldata signature) external {
/// Do not check whether authorization is already set because the nonce increment is a desired side effect.
require(block.timestamp <= authorization.deadline, ErrorsLib.SIGNATURE_EXPIRED);
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));
bytes32 digest = keccak256(bytes.concat("\x19\x01", DOMAIN_SEPARATOR, hashStruct));
address signatory = ecrecover(digest, signature.v, signature.r, signature.s);

require(signatory != address(0) && authorization.authorizer == signatory, ErrorsLib.INVALID_SIGNATURE);
Expand Down Expand Up @@ -470,25 +482,27 @@ contract Morpho is IMorphoStaticTyping {
/// @dev Assumes that the inputs `marketParams` and `id` match.
function _accrueInterest(MarketParams memory marketParams, Id id) internal {
uint256 elapsed = block.timestamp - market[id].lastUpdate;

if (elapsed == 0) return;

uint256 borrowRate = IIrm(marketParams.irm).borrowRate(marketParams, market[id]);
uint256 interest = market[id].totalBorrowAssets.wMulDown(borrowRate.wTaylorCompounded(elapsed));
market[id].totalBorrowAssets += interest.toUint128();
market[id].totalSupplyAssets += interest.toUint128();

uint256 feeShares;
if (market[id].fee != 0) {
uint256 feeAmount = interest.wMulDown(market[id].fee);
// The fee amount is subtracted from the total supply in this calculation to compensate for the fact
// that total supply is already increased by the full interest (including the fee amount).
feeShares = feeAmount.toSharesDown(market[id].totalSupplyAssets - feeAmount, market[id].totalSupplyShares);
position[id][feeRecipient].supplyShares += feeShares;
market[id].totalSupplyShares += feeShares.toUint128();
}
if (marketParams.irm != address(0)) {
uint256 borrowRate = IIrm(marketParams.irm).borrowRate(marketParams, market[id]);
uint256 interest = market[id].totalBorrowAssets.wMulDown(borrowRate.wTaylorCompounded(elapsed));
market[id].totalBorrowAssets += interest.toUint128();
market[id].totalSupplyAssets += interest.toUint128();

uint256 feeShares;
if (market[id].fee != 0) {
uint256 feeAmount = interest.wMulDown(market[id].fee);
// The fee amount is subtracted from the total supply in this calculation to compensate for the fact
// that total supply is already increased by the full interest (including the fee amount).
feeShares =
feeAmount.toSharesDown(market[id].totalSupplyAssets - feeAmount, market[id].totalSupplyShares);
position[id][feeRecipient].supplyShares += feeShares;
market[id].totalSupplyShares += feeShares.toUint128();
}

emit EventsLib.AccrueInterest(id, borrowRate, interest, feeShares);
emit EventsLib.AccrueInterest(id, borrowRate, interest, feeShares);
}

// Safe "unchecked" cast.
market[id].lastUpdate = uint128(block.timestamp);
Expand Down
5 changes: 3 additions & 2 deletions src/interfaces/IIrm.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import {MarketParams, Market} from "./IMorpho.sol";
/// @custom:contact [email protected]
/// @notice Interface that Interest Rate Models (IRMs) used by Morpho must implement.
interface IIrm {
/// @notice Returns the borrow rate of the market `marketParams`.
/// @notice Returns the borrow rate per second (scaled by WAD) of the market `marketParams`.
/// @dev Assumes that `market` corresponds to `marketParams`.
function borrowRate(MarketParams memory marketParams, Market memory market) external returns (uint256);

/// @notice Returns the borrow rate of the market `marketParams` without modifying any storage.
/// @notice Returns the borrow rate per second (scaled by WAD) of the market `marketParams` without modifying any
/// storage.
/// @dev Assumes that `market` corresponds to `marketParams`.
function borrowRateView(MarketParams memory marketParams, Market memory market) external view returns (uint256);
}
31 changes: 17 additions & 14 deletions src/interfaces/IMorpho.sol
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ interface IMorphoBase {
/// @notice Whether the `lltv` is enabled.
function isLltvEnabled(uint256 lltv) external view returns (bool);

/// @notice Whether `authorized` is authorized to modify `authorizer`'s positions.
/// @notice Whether `authorized` is authorized to modify `authorizer`'s position on all markets.
/// @dev Anyone is authorized to modify their own positions, regardless of this variable.
function isAuthorized(address authorizer, address authorized) external view returns (bool);

Expand All @@ -91,6 +91,7 @@ interface IMorphoBase {
function enableLltv(uint256 lltv) external;

/// @notice Sets the `newFee` for the given market `marketParams`.
/// @param newFee The new fee, scaled by WAD.
/// @dev Warning: The recipient can be the zero address.
function setFee(MarketParams memory marketParams, uint256 newFee) external;

Expand Down Expand Up @@ -129,12 +130,12 @@ interface IMorphoBase {

/// @notice Supplies `assets` or `shares` on behalf of `onBehalf`, optionally calling back the caller's
/// `onMorphoSupply` function with the given `data`.
/// @dev Either `assets` or `shares` should be zero. Most usecases should rely on `assets` as an input so the caller
/// is guaranteed to have `assets` tokens pulled from their balance, but the possibility to mint a specific amount
/// of shares is given for full compatibility and precision.
/// @dev If the supply of a market gets depleted, the supply share price instantly resets to
/// `VIRTUAL_ASSETS`:`VIRTUAL_SHARES`.
/// @dev Either `assets` or `shares` should be zero. Most use cases should rely on `assets` as an input so the
/// caller is guaranteed to have `assets` tokens pulled from their balance, but the possibility to mint a specific
/// amount of shares is given for full compatibility and precision.
/// @dev Supplying a large amount can revert for overflow.
/// @dev Supplying an amount of shares may lead to supply more or fewer assets than expected due to slippage.
/// Consider using the `assets` parameter to avoid this.
/// @param marketParams The market to supply assets to.
/// @param assets The amount of assets to supply.
/// @param shares The amount of shares to mint.
Expand All @@ -150,7 +151,7 @@ interface IMorphoBase {
bytes memory data
) external returns (uint256 assetsSupplied, uint256 sharesSupplied);

/// @notice Withdraws `assets` or `shares` on behalf of `onBehalf` to `receiver`.
/// @notice Withdraws `assets` or `shares` on behalf of `onBehalf` and sends the assets to `receiver`.
/// @dev Either `assets` or `shares` should be zero. To withdraw max, pass the `shares`'s balance of `onBehalf`.
/// @dev `msg.sender` must be authorized to manage `onBehalf`'s positions.
/// @dev Withdrawing an amount corresponding to more shares than supplied will revert for underflow.
Expand All @@ -171,14 +172,14 @@ interface IMorphoBase {
address receiver
) external returns (uint256 assetsWithdrawn, uint256 sharesWithdrawn);

/// @notice Borrows `assets` or `shares` on behalf of `onBehalf` to `receiver`.
/// @dev Either `assets` or `shares` should be zero. Most usecases should rely on `assets` as an input so the caller
/// is guaranteed to borrow `assets` of tokens, but the possibility to mint a specific amount of shares is given for
/// full compatibility and precision.
/// @dev If the borrow of a market gets depleted, the borrow share price instantly resets to
/// `VIRTUAL_ASSETS`:`VIRTUAL_SHARES`.
/// @notice Borrows `assets` or `shares` on behalf of `onBehalf` and sends the assets to `receiver`.
/// @dev Either `assets` or `shares` should be zero. Most use cases should rely on `assets` as an input so the
/// caller is guaranteed to borrow `assets` of tokens, but the possibility to mint a specific amount of shares is
/// given for full compatibility and precision.
/// @dev `msg.sender` must be authorized to manage `onBehalf`'s positions.
/// @dev Borrowing a large amount can revert for overflow.
/// @dev Borrowing an amount of shares may lead to borrow fewer assets than expected due to slippage.
/// Consider using the `assets` parameter to avoid this.
/// @param marketParams The market to borrow assets from.
/// @param assets The amount of assets to borrow.
/// @param shares The amount of shares to mint.
Expand All @@ -200,6 +201,7 @@ interface IMorphoBase {
/// @dev Repaying an amount corresponding to more shares than borrowed will revert for underflow.
/// @dev It is advised to use the `shares` input when repaying the full position to avoid reverts due to conversion
/// roundings between shares and assets.
/// @dev An attacker can front-run a repay with a small repay making the transaction revert for underflow.
/// @param marketParams The market to repay assets to.
/// @param assets The amount of assets to repay.
/// @param shares The amount of shares to burn.
Expand All @@ -226,7 +228,7 @@ interface IMorphoBase {
function supplyCollateral(MarketParams memory marketParams, uint256 assets, address onBehalf, bytes memory data)
external;

/// @notice Withdraws `assets` of collateral on behalf of `onBehalf` to `receiver`.
/// @notice Withdraws `assets` of collateral on behalf of `onBehalf` and sends the assets to `receiver`.
/// @dev `msg.sender` must be authorized to manage `onBehalf`'s positions.
/// @dev Withdrawing an amount corresponding to more collateral than supplied will revert for underflow.
/// @param marketParams The market to withdraw collateral from.
Expand All @@ -242,6 +244,7 @@ interface IMorphoBase {
/// @dev Either `seizedAssets` or `repaidShares` should be zero.
/// @dev Seizing more than the collateral balance will underflow and revert without any error message.
/// @dev Repaying more than the borrow balance will underflow and revert without any error message.
/// @dev An attacker can front-run a liquidation with a small repay making the transaction revert for underflow.
/// @param marketParams The market of the position.
/// @param borrower The owner of the position.
/// @param seizedAssets The amount of collateral to seize.
Expand Down
3 changes: 3 additions & 0 deletions src/libraries/ErrorsLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ library ErrorsLib {
/// @notice Thrown when the market is already created.
string internal constant MARKET_ALREADY_CREATED = "market already created";

/// @notice Thrown when a token to transfer doesn't have code.
string internal constant NO_CODE = "no code";

/// @notice Thrown when the market is not created.
string internal constant MARKET_NOT_CREATED = "market not created";

Expand Down
Loading

0 comments on commit 55d2d99

Please sign in to comment.