Back to Blog

Solana Simplificado 02: Escrevendo Seu Primeiro Smart Contract Solana do Zero

21 de junho de 2024
12 min read

Introdução

Em 2024, a Solana ganhou destaque, tornando-se a quarta blockchain pública em classificação por Total Value Locked (TVL), capturando o interesse de investidores e desenvolvedores.

A BlockSec criou a série "Solana Simplificado", que inclui artigos sobre os conceitos básicos da Solana, tutoriais sobre como escrever contratos inteligentes na Solana e guias para analisar transações na Solana. O objetivo é ajudar os leitores a entender o ecossistema da Solana e dominar habilidades essenciais para desenvolver projetos e realizar transações na Solana.

No primeiro artigo desta série, aprofundamos os conceitos-chave da rede Solana, incluindo seu mecanismo de operação, modelo de contas e transações, estabelecendo uma base sólida para escrever contratos Solana corretos e de alto desempenho.

Neste artigo, vamos guiá-lo na escrita de um programa Solana (ou seja, um contrato inteligente Solana) para publicar e exibir artigos. Isso ajudará a consolidar os conceitos do primeiro artigo e apresentará algumas funcionalidades da Solana que ainda não discutimos. O programa relevante e o código de teste foram publicados no GitHub.

Configurar o Ambiente

Nota: Os comandos a seguir cobrem apenas o sistema Ubuntu. Alguns comandos podem não funcionar nos sistemas Windows e macOS.

👉 Você pode usar comandos alternativos ou consultar Configurar o ambiente de desenvolvimento local e instalar a CLI da Solana para resolver isso.

Vamos compilar e implantar programas Solana no ambiente local. Antes de mergulhar no desenvolvimento de programas Solana, precisamos instalar alguns comandos e dependências.

Rust

Os programas Solana são predominantemente escritos na linguagem de programação Rust, portanto, precisamos executar o seguinte comando para instalar o Rust:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

node.js & TypeScript

O script de teste é escrito em TypeScript, portanto, precisamos instalar o Node.js e a cadeia de ferramentas TypeScript:

curl -fsSL https://deb.nodesource.com/setup_15.x | sudo -E bash -
sudo apt-get install -y nodejs npm
npm install -g typescript
npm install -g ts-node

CLI da Solana

Em seguida, precisamos instalar a ferramenta CLI da Solana, que fornece comandos para tarefas como criar carteiras e implantar contratos:

sh -c "$(curl -sSfL https://release.solana.com/stable/install)"

Execute o seguinte comando para aplicar as alterações ao terminal atual:

source ~/.profile

Você pode executar solana --version para verificar se a CLI da Solana foi instalada com sucesso:

$ solana --version
solana-cli 1.17.25 (src:d0ed878d; feat:3580551090, client:SolanaLabs)

Carteira Local

Em seguida, execute o seguinte comando para gerar uma carteira no sistema de arquivos local:

solana-keygen new

Por padrão, este comando cria um arquivo de chave privada em ~/.config/solana/id.json, que usaremos no script de teste mais tarde. Você pode usar o comando solana address para ver o endereço gerado por ele:

$ solana address
9FcGDHvzs8Czo4B1pQq8GPdXfdGHqNqayvh6bu4hVGfy

Devnet

O programa será implantado na devnet, portanto, precisamos executar o seguinte comando para alternar a CLI da Solana para a devnet:

solana config set --url https://api.devnet.solana.com

Tokens SOL

Como a implantação de contratos e o envio de transações de teste não são gratuitos, precisamos solicitar alguns tokens SOL. Você pode executar solana airdrop 2 ou solicitar tokens diretamente pelo faucet público na web.

Escrever o Programa

O programa que vamos apresentar permite que os usuários publiquem artigos e listem todos os artigos publicados atualmente. Ele lida com três tipos de instruções:

  • Instrução init: Inicializa o programa criando uma conta de dados para armazenar o índice máximo atual de artigos.
  • Instrução post: Armazena um artigo publicado em uma nova conta de dados.
  • Instrução list: Imprime todos os artigos publicados no log.

Abaixo está a estrutura do programa:

