Part 2

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

Audit Report for StabilityBranch.sol contract - High severity

Summary Audit Report for

external/
└── market-making/
└── branches/
└── StabilityBranch.sol

The StabilityBranch contract facilitates USD token swaps for collateral assets within a vault system. It integrates with external libraries for collateral management, market-making, and stability configurations.

The audit identified two high-severity issues related to reentrancy risk and precision loss in arithmetic operations. These vulnerabilities pose significant threats, including potential fund theft and incorrect calculations affecting financial transactions.

Vulnerability Details

1. Reentrancy Risk

  • Description: The functions initiateSwap, fulfillSwap, and refundSwap execute external calls without implementing reentrancy guards, exposing the contract to reentrancy attacks. An attacker could manipulate contract state by re-entering functions before the previous execution completes.

  • Code Affected: initiateSwap, fulfillSwap, refundSwap.

initialSwap()

function initiateSwap(
uint128[] calldata vaultIds,
uint128[] calldata amountsIn,
uint128[] calldata minAmountsOut
)
external
{
// Perform length checks
if (vaultIds.length != amountsIn.length) {
revert Errors.ArrayLengthMismatch(vaultIds.length, amountsIn.length);
}
if (amountsIn.length != minAmountsOut.length) {
revert Errors.ArrayLengthMismatch(amountsIn.length, minAmountsOut.length);
}
// working data
InitiateSwapContext memory ctx;
// cache the vault's index token and asset addresses
Vault.Data storage currentVault = Vault.load(vaultIds[0]);
ctx.initialVaultIndexToken = currentVault.indexToken;
ctx.initialVaultCollateralAsset = currentVault.collateral.asset;
// load collateral data; must be enabled
Collateral.Data storage collateral = Collateral.load(ctx.initialVaultCollateralAsset);
collateral.verifyIsEnabled();
// load market making engine config
MarketMakingEngineConfiguration.Data storage configuration = MarketMakingEngineConfiguration.load();
// load usd token swap data
UsdTokenSwapConfig.Data storage tokenSwapData = UsdTokenSwapConfig.load();
// cache additional common fields
// ctx.collateralPriceX18 in zaros internal precision
ctx.collateralPriceX18 = currentVault.collateral.getPrice();
ctx.maxExecTime = uint120(tokenSwapData.maxExecutionTime);
// ctx.vaultAssetBalance in native precision of ctx.initialVaultCollateralAsset
ctx.vaultAssetBalance = IERC20(ctx.initialVaultCollateralAsset).balanceOf(ctx.initialVaultIndexToken);
for (uint256 i; i < amountsIn.length; i++) {
// for all but first iteration, refresh the vault and enforce same collateral asset
if (i != 0) {
currentVault = Vault.load(vaultIds[i]);
// revert for swaps using vaults with different collateral assets
if (currentVault.collateral.asset != ctx.initialVaultCollateralAsset) {
revert Errors.VaultsCollateralAssetsMismatch();
}
// refresh current vault balance in native precision of ctx.initialVaultCollateralAsset
ctx.vaultAssetBalance = IERC20(ctx.initialVaultCollateralAsset).balanceOf(currentVault.indexToken);
}
// cache the expected amount of assets acquired with the provided parameters
// amountsIn[i] and ctx.collateralPriceX18 using zaros internal precision
ctx.expectedAssetOut =
getAmountOfAssetOut(vaultIds[i], ud60x18(amountsIn[i]), ctx.collateralPriceX18).intoUint256();
// revert if the slippage wouldn't pass or the expected output was 0
if (ctx.expectedAssetOut == 0) revert Errors.ZeroOutputTokens();
if (ctx.expectedAssetOut < minAmountsOut[i]) {
revert Errors.SlippageCheckFailed(minAmountsOut[i], ctx.expectedAssetOut);
}
// if there aren't enough assets in the vault to fulfill the swap request, we must revert
if (ctx.vaultAssetBalance < ctx.expectedAssetOut) {
revert Errors.InsufficientVaultBalance(vaultIds[i], ctx.vaultAssetBalance, ctx.expectedAssetOut);
}
// transfer USD: user => address(this) - burned in fulfillSwap
ctx.usdTokenOfEngine = IERC20(configuration.usdTokenOfEngine[currentVault.engine]);
ctx.usdTokenOfEngine.safeTransferFrom(msg.sender, address(this), amountsIn[i]);
// get next request id for user
ctx.requestId = tokenSwapData.nextId(msg.sender);
// load swap request
UsdTokenSwapConfig.SwapRequest storage swapRequest = tokenSwapData.swapRequests[msg.sender][ctx.requestId];
// Set swap request parameters
swapRequest.minAmountOut = minAmountsOut[i];
swapRequest.vaultId = vaultIds[i];
swapRequest.assetOut = ctx.initialVaultCollateralAsset;
ctx.deadlineCache = uint120(block.timestamp) + ctx.maxExecTime;
swapRequest.deadline = ctx.deadlineCache;
swapRequest.amountIn = amountsIn[i];
emit LogInitiateSwap(
msg.sender,
ctx.requestId,
vaultIds[i],
amountsIn[i],
minAmountsOut[i],
ctx.initialVaultCollateralAsset,
ctx.deadlineCache
);
}
}

