Розгортування смарт-контрактів на Polkadot/Kusama
Якщо ви працюєте з EVM та вперше дивитесь на Polkadot — забудьте про аналогії з Ethereum. Тут немає єдиної VM для всієї мережі. Смарт-контракти розгортуються не в Relay Chain, а в парачейни зі включеним модулем pallet-contracts. Kusama — це "канарикова мережа" Polkadot з швидшим управлінням та менш суворими вимогами до слотів. Для тестування production-логіки використовуємо Kusama, для остаточного розгортування — Polkadot або спеціалізований парачейн (Astar, Phala, Aleph Zero).
Ink! — мова контрактів екосистеми Substrate
Контракти для pallet-contracts пишуться на ink! — вбудованому DSL поверх Rust. Це не клон Solidity та не транспайлер; це нативний Rust з макросами, які генерують метадані контракту та ABI-сумісний інтерфейс.
#[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)
}
}
}
Кілька ключових відмінностей від Solidity, які ламають інтуїцію EVM-розробника:
-
Немає
msg.valueу звичайних функціях — тільки в#[ink(message, payable)]. Виклик payable-функції без атрибуту повертає помилку. -
Storage через
Mappingбез ітерації — немаєmapping.keys(). Якщо потрібні списки — зберігайте окремо якVec<AccountId>. -
AccountIdзамістьaddress— 32 байти, не 20. Адреси у форматі SS58, не hex. -
Balanceцеu128, неuint256. Але практично різниці немає.
Інструментарій: cargo-contract
cargo install cargo-contract --force
cargo contract new my_contract
cd my_contract
# Збірка: генерує .contract, .wasm, .json (ABI)
cargo contract build --release
# Запуск тестів
cargo test
Файл .contract — це архів з WASM bytecode та метаданими. Це те, що ми розгортуємо.
Середовища розгортування та тестування
Локальне тестування: substrate-contracts-node
substrate-contracts-node --dev --tmp
Запускає однонодову мережу з попередньо профінансованими аккаунтами (Alice, Bob, Charlie — як Hardhat accounts[0]). Contracts UI на contracts-ui.substrate.io або програмно через @polkadot/api.
Тестнет: Contracts on Rococo
Rococo — тестнет Polkadot з парачейном contracts спеціально для тестування ink!. Faucet: paritytech.github.io/polkadot-testnet-faucet.
Production: Astar Network
Astar — найзрілішій парачейн зі pallet-contracts в екосистемі Polkadot. Підтримує як ink! (Wasm), так і EVM (Solidity) контракти в одній мережі, cross-VM виклики через XVM. Для більшості production use cases на Polkadot — розгортуємо на Astar.
Розгортування через 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; // автоматично
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 модель: WeightV2
На відміну від EVM, де gas — одне число, Substrate використовує двомірний weight:
-
refTime— час вичислень (пікосекунди) -
proofSize— розмір storage proof для light clients
При розгортуванні потрібно оцінити обидва параметри. Спосіб: сухий прогін (contracts.instantiate з estimateGas: true) або користуємо cargo-contract:
cargo contract instantiate --dry-run \
--constructor new \
--args \
--suri //Alice \
--url ws://127.0.0.1:9944
Storage deposit
pallet-contracts вимагає депозиту за займане сховище — аналог EIP-1153 transient storage, але постійний. Вартість пропорційна байтам у сховищі контракту. При видаленні сховища (через ink::env::set_contract_storage::<K, ()>(&key, &())) депозит повертається. Це важливо для довгоживих контрактів з великим state.
Типові помилки при міграції з EVM
| Паттерн EVM | Рішення ink! |
|---|---|
mapping.length() |
Окремий лічильник або Vec |
block.timestamp |
self.env().block_timestamp() (u64, мілісекунди) |
msg.sender |
self.env().caller() |
payable за замовчуванням |
Явний атрибут #[ink(message, payable)] |
Events з indexed |
#[ink(topic)] на полі event |
require(cond, "msg") |
assert! або Result<_, Error> |
Cross-contract виклики в ink! вимагають імпорту ABI викликаного контракту та явного вказання gas лімиту — немає автоматичного forwarding як у Solidity {gas: gasleft()}.







