Skip to content

Commit

Permalink
Merge branch 'refactor/withdraw-deposit' of github.com:morpho-labs/mo…
Browse files Browse the repository at this point in the history
…rpho-blue-metamorpho into refactor/withdraw-deposit
  • Loading branch information
MerlinEgalite committed Sep 22, 2023
2 parents 2f59f62 + 56152f3 commit 1386856
Show file tree
Hide file tree
Showing 16 changed files with 944 additions and 221 deletions.
7 changes: 0 additions & 7 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,6 @@ fs_permissions = [
{ access = "read", path = "./lib/morpho-blue/out/"}
]

[profile.default.fuzz]
runs = 32

[profile.default.invariant]
runs = 16
depth = 4

[profile.default.rpc_endpoints]
mainnet = "https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}"
tenderly = "https://rpc.tenderly.co/fork/${TENDERLY_FORK_ID}"
Expand Down
153 changes: 109 additions & 44 deletions src/MetaMorpho.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
pragma solidity 0.8.21;

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

import "src/libraries/ConstantsLib.sol";
Expand All @@ -16,6 +18,7 @@ import {MorphoBalancesLib} from "@morpho-blue/libraries/periphery/MorphoBalances
import {MarketParamsLib} from "@morpho-blue/libraries/MarketParamsLib.sol";
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";

