WEB3DEV

Cover image for Solana para Desenvolvedores Sem Experiência em Contratos Inteligentes
Paulo Gio
Paulo Gio

Posted on • Atualizado em

Solana para Desenvolvedores Sem Experiência em Contratos Inteligentes

Uma introdução aos contratos inteligentes da Solana para desenvolvedores sem experiência em contratos inteligentes e desenvolvedores de contratos inteligentes sem experiência na Solana.

Este artigo apresenta os conceitos mais importantes e descreve os modelos de programação e segurança de programação de contratos inteligentes na Solana. É destinado a desenvolvedores experientes que podem ou não ter experiência prévia em desenvolvimento de contratos inteligentes em outras blockchains. É pressuposta a familiaridade com Rust.

O objetivo deste artigo é cobrir todos os conceitos importantes que você precisaria para começar a desenvolver contratos inteligentes na Solana e descrever como eles se conectam. Para ser conciso, não me aprofundo em nenhum dos conceitos em muitos detalhes. Os links que incluí na seção "Conclusão" no final do artigo são um bom ponto de partida para se aprofundar em qualquer um dos tópicos.

https://miro.medium.com/v2/resize:fit:1100/format:webp/1*evlGk31z_ii2an1uiubbLQ.png

Modelo de programação

O modelo de programação de contratos inteligentes da Solana é baseado em programas e contas.

Observação: na Solana, os contratos inteligentes são comumente referidos como "programas". Então, quando você ouvir "programas", pense em contratos inteligentes (esses termos são usados ​​de forma intercambiável). Além disso, esqueça o que você sabe sobre EVM, porque algumas das terminologias aqui são confusas.

Os Programas (contratos inteligentes) definem a lógica que será processada quando o programa for chamado em uma transação. Você pode passar argumentos arbitrários para uma chamada de programa, mas os programas são puros no sentido de que, por si só, não podem persistir, ler ou gravar qualquer estado. Para permitir a persistência e o estado, os programas trabalham com contas. As chamadas de programa são feitas por clientes (geralmente aplicativos da web, mas você também pode chamá-los diretamente manualmente).

As contas são essencialmente o que armazena o estado do programa. Os programas gravam em e leem de contas e esses dados são persistidos nas transações. Cada conta é identificada de forma única por seu endereço (a chave pública de um par de chaves Ed25519). As contas têm um tamanho especificado e armazenam dados arbitrários. Além disso, há metadados associados a cada conta, que carregam informações importantes sobre a conta. Os programas em si (executáveis) também são armazenados em contas e têm um endereço (referido como ID do programa). Não há uma estrutura inerente aos dados da conta - do ponto de vista do tempo de execução, é apenas uma matriz de bytes de tamanho especificado.

Você pode pensar no espaço de uma conta da Solana como um armazenamento global de chave-valor, onde as chaves são endereços de conta e os valores são dados da conta. Os programas operam em cima desse armazenamento de chave-valor, lendo e modificando seus valores.

É importante notar que, para cada chamada de programa, o cliente (geralmente um aplicativo da web) precisa especificar antecipadamente quais contas o programa acessará na chamada e se também deseja gravar ou ler algo delas. Isso é muito importante para o desempenho do processamento de transações - como o tempo de execução agora sabe quais contas cada transação modificará, ele pode agendar transações não sobrepostas para serem executadas em paralelo, garantindo a consistência dos dados. Este é um dos principais motivos pelos quais a Solana pode manter uma alta taxa de transferência e também quando você ouvir pessoas dizerem "a EVM não é escalável", isso é em grande parte a que se referem - ela não pode fazer paralelização de processamento de transações. Você pode ler mais sobre o tempo de execução de processamento de transações da Solana no artigo publicado por Anatoly Yakovenko em 2019.

Modelo de Segurança

Propriedade de Conta

As contas da Solana possuem uma noção de propriedade. Cada conta é estritamente propriedade de um (e somente um) programa. Isso significa que apenas o programa que possui a conta pode modificá-la. Nenhum outro programa pode modificá-la, apenas lê-la. As informações de propriedade da conta são armazenadas em seus metadados e não podem ser modificadas pelos programas do usuário.

Assinaturas de Conta

