A malicious actor could exploit this lack of constraint by setting a huge difference between upperBound and lowerBound (significantly higher than necessary), forcing the function to iterate over a much larger range than intended. This excessive iteration could lead to gas exhaustion.
When a function processes an extensive list, there's a risk of it consuming all available gas. Consequently, it fails, throwing an out-of-gas exception, which negatively affects users trying to interact with the contract.
Run: forge test --match-test testFuzz_GivenThereAreLiquidatableAccountsInTheHugeArray -vvvv
function testFuzz_GivenThereAreLiquidatableAccountsInTheHugeArray(
uint256 marketId,
uint256 secondMarketId,
bool isLong,
uint256 timeDelta
)
external
givenTheSenderIsARegisteredLiquidator
whenTheAccountsIdsArrayIsNotEmpty
givenAllAccountsExist
{
uint256 initialGasLeft = gasleft();
console2.log("initialGasLeft", initialGasLeft);
TestFuzz_GivenThereAreLiquidatableAccountsInTheArray_Context memory ctx;
ctx.fuzzMarketConfig = getFuzzMarketConfig(marketId);
ctx.secondMarketConfig = getFuzzMarketConfig(secondMarketId);
vm.assume(ctx.fuzzMarketConfig.marketId != ctx.secondMarketConfig.marketId);
uint256 amountOfTradingAccounts = 10;
timeDelta = bound({ x: timeDelta, min: 1 seconds, max: 1 days });
ctx.marginValueUsd = 1_000_000e18 / amountOfTradingAccounts;
ctx.initialMarginRate = ctx.fuzzMarketConfig.imr;
deal({ token: address(usdz), to: users.naruto.account, give: ctx.marginValueUsd });
ctx.accountsIds = new uint128[](amountOfTradingAccounts + 2);
ctx.accountMarginValueUsd = ctx.marginValueUsd / (amountOfTradingAccounts + 1);
for (uint256 i; i < amountOfTradingAccounts; i++) {
ctx.tradingAccountId = createAccountAndDeposit(ctx.accountMarginValueUsd, address(usdz));
openPosition(
ctx.fuzzMarketConfig,
ctx.tradingAccountId,
ctx.initialMarginRate,
ctx.accountMarginValueUsd / 2,
isLong
);
openPosition(
ctx.secondMarketConfig,
ctx.tradingAccountId,
ctx.secondMarketConfig.imr,
ctx.accountMarginValueUsd / 2,
isLong
);
ctx.accountsIds[i] = ctx.tradingAccountId;
deal({ token: address(usdz), to: users.naruto.account, give: ctx.marginValueUsd });
}
setAccountsAsLiquidatable(ctx.fuzzMarketConfig, isLong);
setAccountsAsLiquidatable(ctx.secondMarketConfig, isLong);
ctx.nonLiquidatableTradingAccountId = createAccountAndDeposit(ctx.accountMarginValueUsd, address(usdz));
ctx.accountsIds[amountOfTradingAccounts] = ctx.nonLiquidatableTradingAccountId;
changePrank({ msgSender: liquidationKeeper });
for (uint256 i; i < ctx.accountsIds.length; i++) {
if (ctx.accountsIds[i] == ctx.nonLiquidatableTradingAccountId || ctx.accountsIds[i] == 0) {
continue;
}
vm.expectEmit({
checkTopic1: true,
checkTopic2: true,
checkTopic3: false,
checkData: false,
emitter: address(perpsEngine)
});
emit LiquidationBranch.LogLiquidateAccount({
keeper: liquidationKeeper,
tradingAccountId: ctx.accountsIds[i],
amountOfOpenPositions: 0,
requiredMaintenanceMarginUsd: 0,
marginBalanceUsd: 0,
liquidatedCollateralUsd: 0,
liquidationFeeUsd: 0
});
}
skip(timeDelta);
ctx.expectedLastFundingRate = perpsEngine.getFundingRate(ctx.fuzzMarketConfig.marketId).intoInt256();
ctx.expectedLastFundingTime = block.timestamp;
perpsEngine.liquidateAccounts(ctx.accountsIds);
ctx.perpMarketData =
PerpMarketHarness(address(perpsEngine)).exposed_PerpMarket_load(ctx.fuzzMarketConfig.marketId);
assertEq(ctx.expectedLastFundingRate, ctx.perpMarketData.lastFundingRate, "last funding rate");
assertEq(ctx.expectedLastFundingTime, ctx.perpMarketData.lastFundingTime, "last funding time");
(,, ctx.openInterestX18) = perpsEngine.getOpenInterest(ctx.fuzzMarketConfig.marketId);
ctx.expectedOpenInterest = sd59x18(
PositionHarness(address(perpsEngine)).exposed_Position_load(
ctx.nonLiquidatableTradingAccountId, ctx.fuzzMarketConfig.marketId
).size
).abs().intoUD60x18().intoUint256();
assertEq(ctx.expectedOpenInterest, ctx.openInterestX18.intoUint256(), "open interest");
ctx.skewX18 = perpsEngine.getSkew(ctx.fuzzMarketConfig.marketId);
ctx.expectedSkew = PositionHarness(address(perpsEngine)).exposed_Position_load(
ctx.nonLiquidatableTradingAccountId, ctx.fuzzMarketConfig.marketId
).size;
assertEq(ctx.expectedSkew, ctx.skewX18.intoInt256(), "skew");
for (uint256 i; i < ctx.accountsIds.length; i++) {
if (ctx.accountsIds[i] == ctx.nonLiquidatableTradingAccountId) {
continue;
}
ctx.marketOrder = perpsEngine.getActiveMarketOrder(ctx.accountsIds[i]);
assertEq(ctx.marketOrder.marketId, 0);
assertEq(ctx.marketOrder.sizeDelta, 0);
assertEq(ctx.marketOrder.timestamp, 0);
ctx.expectedPosition =
Position.Data({ size: 0, lastInteractionPrice: 0, lastInteractionFundingFeePerUnit: 0 });
ctx.position = PositionHarness(address(perpsEngine)).exposed_Position_load(
ctx.accountsIds[i], ctx.fuzzMarketConfig.marketId
);
assertEq(ctx.expectedPosition.size, ctx.position.size, "position size");
assertEq(ctx.expectedPosition.lastInteractionPrice, ctx.position.lastInteractionPrice, "position price");
assertEq(
ctx.expectedPosition.lastInteractionFundingFeePerUnit,
ctx.position.lastInteractionFundingFeePerUnit,
"position funding fee"
);
assertEq(
0,
TradingAccountHarness(address(perpsEngine)).workaround_getActiveMarketsIdsLength(ctx.accountsIds[i]),
"active market id"
);
assertEq(
0,
GlobalConfigurationHarness(address(perpsEngine)).workaround_getAccountsIdsWithActivePositionsLength(),
"accounts ids with active positions"
);
}
uint256 finalGasLeft = gasleft();
console2.log("finalGasLeft", finalGasLeft);
uint256 gasUsed = initialGasLeft - finalGasLeft;
console2.log("gas used", gasUsed);
uint256 GAS_LIMIT = 32_000_000 wei;
assert(gasUsed < GAS_LIMIT);
}
The test passes.
Manual review.
Introduce a maximum limit for the difference between upperBound and lowerBound to prevent excessively large ranges from being processed.