ERC-1271 Signature Replay Vulnerability

ERC-1271 Signature Replay Vulnerability

Author: Howy Ho

Reviewed by Logan Ross

Published on March 29, 20244 min read
Smart Contract Account Signature Replay Vulnerability
Smart Contract Account Signature Replay Vulnerability

On October 27th 2023, Alchemy discovered a ERC1271 contract signature replay vulnerability that affected a large number of smart contract accounts (SCA), and led to risks when interacting with several applications. The SCAs affected included our LightAccount and OKX’s SmartAccount, and applications interactions that we identified to be at risk included Permit2 and Cowswap. We promptly raised this issue to the various affected SCAs and applications and discovered that curiousapple, an independent security researcher, had found the same vulnerability a month prior. We collaborated with curiousapple, Frangio (ERC1271 author), the ERC4337 team, and other SCA technical experts on a fix. At this point, no funds are at risk and the impact to applications is fairly limited. All involved SCAs have either acknowledged the risk or shipped a fix.

On Ethereum and all Ethereum Virtual Machine (EVM) based chains, there are two different types of accounts - Externally Owned Accounts (EOA) and Smart Contracts. EOAs are able to authenticate messages by signing with the private key from it’s associated ECDSA key pair. However, since smart contracts are given a predetermined address during contract creation, it does not have easy access to a private key to sign messages with.

To solve this problem, the ERC-1271 contract signatures standard was proposed in 2018. With this standard, smart contracts can implement restrictions/checks for what constitutes a valid signature, and apps can call contract.isValidSignature to verify if some action was authorized by the smart contract.

interface IERC1271 { /// If valid, return bytes4(0x1626ba7e), else return anything else function isValidSignature(bytes32, bytes calldata) external returns (bytes4); }

In the context of smart contract accounts (SCAs), ERC-1271 is very handy as it enables users of SCAs to use signature based applications exactly how EOAs do. These applications include OpenSea, and most of DeFi (which rely on the token approval → call UX).

/// ERC1271 Reference Implementation: https://eips.ethereum.org/EIPS/eip-1271 function isValidSignature( bytes32 _hash, bytes calldata _signature ) external override view returns (bytes4) { // Validate signatures if (recoverSigner(_hash, _signature) == owner) { return 0x1626ba7e; // 1271_MAGIC_VALUE } else { return 0xffffffff; } }

Most SCAs implement ERC-1271 by using its reference implementation shown above. Engineering wise, it’s a lightweight implementation, and it makes client integrations much easier since we could reuse methods such as signTypedData, signMessage , eth_signTypedData_v* and personal_sign the same way it’s used for EOAs.

However, in the case that the same address owns multiple SCAs, and the application doesn’t include the origin address of the interaction, the same signature would be valid across both accounts for that application.

Because this vulnerability is only possible with a combination of SCA and application, how bad this vulnerability would be depends on what applications this interaction would work with. The first application we looked into was Permit2, which is public infrastructure built by Uniswap that improves the security and UX of ERC20 token approvals flows across the entire industry, and thus is widely used today.

The code block below shows the structs that the Permit2 signature covers. Notably, address owner, the address that tokens are pulled from, is not covered by the signature and is passed as an argument in calls to Permit2 instead.

/// Permit2 structs /// From: https://github.com/Uniswap/permit2/blob/cc56ad0f3439c502c246fc5cfcc3db92bb8b7219/src/interfaces/IAllowanceTransfer.sol#L44-L54 struct PermitDetails { address token; uint160 amount; uint48 expiration; uint48 nonce; } struct PermitSingle { PermitDetails details; address spender; // recipient of token transfer uint256 sigDeadline; }

How an attacker would take advantage of this signature replay vulnerability looks something like:

  1. Bob requests a payment of X tokens from Alice, who owns n SCAs, and requests for it to be done via Permit2

