Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

remove idle supply #260

Merged
merged 36 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
abe7c87
feat: remove idle supply
MathisGD Oct 23, 2023
e1e1e4e
style: error naming
MathisGD Oct 23, 2023
4a8912f
style: skim working
MathisGD Oct 23, 2023
6b05d0c
test: fix erc4626 test
MathisGD Oct 23, 2023
02889d7
chore: fmt
MathisGD Oct 23, 2023
31dc4d1
test: fix a16z test
MathisGD Oct 24, 2023
f30a959
chore: fmt
MathisGD Oct 24, 2023
c18a16b
Merge branch 'main' into feat/remove-idle
MathisGD Oct 24, 2023
868c6f9
chore: increase CI max rejects
MathisGD Oct 24, 2023
e5c2488
chore: increase CI max rejects
MathisGD Oct 24, 2023
d22aede
feat: apply suggestions
MerlinEgalite Oct 27, 2023
586ed73
fix: errors
MerlinEgalite Oct 27, 2023
9ccb15b
test: fix tests
MerlinEgalite Oct 27, 2023
3db84c8
feat: remove todos
MerlinEgalite Oct 27, 2023
1eba06c
refactor(metamorpho): rename rewards to skim
Rubilmax Oct 30, 2023
d1f89ba
Merge branch 'refactor/rewards-skim' of github.com:morpho-org/metamor…
Rubilmax Oct 30, 2023
69417cb
Merge branch 'refactor/rewards-skim' of github.com:morpho-org/metamor…
Rubilmax Oct 30, 2023
1ec7e72
refactor(metamorpho): use market for idle
Rubilmax Oct 30, 2023
d240a93
Merge branch 'feat/idle-market' of github.com:morpho-org/metamorpho i…
Rubilmax Oct 30, 2023
b9e54a9
test(reallocate): revert changes
Rubilmax Oct 31, 2023
cd5b7bc
test(erc4626): fix compliance tests
Rubilmax Oct 31, 2023
f743546
test(erc4626): revert test changes
Rubilmax Oct 31, 2023
dd47e4c
test(hardhat): adapt tests
Rubilmax Oct 31, 2023
635bbf8
Merge branch 'refactor/rewards-skim' of github.com:morpho-org/metamor…
Rubilmax Nov 7, 2023
a110714
refactor(idle): rename internal functions
Rubilmax Nov 7, 2023
67a0ee9
test(permit): revert changes
Rubilmax Nov 8, 2023
a026183
Merge branch 'test/permit' of github.com:morpho-org/metamorpho into f…
Rubilmax Nov 8, 2023
4838932
ci(foundry): increase max test rejects
Rubilmax Nov 9, 2023
5ac5bb7
docs(readme): add idle market doc
Rubilmax Nov 9, 2023
210f943
Merge branch 'test/permit' of github.com:morpho-org/metamorpho into f…
Rubilmax Nov 10, 2023
c4bf039
docs(README): update
Rubilmax Nov 14, 2023
9041eb9
Merge branch 'main' of github.com:morpho-org/metamorpho into feat/rem…
Rubilmax Nov 14, 2023
f6a161d
docs(README): mention infinite cap
Rubilmax Nov 14, 2023
07caa3b
Merge branch 'main' of github.com:morpho-org/metamorpho into feat/rem…
Rubilmax Nov 14, 2023
81b0524
fix: apply suggestions
Rubilmax Nov 15, 2023
bf65c14
docs(errors): fix typo
Rubilmax Nov 15, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/foundry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ jobs:
type: ["slow", "fast"]
include:
- type: "slow"
fuzz-runs: 8192
fuzz-runs: 1024
max-test-rejects: 1048576
invariant-runs: 64
invariant-depth: 1024
invariant-runs: 32
invariant-depth: 512
- type: "fast"
fuzz-runs: 256
max-test-rejects: 65536
Expand Down
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,15 @@ 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 `supplyQueue` in the order set. The remaining funds are left as idle supply on the vault (uncapped).
- 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.

#### Guardian

Only one address can have this role.
Expand All @@ -91,6 +94,23 @@ It can:
- Revoke the pending guardian (which means it can revoke any attempt to change the guardian).
- Revoke the pending cap of any market.

### Idle Supply

