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

Test conversion rate with fee #196

Merged
merged 11 commits into from
Oct 18, 2023
16 changes: 7 additions & 9 deletions src/MetaMorpho.sol
Original file line number Diff line number Diff line change
Expand Up @@ -481,8 +481,6 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph

shares = _convertToSharesWithFeeAccrued(assets, totalSupply(), newTotalAssets, Math.Rounding.Floor);
_deposit(_msgSender(), receiver, assets, shares);

_updateLastTotalAssets(newTotalAssets + assets);
}

/// @inheritdoc IERC4626
Expand All @@ -491,8 +489,6 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph

assets = _convertToAssetsWithFeeAccrued(shares, totalSupply(), newTotalAssets, Math.Rounding.Ceil);
_deposit(_msgSender(), receiver, assets, shares);

_updateLastTotalAssets(newTotalAssets + assets);
}

/// @inheritdoc IERC4626
Expand All @@ -507,8 +503,6 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph

shares = _convertToSharesWithFeeAccrued(assets, totalSupply(), newTotalAssets, Math.Rounding.Ceil);
_withdraw(_msgSender(), receiver, owner, assets, shares);

_updateLastTotalAssets(newTotalAssets - assets);
}

/// @inheritdoc IERC4626
Expand All @@ -523,8 +517,6 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph

assets = _convertToAssetsWithFeeAccrued(shares, totalSupply(), newTotalAssets, Math.Rounding.Floor);
_withdraw(_msgSender(), receiver, owner, assets, shares);

_updateLastTotalAssets(newTotalAssets - assets);
}

/// @inheritdoc IERC4626
Expand Down Expand Up @@ -569,7 +561,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
}

/// @inheritdoc ERC4626
/// @dev The accrual of fees is taken into account in the conversion.
/// @dev The accrual of performance fees is taken into account in the conversion.
function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view override returns (uint256) {
(uint256 feeShares, uint256 newTotalAssets) = _accruedFeeShares();

Expand Down Expand Up @@ -607,6 +599,9 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
super._deposit(caller, owner, assets, shares);

_supplyMorpho(assets);

// `newTotalAssets + assets` cannot be used as input because of rounding errors so we must use `totalAssets`.
_updateLastTotalAssets(totalAssets());
}

/// @inheritdoc ERC4626
Expand All @@ -624,6 +619,9 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
if (_withdrawMorpho(assets) != 0) revert ErrorsLib.WithdrawMorphoFailed();

super._withdraw(caller, receiver, owner, assets, shares);

// `newTotalAssets - assets` cannot be used as input because of rounding errors so we must use `totalAssets`.
_updateLastTotalAssets(totalAssets());
}

/* INTERNAL */
Expand Down
75 changes: 62 additions & 13 deletions test/forge/FeeTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,17 @@ contract FeeTest is BaseTest {

// Create some debt on the market to accrue interest.

loanToken.setBalance(SUPPLIER, 1);
loanToken.setBalance(SUPPLIER, MAX_TEST_ASSETS);

vm.prank(SUPPLIER);
morpho.supply(marketParams, 1, 0, ONBEHALF, hex"");
morpho.supply(marketParams, MAX_TEST_ASSETS, 0, ONBEHALF, hex"");

uint256 collateral = uint256(1).wDivUp(marketParams.lltv);
uint256 collateral = uint256(MAX_TEST_ASSETS).wDivUp(marketParams.lltv);
collateralToken.setBalance(BORROWER, collateral);

vm.startPrank(BORROWER);
morpho.supplyCollateral(marketParams, collateral, BORROWER, hex"");
morpho.borrow(marketParams, 1, 0, BORROWER, BORROWER);
morpho.borrow(marketParams, MAX_TEST_ASSETS, 0, BORROWER, BORROWER);
vm.stopPrank();
}

