Skip to content
2 changes: 1 addition & 1 deletion src/generated/ERC4626Words.pointers.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pragma solidity ^0.8.25;
// file needs the contract to exist so that it can be compiled.

/// @dev Hash of the known bytecode.
bytes32 constant BYTECODE_HASH = bytes32(0x9905f7f79bbbbea0cde049efb56d1aed955479c30db2ee9b6eeae543e3b33bfb);
bytes32 constant BYTECODE_HASH = bytes32(0x742585ed2b4e04325a26604e90eabd1cbe7552a83e9ccec3d894bfce1ae1fcbf);

/// @dev The hash of the meta that describes the contract.
bytes32 constant DESCRIBED_BY_META_HASH = bytes32(0xdde475220ea06da58884f3f7abbfc091637e02cb924b8b9a8a9d20dbd4ae703a);
Expand Down
16 changes: 15 additions & 1 deletion src/lib/erc4626/LibERC4626.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ interface IERC20MetadataMinimal {
function decimals() external view returns (uint8);
}

/// @dev Upper bound on decimals accepted from vault and asset contracts. Values
/// above this would cause toFixedDecimalLossless to overflow uint256 for any
/// non-trivial Float, enabling denial-of-service by a malicious vault.
uint8 constant MAX_DECIMALS = 36;

/// Thrown when a vault or asset reports more decimals than MAX_DECIMALS.
/// @param token The vault or asset address that reported the excessive decimals.
/// @param decimals The reported decimals value.
error UnsupportedDecimals(address token, uint8 decimals);

/// @title LibERC4626
/// @notice Core library for interacting with ERC-4626 tokenised vaults on-chain.
/// Takes the vault as a typed contract reference and handles conversion of the
Expand All @@ -28,12 +38,16 @@ library LibERC4626 {
/// Reads the share and underlying-asset decimal scales from the vault:
/// vault.decimals() then vault.asset() then assetToken.decimals(), giving
/// both conversion functions a single, symmetric read path.
/// Reverts with UnsupportedDecimals if either reported value exceeds MAX_DECIMALS.
/// @param vault The ERC-4626 vault contract.
/// @return shareDecimals The decimal precision of the vault share token.
/// @return assetDecimals The decimal precision of the underlying asset token.
function _vaultScales(IERC4626Minimal vault) private view returns (uint8 shareDecimals, uint8 assetDecimals) {
shareDecimals = vault.decimals();
assetDecimals = IERC20MetadataMinimal(vault.asset()).decimals();
if (shareDecimals > MAX_DECIMALS) revert UnsupportedDecimals(address(vault), shareDecimals);
address assetAddr = vault.asset();
assetDecimals = IERC20MetadataMinimal(assetAddr).decimals();
if (assetDecimals > MAX_DECIMALS) revert UnsupportedDecimals(assetAddr, assetDecimals);
}

/// @notice Converts vault shares to underlying assets via ERC-4626 convertToAssets.
Expand Down
45 changes: 44 additions & 1 deletion test/src/lib/erc4626/LibERC4626.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
pragma solidity =0.8.25;

import {Test} from "forge-std-1.16.1/src/Test.sol";
import {LibERC4626, IERC4626Minimal} from "../../../../src/lib/erc4626/LibERC4626.sol";
import {LibERC4626, IERC4626Minimal, UnsupportedDecimals} from "../../../../src/lib/erc4626/LibERC4626.sol";
import {LibDecimalFloat, Float} from "rain-math-float-0.1.1/src/lib/LibDecimalFloat.sol";
import {MockERC4626} from "../../../utils/MockERC4626.sol";
import {MockERC20} from "../../../utils/MockERC20.sol";
import {MaliciousERC4626} from "../../../utils/MaliciousERC4626.sol";

contract LibERC4626Test is Test {
MockERC20 internal asset;
Expand Down Expand Up @@ -196,6 +197,48 @@ contract LibERC4626Test is Test {
assertEq(LibDecimalFloat.toFixedDecimalLossless(sharesFloat, 18), 0, "0 assets must yield 0 shares");
}

/// A vault reporting decimals() > MAX_DECIMALS must revert UnsupportedDecimals
/// for convertToAssets. Mutation: remove the shareDecimals guard → expectRevert fails.
function testShareDecimalsAboveMaxRevertsConvertToAssets() external {
MaliciousERC4626 malVault = new MaliciousERC4626(255, address(asset));
Float sharesFloat = LibDecimalFloat.packLossless(1, 0);

vm.expectRevert(abi.encodeWithSelector(UnsupportedDecimals.selector, address(malVault), uint8(255)));
this._convertToAssets(IERC4626Minimal(address(malVault)), sharesFloat);
}

/// A vault reporting decimals() > MAX_DECIMALS must revert UnsupportedDecimals
/// for convertToShares. Mutation: remove the shareDecimals guard → expectRevert fails.
function testShareDecimalsAboveMaxRevertsConvertToShares() external {
MaliciousERC4626 malVault = new MaliciousERC4626(255, address(asset));
Float assetsFloat = LibDecimalFloat.packLossless(1, 0);

vm.expectRevert(abi.encodeWithSelector(UnsupportedDecimals.selector, address(malVault), uint8(255)));
this._convertToShares(IERC4626Minimal(address(malVault)), assetsFloat);
}

/// An asset reporting decimals() > MAX_DECIMALS must revert UnsupportedDecimals
/// for convertToAssets. Mutation: remove the assetDecimals guard → expectRevert fails.
function testAssetDecimalsAboveMaxRevertsConvertToAssets() external {
MockERC20 malAsset = new MockERC20(255);
MockERC4626 normalVault = new MockERC4626(18, address(malAsset), 1e18);
Float sharesFloat = LibDecimalFloat.packLossless(1, 0);

vm.expectRevert(abi.encodeWithSelector(UnsupportedDecimals.selector, address(malAsset), uint8(255)));
this._convertToAssets(IERC4626Minimal(address(normalVault)), sharesFloat);
}

/// An asset reporting decimals() > MAX_DECIMALS must revert UnsupportedDecimals
/// for convertToShares. Mutation: remove the assetDecimals guard → expectRevert fails.
function testAssetDecimalsAboveMaxRevertsConvertToShares() external {
MockERC20 malAsset = new MockERC20(255);
MockERC4626 normalVault = new MockERC4626(18, address(malAsset), 1e18);
Float assetsFloat = LibDecimalFloat.packLossless(1, 0);

vm.expectRevert(abi.encodeWithSelector(UnsupportedDecimals.selector, address(malAsset), uint8(255)));
this._convertToShares(IERC4626Minimal(address(normalVault)), assetsFloat);
}

function testConvertToAssetsLargeInput() external view {
IERC4626Minimal typedVault = IERC4626Minimal(address(vault));
// 1e9 whole shares: well within int64 range, exercises large fixed-point packing.
Expand Down
Loading