Skip to content

feat(evm): add MNT support to EVM execution layer#29

Open
ping-ke wants to merge 2 commits into
feature/mnt-statefrom
feature/mnt-evm
Open

feat(evm): add MNT support to EVM execution layer#29
ping-ke wants to merge 2 commits into
feature/mnt-statefrom
feature/mnt-evm

Conversation

@ping-ke

@ping-ke ping-ke commented Jul 5, 2026

Copy link
Copy Markdown

Summary

  • Implement MNT (Multi-Native Token) transfer, gas metering, and precompile contracts in the EVM execution layer
  • Introduce the TokenIDQueried flag mechanism, requiring contracts to acknowledge non-default token transfers by calling the currentMntID precompile
  • Add 5 QKC MNT precompiles: currentMntID, transferMnt, mintMNT, balanceMNT, and deploySystemContract

Background

This PR builds on feature/mnt-state. The EVM layer needs to support:

  1. MNT gas token: Message.GasTokenID allows gas to be paid with a specified token (currently only the QKC default token is supported; non-default tokens revert).
  2. MNT transfer token: Message.TransferTokenID specifies the token used for the transfer. The EVM Transfer() routes to ETH balance or MNT balance based on the token ID.
  3. TokenIDQueried mechanism: A contract receiving a non-default token must call the currentMntID precompile during execution, otherwise the EVM rolls back the transfer. This mirrors pyquarkchain's is_token_id_queried logic, preventing MNT-unaware contracts from silently receiving tokens they cannot handle.

Key Design Decisions

TokenIDQueried propagation: DELEGATECALL / CALLCODE propagate TokenIDQueried back to the caller via ModifyTokenIDQueried(), ensuring correct semantics under proxy contract patterns:

CALL(transferMnt, tokenID=X, value=V)
  └─ temporarily set TxContext.TransferTokenID = X
     └─ CALL(recipient, data, value=V)
          ├─ contract calls currentMntID → sets TokenIDQueried=true → OK
          └─ contract does not call currentMntID → TokenIDQueried=false → transfer reverts

CanTransfer / Transfer signature change: Both functions gain a tokenID uint64 parameter to route value transfers to the correct token balance. All call sites (including tests) are updated to pass the token ID.

QKC Precompile Contracts

Address Contract Gas Description
0x...514b430001 currentMntID 3 Return the current transfer token ID; set TokenIDQueried=true
0x...514b430002 transferMnt dynamic Transfer a specified MNT token and optionally call the recipient
0x...514b430003 deploySystemContract 3 Deploy an MNT system contract (index 2 or 3)
0x...514b430004 mintMNT 9000 Mint a new MNT token (only callable by the NonReservedNativeToken system contract)
0x...514b430005 balanceMNT 400 Query the MNT token balance of an address

MNT System Contracts

Two Solidity system contracts are deployed on-chain via the deploySystemContract precompile. Their bytecode is ported directly from goquarkchain and embedded in contracts_qkc.go.

Address Contract Description
0x514b430000000000000000000000000000000002 NonReservedNativeToken Manages auction-based registration and lifecycle of non-reserved MNT token IDs
0x514b430000000000000000000000000000000003 GeneralNativeToken Manages reserved MNT token IDs with fixed exchange rates

Changed Files

Core execution

File Description
core/evm.go CanTransfer / Transfer take a tokenID param and route to MNT balance methods
core/state_transition.go Add GasTokenID / TransferTokenID to Message; validate non-default gas token in buyGas()
core/vm/interface.go Add GetMntBalance / AddMntBalance / SubMntBalance to StateDB interface
core/vm/evm.go Add GasTokenID / TransferTokenID to TxContext; add TokenIDQueried check in Call(); add runMNTPrecompiledContract dispatch
core/vm/contract.go Add TokenIDQueried bool field to Contract
core/vm/instructions.go CALL / CALLCODE / DELEGATECALL / STATICCALL propagate the flag via ModifyTokenIDQueried()
core/vm/contracts.go Register QKC MNT precompile contracts; add PrecompiledContractWithEVM interface
core/vm/contracts_qkc.go 5 QKC precompile implementations; embeds bytecode for the two MNT system contracts (ported from goquarkchain)

Tests — adapter updates (safe to ignore during review)

These files only update CanTransfer / Transfer call sites to pass the new tokenID parameter, or add MNT stubs to satisfy the updated StateDB interface. No test logic changed.

File Description
core/vm/gas_table_test.go Update CanTransfer / Transfer stubs to include tokenID param
core/vm/interpreter_test.go Update Transfer stub to include tokenID param
eth/tracers/js/tracer_test.go Add MNT stub methods to dummyStatedb

