Vulnerability Details
In VaultRouterBranch#deposit()
function, it will revert if shares = 0 due to invariant 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) {
Vault.Data storage vault = Vault.loadExisting(vaultId);
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)
{
Vault.Data storage vault = Vault.loadExisting(vaultId);
uint256 totalAssetsMinusVaultDebt = getVaultCreditCapacity(vaultId);
. . . . . .
}
And
function getIndexTokenSwapRate(
uint128 vaultId,
uint256 sharesIn,
bool shouldDiscountRedeemFee
)
public
view
returns (UD60x18 assetsOut)
{
Vault.Data storage vault = Vault.loadExisting(vaultId);
uint256 totalAssetsMinusVaultDebt = getVaultCreditCapacity(vaultId);
. . . . . .
}
Convert to asset and convert to share function:
function _convertToAssets(uint256 shares, Math.Rounding ) internal view override returns (uint256) {
ZlpVaultStorage storage zlpVaultStorage = _getZlpVaultStorage();
UD60x18 assetsOut = IMarketMakingEngine(zlpVaultStorage.marketMakingEngine).getIndexTokenSwapRate(
zlpVaultStorage.vaultId, shares, false
);
return assetsOut.intoUint256();
}
function _convertToShares(
uint256 assets,
Math.Rounding
)
*/
internal
view
override
returns (uint256)
{
ZlpVaultStorage storage zlpVaultStorage = _getZlpVaultStorage();
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