Skip to content

feat(evm): fix internal CREATE derivation and add PUSH0 behind ForkEVMCompat - #1354

Open
bysomeone wants to merge 7 commits into
33cn:masterfrom
bysomeone:feat/evm-compat
Open

bysomeone wants to merge 7 commits into
33cn:masterfrom
bysomeone:feat/evm-compat

Conversation

@bysomeone

@bysomeone bysomeone commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

What

Two EVM incompatibilities, both gated by one new dapp fork, ForkEVMCompat:

  1. Internal CREATE derived a colliding address (EVM 合约内部 create(Solidity new)地址冲突:同一笔交易内 factory 创建的多个子合约地址相同,后者覆盖前者,且交易显示成功 #1351). opCreate used hash(txHash || origin), so every internal create in a transaction received the same address, and EVM.Create accepted an existing account: a second child silently overwrote the first while the transaction still reported success.
  2. PUSH0 (0x5f) and BASEFEE (0x48) were missing (evm: PUSH0 (0x5f) and other Shanghai/Cancun opcodes are missing from the instruction set #1352). Solidity 0.8.20 and later emit PUSH0 by default, so a contract compiled with a current solc cannot be deployed at all.

How

ForkEVMCompat is registered at MaxHeight (off by default) and the templates keep it at -1, so each chain opts in with a concrete height. Enabling it from genesis on an existing chain would rewrite historical execution.

Fork on:

  • CREATE address = CreateAddress(executing account, its nonce). The caller nonce is incremented before any create snapshot and persisted in contract state, so it survives across transactions and failed creates. New contracts start at nonce 1 (EIP-161), an account with a nonce is no longer treated as non-existent, and CREATE / CREATE2 return ErrContractAddressCollision when the target already has a nonce or code.
  • CREATE / CREATE2 forward 63/64 of the remaining gas (EIP-150), forward value with the same precision conversion CREATE uses, and roll back on ErrCodeStoreOutOfGas instead of keeping the account.
  • PUSH0 pushes zero and BASEFEE returns zero, both at 2 gas. The chain has no base-fee mechanism.

Fork off, execution is unchanged: the previous CREATE address derivation, the Berlin instruction set, the gas constants and the refund handling are untouched.

Address construction for the new path bypasses the chain33 address driver: a btc-format driver cannot parse a 0x hex string and produced an all-zero raw address.

Testing

  • go test ./plugin/dapp/evm/... (bn256/cloudflare fails on macOS with a dyld: missing LC_UUID load command error; the package does not depend on anything here)
  • New tests cover, for fork off and fork on:
    • the CREATE address matching crypto.CreateAddress, twice in one transaction; nonce persistence across transactions and executor instances; a failed CREATE still consuming a nonce
    • CREATE2 collision returning zero
    • PUSH0 / BASEFEE being invalid opcode when off and executing when on
    • the fork being registered off by default, and the value a failed CREATE keeps when only ForkEVMCompat is on
  • go test ./plugin/dapp/evm/executor/ -shuffle=<seed> is order-independent; a pre-existing global address-driver leak from query_test.go was fixed for that.

Deployment note

The fork has to be enabled with a real height in the deployment repository; the templates here keep it off (-1). Every existing chain configuration must list ForkEVMCompat once this release is deployed — the fork is registered in code, and a configuration that omits it fails the fork check at startup.

  • Enable it at the same height as ForkEVMTransferRevert. That fork rolls back the value attached to a failed CREATE, which this one alone does not do; the full Ethereum semantics need both.
  • Chains that accept ETH-signed transactions must configure [exec.sub.evm] addressDriver="eth". The internal CREATE path here does not depend on the driver, but the pre-existing top-level ETH-signed deploy path still does.
  • Contracts deployed before the fork have no nonce record (treated as 0), so their first post-fork internal CREATE does not match Ethereum exactly; contracts deployed after the fork match exactly.

Not covered

No gas metering changes: cold/warm access accounting, net SSTORE metering and EIP-3529 refunds are not part of this fork, so gas numbers differ from Ethereum for contracts that are sensitive to them. MCOPY, TLOAD/TSTORE, DIFFICULTY/PREVRANDAO, the precompile table and the SELFDESTRUCT pricing are unchanged.

bysomeone and others added 6 commits November 11, 2026 14:09
… fork

EVM-internal CREATE used hash(txHash||origin), so every CREATE inside one
transaction produced the same address. EVM.Create also accepted an existing
account silently, so a factory deploying two children in one call had the
second overwrite the first while the transaction still reported success.

Gate the Ethereum behaviour behind ForkEVMContractCreateV2. Registered at
MaxHeight (off by default, opt-in per chain) like ForkEVMTransferRevert:
enabling it from genesis on an existing chain would rewrite historical
execution.

- CREATE address = CreateAddress(executing account, its nonce); the caller
  nonce is incremented before any create snapshot and persisted in contract
  state, so it survives across transactions and failed creates.
- Newly created contracts start at nonce 1 (EIP-161); an account with a nonce
  is no longer treated as non-existent.
- CREATE/CREATE2 fail with ErrContractAddressCollision (consuming the
  forwarded gas) when the target already has a nonce or code.
- ErrCodeStoreOutOfGas rolls the creation back instead of keeping the account
  and pushing its address.
- CREATE/CREATE2 forward 63/64 of the remaining gas (EIP-150); CREATE2 no
  longer halves it, and converts value precision for ETH-signed transactions
  the same way CREATE does.

Address construction for the new path bypasses the chain33 address driver: a
btc-format driver cannot parse a 0x hex string and produced an all-zero raw
address.

Fork-off execution is unchanged; the nonceChange KV write is gated on the same
fork.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
withdrawTicketBalance compared the post-withdraw coins balance with the
pre-withdraw ticket balance. The ticket balance also grows with mining
rewards, so the check only held when it happened to be exactly 100000 and
failed intermittently otherwise (observed 100005 vs 100001 on the same head).

Record the coins balance before the withdraw and assert the increase is the
withdrawn amount, allowing a small tolerance for fees and rounding.

Co-authored-by: Claude <noreply@anthropic.com>
The withdraw moves 100000 coins minus a ~0.1 coin fee, so the expected
increase is exactly 99999.9 and the previous 99999.9 lower bound sat on the
floating-point boundary: the same balances can compare either side of it
depending on their magnitude and on the awk implementation.

Use a 99999 lower bound instead, and make jq fail on a missing balance rather
than silently comparing a null as zero.

Co-authored-by: Claude <noreply@anthropic.com>
ForkEVMContractCreateV2 and ForkEVMTransferRevert are independent forks, but
the full Ethereum semantics need both: this fork keeps the nonce of a failed
CREATE, and the transfer-revert fork rolls back the value attached to it.
Add a test that enables both, and document the pairing next to the fork.

Co-authored-by: Claude <noreply@anthropic.com>
The fork now covers the whole Ethereum compatibility set (CREATE semantics,
new opcodes and the gas schedule), so a semantic name without a version
suffix is clearer: there is no "V1" fork to pair with.

Co-authored-by: Claude <noreply@anthropic.com>
Solidity 0.8.20 and later emit PUSH0 by default, so a contract built with a
current solc cannot be deployed without it. The fork selects a Berlin-based
instruction set that adds PUSH0 (0x5f) and BASEFEE (0x48); the chain has no
base-fee mechanism, so BASEFEE pushes zero. Both cost 2 gas.

Fork off keeps the existing instruction set, so the two bytes remain invalid
and nothing else changes.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@bysomeone bysomeone changed the title feat(evm): align CREATE, opcodes and gas metering behind ForkEVMCompat feat(evm): fix internal CREATE derivation and add PUSH0 behind ForkEVMCompat Nov 12, 2026
@bysomeone
bysomeone marked this pull request as ready for review November 12, 2026 07:49
@bysomeone
bysomeone requested review from 33cn and vipwzw November 12, 2026 07:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant