Summary
Protocol assumes that decimals of the share token during VaultRouterBranch::getVaultAssetSwapRate
and VaultRouterBranch::getIndexTokenSwapRate
is always 18. That would lead to precision loss and liquidity providers who want to redeem their collateral will take much less than their initial deposit while the rest gets stuck.
Vulnerability Details
Let's look at VaultRouterBranch::getIndexTokenSwapRate
:
function getIndexTokenSwapRate(
uint128 vaultId,
uint256 sharesIn,
bool shouldDiscountRedeemFee
)
public
view
returns (UD60x18 assetsOut)
{
...
uint8 decimalOffset = Constants.SYSTEM_DECIMALS - IERC20Metadata(vault.indexToken).decimals(); <===
uint256 previewAssetsOut = sharesIn.mulDiv(
totalAssetsMinusVaultDebt,
IERC4626(vault.indexToken).totalSupply() + 10 ** decimalOffset, <====
MathOpenZeppelin.Rounding.Floor
);
...
}
We get the decimalOffset
of our indexToken
by subtracting SYSTEM_DECIMALS
, which is 18, by indexToken
decimals that can be any given number. It can be any given number because this functions:
File::ERC4626Upgradeable
function decimals() public view virtual override(IERC20Metadata, ERC20Upgradeable) returns (uint8) {
ERC4626Storage storage $ = _getERC4626Storage();
return $._underlyingDecimals + _decimalsOffset()
}
File::ERC4626Upgradeable
function _decimalsOffset() internal view virtual returns (uint8) {
return 0;
}
Were never overwritten and so if the underlying token has 6 decimals, decimals()
of the index token will equal 6 + 0 = 6 decimals
decimalOffset
will be 12. By adding it to indexToken
total supply will equal much different number, but it's still less than what should be the correct result.
Example:
asset token decimals = 6;
indexToken decimals = asset decimals + decimalOfset = 6 + 0 =6;
sharesIn
= 20e18;
Let's consider totalSupply
= 1320e6;
totalAssetsMinusVaultDebt = 1800e18;
previewAssetOut = (20e18 * 1800e18) / (1320e6 + 1e12) = 3.5952542643710302401e28; a number way bigger than it should
Instead, if we do the math: (20e18 1800e18) / (1320e6 1e12) = 2.7272727272727272727e19 or 27 dollars per share - what should be the cost per share
verification: (20 * * 1800) / 1320 = 27.272727272727272727
Impact
LPs losing huge part of their funds forever on redeem
Tools Used
Manual
Recommendations
Multiply indexToken
total supply by decimalOffset
so it would always equal 18 decimals
function getVaultAssetSwapRate(
uint128 vaultId,
uint256 assetsIn,
bool shouldDiscountDepositFee
)
public
view
returns (UD60x18 sharesOut)
{
...
// get decimal offset
uint8 decimalOffset = Constants.SYSTEM_DECIMALS - IERC20Metadata(vault.indexToken).decimals();
// Get the shares amount out for the input amount of tokens, taking into account the unsettled debt
// See {IERC4626-previewDeposit}.
// `IERC4626(vault.indexToken).totalSupply() + 10 ** decimalOffset` could lead to problems
uint256 previewSharesOut = assetsIn.mulDiv(
- IERC4626(vault.indexToken).totalSupply() + 10 ** decimalOffset,
+ IERC4626(vault.indexToken).totalSupply() * 10 ** decimalOffset,
totalAssetsMinusVaultDebt,
MathOpenZeppelin.Rounding.Floor
);
...
}
function getIndexTokenSwapRate(
uint128 vaultId,
uint256 sharesIn,
bool shouldDiscountRedeemFee
)
public
view
returns (UD60x18 assetsOut)
{
...
// get decimal offset
uint8 decimalOffset = Constants.SYSTEM_DECIMALS - IERC20Metadata(vault.indexToken).decimals();
// Get the asset amount out for the input amount of shares, taking into account the vault's debt
// See {IERC4626-previewRedeem}
// `IERC4626(vault.indexToken).totalSupply() + 10 ** decimalOffset` could lead to problems
uint256 previewAssetsOut = sharesIn.mulDiv(
totalAssetsMinusVaultDebt,
- IERC4626(vault.indexToken).totalSupply() + 10 ** decimalOffset,
+ IERC4626(vault.indexToken).totalSupply() * 10 ** decimalOffset,
MathOpenZeppelin.Rounding.Floor
);
...
}