In some cases, the vault's curator or allocators may want to keep some funds "idle", to guarantee lenders that some liquidity can be withdrawn from the vault (beyond the liquidity of each of the vault's markets).

To achieve this, it is advised to allocate "idle" funds to any market on Morpho Blue having:

- The vault's asset as loan token.
- No collateral token (`address(0)`).
- An arbitrary IRM.
- An arbitrary oracle (`address(0)`).
- An arbitrary LLTV.

Thus, these funds cannot be borrowed on Morpho Blue and are guaranteed to be liquid; though it won't generate interest.

Note that to allocate funds to this idle market, it is first required to enable its cap on MetaMorpho.
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
Enabling an infinite cap (`type(uint184).max`) will always allow users to deposit on the vault.

### Rewards

To redistribute rewards to vault depositors, it is advised to use the [Universal Rewards Distributor (URD)](https://github.com/morpho-org/universal-rewards-distributor).
Expand All @@ -111,8 +131,7 @@ Below is a typical example of how this use case would take place:

NB: Anyone can transfer rewards from the vault to the rewards distributor unless it is unset.
Thus, this step might be already performed by some third-party.
Note: the amount of rewards transferred is calculated based on the balance in the reward asset of the vault.
In case the reward asset is the vault’s asset, the vault’s idle liquidity is automatically subtracted to prevent stealing idle liquidity.
Note: the amount of rewards transferred corresponds to the vault's balance of reward asset.

- Compute the new root for the vault’s rewards distributor, submit it, wait for the timelock (if any), accept the root, and let vault depositors claim their rewards according to the vault manager’s rewards re-distribution strategy.

Expand Down
103 changes: 50 additions & 53 deletions src/MetaMorpho.sol
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,6 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
/// without duplicate.
Id[] public withdrawQueue;

/// @notice Stores the idle liquidity.
/// @dev The idle liquidity does not generate any interest.
uint256 public idle;

/// @notice Stores the total assets managed by this vault when the fee was last accrued.
/// @dev May be a little off `totalAssets()` after each interaction, due to some roundings.
uint256 public lastTotalAssets;
Expand Down Expand Up @@ -418,7 +414,10 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph

totalWithdrawn += withdrawnAssets;
} else {
uint256 suppliedAssets = allocation.assets.zeroFloorSub(supplyAssets);
uint256 suppliedAssets = allocation.assets == type(uint256).max
? totalWithdrawn.zeroFloorSub(totalSupplied)
: allocation.assets.zeroFloorSub(supplyAssets);

if (suppliedAssets == 0) continue;

uint256 supplyCap = config[id].cap;
Expand All @@ -436,19 +435,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
}
}

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

newIdle = idle - idleSupplied;
}

idle = newIdle;

emit EventsLib.ReallocateIdle(_msgSender(), newIdle);
if (totalWithdrawn != totalSupplied) revert ErrorsLib.InconsistentReallocation();
Rubilmax marked this conversation as resolved.
Show resolved Hide resolved
}

/* REVOKE FUNCTIONS */
Expand Down Expand Up @@ -520,7 +507,6 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
if (skimRecipient == address(0)) revert ErrorsLib.ZeroAddress();

uint256 amount = IERC20(token).balanceOf(address(this));
if (token == asset()) amount -= idle;

IERC20(token).safeTransfer(skimRecipient, amount);

Expand All @@ -534,6 +520,18 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
return ERC4626.decimals();
}

/// @inheritdoc IERC4626
function maxDeposit(address) public view override returns (uint256) {
return _maxDeposit();
}

/// @inheritdoc IERC4626
function maxMint(address) public view override returns (uint256) {
uint256 suppliable = _maxDeposit();

return _convertToShares(suppliable, Math.Rounding.Floor);
}

/// @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.
Expand Down Expand Up @@ -609,8 +607,6 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
for (uint256 i; i < withdrawQueue.length; ++i) {
assets += MORPHO.expectedSupplyAssets(_marketParams(withdrawQueue[i]), address(this));
}

assets += idle;
}

/* ERC4626 (INTERNAL) */
Expand All @@ -635,6 +631,20 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
assets -= _simulateWithdrawMorpho(assets);
}

