Part 2

Zaros
PerpetualsDEXFoundrySolidity
70,000 USDC
View results
Submission Details
Severity: high
Invalid

Decimals offset is inconsistent causing the user to redeem lesser tokens

Summary

The virtual offset is used to prevent inflation attack. Zlpvault has a custom offset within their contract. The implementation between Zlpvault and the VaultRouterBranch is inconsistent which can cause users to redeem lesser asset tokens.

Vulnerability Details

When calling VaultRouterBranch::redeem, a specified amount of index tokens is exchanged for collateral assets from the given vault. The calculation of the exchanged tokens can be influenced (inflated) if the decimalOffset differs. When determining the swap rates using previewAssetsOut and previewSharesOut, the decimalOffset is factored into the calculations using the IERC20 interface.

For example, if the initialized decimalOffset is greater than 0—as in the case of ZlpVault, where is can be initizlied to any number [9 is the safest](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3979)—the ERC4626Upgradable::decimals method incorporates this offset using _underlyingDecimals + _decimalsOffset(). Consequently, when users call VaultRouterBranch::redeem, the assets and shares they receive are reduced due to the initialized decimalOffset in ZlpVault. This discrepancy in decimal offset precision is significant because users depositing tokens will consistently receive fewer tokens upon redemption.

Within the VaultRouterBranch.sol :

function getIndexTokenSwapRate(
uint128 vaultId,
uint256 sharesIn,
bool shouldDiscountRedeemFee
)
public
view
returns (UD60x18 assetsOut)
{
-- SNIP --
// get decimal offset //@audit-issue
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
// @audit offset is not using ZLP offset
uint256 previewAssetsOut = sharesIn.mulDiv(
totalAssetsMinusVaultDebt,
IERC4626(vault.indexToken).totalSupply() + 10 ** decimalOffset,
MathOpenZeppelin.Rounding.Floor
);
-- SNIP --
function getVaultAssetSwapRate(
uint128 vaultId,
uint256 assetsIn,
bool shouldDiscountDepositFee
)
public
view
returns (UD60x18 sharesOut)
{
// fetch storage slot for vault by id
Vault.Data storage vault = Vault.loadExisting(vaultId);
// get the vault's net credit capacity, i.e its total assets usd value minus its total debt (or adding its
// credit if debt is negative)
uint256 totalAssetsMinusVaultDebt = getVaultCreditCapacity(vaultId);
// get decimal offset //@audit-issue
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,
totalAssetsMinusVaultDebt,
MathOpenZeppelin.Rounding.Floor
);

Within Zlpvault.sol:

/// @notice Returns the decimals offset between the ZLP Vault's underlying asset and its shares (index tokens).
/// @dev Overridden and used in ERC4626.
function _decimalsOffset() internal view override returns (uint8 offset) {
offset = _getZlpVaultStorage().decimalsOffset;
}

Within the ERC4626Upgradable.sol :

/**
* @dev Decimals are computed by adding the decimal offset on top of the underlying asset's decimals. This
* "original" value is cached during construction of the vault contract. If this read operation fails (e.g., the
* asset has not been created yet), a default of 18 is used to represent the underlying asset's decimals.
*
* See {IERC20Metadata-decimals}.
*/
function decimals() public view virtual override(IERC20Metadata, ERC20Upgradeable) returns (uint8) {
ERC4626Storage storage $ = _getERC4626Storage();
return $._underlyingDecimals + _decimalsOffset();
}

Impact

User redeems lesser assets tokens leading to a loss.

Tools Used

Manual Review

Recommendations

Change the implementation uint8 decimalOffset = Constants.SYSTEM_DECIMALS - IERC20Metadata(vault.indexToken).decimals() into Zlpvault's uint8 decimalOffset = ZLPVAULT.decimals() - IERC20Metadata(vault.indexToken).decimals() . This ensures that the decimalOffset is consistent through out Market Engine and vaults.

Updates

Lead Judging Commences

inallhonesty Lead Judge
7 months ago
inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.