Os clientes também podem passar, juntamente com as contas, assinaturas de conta nas chamadas de programa (lembre-se de que cada conta está associada a um par de chaves Ed25519). Quando você possui uma chave privada de uma conta, pode assinar uma transação com ela e, em seguida, essa conta é marcada como "signatário" no tempo de execução do programa. Essas informações podem então ser usadas arbitrariamente pelos programas para implementar funcionalidades de propriedade e autoridade. De fato, é assim que as carteiras são implementadas na Solana. Cada conta tem uma quantidade de SOL associada a ela (isso também é armazenado nos metadados da conta). Quando você deseja transferir SOL de uma conta para outra, precisa assinar a transação com a chave privada da conta (onde a chave pública é o endereço da conta). Portanto, as carteiras da Solana são, essencialmente, contas para as quais você possui chaves privadas.

Nota: não é necessário que as contas armazenem qualquer dado. Muitas vezes, as contas são usadas apenas por suas capacidades de assinatura de chaves privadas - seu tamanho alocado é 0 e elas não armazenam dados, mas suas assinaturas são usadas, por exemplo, para implementar funcionalidades de autoridade e propriedade.

Chamadas de CPI

Você também pode chamar programas de outros programas por meio da interface de CPI (cross-program invocation, ou invocação entre programas). As chamadas de CPI são muito semelhantes às chamadas de programa do cliente - você referencia o programa que deseja chamar pelo seu ID de programa e passa os argumentos e contas necessários. Internamente, as chamadas de CPI são implementadas por meio de uma chamada de sistema que faz com que o tempo de execução execute o programa especificado praticamente da mesma forma que faria se você o chamasse diretamente do lado do cliente.

PDAs

Os programas em si também têm a capacidade de fornecer assinaturas de conta em chamadas de CPI. Isso é implementado por meio de contas de PDA (program derived address, ou endereço derivado do programa). As contas de PDA são tipos especiais de contas que só podem ser assinadas por um programa. Elas são específicas do programa e cada programa pode gerar quantos PDAs quiser. Os usuários ou outros programas nunca poderão fornecer assinaturas para PDAs criados para um programa específico. Os PDAs são importantes porque permitem que os programas tenham capacidades de autoridade e propriedade. Por exemplo, eles permitem que os programas possuam tokens. É assim que, por exemplo, os cofres de token são implementados, onde apenas o programa (e nenhum usuário ou outro programa) tem a autoridade de retirar tokens do cofre. Devido às garantias que os PDAs fornecem, o cofre é totalmente protegido pela lógica do programa. Em outras palavras, se o programa estiver correto, sabemos que o cofre está seguro.

É importante reconhecer a diferença entre propriedade de conta e assinaturas. As contas são de propriedade dos programas, o que dá ao programa proprietário permissão para modificá-las. As chaves privadas das contas são mantidas pelos usuários, permitindo que o usuário que possui a chave privada de uma conta assine uma transação com ela e isso marcará a conta como "assinada" durante a execução do programa. As contas assinadas ou não assinadas não têm nenhum significado especial no tempo de execução e é totalmente com a implementação do programa dar significado a isso.

Por exemplo, no Programa de Token SPL (que implementa funcionalidades semelhantes às do padrão ERC20 e é separado dos mecanismos de transferência de SOL descritos no início desta seção), cada conta de token é de propriedade do Programa de Token, o que significa que apenas o programa de token pode alterar os valores dessas contas (por exemplo, adicionar ou subtrair quantidades). Mas em cada conta de token, o Programa de Token armazena um campo sobre o endereço (chave pública) da conta de autoridade que tem permissão para gastar os tokens (e que é diferente do proprietário da conta armazenado nos metadados da conta). Então, quando você faz uma chamada de transferência no Programa de Token, é necessário fornecer a assinatura correspondente ao campo de autoridade da conta de token de origem especificada. O Programa de Token irá, durante a execução, verificar se a assinatura fornecida corresponde à autoridade armazenada na conta de token (verificando se a conta correta está marcada como “signatário”) e, se corresponder, permitirá a transferência. Portanto, somente quando você possui a chave privada da autoridade, você pode gastar com essa conta. Quando a autoridade é uma conta de PDA, significa que apenas o programa correspondente a esse PDA pode gastar os tokens dessa conta (por meio de uma chamada CPI). Além disso, o tempo de execução da Solana verificará se a conta de token que está sendo modificada (as quantidades estão sendo subtraídas) é de propriedade do Programa de Token. Isso é para que o Programa de Token seja impedido de modificar quaisquer contas que não possua.

Em Resumo

  • As contas são de propriedade de programas que lhes dão permissão exclusiva para modificá-las;
  • Você pode assinar uma transação com a chave privada da conta e o tempo de execução marcará essa conta como um "signatário", cuja semântica é definida pela implementação do programa.
  • Os programas podem chamar uns aos outros usando chamadas de CPI;
  • Os PDAs permitem que os programas também forneçam assinaturas de contas.

