Part 2

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

Donations attacks on Vault can cause issues in accounting affecting several functions in ZlpVault, Vault and VaultRouterBranch

Description

VaultRouterBranch::getVaultCreditCapacity, VaultRouterBranch::getIndexTokenSwapRate, Vault::getTotalCreditCapacityUsd, Vault::updateVaultAndCreditDelegationWeight and ZlpVault::maxDeposit all use the standard EIP - 4626 totalAssets() for crucial accounting functions.

Vulnerable Code

VaultRouterBranch::getVaultCreditCapacity:

function getVaultCreditCapacity(uint128 vaultId) public view returns (uint256) {
Vault.Data storage vault = Vault.loadExisting(vaultId);
1. @> SD59x18 totalAssetsX18 = @>vault.collateral.convertTokenAmountToSd59x18(IERC4626(vault.indexToken).totalAssets().toInt256());
SD59x18 vaultDebtUsdX18 = vault.getTotalDebt();
UD60x18 assetPriceX18 = vault.collateral.getPrice();
SD59x18 vaultDebtInAssetsX18 = vaultDebtUsdX18.div(assetPriceX18.intoSD59x18());
uint8 decimalOffset = Constants.SYSTEM_DECIMALS - vault.collateral.decimals;
SD59x18 totalAssetsMinusVaultDebtX18 =
totalAssetsX18.add(sd59x18(int256(10 ** uint256(decimalOffset)))).sub(vaultDebtInAssetsX18);
uint256 totalAssetsMinusVaultDebt = vault.collateral.convertSd59x18ToTokenAmount(totalAssetsMinusVaultDebtX18);
return totalAssetsMinusVaultDebt;
}

VaultRouterBranch::getIndexTokenSwapRate

function getIndexTokenSwapRate(
uint128 vaultId,
uint256 sharesIn,
bool shouldDiscountRedeemFee
)
public
view
returns (UD60x18 assetsOut)
{
Vault.Data storage vault = Vault.loadExisting(vaultId);
2. @> uint256 totalAssetsMinusVaultDebt = getVaultCreditCapacity(vaultId);
uint8 decimalOffset = Constants.SYSTEM_DECIMALS - IERC20Metadata(vault.indexToken).decimals();
uint256 previewAssetsOut = sharesIn.mulDiv(
totalAssetsMinusVaultDebt,
IERC4626(vault.indexToken).totalSupply() + 10 ** decimalOffset,
MathOpenZeppelin.Rounding.Floor
);
if (shouldDiscountRedeemFee) {
previewAssetsOut = ud60x18(previewAssetsOut).sub(ud60x18(previewAssetsOut).mul(ud60x18(vault.redeemFee))).intoUint256();
return ud60x18(previewAssetsOut);
}

Vault::getTotalCreditCapacityUsd:

function getTotalCreditCapacityUsd(Data storage self) internal view returns (SD59x18 creditCapacityUsdX18) {
Collateral.Data storage collateral = self.collateral;
3. @> UD60x18 totalAssetsX18 = ud60x18(IERC4626(self.indexToken).totalAssets());
UD60x18 totalAssetsUsdX18 = collateral.getAdjustedPrice().mul(totalAssetsX18);
creditCapacityUsdX18 = totalAssetsUsdX18.intoSD59x18().sub(getTotalDebt(self));
}

Vault::updateVaultAndCreditDelegationWeight:

function updateVaultAndCreditDelegationWeight(
Data storage self,
uint128[] memory connectedMarketsIdsCache
)
internal
{
uint256 connectedMarketsConfigLength = self.connectedMarkets.length;
EnumerableSet.UintSet storage connectedMarkets = self.connectedMarkets[connectedMarketsConfigLength - 1];
4. @> uint128 newWeight = uint128(IERC4626(self.indexToken).totalAssets());
for (uint256 i; i < connectedMarketsIdsCache.length; i++) {
CreditDelegation.Data storage creditDelegation =
CreditDelegation.load(self.id, connectedMarkets.at(i).toUint128());
creditDelegation.weight = newWeight;
}
self.totalCreditDelegationWeight = newWeight;
}

ZlpVault::maxDeposit

function maxDeposit(address) public view override returns (uint256 maxAssets) {
ZlpVaultStorage storage zlpVaultStorage = _getZlpVaultStorage();
IMarketMakingEngine marketMakingEngine = IMarketMakingEngine(zlpVaultStorage.marketMakingEngine);
uint128 depositCap = marketMakingEngine.getDepositCap(zlpVaultStorage.vaultId);
5. @> uint256 totalAssetsCached = totalAssets();
unchecked {
maxAssets = depositCap > totalAssetsCached ? depositCap - totalAssetsCached : 0;
}
}

As you can see in the provided code snippets through point 1. - 5. there are several functions relying on totalAssets() which essentially is balanceOf() accounting, and therefore easily manipulatable via token donations to the Vaults.

Impact

The impact in within the function varies, but is overall within high severity. Crucial steps within the redemption of user claims like in 1. and 2. as you can see the previewAssetsOut is affected.
In 3. the debt calculations of the Vault will cause to report a wrong creditCapacity which will make the Vault believe it actually has more credit capacity than it actually has, therefore affecting the overall health and critical invariants of the system (solvency invariant).

Since the system has no way of dealing with such donations (withdraw functions are limited to 0, also there is no way on integrating it as a donation into the vault), I rate the severity as High.

Tools Used

Manual Review

Recommended Mitigation

Instead of relying on totalAssets() which is a manipulatable value, consider switching into an internal accounting scheme.

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Too generic

Support

FAQs

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