fulfillSwap()

function fulfillSwap(
address user,
uint128 requestId,
bytes calldata priceData,
address engine
)
external
onlyRegisteredSystemKeepers
{
// load request for user by id
UsdTokenSwapConfig.SwapRequest storage request = UsdTokenSwapConfig.load().swapRequests[user][requestId];
// revert if already processed
if (request.processed) {
revert Errors.RequestAlreadyProcessed(user, requestId);
}
// working data
FulfillSwapContext memory ctx;
// if request dealine expired revert
ctx.deadline = request.deadline;
if (ctx.deadline < block.timestamp) {
revert Errors.SwapRequestExpired(user, requestId, ctx.deadline);
}
// set request processed to true
request.processed = true;
// load market making engine config
MarketMakingEngineConfiguration.Data storage marketMakingEngineConfiguration =
MarketMakingEngineConfiguration.load();
// load vault data
ctx.vaultId = request.vaultId;
Vault.Data storage vault = Vault.loadLive(ctx.vaultId);
// get usd token of engine
ctx.usdToken = UsdToken(marketMakingEngineConfiguration.usdTokenOfEngine[engine]);
// load Stability configuration data
StabilityConfiguration.Data storage stabilityConfiguration = StabilityConfiguration.load();
// get price from report in 18 dec
ctx.priceX18 = stabilityConfiguration.verifyOffchainPrice(priceData);
// get amount out asset
ctx.amountIn = request.amountIn;
ctx.amountOutBeforeFeesX18 = getAmountOfAssetOut(ctx.vaultId, ud60x18(ctx.amountIn), ctx.priceX18);
// gets the base fee and swap fee for the given amount out before fees
(ctx.baseFeeX18, ctx.swapFeeX18) = getFeesForAssetsAmountOut(ctx.amountOutBeforeFeesX18, ctx.priceX18);
// cache the collateral asset address
ctx.asset = vault.collateral.asset;
// load the collateral configuration storage pointer
Collateral.Data storage collateral = Collateral.load(ctx.asset);
// subtract the fees and convert the UD60x18 value to the collateral's decimals value
ctx.amountOut =
collateral.convertUd60x18ToTokenAmount(ctx.amountOutBeforeFeesX18.sub(ctx.baseFeeX18.add(ctx.swapFeeX18)));
// slippage check
ctx.minAmountOut = request.minAmountOut;
if (ctx.amountOut < ctx.minAmountOut) {
revert Errors.SlippageCheckFailed(ctx.minAmountOut, ctx.amountOut);
}
// calculates the protocol's share of the swap fee by multiplying the total swap fee by the protocol's fee
// recipients' share.
ctx.protocolSwapFeeX18 = ctx.swapFeeX18.mul(ud60x18(marketMakingEngineConfiguration.totalFeeRecipientsShares));
// the protocol reward amount is the sum of the base fee and the protocol's share of the swap fee
ctx.protocolReward = collateral.convertUd60x18ToTokenAmount(ctx.baseFeeX18.add(ctx.protocolSwapFeeX18));
// update vault debt
vault.marketsRealizedDebtUsd -= int128(ctx.amountIn);
// burn usd amount from address(this)
ctx.usdToken.burn(ctx.amountIn);
// transfer the required assets from the vault to the mm engine contract before distributions
// note: as the swap fee stays in the ZLP Vault, it is technically a net gain to share holders, i.e it is auto
// accumulated to the contract
IERC20(ctx.asset).safeTransferFrom(vault.indexToken, address(this), ctx.amountOut + ctx.protocolReward);
// distribute protocol reward value
marketMakingEngineConfiguration.distributeProtocolAssetReward(ctx.asset, ctx.protocolReward);
// transfers the remaining amount out to the user, discounting fees
// note: the vault's share of the swap fee remains in the index token contract, thus, we don't need transfer
// it anywhere. The end result is that vaults have an amount of their debt paid off with a discount.
IERC20(ctx.asset).safeTransfer(user, ctx.amountOut);
emit LogFulfillSwap(
user,
requestId,
ctx.vaultId,
ctx.amountIn,
ctx.minAmountOut,
request.assetOut,
ctx.deadline,
ctx.amountOut,
ctx.baseFeeX18.intoUint256(),
ctx.swapFeeX18.intoUint256(),
ctx.protocolReward
);
}

