Skip to content

DTVM interpreter build credits a created account with 0x3fffffffa4dabfff wei (~4.6e18) out of thin air (total supply doubles) #593

Description

@erxiaozhou

Bug Report

DTVM_INTERP_CREATE_GARBAGE_BALANCE.zip

The DTVM interpreter build (-m interpreter) credits a freshly CREATEd
account with a non-zero balance out of thin air, violating EVM balance
conservation. With the test case below, a created account receives
0x3fffffffa4dabfff wei (~4.6e18) that no account is debited for — the
post-state total supply is roughly double the pre-state total.

1. Minimal reproduce step (Required)

The test case is a 26-byte contract (22 instructions) executed by the
interpreter build, with an 80-byte calldata and a two-account pre-state.
All inputs are packaged in the attached archive
DTVM_INTERP_CREATE_GARBAGE_BALANCE.zip — the repro/ directory contains the
three input files plus a run.sh that runs the exact command below and prints
the offending balance.

File contract.evm.hex (contract bytecode under test):

5f5f5f5f5f5f5f63304130305f5ff13030f45f5f5ff05f5f5ff0

Disassembly:

[ 0] PUSH0      [ 1] PUSH0      [ 2] PUSH0      [ 3] PUSH0
[ 4] PUSH0      [ 5] PUSH0      [ 6] PUSH0      [ 7] PUSH4 0x30413030
[ 8] PUSH0      [ 9] PUSH0      [10] CALL       [11] ADDRESS
[12] ADDRESS    [13] DELEGATECALL               [14] PUSH0
[15] PUSH0      [16] PUSH0      [17] CREATE     [18] PUSH0
[19] PUSH0      [20] PUSH0      [21] CREATE

Semantics: CALL to 0x30413030 with zero gas (returns immediately);
DELEGATECALL then re-enters the contract itself with gas = ADDRESS and
retOffset = 1, so the contract recurses into itself until gas is exhausted;
each recursion level (and the top level) then performs two CREATEs with
value = 0, offset = 0, size = 0 (empty initcode).

File calldata.hex (0x01 repeated 80 times):

0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101

File state.json (pre-state):

{
  "accounts": {
    "000000000000000000000000000000ca1100f022": {
      "balance": "0000000000000000000000000000000000000000000000000000000000000000",
      "code": "0x5f5f5f5f5f5f5f63304130305f5ff13030f45f5f5ff05f5f5ff0",
      "nonce": 0,
      "storage": {}
    },
    "a94f5374fce5edbc8e2a8697c15331677e6ebf0b": {
      "balance": "0000000000000000000000000000000000000000000000003fffffffffffffff",
      "code": "0x",
      "nonce": 0,
      "storage": {}
    }
  },
  "tx_context": {
    "block_base_fee": "0000000000000000000000000000000000000000000000000000000000000010",
    "block_coinbase": "b94f5374fce5edbc8e2a8697c15331677e6ebf0b",
    "block_gas_limit": 10944489199640098,
    "block_number": 1,
    "block_prev_randao": "0000000000000000000000000000000000000000000000000000000000200000",
    "block_timestamp": 1000,
    "chain_id": "0000000000000000000000000000000000000000000000000000000000000001",
    "gas_price": "0000000000000000000000000000000000000000000000000000000000000080"
  }
}

Run the interpreter build (replace <dtvm_interp> with your dtvm binary,
built per Section 6):

<dtvm_interp> --format evm -m interpreter --evm-revision cancun --log-level debug \
  --load-state state.json \
  --sender a94f5374fce5edbc8e2a8697c15331677e6ebf0b \
  --gas-limit 20000000 \
  --contract-address 000000000000000000000000000000ca1100f022 \
  --calldata "$(tr -d '\n\r' < calldata.hex)" \
  --save-state result.json contract.evm.hex
echo $?   # -> 0

Inspect the created account's balance in result.json:

python3 -c "
import json
d = json.load(open('result.json'))
print(d['accounts']['C83ED00DE536FBD81ED08F34B8CDB26DB29BF1A1']['balance'])
"
# -> 0000000000000000000000000000000000000000000000003FFFFFFFA4DABFFF   (expected: all zeros)

2. What did you expect to see? (Required)