Tests — new and updated logic

File Description
core/vm/contracts_qkc_test.go New: MNT transfer and token ID propagation tests
core/eth_transfer_logs_test.go QKC fork: Transfer() no longer emits EthTransferLog (only SELFDESTRUCT retains it)

Test Plan

  • go test -run=Mnt ./core/vm — all pass
  • go test ./core/vm — all pass
  • go build ./... — build successful

Comment thread core/vm/contracts.go
case rules.IsByzantium:
return PrecompiledAddressesByzantium
default:
return PrecompiledAddressesHomestead

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MNT precompiles are executable through activePrecompiledContracts, but ActivePrecompiles still returns only the base address slices. StateDB.Prepare warms vm.ActivePrecompiles(rules), so under EIP-2929 the MNT precompiles are charged as cold accounts on first access. Please include the MNT addresses when rules.IsQKCMNT. (but need to check the existing behavior to avoid forking)

Comment thread core/vm/evm.go
if isPrecompile {
ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules)
if mntP, ok := p.(PrecompiledContractWithEVM); ok {
ret, gas, err = runMNTPrecompiledContract(evm, mntP, addr, input, gas, caller, value)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call dispatches MNT precompiles through RunWithEVM, but CallCode, DelegateCall, and StaticCall still use RunPrecompiledContract, which calls the plain Run method. The MNT precompile Run methods intentionally return errMNTNotDispatchedDirectly, so these opcodes fail for MNT precompiles. This especially breaks read-only precompiles such as balanceMNT/currentMntID, which should be callable via STATICCALL.

Comment thread core/vm/contracts_qkc.go
return nil, err
}
contract.Gas = leftOver
return targetAddr.Bytes(), nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deploySystemContract selects a fixed targetAddr, but then calls normal evm.Create, which deploys to CreateAddress(caller, nonce) instead. The precompile returns targetAddr without putting code there, so the system contracts remain undeployed and later calls/mints cannot work.

ping-ke and others added 2 commits July 17, 2026 18:33
Integrate Multi-Native Token (MNT) support into the EVM execution layer,
building on the foundational types/state layer from feature/mnt-types-state.

**Core execution:**
- core/evm.go: Add tokenID params to CanTransfer/Transfer, route to
  GetMntBalance/SubMntBalance/AddMntBalance for non-QKC tokens
- core/state_transition.go: Add Message.GasTokenID/TransferTokenID fields,
  check MNT balance in buyGas(), debit correct token in state transition

**VM layer:**
- core/vm/interface.go: Add GetMntBalance/AddMntBalance/SubMntBalance to
  StateDB interface
- core/vm/evm.go: Add TxContext.GasTokenID/TransferTokenID, enforce MNT
  token acknowledgement check, route through runMNTPrecompiledContract
- core/vm/contract.go: Add Contract.TokenIDQueried flag for MNT acknowledgement
- core/vm/instructions.go: Add ModifyTokenIDQueried() to propagate flag from
  CALL/CALLCODE/DELEGATECALL/STATICCALL

**Precompiles:**
- core/vm/contracts.go: Register QKC precompiles (currentMntID, nativeMntTransfer)
- core/vm/contracts_qkc.go: Implement QKC-specific precompiles with EVM context
- core/vm/contracts_qkc_test.go: Add MNT transfer and token ID propagation tests

**Tests:**
- core/eth_transfer_logs_test.go: Update for QKC fork (Transfer() no longer
  emits EthTransferLog, only SELFDESTRUCT does)
- eth/tracers/js/tracer_test.go: Add MNT stubs to dummyStatedb

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Without timestamp gating, replaying blocks before MNT activation would
incorrectly execute the MNT precompile/system-contract addresses and
fork from the canonical chain.

Add ChainConfig.QKCMNTTime *uint64 (json:"qkcMNTTime") following the
same pattern as ShanghaiTime/CancunTime. Add Rules.IsQKCMNT bool and
ChainConfig.IsQKCMNT(time uint64) bool, populated in Rules().

In activePrecompiledContracts (core/vm/contracts.go) gate the MNT
precompile map merge behind rules.IsQKCMNT so that prior to activation
the addresses are ordinary accounts, mirroring goquarkchain's per-
contract enableTime check in core/vm/evm.go run().

Set QKCMNTTime=0 in TestChainConfig and MergedTestChainConfig so
existing MNT precompile tests continue to pass.

Ref: goquarkchain cmd/cluster/config.go SetEnableTime calls
     goquarkchain core/vm/evm.go run() enableTime gating

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

2 participants