Skip to content

fix(evm): derive internal CREATE addresses from caller nonce behind a fork - #1353

Closed
bysomeone wants to merge 5 commits into
33cn:masterfrom
bysomeone:feat/evm-contract-create-v2
Closed

bysomeone wants to merge 5 commits into
33cn:masterfrom
bysomeone:feat/evm-contract-create-v2

Conversation

@bysomeone

@bysomeone bysomeone commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

What

Fixes the EVM-internal CREATE address derivation described in #1351.

opCreate derived the address from hash(txHash || origin), so every internal CREATE in a transaction got the same address. EVM.Create additionally accepted an existing account, so a second child silently overwrote the first while the transaction still reported success.

How

New dapp fork ForkEVMContractCreateV2. Like ForkEVMTransferRevert, it is registered at MaxHeight (off by default) and the templates set -1, so every chain opts in with a concrete height in its deployment configuration. Enabling it from genesis on an existing chain would rewrite historical execution. Fork-off execution is unchanged.

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.
  • CREATE / CREATE2 return ErrContractAddressCollision and consume 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 the gas 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.

Testing

  • go test ./plugin/dapp/evm/...
  • New tests cover:
    • fork-off: the address is exactly hash(txHash||origin) and no nonce is written to contract state
    • fork-on: addresses checked against crypto.CreateAddress, twice in one transaction
    • CREATE2 collision returns zero
    • nonce persistence across transactions and executor instances
    • a failed CREATE still consumes a nonce
  • 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).

  • Enable it at the same height as ForkEVMTransferRevert. This fork keeps the nonce of a failed CREATE; only the transfer-revert fork rolls back the value attached to it, so 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.

… 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>
@bysomeone
bysomeone force-pushed the feat/evm-contract-create-v2 branch from b121b23 to 86b4b37 Compare November 11, 2026 06:22
bysomeone and others added 2 commits November 11, 2026 14:36
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>
@bysomeone
bysomeone requested review from 33cn and vipwzw and removed request for 33cn and vipwzw November 11, 2026 06:57
bysomeone and others added 2 commits November 11, 2026 15:16
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>
@bysomeone

Copy link
Copy Markdown
Collaborator Author

Superseded by #1354, which folds the CREATE fix, the missing opcodes (#1352) and the gas model into a single fork, ForkEVMCompat, behind one activation height.

@bysomeone bysomeone closed this Nov 11, 2026
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