Reproduced Exploit

Term Finance: a zero signature authenticates as address(0) and withdraws 1,000 tokens

Apr 2023•Other•auth•2 min read

Chain

Other

Category

auth

Date

Apr 2023

Source

AuditVault

EVM Playground

Source-level debugger — step opcodes and Solidity in sync

evm-hack-analyzer

The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.

Loading fork state…

Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 26297-no-checks-for-non-zero-output-address-when-using-ecrecover-s. Standalone Foundry PoC and full write-up: 26297-no-checks-for-non-zero-output-address-when-using-ecrecover-s_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/auth/signature-validation

Reproduction: local synthetic, no fork. authenticate is the audited body. A 65-byte zero signature and user = address(0) pass, and the vault sends 1,000 tokens. Trace: output.txt.

Key info#

ImpactHIGH — 1,000 tokens move with no private key
ProtocolTerm Finance — Authenticator.authenticate
Vulnerable codereturn ecrecover(check, v, r, s) == termAuth.user with no zero check
Bug classecrecover of an invalid signature returns address(0)
FindingSigma Prime, Term Finance contracts, April 2023 · TRM-07
Reportsigp/public-audits term1 review

Root cause#

ecrecover returns address(0) when the signature is invalid. The audited function treats that as success whenever termAuth.user is also address(0):

SOLIDITY
return ecrecover(check, v, r, s) == termAuth.user;

A 65-byte buffer of zeros decodes to v = 0, r = 0, s = 0. That is not a signature from any key. ecrecover returns address(0), the comparison is true, and the nonce is consumed so the call looks like a normal authentication. The vault then transfers 1,000 tokens to the caller.

Attack path#

flowchart TD A["Vault holds 1,000 tokens"] --> B["Caller builds a 65-byte zero signature"] B --> C["termAuth.user is address 0 and nonce is 1"] C --> D["withdraw asks authenticate to check the signature"] D --> E["ecrecover returns address 0 and matches the user"] E --> F["Vault transfers 1,000 tokens to the caller"] F --> G["Caller holds 1,000 tokens and the vault is empty"]

PoC#

BASH
cd 26297-no-checks-for-non-zero-output-address-when-using-ecrecover-s_exp
forge test -vv

The exploit asserts the attacker balance is 1_000 ether and the vault balance is 0. The control authenticator reverts with InvalidSignature when ecrecover returns address(0). The same withdraw moves nothing, and the 1,000 tokens stay in the vault.

Remediation#

Reject a zero recovery address before comparing it to the claimed user:

SOLIDITY
address recovered = ecrecover(check, v, r, s);
if (recovered == address(0)) revert InvalidSignature();
return recovered == termAuth.user;

Sigma Prime retested commit e883f0e and the function now reverts with InvalidSignature.

References#


Sources & further analysis#

Reproductions & code

Alerts & third-party analyses

  • Web3Sec X hacked database: search.
  • Rekt leaderboard: search.
  • Solodit incident search: search.

These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.