Part 2

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

Attacker can force other user unable to stake by breaking rate between share and asset

Vulnerability Details

In VaultRouterBranch#deposit() function, there is no minimum deposit amount, letting user deposit any amount if they want.
But in stake() function, staking amount must be greater than minimum stake amount (1e5):

function stake(uint128 vaultId, uint128 shares) external {
// to prevent safe cast overflow errors
if (shares < Constants.MIN_OF_SHARES_TO_STAKE) {
revert Errors.QuantityOfSharesLessThanTheMinimumAllowed(Constants.MIN_OF_SHARES_TO_STAKE, uint256(shares));
}

When we deposit token to Zlp vault, we will receive shares back. In ZlpVault, to calculate share value that will receive, it use _convertToShares() function, that call getVaultAssetSwapRate() function:

function _convertToShares(
uint256 assets,
Math.Rounding
)
/*
*/
internal
view
override
returns (uint256)
{
// load erc-7201 storage pointer
ZlpVaultStorage storage zlpVaultStorage = _getZlpVaultStorage();
// fetch the amount of shares out for the assets input value calling the `MarketMakingEngine`
UD60x18 sharesOut = IMarketMakingEngine(zlpVaultStorage.marketMakingEngine).getVaultAssetSwapRate(
zlpVaultStorage.vaultId, assets, false
);
return sharesOut.intoUint256();
}

And function getVaultAssetSwapRate() rely on totalAssets()of the vault:

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
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
);
if (shouldDiscountDepositFee) {
previewSharesOut =
ud60x18(previewSharesOut).sub(ud60x18(previewSharesOut).mul(ud60x18(vault.depositFee))).intoUint256();
}
// Return the final adjusted amountOut as UD60x18
return ud60x18(previewSharesOut);
}

Function getVaultCreditCapacity():

function getVaultCreditCapacity(uint128 vaultId) public view returns (uint256) {
// fetch storage slot for vault by id
Vault.Data storage vault = Vault.loadExisting(vaultId);
// fetch the vault's total assets in 18 dec
SD59x18 totalAssetsX18 =
vault.collateral.convertTokenAmountToSd59x18(IERC4626(vault.indexToken).totalAssets().toInt256()); // <--
. . . . .
}

totalAssets()function in ERC4626Upgradeable return balanceOf contract:

function totalAssets() public view virtual returns (uint256) {
return IERC20(asset()).balanceOf(address(this));
}

It is possible for attacker to force other user to not able to staking by increasing share price:

  • After vault initalized, attacker deposit dust amount, receive dust amount back

  • After that, attacker transfer some tokens directly to ZlpVault, increasing value per shares alot

After that, most of the user unable to stake, because to be able to do that, deposit amount must be very huge

Impact

Many users cant staking due to number of token required to staking is too high

Recommendations

Do not rely on balance of vault when calculating share, instead tracking them whenever deposit and withdraw from market happen

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Known issue

Support

FAQs

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