WEB3DEV

Cover image for Tutorial de Yield Farming - Parte 1
Diogo Jorge
Diogo Jorge

Posted on

Tutorial de Yield Farming - Parte 1

Contratos inteligentes usando Solidity e Hardhat

Image description

Introdução

Na primeira parte deste tutorial, construiremos um aplicativo descentralizado de yield farming (cultivo de rendimentos) usando o ambiente de desenvolvimento Hardhat e o Solidity. Se você vasculhar o DuckDuckGo, encontrará alguns tutoriais de yield farming; no entanto, não encontrei tutoriais de yield farming que utilizam hardhat e Ethers, nem descobri nenhum tutorial explicando como criar uma calculadora de rendimento automatizada. Geralmente, essas fazendas de rendimento (yield farms) exigem que um proprietário execute um script para fornecer rendimento aos usuários do dApp. Este artigo visa corrigir isso e fornecer as ferramentas para criar algo incrível. Eu recomendo ter alguma experiência com o Solidity para aproveitar ao máximo este tutorial.

Primeiro, o que é uma yield farm? A ideia do yield farm consiste em incentivar os usuários com renda passiva em troca de fornecer liquidez. Para realmente cultivar (farmar), na minha opinião, é necessário que o usuário forneça seu rendimento ganho em outro pool de liquidez; por meio do qual, eles recebem renda passiva sobre sua renda passiva. Esse processo pode, é claro, continuar indo para onde o usuário recebe renda passiva após renda passiva ad nauseam.

Considerando a definição acima, não estaremos tecnicamente construindo um protocolo de “farming”; em vez disso, estamos construindo o primeiro bloco de construção necessário de um protocolo de farming. Depois de entendermos os fundamentos, podemos realmente começar a brincar com os legos de dinheiro DeFi.

Image description

Vamos entrar nessa.

Configuração/Dependências do ambiente

Vamos começar abrindo seu editor de código e criando um novo diretório. Para este projeto, chamei minha fazenda de pmkn (sim, estou cultivando PMKN). Certifique-se de ter o Node instalado (ou Yarn, se preferir).

No terminal do editor de código (estou usando um Mac), cd no diretório do farm. Em seguida, instale as seguintes dependências (seguindo a configuração TypeScript do Hardhat com algumas adições):

npm i --save-dev hardhat
Enter fullscreen mode Exit fullscreen mode

Abra o hardhat com npx hardhat

Role um item para baixo até Crie um hardhat.config.js vazio

Image description

Em seguida, precisamos instalar as dependências do TypeScript. Execute o seguinte:

npm i --save-dev ts-node typescript
Enter fullscreen mode Exit fullscreen mode

E para testar:

npm i --save-dev chai @types/node @types/mocha @types/chai
Enter fullscreen mode Exit fullscreen mode

Em seguida, usaremos um token ERC20 tanto como token de stake quanto como recompensa aos usuários. OpenZeppelin hospeda inúmeras bibliotecas para conveniência dos desenvolvedores. Eles também oferecem excelentes ferramentas de teste. Durante o teste, precisaremos simular a passagem do tempo. Vamos pegar tudo aqui:

npm i --save-dev @openzeppelin/contracts @openzeppelin/test-helpers
Enter fullscreen mode Exit fullscreen mode

Também precisaremos disso para a função time.increase() do OpenZeppelin:

npm em --save-dev @nomiclabs/hardhat-web3 @nomiclabs/hardhat-waffle
Enter fullscreen mode Exit fullscreen mode

Em seguida, se você planeja postar seu trabalho no GitHub ou em qualquer outro lugar fora do seu ambiente local, você precisará dotenv:

npm i --save-dev dotenv
Enter fullscreen mode Exit fullscreen mode

Mudar o hardhat.config para TypeScript:

mv hardhat.config.js hardhat.config.ts
Enter fullscreen mode Exit fullscreen mode

Por fim, vamos alterar a versão do Solidity e reformatar a importação do hardhat-waffle e incluir a importação do hardhat-web3 no hardhat.config.ts:

import "@nomiclabs/hardhat-waffle";
import "@nomiclabs/hardhat-web3"

export default {
  solidity: "0.8.4",
};
Enter fullscreen mode Exit fullscreen mode

