new·The score now tells you which way it movedA brain's exam only ever grows: its own material writes questions, and so does every question a real caller asked and did not get answered. The score is a percentage over that growing set, so a brain that learned more could post a smaller number — and this week three did. One of them answered two MORE questions than the week before and showed eighteen points less. Printed as a single percentage, that reads as decline to a reader and as punishment to anyone who contributes material.all news →
mozg.beta
Sign in

Smart Contract Auditor · all subjects

Signatures and replay

4 notes, read out of this brain and free to use. Each one was extracted from a source and is re-checked against its exam.

How did the Wormhole bridge lose $326M to a signature bug?

February 2022, Solana side. Wormhole's `verify_signatures` instruction used a deprecated `secp256k1` verification wrapper that didn't validate it was reading the genuine `secp256k1` sysvar account — an attacker could pass a fake sysvar account containing whatever "verified guardian signatures" they wanted. With forged guardian approval, they submitted a valid-looking message to mint 120,000 wETH with no ETH locked on Ethereum. Jump Crypto later backstopped the 120k ETH. Lessons: (1) signature verification bugs are often in the PLUMBING (which account/sysvar is trusted), not the crypto; (2) on Solana, every account passed to an instruction must be validated against expected program IDs/addresses; (3) on EVM, the analogous bug is accepting a signer address/verifier contract as calldata. Detection signal EVM-side: `ecrecover` results compared to addresses derived from user-supplied data without domain separation or trusted-verifier pinning.

What makes an off-chain signature replay-proof?

Four ingredients, and missing any one is a finding. (1) EIP-712 domain separator binding the signature to `chainId` + `verifyingContract` — without it, a mainnet signature replays on a testnet/fork or a sibling deployment. (2) A per-signer nonce stored on-chain (`usedNonces[signer][nonce] = true` or sequential `nonces[signer]++`) — without it, the same signed order/permit executes twice. (3) A deadline (`require(block.timestamp <= deadline)`) so stale signatures can't be front-run months later. (4) For airdrop/allowlist claims, bind `msg.sender` or the recipient into the signed payload, else anyone replays the proof with their own address. Detection signals: `ecrecover` without a nonce mapping; EIP-712 domain missing `chainId` (breaks on forks — chainId should be read live, not hardcoded at deploy); permit-style functions without deadline. Classic victims: early airdrop contracts where one signature claimed for many, and bridges replaying messages across chains.

What ecrecover pitfalls should an auditor check?

Three recurring ones. (1) Signature malleability: for a valid `(r, s, v)`, the flipped `(r, n - s, v^1)` also verifies for the same signer. If your replay protection keys on the SIGNATURE bytes (e.g., `usedSignatures[sigHash]`) instead of a nonce, the malleated twin replays. OpenZeppelin's `ECDSA.recover` rejects high-s values; raw `ecrecover` does not — so either use OZ or check `s <= secp256k1n/2` yourself (that's what EIP-2 did at protocol level). (2) `ecrecover` returns `address(0)` on invalid input instead of reverting — `require(signer != address(0))` or, worse, an uninitialized/zero "owner" passing the check. (3) Precompile returns are unchecked when using inline assembly with `staticcall` — check the success flag and return size. Also remember `ecrecover` only proves EOA signatures; for contract wallets you need ERC-1271 `isValidSignature`, and permits must handle both paths.

Is ERC-2612 permit() a front-running / DoS vector?

Yes, in a specific benign-but-breaking way: `permit` signatures are public in the mempool, so anyone can submit your permit before your bundled `depositWithPermit` transaction; the permit then succeeds, and your transaction's own `permit()` call reverts on the already-used nonce, reverting the whole bundle. Protocols integrating permit should wrap it in try/catch: `try token.permit(...) {} catch {}` and then just call `transferFrom` — if the permit was already spent, allowance is already set. Detection signal: integrations that call `permit` unconditionally without a fallback. Second pitfall: tokens whose `permit` exists but is non-standard (DAI-style `permit` uses an `allowed` bool instead of a value); verify against the actual token, not the interface. Also note permit does not equal approval safety: a signed permit is an approval anyone can execute — treat leaked permits like leaked approvals.

Give your agent this brain