Tecnologia

18 ago, 2023

Tutorial de Smart Contract para Staking em Solidity

Publicidade

Em outro momento eu ensinei aqui no blog sobre como você pode criar um primeiro protocolo DeFi em Solidity, que eu chamei de “protocolo Colchão”, isso porque ele serviria para as pessoas guardem dinheiro na blockchain, mas sem rendimento algum, como é feito pelas pessoas que guardam dinheiro embaixo do colchão. Mencionei na ocasião que algo muito comum em protocolos DeFi (e também em bancos) é o uso de recursos dos correntistas/depositantes em outros protocolos que gerem dividendos, os chamados protocolos de staking.

Mas como que esses protocolos são feitos? É isso que você vai aprender no tutorial de hoje: como fazer um protocolo de staking, a fim de remunerar os usuários do seu protocolo DeFi. Usaremos a ferramenta Remix aqui, que é gratuita e dispensa qualquer setup, mas você pode usar toolkits mais profissionais como Truffle e HardHat sem problemas. O importante mesmo é que você já saiba programar Solidity, pelo menos em nível básico, o que você pode aprender neste outro tutorial.

Então vamos lá!

#1 – Liquidity Provider Token

O primeiro passo quando estamos criando um protocolo DeFi profissional é ter um token ERC-20 criado especificamente para ele. Isso porque é através deste token que vamos captar recursos para o protocolo, afinal, sem dinheiro, sem DeFi.

Quando estamos falando de protocolos DeFi, os correntistas ou depositantes são chamados de liquidity providers ou provedores de liquidez, pois é isso que eles fazem: fornecem liquidez para que seu protocolo possa funcionar. Os liquidity providers esperam duas coisas quando atuam em prol de um protocolo fornecendo recursos a ele:

  • garantias que terão seu dinheiro de volta;
  • rendimento sobre o dinheiro depositado;

Para ambos os casos, ter um token ERC-20 próprio do protocolo ajuda e chamamos ele de liquidity provider token, a despeito do nome comercial que você quiser inventar para seu protocolo. Um LPT é um token ERC-20 normal, mas que possui algumas características comuns, como por exemplo:

  • sempre que um liquidity provider adicionar fundos no seu protocolo, ele recebe LPT como garantia (mint);
  • esses LPTs serão usados mais tarde para resgatar seus fundos de volta, caso assim ele deseje, o que ocasiona na queima dos LPTs (burn);
  • esses LPTs podem ser negociados nas exchanges, pois valem dinheiro (seriam como se fossem os CDBs ou Certificados de Depósito Bancário do mundo real);
  • os dividendos dos protocolos serão pagos em LPT também;

Sendo assim, vamos começar criando um token ERC-20, algo que ensinei em profundidade como fazer neste outro tutorial, sendo que abaixo apenas vou colocar um código pronto para você e explicar apenas o que for diferente.

O que temos aqui de diferente é o uso do pattern Ownable, para determinar que quem fizer o deploy do contrato será o administrador dele (owner). Além desse owner, também incluí uma variável de estado protocolContract e uma função setProtocol para que possamos autorizar o endereço de algum contrato DeFi que poderá chamar as funções de mint e de burn, sendo que esta última para que funcione me fez adicionar também a herança a ERC20Burnable.sol.

E por fim, a outra diferença crucial é o uso de uma interface ILPToken.sol que mostro abaixo, que servirá para tornar nosso protocolo mais dinâmico, sem a necessidade dele conhecer os detalhes do smart contract do LPT.

Essa interface define apenas que nosso token de garantia/recompensa do protocolo deve ter funções de mint e de burn, algo que não é obrigatório no padrão ERC-20, então é como se fosse uma extensão dele.

Com o seu LTP pronto, podemos avançar para a próxima etapa.

#2 – Protocolo DeFi

O próximo passo é você criar o contrato do protocolo DeFi em si. Inicialmente ele será como o protocolo Colchão que fiz anteriormente, permitindo apenas depósito e saque, nada demais, como abaixo.

Aqui eu defini logo de cara que usaremos o ReentrancyGuard da OpenZeppelin para evitar Reentrancy Attacks nas nossas funções de depósito e saque.

Depois, eu declaro o token ERC-20 que é a reserva do protocolo (por exemplo, WETH) e o token ERC-20 que será nosso Liquidity Provider Token (LPT). Enquanto que para o primeiro eu posso usar a interface padrão IERC20, para o segundo vamos usar a interface que nós mesmos definimos, que extende a IERC20. Logo adiante, no construtor, inicializamos esses dois tokens a partir de seus endereços logo no deploy.

Depois temos as funções de depósito e saque, respectivamente, onde além da transferência dos valores para dentro e para fora do protocolo, nós fazemos o mint e o burn dos LPT na mesma proporção, fazendo com que o LPT acabe se tornando uma espécie de wrapped token do token ERC20 de reserva do protocolo.

