Skip to content

Commit

Permalink
feat: no share price decrease
Browse files Browse the repository at this point in the history
  • Loading branch information
MathisGD committed Jul 31, 2024
1 parent a10cc16 commit a0934f8
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 37 deletions.
79 changes: 44 additions & 35 deletions src/MetaMorpho.sol
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
/// @inheritdoc IMetaMorphoBase
uint256 public lastTotalAssets;

/// @inheritdoc IMetaMorphoBase
uint256 public hole;

/* CONSTRUCTOR */

/// @dev Initializes the contract.
Expand Down Expand Up @@ -233,8 +236,8 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
if (newFee > ConstantsLib.MAX_FEE) revert ErrorsLib.MaxFeeExceeded();
if (newFee != 0 && feeRecipient == address(0)) revert ErrorsLib.ZeroFeeRecipient();

// Accrue fee using the previous fee set before changing it.
_updateLastTotalAssets(_accrueFee());
// Accrue interest and fee using the previous fee set before changing it.
_accrueInterest();

// Safe "unchecked" cast because newFee <= MAX_FEE.
fee = uint96(newFee);
Expand All @@ -247,8 +250,8 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
if (newFeeRecipient == feeRecipient) revert ErrorsLib.AlreadySet();
if (newFeeRecipient == address(0) && fee != 0) revert ErrorsLib.ZeroFeeRecipient();

// Accrue fee to the previous fee recipient set before changing it.
_updateLastTotalAssets(_accrueFee());
// Accrue interest and fee to the previous fee recipient set before changing it.
_accrueInterest();

feeRecipient = newFeeRecipient;

Expand Down Expand Up @@ -529,63 +532,53 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph

/// @inheritdoc IERC4626
function deposit(uint256 assets, address receiver) public override returns (uint256 shares) {
uint256 newTotalAssets = _accrueFee();
_accrueInterest();

// Update `lastTotalAssets` to avoid an inconsistent state in a re-entrant context.
// It is updated again in `_deposit`.
lastTotalAssets = newTotalAssets;

shares = _convertToSharesWithTotals(assets, totalSupply(), newTotalAssets, Math.Rounding.Floor);
shares = _convertToShares(assets, Math.Rounding.Floor);

_deposit(_msgSender(), receiver, assets, shares);
}

/// @inheritdoc IERC4626
function mint(uint256 shares, address receiver) public override returns (uint256 assets) {
uint256 newTotalAssets = _accrueFee();

// Update `lastTotalAssets` to avoid an inconsistent state in a re-entrant context.
// It is updated again in `_deposit`.
lastTotalAssets = newTotalAssets;
_accrueInterest();

assets = _convertToAssetsWithTotals(shares, totalSupply(), newTotalAssets, Math.Rounding.Ceil);
assets = _convertToAssets(shares, Math.Rounding.Ceil);

_deposit(_msgSender(), receiver, assets, shares);
}

/// @inheritdoc IERC4626
function withdraw(uint256 assets, address receiver, address owner) public override returns (uint256 shares) {
uint256 newTotalAssets = _accrueFee();
_accrueInterest();

// Do not call expensive `maxWithdraw` and optimistically withdraw assets.

shares = _convertToSharesWithTotals(assets, totalSupply(), newTotalAssets, Math.Rounding.Ceil);

// `newTotalAssets - assets` may be a little off from `totalAssets()`.
_updateLastTotalAssets(newTotalAssets.zeroFloorSub(assets));
shares = _convertToShares(assets, Math.Rounding.Ceil);

_withdraw(_msgSender(), receiver, owner, assets, shares);
}

/// @inheritdoc IERC4626
function redeem(uint256 shares, address receiver, address owner) public override returns (uint256 assets) {
uint256 newTotalAssets = _accrueFee();
_accrueInterest();

// Do not call expensive `maxRedeem` and optimistically redeem shares.

assets = _convertToAssetsWithTotals(shares, totalSupply(), newTotalAssets, Math.Rounding.Floor);

// `newTotalAssets - assets` may be a little off from `totalAssets()`.
_updateLastTotalAssets(newTotalAssets.zeroFloorSub(assets));
assets = _convertToAssets(shares, Math.Rounding.Floor);

_withdraw(_msgSender(), receiver, owner, assets, shares);
}

/// @inheritdoc IERC4626
function totalAssets() public view override returns (uint256 assets) {
assets += hole;
for (uint256 i; i < withdrawQueue.length; ++i) {
assets += MORPHO.expectedSupplyAssets(_marketParams(withdrawQueue[i]), address(this));
}
if (assets < lastTotalAssets) {
assets = lastTotalAssets;
}
}