Contratos

1. Contrato ERC20 PmknToken

Como gosto de abóboras, este tutorial recompensará os usuários com PmknTokens. Sinta-se à vontade para mudar o nome para o que quiser.

Você ainda deve estar na raiz do diretório e no seu terminal:

mkdir contracts
touch contracts/PmknToken.sol
Enter fullscreen mode Exit fullscreen mode

Vamos fazer nosso contrato de token ERC20 primeiro. Vamos importar o contrato ERC20 do OpenZeppelin enquanto também importamos o contrato do OpenZeppelin Ownable.sol. Você pode ver esses contratos por conta própria em node_modules. Depois de declarar as importações, vamos construir dois wrappers para as funções mint() e TransferOwnership(). O objetivo desses wrappers consiste em controlar quem pode chamar essas funções (portanto, o modificador onlyOwner). A função mint aloca um número específico de tokens para um endereço específico de usuário. Como queremos automatizar esse processo, também incluímos a função transferOwnership() para transferir a propriedade para o contrato da farm; portanto, apenas o próprio contrato pode emitir tokens.

pragma solidity 0.8.4;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract PmknToken is ERC20, Ownable {

    constructor() ERC20("PmknToken", "PMKN") {}

    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }

    /// (ATUALIZAÇÃO) `transferOwnership` já é um método externo herdado de `Ownable`
    /// Obrigado @brianunlam por apontar isso
    ///
    /// function _transferOwnership(address newOwner) public onlyOwner {
    ///     transferOwnership(newOwner);
    /// }
}
Enter fullscreen mode Exit fullscreen mode

2. Contrato PmknFarm

touch contracts/PmknFarm.sol
Enter fullscreen mode Exit fullscreen mode

Em seu contrato PmknFarm, vamos construir o esqueleto do projeto. Estamos construindo um dApp de yield farming; portanto, vamos precisar de uma função que permita aos usuários fazer stake de seus fundos. Também vamos precisar de uma função para resgatar seus fundos. Além disso, os usuários vão desejar retirar seu rendimento. Então, três funções principais. Importe o contrato PmknToken e o contrato IERC20 do OpenZeppelin. Também precisamos declarar alguns mapeamentos de variáveis ​​de estado e eventos para o front-end. Analisaremos cada aspecto do contrato. Primeiro, vamos examinar o construtor, as variáveis ​​de estado e os eventos.

pragma solidity 0.8.4;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./PmknToken.sol";

contract TokenFarm {

    // userAddress => stakingBalance
    mapping(address => uint256) public stakingBalance;
    // userAddress => isStaking boolean
    mapping(address => bool) public isStaking;
    // userAddress => timeStamp
    mapping(address => uint256) public startTime;
    // userAddress => pmknBalance
    mapping(address => uint256) public pmknBalance;

    string public name = "TokenFarm";

    IERC20 public daiToken;
    PmknToken public pmknToken;

    event Stake(address indexed from, uint256 amount);
    event Unstake(address indexed from, uint256 amount);
    event YieldWithdraw(address indexed to, uint256 amount);

    constructor(
        IERC20 _daiToken,
        PmknToken _pmknToken
        ) {
            daiToken = _daiToken;
            pmknToken = _pmknToken;
        }

    /// Shells de funções principais
    stake() public {}
    unstake() public {}
    withdrawYield() public {}
}
Enter fullscreen mode Exit fullscreen mode

Os mapeamentos startTime e pmknBalance podem exigir uma pequena explicação para entender melhor como eles serão usados ​​em nossas funções. O startTime vai manter o registro de data e hora do endereço do usuário para rastrear o rendimento não realizado (unrealized). O pmknBalance apontará para o realizado (realized), ou a quantidade armazenada esperando para ser cunhada, rendimento PmknToken (não confundir com o PmknToken realmente cunhado) associado ao endereço do usuário. Se você não estiver familiarizado com mapeamentos, eles são simplesmente pares chave/valor. Para uma explicação mais aprofundada, sugiro ler o Documentos do Solidity.

pragma solidity 0.8.4;

string public name = "TokenFarm";
Enter fullscreen mode Exit fullscreen mode

Eu sempre declaro uma variável de nome para teste; no entanto, isso não é necessário.

pragma solidity 0.8.4;