refundSwap()

function refundSwap(uint128 requestId, address engine) external {
// load swap data
UsdTokenSwapConfig.Data storage tokenSwapData = UsdTokenSwapConfig.load();
// load swap request
UsdTokenSwapConfig.SwapRequest storage request = tokenSwapData.swapRequests[msg.sender][requestId];
// if request already procesed revert
if (request.processed) {
revert Errors.RequestAlreadyProcessed(msg.sender, requestId);
}
// if dealine has not yet passed revert
uint120 deadlineCache = request.deadline;
if (deadlineCache > block.timestamp) {
revert Errors.RequestNotExpired(msg.sender, requestId);
}
// set precessed to true
request.processed = true;
// load Market making engine config
MarketMakingEngineConfiguration.Data storage marketMakingEngineConfiguration =
MarketMakingEngineConfiguration.load();
// get usd token for engine
address usdToken = marketMakingEngineConfiguration.usdTokenOfEngine[engine];
// cache the usd token swap base fee
uint256 baseFeeUsd = tokenSwapData.baseFeeUsd;
// cache the amount of usd token previously deposited
uint128 depositedUsdToken = request.amountIn;
// transfer base fee too protocol fee recipients
marketMakingEngineConfiguration.distributeProtocolAssetReward(usdToken, baseFeeUsd);
// cache the amount of usd tokens to be refunded
uint256 refundAmountUsd = depositedUsdToken - baseFeeUsd;
// transfer usd refund amount back to user
IERC20(usdToken).safeTransfer(msg.sender, refundAmountUsd);
emit LogRefundSwap(
msg.sender,
requestId,
request.vaultId,
depositedUsdToken,
request.minAmountOut,
request.assetOut,
deadlineCache,
baseFeeUsd,
refundAmountUsd
);
}
}

2. Precision Loss in Arithmetic Operations

  • Description: The contract employs fixed-point arithmetic (UD60x18, SD59x18), which may lead to rounding errors or precision loss, especially in functions calculating asset output amounts.

  • Code Affected: getAmountOfAssetOut, getFeesForAssetsAmountOut.

Impact

  • Potential fund theft due to reentrancy.

  • Inaccurate calculations leading to incorrect asset swaps.

Tools Used

  • Manual Code Review: Identified missing reentrancy protection.

  • Slither: Detected arithmetic vulnerabilities.

Recommendations

Implement reentrancy guards (e.g., nonReentrant from OpenZeppelin’s ReentrancyGuard).

  • Use safe arithmetic operations with proper rounding mechanisms.

  • Test fixed-point calculations extensively to ensure precision.

Updates

Lead Judging Commences

inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Invalidated
Reason: Lack of quality

Support

FAQs

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