Part 2

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

CreditDelegationBranch::settlevaultsdebt incorrectly swaps token from marketmakingengine and not directly from Zlpvault which breaks protocol intended functionality

Summary

CreditDelegationBranch::settlevaultsdebt is intended to swap a vault’s own assets for USDC to settle its debt. However, due to an incorrect parameter in the swap call, the function swaps assets from the marketmakingengine (address(this)) instead of the vault’s associated zlpvault contract. This discrepancy prevents the vault’s actual assets from being used in the settlement process, leading to inaccurate debt settlement.

Vulnerability Details

The natspec of CreditDelegationBranch::settlevaultsdebt says the following:
"/// @notice Settles the given vaults' debt or credit by swapping assets to USDC or vice versa.
/// @dev Converts ZLP Vaults unsettled debt to settled debt by:
/// - If in debt, swapping the vault's assets to USDC.
/// - If in credit, swapping the vault's available USDC to its underlying assets.
/// exchange for their assets."

The natspec documentation clearly states that the vault’s assets should be swapped for USDC (if the vault is in debt) or vice versa (if in credit). However, the implementation erroneously uses address(this) when calling the swap function. This implies that the swap operation is performed using tokens from the current contract rather than from the vault’s designated asset account.

This is not intended logic and is further proven by looking at CreditDelegationBranch::convertMarketsCreditDepositsToUsdc. See below:

/// @notice Converts assets deposited as credit to a given market for USDC.
/// @dev USDC accumulated by swaps is stored at markets, and later pushed to its connected vaults in order to
/// cover an engine's usd token.
/// @dev The keeper must ensure that the market has received fees for the given asset before calling this
/// function.
/// @dev The keeper doesn't need to settle all deposited assets at once, it can settle them in batches as needed.
/// @param marketId The market identifier.
/// @param assets The array of assets deposited as credit to be settled for USDC.
/// @param dexSwapStrategyIds The identifier of the dex swap strategies to be used.
/// @param paths Used when the keeper wants to perform a multihop swap using one of the swap strategies.
function convertMarketsCreditDepositsToUsdc(
uint128 marketId,
address[] calldata assets,
uint128[] calldata dexSwapStrategyIds,
bytes[] calldata paths
)
external
onlyRegisteredSystemKeepers
{
// revert if the arrays have different lengths
if (assets.length != dexSwapStrategyIds.length || assets.length != paths.length) {
// we ignore in purpose the error params here
revert Errors.ArrayLengthMismatch(0, 0);
}
// load the market's data storage pointer
Market.Data storage market = Market.loadExisting(marketId);
// working area
ConvertMarketsCreditDepositsToUsdcContext memory ctx;
for (uint256 i; i < assets.length; i++) {
// revert if the market hasn't received any fees for the given asset
(bool exists, uint256 creditDeposits) = market.creditDeposits.tryGet(assets[i]);
if (!exists) revert Errors.MarketDoesNotContainTheAsset(assets[i]);
if (creditDeposits == 0) revert Errors.AssetAmountIsZero(assets[i]);
// cache usdc address
address usdc = MarketMakingEngineConfiguration.load().usdc;
// creditDeposits in zaros internal precision so convert to native token decimals
ctx.creditDepositsNativeDecimals =
Collateral.load(assets[i]).convertUd60x18ToTokenAmount(ud60x18(creditDeposits));
// convert the assets to USDC; both input and outputs in native token decimals
uint256 usdcOut = _convertAssetsToUsdc(
dexSwapStrategyIds[i], assets[i], ctx.creditDepositsNativeDecimals, paths[i], address(this), usdc
);
// sanity check to ensure we didn't somehow give away the input tokens
if (usdcOut == 0) revert Errors.ZeroOutputTokens();
// settles the credit deposit for the amount of USDC received
// updating storage so convert from native token decimals to zaros internal precision
market.settleCreditDeposit(assets[i], Collateral.load(usdc).convertTokenAmountToUd60x18(usdcOut));
// emit an event
emit LogConvertMarketCreditDepositsToUsdc(marketId, assets[i], creditDeposits, usdcOut);
}
}

