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
{
if (vaultIds.length != amountsIn.length) {
revert Errors.ArrayLengthMismatch(vaultIds.length, amountsIn.length);
}
if (amountsIn.length != minAmountsOut.length) {
revert Errors.ArrayLengthMismatch(amountsIn.length, minAmountsOut.length);
}
InitiateSwapContext memory ctx;
Vault.Data storage currentVault = Vault.load(vaultIds[0]);
ctx.initialVaultIndexToken = currentVault.indexToken;
ctx.initialVaultCollateralAsset = currentVault.collateral.asset;
Collateral.Data storage collateral = Collateral.load(ctx.initialVaultCollateralAsset);
collateral.verifyIsEnabled();
MarketMakingEngineConfiguration.Data storage configuration = MarketMakingEngineConfiguration.load();
UsdTokenSwapConfig.Data storage tokenSwapData = UsdTokenSwapConfig.load();
ctx.collateralPriceX18 = currentVault.collateral.getPrice();
ctx.maxExecTime = uint120(tokenSwapData.maxExecutionTime);
ctx.vaultAssetBalance = IERC20(ctx.initialVaultCollateralAsset).balanceOf(ctx.initialVaultIndexToken);
for (uint256 i; i < amountsIn.length; i++) {
if (i != 0) {
currentVault = Vault.load(vaultIds[i]);
if (currentVault.collateral.asset != ctx.initialVaultCollateralAsset) {
revert Errors.VaultsCollateralAssetsMismatch();
}
ctx.vaultAssetBalance = IERC20(ctx.initialVaultCollateralAsset).balanceOf(currentVault.indexToken);
}
ctx.expectedAssetOut =
getAmountOfAssetOut(vaultIds[i], ud60x18(amountsIn[i]), ctx.collateralPriceX18).intoUint256();
if (ctx.expectedAssetOut == 0) revert Errors.ZeroOutputTokens();
if (ctx.expectedAssetOut < minAmountsOut[i]) {
revert Errors.SlippageCheckFailed(minAmountsOut[i], ctx.expectedAssetOut);
}
if (ctx.vaultAssetBalance < ctx.expectedAssetOut) {
revert Errors.InsufficientVaultBalance(vaultIds[i], ctx.vaultAssetBalance, ctx.expectedAssetOut);
}
ctx.usdTokenOfEngine = IERC20(configuration.usdTokenOfEngine[currentVault.engine]);
ctx.usdTokenOfEngine.safeTransferFrom(msg.sender, address(this), amountsIn[i]);
ctx.requestId = tokenSwapData.nextId(msg.sender);
UsdTokenSwapConfig.SwapRequest storage swapRequest = tokenSwapData.swapRequests[msg.sender][ctx.requestId];
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
{
UsdTokenSwapConfig.SwapRequest storage request = UsdTokenSwapConfig.load().swapRequests[user][requestId];
if (request.processed) {
revert Errors.RequestAlreadyProcessed(user, requestId);
}
FulfillSwapContext memory ctx;
ctx.deadline = request.deadline;
if (ctx.deadline < block.timestamp) {
revert Errors.SwapRequestExpired(user, requestId, ctx.deadline);
}
request.processed = true;
MarketMakingEngineConfiguration.Data storage marketMakingEngineConfiguration =
MarketMakingEngineConfiguration.load();
ctx.vaultId = request.vaultId;
Vault.Data storage vault = Vault.loadLive(ctx.vaultId);
ctx.usdToken = UsdToken(marketMakingEngineConfiguration.usdTokenOfEngine[engine]);
StabilityConfiguration.Data storage stabilityConfiguration = StabilityConfiguration.load();
ctx.priceX18 = stabilityConfiguration.verifyOffchainPrice(priceData);
ctx.amountIn = request.amountIn;
ctx.amountOutBeforeFeesX18 = getAmountOfAssetOut(ctx.vaultId, ud60x18(ctx.amountIn), ctx.priceX18);
(ctx.baseFeeX18, ctx.swapFeeX18) = getFeesForAssetsAmountOut(ctx.amountOutBeforeFeesX18, ctx.priceX18);
ctx.asset = vault.collateral.asset;
Collateral.Data storage collateral = Collateral.load(ctx.asset);
ctx.amountOut =
collateral.convertUd60x18ToTokenAmount(ctx.amountOutBeforeFeesX18.sub(ctx.baseFeeX18.add(ctx.swapFeeX18)));
ctx.minAmountOut = request.minAmountOut;
if (ctx.amountOut < ctx.minAmountOut) {
revert Errors.SlippageCheckFailed(ctx.minAmountOut, ctx.amountOut);
}
ctx.protocolSwapFeeX18 = ctx.swapFeeX18.mul(ud60x18(marketMakingEngineConfiguration.totalFeeRecipientsShares));
ctx.protocolReward = collateral.convertUd60x18ToTokenAmount(ctx.baseFeeX18.add(ctx.protocolSwapFeeX18));
vault.marketsRealizedDebtUsd -= int128(ctx.amountIn);
ctx.usdToken.burn(ctx.amountIn);
IERC20(ctx.asset).safeTransferFrom(vault.indexToken, address(this), ctx.amountOut + ctx.protocolReward);
marketMakingEngineConfiguration.distributeProtocolAssetReward(ctx.asset, ctx.protocolReward);
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 {
UsdTokenSwapConfig.Data storage tokenSwapData = UsdTokenSwapConfig.load();
UsdTokenSwapConfig.SwapRequest storage request = tokenSwapData.swapRequests[msg.sender][requestId];
if (request.processed) {
revert Errors.RequestAlreadyProcessed(msg.sender, requestId);
}
uint120 deadlineCache = request.deadline;
if (deadlineCache > block.timestamp) {
revert Errors.RequestNotExpired(msg.sender, requestId);
}
request.processed = true;
MarketMakingEngineConfiguration.Data storage marketMakingEngineConfiguration =
MarketMakingEngineConfiguration.load();
address usdToken = marketMakingEngineConfiguration.usdTokenOfEngine[engine];
uint256 baseFeeUsd = tokenSwapData.baseFeeUsd;
uint128 depositedUsdToken = request.amountIn;
marketMakingEngineConfiguration.distributeProtocolAssetReward(usdToken, baseFeeUsd);
uint256 refundAmountUsd = depositedUsdToken - baseFeeUsd;
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
Tools Used
Recommendations
Implement reentrancy guards (e.g., nonReentrant
from OpenZeppelin’s ReentrancyGuard
).