Smart Contract Deployment to Polkadot/Kusama

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Smart Contract Deployment to Polkadot/Kusama
Medium
from 4 hours to 2 business days
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1215
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1043
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

Deploying Smart Contracts on Polkadot/Kusama

If you work with EVM and are looking at Polkadot for the first time — forget your Ethereum analogies. There is no single VM for the entire network here. Smart contracts are not deployed to the Relay Chain, but to parachains with the pallet-contracts module enabled. Kusama is the "canary network" of Polkadot with faster governance and less strict slot requirements. For testing production logic we use Kusama, for the final deployment — Polkadot or a specialized parachain (Astar, Phala, Aleph Zero).

Ink! — The Contract Language of the Substrate Ecosystem

Contracts for pallet-contracts are written in ink! — an embedded DSL on top of Rust. This is not a Solidity clone and not a transpiler; it's native Rust with macros that generate contract metadata and an ABI-compatible interface.

#[ink::contract]
mod vault {
    use ink::storage::Mapping;

    #[ink(storage)]
    pub struct Vault {
        balances: Mapping<AccountId, Balance>,
        owner: AccountId,
    }

    impl Vault {
        #[ink(constructor)]
        pub fn new() -> Self {
            Self {
                balances: Mapping::default(),
                owner: Self::env().caller(),
            }
        }

        #[ink(message, payable)]
        pub fn deposit(&mut self) {
            let caller = self.env().caller();
            let value = self.env().transferred_value();
            let current = self.balances.get(caller).unwrap_or(0);
            self.balances.insert(caller, &(current + value));
        }

        #[ink(message)]
        pub fn withdraw(&mut self, amount: Balance) -> Result<(), Error> {
            let caller = self.env().caller();
            let balance = self.balances.get(caller).unwrap_or(0);
            if balance < amount {
                return Err(Error::InsufficientBalance);
            }
            self.balances.insert(caller, &(balance - amount));
            self.env().transfer(caller, amount)
                .map_err(|_| Error::TransferFailed)
        }
    }
}

Several key differences from Solidity that break EVM developer intuition:

  • No msg.value in regular functions — only in #[ink(message, payable)]. Calling a payable function without the flag returns an error.
  • Storage via Mapping without iteration — no mapping.keys(). If you need lists — store them separately as Vec<AccountId>.
  • AccountId instead of address — 32 bytes, not 20. Addresses in SS58 format, not hex.
  • Balance is u128, not uint256. But practically there's no difference.

Tooling: cargo-contract

cargo install cargo-contract --force
cargo contract new my_contract
cd my_contract

# Build: generates .contract, .wasm, .json (ABI)
cargo contract build --release

# Run tests
cargo test

The .contract file is an archive with WASM bytecode and metadata. This is what we deploy.

Deployment Environments and Testing

Local Testing: substrate-contracts-node

substrate-contracts-node --dev --tmp

Runs a single-node network with pre-funded accounts (Alice, Bob, Charlie — like Hardhat accounts[0]). Contracts UI at contracts-ui.substrate.io or programmatically via @polkadot/api.

Testnet: Contracts on Rococo

Rococo is Polkadot's testnet with a contracts parachain specifically for testing ink!. Faucet: paritytech.github.io/polkadot-testnet-faucet.

Production: Astar Network

Astar is the most mature parachain with pallet-contracts in the Polkadot ecosystem. Supports both ink! (Wasm) and EVM (Solidity) contracts in one network, cross-VM calls via XVM. For most production use cases on Polkadot — deploy on Astar.

Deployment via polkadot.js

import { ApiPromise, WsProvider, Keyring } from '@polkadot/api';
import { CodePromise } from '@polkadot/api-contract';
import * as fs from 'fs';

async function deploy() {
  const provider = new WsProvider('wss://rpc.astar.network');
  const api = await ApiPromise.create({ provider });

  const keyring = new Keyring({ type: 'sr25519' });
  const deployer = keyring.addFromUri(process.env.MNEMONIC!);

  const wasm = fs.readFileSync('./target/ink/vault.wasm');
  const abi = JSON.parse(fs.readFileSync('./target/ink/vault.json', 'utf8'));

  const code = new CodePromise(api, abi, wasm);

  const gasLimit = api.registry.createType('WeightV2', {
    refTime: 30_000_000_000n,
    proofSize: 1_000_000n,
  });
  const storageDepositLimit = null; // automatic

  const tx = code.tx.new({ gasLimit, storageDepositLimit });

  await new Promise<void>((resolve, reject) => {
    tx.signAndSend(deployer, ({ contract, status }) => {
      if (status.isInBlock) {
        console.log('Contract address:', contract?.address.toString());
        resolve();
      }
    }).catch(reject);
  });

  await api.disconnect();
}

Gas Model: WeightV2

Unlike EVM where gas is a single number, Substrate uses two-dimensional weight:

  • refTime — computation time (picoseconds)
  • proofSize — storage proof size for light clients

When deploying you need to estimate both parameters. Method: dry run (contracts.instantiate with estimateGas: true) or use cargo-contract:

cargo contract instantiate --dry-run \
  --constructor new \
  --args \
  --suri //Alice \
  --url ws://127.0.0.1:9944

Storage Deposit

pallet-contracts requires a deposit for occupied storage — similar to EIP-1153 transient storage, but permanent. Cost is proportional to bytes in contract storage. When deleting storage (via ink::env::set_contract_storage::<K, ()>(&key, &())) the deposit is returned. This is important for long-lived contracts with large state.

Common Mistakes When Migrating from EVM

EVM Pattern ink! Solution
mapping.length() Separate counter or Vec
block.timestamp self.env().block_timestamp() (u64, milliseconds)
msg.sender self.env().caller()
payable by default Explicit #[ink(message, payable)] attribute
Events with indexed #[ink(topic)] on event field
require(cond, "msg") assert! or Result<_, Error>

Cross-contract calls in ink! require importing the ABI of the called contract and explicit gas limit specification — there's no automatic forwarding like Solidity's {gas: gasleft()}.