This function is used to convert market credit which are tokens deposited by the perp engine using CreditDelegationBranch::depositcreditformarket to usdc. As a result, it is evident that CreditDelegationBranch::settlevaultsdebt intended function is to swap assets from the vaults associated zlp vault to usdc. Assets in the zlp vault are deposited via VaultRouterBranch::deposit and the function contains the following lines:

// increase vault allowance to transfer tokens minus fees from this contract to vault
address indexTokenCache = vault.indexToken;
IERC20(ctx.vaultAsset).approve(indexTokenCache, ctx.assetsMinusFees);
// then perform the actual deposit
// NOTE: the following call will update the total assets deposited in the vault
// NOTE: the following call will validate the vault's deposit cap
// invariant: no tokens should remain stuck in this contract
ctx.shares = IERC4626(indexTokenCache).deposit(ctx.assetsMinusFees, msg.sender);

These lines deposit the assets that user has deposited into the zlp vault. As a result, these tokens are no longer in the marketmakingengine contract. They are stored in the zlp vault. As a result, when CreditDelegationBranch::settlevaultsdebt, for it to work as intended, it must swap tokens directly from the associated zlp vault for usdc which currently is not the case.

The call:

ctx.usdcOut = _convertAssetsToUsdc(
vault.swapStrategy.usdcDexSwapStrategyId,
ctx.vaultAsset,
ctx.swapAmount,
vault.swapStrategy.usdcDexSwapPath,
address(this), // Incorrect: should target the vault's asset source
ctx.usdc
);

mistakenly directs the swap to pull tokens from address(this) (the current contract) instead of from the vault's actual asset holdings.
Because the vault’s assets are not swapped as intended, the resulting USDC is not correctly allocated. This leads to an incorrect update of the vault’s unsettled debt—ultimately causing the debt to remain unaddressed when the settlement function is called.

Impact

The vault’s actual assets are not used for the swap, meaning the vault’s debt remains unsettled or is settled using an incorrect source. This discrepancy results in the vault’s debt not being properly reduced. Since the marketmakingengine contract makes the swap, it ends up with less creditdeposits than intended. If using the marketmakingengine's tokens to perform the swap was intended behaviour, then Market::creditdeposits should have been updated to reflect that there are less credit deposits in the contract and more usdc which was done in CreditDelegationBranch::convertMarketsCreditDepositsToUsdc with the following line:

// settles the credit deposit for the amount of USDC received
// updating storage so convert from native token decimals to zaros internal precision
market.settleCreditDeposit(assets[i], Collateral.load(usdc).convertTokenAmountToUd60x18(usdcOut));

This was not done in CreditDelegationBranch::settlevaultsdebt which further indicates that the marketmakingengine's creditdeposits are not intended to be used in CreditDelegationBranch::settlevaultsdebt.

Tools Used

Manual Review

Recommendations

Correct the Swap Parameter:
Update the swap call in the debt settlement function to ensure that it pulls the vault’s assets from the correct contract. Replace
address(this) with the appropriate vault asset source address so that the swap operation utilizes the vault’s own tokens for settling its debt.

Ensure Proper Asset Approval: Before executing the swap, the ZLP vault must explicitly approve the MarketMakingEngine to spend all its tokens. This approval is critical to guarantee that the MarketMakingEngine can access and swap the vault’s assets as intended.

Review Asset Flow and Design: Verify that the vault’s assets are managed in a separate contract as originally designed, and update the debt settlement logic accordingly. Confirm that the asset flow during swaps matches the specifications outlined in the natSpec documentation.

Updates

Lead Judging Commences

inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Validated
Assigned finding tags:

settlevaultsdebt and rebalanceVaultAssets inside CreditDelegationBranch incorrectly swaps tokens from marketmakingengine and not directly from Zlpvault which breaks protocol

Support

FAQs

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