Part 2

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

invariant: `received shares must be > 0 even when minShares = 0; no donation allowed` can be broken by directly transfer token to vault

Vulnerability Details

In VaultRouterBranch#deposit()function, it will revert if shares = 0 due to invariant no donation allowed:

// invariant: received shares must be > 0 even when minShares = 0; no donation allowed
if (ctx.shares == 0) revert Errors.DepositMustReceiveShares();

In ZlpVault, it inherit ERC4626Upgradeable:

contract ZlpVault is Initializable, UUPSUpgradeable, OwnableUpgradeable, ERC4626Upgradeable {

It do not override totalAsset()function, that return balance of contract:

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

So the invariant can be broken by directly transfer token to the contract. Because this function is used to calculate shares in getVaultCreditCapacity()function:

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()); // <--
. . . . .
}

Which is called by getIndexTokenSwapRate and getVaultAssetSwapRate function to get total asset, that is called to convert share to asset and vice versa:

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); // <--
. . . . . .
}

And

function getIndexTokenSwapRate(
uint128 vaultId,
uint256 sharesIn,
bool shouldDiscountRedeemFee
)
public
view
returns (UD60x18 assetsOut)
{
// 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); // <--
. . . . . .
}

Convert to asset and convert to share function:

function _convertToAssets(uint256 shares, Math.Rounding /**/ ) internal view override returns (uint256) {
// load erc-7201 storage pointer
ZlpVaultStorage storage zlpVaultStorage = _getZlpVaultStorage();
// fetch the amount of assets out for the shares input value calling the `MarketMakingEngine`
UD60x18 assetsOut = IMarketMakingEngine(zlpVaultStorage.marketMakingEngine).getIndexTokenSwapRate( // <--
zlpVaultStorage.vaultId, shares, false
);
return assetsOut.intoUint256();
}
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();
}

Impact

Invariant can be broken

Recommendations

Override totalAssets() function in ZlpVault by only recording amount deposit/withdraw from market

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.