Early rate$2,400 of senior audit time for $500. Early members keep the rate as it climbs.$2,400 of senior audit time for $500See how →
F-2026-0014·missing-event

settleBatch's gas-exhaustion exit emits nothing, so an unattempted pair is indistinguishable from a failed one

Fixednfterc-1155marketplace
TL;DR

The gas-exhaustion exit from settleBatch emits nothing, so an unattempted pair cannot be distinguished from a failed one in the logs, which are the relayer's only observable channel.

Severity
LOW
Impact
LOW
Likelihood
MEDIUM
Method
MManual review
CAT.
Complexity
MEDIUM
Exploitability
LOW
02Section · Description

Description

settleBatch has three per-index outcomes and only two of them write to the log stream.

The oversized-signature skip and the failed self-call both emit SettlementSkipped. The gas-exhaustion exit does not:

solidity
for (uint256 i = 0; i < n; ++i) {
if (gasleft() < SETTLE_ONE_GAS_CAP + BATCH_GAS_FLOOR) break;

settled is a return value, and settleBatch is called by a relayer EOA in a transaction, so the return data is not available to the submitter — logs are the only observable channel. An index that was never attempted therefore produces no Settled and no SettlementSkipped, which is indistinguishable from an index that was attempted and failed silently, or from a dropped log.

Settled also carries no batch index:

solidity
event Settled(
bytes32 indexed askHash,
bytes32 indexed bidHash,
address seller,
...
);

so resolving which positions succeeded requires recomputing every EIP-712 digest in the batch and matching them against unindexed events.

The fail-safe direction is correct — re-submitting a pair that did settle reverts OrderAlreadySettled — so this is an observability gap rather than a double-settlement risk. It matters because the off-chain layer decides whose order to cancel from these signals, and treating absence as failure cancels the orders of every maker in the untouched tail.

03Section · Impact

Impact

An index that was never attempted is indistinguishable from one that was attempted and failed. Because the off-chain layer decides whose order to cancel from these signals, treating absence as failure cancels the orders of every maker in the untouched tail of the batch. No double settlement is possible — re-submitting a settled pair reverts — so the exposure is observability rather than custody.

04Section · Recommendation

Recommendation

Emit the stop point, and index the successes:

diff
+event BatchStopped(uint256 firstUnprocessedIndex, uint256 gasRemaining);
diff
-if (gasleft() < SETTLE_ONE_GAS_CAP + BATCH_GAS_FLOOR) break;
+if (gasleft() < SETTLE_ONE_GAS_CAP + BATCH_GAS_FLOOR) {
+ emit BatchStopped(i, gasleft());
+ break;
+}
diff
event Settled(
+ uint256 indexed index,
bytes32 indexed askHash,
bytes32 indexed bidHash,

Adding a fourth indexed parameter is not possible, so if index should be indexed, demote one of the existing three. bidHash is the least useful as a topic given askHash already identifies the pair.

05Section · Resolution

Resolution

The gas-exhaustion exit now emits its stop point, and the settlement event leads with the batch index, so a pair is attributed from its own log rather than by counting. The off-chain dual-decode that carried the migration has been retired. Verified at audit-v2.

06Section · Affected files

Affected files

  • src/PlakxioSettlement.sol#L374-L379 and src/PlakxioSettlement.sol#L186-L196 at commit 5c38893
Status
Fixed
F-2026-0014