Apple’s App Store recently promoted a fake Bitcoin wallet app called SpinUp that ended up stealing roughly $1.8 million, despite a developer’s year-long effort to warn them. The crux: SpinUp’s signature verification and replay protections were flawed, opening the door for a signature replay attack that let attackers siphon funds with forged or replayed signatures. For smart contract devs building wallet contracts, this is a critical case study in understanding the nuances of signature replay vulnerabilities and how you can detect and mitigate them in Solidity.
What Happened in the SpinUp Replay Attack?
SpinUp’s exploit was a textbook example of a signature replay attack where malicious actors reused valid signatures to authorize unauthorized transactions on the chain. The wallet contract failed to properly guard against signature malleability and reused nonces, allowing adversaries to:
- Capture signed transactions off-chain
- Modify or replay these signatures on-chain against wallet contracts
- Bypass intended replay protections like nonces or domain separators
Industry reporting pegged the stolen funds at approximately $1.8M, a sizable loss rooted in fundamental problems around ecrecover usage and replay checks.
Why Was SpinUp Vulnerable to a Signature Replay Attack?
At the heart of signature-based wallet authorization is Solidity’s ecrecover function, which takes a signature and recovers the signer's address. However, ecrecover is notorious for subtle edge cases:
-
Signature malleability: Signatures can be tweaked (notably via
vvalues 27 or 28 or canonical vs non-canonical forms) producing different but valid signatures that recover the same signer. - Lack of strict nonce management: Without robust nonce tracking, replayed signatures can validate multiple transactions.
- Absent or inconsistent domain separators: Including fully qualified context in signed messages (like EIP-712 domain hashing) is essential to prevent cross-domain replay.
SpinUp’s contract reportedly did not fully enforce a strict nonce or domain separator, enabling attackers to replay signatures and authorize unauthorized fund transfers.
Breakdown of Vulnerabilities Involved
| Vulnerability | Description | Impact | Mitigation |
|---|---|---|---|
| Signature Malleability |
ecrecover accepts multiple valid signatures for the same message hash |
Attackers tweak signatures slightly yet still validly authorize | Normalize v value; enforce canonical s |
| Nonce Reuse/Skipped Nonces | Nonce tracking on wallet failed or was incomplete | Replayed signatures with same nonce succeed | Use incremental nonce checks, reject duplicates |
| Weak Domain Separation | Signed messages lacked protocol-specific domain context | Signatures valid across different contracts or chains | Implement EIP-712 domain separators |
Technical Deep Dive: Detecting This Replay Attack Pattern With Foundry
If you’re worried your own wallet-like contracts might have similar replay risks, validating signature behavior locally pre-deployment is a must-have safeguard. Here’s an example of building a replay detection test harness using Foundry, the Solidity dev toolkit, which lets you simulate and catch these issues.
Steps to Detect Replay Vulnerability Locally
- Set up Wallet contract with signature verification and nonce tracking
contract Wallet {
mapping(uint256 => bool) public executedNonces;
address public owner;
constructor(address _owner) {
owner = _owner;
}
function execute(uint256 nonce, bytes memory signature) public {
require(!executedNonces[nonce], "Nonce already used");
bytes32 message = keccak256(abi.encodePacked(address(this), nonce));
address signer = recoverSigner(message, signature);
require(signer == owner, "Invalid signature");
executedNonces[nonce] = true;
// Execute logic here (transfer, call, etc.)
}
function recoverSigner(bytes32 message, bytes memory sig) public pure returns (address) {
// EIP-191 prepends \x19Ethereum Signed Message:\n32 here
bytes32 ethMessageHash = ECDSA.toEthSignedMessageHash(message);
return ECDSA.recover(ethMessageHash, sig);
}
}
- Write Foundry test to replay signature
function testReplaySignature() public {
uint256 nonce = 1;
bytes32 message = keccak256(abi.encodePacked(address(wallet), nonce));
bytes memory signature = vm.sign(privateKey, ECDSA.toEthSignedMessageHash(message));
wallet.execute(nonce, signature); // succeeds first time
vm.expectRevert("Nonce already used");
wallet.execute(nonce, signature); // should revert on replay
}
- Add checks for signature malleability variants
You can generate signatures that differ only in v or recover method variants and check if your contract accepts both as valid.
Quick Table: Benefits and Limits of This Testing Approach
| Detection Aspect | Benefit | Limitation |
|---|---|---|
| Nonce replay detection | Ensures one-time use of signatures per nonce | Only detects nonce reuse, not all malleability |
| Signature malleability checks | Can highlight acceptance of multiple valid signatures | Requires crafting alternate signatures manually |
| Domain separation tests | Confirm cross-domain replay prevention | Need to encode fully structured typed data (EIP-712) |
Solidity Best Practices to Avoid Signature Replay Attacks
If you build wallet contracts or use custom signature verification (e.g., meta-transactions, multisigs), be sure to:
Enforce strict nonce incrementing and reject duplicates
Never allow the same nonce to authorize more than one transaction.Normalize signatures (especially
vvalues) and validate canonical forms
Reject signatures using ambiguousvvalues or non-canonical representations.Use EIP-712 typed structured data with domain separators
This binds signatures to your contract and chain, preventing cross-protocol replay.Add expiration timestamps or block numbers in signed payloads
This reduces long-lived replay windows in case of leaks.Test thoroughly with framework tools like Foundry or Hardhat
Simulate replay attacks using signature variants before going live.
Closing Thoughts
Security incidents like SpinUp’s replay attack highlight how seemingly trivial oversights in signature validation and replay protection can lead to multimillion-dollar losses. For any wallet or meta-transaction setup, replay safeguards aren’t optional—they’re fundamental. Leveraging robust nonce schemes, proper domain separation, and signature normalization improves your contract's resilience. Running simulation tests that mimic the attacker’s signature replay tactics is a concrete way to gain confidence before mainnet deployment.
Researching these signature replay mechanisms and simulating exploit patterns always reminds me how nuanced and fragile off-chain signature verification can be. The team I work with at Soken (smart-contract audit firm) found that replay attacks consistently pop up when devs take shortcuts around nonce management or domain boundaries. Safeguarding these measures early on is the difference between surviving flash loan days and being a headline.
Top comments (0)