Как писать смарт-контракты под Solana и не сойти с ума от бойлерплейта
Если вы хоть раз пробовали писать программы под Solana на чистом Rust, то наверняка помните то чувство, когда на пять строк бизнес-логики приходится писать пятьдесят строк проверок аккаунтов, ручной десериализации через Borsh и валидации подписей. Ошиблись в порядке передачи ключей в транзакции — получаете ошибку выполнения, которую порой приходится дебажить часами.
В мире Ethereum разработчики давно привыкли к Solidity и готовым обвязкам вроде Hardhat или Foundry. В Solana аналогичным стандартом стал Anchor — фреймворк, который берет на себя всю рутину по парсингу данных, проверке прав доступа и генерации клиентского кода.
Что берет на себя Anchor
По своей сути Anchor представляет собой DSL (предметно-ориентированный язык) поверх Rust. Он не меняет внутреннюю модель исполнения Solana BPF, но упаковывает низкоуровневые вызовы в понятные декларативные макросы.
Когда вы пишете программу с Anchor, фреймворк решает четыре основные задачи:
- Сериализация и десериализация данных аккаунтов без ручного вызова методов распаковки.
- Проверка ограничений аккаунтов прямо в сигнатуре структуры через атрибуты (проверка владельца, проверка подписи, инициализация памяти).
- Сборка спецификации IDL (Interface Description Language) — аналога ABI в Ethereum.
- Создание готовых TypeScript и Rust клиентов для взаимодействия с контрактом прямо из фронтенда или тестов.
Как выглядит код на практике
Давайте взглянем на классический пример счетчика. В чистом Solana SDK вам пришлось бы вручную парсить массив байтов InstructionData, доставать слайсы аккаунтов, проверять is_signer у вызывающего адреса и валидировать адрес системной программы.
Вот как этот же контракт выглядит на Anchor:
use anchor_lang::prelude::*;
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
#[program]
mod counter {
use super::*;
pub fn initialize(ctx: Context<Initialize>, start: u64) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.authority = *ctx.accounts.authority.key;
counter.count = start;
Ok(())
}
pub fn increment(ctx: Context<Increment>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count += 1;
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init, payer = authority, space = 48)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(mut, has_one = authority)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
#[account]
pub struct Counter {
pub authority: Pubkey,
pub count: u64,
}
Разница бросается в глаза сразу. Вся логика валидации вынесена в структуры Initialize и Increment.
Атрибут #[account(init, payer = authority, space = 48)] говорит рантайму: создай новый аккаунт, выдели под него 48 байт, а оплату за аренду спиши с кошелька authority.
Конструкция #[account(mut, has_one = authority)] автоматически проверяет, что поле authority внутри структуры Counter совпадает с переданным аккаунтом authority, и что этот аккаунт действительно подписал транзакцию. Если подписи нет или передан чужой кошелек, выполнение прервется еще до входа в тело функции increment.
IDL и фронтенд без головной боли
Самая удобная вещь в связке с Anchor — это файл IDL в формате JSON. Компилятор собирает его автоматически при сборке проекта.
В IDL описаны все инструкции, структуры данных и возможные кастомные ошибки программы. На базе этого файла библиотека @anchor-lang/core генерирует типизированный интерфейс для JavaScript или TypeScript.
Вам больше не нужно помнить смещения байтов при сборке транзакции на клиенте. Вызов метода с фронтенда превращается в обычный вызов функции:
await program.methods
.increment()
.accounts({
counter: counterPubkey,
authority: wallet.publicKey,
})
.rpc();
TypeScript подсветит ошибки, если вы забудете передать обязательный аккаунт или укажете не тот тип аргумента.
Встроенный фаззинг для поиска уязвимостей
В смарт-контрактах цена ошибки слишком высока, поэтому тестирование играет особую роль. В состав CLI входит интеграция с инструментом Crucible для coverage-guided фаззинг-тестирования.
Команда anchor fuzz init генерирует тестовую обвязку, а anchor fuzz run запускает прогон со случайными входными данными, пытаясь найти граничные случаи, которые приводят к панике или невалидному состоянию аккаунтов. Это помогает ловить неочевидные переполнения или пропущенные проверки еще до выкатки в тестовую сеть.
# Инициализация фазз-тестов
anchor fuzz init program_name
# Запуск тестов в release-сборке
anchor fuzz run program_name test_name --release
Установка и начало работы
Для управления версиями тулчейна разработчики создали специальную утилиту AVM (Anchor Version Manager). Это спасает от конфликтов версий компилятора Solana и самого фреймворка, когда разные проекты требуют разных подверсий.
Ставится утилита одной строчкой:
curl -sSfL https://raw.githubusercontent.com/otter-sec/anchor/master/avm/install | sh
После установки можно переключаться на ночные сборки или фиксировать конкретные релизы для своих рабочих воркспейсов:
avm nightly
avm nightly --disable
Кому пригодится Anchor
Если вы только заходите в разработку под Solana, начинать без Anchor практически бессмысленно. Вы потратите недели на изобретение собственного велосипеда для парсинга аккаунтов и проверки дискриминаторов.
Фреймворк закрывает основные потребности:
- Бэкенд-разработчикам на Rust дает строгую типизацию и защиту от типовых уязвимостей валидации.
- Фронтендерам дает готовые TypeScript-типы и удобные методы для отправки транзакций.
Единственный случай, когда Anchor может показаться избыточным — написание микро-программ с экстремальной оптимизацией размера бинарника (compute budget limit), где важен буквально каждый байт инструкций. Во всех остальных сценариях это стандарт де-факто, который экономит сотни часов работы.