/// @dev Returns the maximum amount of assets that the vault can supply on Morpho.
function _maxDeposit() internal view returns (uint256 totalSuppliable) {
Rubilmax marked this conversation as resolved.
Show resolved Hide resolved
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
for (uint256 i; i < supplyQueue.length; ++i) {
Id id = supplyQueue[i];

uint256 supplyCap = config[id].cap;
if (supplyCap == 0) continue;

uint256 supplyAssets = MORPHO.expectedSupplyAssets(_marketParams(id), address(this));

totalSuppliable += supplyCap.zeroFloorSub(supplyAssets);
}
}

/// @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) {
Expand Down Expand Up @@ -689,13 +699,13 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
/// @dev Depending on 4 cases, reverts when withdrawing "too much" with:
/// 1. ERC20InsufficientAllowance when withdrawing more than `caller`'s allowance.
/// 2. ERC20InsufficientBalance when withdrawing more than `owner`'s balance but less than vault's total assets.
/// 3. WithdrawMorphoFailed when withdrawing more than vault's total assets.
/// 4. WithdrawMorphoFailed when withdrawing more than `owner`'s balance but less than the available liquidity.
/// 3. NotEnoughLiquidity when withdrawing more than vault's total assets.
/// 4. NotEnoughLiquidity when withdrawing more than `owner`'s balance but less than the available liquidity.
function _withdraw(address caller, address receiver, address owner, uint256 assets, uint256 shares)
internal
override
{
if (_withdrawMorpho(assets) != 0) revert ErrorsLib.WithdrawMorphoFailed();
MerlinEgalite marked this conversation as resolved.
Show resolved Hide resolved
_withdrawMorpho(assets);

super._withdraw(caller, receiver, owner, assets, shares);
}
Expand Down Expand Up @@ -776,7 +786,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph

/* LIQUIDITY ALLOCATION */

/// @dev Supplies `assets` to Morpho and increase the idle liquidity if necessary.
/// @dev Supplies `assets` to Morpho.
function _supplyMorpho(uint256 assets) internal {
for (uint256 i; i < supplyQueue.length; ++i) {
Id id = supplyQueue[i];
Expand All @@ -799,43 +809,36 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
if (assets == 0) return;
}

idle += assets;
if (assets != 0) revert ErrorsLib.AllCapsReached();
}

/// @dev Withdraws `assets` from the idle liquidity and Morpho if necessary.
/// @return remaining The assets left to be withdrawn.
function _withdrawMorpho(uint256 assets) internal returns (uint256 remaining) {
(remaining, idle) = _withdrawIdle(assets);

if (remaining == 0) return 0;

/// @dev Withdraws `assets` from Morpho.
function _withdrawMorpho(uint256 assets) internal {
for (uint256 i; i < withdrawQueue.length; ++i) {
Id id = withdrawQueue[i];
MarketParams memory marketParams = _marketParams(id);
(uint256 supplyAssets,, Market memory market) = _accruedSupplyBalance(marketParams, id);

uint256 toWithdraw = UtilsLib.min(
_withdrawable(marketParams, market.totalSupplyAssets, market.totalBorrowAssets, supplyAssets), remaining
_withdrawable(marketParams, market.totalSupplyAssets, market.totalBorrowAssets, supplyAssets), assets
);

if (toWithdraw > 0) {
// Using try/catch to skip markets that revert.
try MORPHO.withdraw(marketParams, toWithdraw, 0, address(this), address(this)) {
remaining -= toWithdraw;
assets -= toWithdraw;
} catch {}
}

if (remaining == 0) return 0;
if (assets == 0) return;
}
}

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

if (remaining == 0) return 0;
if (assets != 0) revert ErrorsLib.NotEnoughLiquidity();
}

/// @dev Simulates a withdraw of `assets` from Morpho.
/// @return The remaining assets to be withdrawn.
function _simulateWithdrawMorpho(uint256 assets) internal view returns (uint256) {
for (uint256 i; i < withdrawQueue.length; ++i) {
Id id = withdrawQueue[i];
MarketParams memory marketParams = _marketParams(id);
Expand All @@ -848,25 +851,19 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
// 1. oracle.price() is never called (the vault doesn't borrow)
// 2. the amount is capped to the liquidity available on Morpho
// 3. virtually accruing interest didn't fail
remaining -= UtilsLib.min(
assets = assets.zeroFloorSub(
_withdrawable(
marketParams,
totalSupplyAssets,
totalBorrowAssets,
supplyShares.toAssetsDown(totalSupplyAssets, totalSupplyShares)
),
remaining
)
);

if (remaining == 0) return 0;
if (assets == 0) break;
}
}

