Deploying EVM Smart Contracts: A Developer’s Guide to Post-Quantum dApps on ARMChain
post quantum cryptography

Deploying EVM Smart Contracts: A Developer’s Guide to Post-Quantum dApps on ARMChain

Deploy Solidity contracts on ARMChain, an EVM-compatible quantum-safe blockchain. A step-by-step guide for Melbourne developers using Remix and Fakenet.

Roohan Adeel

The first stumble on ARMChain is usually the signer. The contract compiles, the deploy script runs, and then the transaction bounces because Hardhat signed it with ECDSA. The Solidity is fine. The network won’t accept that signature.

That’s most of what you need to know about building on an EVM-compatible quantum-safe blockchain. ARMchain runs the London fork, so your existing contracts, OpenZeppelin imports, ABI encoding, and events work the same as on Ethereum. What changes is signing. ARMchain accepts only Type 3 transactions, signed with ML-DSA, and the ARMchain Ethers SDK handles that for you.

This guide deploys a contract the way you would on Ethereum and points out every place where ARMchain behaves differently. It’s written for post-quantum dApp development in Melbourne and the rest of Victoria, though nothing in it depends on where you are. If you can deploy a contract from Remix, you can follow along. 

How EVM Compatibility Works on ARMChain

ARMchain targets the London hard fork. A Solidity 0.8.x contract uses the same opcodes, ABI encoding, events, and logs as on Ethereum, and addresses are 20 bytes. CREATE and CREATE2 work the same way, and the standard eth_*. JSON-RPC methods return what you’d expect. Fees use EIP-1559 with a minimum gas price of 1 Gwei.

ERC-20, ERC-721, ERC-1155, and ERC-4626 all work, and so do OpenZeppelin’s UUPS upgradeable contracts. On an EVM Compatible Quantum Safe Blockchain the contract layer stays as it is, and only the signature underneath it changes.

London is the part that catches people. Shanghai added the PUSH0 opcode, and ARMchain doesn’t support it. Cancun features such as transient storage aren’t supported either. Solidity 0.8.20 and later compile for Shanghai by default, so if you leave evmVersion alone, your bytecode can contain PUSH0 and fail on ARMchain. Set evmVersion to london and use pragma solidity ^0.8.24;, which is what the docs recommend.

The contract size limit is the same as Ethereum’s: 24,576 bytes. If a contract fits on mainnet, it fits here.

Type 3 Transactions on ARMChain

ARMChain accepts one transaction type. Type 3 has the same fields as an EIP-1559 transaction. Types 0, 1, and 2 are rejected because they’re signed with ECDSA.

The difference is at the end of the transaction. There is no v, r, or s. There’s a single signatureBlob field of 3,732 bytes: a 2,420-byte ML-DSA signature followed by the 1,312-byte public key. The key travels with the signature because ML-DSA has no public key recovery, so a node can’t work out the sender from the signature alone. It checks the signature against the key in the blob, then derives the sender address from the Keccak-256 hash of that key.

You won’t build this by hand. The ARMchain Ethers SDK, @armchain-ethersv6/ethers, is a drop-in replacement for ethers.js v6 and sets the transaction type to 3 for you. Standard ethers.js can still read balances, blocks, and contract state. t can’t sign, so anything that sends a transaction or signs a message needs the SDK. Wallet.createRandom() looks the same as before, but the private key it returns is 2,560 bytes.

To port an existing ethers v6 app, you make four changes:

  • Replace the ethers package with @armchain-ethersv6/ethers.
  • Update your imports.
  • Point the provider at an ARMchain RPC endpoint.
  • Generate new keys with the SDK. Your old ECDSA keys don’t carry over.

Contract calls, event listeners, and transaction building stay the same. One method is gone: Signature.recoverPublicKey() was removed, since the key is already inside the signature.

typescript

import { JsonRpcProvider, Wallet, parseEther } from "@armchain-ethersv6/ethers";

const provider = new JsonRpcProvider("http://localhost:4000"); // local Fakenet

