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 14 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
4 changes: 2 additions & 2 deletions .github/workflows/foundry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,12 @@ jobs:
include:
- type: "slow"
fuzz-runs: 32768
max-test-rejects: 1048576
max-test-rejects: 16777216
invariant-runs: 64
invariant-depth: 1024
- type: "fast"
fuzz-runs: 256
max-test-rejects: 65536
max-test-rejects: 262144
invariant-runs: 16
invariant-depth: 256

Expand Down
2 changes: 2 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ tenderly = "https://rpc.tenderly.co/fork/${TENDERLY_FORK_ID}"
[profile.default.fmt]
wrap_comments = true

[profile.default.fuzz]
max_test_rejects = 16777216

[profile.build]
via-ir = true
Expand Down
107 changes: 51 additions & 56 deletions src/MetaMorpho.sol
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
/// @notice The fee recipient.
address public feeRecipient;

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

/// @notice The pending guardian.
PendingAddress public pendingGuardian;
Expand All @@ -89,10 +89,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.
uint256 public lastTotalAssets;

Expand Down Expand Up @@ -183,13 +179,13 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
emit EventsLib.SetIsAllocator(newAllocator, newIsAllocator);
}

/// @notice Sets `rewardsRecipient` to `newRewardsRecipient`.
function setRewardsRecipient(address newRewardsRecipient) external onlyOwner {
if (newRewardsRecipient == rewardsRecipient) revert ErrorsLib.AlreadySet();
/// @notice Sets `skimRecipient` to `newSkimRecipient`.
function setSkimRecipient(address newSkimRecipient) external onlyOwner {
if (newSkimRecipient == skimRecipient) revert ErrorsLib.AlreadySet();

rewardsRecipient = newRewardsRecipient;
skimRecipient = newSkimRecipient;

emit EventsLib.SetRewardsRecipient(newRewardsRecipient);
emit EventsLib.SetSkimRecipient(newSkimRecipient);
}

/// @notice Submits a `newTimelock`.
Expand Down Expand Up @@ -376,6 +372,8 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph

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

if (allocation.assets == type(uint256).max) allocation.assets = totalWithdrawn.zeroFloorSub(totalSupplied);

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

Expand All @@ -386,14 +384,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
totalSupplied += suppliedAssets;
Rubilmax marked this conversation as resolved.
Show resolved Hide resolved
}

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

idle -= idleSupplied;
}
if (totalWithdrawn != totalSupplied) revert ErrorsLib.InconsistentReallocation();
Rubilmax marked this conversation as resolved.
Show resolved Hide resolved
}

/* ONLY GUARDIAN FUNCTIONS */
Expand Down Expand Up @@ -451,17 +442,15 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
_setCap(id, pendingCap[id].value);
}

/// @notice Transfers `token` rewards collected by the vault to the `rewardsRecipient`.
/// @dev Can be used to extract any token that would be stuck on the contract as well.
function transferRewards(address token) external {
if (rewardsRecipient == address(0)) revert ErrorsLib.ZeroAddress();
/// @notice Skims the vault `token` balance to `skimRecipient`.
function skim(address token) external {
if (skimRecipient == address(0)) revert ErrorsLib.ZeroAddress();

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

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

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

/* ERC4626 (PUBLIC) */
Expand All @@ -471,6 +460,18 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
return ERC4626.decimals();
}

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

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

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

/// @inheritdoc IERC4626
function maxWithdraw(address owner) public view override(IERC4626, ERC4626) returns (uint256 assets) {
(assets,,) = _maxWithdraw(owner);
Expand Down Expand Up @@ -532,8 +533,6 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
for (uint256 i; i < withdrawQueue.length; ++i) {
assets += _supplyBalance(_marketParams(withdrawQueue[i]));
}

assets += idle;
}

/* ERC4626 (INTERNAL) */
Expand All @@ -558,6 +557,14 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
assets -= _staticWithdrawMorpho(assets);
}

