Разработка смарт-контрактов на Ink! (Polkadot)

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1 услугВсе 1306 услуг
Разработка смарт-контрактов на Ink! (Polkadot)
Сложная
~3-5 рабочих дней
Часто задаваемые вопросы
Направления блокчейн-разработки
Этапы блокчейн-разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1258
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1170
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    873
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1092
  • image_logo-advance_0.png
    Разработка логотипа компании B2B Advance
    563
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    830

Разработка смарт-контрактов на Ink! (Polkadot)

Substrate-цепи — это не EVM. Разработчики, которые приходят в экосистему Polkadot с опытом Solidity, первым делом натыкаются на принципиально другую модель исполнения: WebAssembly-рантайм вместо EVM, Rust вместо Solidity, и cargo-contract вместо Hardhat. Ink! — это embedded DSL поверх Rust, который компилируется в Wasm и разворачивается на паллете pallet-contracts. Переносить ментальные модели из EVM напрямую сюда опасно.

Чем Ink! принципиально отличается от Solidity

Первое, что режет глаз — модель хранилища. В Solidity mapping(address => uint256) — это просто слот в storage с keccak256-ключом. В Ink! каждое поле #[ink(storage)] транслируется в отдельные Lazy-записи в дереве Merkel storage Substrate. Это означает:

  • Нет понятия «слот» в EVM-смысле — нет slot packing
  • Доступ к Mapping<AccountId, Balance> — это get из off-chain state, не арифметика над 32-байтовым словом
  • StorageVec в Ink! 5.x ленив по умолчанию: элементы загружаются только при явном чтении

Второе принципиальное отличие — модель вызовов. В EVM msg.sender — всегда непосредственный вызывающий. В Ink! self.env().caller() возвращает предыдущий вызывающий в цепочке. Reentrancy в Ink! физически отключён по умолчанию через ReentrancyGuard на уровне среды исполнения, если не передан флаг --allow-reentrant-calls явно. Но это не значит, что можно расслабиться — cross-contract вызовы с CallBuilder всё ещё требуют аккуратного управления состоянием.

Третья особенность — жизненный цикл контракта. Ink! поддерживает #[ink(message, payable)] для приёма нативного токена, #[ink(constructor)] для инициализации, и — уникально для Polkadot-экосистемы — set_code_hash() для обновления кода контракта без смены адреса. Это аналог UUPS proxy из EVM-мира, но встроенный в протокол.

Типичные ошибки при написании Ink!-контрактов

Неправильное использование Mapping vs StorageHashMap

В Ink! 4.x был ink::storage::Mapping, который не реализует итерацию по ключам (это сделано намеренно — off-chain индексирование через события, не on-chain). Разработчики, привыкшие к EnumerableMap из OpenZeppelin, начинают хранить ключи в Vec<AccountId> рядом с Mapping, и это ломается при попытке масштабирования: Vec загружается целиком при каждом чтении, что делает вызов O(n) по gas weight.

Правильное решение — индексировать через ink::env::emit_event! и строить off-chain state через Subsquid или SubQuery. Не пытаться воссоздать on-chain итерируемые структуры.

Weight vs Gas: принципиально другая модель стоимости

EVM считает gas операционно. Substrate считает weight — это двумерный ресурс: ref_time (наносекунды CPU) и proof_size (байты доказательства для light client). При деплое через cargo-contract нужно явно указывать --gas-limit в weight-единицах, или использовать dry_run для оценки.

Паттерн, который регулярно приводит к проблемам: разработчик делает cargo-contract call без предварительного dry_run, контракт падает с OutOfGas, и команда начинает гадать, что не так — хотя достаточно было запустить:

cargo contract call --dry-run --contract <address> --message transfer --args <args>

Обновление через set_code_hash

Ink! позволяет обновить код контракта через self.env().set_code_hash(&new_code_hash). Но storage layout нового кода должен быть совместим со старым. Если поменять порядок полей в #[ink(storage)] — данные в хранилище будут читаться неверно. В EVM proxy-паттерне это называется storage collision — здесь та же проблема, но без bytecode-level инструментов для проверки.

Мы используем ink_storage_traits::StorageLayout derive-макрос и вручную проверяем storage layout через cargo-contract info --output-json перед каждым апгрейдом.

Как мы строим Ink!-проекты

Стек и инструменты

Инструмент Роль
cargo-contract 4.x Компиляция, деплой, вызовы
substrate-contracts-node Локальная нода для разработки
drink! Unit-тестирование без ноды (mock runtime)
openbrush Библиотека стандартов (PSP22, PSP34)
Subsquid Индексирование событий контракта
polkadot.js API Фронтенд-интеграция

Тестируем на трёх уровнях:

  1. Unit-тесты через #[ink::test] — синхронный, mock-окружение, быстро
  2. Integration-тесты через drink! — реальный runtime Substrate без сети, можно тестировать cross-contract вызовы
  3. E2E-тесты на substrate-contracts-node — полный стек с реальными транзакциями

Стандарты токенов в экосистеме Polkadot

PSP22 — аналог ERC-20. PSP34 — аналог ERC-721. Оба стандарта реализованы через openbrush, который предоставляет trait-based систему расширений — похоже на миксины в Solidity через diamond proxy, но без selector clashing проблем, потому что Ink! использует blake2b-хэши для диспетчеризации сообщений.

Одна тонкость: PSP22::transfer принимает data: Vec<u8> — это хук для получателя (аналог ERC-777 hook). Если контракт-получатель реализует PSP22Receiver, он может отреагировать на входящий перевод. Если не реализует — вызов всё равно проходит. Это отличается от ERC-777, где tokensReceived обязателен для контрактных адресов.

Процесс работы

Аналитика. Изучаем целевую Substrate-цепь: какая версия pallet-contracts, есть ли кастомные chain extensions, какой нативный токен, нужна ли интеграция с XCM для кросс-чейн вызовов.

Проектирование. Определяем storage layout (изменить после деплоя без миграции нельзя), события для индексирования, message-интерфейс. На этом этапе закладывается возможность апгрейда через set_code_hash — если нужна.

Разработка. Пишем контракт с тестами на drink!. Покрытие логики — 90%+. Cross-contract взаимодействия тестируем отдельно на substrate-contracts-node.

Аудит и деплой. Статический анализ через cargo clippy + ручной просмотр критических путей. Деплой на тестнет (Rococo Contracts), верификация через polkadot.js Apps.

Ориентиры по срокам

Простой контракт (PSP22 токен, 1-2 кастомных сообщения): 3-5 дней включая тесты. Контракт средней сложности с cross-contract вызовами и апгрейдом: 1-2 недели. Сложный протокол с XCM-интеграцией и кастомными chain extensions: от 1 месяца.

Конкретные сроки зависят от целевой цепи — на контрактных парачейнах типа Astar или Shiden могут быть свои особенности конфигурации pallet-contracts.