$ tree program/
program/
├── Cargo.lock
├── Cargo.toml
└── src
    ├── instructions
    │   ├── init.rs
    │   ├── list.rs
    │   ├── mod.rs
    │   └── post.rs
    ├── lib.rs
    └── processor.rs

O Cargo.toml especifica as bibliotecas externas para o projeto:

[dependencies]
borsh = "0.10.0"
borsh-derive = "0.10.0"
solana-program = "=1.17.25"

Nota: A versão do solana-program deve corresponder à versão da CLI da Solana instalada (você pode verificar isso usando solana --version). Caso contrário, você pode encontrar erros durante a compilação.

A função de entrada do contrato é definida em processor.rs. O arquivo começa importando as dependências necessárias (pulamos partes semelhantes em outros arquivos Rust):

Em seguida, vem a definição da função de entrada do contrato:

Na linha 10 do programa, a macro entrypoint! da biblioteca Solana especifica que a função de entrada é process_instruction. Esta função aceita três parâmetros:

  • program_id: O endereço implantado do programa atual.
  • accounts: Todas as contas envolvidas na instrução.
  • instruction_data: O array de bytes usado para processar as instruções.

process_instruction extrai o primeiro byte de instruction_data para identificar o tipo de instrução e chama a função correspondente para tratá-la. Em seguida, veremos como essas três funções são implementadas.

init

Cada uma das três funções é definida em um arquivo com o mesmo nome no diretório program/src/instructions. Vamos começar com init.rs.

Como a instrução init não requer um array de bytes adicional, ela aceita apenas dois parâmetros: program_id e accounts.

Entre as linhas 12 e 15, o programa extrai sequencialmente as informações de conta necessárias, depois chama a função find_program_address para calcular o endereço da conta de dados usada para armazenar o índice máximo atual de artigos, index_pda_key. Em seguida, o programa verifica que index_pda_key é igual ao endereço de index_pda.

Aqui, o endereço de index_pda é um endereço especial chamado PDA (Program Derived Addresses). Ao contrário dos endereços de conta "carteira" gerados a partir de chaves públicas, os PDAs são derivados de um array de bytes opcional, um byte chamado "bump" e o endereço de um programa. O array de bytes e o endereço do programa são fornecidos explicitamente pelo chamador, enquanto o bump é gerado automaticamente e retornado pela função find_program_address. O bump garante que os endereços criados dessa forma não possam colidir com endereços gerados por chave pública.

Após confirmar que o endereço de index_pda é o esperado, usamos a instrução create_account fornecida pelo SystemProgram para criar o index_pda. Na linha 22, o programa cria uma variável do tipo IndexAccountData, que registra o índice máximo atual de artigos (inicializado em 0). Como mostrado na figura abaixo, esse tipo implementa as traits BorshSerialize e BorshDeserialize, permitindo que seja serializado e desserializado no formato Borsh:

As linhas 23 a 24 calculam o espaço necessário para armazenar os dados da conta e o aluguel mínimo para criar a conta.

As linhas 27 a 37 criam uma instrução create_account do SystemProgram e a invocam usando o método invoke_signed. A ação de invocar uma instrução para outro programa dentro de um programa é chamada de CPI (Cross-Program Invocation). Na Solana, você pode usar duas funções para realizar CPI: invoke e invoke_signed.

A função invoke processa diretamente a instrução fornecida, e sua assinatura de função é a seguinte:

pub fn invoke(
    instruction: &Instruction,
    account_infos: &[AccountInfo<'_>]
) -> Result<(), ProgramError>

A única diferença entre invoke_signed e invoke é que invoke_signed fornece uma maneira de assinar contas PDA. As contas PDA não possuem par de chaves pública-privada e, portanto, não podem fornecer assinaturas diretamente. invoke_signed resolve esse problema. Esta função aceita um parâmetro adicional chamado signers_seeds. Sua assinatura de função é a seguinte.

De forma semelhante ao funcionamento da função find_program_address, invoke_signed calcula um conjunto de endereços PDA com base nos signers_seeds fornecidos e no program_id do chamador. Se account_infos contiver esses endereços PDA, eles serão marcados como signatários, como se tivessem fornecido assinaturas.

pub fn invoke_signed(
    instruction: &Instruction,
    account_infos: &[AccountInfo<'_>],
    signers_seeds: &[&[&[u8]]]
) -> Result<(), ProgramError>

Aqui, como a instrução create_account requer que a conta sendo criada seja uma signatária, precisamos usar invoke_signed para fornecer uma assinatura para index_pda.

No final do programa, o contrato chama o método serialize em index_data para serializar os dados e gravá-los no campo data de index_pda.

post_article

Agora vamos ver a implementação da instrução post. Ao contrário da instrução init, a instrução post aceita um artigo publicado, então ela tem um parâmetro adicional chamado instruction_data_inner, que armazena os dados serializados do artigo.

Como de costume, o programa começa extraindo as informações de conta necessárias para a instrução. Como geramos uma conta PDA separada para cada artigo, o programa aceita uma conta adicional, new_article_pda, na linha 23.

Semelhante à instrução init, as linhas 27 a 30 usam find_program_address para verificar que o endereço do index_pda passado está correto. Em seguida, desserializamos os dados no campo data para ler o índice máximo alocado atualmente.

As linhas 33 a 40 também verificam o endereço de new_article_pda. O programa usa a string "ARTICLE_PDA" e o índice atual para gerar um novo endereço. O índice é incrementado a cada upload de artigo, o que garante que o endereço gerado seja único.

Em seguida, o programa desserializa instruction_data_inner em dados de artigo, que são do tipo PostArticleData:

Essa estrutura tem apenas dois campos: title e content. Nas linhas 45 a 49, o programa realiza uma verificação para garantir que o artigo não seja grande demais para ser contido em uma única conta.

Os passos seguintes também são semelhantes à instrução init: O programa primeiro calcula o espaço e o aluguel necessários para a conta, depois invoca a instrução create_account do SystemProgram para criar a conta PDA que armazenará o artigo. Por fim, o artigo publicado é serializado no campo data de new_article_pda, e o índice máximo atual é incrementado em um e serializado no campo data de index_pda.

list_articles

Por fim, vamos ver a implementação da instrução list. Como essa instrução precisa listar todos os artigos, um vetor é usado na linha 15 para representar todas as contas PDA que armazenam os artigos, e o endereço da conta index_pda também é validado.

Depois disso, o programa verifica que o tamanho do vetor é igual ao índice máximo atual e verifica a correção do endereço de cada conta.

No final do programa, ele itera sobre cada conta e usa a macro msg! do solana-program para exibir o conteúdo desserializado de cada artigo um por um.

Teste de Transações

Para testar nosso contrato, usamos um script TypeScript localizado em client/main.ts no repositório.

No topo do script, importamos todas as bibliotecas necessárias e definimos três constantes globais. KEYPAIR_PATH indica o caminho para o arquivo de chave privada gerado usando solana-keygen new, PROGRAM_ID é o endereço do programa implantado que escrevemos na última seção, e POST_ARTICLE_SCHEMA é o objeto usado para serialização de artigos.

No corpo principal do script, ele primeiro chama a função loadKeyFromFile para analisar o arquivo de chave privada e obter um objeto Keypair como pagador das taxas de transação. Em seguida, usa o método findProgramAddressSync fornecido por @solana/web3.js para calcular o endereço de indexPda. Este método usa o mesmo algoritmo que o contrato Rust, garantindo que calcule o mesmo endereço com os mesmos parâmetros. O objeto connection especifica que usaremos a devnet para os testes.

Em seguida, enviamos a primeira transação de inicialização. Criamos uma transação que contém uma única instrução. O campo data desta instrução contém apenas um byte "0", indicando que esta é uma invocação à instrução init. A transação é então enviada e confirmada usando sendAndConfirmTransaction.

A segunda transação invoca a instrução post para publicar alguns artigos. Das linhas 56 a 63, o script serializa dois artigos e calcula os endereços PDA correspondentes. Em seguida, duas instruções post são adicionadas a uma única transação para criar esses artigos.

A transação final invoca a instrução list, e em seguida os logs são impressos após a confirmação da transação.

Para testar o programa com o script, substitua o KEYPAIR_PATH pelo caminho para sua chave privada (se não for o caminho padrão). Em seguida, compile e implante o contrato inteligente Rust executando os seguintes comandos:

cd program
cargo build-bpf
solana program deploy ./target/deploy/hello_solana.so

Em seguida, copie o endereço implantado e cole-o no campo PROGRAM_ID. No diretório raiz do projeto, execute npm install para instalar as dependências, e depois execute npm start para executar o script.

Depois disso, podemos usar o Solscan para visualizar os detalhes de execução das transações de teste.

Vamos ver diretamente a segunda transação para publicação de artigos. Ela contém duas instruções, cada uma com o primeiro byte do Instruction Data Raw sendo 0x01. Além disso, cada instrução inclui uma instrução interna, que é uma invocação à instrução CreateAccount do System Program.

Da mesma forma, podemos inspecionar a terceira transação para listar todos os artigos. Na seção Program Logs, podemos verificar que os índices, títulos e conteúdos dos dois artigos são impressos.

Conclusão

Neste artigo, primeiro explicamos como configurar o ambiente de programação e execução da Solana localmente. Em seguida, detalhamos a lógica de implementação de um contrato Solana. Por fim, testamos as funcionalidades do contrato usando um script TypeScript. Com este tutorial passo a passo, acreditamos que você aprendeu como escrever um contrato inteligente Solana simples 🥳.

No próximo artigo, forneceremos um guia detalhado sobre como visualizar e analisar transações Solana usando o Phalcon Explorer (o Phalcon Explorer agora suporta Solana). Fique ligado!

Leia outros artigos desta série:

Sign up for the latest updates
Como a solução de segurança de pagamentos da BlockSec ajuda instituições a se manterem seguras na era da IA
Security Services

Como a solução de segurança de pagamentos da BlockSec ajuda instituições a se manterem seguras na era da IA

Descubra como a BlockSec ajuda instituições de pagamento a proteger recursos financeiros e dados, além de fortalecer sua resiliência por meio de avaliações de segurança, testes e monitoramento contínuo.

Perdas de ~5,57 milhões USD: exploração de falhas no X100 Freezer e no NEM | Boletim semanal da BlockSec
Security Insights

Perdas de ~5,57 milhões USD: exploração de falhas no X100 Freezer e no NEM | Boletim semanal da BlockSec

Este relatório semanal aborda dois incidentes de segurança na Ethereum e na BNB Chain no período de 19/10/2026–25/10/2026. Ele examina uma tomada de controle da governança do X100 Freezer financiada por um empréstimo-relâmpago e uma falha na liquidação de impostos que afetou um pool de liquidez de um token NEM na BNB Chain. As perdas estimadas no escopo do relatório totalizam aproximadamente 5,57 milhões USD: cerca de 4,75 milhões USD da transação representativa do X100 e cerca de 820 mil USD do incidente no pool do NEM. O total exclui as extrações subsequentes do X100 relatadas separadamente.

Perdas de ~3,87 milhões USD: ataques à Autheo Network e à UNCX | Resumo semanal da BlockSec
Security Insights

Perdas de ~3,87 milhões USD: ataques à Autheo Network e à UNCX | Resumo semanal da BlockSec

Este relatório semanal aborda 3 incidentes de segurança no período de 12/10/2026–18/10/2026, com perdas totais estimadas em aproximadamente 3,87 milhões USD. A Autheo Network sofreu um comprometimento de chave privada com perdas de cerca de 1,76 milhões USD, a Qubic Wallet perdeu cerca de 1,30 milhões USD devido à geração de seeds fracas, e os contratos Smart Token Vesting V2 da UNCX Network sofreram um erro contábil de cerca de 807 mil USD nas redes Ethereum, BNB Chain, Base e Robinhood Chain. A análise em destaque da UNCX explica como um cálculo incorreto do total de cotas misturou parcelas de vesting criadas com diferentes proporções entre tokens e cotas, permitindo saques acima do devido de pools compartilhados.