/// @dev Returns the maximum amount of assets that the vault can withdraw from Morpho.
Rubilmax marked this conversation as resolved.
Show resolved Hide resolved
function _maxSupply() internal view returns (uint256 totalSuppliable) {
Rubilmax marked this conversation as resolved.
Show resolved Hide resolved
for (uint256 i; i < supplyQueue.length; ++i) {
Id id = supplyQueue[i];
totalSuppliable += _suppliable(_marketParams(id), id);
}
}

/// @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 @@ -618,7 +625,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
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 @@ -703,7 +710,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 @@ -721,40 +728,33 @@ 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 toWithdraw = UtilsLib.min(_withdrawable(marketParams, id), remaining);
uint256 toWithdraw = UtilsLib.min(_withdrawable(marketParams, id), 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 Fakes 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) {
MerlinEgalite marked this conversation as resolved.
Show resolved Hide resolved
(remaining,) = _withdrawIdle(assets);

if (remaining == 0) return 0;
if (assets != 0) revert ErrorsLib.WithdrawMorphoFailed();
MerlinEgalite marked this conversation as resolved.
Show resolved Hide resolved
Rubilmax marked this conversation as resolved.
Show resolved Hide resolved
}

/// @dev Fakes a withdraw of `assets` from Morpho.
Rubilmax marked this conversation as resolved.
Show resolved Hide resolved
/// @return The assets left to be withdrawn.
function _staticWithdrawMorpho(uint256 assets) internal view returns (uint256) {
for (uint256 i; i < withdrawQueue.length; ++i) {
Id id = withdrawQueue[i];
MarketParams memory marketParams = _marketParams(id);
Expand All @@ -763,17 +763,12 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
// 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`
remaining -= UtilsLib.min(_withdrawable(marketParams, id), remaining);
assets -= UtilsLib.min(_withdrawable(marketParams, id), assets);
Rubilmax marked this conversation as resolved.
Show resolved Hide resolved

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;
MerlinEgalite marked this conversation as resolved.
Show resolved Hide resolved
}

/// @dev Returns the suppliable amount of assets on the market defined by `marketParams`.
Expand Down
7 changes: 3 additions & 4 deletions src/interfaces/IMetaMorpho.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,14 @@ interface IMetaMorpho is IERC4626 {

function fee() external view returns (uint96);
function feeRecipient() external view returns (address);
function rewardsRecipient() external view returns (address);
function skimRecipient() external view returns (address);
function timelock() external view returns (uint256);
function supplyQueue(uint256) external view returns (Id);
function supplyQueueSize() external view returns (uint256);
function withdrawQueue(uint256) external view returns (Id);
function withdrawQueueSize() 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;
Expand All @@ -74,12 +73,12 @@ interface IMetaMorpho is IERC4626 {
function revokeGuardian() external;
function pendingGuardian() external view returns (address guardian, uint96 submittedAt);

function transferRewards(address) external;
function skim(address) external;

function setIsAllocator(address newAllocator, bool newIsAllocator) external;
function setCurator(address newCurator) external;
function setFeeRecipient(address newFeeRecipient) external;
function setRewardsRecipient(address) external;
function setSkimRecipient(address) external;

function setSupplyQueue(Id[] calldata newSupplyQueue) external;
function sortWithdrawQueue(uint256[] calldata indexes) external;
Expand Down
7 changes: 5 additions & 2 deletions src/libraries/ErrorsLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,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();
}
12 changes: 5 additions & 7 deletions src/libraries/EventsLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ 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(uint256 newTimelock);

/// @notice Emitted `rewardsDistibutor` is set to `newRewardsRecipient`.
event SetRewardsRecipient(address indexed newRewardsRecipient);
/// @notice Emitted when `skimRecipient` is set to `newSkimRecipient`.
event SetSkimRecipient(address indexed newSkimRecipient);

/// @notice Emitted when a pending `newFee` is submitted.
event SubmitFee(uint256 newFee);
Expand Down Expand Up @@ -66,10 +66,8 @@ library EventsLib {
/// @notice Emitted when fees are accrued.
event AccrueFee(uint256 feeShares);

/// @notice Emitted when an `amount` of `token` is transferred to the `rewardsRecipient` by `caller`.
event TransferRewards(
address indexed caller, address indexed rewardsRecipient, address indexed token, uint256 amount
);
/// @notice Emitted when an `amount` of `token` is transferred to the `skimRecipient` by `caller`.
event Skim(address indexed caller, address indexed skimRecipient, address indexed token, uint256 amount);

/// @notice Emitted when a new MetaMorpho vault is created.
/// @param metaMorpho The address of the MetaMorpho vault.
Expand Down
2 changes: 2 additions & 0 deletions test/forge/ERC4626A16zTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,7 @@ contract ERC4626A16zTest is IntegrationTest, ERC4626Test {
_delta_ = 0;
_vaultMayBeEmpty = true;
_unlimitedAmount = true;

_setCap(allMarkets[0], 1e28);
}
}
45 changes: 6 additions & 39 deletions test/forge/ERC4626Test.sol
Original file line number Diff line number Diff line change
Expand Up @@ -87,26 +87,6 @@ contract ERC4626Test is IntegrationTest, IMorphoFlashLoanCallback {
assertEq(vault.balanceOf(ONBEHALF), shares - redeemed, "balanceOf(ONBEHALF)");
}

function testWithdrawIdle(uint256 deposited, uint256 withdrawn) public {
deposited = bound(deposited, MIN_TEST_ASSETS, MAX_TEST_ASSETS);
withdrawn = bound(withdrawn, 0, deposited);

_setCap(allMarkets[0], 0);

loanToken.setBalance(SUPPLIER, deposited);

vm.prank(SUPPLIER);
uint256 shares = vault.deposit(deposited, ONBEHALF);

vm.expectEmit();
emit EventsLib.UpdateLastTotalAssets(vault.totalAssets() - withdrawn);
vm.prank(ONBEHALF);
uint256 redeemed = vault.withdraw(withdrawn, RECEIVER, ONBEHALF);

assertEq(vault.balanceOf(ONBEHALF), shares - redeemed, "balanceOf(ONBEHALF)");
assertEq(vault.idle(), deposited - withdrawn, "idle");
}

function testRedeemTooMuch(uint256 deposited) public {
deposited = bound(deposited, MIN_TEST_ASSETS, MAX_TEST_ASSETS);

Expand Down Expand Up @@ -236,28 +216,15 @@ contract ERC4626Test is IntegrationTest, IMorphoFlashLoanCallback {
vault.transferFrom(ONBEHALF, RECEIVER, shares);
}

function testWithdrawMoreThanBalanceButLessThanTotalAssets(uint256 deposited, uint256 assets) public {
function testWithdrawMoreThanBalanceButLessThanTotalAssets(uint256 deposited) public {
deposited = bound(deposited, MIN_TEST_ASSETS, MAX_TEST_ASSETS);

loanToken.setBalance(SUPPLIER, deposited);

vm.prank(SUPPLIER);
uint256 shares = vault.deposit(deposited, ONBEHALF);

assets = bound(assets, deposited + 1, type(uint256).max / (deposited + 10 ** ConstantsLib.DECIMALS_OFFSET));

uint256 toAdd = assets - deposited + 1;
loanToken.setBalance(SUPPLIER, toAdd);

vm.prank(SUPPLIER);
vault.deposit(toAdd, SUPPLIER);

uint256 sharesBurnt = vault.previewWithdraw(assets);
vm.prank(ONBEHALF);
vm.expectRevert(
abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, ONBEHALF, shares, sharesBurnt)
);
vault.withdraw(assets, RECEIVER, ONBEHALF);
vm.startPrank(SUPPLIER);
vault.deposit(deposited / 2, ONBEHALF);
vault.deposit(deposited / 2, SUPPLIER);
vm.expectRevert();
vault.withdraw(deposited, RECEIVER, SUPPLIER);
}

function testWithdrawMoreThanTotalAssets(uint256 deposited, uint256 assets) public {
Expand Down
2 changes: 1 addition & 1 deletion test/forge/MetaMorphoFactoryTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ contract MetaMorphoFactoryTest is IntegrationTest {
factory = new MetaMorphoFactory(address(morpho));
}

function testFactoryAddresssZero() public {
function testFactoryAddressZero() public {
vm.expectRevert(ErrorsLib.ZeroAddress.selector);
new MetaMorphoFactory(address(0));
}
Expand Down
Loading
Loading