Skip to content

Commit

Permalink
Merge branch 'cantina-review' of github.com:morpho-org/metamorpho int…
Browse files Browse the repository at this point in the history
…o refactor/timelock-submittedAt
  • Loading branch information
Rubilmax committed Nov 7, 2023
2 parents b25b305 + 039bd2f commit ed465ae
Show file tree
Hide file tree
Showing 17 changed files with 265 additions and 168 deletions.
110 changes: 64 additions & 46 deletions src/MetaMorpho.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
using Math for uint256;
using UtilsLib for uint256;
using SafeCast for uint256;
using SafeERC20 for IERC20;
using MorphoLib for IMorpho;
using SharesMathLib for uint256;
using MorphoBalancesLib for IMorpho;
Expand Down Expand Up @@ -114,12 +115,14 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
string memory _name,
string memory _symbol
) ERC4626(IERC20(_asset)) ERC20Permit(_name) ERC20(_name, _symbol) Ownable(owner) {
if (morpho == address(0)) revert ErrorsLib.ZeroAddress();

MORPHO = IMorpho(morpho);

_checkTimelockBounds(initialTimelock);
_setTimelock(initialTimelock);

SafeERC20.safeIncreaseAllowance(IERC20(_asset), morpho, type(uint256).max);
IERC20(_asset).forceApprove(morpho, type(uint256).max);
}

/* MODIFIERS */
Expand Down Expand Up @@ -243,7 +246,8 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
}

/// @notice Submits a `newGuardian`.
/// @notice Warning: the guardian has the power to revoke any pending guardian.
/// @notice Warning: a malicious guardian could disrupt the vault's operation, and would have the power to revoke
/// any pending guardian.
/// @dev In case there is no guardian, the gardian is set immediately.
/// @dev Warning: Submitting a gardian will overwrite the current pending gardian.
function submitGuardian(address newGuardian) external onlyOwner {
Expand Down Expand Up @@ -276,19 +280,19 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
} else {
pendingCap[id].update(newSupplyCap.toUint192(), timelock);

emit EventsLib.SubmitCap(id, newSupplyCap);
emit EventsLib.SubmitCap(_msgSender(), id, newSupplyCap);
}
}

/* ONLY ALLOCATOR FUNCTIONS */

