diff --git a/.github/workflows/foundry.yml b/.github/workflows/foundry.yml index 71134732..767033c6 100644 --- a/.github/workflows/foundry.yml +++ b/.github/workflows/foundry.yml @@ -82,7 +82,7 @@ jobs: - uses: ./.github/actions/install-cache - name: Run tests in ${{ matrix.type }} mode - run: yarn test:forge + run: yarn test:forge -vvv env: FOUNDRY_FUZZ_RUNS: ${{ matrix.fuzz-runs }} FOUNDRY_FUZZ_MAX_TEST_REJECTS: ${{ matrix.max-test-rejects }} diff --git a/src/MetaMorpho.sol b/src/MetaMorpho.sol index aec32622..ea9285b0 100644 --- a/src/MetaMorpho.sol +++ b/src/MetaMorpho.sol @@ -58,6 +58,11 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph /// @notice The address of the Morpho contract. IMorpho public immutable MORPHO; + /// @notice OpenZeppelin decimals offset used by the ERC4626 implementation. + /// @dev Calculated to be max(0, 18 - underlyingDecimals) at construction, so the initial conversion rate maximizes + /// precision between shares and assets. + uint8 public immutable DECIMALS_OFFSET; + /* STORAGE */ /// @notice The address of the curator. @@ -126,6 +131,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph if (morpho == address(0)) revert ErrorsLib.ZeroAddress(); MORPHO = IMorpho(morpho); + DECIMALS_OFFSET = uint8(uint256(18).zeroFloorSub(IERC20Metadata(_asset).decimals())); _checkTimelockBounds(initialTimelock); _setTimelock(initialTimelock); @@ -619,8 +625,8 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph /* ERC4626 (INTERNAL) */ /// @inheritdoc ERC4626 - function _decimalsOffset() internal pure override returns (uint8) { - return ConstantsLib.DECIMALS_OFFSET; + function _decimalsOffset() internal view override returns (uint8) { + return DECIMALS_OFFSET; } /// @dev Returns the maximum amount of asset (`assets`) that the `owner` can withdraw from the vault, as well as the @@ -675,7 +681,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph uint256 newTotalSupply, uint256 newTotalAssets, Math.Rounding rounding - ) internal pure returns (uint256) { + ) internal view returns (uint256) { return assets.mulDiv(newTotalSupply + 10 ** _decimalsOffset(), newTotalAssets + 1, rounding); } @@ -686,7 +692,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph uint256 newTotalSupply, uint256 newTotalAssets, Math.Rounding rounding - ) internal pure returns (uint256) { + ) internal view returns (uint256) { return shares.mulDiv(newTotalAssets + 1, newTotalSupply + 10 ** _decimalsOffset(), rounding); } diff --git a/src/interfaces/IMetaMorpho.sol b/src/interfaces/IMetaMorpho.sol index f92527ac..e3f129f2 100644 --- a/src/interfaces/IMetaMorpho.sol +++ b/src/interfaces/IMetaMorpho.sol @@ -31,6 +31,7 @@ interface IOwnable { /// @dev Consider using the IMetaMorpho interface instead of this one. interface IMetaMorphoBase { function MORPHO() external view returns (IMorpho); + function DECIMALS_OFFSET() external view returns (uint8); function curator() external view returns (address); function isAllocator(address target) external view returns (bool); diff --git a/src/libraries/ConstantsLib.sol b/src/libraries/ConstantsLib.sol index 8a77118d..8d0d1639 100644 --- a/src/libraries/ConstantsLib.sol +++ b/src/libraries/ConstantsLib.sol @@ -12,9 +12,6 @@ library ConstantsLib { /// @dev The minimum delay of a timelock. uint256 internal constant MIN_TIMELOCK = 1 days; - /// @dev OpenZeppelin's decimals offset used in MetaMorpho's ERC4626 implementation. - uint8 internal constant DECIMALS_OFFSET = 6; - /// @dev The maximum number of markets in the supply/withdraw queue. uint256 internal constant MAX_QUEUE_LENGTH = 30; diff --git a/test/forge/ERC4626Test.sol b/test/forge/ERC4626Test.sol index 6400d127..cec46418 100644 --- a/test/forge/ERC4626Test.sol +++ b/test/forge/ERC4626Test.sol @@ -17,8 +17,14 @@ contract ERC4626Test is IntegrationTest, IMorphoFlashLoanCallback { _sortSupplyQueueIdleLast(); } - function testDecimals() public { - assertEq(vault.decimals(), loanToken.decimals() + ConstantsLib.DECIMALS_OFFSET, "decimals"); + function testDecimals(uint8 decimals) public { + vm.mockCall(address(loanToken), abi.encodeWithSignature("decimals()"), abi.encode(decimals)); + + vault = IMetaMorpho( + address(new MetaMorpho(OWNER, address(morpho), TIMELOCK, address(loanToken), "MetaMorpho Vault", "MMV")) + ); + + assertEq(vault.decimals(), Math.max(18, decimals), "decimals"); } function testMint(uint256 assets) public { @@ -116,16 +122,18 @@ contract ERC4626Test is IntegrationTest, IMorphoFlashLoanCallback { function testRedeemTooMuch(uint256 deposited) public { deposited = bound(deposited, MIN_TEST_ASSETS, MAX_TEST_ASSETS); - loanToken.setBalance(SUPPLIER, deposited); + loanToken.setBalance(SUPPLIER, deposited * 2); - vm.prank(SUPPLIER); - uint256 shares = vault.deposit(deposited, ONBEHALF); + vm.startPrank(SUPPLIER); + uint256 shares = vault.deposit(deposited, SUPPLIER); + vault.deposit(deposited, ONBEHALF); + vm.stopPrank(); - vm.prank(ONBEHALF); + vm.prank(SUPPLIER); vm.expectRevert( - abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, ONBEHALF, shares, shares + 1) + abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, SUPPLIER, shares, shares + 1) ); - vault.redeem(shares + 1, RECEIVER, ONBEHALF); + vault.redeem(shares + 1, RECEIVER, SUPPLIER); } function testWithdrawAll(uint256 assets) public { @@ -270,7 +278,7 @@ contract ERC4626Test is IntegrationTest, IMorphoFlashLoanCallback { vm.prank(SUPPLIER); vault.deposit(deposited, ONBEHALF); - assets = bound(assets, deposited + 1, type(uint256).max / (deposited + 10 ** ConstantsLib.DECIMALS_OFFSET)); + assets = bound(assets, deposited + 1, type(uint256).max / (deposited + 1)); vm.prank(ONBEHALF); vm.expectRevert(ErrorsLib.NotEnoughLiquidity.selector); @@ -285,7 +293,7 @@ contract ERC4626Test is IntegrationTest, IMorphoFlashLoanCallback { vm.prank(SUPPLIER); vault.deposit(deposited, ONBEHALF); - assets = bound(assets, deposited + 1, type(uint256).max / (deposited + 10 ** ConstantsLib.DECIMALS_OFFSET)); + assets = bound(assets, deposited + 1, type(uint256).max / (deposited + 1)); collateralToken.setBalance(BORROWER, type(uint128).max); diff --git a/test/forge/FeeTest.sol b/test/forge/FeeTest.sol index ad51a21f..7d9f6010 100644 --- a/test/forge/FeeTest.sol +++ b/test/forge/FeeTest.sol @@ -55,11 +55,7 @@ contract FeeTest is IntegrationTest { uint256 interest = totalAssetsAfter - vault.lastTotalAssets(); uint256 feeAssets = interest.mulDiv(FEE, WAD); - return feeAssets.mulDiv( - vault.totalSupply() + 10 ** ConstantsLib.DECIMALS_OFFSET, - totalAssetsAfter - feeAssets + 1, - Math.Rounding.Floor - ); + return feeAssets.mulDiv(vault.totalSupply() + 1, totalAssetsAfter - feeAssets + 1, Math.Rounding.Floor); } function testAccrueFeeWithinABlock(uint256 deposited, uint256 withdrawn) public { @@ -312,11 +308,8 @@ contract FeeTest is IntegrationTest { _forward(blocks); uint256 feeShares = _feeShares(); - uint256 expectedShares = assets.mulDiv( - vault.totalSupply() + feeShares + 10 ** ConstantsLib.DECIMALS_OFFSET, - vault.totalAssets() + 1, - Math.Rounding.Floor - ); + uint256 expectedShares = + assets.mulDiv(vault.totalSupply() + feeShares + 1, vault.totalAssets() + 1, Math.Rounding.Floor); uint256 shares = vault.convertToShares(assets); assertEq(shares, expectedShares, "shares"); @@ -325,7 +318,7 @@ contract FeeTest is IntegrationTest { function testConvertToSharesWithFeeAndInterest(uint256 deposited, uint256 shares, uint256 blocks) public { deposited = bound(deposited, MIN_TEST_ASSETS, MAX_TEST_ASSETS); - shares = bound(shares, 10 ** ConstantsLib.DECIMALS_OFFSET, MAX_TEST_ASSETS); + shares = bound(shares, 1, MAX_TEST_ASSETS); blocks = _boundBlocks(blocks); loanToken.setBalance(SUPPLIER, deposited); @@ -338,11 +331,8 @@ contract FeeTest is IntegrationTest { _forward(blocks); uint256 feeShares = _feeShares(); - uint256 expectedAssets = shares.mulDiv( - vault.totalAssets() + 1, - vault.totalSupply() + feeShares + 10 ** ConstantsLib.DECIMALS_OFFSET, - Math.Rounding.Floor - ); + uint256 expectedAssets = + shares.mulDiv(vault.totalAssets() + 1, vault.totalSupply() + feeShares + 1, Math.Rounding.Floor); uint256 assets = vault.convertToAssets(shares); assertEq(assets, expectedAssets, "assets");