Por fim, adicionei uma função para ver o saldo da reserva de liquidez (liquidity pool) do protocolo, o que pode ser útil em situações reais.

Mas e o staking? Pois é, o código acima é um Colchão V1 ainda, mas vamos evolui-lo para v2 na sequência.

#3 – Protocolo de Staking

É importante deixar bem claro que o algoritmo de staking que vou propor abaixo é didático e não fiz nenhuma análise econômica da viabilidade financeira dele até porque o modelo de negócios que irá gerar rentabilidade como seu protocolo DeFi pode variar enormemente e não é o foco do artigo ou a minha especialidade. Me aterei meramente a lhe ensinar uma algoritmo que vai premiar os liquidity providers conforme o tempo deles mantendo os recursos conosco. Quanto mais tempo eles deixarem seus recursos conosco, mais eles receberão recompensas de LPT.

Existem várias formas de computar as recompensas, então adotarei uma abordagem em que o liquidity provider vai receber x weis de LPT (a menor fração do token) considerando que:

  • x é um fração (%) proporcional a quanto de liquidez ele forneceu ao protocolo;
  • e que x é multiplicado pelo tempo que ele deixou a liquidez conosco;
  • e que x é multiplicado por um fator y de recompensas por período, definido por você;

Dado esta ideia, vamos começar definindo três variáveis de estado que vamos usar como auxiliares ao cálculo:

O mapping checkpoints vai registrar para cada liquidity provider o timestamp em que ele fez o último depósito. Já o rewardPerPeriod vai registrar o fator de multiplicação de recompensas por período de tempo que será pago. O tamanho desse período é definido pela variável duration, inicialmente setada em 30 dias (em segundos, unidade de tempo usada na blockchain).

Agora vamos criar uma função que calcula a recompensa a ser paga pela liquidez fornecida dado o tempo que passou (em períodos iguais):

Nessa função eu começo calculando quantos meses se passaram desde o último depósito registrado (checkpoint). Então eu pego 1% do saldo fornecido e multiplico pelo número de meses do stake e pela quantidade de recompensas por período. Esse número deve dar 1% a.m. de rendimento com as configurações padrões do código. Ajuste de acordo se quiser dar mais ou menos prêmios, lembrando que não estou discutindo aqui viabilidade econômica, apenas dando uma ideia.

Essa função vai ser útil na hora de pagar recompensas, que será feita através da função abaixo, ou quando você quer apenas saber quanto teria de retorno se sacasse agora seus fundos do protocolo.

Na função acima a gente manda calcular as recompensas e vê se tem recompensas a serem pagas ou se o usuário acabou de fazer um depósito (depositAmount > 0), em ambos casos precisamos mintar LPT para o usuário na proporção necessária e independente de tudo devemos registrar o checkpoint de que não devemos nada para este usuário nesta data. Essa função rewardPayment será usada tanto nos depósitos quanto nos saques, para pagarmos as recompensas devidas.

Por exemplo, no depósito temos:

Começamos transferindo os tokens para nosso pool de liquidez a partir da carteira do liquidity provider usando transferFrom (tranferência delegada). Depois, verificamos se é o primeiro depósito dele. Se for, vamos registrar o checkpoint e fazer o mint dos tokens na mesma quantidade depositada. Se não for o primeiro depósito, chamamos a função rewardPayment que criamos antes. Ela irá pagar as recompensas devidas desde o último depósito e vai mintar os tokens do novo depósito (sem recompensa) também. Eu poderia ter feito estas duas operações separadamente para maior clareza, mas como é uma transação na blockchain, é melhor um mint do que dois mints, por economia de taxas.

Já no saque, temos:

Aqui temos a verificação se o usuário já está ao menos 30 dias fazendo staking conosco desde seu último depósito, pois uma das exigências comuns de protocolos de staking (e que o diferem de outros protocolos DeFi) é que você deve deixar os fundos bloqueados, sem resgate, por um tempo pré-determinado. No nosso caso, coloquei 30 dias (duration), mas você pode ajustar como quiser. Além disso, eu não estou pagando recompensas pro rata (proporcionais ao tempo), mas sim no modelo de “aniversário”, assim como a caderneta de poupança faz.

Depois, verificamos se o usuário possui saldo para este saque, para então fazermos o rewardPayment, visando quitar recompensas devidas e depois o burn do LPT e transfer dos tokens do liquidity pool. É importante que a ordem seja essa para que o rewardPayment use o saldo passado no cálculo das recompensas devidas. Como tanto esta função quanto a de depósito estão com o function modifier de nonReentrant (do ReentracyGuard) não precisamos nos preocupar com reentradas.

E com isso finalizamos nosso protocolo de staking chamado ColchaoV2.

*O conteúdo deste artigo é de responsabilidade do(a) autor(a) e não reflete necessariamente a opinião do iMasters.