WEB3DEV

Cover image for Construindo sua primeira integração na Uniswap
Isabela Curado Nehme
Isabela Curado Nehme

Posted on

Construindo sua primeira integração na Uniswap

25 de Agosto de 2022

Desenvolvedores que são novos no ecossistema Uniswap geralmente ficam presos nos primeiros estágios de desenvolvimento de integrações on-chain com o Protocolo. Neste post, vamos ajudá-lo a superar esses primeiros obstáculos. Aqui está o que vamos realizar:

  1. Configurar um ambiente de desenvolvimento básico e reutilizável para construir e testar contratos inteligentes que interagem com o Protocolo Uniswap.
  2. Executar um nó de desenvolvimento local Ethereum bifurcado da Mainnet (Rede Principal) que inclua o Protocolo Uniswap completo.
  3. Escrever um contrato inteligente básico que execute uma troca na Uniswap V3
  4. Implantar e testar esse contrato

Configurando um ambiente de desenvolvimento

Uma das perguntas mais comuns que somos questionados é qual pilha de desenvolvimento utilizar para construir na Uniswap. Não existe uma resposta certa para isso, mas para este post vamos utilizar uma pilha comum: Hardhat como nosso conjunto de ferramentas principal, que executa nós e fornece uma estrutura de teste, e a Alchemy como nosso provedor RPC (que suporta a bifurcação da Rede Principal em seu nível gratuito).

Configurar uma conta na Alchemy