Expand All @@ -43,10 +43,10 @@ contract FeeTest is BaseTest {
function _feeShares(uint256 totalAssetsBefore) internal view returns (uint256) {
uint256 totalAssetsAfter = vault.totalAssets();
uint256 interest = totalAssetsAfter - totalAssetsBefore;
uint256 feeAmount = interest.wMulDown(FEE);
uint256 feeAssets = interest.mulDiv(FEE, WAD);

return feeAmount.mulDiv(
vault.totalSupply() + 10 ** DECIMALS_OFFSET, totalAssetsAfter - feeAmount + 1, Math.Rounding.Floor
return feeAssets.mulDiv(
vault.totalSupply() + 10 ** DECIMALS_OFFSET, totalAssetsAfter - feeAssets + 1, Math.Rounding.Floor
);
}

Expand All @@ -62,8 +62,9 @@ contract FeeTest is BaseTest {
}

function testAccrueFeeWithinABlock(uint256 deposited, uint256 withdrawn) public {
deposited = bound(deposited, MIN_TEST_ASSETS, MAX_TEST_ASSETS);
withdrawn = bound(withdrawn, MIN_TEST_ASSETS, deposited);
deposited = bound(deposited, MIN_TEST_ASSETS + 1, MAX_TEST_ASSETS);
// The deposited amount is rounded down on Morpho and thus cannot be withdrawn in a block in most cases.
withdrawn = bound(withdrawn, MIN_TEST_ASSETS, deposited - 1);

loanToken.setBalance(SUPPLIER, deposited);

Expand All @@ -73,7 +74,7 @@ contract FeeTest is BaseTest {
vm.prank(ONBEHALF);
vault.withdraw(withdrawn, RECEIVER, ONBEHALF);

assertEq(vault.balanceOf(FEE_RECIPIENT), 0, "vault.balanceOf(FEE_RECIPIENT)");
assertApproxEqAbs(vault.balanceOf(FEE_RECIPIENT), 0, 1, "vault.balanceOf(FEE_RECIPIENT)");
}

function testDepositAccrueFee(uint256 deposited, uint256 newDeposit, uint256 blocks) public {
Expand Down Expand Up @@ -178,11 +179,9 @@ contract FeeTest is BaseTest {

function testSetFeeAccrueFee(uint256 deposited, uint256 fee, uint256 blocks) public {
deposited = bound(deposited, MIN_TEST_ASSETS, MAX_TEST_ASSETS);
fee = bound(fee, 0, MAX_FEE);
fee = bound(fee, 0, FEE - 1);
blocks = _boundBlocks(blocks);

vm.assume(fee != FEE);

loanToken.setBalance(SUPPLIER, deposited);

vm.prank(SUPPLIER);
Expand Down Expand Up @@ -253,4 +252,54 @@ contract FeeTest is BaseTest {
vm.expectRevert(ErrorsLib.ZeroFeeRecipient.selector);
vault.setFeeRecipient(address(0));
}

function testConvertToAssetsWithFeeAndInterest(uint256 deposited, uint256 assets, uint256 blocks) public {
deposited = bound(deposited, MIN_TEST_ASSETS, MAX_TEST_ASSETS);
assets = bound(assets, 1, MAX_TEST_ASSETS);
blocks = _boundBlocks(blocks);

loanToken.setBalance(SUPPLIER, deposited);

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

uint256 totalAssetsBefore = vault.totalAssets();
uint256 sharesBefore = vault.convertToShares(assets);

_forward(blocks);

uint256 feeShares = _feeShares(totalAssetsBefore);
uint256 expectedShares = assets.mulDiv(
vault.totalSupply() + feeShares + 10 ** DECIMALS_OFFSET, vault.totalAssets() + 1, Math.Rounding.Floor
);
uint256 shares = vault.convertToShares(assets);

assertEq(shares, expectedShares, "shares");
assertLt(shares, sharesBefore, "shares decreased");
}

function testConvertToSharesWithFeeAndInterest(uint256 deposited, uint256 shares, uint256 blocks) public {
deposited = bound(deposited, MIN_TEST_ASSETS, MAX_TEST_ASSETS);
shares = bound(shares, 10 ** DECIMALS_OFFSET, MAX_TEST_ASSETS);
blocks = _boundBlocks(blocks);

loanToken.setBalance(SUPPLIER, deposited);

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

uint256 totalAssetsBefore = vault.totalAssets();
uint256 assetsBefore = vault.convertToAssets(shares);

_forward(blocks);

uint256 feeShares = _feeShares(totalAssetsBefore);
uint256 expectedAssets = shares.mulDiv(
vault.totalAssets() + 1, vault.totalSupply() + feeShares + 10 ** DECIMALS_OFFSET, Math.Rounding.Floor
);
uint256 assets = vault.convertToAssets(shares);

assertEq(assets, expectedAssets, "assets");
assertGe(assets, assetsBefore, "assets increased");
}
}
2 changes: 1 addition & 1 deletion test/forge/helpers/BaseTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ contract BaseTest is Test {

/// @dev Bounds the fuzzing input to a realistic number of blocks.
function _boundBlocks(uint256 blocks) internal view returns (uint256) {
return bound(blocks, 1, type(uint24).max);
return bound(blocks, 2, type(uint24).max);
Rubilmax marked this conversation as resolved.
Show resolved Hide resolved
}

/// @dev Bounds the fuzzing input to a non-zero address.
Expand Down
Loading