What’s wrong with ERC20Permit?

13 December, 2023
article image
Research

Contents

Introduction

Recently, our partners from Lido, in collaboration with Immunefi, were alerted to a vulnerability classified as a WARNING. This vulnerability, under specific conditions, can significantly disrupt the user experience (UX) during the processing of a withdrawal request. This revelation is particularly concerning because it not only affects Lido but also extends to many of the largest DeFi protocols and their major forks, indicating a widespread potential risk in the DeFi ecosystem.

What makes this situation especially intriguing is the discovery by our security researchers that this vulnerability is not isolated to a single protocol or implementation but is, in fact, a systemic issue that many top DeFi platforms are exposed to. The commonality of the vulnerability across various protocols suggests a fundamental oversight in the standard practices adopted by DeFi developers.

We are strongly concerned that all protocols that use ERC-20 Permit should figure out what is this issue about and explore the ways of preventing it. Let’s figure out what the problem is here and how we can prevent them in the future!

Vulnerability description

Let’s start with the core of the problem. Let’s have a look at a specific example of Lido code:

Let’s look at the code below. These are the methods that Lido uses to process withdrawal requests using the Permit mechanism in the context of ERC-20 tokens:

function requestWithdrawalsWithPermit(uint256[] calldata _amounts, address _owner, PermitInput calldata _permit)
external returns (uint256[] memory requestIds) {
        STETH.permit(msg.sender, address(this), _permit.value, _permit.deadline, _permit.v, _permit.r, _permit.s);
        return requestWithdrawals(_amounts, _owner);
    }

function requestWithdrawalsWstETHWithPermit(uint256[] calldata _amounts, address _owner, PermitInput calldata _permit)
external returns (uint256[] memory requestIds) {
        WSTETH.permit(msg.sender, address(this), _permit.value, _permit.deadline, _permit.v, _permit.r, _permit.s);
        return requestWithdrawalsWstETH(_amounts, _owner);
    }

These functions are designed to streamline transactions by combining token approval and withdrawal request operations into a single call.

ERC20Permit (stETH and wstETH inherit from this) uses the nonces mapping for replay protection. Once a signature is verified and approved, the nonce increases, invalidating the same signature being replayed.

Both the above methods expect the user to sign their tokens with the WithdrawlQueue contract address as spender and submit the transaction to the chain with the signature uint8 v, bytes32 r, bytes32 s as part of the arguments.

When the user transaction is in the mempool, an attacker can take this signature, call the token.permit function on the token themselves with the following arguments:

address owner = victim address
address spender = WithdrawlQueue contract address

uint256 value = _permit.value copied from the still pending transaction
uint256 deadline = _permit.deadline copied from the still pending transaction

uint8 v = _permit.v copied from the still pending transaction
bytes32 r = _permit.r copied from the still pending transaction
bytes32 s = _permit.s copied from the still pending transaction

Since this is a valid signature, the token accepts it and increases the nonce. This makes the user’s transaction fail whenever it gets mined, which of course breaks the UX.

Mitigation strategies

Now, let’s have a look on what is happening inside the ERC-2612.

ERC-2612 itself makes the following mitigation recommendations:

The key information here is contained in the following paragraph

Signed Permit messages are censorable. The relaying party can always choose to not submit the Permit after having received it, withholding the option to submit it. The deadline parameter is one mitigation to this. If the signing party holds ETH they can also just submit the Permit themselves, which can render previously signed Permits invalid.

Inattentive reading of this paragraph led to the omission of the fact that the signature can become invalid even after submitting a Permit by any person who saw the transaction in the mempool.

What makes it more noticeable is a EIP-712 Security Considerations:

As you can see, this subarticle takes into account the possibility of front-running and recommends to implement all the necessary checks according to the protocol logic. However, most of vulnerable contracts just decided to reject front-runned TXs.

The only way to mitigate that is to implement multiple custom preventing mechanisms or, at any rate, add some fixes according to provided specifications.

Conclusion

In concluding this article, it becomes evident that meticulous attention to detail in reading specifications and implementing secure practices is not just advisable but essential in the realm of blockchain technology and smart contract development. This issue serves as a compelling case study underscoring this necessity.

References

(1) - Potential withdrawal request griefing vector via `permit` front-running #803 | Lido-dao

(2) - ERC-2612 specification | by Martin Lundfall in Ethereum EIPs

(3) - EIP-712 specification | “Security Considerations” subarticle

Telegram
Research

Contents

Telegram

Have a question?

Have a question?

Stay Connected with OXORIO

We're here to help and guide you through any inquiries you might have about blockchain security and audits.