ERC-1271 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.
Technical Details
ERC-1271 Contract Signatures
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 Signature Replay Vulnerability
/// 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:
Bob requests a payment of
X
tokens from Alice, who ownsn
SCAs, and requests for it to be done via Permit2After Alice signs the first permit, Bob can replay this permit across all of Alice’s SCAs to receive
n * X
tokens total.
During this process, we made a proof-of-concept to confirm this vulnerability. That can be found here: replay-sig-poc
Impact
As part of our investigation, we discovered that:
Multiple SCAs were at risk.
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.
Multiple applications were at risk:
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.
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.
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:
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.
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.
Acknowledgements
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:
Dror Tirosh for brainstorming the EIP-712 struct approach fix that most SCAs adopted
Frangio for sharing more background on ERC-1271 and the huge push to update ERC-1271’s reference implementation via the EIP committee
Ivo (Ambire) for his deep dive into technical implementation differences between the two proposed solutions
Vectorized for putting up and funding a 0.5 ETH bounty for a client implementation of the nested EIP-712 solution
Juno (ChainLight) for rising to the above challenge, shipping a client implementation of the nested EIP712 solution and claiming Vectorized’s bounty
David Eiber for his help with brainstorming related vulnerabilities, indexing affected SCAs and protocols, and creating PoCs
Yoav Weiss for his help during the whole process including connecting us with security researchers and other affected SCAs and applications
Related articles
Preparing for the Agave 2.0 Upgrade
What is RIP-7212? Precompile for secp256r1 Curve Support
RIP-7212 is a core change in the Ethereum protocol that opens up a way to have cheap, secure, and fast P256 curve verification with a precompiled contract.
Base Goerli Support Ending 2/9 - Migrate to Sepolia
Base's Goerli testnet is scheduled to be spun down on February 9th. We will keep our nodes running for an extra week after this date.