Full Report
EIP-2612 is an extension of the ERC20 standard that adds in the Permit() function. This removes the burden of paying for gas on a call to approve(). Instead, a user can sign offline a permit signature, give it to a user and make it usable for them to transfer funds from their account. Good idea that saves lots of gas! There are two key items to verify: the signature is valid and the deadline has not passed. Crucially, the msg.sender of the call does NOT need to be validated. This is a known limitation but was brushed off, as "The end result is the same for the Permit signer..." The authors of this post asked themselves if this is true or not. Many times, the call to Permit() is in the middle of lots of other code. So, if an attacker frontruns the call to the other function, extracts the signature and uses this signature directly on the call to Permit(), they would lose the ability to use the functionality after that point. The authors went around and starting looking for cases of this being true. The case they saw over and over again was within custom EIP712 functions, mostly in deposit() functions. With these, there's a permit, a transfer then some custom logic. In the example, the logic called _creditUser(). Since we can frontrun this call, the final step will never happen, losing the user some value. The author has a very good point on this: "The issue is a great example of how important it is to be security-focused when defining widely used standards." When creating ideas for everyone to use, they better be well-thought out. The payouts were mixed for the reports. Some paid, were supposed to by Immunefi and didn't... Just how the life of a bug bounty hunter goes. They claim this falls under the griefing category, which is a medium severity. This is simply a bug that can be used to hurt an individual user or protocol for a small period of time but has no incentive or profit from an attacker. To me, this doesn't fall under the griefing category, since they permanently lose access to the functionality. Overall, good write up on an issue that appears to be everywhere. I'm curious to see if this will turn into the ERC20 approval frontrun bug in terms of reporting.
Analysis Summary
# Vulnerability: Denial of Service via Front-running EIP-2612 Permit Calls within Complex Transactions
## CVE Details
- CVE ID: Not explicitly assigned in the text.
- CVSS Score: Not provided. The reporter suggests a Medium severity based on the griefing category, although they disagree with this classification, leaning towards a permanent loss of functionality.
- CWE: Likely related to CWE-776 (Improper Restriction of Operations within the Bounds of a Resource) or a logic flaw CWE, due to the reliance on transaction ordering for intended functionality.
## Affected Systems
- Products: Contracts implementing or integrating the EIP-2612 `permit()` function, especially when used within multi-step functions (e.g., `deposit()`) that rely on the subsequent allowance being set.
- Versions: Any implementation using EIP-2612 where the `permit()` call is followed by critical custom logic that relies on the allowance, and where attackers can observe and front-run the transaction in the mempool.
- Configurations: Specifically identified in custom EIP-712 functions, often seen in `deposit()` functions that combine `permit()`, a transfer, and custom logic (like `_creditUser`). Also affects the ERC20 Governance `delegateBySig()` module when composed.
## Vulnerability Description
The EIP-2612 standard allows users to grant token approvals off-chain via a signed permit, saving gas by avoiding an explicit `approve()` transaction. The design crucially ignores the `msg.sender` of the `permit()` call. A security consideration in the EIP notes that an external party can front-run the intended `permit()` transaction. The EIP dismisses this by stating, "The end result is the same for the Permit signer..."
The vulnerability arises when the `permit()` call is nested within a larger contract operation (e.g., a deposit function) that executes subsequent logic contingent on the permit succeeding. An attacker can observe the transaction containing the `permit()` call in the mempool, replicate the call arguments, and execute a direct `permit()` transaction first. This front-run consumes the necessary resources (like nonce increment, if applicable) or simply sets the allowance. Crucially, if the subsequent logic in the original transaction relies on the *successful execution* of the permit call to proceed, forcing the original `permit()` to revert (or consuming the necessary state change), the attacker effectively denies the user the intended follow-on action. This results in a sustained Denial of Service (DoS) for that specific functionality path until the user initiates new transactions or alternative flows.
## Exploitation
- Status: PoC available (demonstrated via front-running deposit functions). Not confirmed as exploited in the wild for financial gain, but reported across 30+ projects.
- Complexity: Low (Attacker needs mempool monitoring/manipulation capabilities common in MEV/sequencer interactions).
- Attack Vector: Network (Requires observing and submitting conflicting transactions to the blockchain/mempool).
## Impact
- Confidentiality: No direct impact.
- Integrity: Potential integrity impact if the failure prevents necessary state updates or transfers committed by the original transaction sequence.
- Availability: Direct impact on **Availability** of the specific functionality (DoS). The original transaction sequence following the permit call will fail or revert, permanently denying the user access to that specific flow until they manually re-initiate or use a fallback.
## Remediation
### Patches
No specific CVE patch numbers provided, as this is a standards-level limitation being addressed incrementally by adopting projects. Projects must manually fix implementations.
- **General Fix Concept:** Implement logic that verifies the context or intent of the signed permit, or implements robust error handling around the potentially front-run `permit()` call.
- **Example Workaround:** Catching exceptions after attempting the `permit()` call and allowing execution to continue if the required state (e.g., allowance) has been set by a preceding transaction (the attacker's front-run).
### Workarounds
1. **Contextual Intent:** Modify signature parameters to include an "intent" field or an "on/off" bit to ensure the nonce is only increased for the specific, intended action.
2. **State Tracking:** Use a `has_used` bit in storage to properly gate the allowance increase, ensuring only the first successful invocation modifies the associated state permanently.
3. **Fallback Logic:** Ensure that if the `permit()` fails (due to a front-run), the contract can gracefully fall back to using a direct user-approved allowance or alternative logic path if one exists.
## Detection
- Indicators of Compromise: Transactions showing an EIP-2612 `permit()` call followed immediately by a revert in the same complex function that was intended to execute afterwards.
- Detection Methods and Tools: Monitoring mempool for transactions that immediately submit identical or conflicting `permit()` calls ahead of legitimate transactions that utilize EIP-2612 within complex functions.
## References
- Vendor Advisories: Disclosed to 30+ projects via Immunefi and private bug bounty programs. Specific project responses varied dramatically.
- Relevant Links:
- EIP-2612 Specification: eips dot ethereum dot org/EIPS/eip-2612
- Code examples of workarounds provided for reference by the original reporters: github dot com/trust1995/trustlessPermit/tree/main/