The bytecode is valid and halts successfully. Every CREATE in this test case
has value = 0, so each created account must have balance 0; the only
value movement is gas-fee accounting (sender debited the fee, coinbase credited
the fee, base fee burned). The sum of all balances must stay equal to the
sender's pre-state balance 0x3fffffffffffffff. The reference implementations
(Section 4) all create 0xc83ed00de5...1a1 with balance 0.

3. What did you see instead (Required)

The interpreter build exits 0, but credits the created account
0xc83ed00de536fbd81ed08f34b8cdb26db29bf1a1 with 0x3fffffffa4dabfff wei,
while no account is debited for it:

Account Balance (wei)
0xc83ed00d...1a1 (created) 0x3fffffffa4dabfff = 4611686016898220031
sender 0xa94f5374...ebf0b 0x3fffffff67697fff (gas fee only)
coinbase 0xb94f5374...ebf0b 0x4fc09800 (fee only)
contract 0x...ca1100f022 0

Post-state total supply = 9223372034103629822 the pre-state total
4611686018427387903. The increase equals the created account's balance minus
the burned base fee — no account was debited for the created balance.
Reproduced deterministically (3/3 CLI runs; 40/40 executor runs).

4. Error logs / Stack trace

DTVM's CLI does not expose a per-opcode EVM execution trace at any
--log-level. Evidence below: (a) the EELS reference execution of the same
test case, and (b) the cross-EVM comparison.

(a) EELS reference execution (same test case, Cancun):

  • Receipt: succeeded = True (no revert, no out-of-gas),
    cumulative_gas_used = 10918771, 0 logs.
  • 196 accounts in the post-state, including
    0xc83ed00de536fbd81ed08f34b8cdb26db29bf1a1 with balance 0.
  • Top-level instruction trace (stack before each instruction):
[ 0] PUSH0         gas=19977720 stack=[]
[10] CALL          gas=19977699 stack=[0x0, 0x0, 0x0, 0x30413030, 0x0, 0x0]
                   (gas=0, to=0x30413030, value=0, in=0/0, out=0/0)
[11] ADDRESS       gas=19943399 stack=[0x0, 0x0, 0x0, 0x1]
[13] DELEGATECALL  gas=19943395 stack=[0x0, 0x0, 0x0, 0x1, 0xca1100f022, 0xca1100f022]
                   (gas=0xca1100f022, to=self, argsOffset=0, argsSize=0, retOffset=1, retSize=0)
[17] CREATE        gas=   116 stack=[0x0, 0x0, 0x0, 0x0]
                   (value=0, offset=0, size=0)
[21] CREATE        gas=   284 stack=[0x0, f0454382..., 0x0, 0x0, 0x0]
                   (value=0, offset=0, size=0)

(b) Cross-EVM comparison (same test case):

EVM Execution Gas used Accounts c83ed00de5... balance
EELS (reference) SUCCEEDED (succeeded=True) 10,918,771 196 0
evmone 0.11.0 (t8n) SUCCESS (exit 0) ~10.9M 196 0
geth 1.14.9 (statetest) SUCCESS (exit 0) n/a (statetest) 1512 0
DTVM interpreter build SUCCESS (exit 0) 20,000,000 165 0x3fffffffa4dabfff

All implementations execute the same bytecode to a successful halt and create
the same 0xc83ed00de5...1a1 account; only the DTVM interpreter build gives it
a non-zero balance.

5. What is the version of this project you are using? (Required)

DTVM git commit 8268abb94531b2553d20a77fcb08586cfcc014fc.

6. Environment (Required)

  • OS: Ubuntu 20.04.6 LTS (Linux 6.8.0-111-generic x86_64)
  • GCC: 11.4.0
  • Clang: 10.0.0-4ubuntu1
  • LLVM: 20.1.5 (/usr/lib/llvm-20)
  • CMake: 3.28.1
  • Build (interpreter):
    cmake -B build_interpreter_evm -DCMAKE_BUILD_TYPE=Debug -DZEN_ENABLE_EVM=ON -DZEN_ENABLE_SINGLEPASS_JIT=OFF
    cmake --build build_interpreter_evm --parallel 4
    produces build_interpreter_evm/dtvm (use this path as <dtvm_interp> in
    Section 1).
  • Execution command: see Section 1.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions