F-2026-0016·ordering-dependent-revert

Early loop break in _executePermits causes order-dependent revert on valid owner transactions

Fixedbridgecross-chainkey-registrygithub.com/pdxwebdev/yadakeyeventwallet
TL;DR

An early break in the pre-flight loop exits before checking the remaining permits for a native transfer entry, so valid owner transactions revert with MissingPermit when the permits array is ordered ERC-20-first.

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

Description

The _executePermits function scans the permits[] array in a single pass to detect two independent conditions:

  • Whether a native transfer permit exists (hasNativeTransfer)
  • Whether any recipient requires mint/burn privileges (requiresOwner)

However, when condition (2) is found, the loop immediately breaks:

solidity
for (uint256 i = 0; i < ectx.permits.length; i++) {
PermitData memory permit = ectx.permits[i];
if (permit.token == address(0)) {
hasNativeTransfer = true;
}
if (permit.token == ectx.token) {
for (uint256 j = 0; j < permit.recipients.length; j++) {
Recipient memory recipient = permit.recipients[j];
if (recipient.mint || recipient.burn) {
requiresOwner = true;
break;
}
}
if (requiresOwner) break; // exits outer loop
}
}
if (!hasNativeTransfer) revert MissingPermit();

Valid owner transactions (direct mint/burn) revert with a misleading MissingPermit() error if the permits array is not ordered with the native transfer entry before the ERC-20 mint/burn entry. While not exploitable by attackers, it creates a fragile integration surface and confusing debugging experience.

03Section · Recommendation

Recommendation

Separate the two concerns by removing the early break, allowing the full array to be scanned:

solidity
for (uint256 i = 0; i < ectx.permits.length; i++) {
PermitData memory permit = ectx.permits[i];
if (permit.token == address(0)) {
hasNativeTransfer = true;
}
if (!requiresOwner && permit.token == ectx.token) {
for (uint256 j = 0; j < permit.recipients.length; j++) {
if (permit.recipients[j].mint
|| permit.recipients[j].burn) {
requiresOwner = true;
break;
}
}
}
}

This preserves the inner break optimization while ensuring the full permits array is always traversed for the native transfer check.

04Section · Resolution

Resolution

YadaCoin, Confirmed.

Zealynx, Fixed.

Status
Fixed
F-2026-0016

oog
zealynx

Smart Contract Security Digest

Monthly exploit breakdowns, audit checklists, and DeFi security research — straight to your inbox

© 2026 Zealynx