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.valuein regular functions — only in#[ink(message, payable)]. Calling a payable function without the flag returns an error. -
Storage via
Mappingwithout iteration — nomapping.keys(). If you need lists — store them separately asVec<AccountId>. -
AccountIdinstead ofaddress— 32 bytes, not 20. Addresses in SS58 format, not hex. -
Balanceisu128, notuint256. 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()}.