const wallet = new Wallet(process.env.PRIVATE_KEY, provider);

const tx = await wallet.sendTransaction({

  to: "0xRECIPIENT_ADDRESS",

  value: parseEther("1.0"),

  // the SDK sets type 3 for you

});

const receipt = await tx.wait();

console.log("Confirmed in block:", receipt.blockNumber);

Tooling Setup

You need Node.js 18 or later, the Remix IDE in a browser, and the SDK:

bash

npm install @armchain-ethersv6/ethers

To deploy through a wallet, install the ARMchain Wallet browser extension. For local work you’ll run a Fakenet, covered below. This table shows what each tool can do: 

Tool

What works

What doesn’t

Remix IDE

Compile, deploy, and call, using the ARMchain Wallet or a local HTTP provider

Nothing documented

ARMchain Ethers SDK

Signing, deploying, contract calls, HD wallets

Nothing documented

Standard ethers.js

Reading balances, blocks, and contract state

Signing transactions or messages

Hardhat

npx hardhat compile, artifacts, project layout

hre.ethers signers, deploying and sending transactions from tests, fork testing

Foundry

Not documented for ARMChain

Native ML-DSA signing

The rule is simple. Compiling works anywhere, and signing goes through the SDK. In practice, Hardhat becomes a compiler and a project layout, and deployment goes through Remix or the SDK.

Deploying Your First Contract in Remix

Remix is the quickest way to deploy. It compiles the contract, and your ARMchain Wallet signs the transaction, so you don’t need any local setup.

  1. Connect your ARMchain Wallet to the ARMchain network, following the network configuration guide.
  2. Open Remix at https://remix.ethereum.org.
  3. In the file explorer, create a file named HelloArmchain.sol and paste in this contract:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;

contract HelloArmchain {

    string public greeting;

    address public owner;

    event GreetingChanged(string oldGreeting, string newGreeting, address changedBy);

    constructor(string memory _greeting) {

        greeting = _greeting;

        owner = msg.sender;

    }

    function setGreeting(string memory _newGreeting) external {

        string memory oldGreeting = greeting;

        greeting = _newGreeting;

        emit GreetingChanged(oldGreeting, _newGreeting, msg.sender);

    }

    function getGreeting() external view returns (string memory) {

        return greeting;

    }

}

  1. On the Solidity Compiler tab, select version 0.8.24. Under Advanced Configuration, set the EVM version to london. 
  2. Click Compile.
  3. On the Deploy & Run Transactions tab, set Environment to Injected Provider. Remix connects to your ARMchain Wallet.
  4. Check that the wallet shows chain ID 55.
  5. Enter "Hello from ARMchain!" in the constructor field and click Deploy.
  6. Confirm the transaction in the wallet.
  7. Find the contract under Deployed Contracts and call getGreeting.

ARMchain has instant finality, so you don’t need to wait for extra confirmations. Once the transaction is mined, the contract is live.

To call the contract from code, copy the ABI from the Compiler tab and use the SDK:

typescript

import { JsonRpcProvider, Wallet, Contract } from "@armchain-ethersv6/ethers";

const provider = new JsonRpcProvider("http://localhost:4000");

const wallet = new Wallet(process.env.PRIVATE_KEY, provider);

const abi = [

  "function getGreeting() view returns (string)",

  "function setGreeting(string) external",

  "event GreetingChanged(string, string, address)"

];

const contract = new Contract("YOUR_CONTRACT_ADDRESS", abi, wallet);

console.log(await contract.getGreeting());

const tx = await contract.setGreeting("Signed with ML-DSA");

await tx.wait();

Deploying from a Script

Once you have a project, you’ll want to deploy from the command line. Hardhat can compile, and the SDK does the signing.

  1. Set up the project:

bash

mkdir my-armchain-dapp && cd my-armchain-dapp

npm init -y

npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox

npm install @armchain-ethersv6/ethers

npx hardhat init

  1. In hardhat.config.js, set the compiler to 0.8.24 and the EVM version to london:

