Reproduced Exploit

StaticHyVM — delegatecall to an account with no code returns success

doDelegateCall delegatecalls hyvm and reverts only when the call returns success == false. The EVM reports success for an account that has no code and runs nothing. If hyvm was destroyed, or was never deployed, every call still "succeeds". A vault that releases funds on that success sends 1,000 tok…

Jun 2023•Other•logic•4 min read

Loss

1,000 vault tokens released (output.txt:8). The control vault still holds 1,000 (output.txt:9).

Chain

Other

Category

logic

Date

Jun 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: 29658-lack-of-contract-existence-check-on-statichyvm-trailofbits-n. Standalone Foundry PoC and full write-up: 29658-lack-of-contract-existence-check-on-statichyvm-trailofbits-n_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/logic/missing-check · vuln/dependency/unsafe-external-call

Reproduction: Cheatcode-free. doDelegateCall is the audited function. hyvm is an address with no code. The delegatecall returns success, the guard payload never runs, and a vault that treats success as permission transfers 1,000 tokens. The same call with a code.length check reverts and the control vault keeps its 1,000.

Key info#

Loss1,000 vault tokens released (output.txt:8). The control vault still holds 1,000 (output.txt:9).
Vulnerable contractStaticHyVM.doDelegateCall
AttackerAny caller of a consumer that treats a successful HyVM call as authorization
Attack contractExploit.run
Attack txLocal synthetic, one transaction
Chain · dateTrail of Bits Nested / Tetris HyVM review, June 2023
Compilersolc 0.8.27, cancun, via_ir
Bug classMissing contract-existence check on delegatecall

TL;DR#

doDelegateCall delegatecalls hyvm and reverts only when the call returns success == false. The EVM reports success for an account that has no code and runs nothing. If hyvm was destroyed, or was never deployed, every call still "succeeds". A vault that releases funds on that success sends 1,000 tokens. Checking hyvm.code.length first reverts, and those tokens stay.

The vulnerable code#

From the review, StaticHyVM.sol lines 30–36:

SOLIDITY
function doDelegateCall(bytes calldata payload) public returns (bytes memory) {
    if (msg.sender != address(this)) revert OnlySelf();
    // @> VULN: hyvm has no code; delegatecall still returns success and the guard payload never executes
    // FIX: if (hyvm.code.length == 0) revert EmptyCode();
    (bool success, bytes memory data) = hyvm.delegatecall(payload);
    if (!success) revert DelegateFailed();
    return data;
}

OnlySelf is unchanged. The call is reached through run, which does this.doDelegateCall(payload).

Root cause#

delegatecall to an empty account is defined to return success and empty return data. The function checks success and does not check that hyvm has code. A payload that was supposed to revert, or to set a flag, is skipped, and the caller continues.

Preconditions#

  • hyvm is 0x000000000000000000000000000000000000bEEF. It has no runtime code.
  • The payload is 0x01, a guard a real hyvm would have to execute.
  • Two vaults each hold 1,000 tokens. Both release their balance only after run returns.
  • The control runner is the same function plus if (hyvm.code.length == 0) revert EmptyCode().

Attack walkthrough#

  1. Mint 1,000 tokens into each vault.
  2. The open vault calls StaticHyVM.run.
  3. doDelegateCall delegatecalls the empty address. success is true. DelegateFailed is not thrown.
  4. The vault transfers 1,000 tokens to the caller (output.txt:8). Its balance is 0.
  5. The control vault calls the checked runner. code.length == 0 reverts EmptyCode before the delegatecall.
  6. The control vault still holds 1,000 (output.txt:9).

Diagrams#

flowchart TD A["Vault holds 1000 tokens"] --> B["withdraw calls StaticHyVM.run"] B --> C["doDelegateCall delegatecalls hyvm"] C --> D["hyvm has no code so success is true"] D --> E["Vault transfers 1000 to the caller"] E --> F["Checked runner sees code.length zero and reverts"] F --> G["Control vault still holds 1000"]

Impact#

The caller receives 1,000 tokens that the missing guard was supposed to keep in the vault. The call did not execute the payload. The codesize check stops the same withdrawal.

Remediation#

Before delegatecall, require hyvm.code.length != 0. Do not treat a successful low-level call to an empty account as an executed payload.

How to reproduce#

BASH
cd 29658-lack-of-contract-existence-check-on-statichyvm-trailofbits-n_exp
forge test -vv

Sources#


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.