Summary
The createTradingAccountAndMulticall
function may run into out-of-gas errors due to performing multiple calls within a loop, especially if the number of calls is large or if individual calls are gas-intensive.
Vulnerability Details
Description:
The function is designed to create a new trading account and then execute multiple delegate calls based on the provided data payload. Each delegate call consumes gas, and the total gas consumption of all calls combined can exceed the gas limit of a single transaction, resulting in an out-of-gas error.
function createTradingAccountAndMulticall(
bytes[] calldata data,
bytes memory referralCode,
bool isCustomReferralCode
)
external
payable
virtual
returns (bytes[] memory results)
{
uint128 tradingAccountId = createTradingAccount(referralCode, isCustomReferralCode);
results = new bytes[](data.length);
for (uint256 i; i < data.length; i++) {
bytes memory dataWithAccountId = bytes.concat(data[i][0:4], abi.encode(tradingAccountId), data[i][4:]);
(bool success, bytes memory result) = address(this).delegatecall(dataWithAccountId);
if (!success) {
uint256 len = result.length;
assembly {
revert(add(result, 0x20), len)
}
}
results[i] = result;
}
}
Root Cause:
The gas limit for a single transaction is finite. When the function iterates over the data array, it executes multiple delegate calls. If the data array contains a large number of entries or if any individual delegate call requires a significant amount of gas, the cumulative gas consumption can surpass the transaction gas limit.
Proof of concept
copy and paste into createTradingAccountAndMulticall.t.sol
and create event log_named_uint and log_named_bytes
function test_GasConsumptionWithVaryingDataArraySizes() external {
uint256[] memory sizes = new uint256[](5);
sizes[0] = 10;
sizes[1] = 100;
sizes[2] = 1000;
sizes[3] = 10000;
sizes[4] = 100000;
for (uint256 j = 0; j < sizes.length; j++) {
bytes[] memory data = new bytes[](sizes[j]);
for (uint256 i = 0; i < sizes[j]; i++) {
data[i] = abi.encodeWithSelector(TradingAccountBranch.depositMargin.selector, address(usdc), uint256(1));
}
uint256 gasStart = gasleft();
try perpsEngine.createTradingAccountAndMulticall(data, bytes(""), false) {
uint256 gasUsed = gasStart - gasleft();
emit log_named_uint("Gas used for data array size:", sizes[j]);
emit log_named_uint("Gas used :", gasUsed);
} catch (bytes memory reason) {
uint256 gasUsed = gasStart - gasleft();
emit log_named_uint("Gas used for data array size (failed):", sizes[j]);
emit log_named_uint("Gas used (failed):", gasUsed);
emit log_named_bytes("Revert reason :", reason);
}
}
}
function test_DosAttackUsingLargeDataArray2() external {
uint256 largeArraySize = 100000;
bytes[] memory data = new bytes[](largeArraySize);
for (uint256 i = 0; i < largeArraySize; i++) {
data[i] = abi.encodeWithSelector(TradingAccountBranch.depositMargin.selector, address(usdc), uint256(1));
}
try perpsEngine.createTradingAccountAndMulticall(data, bytes(""), false) {
assertTrue(false, "Expected out-of-gas error, but function succeeded");
} catch (bytes memory reason) {
assertTrue(keccak256(reason) == keccak256(""), "Expected out-of-gas error");
}
}
Impact
Transaction Failure: The entire transaction will fail if the gas limit is exceeded, reverting all state changes made within the function.
Service Disruption: Users may experience failures and be unable to create trading accounts or perform multicalls, leading to potential service disruptions and a negative user experience.
Increased Costs: Repeatedly encountering out-of-gas errors can increase costs for users, as they have to pay for failed transactions.
Tools Used
Manual Review
Recommendations
Verify the data length by limiting it to certain values or Implement gas estimation for each call and split the calls into multiple transactions if the estimated gas exceeds a safe threshold.