javascript

require("@nomicfoundation/hardhat-toolbox");

module.exports = {

  solidity: {

    version: "0.8.24",

    settings: {

      optimizer: { enabled: true, runs: 200 },

      evmVersion: "london",

    },

  },

};

  1. Compile with npx hardhat compile. Hardhat writes the ABI and bytecode to the artifacts folder.
  2. Write a deploy script that uses the SDK. Add "type": "module" to your package.json first:

javascript

// scripts/deploy.js

import { readFileSync } from "node:fs";

import { JsonRpcProvider, Wallet, ContractFactory } from "@armchain-ethersv6/ethers";

const artifact = JSON.parse(

  readFileSync("./artifacts/contracts/HelloArmchain.sol/HelloArmchain.json", "utf8")

);

const provider = new JsonRpcProvider(process.env.RPC_URL ?? "http://localhost:4000");

const wallet = new Wallet(process.env.PRIVATE_KEY, provider);

const factory = new ContractFactory(artifact.abi, artifact.bytecode, wallet);

const contract = await factory.deploy("Hello from ARMchain!");

await contract.waitForDeployment();

console.log("Deployed to:", await contract.getAddress());

  1. Run it with Node:

bash

node scripts/deploy.js

Don’t run it with npx hardhat run. That command injects Hardhat’s own ECDSA signer, and ARMchain rejects what it signs. For the same reason, avoid hre.ethers.getSigners(), hre.ethers.getContractFactory() and deployer.sendTransaction().

Testing Against a Local Fakenet

 Fakenet is ARMchain’s local development network, similar in purpose to Hardhat Network or Ganache. It runs a private chain with auto-generated validators and pre-funded accounts, and every consensus, EVM and signing feature is switched on. Hardhat’s default test environment can’t send ARMchain transactions, so Fakenet is where you run integration tests. 

To start a single-validator Fakenet from the armchain-client repository:

bash

make armnode

 

./build/armnode --fakenet 1/1 \

  --http \

  --http.addr 0.0.0.0 \

  --http.port 4000 \

  --http.api eth,web3,net,personal,txpool,arm,dag,debug,admin \

  --http.corsdomain "*" \

  --allow-insecure-unlock

The RPC endpoint is now http://localhost:4000, and the chain ID is 55.

Each Fakenet validator starts with 1 billion ARM. To fund the wallet your tests use, attach to the node console, unlock the validator account with the Fakenet password and send some ARM to your SDK wallet:

 

> personal.unlockAccount(eth.accounts[0], "fakepassword")

> eth.sendTransaction({ from: eth.accounts[0], to: "0xYOUR_SDK_WALLET", value: web3.toWei(100, "ether") })

Then deploy and assert with the SDK:

javascript

import { JsonRpcProvider, Wallet, ContractFactory } from "@armchain-ethersv6/ethers";

 

const provider = new JsonRpcProvider("http://localhost:4000");

const wallet = new Wallet(process.env.FAKENET_KEY, provider);

 

const factory = new ContractFactory(abi, bytecode, wallet);

const token = await factory.deploy(1000000);

await token.waitForDeployment();

 

const supply = await token.totalSupply();

console.assert(supply === 1000000n * 10n ** 18n, "Unexpected supply");

Fakenet keys are deterministic and public, so never use them for anything real. To reset, stop the node and delete the data directory, or run clean.sh if you use the demo scripts. A multi-validator setup is also available with --fakenet 1/3, 2/3 and 3/3. The Fakenet guide has the details.

Writing ML-DSA Smart Contracts

Most contracts never touch signatures. If yours does, for example by checking a signed message from off-chain, there’s one thing to change.

ecRecover at address 0x01 still exists on ARMchain, so contracts that verify ECDSA signatures keep working. But it isn’t what ARMchain signs with. For ML-DSA Smart Contracts, use the pqcRecover precompile at address 0x13. It takes a 3,732-byte signature blob (signature plus public key), checks the ML-DSA signature, and returns the signer’s address as a 32-byte padded value. The address comes from the last 20 bytes of the Keccak-256 hash of the public key. 