IERC20 public daiToken;
PmknToken public pmknToken;
Enter fullscreen mode Exit fullscreen mode

Essas declarações de variáveis ​​de estado precedem o tipo (ou seja, IERC20, PmknToken) e a visibilidade (pública).

Para evitar confusão, recomendo seguir esta convenção:

  • tipo => PascalCasing
  • declaração de estado => camelCasing
  • parâmetro do construtor => _underscoreCamelCasing

Quando comecei a trabalhar com o Solidity, levei algum tempo para entender o que exatamente estava acontecendo com a utilização de tokens ERC20. É assim que eu gostaria que o conceito fosse explicado para mim: O IERC20 e o PmknToken consistem em tipos; como em, o tipo de token importado. A declaração da variável de estado consiste na instância do contrato do tipo token. Por fim, o parâmetro do construtor aponta para o endereço que cria totalmente uma instância de contrato do token importado.

O construtor, para quem não conhece, é uma função utilizada uma única vez durante a implantação do contrato. Um caso de uso comum para um construtor inclui a configuração de endereços constantes (exatamente como estamos fazendo aqui). Para implantar este contrato, o usuário deve inserir endereços para _daiToken e _pmknToken.

Avante para o coração deste artigo.

Funções principais

pragma solidity 0.8.4;

function stake(uint256 amount) public {
        require(
            amount > 0 &&
            daiToken.balanceOf(msg.sender) >= amount, 
            "Você não pode fazer stake de zero tokens");


        if(isStaking[msg.sender] == true){
            uint256 toTransfer = calculateYieldTotal(msg.sender);
            pmknBalance[msg.sender] += toTransfer;
        }

        daiToken.transferFrom(msg.sender, address(this), amount);
        stakingBalance[msg.sender] += amount;
        startTime[msg.sender] = block.timestamp;
        isStaking[msg.sender] = true;
        emit Stake(msg.sender, amount);
    }
Enter fullscreen mode Exit fullscreen mode

A função stake() requer primeiro que o parâmetro de valor seja maior que 0 e que o usuário tenha DAI suficiente para cobrir a transação. A instrução condicional if verifica se o usuário já tem DAI em stake. Nesse caso, o contrato adiciona o rendimento não realizado ao pmknBalance. Isso garante que o rendimento acumulado não desapareça. Depois, o contrato chama a função IERC20 transferFrom. O usuário primeiro deve aprovar a solicitação do contrato para transferir seus fundos. Depois disso, o usuário deve assinar a transação atual. A função atualiza os mapeamentos stakingBalance, startTime e isStaking. Por fim, ele emite o evento Stake para permitir que nosso frontend ouça facilmente o referido evento.

pragma solidity 0.8.4;

function unstake(uint256 amount) public {
        require(
            isStaking[msg.sender] = true &&
            stakingBalance[msg.sender] >= amount, 
            "Nada para retirar"
        );
        uint256 yieldTransfer = calculateYieldTotal(msg.sender);
        startTime[msg.sender] = block.timestamp; // bug fix
        uint256 balanceTransfer = amount;
        amount = 0;
        stakingBalance[msg.sender] -= balanceTransfer;
        daiToken.transfer(msg.sender, balanceTransfer);
        pmknBalance[msg.sender] += yieldTransfer;
        if(stakingBalance[msg.sender] == 0){
            isStaking[msg.sender] = false;
        }
        emit Unstake(msg.sender, amount);
    }
Enter fullscreen mode Exit fullscreen mode

A função **unstake() **exige que o mapeamento isStaking seja igual a verdadeiro (o que só acontece quando a função de stake é chamada) e exige que o valor solicitado para unstake não seja maior que o saldo em stake do usuário. Declarei uma variável local toTransfer igual à função calculateYieldTotal (falaremos mais sobre essa função depois) para facilitar meus testes (a latência me deu problemas na verificação de saldos). Depois disso, seguimos o padrão de verificações-efeitos-transações definindo balanceTransfer para igualar o valor e, em seguida, definindo o valor como 0. Isso evita que os usuários abusem da função com reentrância.