/// @dev Withdraws `assets` from the idle liquidity.
/// @return The remaining assets to withdraw.
/// @return The new `idle` liquidity value.
function _withdrawIdle(uint256 assets) internal view returns (uint256, uint256) {
return (assets.zeroFloorSub(idle), idle.zeroFloorSub(assets));
return assets;
}

/// @dev Returns the withdrawable amount of assets from the market defined by `marketParams`, given the market's
Expand Down
1 change: 0 additions & 1 deletion src/interfaces/IMetaMorpho.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ interface IMetaMorphoBase {
function withdrawQueue(uint256) external view returns (Id);
function withdrawQueueLength() external view returns (uint256);

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

function submitTimelock(uint256 newTimelock) external;
Expand Down
11 changes: 7 additions & 4 deletions src/libraries/ErrorsLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ library ErrorsLib {
/// @notice Thrown when there's no pending value to set.
error NoPendingValue();

/// @notice Thrown when the remaining asset to withdraw is not 0.
error WithdrawMorphoFailed();
/// @notice Thrown when the requested liquidity cannot be withdrawn from Morpho.
error NotEnoughLiquidity();

/// @notice Thrown when submitting a cap for a market which does not exist.
error MarketNotCreated();
Expand All @@ -81,6 +81,9 @@ library ErrorsLib {
/// @notice Thrown when setting the fee to a non zero value while the fee recipient is the zero address.
error ZeroFeeRecipient();

/// @notice Thrown when the idle liquidity is insufficient to cover supply during a reallocation of funds.
error InsufficientIdle();
/// @notice Thrown when the amount withdrawn is not excatly the amount supplied.
Rubilmax marked this conversation as resolved.
Show resolved Hide resolved
error InconsistentReallocation();
MerlinEgalite marked this conversation as resolved.
Show resolved Hide resolved

/// @notice Thrown when all caps have been reached.
error AllCapsReached();
}
5 changes: 1 addition & 4 deletions src/libraries/EventsLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ library EventsLib {
/// @notice Emitted when a pending `newTimelock` is submitted.
event SubmitTimelock(uint256 newTimelock);

/// @notice Emitted `timelock` is set to `newTimelock`.
/// @notice Emitted when `timelock` is set to `newTimelock`.
event SetTimelock(address indexed caller, uint256 newTimelock);

/// @notice Emitted when `skimRecipient` is set to `newSkimRecipient`.
Expand Down Expand Up @@ -79,9 +79,6 @@ library EventsLib {
/// @param withdrawnShares The amount of shares burned.
event ReallocateWithdraw(address indexed caller, Id indexed id, uint256 withdrawnAssets, uint256 withdrawnShares);

/// @notice Emitted when a reallocation added or removed assets from idle.
event ReallocateIdle(address indexed caller, uint256 idle);

/// @notice Emitted when interest are accrued.
/// @param newTotalAssets The assets of the vault after accruing the interest but before the interaction.
/// @param feeShares The shares minted to the fee recipient.
Expand Down
2 changes: 2 additions & 0 deletions src/mocks/ERC20Mock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import {ERC20} from "@openzeppelin/token/ERC20/ERC20.sol";
contract ERC20Mock is ERC20 {
constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol) {}

/// @dev Required for ERC4626-compliance tests.
function mint(address account, uint256 amount) external {
_mint(account, amount);
}

/// @dev Required for ERC4626-compliance tests.
function burn(address account, uint256 amount) external {
_burn(account, amount);
}
Expand Down
3 changes: 2 additions & 1 deletion test/forge/ERC4626ComplianceTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ contract ERC4626ComplianceTest is IntegrationTest, ERC4626Test {
_vaultMayBeEmpty = true;
_unlimitedAmount = true;

_setCap(allMarkets[0], 1e28);
_setCap(allMarkets[0], 100e18);
_sortSupplyQueueIdleLast();
}
}
Loading
Loading