  2. After Alice signs the first permit, Bob can replay this permit across all of Alice’s SCAs to receive n * X tokens total.

Diagram of Bob’s attack on Alice that owns 2 SCAs
Diagram of Bob’s attack on Alice that owns 2 SCAs

During this process, we made a proof-of-concept to confirm this vulnerability. That can be found here: replay-sig-poc

As part of our investigation, we discovered that:

  1. Multiple SCAs were at risk.

    1. Besides our LightAccount, other SCAs included Zerodev’s Kernel, Biconomy, Soul Wallet, eth-infinitism’s EIP4337Fallback for Gnosis Safes, AmbireAccount, OKX’s SmartAccount, Argent’s BaseWallet, and Fuse Wallet.

  2. Multiple applications were at risk:

    1. Permit2 - Signature based transfers are replayable. However, most Permit2 usage is to Universal Router, and any way to take advantage of this would require a standalone critical vulnerability in Universal Router.

    2. Cowswap - Trades using the ERC-1271 path are replayable. The signature covers address recipient, so the risk here would at most be stale prices and/or some losses to MEV.

  3. Gnosis Safe was not vulnerable to this attack vector.

At this point, we disclosed this to the SCAs and applications via a telegram group and discovered that curiousapple had also discovered the same issue a month prior and was collaborating with Frangio and other SCA technical experts on a fix. All in all, the full list of affected combinations of SCAs and applications thus far are shown below:

Impact: Accounts and Applications
Impact: Accounts and Applications

Note: For Argent, as they are a mobile app and generate the signer per device, it is impossible for 2 SCAs to be owned by the same EOA, thus the signature replay attack does not work against Argent. However, projects that fork Argent’s contracts without forking their entire architecture could be at risk and should either adopt Argent’s wallet architecture, or ship a fix.

There were two SCAs fixes that were proposed. SCA builders should note that they should implement one of these two solutions to prevent the replay attack above:

// Solution 1: // Wrap incoming digest with SCA EIP712 domain function isValidSignature(bytes32 digest, bytes calldata sig) external view returns (bytes4) { bytes32 domainSeparator = keccak256( abi.encode( _DOMAIN_SEPARATOR_TYPEHASH, _NAME_HASH, _VERSION_HASH, block.chainid, address(SCA) ) ); bytes32 wrappedDigest = keccak256(abi.encode("\x19\x01", domainSeparator, digest)); return ECDSA.recover(wrappedDigest, sig); } // Solution 2: // Wrap incoming digest with just the address of the SCA function isValidSignature(bytes32 digest, bytes calldata sig) external view returns (bytes4) { bytes32 wrappedDigest = keccak256(abi.encode(digest, address(SCA)); return ECDSA.recover(wrappedDigest, sig); }

Both solutions would prevent the ERC-1271 signature replay attack. The latter solution is more lightweight, but would mean that wallet clients would have to display an opaque hash for users to sign. The former fix is an easier path to ensuring that signatures would not be opaque to the user which is why we opted for the former fix for LightAccount. Most other SCAs have also opted for the same fix.

Big thanks to OKX for paying out a bug bounty to Howy for this issue!

Congratulations to curiousapple for receiving bug bounties from Ambire, Instadapp, Biconomy and Cowswap!

Additionally, huge shoutout to:

  1. Dror Tirosh for brainstorming the EIP-712 struct approach fix that most SCAs adopted

  2. Frangio for sharing more background on ERC-1271 and the huge push to update ERC-1271’s reference implementation via the EIP committee

  3. Ivo (Ambire) for his deep dive into technical implementation differences between the two proposed solutions

  4. Vectorized for putting up and funding a 0.5 ETH bounty for a client implementation of the nested EIP-712 solution

  5. Juno (ChainLight) for rising to the above challenge, shipping a client implementation of the nested EIP712 solution and claiming Vectorized’s bounty

  6. David Eiber for his help with brainstorming related vulnerabilities, indexing affected SCAs and protocols, and creating PoCs

  7. Yoav Weiss for his help during the whole process including connecting us with security researchers and other affected SCAs and applications

Section background image

Build blockchain magic

Alchemy combines the most powerful web3 developer products and tools with resources, community and legendary support.

Get your API key