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.
What is the same
Section titled “What is the same”- 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
0x14and0x15; 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.
Why contracts do not need to change
Section titled “Why contracts do not need to change”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: MITpragma 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.
What to do instead
Section titled “What to do instead”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.
The classical corners that remain
Section titled “The classical corners that remain”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 createand 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. ecrecoveris still at0x01. 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.senderlooks like. - Ecosystem — what is deployed today, unmodified.