The docs give this helper:

solidity

function pqcRecover(bytes memory signature) internal view returns (address signer) {

    (bool success, bytes memory result) = address(0x13).staticcall(signature);

    require(success, "pqcRecover failed");

    signer = address(uint160(uint256(bytes32(result))));

}

The signature blob is large, so a function that takes one as an argument carries about 3.7 KB of calldata in every call. Measure the gas on a Fakenet before you design an API around it, and don’t assume your ECDSA numbers still apply. Check the EVM integration page for the exact input format before you rely on pqcRecover for anything that holds value.

Gotchas Before Production

These are the points most likely to cost you time:

Issue

What happens

What to do

PUSH0 in bytecode

Deployment or execution fails

Set evmVersion to london

Hardhat signers

ECDSA transactions are rejected

Sign with @armchain-ethersv6/ethers, run scripts with node

Transaction size

A simple transfer is about 3,930 bytes against about 110 for ECDSA, roughly 36 times larger

Plan for bandwidth, storage and data gas

Gas price

The minimum is 1 Gwei

Don’t hardcode lower values

Block gas

The maximum is 20,500,000

Keep large deployments within it

Custom account validation

New AA opcodes that solc doesn’t recognise

Write those contracts in Yul

Contract verification

Source isn’t public until you submit it

Verify on the ARMchain Explorer

The Yul requirement applies only to custom account validation schemes and paymaster contracts. Ordinary Solidity contracts, such as tokens, NFTs and DeFi protocols, compile and deploy normally.

Execution cost doesn’t change. EVM computation uses the same gas as on Ethereum, and finality is still instant. The larger transaction affects bandwidth, storage and data-related gas. To verify a contract, open its address in the explorer, choose Verify & Publish, and submit the source, compiler version and constructor arguments. Single file, standard JSON input and multi-file verification are all supported. Before production, audit as you would on any chain.

Post-Quantum dApp Development From Melbourne

If you’re starting post-quantum dApp development in Melbourne, a week is enough to get a first contract running and understand the differences. A workable plan:

  • Day one. Deploy HelloArmchain from Remix and call it.
  • Day two. Start a Fakenet, fund an SDK wallet and deploy the same contract from a script.
  • Day three. Take one of your own contracts, set evmVersion to london and deploy it to Fakenet. Fix whatever breaks.
  • Day four. Write integration tests with the SDK against Fakenet, since Hardhat tests that send transactions won’t work.
  • Day five. Go through the gotchas table above, then decide whether the larger transaction size matters for your use case.

By the end you’ll know how much of your stack carries over, and the answer is usually most of it.

 

FAQs

Can I keep using Hardhat?

For compiling, yes. npx hardhat compile and the artifacts it generates work. Deployments and tests that send transactions don’t work with Hardhat’s built-in signers, so those go through the ARMchain Ethers SDK. Foundry doesn’t support ML-DSA signing natively.

Do my existing Solidity contracts need changes?

Usually not. Contracts compiled for London run unchanged. The thing to check is the compiler’s EVM target, because a Shanghai target can produce PUSH0, which ARMchain doesn’t support. 

Does deploying cost more gas?

Execution gas is the same as on Ethereum. A deployment transaction is larger because of the 3,732-byte signature blob, so data-related gas is higher. Deploy to a Fakenet first and read the actual numbers.

Which wallet do I use with Remix?

The ARMchain Wallet browser extension, through Remix’s Injected Provider. For local development, you can select External HTTP Provider and enter http://localhost:4000 instead. 

Key Takeaways

Writing Solidity for ARMchain feels like writing it for Ethereum. The work is in the signer, the compiler target, and the tooling around them. Once those three are set up, you deploy and test the way you always have, on a chain whose signatures are built for the quantum era.

Start with the ARMchain documentation, deploy the example contract from Remix, and then move it to a Fakenet and a script.

Share this article