Signature Replay Attacks
Signature Replay Attacks
The signature replay attack that led to the theft of 20 million $OP tokens from Wintermute exposes critical vulnerabilities in the management of digital signatures on blockchain platforms. This incident was primarily triggered by a signature replay but was compounded by an additional transactional error with Optimism, a Layer-2 Ethereum scaling solution. In this case, the tokens were mistakenly sent to a multisignature wallet address that had not yet been properly initialized on the Optimism network.
For a more detailed report on this incident, you can read further on Blockworks and Decrypt:
- Blockworks Report on the Wintermute and Optimism Incident
- Decrypt's Coverage of the Optimism Token Theft
Digital signatures play a pivotal role in blockchain technology, serving to identify the signer of data and to ensure the integrity of that data. When users initiate transactions, they use their private keys to sign them. This allows anyone to verify that the transactions truly originate from the stated accounts. Additionally, smart contracts often employ the ECDSA
algorithm to authenticate off-chain signatures before performing critical functions such as token minting or transfers.
There are generally two common types of signature replay attacks:
- Standard Replay: A signature meant for a single use was repeatedly exploited, leading to thousands of unauthorized mints of the NBA’s “The Association” series NFTs. For details on this signature replay attack, read Decrypt’s report on the exploit: NBA Botches Ethereum NFT Drop as 'The Association' Suffers Exploit - Decrypt.
This article covers the security vulnerabilities that were exploited during the NFT drop, leading to thousands of unauthorized mints.
- Cross-chain Replay: A signature intended for use on one blockchain is reused on another. This type of attack was exploited in the theft of 20 million $OP tokens from Wintermute.
Example of a Vulnerable Contract
The SigAuth
contract below is an ERC20
token contract that includes a mint function vulnerable to signature replay attacks. It uses off-chain signatures to allow whitelisted addresses to mint a specified number of tokens. The contract stores a signer
address to verify the authenticity of signatures.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
contract SigAuth is ERC20, Ownable {
address public authorizedSigner;
constructor() ERC20("SignatureToken", "SIGT") {
authorizedSigner = msg.sender;
}
function vulnerableMint(address recipient, uint amount, bytes memory signature) public {
bytes32 messageHash = keccak256(abi.encodePacked(recipient, amount));
bytes32 ethSignedMessageHash = ECDSA.toEthSignedMessageHash(messageHash);
require(ECDSA.recover(ethSignedMessageHash, signature) == authorizedSigner, "Invalid signature!");
_mint(recipient, amount);
}
// Other helper functions...
}
Note: The vulnerableMint()
function does not check if the signature
has been used before, allowing the same signature to be reused for unlimited token minting.
Prevention Measures
To prevent signature replay attacks, you can use the following methods:
-
Tracking Used Signatures: Record signatures that have been used for operations like token minting to prevent their reuse.
// Record the minted addresses
mapping(address => bool) public alreadyMinted;
function secureMint(address recipient, uint amount, bytes memory signature) public {
require(!alreadyMinted[recipient], "Tokens already minted for this address");
bytes32 messageHash = keccak256(abi.encodePacked(recipient, amount));
bytes32 ethSignedMessageHash = ECDSA.toEthSignedMessageHash(messageHash);
require(ECDSA.recover(ethSignedMessageHash, signature) == authorizedSigner, "Invalid signature!");
alreadyMinted[recipient] = true;
_mint(recipient, amount);
} -
Including Nonce and ChainID: Incorporate a
nonce
(which increments with each transaction) and thechainID
in the signature message to prevent both standard and cross-chain replays.uint public nonce = 0;
function nonceMint(address recipient, uint amount, bytes memory signature) public {
bytes32 messageHash = keccak256(abi.encodePacked(recipient, amount, nonce, block.chainid));
bytes32 ethSignedMessageHash = ECDSA.toEthSignedMessageHash(messageHash);
require(ECDSA.recover(ethSignedMessageHash, signature) == authorizedSigner, "Invalid signature!");
_mint(recipient, amount);
nonce++;
} -
Using EIP-712 for Structured Data: Implement EIP-712 to create a more secure and structured data signing experience, which helps prevent signature replays across different contexts and contracts.
// Utilizing EIP-712 to create a typed structured data signature
using EIP712 for bytes32;
function eip712Mint(address recipient, uint amount, bytes memory signature) public {
bytes32 structHash = keccak256(abi.encode(
keccak256("Mint(address recipient,uint256 amount,uint256 nonce)"),
recipient,
amount,
nonce
));
bytes32 digest = _hashTypedDataV4(structHash);
require(ECDSA.recover(digest, signature) == authorizedSigner, "Invalid EIP-712 signature!");
_mint(recipient, amount);
nonce++;
}
You can find more detailed implementation guidelines and tools in OpenZeppelin's EIP-712 documentation.
4.Implementing CIP-23 for Cross-Chain Safety: CIP-23 is an adaptation of Ethereum's EIP-712 for Conflux core space, designed to enhance security in cross-chain operations. It introduces specific measures to prevent replay attacks, ensuring that signatures for EVM-compatible chains cannot be replayed for Conflux core space, and vice versa.
More information and detailed guidelines can be found on the Conflux CIP-23