import {Multicall} from "@openzeppelin/contracts/utils/Multicall.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {
IERC20,
Expand All @@ -25,8 +28,9 @@ import {
Math,
SafeERC20
} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import {IERC20Metadata, ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";

contract MetaMorpho is ERC4626, Ownable2Step, IMetaMorpho {
contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorpho {
using Math for uint256;
using UtilsLib for uint256;
using SafeCast for uint256;
Expand All @@ -41,10 +45,11 @@ contract MetaMorpho is ERC4626, Ownable2Step, IMetaMorpho {

/* STORAGE */

mapping(address => uint256) internal _roleOf;
address public riskManager;
mapping(address => bool) internal _isAllocator;

mapping(Id => MarketConfig) public config;
mapping(Id => Pending) public pendingCap;
mapping(Id => PendingUint192) public pendingCap;

/// @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 All @@ -55,13 +60,15 @@ contract MetaMorpho is ERC4626, Ownable2Step, IMetaMorpho {
/// duplicate.
Id[] public withdrawQueue;

Pending public pendingFee;
Pending public pendingTimelock;
PendingUint192 public pendingFee;
PendingUint192 public pendingTimelock;
PendingAddress public pendingGuardian;

uint96 public fee;
address public feeRecipient;

uint256 public timelock;
uint96 public timelock;
address public guardian;

/// @dev Stores the total assets owned by this vault when the fee was last accrued.
uint256 public lastTotalAssets;
Expand All @@ -71,6 +78,7 @@ contract MetaMorpho is ERC4626, Ownable2Step, IMetaMorpho {

constructor(address morpho, uint256 initialTimelock, address _asset, string memory _name, string memory _symbol)
ERC4626(IERC20(_asset))
ERC20Permit(_name)
ERC20(_name, _symbol)
{
require(initialTimelock <= MAX_TIMELOCK, ErrorsLib.MAX_TIMELOCK_EXCEEDED);
Expand All @@ -85,7 +93,13 @@ contract MetaMorpho is ERC4626, Ownable2Step, IMetaMorpho {
/* MODIFIERS */

modifier onlyRiskManager() {
require(isRiskManager(_msgSender()), ErrorsLib.NOT_RISK_MANAGER);
require(_msgSender() == riskManager || _msgSender() == owner(), ErrorsLib.NOT_RISK_MANAGER);

_;
}

modifier onlyGuardian() {
require(_msgSender() == guardian, ErrorsLib.NOT_GUARDIAN);

_;
}
Expand All @@ -96,7 +110,8 @@ contract MetaMorpho is ERC4626, Ownable2Step, IMetaMorpho {
_;
}

modifier timelockElapsed(uint64 submittedAt) {
modifier timelockElapsed(uint256 submittedAt) {
require(submittedAt != 0, ErrorsLib.NO_PENDING_VALUE);
require(block.timestamp >= submittedAt + timelock, ErrorsLib.TIMELOCK_NOT_ELAPSED);
require(block.timestamp <= submittedAt + timelock + TIMELOCK_EXPIRATION, ErrorsLib.TIMELOCK_EXPIRATION_EXCEEDED);

Expand All @@ -105,43 +120,45 @@ contract MetaMorpho is ERC4626, Ownable2Step, IMetaMorpho {

/* ONLY OWNER FUNCTIONS */

function setIsRiskManager(address newRiskManager, bool newIsRiskManager) external onlyOwner {
_setRole(newRiskManager, RISK_MANAGER_ROLE, newIsRiskManager);
function setRiskManager(address newRiskManager) external onlyOwner {
riskManager = newRiskManager;

emit EventsLib.SetRiskManager(newRiskManager);
}

function setIsAllocator(address newAllocator, bool newIsAllocator) external onlyOwner {
_setRole(newAllocator, ALLOCATOR_ROLE, newIsAllocator);
_isAllocator[newAllocator] = newIsAllocator;

emit EventsLib.SetIsAllocator(newAllocator, newIsAllocator);
}

function submitTimelock(uint256 newTimelock) external onlyOwner {
require(newTimelock != timelock, ErrorsLib.ALREADY_SET);
require(newTimelock <= MAX_TIMELOCK, ErrorsLib.MAX_TIMELOCK_EXCEEDED);
require(newTimelock != timelock, ErrorsLib.ALREADY_SET);

if (timelock == 0) {
if (newTimelock > timelock || timelock == 0) {
_setTimelock(newTimelock);
} else {
// Safe "unchecked" cast because newTimelock <= MAX_TIMELOCK.
pendingTimelock = Pending(uint192(newTimelock), uint64(block.timestamp));
pendingTimelock = PendingUint192(uint192(newTimelock), uint64(block.timestamp));

emit EventsLib.SubmitTimelock(newTimelock);
}
}

function acceptTimelock() external timelockElapsed(pendingTimelock.submittedAt) onlyOwner {
_setTimelock(pendingTimelock.value);

delete pendingTimelock;
}

function submitFee(uint256 newFee) external onlyOwner {
require(newFee != fee, ErrorsLib.ALREADY_SET);
require(newFee <= WAD, ErrorsLib.MAX_FEE_EXCEEDED);
require(newFee != fee, ErrorsLib.ALREADY_SET);

if (newFee == 0 || timelock == 0) {
if (newFee < fee || timelock == 0) {
_setFee(newFee);
} else {
// Safe "unchecked" cast because newFee <= WAD.
pendingFee = Pending(uint192(newFee), uint64(block.timestamp));
pendingFee = PendingUint192(uint192(newFee), uint64(block.timestamp));

emit EventsLib.SubmitFee(newFee);
}
Expand All @@ -156,6 +173,7 @@ contract MetaMorpho is ERC4626, Ownable2Step, IMetaMorpho {

function setFeeRecipient(address newFeeRecipient) external onlyOwner {
require(newFeeRecipient != feeRecipient, ErrorsLib.ALREADY_SET);
require(newFeeRecipient != address(0) || fee == 0, ErrorsLib.ZERO_FEE_RECIPIENT);

// Accrue interest to the previous fee recipient set before changing it.
_updateLastTotalAssets(_accrueFee());
Expand All @@ -165,27 +183,45 @@ contract MetaMorpho is ERC4626, Ownable2Step, IMetaMorpho {
emit EventsLib.SetFeeRecipient(newFeeRecipient);
}

function submitGuardian(address newGuardian) external onlyOwner {
require(timelock != 0, ErrorsLib.NO_TIMELOCK);
require(newGuardian != guardian, ErrorsLib.ALREADY_SET);

if (guardian == address(0)) {
_setGuardian(newGuardian);
} else {
pendingGuardian = PendingAddress(newGuardian, uint64(block.timestamp));

emit EventsLib.SubmitGuardian(newGuardian);
}
}

function acceptGuardian() external timelockElapsed(pendingGuardian.submittedAt) onlyOwner {
_setGuardian(pendingGuardian.value);
}

/* ONLY RISK MANAGER FUNCTIONS */

function submitCap(MarketParams memory marketParams, uint256 marketCap) external onlyRiskManager {
function submitCap(MarketParams memory marketParams, uint256 newMarketCap) external onlyRiskManager {
require(marketParams.borrowableToken == asset(), ErrorsLib.INCONSISTENT_ASSET);

Id id = marketParams.id();
require(MORPHO.lastUpdate(id) != 0, ErrorsLib.MARKET_NOT_CREATED);

if (marketCap == 0 || timelock == 0) {
_setCap(id, marketCap.toUint192());
uint256 marketCap = config[id].cap;
require(newMarketCap != marketCap, ErrorsLib.ALREADY_SET);

if (newMarketCap < marketCap || timelock == 0) {
_setCap(id, newMarketCap.toUint192());
} else {
pendingCap[id] = Pending(marketCap.toUint192(), uint64(block.timestamp));
pendingCap[id] = PendingUint192(newMarketCap.toUint192(), uint64(block.timestamp));

emit EventsLib.SubmitCap(id, marketCap);
emit EventsLib.SubmitCap(id, newMarketCap);
}
}

function acceptCap(Id id) external timelockElapsed(pendingCap[id].submittedAt) onlyRiskManager {
_setCap(id, pendingCap[id].value);

delete pendingCap[id];
}

/* ONLY ALLOCATOR FUNCTIONS */
Expand All @@ -200,6 +236,8 @@ contract MetaMorpho is ERC4626, Ownable2Step, IMetaMorpho {
}

supplyQueue = newSupplyQueue;

emit EventsLib.SetSupplyQueue(msg.sender, newSupplyQueue);
}

function sortWithdrawQueue(uint256[] calldata indexes) external onlyAllocator {
Expand Down Expand Up @@ -236,6 +274,8 @@ contract MetaMorpho is ERC4626, Ownable2Step, IMetaMorpho {
}

withdrawQueue = newWithdrawQueue;

emit EventsLib.SetWithdrawQueue(msg.sender, newWithdrawQueue);
}

function reallocate(MarketAllocation[] calldata withdrawn, MarketAllocation[] calldata supplied)
Expand All @@ -245,18 +285,44 @@ contract MetaMorpho is ERC4626, Ownable2Step, IMetaMorpho {
_reallocate(withdrawn, supplied);
}

/* PUBLIC */
/* ONLY GUARDIAN FUNCTIONS */

function isRiskManager(address target) public view returns (bool) {
return _hasRole(target, RISK_MANAGER_ROLE);
function revokeTimelock() external onlyGuardian {
emit EventsLib.RevokeTimelock(msg.sender, pendingTimelock.value, pendingTimelock.submittedAt);

delete pendingTimelock;
}

function revokeFee() external onlyGuardian {
emit EventsLib.RevokeFee(msg.sender, pendingFee.value, pendingFee.submittedAt);

delete pendingFee;
}

function revokeCap(Id id) external onlyGuardian {
emit EventsLib.RevokeCap(msg.sender, id, pendingCap[id].value, pendingCap[id].submittedAt);

delete pendingCap[id];
}

function revokeGuardian() external onlyGuardian {
emit EventsLib.RevokeGuardian(msg.sender, pendingGuardian.value, pendingGuardian.submittedAt);

delete pendingGuardian;
}

/* PUBLIC */

function isAllocator(address target) public view returns (bool) {
return _hasRole(target, ALLOCATOR_ROLE);
return _isAllocator[target] || target == riskManager || target == owner();
}

/* ERC4626 (PUBLIC) */

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

function maxWithdraw(address owner) public view override(IERC4626, ERC4626) returns (uint256 assets) {
(assets,) = _maxWithdraw(owner);
}
Expand Down Expand Up @@ -401,13 +467,21 @@ contract MetaMorpho is ERC4626, Ownable2Step, IMetaMorpho {

function _setTimelock(uint256 newTimelock) internal {
// Safe "unchecked" cast because newTimelock <= MAX_TIMELOCK.
timelock = newTimelock;
timelock = uint96(newTimelock);

emit EventsLib.SetTimelock(newTimelock);

delete pendingTimelock;
}

function _setGuardian(address newGuardian) internal {
guardian = newGuardian;

emit EventsLib.SetGuardian(newGuardian);

delete pendingGuardian;
}

function _setCap(Id id, uint192 marketCap) internal {
MarketConfig storage marketConfig = config[id];

Expand All @@ -429,6 +503,8 @@ contract MetaMorpho is ERC4626, Ownable2Step, IMetaMorpho {
}

function _setFee(uint256 newFee) internal {
require(newFee == 0 || feeRecipient != address(0), ErrorsLib.ZERO_FEE_RECIPIENT);

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

Expand Down Expand Up @@ -525,7 +601,7 @@ contract MetaMorpho is ERC4626, Ownable2Step, IMetaMorpho {
// 1. oracle.price() is never called (the vault doesn't borrow)
// 2. `_withdrawable` caps to the liquidity available on Morpho
// 3. virtually accruing interest didn't fail in `_withdrawable`
assets = assets.zeroFloorSub(_withdrawable(marketParams, id));
assets -= _withdrawable(marketParams, id);

if (assets == 0) return 0;
}
Expand Down Expand Up @@ -584,15 +660,4 @@ contract MetaMorpho is ERC4626, Ownable2Step, IMetaMorpho {
);
}
}

function _hasRole(address target, uint256 role) internal view returns (bool) {
return _roleOf[target] >= role || _msgSender() == owner();
}

function _setRole(address target, uint256 role, bool hasRole) internal {
if (hasRole) _roleOf[target] = role;
else delete _roleOf[target];

emit EventsLib.SetRole(target, role);
}
}
Loading

0 comments on commit 1386856

Please sign in to comment.