RPCs são serviços que permitem clientes off-chain se comunicarem com a blockchain, um requisito para construir qualquer tipo de dApp. Para esse exemplo, vamos usar a Alchemy, que suporta uma Mainnet Fork (bifurcação da Rede Principal, que é a chave para nossa configuração de desenvolvimento.

Vá até https://www.alchemy.com/ para criar uma conta gratuita. Clique em “Create an App” (Criar um aplicativo), siga os comandos e pegue sua chave API. Vamos precisar dela mais tarde.

Clonando o projeto de amostra

Para levantar e executar rapidamente, vamos começar clonando um repositório boilerplate (modelo básico) reutilizável que a Uniswap Labs forneceu navegando até um diretório do seu projeto e usando o git:

git clone https://github.com/Uniswap/uniswap-first-contract-example

O repositório é muito semelhante a como o Hardhat configuraria um ambiente padrão, com alguns arquivos separados para que possamos construir. Navegue até o projeto clonado e instale suas dependências:

cd uniswap-first-contract-example
npm install
Enter fullscreen mode Exit fullscreen mode

Agora você deve ter o ambiente de teste do Hardhat instalado junto com algumas outras dependências. O Hardhat vai executar uma bifurcação da Rede Principal para você, lidando com implantações nesse nó e executando testes nele. Nós não vamos nos aprofundar muito no Hardhat, mas se você não estiver familiarizado, recomendamos fortemente que leia mais aqui.

Bifurcando a Rede Principal

O Hardhat tem um recurso poderoso que executa um nó de teste localmente na Ethereum para você desenvolver. Isso economiza os custos de gas dos testes na produção e as dores de cabeça do desenvolvimento diante da testnet (rede de teste).

Entretanto, um problema com o desenvolvimento em um nó local é que ele começa do zero toda vez que é inicializado. Nenhum protocolo implantado (como a Uniswap) estará presente nele, o que dificulta a construção e o teste de integrações.

É aqui onde entra a bifurcação da Rede Principal. Quando usamos uma bifurcação da Rede Principal, o Hardhat realmente tira um instantâneo da Rede Principal da Ethereum e o utiliza como o iniciador para o nó local. Dessa forma, todos os protocolos implantados estão presentes e prontos para serem integrados.

A partir daqui, é hora de usar a chave API da Alchemy que geramos anteriormente. Execute o comando a seguir para iniciar o nó de teste local na Ethereum, bifurcado de um instantâneo da Rede Principal:

npx hardhat node --fork https://eth-mainnet.alchemyapi.io/v2/{YOUR_API_KEY}

Parabéns! Agora você está executando um nó de teste na Ethereum totalmente funcional em sua máquina local. E como iniciamos de uma bifurcação da Rede Principal, seu nó local contém o histórico completo da blockchain, incluindo o estado atual do Protocolo Uniswap. Agora estamos prontos para construir uma integração.

Escrevendo um contrato Swap (de troca) básico

Para este exemplo, começaremos criando uma integração básica com o Protocolo Uniswap V3. Nosso contrato receberá um valor de entrada de Wrapped Ether (Ether encapsulado), trocará pelo valor máximo de DAI e retornar esse DAI para a carteira do chamador.

Vamos começar abrindo o arquivo contracts/SimpleSwap.sol.

Nós fornecemos alguns boiler plate (modelos básicos) para configurar o contrato. Vamos percorrê-lo:

/* contracts/Swap.sol */

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity =0.7.6;
pragma abicoder v2;

import '@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol';
import '@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol';

contract SimpleSwap {
    constructor() {
    }
}
Enter fullscreen mode Exit fullscreen mode

As primeiras três linhas são padrão em programação na Solitidy, elas configuram as informações de licença e compilação. Não vamos nos aprofundar muito no que elas significam aqui, mas se você não está familiarizado com o que elas fazem, recomendamos a leitura dos Solidity language docs (documentos da linguagem Solidity).

Em seguida, importamos dois elementos do Protocolo Uniswap V3 (esses foram importados quando executamos o npm install anteriormente). O primeiro - ISwapRouter.sol - é a interface do contrato Uniswap SwapRouter, que usaremos para encaminhar nossa troca para os métodos de contrato apropriados no Protocolo. O segundo - TransferHelper.sol - é uma biblioteca utilitária para nos ajudar a fazer algumas operações necessárias nos tokens ERC20. Vamos utilizá-los mais tarde no nosso contrato.

Finalmente, definimos nosso contrato chamado SimpleSwap. Esse contrato não faz nada no momento, mas podemos adicionar alguns códigos para que ele execute uma troca simples.

Configuração do código de troca

Começaremos adicionando algumas constantes de endereços e referências. Em uma versão de produção deste contrato, elas devem ser dinâmicas (um ótimo projeto de acompanhamento) - mas, para manter as coisas simples, vamos codificá-las.

Adicione as seguintes linhas ao seu arquivo contracts/SimpleSwap.sol:

// ...
contract SimpleSwap {
    ISwapRouter public immutable swapRouter;
    address public constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
    address public constant WETH9 = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    uint24 public constant feeTier = 3000;

        constructor(ISwapRouter _swapRouter) {
        swapRouter = _swapRouter;
    }
}
Enter fullscreen mode Exit fullscreen mode

Primeiro, usamos a interface ISwapRouter para criar uma referência ao SwapRouter. O SwapRouter faz parte dos contratos Uniswap V3 Periphery, projetados para facilitar a execução de trocas. Na verdade, vamos mapear isso para uma versão implantada do SwapRouter no construtor:

ISwapRouter public immutable swapRouter;

Em segundo lugar, criamos variáveis constantes para os tokens ERC20 que iremos trocar. Você pode e deve colocar esses endereços no Ether Scan e confirmar que eles são os tokens que você está esperando:

address public constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
address public constant WETH9 = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
Enter fullscreen mode Exit fullscreen mode

Em seguida, criamos outra variável constante para indicar o nível da taxa do pool que queremos usar na troca. Novamente, em um contrato mais geral, essa seria uma entrada definida no tempo de execução, mas, para simplificar, estamos codificando para o pool o nível de taxa de 0,3%. Níveis de taxa são indicados como 1 centésimo (1/100) de um ponto base, portanto, nosso nível de taxa será 3000.

uint24 public constant feeTier = 3000;

Por fim, criamos o construtor, que é chamado quando nosso contrato está implantado. Estamos exigindo que um integrador passe o endereço do SwapRouter que deseja usar. Tudo o que o construtor faz a seguir é configurar o swapRouter para usar aquele fornecido pelo integrador:

constructor(ISwapRouter _swapRouter) {
    swapRouter = _swapRouter;
}
Enter fullscreen mode Exit fullscreen mode

E é isso! Agora que temos todo o código de configuração escrito, podemos prosseguir para a real execução da nossa troca.

Implementação do código de troca

Novamente, esse contrato de amostra só faz uma coisa: troca WETH por DAI. Vamos começar criando a função assinatura para nossa troca.

contract SimpleSwap {
    //...
    function swapWETHForDAI(uint amountIn) external returns (uint256 amountOut) {
        // Vamos preencher isso em seguida
        }
}
Enter fullscreen mode Exit fullscreen mode

Nossa função swapWETHForDAI está marcada external para que clientes e outros contratos possam chamá-la. A função recebe um parâmetro, a quantidade de WETH que queremos trocar definida em Wei (que é 10^(-18) WETH). O OpenZeppelin tem uma boa explicação para o por quê definimos em Wei em oposição ao WETH (TLDR; nos permite fazer uma aritmética precisa em Solidity, que não suporta números de ponto flutuante). Para os propósitos desse projeto, apenas saiba que para trocar 1 WETH você deve passar a esta função um amountIn de 1*10^18.

Realizar uma troca de WETH para DAI vai exigir duas etapas de pré-requisito. Primeiro, nosso contrato terá que mover a quantidade solicitada de WETH da carteira do chamador para si e, em seguida, precisará aprovar o swapRouter para gastar esse WETH na troca por DAI. Parece complicado, mas você vai ver que os contratos Uniswap V3 Periphery nos fornecem ferramentas para tornar fácil. Comece adicionando as seguintes linhas à sua função swapWETHForDAI:

function swapWETHForDAI(uint amountIn) external returns (uint256 amountOut) {
    // Transfere a quantidade especificada de WETH9 para este contrato.
    TransferHelper.safeTransferFrom(WETH9, msg.sender, address(this), amountIn);

        // Aprova ao roteador gastar o WETH9.
    TransferHelper.safeApprove(WETH9, address(swapRouter), amountIn);
}
Enter fullscreen mode Exit fullscreen mode

A primeira etapa chama a função auxiliar de periferia safeTransferFrom, que transfere a quantidade desejada de WETH da carteira do chamador para o contrato. Mantenha em mente que, como estamos transferindo em nome do usuário, aquele usuário terá que assinar uma aprovação antes de chamar esse método. Este é um conceito crítico de se entender e veremos como fazer isso na próxima seção quando criarmos um cliente de teste.

A próxima linha chama o safeApprove para permitir swapRouter que gaste a quantidade especificada de WETH. Isso dará permissão ao SwapRouter para realmente executar a troca para nós. Com isso, finalmente estamos prontos para trocar!

Lembra de como dissemos que o SwapRouter nos facilita executar uma troca? Aqui é onde vamos utilizá-lo. Chamaremos o método exactInputSingle, que executará uma troca de uma “quantidade exata” de um token de entrada pela quantidade máxima de um token de saída.

Adicione o código a seguir à função swapWETHForDAI para executar a troca. Pode parecer complicado, mas vamos passar por isso.

function swapWETHForDAI(uint amountIn) external returns (uint256 amountOut) {
    // ...
    ISwapRouter.ExactInputSingleParams memory params =
      ISwapRouter.ExactInputSingleParams({
          tokenIn: WETH9,
          tokenOut: DAI,
          fee: feeTier,
          recipient: msg.sender,
          deadline: block.timestamp,
          amountIn: amountIn,
          amountOutMinimum: 0,
          sqrtPriceLimitX96: 0
      });
  // A chamada para o `exactInputSingle` executa a troca.
  amountOut = swapRouter.exactInputSingle(params);
  return amountOut;
}
Enter fullscreen mode Exit fullscreen mode

Começaremos configurando os parâmetros exigidos para executar o método exactInputSingle. Aqueles que são novos na Solidity perceberão a palavra-chave memory ao lado da declaração da variável. Isso apenas informa ao compilador para armazenar essa variável localmente durante a execução em vez de escrevê-la na blockchain, o que seria caro (saiba mais aqui).

Mais uma vez, a interface SwapRouter torna nossas vidas mais fáceis aqui, fornecendo a forma exata do parâmetro que a função de troca precisa com ExactInputSingleParams. Todos, menos os dois últimos elementos desse objeto, devem ser bem autoexplicativos, estamos apenas mapeando variáveis que já definimos para o objeto de parâmetro.

Para simplificar neste exemplo, definiremos os dois últimos elementos amountOutMinimum e sqrtPriceLimitX96 como zero. Eles estão fora do escopo deste exemplo básico, mas basicamente permitem que você defina uma quantidade mínima de saída, nesse caso, DAI, que você receberá por uma troca. Em produção, esse é um jeito de limitar o o “escorregão” (slippage) de preço de uma troca.

Por fim, nas duas últimas linhas, vamos executar o método exactInputSingle do SwapRouter, com os parâmetros que configuramos que realmente executam o negócio e então retornam a quantidade de DAI que aquele negócio rendeu.

Contrato completo

É isso! Agora você tem um contrato de trabalho que trocará uma quantidade inserida de WETH pela quantidade máxima de DAI considerando os preços atuais de mercado. Antes de passar para o teste, faça uma dupla verificação se o seu contrato corresponde ao exemplo mostrado aqui.

Testando nosso contrato

Agora que temos nosso contrato SimpleSwap escrito, é hora de usá-lo. Usaremos a estrutura de teste Chai que acompanha o Hardhat para:

  1. Implantar nosso contrato SimpleSwap para uma bifurcação da Rede Principal da Ethereum
  2. Verificar o saldo de DAI da nossa carteira de teste
  3. Chamar o swapWETHForDAI para trocar alguns WETH para DAI para teste
  4. Confirmar que o saldo de DAI do usuário de teste realmente aumentou

Escrever contratos para teste poderia (e provavelmente será) o conteúdo de um post inteiro. Para manter as coisas simples, vamos passar por cima de muitos detalhes aqui. Depois de concluir esse exemplo, é altamente recomendável fazer um pouco de aprendizado externo sobre a escrita de testes para contratos (os documentos de teste do Hardhat são um ótimo lugar para começar).

Arquivo de teste de contrato

Para começar, vá para tests/SimpleSwap.test.js, onde temos o código para testar nosso contrato apagado.

O arquivo começa importando a estrutura de teste Chai e Hardhat, que usamos para nosso ambiente de desenvolvimento e define algumas constantes familiares. O trecho ERC-20 ABI nos permite chamar funções como Approve em tokens ERC-20 (leia mais sobre ABIs aqui).

Com o código de configuração resolvido, vamos pular para o teste real, que começa na linha 20. Adicione o código a seguir para implantar nosso contrato SimpleSwap:

/* Implantar o contrato SimpleSwap */
const simpleSwapFactory = await ethers.getContractFactory('SimpleSwap')
const simpleSwap = await simpleSwapFactory.deploy(SwapRouterAddress)
await simpleSwap.deployed()
Enter fullscreen mode Exit fullscreen mode

O Hardhat está fazendo muito trabalho de perna para nós aqui. É implantar o contrato para nosso ambiente local e salvar uma versão resgatável dele - simpleSwap - na qual poderemos executar métodos.

O nó local do Hardhat nos fornece uma conta que está pré carregada com vários ETH de teste. Como estamos trocando WETH por DAI, temos que pegar um pouco desse ETH e encapsulá-lo. Adicione o seguinte código para primeiro obter as chaves da conta, armazenadas na variável signers e depois deposite parte do seu ETH no contrato WETH para embrulhá-lo:

/* Conecta ao WETH e embrulha parte do eth  */
let signers = await hre.ethers.getSigners()
const WETH = new hre.ethers.Contract(WETH_ADDRESS, ercAbi, signers[0])
const deposit = await WETH.deposit({ value: hre.ethers.utils.parseEther('10') })
await deposit.wait()
Enter fullscreen mode Exit fullscreen mode

Se tudo correr bem, essas etapas nos darão 10 WETH prontos para serem trocados por DAI. Para começar o teste, pegaremos o saldo atual de DAI da nossa conta falsa e salvaremos na variável local DAIBalanceBefore. Você notará que usamos o utilitário formatUnits em Ethers para converter a quantia em DAI para um número de ponto flutuante legível:

/* Verifica o saldo inicial em DAI */
const DAI = new hre.ethers.Contract(DAI_ADDRESS, ercAbi, signers[0])
const expandedDAIBalanceBefore = await DAI.balanceOf(signers[0].address)
const DAIBalanceBefore = Number(hre.ethers.utils.formatUnits(expandedDAIBalanceBefore, DAI_DECIMALS))
Enter fullscreen mode Exit fullscreen mode

Em seguida está um passo crítico, muitas vezes esquecido. Nosso contrato SimpleSwap vai mover WETH para fora de nossa carteira e para dentro do contrato de troca. Ele não pode fazer isso sem nossa aprovação, que damos chamando o método approve no próprio contrato WETH ERC-20. Neste exemplo, vamos aprová-lo para mover 1 WETH em nosso nome:

/* Aprova o contrato trocador para gastar WETH para mim */
await WETH.approve(simpleSwap.address, hre.ethers.utils.parseEther('1'))
Enter fullscreen mode Exit fullscreen mode

E, finalmente, é o momento que todos esperávamos, hora de realmente executar uma troca de WETH para DAI usando nosso novíssimo contrato SimpleSwap! O Hardhat torna isso super fácil, podemos apenas chamar o método swapWETHForDAI no nosso objeto de contrato, solicitando uma troca de 0,1 WETH por DAI:

/* Executa a troca */
const amountIn = hre.ethers.utils.parseEther('0.1')
const swap = await simpleSwap.swapWETHForDAI(amountIn, { gasLimit: 300000 })
swap.wait()
Enter fullscreen mode Exit fullscreen mode

Uma vez que a transação for concluída, faremos outra verificação do saldo em DAI da nossa conta:

/* Verifica o saldo final em DAI */
const expandedDAIBalanceAfter = await DAI.balanceOf(signers[0].address)
const DAIBalanceAfter = Number(hre.ethers.utils.formatUnits(expandedDAIBalanceAfter, DAI_DECIMALS))
Enter fullscreen mode Exit fullscreen mode

Por fim, compare ao saldo em DAI que verificamos antes da troca. Se a troca tiver funcionado, agora devemos ter mais DAI do que quando começamos:

/* Teste que agora temos mais DAI do que quando começamos */
expect(DAIBalanceAfter).is.greaterThan(DAIBalanceBefore)
Enter fullscreen mode Exit fullscreen mode

Isso é tudo que você precisa para testar seu novo contrato inteligente. Verifique duplamente o que você tem em relação ao exemplo completo aqui e, quando estiver pronto, poderá usar o Hardhat para executar o teste e ver se funcionou. Para executar o teste, abra uma nova linha de comando na raiz do repositório e execute o comando a seguir:

npx hardhat test

O teste falhou! O que poderia ter dado errado?

Error: call revert exception [ See: https://links.ethers.org/v5-errors-CALL_EXCEPTION ] (method="balanceOf(address)", data="0x", errorArgs=null, errorName=null, errorSignature=null, reason=null, code=CALL_EXCEPTION, version=abi/5.6.4)
  at Logger.makeError (node_modules/@ethersproject/logger/src.ts/index.ts:261:28)
  at Logger.throwError (node_modules/@ethersproject/logger/src.ts/index.ts:273:20)
  at Interface.decodeFunctionResult (node_modules/@ethersproject/abi/src.ts/interface.ts:427:23)
  at Contract.<anonymous> (node_modules/@ethersproject/contracts/src.ts/index.ts:400:44)
  at step (node_modules/@ethersproject/contracts/lib/index.js:48:23)
  at Object.next (node_modules/@ethersproject/contracts/lib/index.js:29:53)
  at fulfilled (node_modules/@ethersproject/contracts/lib/index.js:20:58)
Enter fullscreen mode Exit fullscreen mode

Usando nossa bifurcação da Rede Principal

Quando você executa um teste no Hardhat sem parâmetros, o Hardhat cria um novo nó de teste da Ethereum para o seu teste ser executado. O problema? Nosso contrato faz uma chamada para os contratos de Protocolo Uniswap implantados, que não estão presentes numa nova configuração.

Lembra como começamos um nó de “bifurcação da Rede Principal” na primeira seção? É aqui que isso se torna útil. Por ser uma bifurcação da Rede Principal, ele possui uma versão implantada dos contratos Uniswap prontos para uso. Certifique-se de que o nó que você iniciou ainda está em execução (se não estiver, apenas repita as etapas da seção “Bifurcando a Rede Principal”) e executaremos novamente o teste apontando o Hardhat explicitamente para o nosso nó local:

npx hardhat test --network localhost

Agora você deve ver uma mensagem como a seguinte indicando que o seu contrato está funcionando. Parabéns!

SimpleSwap
    ✔ Deve fornecer a um chamador mais DAI do quando começou após uma troca (1999ms)

  1 passing (2s)
Enter fullscreen mode Exit fullscreen mode

O que vem depois?

Você pode continuar criando com esse ambiente ou clonar o repositório novamente para começar do zero. Para continuar aprendendo, tente adicionar algumas funções mais avançadas:

  • Crie um front-end simples: o teste fornecido demonstra como usar Javascript para interagir com seu contrato. Você consegue mover aquela lógica para um site Simple Swap (de troca simples)?
  • Adicione uma função de saída exata de troca ao SimpleSwap: no momento, nosso contrato aceita uma quantidade de WETH e troca pelo valor máximo de DAI. Esse novo método deve receber uma quantidade de DAI e trocar o valor correto de WETH para obter isso.
  • Escreva um contrato GeneralSwap: nosso contrato foi codificado para trocar apenas WETH por DAI. Você consegue escrever um contrato que pode trocar entre quaisquer pares de ERC-20?
  • Escreva um contrato Quote: crie um novo contrato que obtenha os preços atuais para trocas sem realmente realizar uma troca.
  • Implante seu contrato numa rede de teste: no momento, seu contrato está implantado apenas em seu nó de teste, você consegue implantá-lo em uma rede de teste como a Goerli?
  • Seja criativo: agora você tem acesso à maior fonte de liquidez na Ethereum. Crie seu próprio caso de uso e construa-o.

Um bom lugar para começar tudo isso é o site de documentos Uniswap. Se você ficar preso e precisar de ajuda, poste no canal #dev-chat do Discord Uniswap.

Até a próxima, boa construção!

Para se envolver e manter-se atualizado:

  • Junte-se à comunidade Uniswap no discord
  • Siga a Uniswap Labs no Twitter
  • Inscreva-se no blog da Uniswap Labs
  • Registre-se como delegado no Sybil
  • Participe da governance da Uniswap

Equipe Uniswap 🦄

Esse artigo foi escrito pela Equipe Uniswap e traduzido por Isabela Curado Nehme. Seu original pode ser lido aqui.

Latest comments (0)