Деплой смарт-контрактов в Polkadot/Kusama
Если вы работаете с EVM и впервые смотрите на Polkadot — забудьте про аналогии с Ethereum. Здесь нет единой VM для всей сети. Смарт-контракты деплоятся не в Relay Chain, а в парачейны с включённым модулем pallet-contracts. Kusama — это "канареечная сеть" Polkadot с более быстрым governance и менее строгими требованиями к слотам. Для тестирования production-логики используем Kusama, для финального деплоя — Polkadot или специализированный парачейн (Astar, Phala, Aleph Zero).
Ink! — контрактный язык экосистемы Substrate
Контракты для pallet-contracts пишутся на ink! — embedded 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 или programmatic через @polkadot/api.
Тестнет: Contracts on Rococo
Rococo — тестнет Polkadot с парачейном contracts специально для тестирования ink!. Faucet: paritytech.github.io/polkadot-testnet-faucet.
Production: Astar Network
Astar — наиболее зрелый парачейн с pallet-contracts в Polkadot ecosystem. Поддерживает и 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 требует депозит за занятый storage — аналог EIP-1153 transient storage, но постоянный. Стоимость пропорциональна байтам в storage контракта. При удалении storage (через ink::env::set_contract_storage::<K, ()>(&key, &())) депозит возвращается. Это важно для long-lived контрактов с большим 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)] на поле события |
require(cond, "msg") |
assert! или Result<_, Error> |
Cross-contract вызовы в ink! требуют импорта ABI вызываемого контракта и явного указания gas лимита — нет автоматического forwarding как в Solidity {gas: gasleft()}.







