Summary
Some swap requests can not be fulfilled and refunded in boundary cases.
Vulnerability Details
StabilityBranch.sol#initiateSwap()
function is as follows.
function initiateSwap(
uint128[] calldata vaultIds,
uint128[] calldata amountsIn,
uint128[] calldata minAmountsOut
)
external
{
...
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
);
}
}
As we can see above, there is no check if amountIn is less than base fee.
We say that this swap request can not be fulfilled for some reasons.
Then, user refunds it.
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);
474 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
);
}
But if amountIn is less than baseFeeUsd, refund transaction is reverted because of underflow on L474.
So the user cannot fulfill and refund that request.
Then user's fund is frozen to protocol.
Impact
The user cannot fulfill and refund that request.
Then user's fund is frozen to protocol.
Tools Used
Manual review
Recommendations
Modify StabilityBranch.sol#initiateSwap()
function as follows.
function initiateSwap(
uint128[] calldata vaultIds,
uint128[] calldata amountsIn,
uint128[] calldata minAmountsOut
)
external
{
...
for (uint256 i; i < amountsIn.length; i++) {
...
++ require(amountsIn[i] >= tokenSwapData.baseFeeUsd, "amountIn is less than baseFee");
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
);
}
}