Além disso, a lógica atualiza o mapeamento stakingBalance e transfere a DAI de volta para o usuário. Em seguida, a lógica atualiza o mapeamento pmknBalance. Este mapeamento constitui o rendimento não realizado do usuário; portanto, se o usuário já tiver um saldo de rendimento não realizado, o novo saldo incluirá o saldo anterior com o saldo atual (novamente, mais informações sobre isso na seção calculateYieldTotal). Por fim, incluímos uma declaração condicional que verifica se o usuário ainda possui fundos em stake. Se o usuário não tiver, o mapeamento isStake apontará para falso.

*Também devo observar que a versão do Solidity >= 0.8.0 inclui o SafeMath já integrado. Se você estiver usando Solidity < 0.8.0, recomendo fortemente que você use uma biblioteca SafeMath para evitar estouros.

**A função unstake() original falhou ao redefinir o mapeamento startTime. O código abaixo reflete a correção do bug.

pragma solidity 0.8.4;

function withdrawYield() public {
        uint256 toTransfer = calculateYieldTotal(msg.sender);

        require(
            toTransfer > 0 ||
            pmknBalance[msg.sender] > 0,
            "Nada para retirar"
            );


        if(pmknBalance[msg.sender] != 0){
            uint256 oldBalance = pmknBalance[msg.sender];
            pmknBalance[msg.sender] = 0;
            toTransfer += oldBalance;
        }

        startTime[msg.sender] = block.timestamp;
        pmknToken.mint(msg.sender, toTransfer);
        emit YieldWithdraw(msg.sender, toTransfer);
    }
Enter fullscreen mode Exit fullscreen mode

A função withdrawYield() requer que a função calculateYieldTotal ou pmknBalance mantenha um saldo para o usuário. A instrução condicional if verifica o pmknBalance especificamente. Se esse mapeamento apontar para um equilíbrio, isso significa que o usuário fez stake de DAI mais de uma vez. A lógica do contrato adiciona o antigo pmknBalance ao total de rendimento em execução que recebemos do calculateYieldTotal. Observe que a lógica segue o padrão verifica-efetua-transações; onde, oldBalance pega o uint pmknBalance. Imediatamente depois disso, pmknBalance é atribuído zero (novamente, para evitar a reentrada). Depois, o startTime é atribuído ao timestamp atual para redefinir o rendimento acumulado. Finalmente, o contrato evoca a função pmknToken.mint que transfere PMKN diretamente para o usuário.

Funções Auxiliares

pragma solidity 0.8.4;

function calculateYieldTime(address user) public view returns(uint256){
        uint256 end = block.timestamp;
        uint256 totalTime = end - startTime[user];
        return totalTime;
    }
Enter fullscreen mode Exit fullscreen mode

A função calculateYieldTime() simplesmente subtrai o timestamp startTime do endereço do usuário especificado pelo timestamp atual. Essa função age mais como o ajudante de uma função auxiliar. A visibilidade para esta função deve ser interna; no entanto, optei por dar aos testes visibilidade pública.

pragma solidity 0.8.4;

function calculateYieldTotal(address user) public view returns(uint256) {
        uint256 time = calculateYieldTime(user) * 10**18;
        uint256 rate = 86400;
        uint256 timeRate = time / rate;
        uint256 rawYield = (stakingBalance[user] * timeRate) / 10**18;
        return rawYield;
    }
Enter fullscreen mode Exit fullscreen mode

A função calculateYieldTotal() permite que o processo de staking automatizado ocorra. Primeiro, a lógica pega o valor de retorno da função calculateYieldTime e o multiplica por 10¹⁸. Isso se mostra necessário, pois o Solidity não lida com ponto flutuante ou números fracionários. Ao transformar a diferença de timestamp retornada em um BigNumber, Solidity pode fornecer muito mais precisão. A variável de taxa equivale a 86.400, o que equivale ao número de segundos em um único dia. A ideia é: o usuário recebe 100% de seu DAI em stake a cada 24 horas.

*Em uma yield farm mais tradicional, a taxa é determinada pela porcentagem do pool do usuário em vez do tempo.

Além disso, a variável de tempo BigNumber é dividida pela taxa codificada (86400). A função pega o quociente e o multiplica pelo saldo de DAI em stake do usuário, que é então dividido por 10¹⁸. Quando o frontend busca o rendimento bruto, ele deve dividir por 10¹⁸ novamente para exibir o rendimento real.