Essas quatro coisas são os blocos de construção básicos para programação de contrato inteligente combinável e segura na Solana. Usando isso, você pode construir praticamente qualquer coisa que precisar.

Programação de Contratos Inteligentes na Solana

Os contratos inteligentes da Solana podem ser escritos em qualquer linguagem de programação que compila para SBF (Solana Bytecode Format, uma modificação do eBPF), mas geralmente são escritos em Rust. Todos os programas na Solana definem um ponto de entrada (entrypoint) que se parece com este:

https://miro.medium.com/v2/resize:fit:1100/format:webp/1*psg--81-8SKZmT4qNDVnBg.png

Quando o programa é chamado, o tempo de execução chama a função de ponto de entrada passando:

Id do programa (program ID) que está sendo chamado (endereço da conta que armazena o programa) - porque às vezes (muito raramente) é útil implantar o mesmo programa em diferentes endereços (IDs de programa);

  • Contas arbitrárias especificadas pelo cliente;
  • Dados de instrução arbitrários especificados pelo cliente (normalmente usados ​​para codificar argumentos).

Em seguida, o programa precisa processar essas entradas e executar sua lógica.

Observação: cada transação pode conter várias chamadas sequenciais de programas (para os mesmos ou diferentes programas) referidas como "instruções". As transações são atômicas no sentido de que, se alguma das instruções (chamadas de programa) falhar, a transação inteira falhará e não terá efeito no estado global.

Cada struct AccountInfo contém os dados da conta, bem como alguns metadados relevantes - como qual programa é seu proprietário, se a conta pode ser mutada, se a transação foi assinada com sua chave privada, etc...

Segurança de entrada

Podemos ver logo de cara que, como o cliente pode passar qualquer conta e qualquer dado de instrução, o programa precisa ter muito cuidado ao processar essas entradas para garantir que entradas criadas de forma adversa não possam afetar a execução do programa de maneiras inesperadas.

Por exemplo, ao processar uma chamada de transferência, o Programa de Token precisa verificar se a conta de token de origem fornecida pelo cliente é uma conta de token válida - ou seja, pertence ao Programa de Token e é do tipo correto (o Programa de Token também possui contas de cunhagem que armazenam dados diferentes). Como as contas de token só podem ser criadas por meio de uma chamada de instrução de inicialização de conta no Programa de Token, após essas verificações, sabemos que o uso dessa conta é seguro. Caso contrário, alguém poderia criar uma conta de token personalizada usando um programa personalizado, passá-la como uma conta real e, efetivamente, criar uma quantidade arbitrária de tokens porque o Programa de Token permitiria transferências de tokens dessa conta falsa, depositando-os como tokens reais na conta de destino. Na verdade, uma exploração do mesmo tipo (passando uma conta falsa) foi o que levou à perda de US$326 milhões de fundos do Wormhole (e inúmeros outros hacks semelhantes).

Instruções

Na maioria das vezes, queremos implementar múltiplas chamadas de instrução diferentes no mesmo programa - por exemplo, o Programa de Token implementa InitializeAccount, InitializeMint, Transfer e inúmeras outras chamadas de instrução - cada uma das quais requer diferentes argumentos de chamada. Um padrão comum é codificar um discriminador no primeiro byte do argumento instruction_data na função de ponto de entrada. Em seguida, no início do programa, decodificamos o primeiro byte que nos dirá qual instrução o usuário está tentando chamar (com um byte podemos diferenciar até 256 instruções diferentes). Podemos codificar os argumentos de instrução nos bytes restantes da sequência de bytes instruction_data. O Programa de Token é um bom exemplo desse padrão.

Em resumo, aqui está um fluxo aproximado dos programas da Solana:

  • Definição da função de ponto de entrada;
  • Processamento de dados de instrução para decodificar a chamada de instrução e seus argumentos;
  • Chamada do manipulador de instrução relevante;
  • Verificações de conta e argumento (super importante fazer isso corretamente, pois essas entradas não são validadas)
  • Processamento da instrução. Decodificação dos dados da conta, se necessário.

Anchor

Os programas da Solana são escritos em Rust, uma linguagem muito poderosa, flexível e segura. O Rust nos dá uma tela em branco, mas como vimos no capítulo anterior, existe um tema comum e padrões quando se trata de implementar contratos inteligentes, como:

  • Decodificação das instruções e seus argumentos a partir dos dados da instrução (instruction_data);
  • Implementação de manipuladores (handlers) para cada instrução;
  • Verificações de contas;
  • Diferenciação entre vários tipos de contas;
  • Codificação e decodificação de dados em contas;
  • ...

