Skip to content

Commit

Permalink
Merge branch 'test/permit' of github.com:morpho-org/metamorpho into f…
Browse files Browse the repository at this point in the history
…eat/remove-idle
  • Loading branch information
Rubilmax committed Nov 10, 2023
2 parents 5ac5bb7 + f6a5ff5 commit a997930
Show file tree
Hide file tree
Showing 15 changed files with 638 additions and 337 deletions.
21 changes: 12 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Only one address can have this role.
It can:

- Do what the curator can do.
- Do what the guardian can do.
- Transfer or renounce the ownership.
- Set the curator.
- Set allocators.
Expand All @@ -56,9 +57,9 @@ Only one address can have this role.

It can:

- Do what the allocators can do.
- [Timelocked] Enable or disable a market by setting a cap to a specific market.
- The cap must be set to 0 to disable the market.
- Do what allocators can do.
- [Timelocked] Enable or disable a market by setting a supply cap to a specific market.
- The supply cap must be set to 0 to disable the market.
- Disabling a market can then only be done if the vault has no liquidity supplied on the market.

#### Allocator
Expand All @@ -68,11 +69,11 @@ Multiple addresses can have this role.
It can:

- Set the `supplyQueue` and `withdrawQueue`, i.e. decide on the order of the markets to supply/withdraw from.
- Upon a deposit, the vault will supply up to the cap of each Morpho Blue market in the supply queue in the order set.
- Upon a withdrawal, the vault will withdraw up to the liquidity of each Morpho Blue market in the withdrawal queue in the order set.
- The `supplyQueue` contains only enabled markets (enabled market are markets with non-zero cap or with non-zero vault's supply).
- The `withdrawQueue` contains all enabled markets.
- Instantaneously reallocate funds among the enabled market at any moment.
- Upon a deposit, the vault will supply up to the cap of each Morpho Blue market in the `supplyQueue` in the order set.
- Upon a withdrawal, the vault will first withdraw from the idle supply and then withdraw up to the liquidity of each Morpho Blue market in the `withdrawalQueue` in the order set.
- The `supplyQueue` only contains markets which cap has previously been non-zero.
- The `withdrawQueue` contains all markets that have a non-zero cap or a non-zero vault allocation.
- Instantaneously reallocate funds by supplying on markets of the `withdrawQueue` and withdrawing from markets that have the same loan asset as the vault's asset.

> **Warning**
> If `supplyQueue` is empty, depositing to the vault is disabled.
Expand All @@ -83,7 +84,9 @@ Only one address can have this role.

It can:

- Revoke any timelocked action except it cannot revoke a pending fee.
- Revoke the pending timelock.
- Revoke the pending guardian (which means it can revoke any attempt to change the guardian).
- Revoke the pending cap of any market.

### Idle Supply

Expand Down
159 changes: 79 additions & 80 deletions src/MetaMorpho.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@ pragma solidity 0.8.21;

import {IMorphoMarketParams} from "./interfaces/IMorphoMarketParams.sol";
import {
IMetaMorpho, MarketConfig, PendingUint192, PendingAddress, MarketAllocation
MarketConfig,
PendingUint192,
PendingAddress,
MarketAllocation,
IMetaMorphoStaticTyping
} from "./interfaces/IMetaMorpho.sol";
import {Id, MarketParams, Market, IMorpho} from "@morpho-blue/interfaces/IMorpho.sol";

import {PendingUint192, PendingAddress, PendingLib} from "./libraries/PendingLib.sol";
import {ConstantsLib} from "./libraries/ConstantsLib.sol";
import {ErrorsLib} from "./libraries/ErrorsLib.sol";
import {EventsLib} from "./libraries/EventsLib.sol";
Expand All @@ -28,7 +33,7 @@ import {IERC20, IERC4626, ERC20, ERC4626, Math, SafeERC20} from "@openzeppelin/t
/// @author Morpho Labs
/// @custom:contact security@morpho.org
/// @notice ERC4626 compliant vault allowing users to deposit assets to Morpho.
contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorpho {
contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorphoStaticTyping {
using Math for uint256;
using UtilsLib for uint256;
using SafeCast for uint256;
Expand All @@ -37,6 +42,8 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
using SharesMathLib for uint256;
using MorphoBalancesLib for IMorpho;
using MarketParamsLib for MarketParams;
using PendingLib for PendingUint192;
using PendingLib for PendingAddress;

/* IMMUTABLES */

Expand All @@ -60,15 +67,6 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
/// @notice The current timelock.
uint256 public timelock;

/// @notice The current fee.
uint96 public fee;

/// @notice The fee recipient.
address public feeRecipient;

/// @notice The skim recipient.
address public skimRecipient;

/// @notice The pending guardian.
PendingAddress public pendingGuardian;

Expand All @@ -78,8 +76,14 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
/// @notice The pending timelock.
PendingUint192 public pendingTimelock;

/// @notice The pending fee.
PendingUint192 public pendingFee;
/// @notice The current fee.
uint96 public fee;

/// @notice The fee recipient.
address public feeRecipient;

/// @notice The skim recipient.
address public skimRecipient;

/// @dev Stores the order of markets on which liquidity is supplied upon deposit.
/// @dev Can contain any market. A market is skipped as soon as its supply cap is reached.
Expand Down Expand Up @@ -140,9 +144,18 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
_;
}

/// @dev Reverts if the caller is not the `guardian`.
modifier onlyGuardian() {
if (_msgSender() != guardian) revert ErrorsLib.NotGuardian();
/// @dev Reverts if the caller doesn't have the guardian role.
modifier onlyGuardianRole() {
if (_msgSender() != owner() && _msgSender() != guardian) revert ErrorsLib.NotGuardianRole();

_;
}

/// @dev Reverts if the caller doesn't have the curator nor the guardian role.
modifier onlyCuratorOrGuardianRole() {
if (_msgSender() != guardian && _msgSender() != curator && _msgSender() != owner()) {
revert ErrorsLib.NotCuratorNorGuardianRole();
}

_;
}
Expand All @@ -151,9 +164,9 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
/// @dev Reverts if:
/// - there's no pending value;
/// - the timelock has not elapsed since the pending value has been submitted.
modifier afterTimelock(uint256 submittedAt) {
if (submittedAt == 0) revert ErrorsLib.NoPendingValue();
if (block.timestamp < submittedAt + timelock) revert ErrorsLib.TimelockNotElapsed();
modifier afterTimelock(uint256 validAt) {
if (validAt == 0) revert ErrorsLib.NoPendingValue();
if (block.timestamp < validAt) revert ErrorsLib.TimelockNotElapsed();

_;
}
Expand Down Expand Up @@ -197,28 +210,29 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
if (newTimelock > timelock) {
_setTimelock(newTimelock);
} else {
// newTimelock >= MIN_TIMELOCK > 0 so there's no need to check `pendingTimelock.validAt != 0`.
if (newTimelock == pendingTimelock.value) revert ErrorsLib.AlreadyPending();

// Safe "unchecked" cast because newTimelock <= MAX_TIMELOCK.
pendingTimelock = PendingUint192(uint192(newTimelock), uint64(block.timestamp));
pendingTimelock.update(uint192(newTimelock), timelock);

emit EventsLib.SubmitTimelock(newTimelock);
}
}

/// @notice Submits a `newFee`.
/// @dev In case the new fee is lower than the current one, the fee is set immediately.
/// @dev Warning: Submitting a fee will overwrite the current pending fee.
function submitFee(uint256 newFee) external onlyOwner {
/// @notice Sets the `fee` to `newFee`.
function setFee(uint256 newFee) external onlyOwner {
if (newFee == fee) revert ErrorsLib.AlreadySet();
if (newFee > ConstantsLib.MAX_FEE) revert ErrorsLib.MaxFeeExceeded();
if (newFee != 0 && feeRecipient == address(0)) revert ErrorsLib.ZeroFeeRecipient();

if (newFee < fee) {
_setFee(newFee);
} else {
// Safe "unchecked" cast because newFee <= MAX_FEE.
pendingFee = PendingUint192(uint192(newFee), uint64(block.timestamp));
// Accrue interest using the previous fee set before changing it.
_updateLastTotalAssets(_accrueFee());

emit EventsLib.SubmitFee(newFee);
}
// Safe "unchecked" cast because newFee <= MAX_FEE.
fee = uint96(newFee);

emit EventsLib.SetFee(_msgSender(), fee);
}

/// @notice Sets `feeRecipient` to `newFeeRecipient`.
Expand All @@ -245,7 +259,11 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
if (guardian == address(0)) {
_setGuardian(newGuardian);
} else {
pendingGuardian = PendingAddress(newGuardian, uint64(block.timestamp));
if (pendingGuardian.validAt != 0 && newGuardian == pendingGuardian.value) {
revert ErrorsLib.AlreadyPending();
}

pendingGuardian.update(newGuardian, timelock);

emit EventsLib.SubmitGuardian(newGuardian);
}
Expand All @@ -267,7 +285,10 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
if (newSupplyCap < supplyCap) {
_setCap(id, newSupplyCap.toUint192());
} else {
pendingCap[id] = PendingUint192(newSupplyCap.toUint192(), uint64(block.timestamp));
// newSupplyCap > supplyCap >= 0 so there's no need to check `pendingCap[id].validAt != 0`.
if (newSupplyCap == pendingCap[id].value) revert ErrorsLib.AlreadyPending();

pendingCap[id].update(newSupplyCap.toUint192(), timelock);

emit EventsLib.SubmitCap(_msgSender(), id, newSupplyCap);
}
Expand Down Expand Up @@ -393,24 +414,30 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
if (totalWithdrawn != totalSupplied) revert ErrorsLib.InconsistentReallocation();
}

/* ONLY GUARDIAN FUNCTIONS */
/* REVOKE FUNCTIONS */

/// @notice Revokes the pending timelock.
function revokePendingTimelock() external onlyGuardianRole {
if (pendingTimelock.validAt == 0) revert ErrorsLib.NoPendingValue();

/// @notice Revokes the `pendingTimelock`.
function revokePendingTimelock() external onlyGuardian {
delete pendingTimelock;

emit EventsLib.RevokePendingTimelock(_msgSender());
}

/// @notice Revokes the `pendingGuardian`.
function revokePendingGuardian() external onlyGuardian {
/// @notice Revokes the pending guardian.
function revokePendingGuardian() external onlyGuardianRole {
if (pendingGuardian.validAt == 0) revert ErrorsLib.NoPendingValue();

delete pendingGuardian;

emit EventsLib.RevokePendingGuardian(_msgSender());
}

/// @notice Revokes the pending cap of the market defined by `id`.
function revokePendingCap(Id id) external onlyGuardian {
function revokePendingCap(Id id) external onlyCuratorOrGuardianRole {
if (pendingCap[id].validAt == 0) revert ErrorsLib.NoPendingValue();

delete pendingCap[id];

emit EventsLib.RevokePendingCap(_msgSender(), id);
Expand All @@ -428,23 +455,18 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
return withdrawQueue.length;
}

/// @notice Accepts the `pendingTimelock`.
function acceptTimelock() external afterTimelock(pendingTimelock.submittedAt) {
/// @notice Accepts the pending timelock.
function acceptTimelock() external afterTimelock(pendingTimelock.validAt) {
_setTimelock(pendingTimelock.value);
}

/// @notice Accepts the `pendingFee`.
function acceptFee() external afterTimelock(pendingFee.submittedAt) {
_setFee(pendingFee.value);
}

/// @notice Accepts the `pendingGuardian`.
function acceptGuardian() external afterTimelock(pendingGuardian.submittedAt) {
/// @notice Accepts the pending guardian.
function acceptGuardian() external afterTimelock(pendingGuardian.validAt) {
_setGuardian(pendingGuardian.value);
}

/// @notice Accepts the pending cap of the market defined by `id`.
function acceptCap(Id id) external afterTimelock(pendingCap[id].submittedAt) {
function acceptCap(Id id) external afterTimelock(pendingCap[id].validAt) {
_setCap(id, pendingCap[id].value);
}

Expand All @@ -462,7 +484,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
/* ERC4626 (PUBLIC) */

/// @inheritdoc IERC20Metadata
function decimals() public view override(IERC20Metadata, ERC20, ERC4626) returns (uint8) {
function decimals() public view override(ERC20, ERC4626) returns (uint8) {
return ERC4626.decimals();
}

Expand All @@ -481,41 +503,37 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
/// @inheritdoc IERC4626
/// @dev Warning: May be lower than the actual amount of assets that can be withdrawn by `owner` due to conversion
/// roundings between shares and assets.
function maxWithdraw(address owner) public view override(IERC4626, ERC4626) returns (uint256 assets) {
function maxWithdraw(address owner) public view override returns (uint256 assets) {
(assets,,) = _maxWithdraw(owner);
}

/// @inheritdoc IERC4626
/// @dev Warning: May be lower than the actual amount of shares that can be redeemed by `owner` due to conversion
/// roundings between shares and assets.
function maxRedeem(address owner) public view override(IERC4626, ERC4626) returns (uint256) {
function maxRedeem(address owner) public view override returns (uint256) {
(uint256 assets, uint256 newTotalSupply, uint256 newTotalAssets) = _maxWithdraw(owner);

return _convertToSharesWithTotals(assets, newTotalSupply, newTotalAssets, Math.Rounding.Floor);
}

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

shares = _convertToSharesWithTotals(assets, totalSupply(), newTotalAssets, Math.Rounding.Floor);
_deposit(_msgSender(), receiver, assets, shares);
}

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

assets = _convertToAssetsWithTotals(shares, totalSupply(), newTotalAssets, Math.Rounding.Ceil);
_deposit(_msgSender(), receiver, assets, shares);
}

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

// Do not call expensive `maxWithdraw` and optimistically withdraw assets.
Expand All @@ -525,11 +543,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
}

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

// Do not call expensive `maxRedeem` and optimistically redeem shares.
Expand All @@ -539,7 +553,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
}

/// @inheritdoc IERC4626
function totalAssets() public view override(IERC4626, ERC4626) returns (uint256 assets) {
function totalAssets() public view override returns (uint256 assets) {
for (uint256 i; i < withdrawQueue.length; ++i) {
assets += _supplyBalance(_marketParams(withdrawQueue[i]));
}
Expand Down Expand Up @@ -706,21 +720,6 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
delete pendingCap[id];
}

/// @dev Sets `fee` to `newFee`.
function _setFee(uint256 newFee) internal {
if (newFee != 0 && feeRecipient == address(0)) revert ErrorsLib.ZeroFeeRecipient();

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

// Safe "unchecked" cast because newFee <= MAX_FEE.
fee = uint96(newFee);

emit EventsLib.SetFee(_msgSender(), newFee);

delete pendingFee;
}

/* LIQUIDITY ALLOCATION */

/// @dev Supplies `assets` to Morpho.
Expand Down
Loading

0 comments on commit a997930

Please sign in to comment.