Conclusão

Aqui está o contrato final:

pragma solidity 0.8.4;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./PmknToken.sol";

contract PmknFarm {

    mapping(address => uint256) public stakingBalance;
    mapping(address => bool) public isStaking;
    mapping(address => uint256) public startTime;
    mapping(address => uint256) public pmknBalance;

    string public name = "PmknFarm";

    IERC20 public daiToken;
    PmknToken public pmknToken;

    event Stake(address indexed from, uint256 amount);
    event Unstake(address indexed from, uint256 amount);
    event YieldWithdraw(address indexed to, uint256 amount);

    constructor(
        IERC20 _daiToken,
        PmknToken _pmknToken
        ) {
            daiToken = _daiToken;
            pmknToken = _pmknToken;
        }

    function stake(uint256 amount) public {
        require(
            amount > 0 &&
            daiToken.balanceOf(msg.sender) >= amount, 
            "Você não pode fazer stake de zero tokens");


        if(isStaking[msg.sender] == true){
            uint256 toTransfer = calculateYieldTotal(msg.sender);
            pmknBalance[msg.sender] += toTransfer;
        }

        daiToken.transferFrom(msg.sender, address(this), amount);
        stakingBalance[msg.sender] += amount;
        startTime[msg.sender] = block.timestamp;
        isStaking[msg.sender] = true;
        emit Stake(msg.sender, amount);
    }

    function unstake(uint256 amount) public {
        require(
            isStaking[msg.sender] = true &&
            stakingBalance[msg.sender] >= amount, 
            "Nada para unstake"
        );
        uint256 yieldTransfer = calculateYieldTotal(msg.sender);
        startTime[msg.sender] = block.timestamp;
        uint256 balTransfer = amount;
        amount = 0;
        stakingBalance[msg.sender] -= balTransfer;
        daiToken.transfer(msg.sender, balTransfer);
        pmknBalance[msg.sender] += yieldTransfer;
        if(stakingBalance[msg.sender] == 0){
            isStaking[msg.sender] = false;
        }
        emit Unstake(msg.sender, balTransfer);
    }

    function calculateYieldTime(address user) public view returns(uint256){
        uint256 end = block.timestamp;
        uint256 totalTime = end - startTime[user];
        return totalTime;
    }

    function calculateYieldTotal(address user) public view returns(uint256) {
        uint256 time = calculateYieldTime(user) * 10**18;
        uint256 rate = 86400;
        uint256 timeRate = time / rate;
        uint256 rawYield = (stakingBalance[user] * timeRate) / 10**18;
        return rawYield;
    } 

    function withdrawYield() public {
        uint256 toTransfer = calculateYieldTotal(msg.sender);

        require(
            toTransfer > 0 ||
            pmknBalance[msg.sender] > 0,
            "nada para resgatar"
            );


        if(pmknBalance[msg.sender] != 0){
            uint256 oldBalance = pmknBalance[msg.sender];
            pmknBalance[msg.sender] = 0;
            toTransfer += oldBalance;
        }

        startTime[msg.sender] = block.timestamp;
        pmknToken.mint(msg.sender, toTransfer);
        emit YieldWithdraw(msg.sender, toTransfer);
    } 
}
Enter fullscreen mode Exit fullscreen mode

Isso conclui a parte dos contratos do dApp de cultivo de rendimento. Se você tiver alguma dúvida, sinta-se à vontade para entrar em contato. Espero que isso ajude você em sua jornada em Solidity. Muito obrigado pela leitura!

Parte 2: Testando contratos inteligentes usando Hardhat e Chai

*Para ver o repositório completo com testes, scripts e frontend, aqui está: https://github.com/andrew-fleming/pmkn-farm

*Dicas são muito apreciadas!

Endereço ETH: 0xD300fAeD55AE89229f7d725e0D710551927b5B15

Junte-se ao Canal do Telegram do Coinmonks e aprenda sobre negociação e investimento em criptomoedas

Enter fullscreen mode Exit fullscreen mode




Além disso, leia

*DeFi Yield Farming e Liquidity Mining

Este artigo foi escrito por Andrew Flaming e traduzido por Diogo Jorge. O artigo original pode ser encontrado aqui.

Latest comments (0)