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

fix(reallocate): prevent slippage #298

Merged
merged 2 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 9 additions & 5 deletions src/MetaMorpho.sol
Original file line number Diff line number Diff line change
Expand Up @@ -357,11 +357,15 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
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(id, address(this));
uint256 shares;
if (allocation.assets == type(uint256).max) {
shares = MORPHO.supplyShares(id, address(this));

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

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

totalWithdrawn += withdrawnAssets;

Expand All @@ -377,7 +381,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
if (supplyCap == 0) revert ErrorsLib.UnauthorizedMarket(id);

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

if (_supplyBalance(allocation.marketParams) > supplyCap) {
revert ErrorsLib.SupplyCapExceeded(id);
Expand Down
2 changes: 0 additions & 2 deletions src/interfaces/IMetaMorpho.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ struct MarketAllocation {
MarketParams marketParams;
/// @notice The amount of assets to allocate.
uint256 assets;
/// @notice The amount of shares to allocate.
uint256 shares;
Comment on lines -34 to -35
Copy link
Contributor

Choose a reason for hiding this comment

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

We should also change the dev comment above the struct @MerlinEgalite

Copy link
Contributor

Choose a reason for hiding this comment

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

}

interface IMetaMorpho is IERC4626 {
Expand Down
37 changes: 24 additions & 13 deletions test/forge/ReallocateIdleTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,26 +31,37 @@ contract ReallocateIdleTest is IntegrationTest {
vault.deposit(INITIAL_DEPOSIT, ONBEHALF);
}

function testReallocateSupplyIdle(uint256[3] memory suppliedShares) public {
suppliedShares[0] = bound(suppliedShares[0], SharesMathLib.VIRTUAL_SHARES, CAP2 * SharesMathLib.VIRTUAL_SHARES);
suppliedShares[1] = bound(suppliedShares[1], SharesMathLib.VIRTUAL_SHARES, CAP2 * SharesMathLib.VIRTUAL_SHARES);
suppliedShares[2] = bound(suppliedShares[2], SharesMathLib.VIRTUAL_SHARES, CAP2 * SharesMathLib.VIRTUAL_SHARES);
function testReallocateSupplyIdle(uint256[3] memory suppliedAssets) public {
suppliedAssets[0] = bound(suppliedAssets[0], 1, CAP2);
suppliedAssets[1] = bound(suppliedAssets[1], 1, CAP2);
suppliedAssets[2] = bound(suppliedAssets[2], 1, CAP2);

supplied.push(MarketAllocation(allMarkets[0], 0, suppliedShares[0]));
supplied.push(MarketAllocation(allMarkets[1], 0, suppliedShares[1]));
supplied.push(MarketAllocation(allMarkets[2], 0, suppliedShares[2]));
supplied.push(MarketAllocation(allMarkets[0], suppliedAssets[0]));
supplied.push(MarketAllocation(allMarkets[1], suppliedAssets[1]));
supplied.push(MarketAllocation(allMarkets[2], suppliedAssets[2]));

uint256 idleBefore = vault.idle();

vm.prank(ALLOCATOR);
vault.reallocate(withdrawn, supplied);

assertEq(morpho.supplyShares(allMarkets[0].id(), address(vault)), suppliedShares[0], "morpho.supplyShares(0)");
assertEq(morpho.supplyShares(allMarkets[1].id(), address(vault)), suppliedShares[1], "morpho.supplyShares(1)");
assertEq(morpho.supplyShares(allMarkets[2].id(), address(vault)), suppliedShares[2], "morpho.supplyShares(2)");

uint256 expectedIdle = idleBefore - suppliedShares[0] / SharesMathLib.VIRTUAL_SHARES
- suppliedShares[1] / SharesMathLib.VIRTUAL_SHARES - suppliedShares[2] / SharesMathLib.VIRTUAL_SHARES;
assertEq(
morpho.supplyShares(allMarkets[0].id(), address(vault)),
suppliedAssets[0] * SharesMathLib.VIRTUAL_SHARES,
"morpho.supplyShares(0)"
);
assertEq(
morpho.supplyShares(allMarkets[1].id(), address(vault)),
suppliedAssets[1] * SharesMathLib.VIRTUAL_SHARES,
"morpho.supplyShares(1)"
);
assertEq(
morpho.supplyShares(allMarkets[2].id(), address(vault)),
suppliedAssets[2] * SharesMathLib.VIRTUAL_SHARES,
"morpho.supplyShares(2)"
);

uint256 expectedIdle = idleBefore - suppliedAssets[0] - suppliedAssets[1] - suppliedAssets[2];
assertApproxEqAbs(vault.idle(), expectedIdle, 3, "vault.idle() 1");
}
}
91 changes: 41 additions & 50 deletions test/forge/ReallocateWithdrawTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,10 @@ contract ReallocateWithdrawTest is IntegrationTest {
vault.deposit(INITIAL_DEPOSIT, ONBEHALF);
}

function testReallocateWithdrawAll() public {
withdrawn.push(MarketAllocation(allMarkets[0], 0, morpho.supplyShares(allMarkets[0].id(), address(vault))));
withdrawn.push(MarketAllocation(allMarkets[1], 0, morpho.supplyShares(allMarkets[1].id(), address(vault))));
withdrawn.push(MarketAllocation(allMarkets[2], 0, morpho.supplyShares(allMarkets[2].id(), address(vault))));

vm.prank(ALLOCATOR);
vault.reallocate(withdrawn, supplied);

assertEq(morpho.supplyShares(allMarkets[0].id(), address(vault)), 0, "morpho.supplyShares(0)");
assertEq(morpho.supplyShares(allMarkets[1].id(), address(vault)), 0, "morpho.supplyShares(1)");
assertEq(morpho.supplyShares(allMarkets[2].id(), address(vault)), 0, "morpho.supplyShares(2)");
assertEq(vault.idle(), INITIAL_DEPOSIT, "vault.idle() 1");
}

function testReallocateWithdrawMax() public {
withdrawn.push(MarketAllocation(allMarkets[0], 0, type(uint256).max));
withdrawn.push(MarketAllocation(allMarkets[1], 0, type(uint256).max));
withdrawn.push(MarketAllocation(allMarkets[2], 0, type(uint256).max));
withdrawn.push(MarketAllocation(allMarkets[0], type(uint256).max));
withdrawn.push(MarketAllocation(allMarkets[1], type(uint256).max));
withdrawn.push(MarketAllocation(allMarkets[2], type(uint256).max));

vm.prank(ALLOCATOR);
vault.reallocate(withdrawn, supplied);
Expand All @@ -63,48 +49,51 @@ contract ReallocateWithdrawTest is IntegrationTest {
function testReallocateWithdrawInconsistentAsset() public {
allMarkets[0].loanToken = address(1);

withdrawn.push(MarketAllocation(allMarkets[0], 0, 0));
withdrawn.push(MarketAllocation(allMarkets[0], 0));

vm.prank(ALLOCATOR);
vm.expectRevert(abi.encodeWithSelector(ErrorsLib.InconsistentAsset.selector, allMarkets[0].id()));
vault.reallocate(withdrawn, supplied);
}

function testReallocateWithdrawSupply(uint256[3] memory withdrawnShares, uint256[3] memory suppliedAssets) public {
function testReallocateWithdrawSupply(uint256[3] memory withdrawnAssets, uint256[3] memory suppliedAssets) public {
uint256[3] memory sharesBefore = [
morpho.supplyShares(allMarkets[0].id(), address(vault)),
morpho.supplyShares(allMarkets[1].id(), address(vault)),
morpho.supplyShares(allMarkets[2].id(), address(vault))
];

withdrawnShares[0] = bound(withdrawnShares[0], 0, sharesBefore[0]);
withdrawnShares[1] = bound(withdrawnShares[1], 0, sharesBefore[1]);
withdrawnShares[2] = bound(withdrawnShares[2], 0, sharesBefore[2]);

uint256[3] memory totalSupplyAssets;
uint256[3] memory totalSupplyShares;
(totalSupplyAssets[0], totalSupplyShares[0],,) = morpho.expectedMarketBalances(allMarkets[0]);
(totalSupplyAssets[1], totalSupplyShares[1],,) = morpho.expectedMarketBalances(allMarkets[1]);
(totalSupplyAssets[2], totalSupplyShares[2],,) = morpho.expectedMarketBalances(allMarkets[2]);

uint256[3] memory withdrawnAssets = [
withdrawnShares[0].toAssetsDown(totalSupplyAssets[0], totalSupplyShares[0]),
withdrawnShares[1].toAssetsDown(totalSupplyAssets[1], totalSupplyShares[1]),
withdrawnShares[2].toAssetsDown(totalSupplyAssets[2], totalSupplyShares[2])
withdrawnAssets[0] =
bound(withdrawnAssets[0], 0, sharesBefore[0].toAssetsDown(totalSupplyAssets[0], totalSupplyShares[0]));
withdrawnAssets[1] =
bound(withdrawnAssets[1], 0, sharesBefore[1].toAssetsDown(totalSupplyAssets[1], totalSupplyShares[1]));
withdrawnAssets[2] =
bound(withdrawnAssets[2], 0, sharesBefore[2].toAssetsDown(totalSupplyAssets[2], totalSupplyShares[2]));

uint256[3] memory withdrawnShares = [
withdrawnAssets[0].toSharesUp(totalSupplyAssets[0], totalSupplyShares[0]),
withdrawnAssets[1].toSharesUp(totalSupplyAssets[1], totalSupplyShares[1]),
withdrawnAssets[2].toSharesUp(totalSupplyAssets[2], totalSupplyShares[2])
];

vm.expectEmit(true, true, true, false, address(vault));

if (withdrawnShares[0] > 0) {
withdrawn.push(MarketAllocation(allMarkets[0], 0, withdrawnShares[0]));
if (withdrawnAssets[0] > 0) {
withdrawn.push(MarketAllocation(allMarkets[0], withdrawnAssets[0]));
emit EventsLib.ReallocateWithdraw(ALLOCATOR, allMarkets[0].id(), 0, 0);
}
if (withdrawnAssets[1] > 0) {
withdrawn.push(MarketAllocation(allMarkets[1], withdrawnAssets[1], 0));
withdrawn.push(MarketAllocation(allMarkets[1], withdrawnAssets[1]));
emit EventsLib.ReallocateWithdraw(ALLOCATOR, allMarkets[1].id(), 0, 0);
}
if (withdrawnShares[2] > 0) {
withdrawn.push(MarketAllocation(allMarkets[2], 0, withdrawnShares[2]));
if (withdrawnAssets[2] > 0) {
withdrawn.push(MarketAllocation(allMarkets[2], withdrawnAssets[2]));
emit EventsLib.ReallocateWithdraw(ALLOCATOR, allMarkets[2].id(), 0, 0);
}

Expand Down Expand Up @@ -133,16 +122,16 @@ contract ReallocateWithdrawTest is IntegrationTest {
suppliedAssets[2].toSharesDown(totalSupplyAssets[2], totalSupplyShares[2])
];

if (suppliedShares[0] > 0) {
supplied.push(MarketAllocation(allMarkets[0], suppliedAssets[0], 0));
if (suppliedAssets[0] > 0) {
supplied.push(MarketAllocation(allMarkets[0], suppliedAssets[0]));
emit EventsLib.ReallocateSupply(ALLOCATOR, allMarkets[0].id(), 0, 0);
}
if (suppliedAssets[1] > 0) {
supplied.push(MarketAllocation(allMarkets[1], 0, suppliedShares[1]));
supplied.push(MarketAllocation(allMarkets[1], suppliedAssets[1]));
emit EventsLib.ReallocateSupply(ALLOCATOR, allMarkets[1].id(), 0, 0);
}
if (suppliedShares[2] > 0) {
supplied.push(MarketAllocation(allMarkets[2], suppliedAssets[2], 0));
if (suppliedAssets[2] > 0) {
supplied.push(MarketAllocation(allMarkets[2], suppliedAssets[2]));
emit EventsLib.ReallocateSupply(ALLOCATOR, allMarkets[2].id(), 0, 0);
}

Expand Down Expand Up @@ -170,30 +159,32 @@ contract ReallocateWithdrawTest is IntegrationTest {
assertApproxEqAbs(vault.idle(), expectedIdle, 1, "vault.idle() 1");
}

function testReallocateUnauthorizedMarket(uint256 amount) public {
amount = bound(amount, 1, CAP2);
function testReallocateUnauthorizedMarket(uint256[3] memory suppliedAssets) public {
suppliedAssets[0] = bound(suppliedAssets[0], 1, CAP2);
suppliedAssets[1] = bound(suppliedAssets[1], 1, CAP2);
suppliedAssets[2] = bound(suppliedAssets[2], 1, CAP2);

_setCap(allMarkets[1], 0);

withdrawn.push(MarketAllocation(allMarkets[0], 0, type(uint256).max));
withdrawn.push(MarketAllocation(allMarkets[1], 0, type(uint256).max));
withdrawn.push(MarketAllocation(allMarkets[2], 0, type(uint256).max));
withdrawn.push(MarketAllocation(allMarkets[0], type(uint256).max));
withdrawn.push(MarketAllocation(allMarkets[1], type(uint256).max));
withdrawn.push(MarketAllocation(allMarkets[2], type(uint256).max));

supplied.push(MarketAllocation(allMarkets[0], amount, 0));
supplied.push(MarketAllocation(allMarkets[1], amount, 0));
supplied.push(MarketAllocation(allMarkets[2], amount, 0));
supplied.push(MarketAllocation(allMarkets[0], suppliedAssets[0]));
supplied.push(MarketAllocation(allMarkets[1], suppliedAssets[1]));
supplied.push(MarketAllocation(allMarkets[2], suppliedAssets[2]));

vm.prank(ALLOCATOR);
vm.expectRevert(abi.encodeWithSelector(ErrorsLib.UnauthorizedMarket.selector, allMarkets[1].id()));
vault.reallocate(withdrawn, supplied);
}

function testReallocateSupplyCapExceeded() public {
withdrawn.push(MarketAllocation(allMarkets[0], 0, type(uint256).max));
withdrawn.push(MarketAllocation(allMarkets[1], 0, type(uint256).max));
withdrawn.push(MarketAllocation(allMarkets[2], 0, type(uint256).max));
withdrawn.push(MarketAllocation(allMarkets[0], type(uint256).max));
withdrawn.push(MarketAllocation(allMarkets[1], type(uint256).max));
withdrawn.push(MarketAllocation(allMarkets[2], type(uint256).max));

supplied.push(MarketAllocation(allMarkets[0], CAP2 + 1, 0));
supplied.push(MarketAllocation(allMarkets[0], CAP2 + 1));

vm.prank(ALLOCATOR);
vm.expectRevert(abi.encodeWithSelector(ErrorsLib.SupplyCapExceeded.selector, allMarkets[0].id()));
Expand All @@ -210,7 +201,7 @@ contract ReallocateWithdrawTest is IntegrationTest {

_setCap(allMarkets[0], type(uint192).max);

supplied.push(MarketAllocation(allMarkets[0], CAP2 + rewards, 0));
supplied.push(MarketAllocation(allMarkets[0], CAP2 + rewards));

vm.prank(ALLOCATOR);
vm.expectRevert(ErrorsLib.InsufficientIdle.selector);
Expand Down
17 changes: 6 additions & 11 deletions test/hardhat/MetaMorpho.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,29 +250,25 @@ describe("MetaMorpho", () => {
const market = await expectedMarket(marketParams);
const position = await morpho.position(identifier(marketParams), metaMorphoAddress);

const liquidity = market.totalSupplyAssets - market.totalBorrowAssets;
const liquidityShares = liquidity.toSharesDown(market.totalSupplyAssets, market.totalSupplyShares);

return {
marketParams,
market,
liquidShares: position.supplyShares.min(liquidityShares),
liquidity: market.totalSupplyAssets - market.totalBorrowAssets,
supplyAssets: position.supplyShares.toAssetsDown(market.totalSupplyAssets, market.totalSupplyShares),
};
}),
);

const withdrawn = allocation
.map(({ marketParams, liquidShares }) => ({
.map(({ marketParams, liquidity, supplyAssets }) => ({
marketParams,
assets: 0n,
// Always withdraw all, up to the liquidity.
shares: liquidShares,
assets: liquidity > supplyAssets ? MaxUint256 : liquidity,
}))
.filter(({ shares }) => shares > 0n);
.filter(({ assets }) => assets > 0n);

const withdrawnAssets = allocation.reduce(
(total, { market, liquidShares }) =>
total + liquidShares.toAssetsDown(market.totalSupplyAssets, market.totalSupplyShares),
(total, { supplyAssets, liquidity }) => total + supplyAssets.min(liquidity),
0n,
);

Expand All @@ -284,7 +280,6 @@ describe("MetaMorpho", () => {
marketParams,
// Always supply evenly on each market 90% of what the vault withdrawn in total.
assets: marketAssets,
shares: 0n,
}))
.filter(({ assets }) => assets > 0n);

Expand Down