Summary
when user wants to swap usdToken for vault assets , the user will initiate swap request than the keeper will call FulfillSwap
function to complete the swap, However in first call we did not check for LiveVault
which will got executed when vault is not live, in 2nd call to fulfill the request it will revert because here we check for liveVault.
Vulnerability Details
Let's have a look on the code , when user submit the request to initiate swap he will calls initiateSwap
function:
/src/market-making/branches/StabilityBranch.sol:201
201: function initiateSwap(
202: uint128[] calldata vaultIds,
203: uint128[] calldata amountsIn,
204: uint128[] calldata minAmountsOut
205: )
206: external
207: {
...
216:
217:
218: InitiateSwapContext memory ctx;
219:
220:
221:
222: Vault.Data storage currentVault = Vault.load(vaultIds[0]);
223: ctx.initialVaultIndexToken = currentVault.indexToken;
224: ctx.initialVaultCollateralAsset = currentVault.collateral.asset;
225:
226:
227: Collateral.Data storage collateral = Collateral.load(ctx.initialVaultCollateralAsset);
228: collateral.verifyIsEnabled();
229:
230:
231: MarketMakingEngineConfiguration.Data storage configuration = MarketMakingEngineConfiguration.load();
232:
233:
234: UsdTokenSwapConfig.Data storage tokenSwapData = UsdTokenSwapConfig.load();
235:
236:
237:
238: ctx.collateralPriceX18 = currentVault.collateral.getPrice();
239: ctx.maxExecTime = uint120(tokenSwapData.maxExecutionTime);
240:
241: ctx.vaultAssetBalance = IERC20(ctx.initialVaultCollateralAsset).balanceOf(ctx.initialVaultIndexToken);
242:
243: for (uint256 i; i < amountsIn.length; i++) {
244:
245: if (i != 0) {
246: currentVault = Vault.load(vaultIds[i]);
247:
248:
249: if (currentVault.collateral.asset != ctx.initialVaultCollateralAsset) {
250: revert Errors.VaultsCollateralAssetsMismatch();
251: }
252:
253:
254: ctx.vaultAssetBalance = IERC20(ctx.initialVaultCollateralAsset).balanceOf(currentVault.indexToken);
255: }
From the above it can be seen that we did not check that the vault is love or not.
but in case of fulfill swap :
/src/market-making/branches/StabilityBranch.sol:330
330: function fulfillSwap(
331: address user,
332: uint128 requestId,
333: bytes calldata priceData,
334: address engine
335: )
336: external
337: onlyRegisteredSystemKeepers
338: {
339:
340: UsdTokenSwapConfig.SwapRequest storage request = UsdTokenSwapConfig.load().swapRequests[user][requestId];
341:
342:
343: if (request.processed) {
344: revert Errors.RequestAlreadyProcessed(user, requestId);
345: }
346:
347:
348: FulfillSwapContext memory ctx;
349:
350:
351: ctx.deadline = request.deadline;
352: if (ctx.deadline < block.timestamp) {
353: revert Errors.SwapRequestExpired(user, requestId, ctx.deadline);
354: }
355:
356:
357: request.processed = true;
358:
359:
360: MarketMakingEngineConfiguration.Data storage marketMakingEngineConfiguration =
361: MarketMakingEngineConfiguration.load();
362:
363:
364: ctx.vaultId = request.vaultId;
365: Vault.Data storage vault = Vault.loadLive(ctx.vaultId);
366:
At line 365
we only fetch live vault So in case if vault is not live it will revert.
The user have no way to wait for deadline and call refundSwap
function will charge baseFee
from user, thus user will suffer a lose here.
##POC :
function test_vault_not_live(
)
external
whenCallerIsKeeper
whenRequestWasNotYetProcessed
whenSwapRequestNotExpired
{
uint256 vaultId = 100;
uint256 marketId = 100;
uint256 vaultAssetsBalance = 5000e16;
uint256 swapAmount = 1000e6;
int256 vaultDebtAbsUsd = int256(2e18);
bool useCredit= false;
TestFuzz_WhenSlippageCheckPassesAndThePremiumOrDiscountIsNotZero_Context memory ctx;
ctx.fuzzVaultConfig = getFuzzVaultConfig(vaultId);
ctx.oneAsset = 10 ** ctx.fuzzVaultConfig.decimals;
changePrank({ msgSender: users.owner.account });
marketMakingEngine.configureUsdTokenSwapConfig(1, 30, type(uint96).max);
Vault.UpdateParams memory params = Vault.UpdateParams({
vaultId: ctx.fuzzVaultConfig.vaultId,
depositCap: uint128(100000e18),
withdrawalDelay: uint128(230),
isLive: false,
lockedCreditRatio: 0
});
marketMakingEngine.updateVaultConfiguration(params);
changePrank({ msgSender: users.naruto.account });
deal({
token: address(ctx.fuzzVaultConfig.asset),
to: ctx.fuzzVaultConfig.indexToken,
give: vaultAssetsBalance
});
swapAmount = vaultAssetsBalance / 1e9;
deal({ token: address(usdToken), to: users.naruto.account, give: swapAmount });
ctx.fuzzPerpMarketCreditConfig = getFuzzPerpMarketCreditConfig(marketId);
ctx.engine = IMockEngine(perpMarketsCreditConfig[ctx.fuzzPerpMarketCreditConfig.marketId].engine);
ctx.engine.setUnrealizedDebt(useCredit ? -int256(vaultDebtAbsUsd) : int256(vaultDebtAbsUsd));
ctx.minAmountOut = 0;
UD60x18 priceUsdX18 = IPriceAdapter(vaultsConfig[ctx.fuzzVaultConfig.vaultId].priceAdapter).getPrice();
ctx.priceData = getMockedSignedReport(ctx.fuzzVaultConfig.streamId, priceUsdX18.intoUint256());
ctx.usdTokenSwapKeeper = usdTokenSwapKeepers[ctx.fuzzVaultConfig.asset];
ctx.amountOut =
marketMakingEngine.getAmountOfAssetOut(ctx.fuzzVaultConfig.vaultId, ud60x18(swapAmount), priceUsdX18);
vm.assume(ctx.amountOut.intoUint256() > 0);
ctx.minAmountOut = ctx.amountOut.intoUint128();
initiateUsdSwap(uint128(ctx.fuzzVaultConfig.vaultId), swapAmount, ctx.minAmountOut);
(ctx.baseFeeX18, ctx.swapFeeX18) = marketMakingEngine.getFeesForAssetsAmountOut(ctx.amountOut, priceUsdX18);
ctx.amountOutAfterFee = convertUd60x18ToTokenAmount(
ctx.fuzzVaultConfig.asset, ctx.amountOut.sub(ctx.baseFeeX18.add(ctx.swapFeeX18))
);
changePrank({ msgSender: ctx.usdTokenSwapKeeper });
ctx.protocolSwapFee = ctx.swapFeeX18.mul(ud60x18(marketMakingEngine.exposed_getTotalFeeRecipientsShares()));
ctx.protocolReward =
convertUd60x18ToTokenAmount(ctx.fuzzVaultConfig.asset, ctx.baseFeeX18.add(ctx.protocolSwapFee));
ctx.requestId = 1;
marketMakingEngine.fulfillSwap(
users.naruto.account, ctx.requestId, ctx.priceData, address(marketMakingEngine)
);
}
Add above test case to fulFillSwap.t.sol
and run with command : forge test --mt test_vault_not_live -vvv
.
Impact
the user will lose USDToken
to pay the baseFee due to not checking the vault Livens in initialSwap
call.
Tools Used
Manual Review
Recommendations
check that the vault is live when user initialSwap
.