/* ERC4626 (INTERNAL) */
Expand All @@ -603,7 +596,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
returns (uint256 assets, uint256 newTotalSupply, uint256 newTotalAssets)
{
uint256 feeShares;
(feeShares, newTotalAssets) = _accruedFeeShares();
(feeShares, newTotalAssets,) = _accruedFeeShares();
newTotalSupply = totalSupply() + feeShares;

assets = _convertToAssetsWithTotals(balanceOf(owner), newTotalSupply, newTotalAssets, Math.Rounding.Floor);
Expand All @@ -630,15 +623,15 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
/// @inheritdoc ERC4626
/// @dev The accrual of performance fees is taken into account in the conversion.
function _convertToShares(uint256 assets, Math.Rounding rounding) internal view override returns (uint256) {
(uint256 feeShares, uint256 newTotalAssets) = _accruedFeeShares();
(uint256 feeShares, uint256 newTotalAssets,) = _accruedFeeShares();

return _convertToSharesWithTotals(assets, totalSupply() + feeShares, newTotalAssets, rounding);
}

/// @inheritdoc ERC4626
/// @dev The accrual of performance fees is taken into account in the conversion.
function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view override returns (uint256) {
(uint256 feeShares, uint256 newTotalAssets) = _accruedFeeShares();
(uint256 feeShares, uint256 newTotalAssets,) = _accruedFeeShares();

return _convertToAssetsWithTotals(shares, totalSupply() + feeShares, newTotalAssets, rounding);
}
Expand Down Expand Up @@ -686,6 +679,9 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
internal
override
{
// `lastTotalAssets - assets` may be a little off from `totalAssets()`.
_updateLastTotalAssets(lastTotalAssets - assets);

_withdrawMorpho(assets);

super._withdraw(caller, receiver, owner, assets, shares);
Expand Down Expand Up @@ -879,10 +875,12 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
}

/// @dev Accrues the fee and mints the fee shares to the fee recipient.
/// @return newTotalAssets The vaults total assets after accruing the interest.
function _accrueFee() internal returns (uint256 newTotalAssets) {
uint256 feeShares;
(feeShares, newTotalAssets) = _accruedFeeShares();
function _accrueInterest() internal {
(uint256 feeShares, uint256 newTotalAssets, uint256 newHole) = _accruedFeeShares();

_updateLastTotalAssets(newTotalAssets);
hole = newHole;
emit EventsLib.UpdateHole(newHole);

if (feeShares != 0) _mint(feeRecipient, feeShares);

Expand All @@ -891,8 +889,19 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph

/// @dev Computes and returns the fee shares (`feeShares`) to mint and the new vault's total assets
/// (`newTotalAssets`).
function _accruedFeeShares() internal view returns (uint256 feeShares, uint256 newTotalAssets) {
newTotalAssets = totalAssets();
function _accruedFeeShares() internal view returns (uint256 feeShares, uint256 newTotalAssets, uint256 newHole) {
uint256 realTotalAssets;
for (uint256 i; i < withdrawQueue.length; ++i) {
realTotalAssets += MORPHO.expectedSupplyAssets(_marketParams(withdrawQueue[i]), address(this));
}

if (realTotalAssets + hole > lastTotalAssets) {
newTotalAssets = realTotalAssets + hole;
} else {
// Handle the case where the vault lost some assets.
newHole = hole + lastTotalAssets - realTotalAssets;
newTotalAssets = lastTotalAssets;
}

uint256 totalInterest = newTotalAssets.zeroFloorSub(lastTotalAssets);
if (totalInterest != 0 && fee != 0) {
Expand Down
3 changes: 3 additions & 0 deletions src/interfaces/IMetaMorpho.sol
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ interface IMetaMorphoBase {
/// triggered (deposit/mint/withdraw/redeem/setFee/setFeeRecipient).
function lastTotalAssets() external view returns (uint256);

/// @notice Stores the missing assets due to bad debt.
function hole() external view returns (uint256);

/// @notice Submits a `newTimelock`.
/// @dev Warning: Reverts if a timelock is already pending. Revoke the pending timelock to overwrite it.
/// @dev In case the new timelock is higher than the current one, the timelock is set immediately.
Expand Down
6 changes: 6 additions & 0 deletions src/libraries/EventsLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ library EventsLib {
/// @notice Emitted when the vault's last total assets is updated to `updatedTotalAssets`.
event UpdateLastTotalAssets(uint256 updatedTotalAssets);

/// @notice Emitted when the vault's last real total assets is updated to `updatedRealTotalAssets`.
event UpdateLastRealTotalAssets(uint256 updatedRealTotalAssets);

/// @notice Emitted when the vault's hole is updated to `updatedHole`.
event UpdateHole(uint256 updatedHole);

/// @notice Emitted when the market identified by `id` is submitted for removal.
event SubmitMarketRemoval(address indexed caller, Id indexed id);

Expand Down
4 changes: 2 additions & 2 deletions test/forge/ERC4626Test.sol
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ contract ERC4626Test is IntegrationTest, IMorphoFlashLoanCallback {
assets = bound(assets, deposited + 1, type(uint256).max / (deposited + 1));

vm.prank(ONBEHALF);
vm.expectRevert(ErrorsLib.NotEnoughLiquidity.selector);
vm.expectRevert(stdError.arithmeticError);
vault.withdraw(assets, RECEIVER, ONBEHALF);
}

Expand All @@ -301,7 +301,7 @@ contract ERC4626Test is IntegrationTest, IMorphoFlashLoanCallback {
morpho.borrow(allMarkets[0], 1, 0, BORROWER, BORROWER);

vm.startPrank(ONBEHALF);
vm.expectRevert(ErrorsLib.NotEnoughLiquidity.selector);
vm.expectRevert(stdError.arithmeticError);
vault.withdraw(assets, RECEIVER, ONBEHALF);
}

Expand Down
115 changes: 115 additions & 0 deletions test/forge/HoleTest.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import "./helpers/IntegrationTest.sol";

contract ModifiedMorpho {
address public owner;
address public feeRecipient;
mapping(bytes32 => mapping(address => Position)) public position;
mapping(bytes32 => Market) public market;

function writeTotalSupplyAssets(bytes32 id, uint128 newValue) external {
market[id].totalSupplyAssets = newValue;
}
}

contract SharePriceTest is IntegrationTest {
using stdStorage for StdStorage;
using MorphoBalancesLib for IMorpho;
using MarketParamsLib for MarketParams;

bytes modifiedCode = _makeModifiedCode();
bytes normalCode = address(morpho).code;

function _makeModifiedCode() internal returns (bytes memory) {
return address(new ModifiedMorpho()).code;
}

function _writeTotalSupplyAssets(bytes32 id, uint128 newValue) internal {
vm.etch(address(morpho), modifiedCode);
ModifiedMorpho(address(morpho)).writeTotalSupplyAssets(id, newValue);
vm.etch(address(morpho), normalCode);
}

function setUp() public override {
super.setUp();

_setCap(allMarkets[0], CAP);
_sortSupplyQueueIdleLast();
}

function test_totalAssetsCannotDecrease(uint256 assets, uint128 totalSupplyAssets) public {
assets = bound(assets, MIN_TEST_ASSETS, MAX_TEST_ASSETS);

loanToken.setBalance(SUPPLIER, assets);

vm.prank(SUPPLIER);
vault.deposit(assets, ONBEHALF);

uint256 totalAssetsBefore = vault.totalAssets();
_writeTotalSupplyAssets(Id.unwrap(allMarkets[0].id()), totalSupplyAssets);
uint256 totalAssetsAfter = vault.totalAssets();

assertGe(totalAssetsAfter, totalAssetsBefore, "totalAssets decreased");
}

function invariant_totalAssetsCannotDecrease() public {
uint256 totalAssetsBefore = vault.totalAssets();
_writeTotalSupplyAssets(Id.unwrap(allMarkets[0].id()), 0);
uint256 totalAssetsAfter = vault.totalAssets();

assertGe(totalAssetsAfter, totalAssetsBefore, "totalAssets decreased");
}

function test_HoleValue() public {
loanToken.setBalance(SUPPLIER, 1 ether);

vm.prank(SUPPLIER);
vault.deposit(1 ether, ONBEHALF);

_writeTotalSupplyAssets(Id.unwrap(allMarkets[0].id()), 0.5 ether);

vault.deposit(0, ONBEHALF); // update hole.

assertEq(vault.hole(), 0.5 ether, "totalAssets decreased");
}

function test_HoleValue(uint256 assets, uint128 expectedHole) public {
assets = bound(assets, MIN_TEST_ASSETS, MAX_TEST_ASSETS);

loanToken.setBalance(SUPPLIER, assets);

vm.prank(SUPPLIER);
vault.deposit(assets, ONBEHALF);

uint128 totalSupplyAssetsBefore = morpho.market(allMarkets[0].id()).totalSupplyAssets;
expectedHole = uint128(bound(expectedHole, 0, totalSupplyAssetsBefore));

_writeTotalSupplyAssets(Id.unwrap(allMarkets[0].id()), totalSupplyAssetsBefore - expectedHole);

vault.deposit(0, ONBEHALF); // update hole.

assertEq(vault.hole(), expectedHole, "totalAssets decreased");
}

function test_HoleEvent(uint256 assets, uint128 expectedHole) public {
assets = bound(assets, MIN_TEST_ASSETS, MAX_TEST_ASSETS);

loanToken.setBalance(SUPPLIER, assets);

vm.prank(SUPPLIER);
vault.deposit(assets, ONBEHALF);

uint128 totalSupplyAssetsBefore = morpho.market(allMarkets[0].id()).totalSupplyAssets;
expectedHole = uint128(bound(expectedHole, 0, totalSupplyAssetsBefore));

_writeTotalSupplyAssets(Id.unwrap(allMarkets[0].id()), totalSupplyAssetsBefore - expectedHole);

vm.expectEmit();
emit EventsLib.UpdateHole(expectedHole);
vault.deposit(0, ONBEHALF); // update hole.

assertEq(vault.hole(), expectedHole, "totalAssets decreased");
}
}

0 comments on commit a0934f8

Please sign in to comment.