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

Update last total assets in _setCap #338

Merged
merged 12 commits into from
Dec 28, 2023
Merged
21 changes: 15 additions & 6 deletions src/MetaMorpho.sol
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
if (newSupplyCap == supplyCap) revert ErrorsLib.AlreadySet();

if (newSupplyCap < supplyCap) {
_setCap(id, newSupplyCap.toUint184());
_setCap(marketParams, id, newSupplyCap.toUint184());
} else {
// newSupplyCap > supplyCap >= 0 so there's no need to check `pendingCap[id].validAt != 0`.
if (newSupplyCap == pendingCap[id].value) revert ErrorsLib.AlreadyPending();
Expand All @@ -298,11 +298,12 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
}

/// @inheritdoc IMetaMorphoBase
function submitMarketRemoval(Id id) external onlyCuratorRole {
function submitMarketRemoval(MarketParams memory marketParams) external onlyCuratorRole {
Id id = marketParams.id();
if (config[id].removableAt != 0) revert ErrorsLib.AlreadySet();
if (!config[id].enabled) revert ErrorsLib.MarketNotEnabled(id);

_setCap(id, 0);
_setCap(marketParams, id, 0);

// Safe "unchecked" cast because timelock <= MAX_TIMELOCK.
config[id].removableAt = uint64(block.timestamp + timelock);
Expand Down Expand Up @@ -480,9 +481,14 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
}

/// @inheritdoc IMetaMorphoBase
function acceptCap(Id id) external afterTimelock(pendingCap[id].validAt) {
function acceptCap(MarketParams memory marketParams)
external
afterTimelock(pendingCap[marketParams.id()].validAt)
{
Id id = marketParams.id();

// Safe "unchecked" cast because pendingCap <= type(uint184).max.
_setCap(id, uint184(pendingCap[id].value));
_setCap(marketParams, id, uint184(pendingCap[id].value));
}

/// @inheritdoc IMetaMorphoBase
Expand Down Expand Up @@ -739,7 +745,8 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
}

/// @dev Sets the cap of the market defined by `id` to `supplyCap`.
function _setCap(Id id, uint184 supplyCap) internal {
/// @dev Assumes that the inputs `marketParams` and `id` match.
function _setCap(MarketParams memory marketParams, Id id, uint184 supplyCap) internal {
MarketConfig storage marketConfig = config[id];

if (supplyCap > 0) {
Expand All @@ -750,6 +757,8 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph

marketConfig.enabled = true;

_updateLastTotalAssets(lastTotalAssets + MORPHO.expectedSupplyAssets(marketParams, address(this)));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider avoiding executing this function if MORPHO.expectedSupplyAssets(marketParams, address(this)) is equal to zero.

I would say that the normal scenario should be that you are enabling a market where no one has supplied on behalf of MM.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, but it would save only ~2k gas since we still need to call expectedSupplyAssets. In the current configuration we're adding one cold SLOAD and one warm SSTORE that is not changing the value. So no sure it's worth considering this case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure it's worth it considering this case. The current implementation only adds one cold SLOAD and one warm SSTORE so around 2k gas.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If not documented yet, consider documenting the side effects of this operation. The share value (of MM) is increased.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should there be a check that reverts if the market is broken?

MORPHO.expectedSupplyAssets won't revert if elapsed == 0 (and the IRM is broken) and it does not use the oracle (called once you need to withdraw)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's ok not to revert in this case. Vaults curators should not list broken markets in the first place.


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

Expand Down
6 changes: 3 additions & 3 deletions src/interfaces/IMetaMorpho.sol
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ interface IMetaMorphoBase {
/// @dev Warning: Submitting a cap will overwrite the current pending cap.
function submitCap(MarketParams memory marketParams, uint256 newSupplyCap) external;

/// @notice Accepts the pending cap of the market defined by `id`.
function acceptCap(Id id) external;
/// @notice Accepts the pending cap of the market defined by `marketParams`.
function acceptCap(MarketParams memory marketParams) external;

/// @notice Revokes the pending cap of the market defined by `id`.
function revokePendingCap(Id id) external;
Expand All @@ -105,7 +105,7 @@ interface IMetaMorphoBase {
/// To softly remove a sane market, the curator role is expected to bundle a reallocation that empties the market
/// first (using `reallocate`), followed by the removal of the market (using `updateWithdrawQueue`).
/// @dev Warning: Removing a market with non-zero supply will instantly impact the vault's price per share.
function submitMarketRemoval(Id id) external;
function submitMarketRemoval(MarketParams memory marketParams) external;

/// @notice Revokes the pending removal of the market defined by `id`.
function revokePendingMarketRemoval(Id id) external;
Expand Down
2 changes: 1 addition & 1 deletion test/forge/GuardianTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ contract GuardianTest is IntegrationTest {
_setCap(marketParams, 0);

vm.prank(CURATOR);
vault.submitMarketRemoval(id);
vault.submitMarketRemoval(allMarkets[0]);

vm.warp(block.timestamp + elapsed);

Expand Down
48 changes: 42 additions & 6 deletions test/forge/MarketTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {SafeCast} from "../../lib/openzeppelin-contracts/contracts/utils/math/Sa
import "./helpers/IntegrationTest.sol";

contract MarketTest is IntegrationTest {
using MathLib for uint256;
using MarketParamsLib for MarketParams;
using MorphoLib for IMorpho;

Expand Down Expand Up @@ -116,7 +117,7 @@ contract MarketTest is IntegrationTest {
vm.warp(block.timestamp + 1 weeks);

vm.expectRevert(ErrorsLib.MaxQueueLengthExceeded.selector);
vault.acceptCap(marketParams.id());
vault.acceptCap(marketParams);
}

function testSetSupplyQueueUnauthorizedMarket() public {
Expand Down Expand Up @@ -156,7 +157,7 @@ contract MarketTest is IntegrationTest {
_setCap(allMarkets[2], 0);

vm.prank(CURATOR);
vault.submitMarketRemoval(allMarkets[2].id());
vault.submitMarketRemoval(allMarkets[2]);

vm.warp(block.timestamp + TIMELOCK);

Expand Down Expand Up @@ -185,17 +186,17 @@ contract MarketTest is IntegrationTest {
vm.expectEmit();
emit EventsLib.SubmitMarketRemoval(CURATOR, allMarkets[2].id());
vm.prank(CURATOR);
vault.submitMarketRemoval(allMarkets[2].id());
vault.submitMarketRemoval(allMarkets[2]);

assertEq(vault.config(allMarkets[2].id()).cap, 0);
assertEq(vault.config(allMarkets[2].id()).removableAt, block.timestamp + TIMELOCK);
}

function testSubmitMarketRemovalAlreadySet() public {
vm.startPrank(CURATOR);
vault.submitMarketRemoval(allMarkets[2].id());
vault.submitMarketRemoval(allMarkets[2]);
vm.expectRevert(ErrorsLib.AlreadySet.selector);
vault.submitMarketRemoval(allMarkets[2].id());
vault.submitMarketRemoval(allMarkets[2]);
vm.stopPrank();
}

Expand Down Expand Up @@ -264,7 +265,7 @@ contract MarketTest is IntegrationTest {
_setCap(idleParams, 0);

vm.prank(CURATOR);
vault.submitMarketRemoval(idleParams.id());
vault.submitMarketRemoval(idleParams);

vm.warp(block.timestamp + elapsed);

Expand All @@ -279,4 +280,39 @@ contract MarketTest is IntegrationTest {
);
vault.updateWithdrawQueue(indexes);
}

function testEnableMarketWithLiquidity(uint256 deposited, uint256 additionalSupply, uint256 blocks) public {
deposited = bound(deposited, MIN_TEST_ASSETS, MAX_TEST_ASSETS);
additionalSupply = bound(additionalSupply, MIN_TEST_ASSETS, MAX_TEST_ASSETS);
blocks = _boundBlocks(blocks);

Id[] memory supplyQueue = new Id[](1);
supplyQueue[0] = allMarkets[0].id();

_setCap(allMarkets[0], deposited);

vm.prank(ALLOCATOR);
vault.setSupplyQueue(supplyQueue);

loanToken.setBalance(SUPPLIER, deposited + additionalSupply);

vm.startPrank(SUPPLIER);
vault.deposit(deposited, ONBEHALF);
morpho.supply(allMarkets[3], additionalSupply, 0, address(vault), hex"");
vm.stopPrank();

uint256 collateral = uint256(MAX_TEST_ASSETS).wDivUp(allMarkets[0].lltv);
collateralToken.setBalance(BORROWER, collateral);

vm.startPrank(BORROWER);
morpho.supplyCollateral(allMarkets[0], collateral, BORROWER, hex"");
morpho.borrow(allMarkets[0], deposited, 0, BORROWER, BORROWER);
vm.stopPrank();

_forward(blocks);

_setCap(allMarkets[3], CAP);

assertEq(vault.lastTotalAssets(), deposited + additionalSupply);
}
}
11 changes: 4 additions & 7 deletions test/forge/MetaMorphoInternalTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,19 @@ contract MetaMorphoInternalTest is InternalTest {

function testSetCapMaxQueueLengthExcedeed() public {
for (uint256 i; i < NB_MARKETS - 1; ++i) {
Id id = allMarkets[i].id();
_setCap(id, CAP);
_setCap(allMarkets[i], allMarkets[i].id(), CAP);
}

Id lastId = allMarkets[NB_MARKETS - 1].id();
vm.expectRevert(ErrorsLib.MaxQueueLengthExceeded.selector);
_setCap(lastId, CAP);
_setCap(allMarkets[NB_MARKETS - 1], allMarkets[NB_MARKETS - 1].id(), CAP);
}

function testSimulateWithdraw(uint256 suppliedAmount, uint256 borrowedAmount, uint256 assets) public {
suppliedAmount = bound(suppliedAmount, MIN_TEST_ASSETS, MAX_TEST_ASSETS);
borrowedAmount = bound(borrowedAmount, MIN_TEST_ASSETS, suppliedAmount);

Id id = allMarkets[0].id();
_setCap(id, CAP);
supplyQueue = [id];
_setCap(allMarkets[0], allMarkets[0].id(), CAP);
supplyQueue = [allMarkets[0].id()];

loanToken.setBalance(SUPPLIER, suppliedAmount);
vm.prank(SUPPLIER);
Expand Down
15 changes: 7 additions & 8 deletions test/forge/TimelockTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ contract TimelockTest is IntegrationTest {

vm.expectEmit(address(vault));
emit EventsLib.SetCap(address(this), id, cap);
vault.acceptCap(id);
vault.acceptCap(marketParams);

MarketConfig memory marketConfig = vault.config(id);
PendingUint192 memory pendingCap = vault.pendingCap(id);
Expand Down Expand Up @@ -383,7 +383,7 @@ contract TimelockTest is IntegrationTest {

vm.expectEmit();
emit EventsLib.SetCap(address(this), id, cap);
vault.acceptCap(id);
vault.acceptCap(marketParams);

MarketConfig memory marketConfig = vault.config(id);
PendingUint192 memory pendingCap = vault.pendingCap(id);
Expand All @@ -408,7 +408,6 @@ contract TimelockTest is IntegrationTest {
vm.warp(block.timestamp + elapsed);

MarketParams memory marketParams = allMarkets[0];
Id id = marketParams.id();

vm.prank(CURATOR);
vault.submitCap(marketParams, cap);
Expand All @@ -418,12 +417,12 @@ contract TimelockTest is IntegrationTest {
vault.acceptTimelock();

vm.expectRevert(ErrorsLib.TimelockNotElapsed.selector);
vault.acceptCap(id);
vault.acceptCap(marketParams);
}

function testAcceptCapNoPendingValue() public {
vm.expectRevert(ErrorsLib.NoPendingValue.selector);
vault.acceptCap(allMarkets[0].id());
vault.acceptCap(allMarkets[0]);
}

function testAcceptCapTimelockNotElapsed(uint256 elapsed) public {
Expand All @@ -435,7 +434,7 @@ contract TimelockTest is IntegrationTest {
vm.warp(block.timestamp + elapsed);

vm.expectRevert(ErrorsLib.TimelockNotElapsed.selector);
vault.acceptCap(allMarkets[1].id());
vault.acceptCap(allMarkets[1]);
}

function testSubmitMarketRemoval() public {
Expand All @@ -445,7 +444,7 @@ contract TimelockTest is IntegrationTest {
_setCap(marketParams, 0);

vm.prank(CURATOR);
vault.submitMarketRemoval(id);
vault.submitMarketRemoval(marketParams);

MarketConfig memory marketConfig = vault.config(id);

Expand All @@ -457,6 +456,6 @@ contract TimelockTest is IntegrationTest {
function testSubmitMarketRemovalMarketNotEnabled() public {
vm.expectRevert(abi.encodeWithSelector(ErrorsLib.MarketNotEnabled.selector, allMarkets[1].id()));
vm.prank(CURATOR);
vault.submitMarketRemoval(allMarkets[1].id());
vault.submitMarketRemoval(allMarkets[1]);
}
}
2 changes: 1 addition & 1 deletion test/forge/helpers/IntegrationTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ contract IntegrationTest is BaseTest {

vm.warp(block.timestamp + vault.timelock());

vault.acceptCap(id);
vault.acceptCap(marketParams);

assertEq(vault.config(id).cap, newCap, "_setCap");

Expand Down
6 changes: 3 additions & 3 deletions test/hardhat/MetaMorpho.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { AbiCoder, MaxUint256, ZeroAddress, ZeroHash, keccak256, toBigInt } from
import hre from "hardhat";
import _range from "lodash/range";
import { ERC20Mock, OracleMock, MetaMorpho, IMorpho, MetaMorphoFactory, MetaMorpho__factory, IrmMock } from "types";
import { MarketParamsStruct } from "types/@morpho-blue/interfaces/IMorpho";
import { MarketParamsStruct } from "types/src/MetaMorpho";

import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers";
import { mine } from "@nomicfoundation/hardhat-network-helpers";
Expand Down Expand Up @@ -224,10 +224,10 @@ describe("MetaMorpho", () => {

await forwardTimestamp(timelock);

await metaMorpho.connect(admin).acceptCap(identifier(idleParams));
await metaMorpho.connect(admin).acceptCap(idleParams);

for (const marketParams of allMarketParams) {
await metaMorpho.connect(admin).acceptCap(identifier(marketParams));
await metaMorpho.connect(admin).acceptCap(marketParams);
}

await metaMorpho.connect(curator).setSupplyQueue(
Expand Down
Loading