Essas são coisas que praticamente todos os programas fazem, mas o Rust em si não nos oferece ferramentas para agilizar pelo menos algumas delas. É aqui que entra o Anchor...

https://miro.medium.com/v2/resize:fit:1100/format:webp/1*aOVVIob0GQnK1uyDsKzWZw.png

O Anchor é um framework para desenvolvimento de contratos inteligentes na Solana usando a linguagem Rust. Ele preenche algumas lacunas que temos com o Rust puro e fornece padrões seguros. Isso torna o desenvolvimento de contratos inteligentes na Solana muito mais ergonômico e seguro - não apenas porque remove armadilhas e fornece padrões seguros, mas também porque torna as coisas críticas de segurança mais explícitas e legíveis. Ele também fornece muitas ferramentas que economizam tempo.

O que o Anchor fornece por padrão:

  • Discriminação de instruções e análise de argumentos;
  • Serialização e desserialização seguras de contas;
  • DSL incorporado para fazer verificações de conta;
  • Código de biblioteca para chamadas de CPI;
  • Erros e eventos;
  • Composição trivial com outros programas Anchor;
  • Geração de IDL (Interface Description Language, ou Linguagem de Descrição de Interface) e SDKs de cliente;
  • Geração de estrutura da área de trabalho (workspace);
  • ...

Um programa de token escrito em Anchor poderia se parecer com isto:

https://miro.medium.com/v2/resize:fit:1100/format:webp/1*VYYUNNa4WMSGFWHDwt_FBg.png

Como você pode perceber, este exemplo não é um programa completo, pois implementa apenas duas instruções — initialize_account e transfer — um programa completo teria que, no mínimo, implementar a instrução initialize_mint e uma conta Mint, o que não é abordado aqui. Além disso, eu omiti os corpos das instruções para me concentrar nos recursos do Anchor em vez da lógica específica do programa.

Há muito que o Anchor faz aqui nos bastidores, então vamos entender isso.

Instruções

Em primeiro lugar, podemos ver que o módulo de token é anotado com uma macro #[program] (linha 5). Essa macro será expandida em uma função de ponto de entrada (veja o capítulo anterior) e adicionará código para discriminar entre diferentes chamadas de instrução (neste caso, as instruções initialize_account e transfer). O discriminador de instrução é codificado nos primeiros 8 bytes dos dados da instrução e é derivado internamente do nome da instrução (portanto, instruções com nomes diferentes terão discriminadores diferentes). O restante dos dados da instrução codifica os argumentos da instrução - por exemplo, para a chamada de instrução transfer, o discriminador será seguido pelo parâmetro amount codificado. O Anchor usa a serialização Borsh para codificação / decodificação.

Contextos

Em seguida, podemos ver que cada instrução tem um contexto como seu primeiro argumento - por exemplo, na instrução initialize_account temos ctx: Context<InitializeAccount> (linha 9). Essas structs são semelhantes à fatia (slice) &[AccountInfo] no ponto de entrada (ver capítulo anterior), no sentido de que eles possuem as contas que foram passadas como entradas para a chamada de instrução, mas a principal diferença é que essas contas são validadas. Para entender o que isso significa, vamos dar uma olhada na struct InitializeAccount (linha 30) correspondente. Essa struct é anotada com a macro #[derive(Accounts)] e os campos representam as contas que a instrução initialize_account espera como entradas. Essas contas são new_token_account, system_program e payer. Quando o Anchor processa essa chamada de instrução, ele tenta carregar cada conta da entrada de contas (fatia &[AccountInfo]) no campo correspondente da struct ctx (em ordem sequencial - a primeira conta da fatia no primeiro campo).

Vamos olhar cada campo da conta individualmente:

new_token_account é a nova conta de token que será criada para armazenar o estado. Seu tipo é Account<'info, TokenAccount>. Isso diz ao Anchor que esta conta precisa ser de propriedade deste programa (esta afirmação é feita pelo atributo (trait) Account aqui), e os dados codificados dentro dela são a struct TokenAccount (definido na linha 21). Este campo também é anotado com uma macro #[account(...)]:

  • init - isso indica ao Anchor que a conta precisa ser inicializada. Quando a instrução é chamada, o Anchor fará uma chamada CPI para o Programa do Sistema para inicializar esta conta. Isso é muito conveniente, pois caso contrário, teríamos que fazer isso manualmente e potencialmente cometer um erro (existem algumas nuances na inicialização de contas que o Anchor trata para nós)
  • space - uma vez que estamos inicializando uma nova conta, precisamos dizer ao Anchor qual tamanho ela precisa ter (o Anchor não pode inferir isso por conta própria). Criamos uma conta com o tamanho de 80 bytes (8 + 32 + 32 + 8). Os primeiros 8 bytes são reservados para o discriminador da conta (mais sobre isso abaixo) e o restante é espaço reservado para os campos TokenAccount (linha 21). Os campos authority e mint são do tipo Pubkey, que tem 32 bytes. O campo de saldo é um u64, que tem 8 bytes.
  • payer - esta é a conta que pagará pela criação da conta (aluguel). Refere-se à conta payer na linha 39.

