Skip to content

Deploy a contract

Frost runs the EVM at the Osaka feature level. Solidity contracts deploy and run unchanged — same bytecode, same opcodes, same gas semantics, same precompiles at the same addresses. If it works on Ethereum, it works here.

Terminal window
forge create src/MyContract.sol:MyContract \
--rpc-url https://rpc.frostfi.net \
--private-key $PRIVATE_KEY \
--priority-gas-price 0
foundry.toml
[rpc_endpoints]
frost = "https://rpc.frostfi.net"
hardhat.config.js
module.exports = {
networks: {
frost: {
url: 'https://rpc.frostfi.net',
chainId: 8141,
accounts: [process.env.PRIVATE_KEY],
},
},
};

Set the priority fee to zero. Frost is fee-blind and a tip buys nothing. eth_maxPriorityFeePerGas returns 0, so tooling that queries it does the right thing automatically; tooling with a hardcoded tip wastes gas.

Block gas limit is 60,000,000. Larger than Ethereum mainnet’s, so contracts that are tight on deployment gas have more room.

msg.sender from a post-quantum account is just an address. When a frame transaction’s SENDER frame calls your contract, msg.sender is the account’s address. Your contract cannot tell the difference between that and an EOA, and does not need to.

Do not gate on tx.origin == msg.sender. That idiom is used to reject contract callers, and it rejects every post-quantum account on this chain. It was already discouraged; here it is actively wrong.

CREATE2 factory is a predeploy. The deterministic factory lives at 0x4e59b44847b379578588920cA78FbF26c0B4956C from genesis, so counterfactual deployment patterns work without deploying a factory first. Multicall3 is also predeployed. See Predeploys.

Any contract can verify an ML-DSA signature. The precompiles have no ABI: pass the raw byte string at fixed offsets and use staticcall.

/// @notice Verify an ML-DSA-65 (FIPS 204) signature.
/// @param message 32-byte message
/// @param signature 3,309-byte FIPS 204 signature
/// @param publicKey 1,952-byte FIPS 204 public key
function verifyMlDsa65(
bytes32 message,
bytes calldata signature,
bytes calldata publicKey
) internal view returns (bool) {
bytes memory input = abi.encodePacked(message, signature, publicKey);
(bool ok, bytes memory out) = address(0x14).staticcall(input);
// 0x14 always returns exactly one 32-byte word: 1 valid, 0 invalid.
// It never returns empty, so a length check is belt-and-braces here —
// unlike ecrecover, where it is mandatory.
return ok && out.length == 32 && abi.decode(out, (uint256)) == 1;
}

Gas: 16,000 flat for 0x14 (ML-DSA-65), 10,500 for 0x15 (ML-DSA-44), regardless of input. Full specifications: VERIFY_MLDSA65 and VERIFY_MLDSA44.

Contract verification is available through the block explorer at explorer.frostfi.net.