/// @notice Sets `supplyQueue` to `newSupplyQueue`.
/// @dev The supply queue can be a set containing duplicate markets, but it would only increase the cost of
/// depositing to the vault.
/// @param newSupplyQueue is an array of enabled markets, and can contain duplicate markets, but it would only
/// increase the cost of depositing to the vault.
function setSupplyQueue(Id[] calldata newSupplyQueue) external onlyAllocatorRole {
uint256 length = newSupplyQueue.length;

if (length > ConstantsLib.MAX_QUEUE_SIZE) revert ErrorsLib.MaxQueueSizeExceeded();
if (length > ConstantsLib.MAX_QUEUE_LENGTH) revert ErrorsLib.MaxQueueLengthExceeded();

for (uint256 i; i < length; ++i) {
if (config[newSupplyQueue[i]].cap == 0) revert ErrorsLib.UnauthorizedMarket(newSupplyQueue[i]);
Expand All @@ -299,13 +303,14 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
emit EventsLib.SetSupplyQueue(_msgSender(), newSupplyQueue);
}

/// @notice Sets the withdraw queue as a permutation of the previous one, although markets with zero cap and zero
/// vault's supply can be removed.
/// @notice Sets the withdraw queue as a permutation of the previous one, although markets with both zero cap and
/// zero vault's supply can be removed from the permutation.
/// @notice This is the only entry point to disable a market.
/// @notice Removing a market requires the vault to have 0 supply on it; but anyone can supply on behalf of the
/// vault so the call to `sortWithdrawQueue` can be griefed by a frontrun. To circumvent this, the allocator can
/// simply bundle a reallocation that withdraws max from this market with a call to `sortWithdrawQueue`.
/// @param indexes The indexes of each market in the previous withdraw queue, in the new withdraw queue's order.
function sortWithdrawQueue(uint256[] calldata indexes) external onlyAllocatorRole {
function updateWithdrawQueue(uint256[] calldata indexes) external onlyAllocatorRole {
uint256 newLength = indexes.length;
uint256 currLength = withdrawQueue.length;

Expand All @@ -331,7 +336,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
Id id = withdrawQueue[i];

if (MORPHO.supplyShares(id, address(this)) != 0 || config[id].cap != 0) {
revert ErrorsLib.MissingMarket(id);
revert ErrorsLib.InvalidMarketRemoval(id);
}

delete config[id].withdrawRank;
Expand All @@ -354,21 +359,20 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
uint256 totalWithdrawn;
for (uint256 i; i < withdrawn.length; ++i) {
MarketAllocation memory allocation = withdrawn[i];
Id id = allocation.marketParams.id();

if (allocation.marketParams.loanToken != asset()) {
revert ErrorsLib.InconsistentAsset(allocation.marketParams.id());
}
if (allocation.marketParams.loanToken != asset()) revert ErrorsLib.InconsistentAsset(id);

// Guarantees that unknown frontrunning donations can be withdrawn, in order to disable a market.
if (allocation.shares == type(uint256).max) {
allocation.shares = MORPHO.supplyShares(allocation.marketParams.id(), address(this));
}
if (allocation.shares == type(uint256).max) allocation.shares = MORPHO.supplyShares(id, address(this));

(uint256 withdrawnAssets,) = MORPHO.withdraw(
(uint256 withdrawnAssets, uint256 withdrawnShares) = MORPHO.withdraw(
allocation.marketParams, allocation.assets, allocation.shares, address(this), address(this)
);

totalWithdrawn += withdrawnAssets;

emit EventsLib.ReallocateWithdraw(_msgSender(), id, withdrawnAssets, withdrawnShares);
}

uint256 totalSupplied;
Expand All @@ -379,58 +383,65 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph

if (supplyCap == 0) revert ErrorsLib.UnauthorizedMarket(id);

(uint256 suppliedAssets,) =
(uint256 suppliedAssets, uint256 suppliedShares) =
MORPHO.supply(allocation.marketParams, allocation.assets, allocation.shares, address(this), hex"");

if (_supplyBalance(allocation.marketParams) > supplyCap) {
revert ErrorsLib.SupplyCapExceeded(id);
}

totalSupplied += suppliedAssets;

emit EventsLib.ReallocateSupply(_msgSender(), id, suppliedAssets, suppliedShares);
}

uint256 newIdle;
if (totalWithdrawn > totalSupplied) {
idle += totalWithdrawn - totalSupplied;
newIdle = idle + totalWithdrawn - totalSupplied;
} else {
uint256 idleSupplied = totalSupplied - totalWithdrawn;
if (idle < idleSupplied) revert ErrorsLib.InsufficientIdle();

idle -= idleSupplied;
newIdle = idle - idleSupplied;
}

idle = newIdle;

emit EventsLib.ReallocateIdle(_msgSender(), newIdle);
}

/* ONLY GUARDIAN FUNCTIONS */

/// @notice Revokes the `pendingTimelock`.
function revokeTimelock() external onlyGuardian {
emit EventsLib.RevokeTimelock(_msgSender(), pendingTimelock);

function revokePendingTimelock() external onlyGuardian {
delete pendingTimelock;

emit EventsLib.RevokePendingTimelock(_msgSender());
}

/// @notice Revokes the `pendingGuardian`.
function revokeGuardian() external onlyGuardian {
emit EventsLib.RevokeGuardian(_msgSender(), pendingGuardian);

function revokePendingGuardian() external onlyGuardian {
delete pendingGuardian;

emit EventsLib.RevokePendingGuardian(_msgSender());
}

/// @notice Revokes the pending cap of the market defined by `id`.
function revokeCap(Id id) external onlyGuardian {
emit EventsLib.RevokeCap(_msgSender(), id, pendingCap[id]);

function revokePendingCap(Id id) external onlyGuardian {
delete pendingCap[id];

emit EventsLib.RevokePendingCap(_msgSender(), id);
}

/* EXTERNAL */

/// @notice Returns the size of the supply queue.
function supplyQueueSize() external view returns (uint256) {
/// @notice Returns the length of the supply queue.
function supplyQueueLength() external view returns (uint256) {
return supplyQueue.length;
}

/// @notice Returns the size of the withdraw queue.
function withdrawQueueSize() external view returns (uint256) {
/// @notice Returns the length of the withdraw queue.
function withdrawQueueLength() external view returns (uint256) {
return withdrawQueue.length;
}

Expand Down Expand Up @@ -462,9 +473,9 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
uint256 amount = IERC20(token).balanceOf(address(this));
if (token == asset()) amount -= idle;

SafeERC20.safeTransfer(IERC20(token), rewardsRecipient, amount);
IERC20(token).safeTransfer(rewardsRecipient, amount);

emit EventsLib.TransferRewards(_msgSender(), rewardsRecipient, token, amount);
emit EventsLib.TransferRewards(_msgSender(), token, amount);
}

/* ERC4626 (PUBLIC) */
Expand All @@ -475,11 +486,15 @@ 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) {
(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) {
(uint256 assets, uint256 newTotalSupply, uint256 newTotalAssets) = _maxWithdraw(owner);

Expand Down Expand Up @@ -558,7 +573,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
newTotalSupply = totalSupply() + feeShares;

assets = _convertToAssetsWithTotals(balanceOf(owner), newTotalSupply, newTotalAssets, Math.Rounding.Floor);
assets -= _staticWithdrawMorpho(assets);
assets -= _simulateWithdrawMorpho(assets);
}

/// @inheritdoc ERC4626
Expand Down Expand Up @@ -651,7 +666,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
function _setTimelock(uint256 newTimelock) internal {
timelock = newTimelock;

emit EventsLib.SetTimelock(newTimelock);
emit EventsLib.SetTimelock(_msgSender(), newTimelock);

delete pendingTimelock;
}
Expand All @@ -660,7 +675,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
function _setGuardian(address newGuardian) internal {
guardian = newGuardian;

emit EventsLib.SetGuardian(newGuardian);
emit EventsLib.SetGuardian(_msgSender(), newGuardian);

delete pendingGuardian;
}
Expand All @@ -673,18 +688,20 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
supplyQueue.push(id);
withdrawQueue.push(id);

if (supplyQueue.length > ConstantsLib.MAX_QUEUE_SIZE || withdrawQueue.length > ConstantsLib.MAX_QUEUE_SIZE)
{
revert ErrorsLib.MaxQueueSizeExceeded();
if (
supplyQueue.length > ConstantsLib.MAX_QUEUE_LENGTH
|| withdrawQueue.length > ConstantsLib.MAX_QUEUE_LENGTH
) {
revert ErrorsLib.MaxQueueLengthExceeded();
}

// Safe "unchecked" cast because withdrawQueue.length <= MAX_QUEUE_SIZE.
// Safe "unchecked" cast because withdrawQueue.length <= MAX_QUEUE_LENGTH.
marketConfig.withdrawRank = uint64(withdrawQueue.length);
}

marketConfig.cap = supplyCap;

emit EventsLib.SetCap(id, supplyCap);
emit EventsLib.SetCap(_msgSender(), id, supplyCap);

delete pendingCap[id];
}
Expand All @@ -699,7 +716,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
// Safe "unchecked" cast because newFee <= MAX_FEE.
fee = uint96(newFee);

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

delete pendingFee;
}
Expand Down Expand Up @@ -751,9 +768,9 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
}
}

/// @dev Fakes a withdraw of `assets` from the idle liquidity and Morpho if necessary.
/// @dev Simulates a withdraw of `assets` from the idle liquidity and Morpho if necessary.
/// @return remaining The assets left to be withdrawn.
function _staticWithdrawMorpho(uint256 assets) internal view returns (uint256 remaining) {
function _simulateWithdrawMorpho(uint256 assets) internal view returns (uint256 remaining) {
(remaining,) = _withdrawIdle(assets);

if (remaining == 0) return 0;
Expand Down Expand Up @@ -795,6 +812,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
(uint256 totalSupplyAssets, uint256 totalSupplyShares, uint256 totalBorrowAssets,) =
MORPHO.expectedMarketBalances(marketParams);

// Inside a flashloan callback, liquidity on Morpho Blue may be limited to the singleton's balance.
uint256 availableLiquidity = UtilsLib.min(
totalSupplyAssets - totalBorrowAssets, ERC20(marketParams.loanToken).balanceOf(address(MORPHO))
);
Expand Down
2 changes: 1 addition & 1 deletion src/MetaMorphoFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ contract MetaMorphoFactory {
/// @notice Whether a MetaMorpho vault was created with the factory.
mapping(address => bool) public isMetaMorpho;

/* CONSTRCUTOR */
/* CONSTRUCTOR */

/// @dev Initializes the contract.
/// @param morpho The address of the Morpho contract.
Expand Down
12 changes: 6 additions & 6 deletions src/interfaces/IMetaMorpho.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,22 +35,22 @@ interface IMetaMorpho is IERC4626 {
function rewardsRecipient() external view returns (address);
function timelock() external view returns (uint256);
function supplyQueue(uint256) external view returns (Id);
function supplyQueueSize() external view returns (uint256);
function supplyQueueLength() external view returns (uint256);
function withdrawQueue(uint256) external view returns (Id);
function withdrawQueueSize() external view returns (uint256);
function withdrawQueueLength() external view returns (uint256);
function config(Id) external view returns (uint192 cap, uint64 withdrawRank);

function idle() external view returns (uint256);
function lastTotalAssets() external view returns (uint256);

function submitTimelock(uint256 newTimelock) external;
function acceptTimelock() external;
function revokeTimelock() external;
function revokePendingTimelock() external;
function pendingTimelock() external view returns (uint192 value, uint64 validAt);

function submitCap(MarketParams memory marketParams, uint256 supplyCap) external;
function acceptCap(Id id) external;
function revokeCap(Id id) external;
function revokePendingCap(Id id) external;
function pendingCap(Id) external view returns (uint192 value, uint64 validAt);

function submitFee(uint256 newFee) external;
Expand All @@ -59,7 +59,7 @@ interface IMetaMorpho is IERC4626 {

function submitGuardian(address newGuardian) external;
function acceptGuardian() external;
function revokeGuardian() external;
function revokePendingGuardian() external;
function pendingGuardian() external view returns (address guardian, uint96 validAt);

function transferRewards(address) external;
Expand All @@ -70,7 +70,7 @@ interface IMetaMorpho is IERC4626 {
function setRewardsRecipient(address) external;

function setSupplyQueue(Id[] calldata newSupplyQueue) external;
function sortWithdrawQueue(uint256[] calldata indexes) external;
function updateWithdrawQueue(uint256[] calldata indexes) external;
function reallocate(MarketAllocation[] calldata withdrawn, MarketAllocation[] calldata supplied) external;
}

Expand Down
4 changes: 2 additions & 2 deletions src/libraries/ConstantsLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ library ConstantsLib {
uint256 internal constant MAX_TIMELOCK = 2 weeks;

/// @dev The minimum delay of a timelock.
uint256 internal constant MIN_TIMELOCK = 12 hours;
uint256 internal constant MIN_TIMELOCK = 1 days;

/// @dev OpenZeppelin's decimals offset used in MetaMorpho's ERC4626 implementation.
uint8 internal constant DECIMALS_OFFSET = 6;

/// @dev The maximum number of markets in the supply/withdraw queue.
uint256 internal constant MAX_QUEUE_SIZE = 30;
uint256 internal constant MAX_QUEUE_LENGTH = 30;

/// @dev The maximum fee the vault can have (50%).
uint256 internal constant MAX_FEE = 0.5e18;
Expand Down
Loading

0 comments on commit ed465ae

Please sign in to comment.