system_program se refere ao Programa do Sistema, que é um programa especial na Solana usado para criar contas, transferir SOL, etc. Ele precisa ser passado porque estamos fazendo uma chamada CPI para inicializar uma nova conta de token (lembre-se de que todas as contas que uma instrução precisa acessar e programas também são armazenados como contas e precisam ser especificados pelo cliente para que o tempo de execução possa paralelizar o processamento da transação). O tipo deste campo é Program<'info, SystemProgram>, que fará com que o Anchor verifique se a conta que foi passada corresponde ao Programa do Sistema.


Aprenda a criar e implementar contratos inteligentes na Solana. Desenvolva soluções personalizadas na blockchain Solana!

payer é necessário porque estamos inicializando uma nova conta (linha 36). O tipo deste campo é Signer<'info>, o que fará com que o Anchor verifique se a transação está assinada com a chave privada desta conta. Isso é necessário, já que vamos gastar SOL desta conta para pagar a inicialização da conta de token. O campo da conta payer, ou seja, do pagador, também é anotado com o atributo #[account(mut)], o que significa que os clientes que chamam esta instrução precisam marcá-la como mutável (novamente, porque o SOL será subtraído desta conta).

A struct ctx Transfer (linha 42) também é muito semelhante. A conta source é a conta de token da qual transferimos os tokens, ou seja, de origem. Já a conta destination é a conta onde os tokens serão depositados, ou seja, de destino. Está anotado com constraint = destination.mint == source.mint. Isso verificará se as contas de token pertencem ao mesmo "tipo" de token (devemos impedir transferências de uma conta de token USDC para uma conta de token SRM, por exemplo). A conta authority é um Signer (signatário), e com address = source.authority, verificamos se esta conta de signatário corresponde à autoridade da conta source. Isso torna impossível para qualquer pessoa, exceto o proprietário da chave privada da conta authority, gastar da conta source.

Então, acho que isso ilustra muito bem como o Anchor torna a programação de contratos inteligentes muito mais ergonômica e segura. Sem o Anchor, teríamos que fazer toda essa inicialização, processamento de argumentos, codificação/decodificação, verificações de conta, etc. manualmente. E se você cometer algum erro em qualquer um desses processos, pode facilmente levar à perda de fundos do usuário (como vimos em numerosos ataques que já aconteceram).

Para cada programa, o Anchor gera uma IDL que descreve como o programa deve ser chamado do lado do cliente (semelhante às ABIs na Ethereum). Existem várias ferramentas disponíveis que podem usar as IDLs para simplificar a interação com o programa do lado do cliente:

  • [@project-serum/anchor](https://www.npmjs.com/package/@project-serum/anchor) — o pacote npm oficial do Anchor (cliente dinâmico em Typescript)
  • [anchor-client-gen](https://github.com/kklas/anchor-client-gen) — gera clientes em Typescript a partir das IDLs
  • [anchorpy](https://github.com/kevinheavey/anchorpy) — clientes estáticos (codegen) e dinâmicos para Python
  • [anchor-gen](https://github.com/saber-hq/anchor-gen) — gera clientes CPI Rust a partir das IDLs.

Ao iniciar o desenvolvimento de um novo contrato inteligente na Solana, é altamente recomendável utilizar o Anchor em vez do Rust "puro", a menos que você tenha uma boa razão para não fazê-lo.

Conclusão`

Este artigo fornece uma visão geral dos conceitos mais importantes no desenvolvimento de contratos inteligentes na Solana, descreve os modelos de programação e segurança e dá uma introdução ao framework Anchor.

Para leituras adicionais, recomendo os seguintes recursos:

Também é recomendável juntar-se ao discord de desenvolvedores Solana e ao Stack Exchange da Solana.

Siga-me no Twitter: @kklas_

Artigo original publicado por Krešimir Klas. Traduzido por Paulinho Giovannini.


Crie seu próprio token na Solana e explore novas possibilidades. Aprenda a emitir e distribuir tokens na rede Solana!

Latest comments (0)