Early loop break in _executePermits causes order-dependent revert on valid owner transactions
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.
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:
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.
Recommendation
Separate the two concerns by removing the early break, allowing the full array to be scanned:
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.
Resolution
YadaCoin, Confirmed.
Zealynx, Fixed.

