Skip to content

Ethereum-compatible

Frost changes how a transaction is authorised. It changes almost nothing about what a transaction does. That split is deliberate, and it is what lets a Solidity contract written for Ethereum deploy on Frost unchanged and come out post-quantum on the other side.

  • The EVM, at the Osaka feature level. Same opcodes, same gas semantics, same bytecode. A contract compiled for mainnet runs here byte for byte.
  • Every Ethereum precompile, at its usual address. Frost adds two of its own, the ML-DSA verifiers at 0x14 and 0x15; it removes none.
  • The JSON-RPC surface. eth_getBalance, eth_call, eth_getLogs, eth_sendRawTransaction, receipts, logs, and the rest behave as you expect. Connect to Frost lists what the public endpoint serves.
  • Standard transaction types. Legacy and EIP-1559 transactions are valid. Frame transactions are an addition, not a replacement. The one exception is EIP-4844: blob transactions are not accepted, because blob sidecars have no data-availability path through Frost consensus.
  • The tooling. Foundry, Hardhat, ethers, viem, indexers and explorers work for everything except producing an ML-DSA signature. Deploy a contract shows the configuration.
  • The parameters you would check first. Chain ID 8141, block gas limit 60,000,000 — the same as Ethereum mainnet’s — and 18-decimal native currency.

The proof is what is running. A Uniswap v3 deployment, Multicall3, a block explorer, and a compliance-gated token suite all run on Frost without modification; the Ecosystem page lists them.

Here is the observation the compatibility rests on: contracts never see keys. Not on Ethereum, and not on Frost. A contract sees msg.sender, a 20-byte address. By the time the first opcode of a contract runs, the question “was this call authorised?” has already been answered, and the contract is handed only the answer.

Consider a small vault:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// Anyone can deposit. Only the depositor can withdraw their balance.
/// Only the owner can hand the contract to a new owner.
contract Vault {
address public owner;
mapping(address => uint256) public balances;
constructor(address initialOwner) {
owner = initialOwner;
}
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount, "insufficient balance");
balances[msg.sender] -= amount;
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "transfer failed");
}
function transferOwnership(address next) external {
require(msg.sender == owner, "not owner");
owner = next;
}
}

Every authorisation decision in this contract is a comparison of msg.sender against a stored address. Nothing in it names a curve, a key, a signature format, or a hash-to-address rule.

On Ethereum, the protocol fills in msg.sender by recovering an ECDSA public key from the transaction signature and hashing it. On Frost, when the caller is a post-quantum account, the account’s VERIFY frame checks an ML-DSA signature against the key the account holds, and the SENDER frame then calls the vault with CALLER set to the account’s address. The vault receives the same 20 bytes either way, and cannot tell — and does not need to tell — which happened.

So: deploy this contract with the same bytecode you would use on mainnet, pass a post-quantum account’s address as initialOwner, and every privileged path is post-quantum. Depositors who use post-quantum accounts have post-quantum balances. The contract was not modified, recompiled, or audited again. It inherited the security of its callers’ accounts, because that is where the security always lived.

The rule that follows is short enough to remember: a contract is post-quantum on Frost exactly when it authorises by msg.sender.

The counterexample: contracts that verify signatures themselves

Section titled “The counterexample: contracts that verify signatures themselves”

Some contracts do not leave authorisation to the account. They take a signature as an argument and check it in their own code. The standard example is an ERC-20 permit, in the ERC-2612 form most tokens use:

function permit(
address owner, address spender, uint256 value, uint256 deadline,
uint8 v, bytes32 r, bytes32 s
) external {
require(block.timestamp <= deadline, "permit expired");
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01", DOMAIN_SEPARATOR,
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
));
address signer = ecrecover(digest, v, r, s);
require(signer != address(0) && signer == owner, "invalid signature");
_approve(owner, spender, value);
}

The ecrecover line bakes secp256k1 into the token. Uniswap’s Permit2 generalises the same idea to every token, and the pattern recurs wherever a contract accepts a signed message: EIP-712 meta-transactions, OpenZeppelin’s ECDSA.recover, signer sets in bridges and oracles.

Two things follow on Frost.

A post-quantum account cannot use it. The account holds no ECDSA key, so it can never produce a (v, r, s). permit deploys and runs on Frost — the bytecode is the same — but from a post-quantum account the function is simply unreachable.

Any account that can use it is classical. If a signature passes ecrecover, the signer holds a secp256k1 key, and that key is the one Shor’s algorithm recovers. A contract that authorises through ecrecover has reintroduced the primitive the chain was built to remove, one function at a time.

Permit2 and OpenZeppelin’s SignatureChecker fall back to ERC-1271 isValidSignature when the signer has code, which is how smart-contract wallets use them on Ethereum. Frost’s account contracts do not implement ERC-1271 — their code answers only frame-entry calls, rotation requests from the account itself, and plain transfers — so that fallback does not rescue the flow today.

You may not need a signature at all. permit exists to fold “approve” and “act” into one transaction so a user does not pay for two. A frame transaction carries up to 64 frames, executed in order and atomically if you ask for it, so a post-quantum account approves and calls in a single transaction natively — without anyone verifying a signature in application code. See Frame transactions.

If a contract must verify a signature, verify ML-DSA. Any contract can call the 0x14 precompile for 16,000 gas, flat, and check the result against a public key it already stores. The Deploy a contract page has a Solidity wrapper. The one rule that matters is the same one the account contracts follow: never accept both the signature and the public key from the caller, or an attacker supplies a matched pair and passes trivially.

Do not treat tx.origin as proof of a key. Under EIP-8141, ORIGIN returns the frame’s caller, so a post-quantum account calling your contract directly from a SENDER frame passes a tx.origin == msg.sender check — the check does not lock post-quantum accounts out. What it no longer does is prove that the caller is a key-holding EOA, which is the property contracts used it for. It still excludes any contract in between, exactly as on Ethereum. Use it, if at all, as a reentrancy heuristic, never as an authorisation primitive.

Compatibility means the old primitives are still present, and it is worth knowing where.

  • Ordinary EOAs work. A secp256k1 key in MetaMask can hold FROST and send transactions. It is simply not quantum-safe, which is the thing the chain exists to fix.
  • Deployment tooling signs with ECDSA. forge create and Hardhat deploy from an EOA. For a contract with no privileged owner that is irrelevant; if a contract has an owner or an upgrade path, point it at a post-quantum account rather than the deploying key, exactly as the vault above does.
  • ecrecover is still at 0x01. It is there for compatibility. The chain’s own treasury and deployment flow do not depend on it.
  • Deploy a contract — Foundry and Hardhat configuration, and calling the precompiles from Solidity.
  • Accounts and keys — what the account on the other end of msg.sender looks like.
  • Ecosystem — what is deployed today, unmodified.