A Uniswap foi a primeira e ainda é a maior dex (exchange descentralizada) bem sucedida do mercado cripto. Na data que escrevo este tutorial, ela representa 70% do market share dex na rede Ethereum e 58% do market share em todas as redes, sendo mais 3x maior que a segunda colocada, PancakeSwap. Por este motivo, é muito comum que outros protocolos DeFi se integram à Uniswap para conseguir fazer swaps programaticamente.
E é exatamente isso que veremos neste tutorial: como integrar o seu smart contract com a Uniswap para conseguir fazer swaps via código.
Usaremos o Remix como ferramenta, mas você pode tranquilamente usar toolkits profissionais como Truffle e HardHat. Além disso, é esperado que você já possua conhecimentos básicos de Solidity para fazer este tutorial, em especial sobre tokens ERC-20, o que pode ser aprendido neste outro tutorial aqui. Também é interessante que você já tenha tido a experiência de usar a Uniswap v3 manualmente, pelo site, para entender o que vai se passar aqui.
Vamos lá!
#1 – Arquitetura da Uniswap
Antes de meter a mão no código você precisa primeiro entender como a arquitetura da Uniswap funciona em linhas gerais, para conseguir justamente entender com o que de fato você vai se integrar, já que se você entrar no GitHub do projeto vai encontrar vários smart contracts diferentes por lá.
A imagem abaixo é da arquitetura da V2, enquanto nós usaremos a V3 neste tutorial, mas os elementos centrais já aparecem aí e será o suficiente para o que quero explicar a seguir.
No uso geral do dia a dia, temos dois atores centrais: os liquidity providers e os traders.
Os liquidity provider criam liquidity pools usando um contrato de Factory (fábrica), que nada mais são do que reservas de um par de moedas, que na imagem acima são chamados de Pair (par), mas que na v3 o nome é Pool mesmo. Assim, tudo nasce de um monte de smart contracts, cada um com uma reserva de um par de moedas fornecido por um investidor.
Do outro lado, os traders, usam os Routers (roteadores) que são contratos auxiliares para facilitar o uso dos pools, já que se integrar com eles diretamente, embora tecnicamente possível, é humanamente inviável já que são muitos, são criados o tempo inteiro e cada um tem o seu endereço na blockchain. Fora que muitos swaps (como os trades são chamados em uma dex) exigem o uso de mais de um pool, o que exige percorrer uma rota de negociação que passa às vezes por dois, três ou até mais contratos de pools.
Para facilitar esse roteamento dos swaps temos o contrato de Router que é justamente com o qual todas as aplicações e protocolos se comunicam, incluindo o que vamos criar. Com o router, basta sabermos o endereço dos tokens ERC-20 que vamos negociar e ele se encarrega do trabalho pesado de encontrar os pools e interagir com eles.
Apenas para finalizar a explicação do diagrama, em Library temos um monte de interfaces, structs e functions auxiliares e de uso comum dos demais contratos do repositório.
#2 – Setup da Carteira
O primeiro passo que será necessário para nossa integração é você ter uma carteira MetaMask com saldo de teste. Eu ensino isso em detalhes no vídeo abaixo, caso ainda não possua. Usarei a rede de testes Polygon Mumbai aqui, mas você pode usar a que se sentir mais confortável.
Para saldo de teste na Polygon Mumbai, recomendo os faucets da Polygon e da Alchemy.
Agora você vai precisar de WMATIC, que é o Wrapped Token do Matic, uma versão ERC-20 do mesmo que é usada na Mumbai para fazer os swaps. Você pode trocar MATIC por WMATIC no próprio site da Uniswap, estando autenticado com sua MetaMask apontada para Mumbai. Essa etapa é importante para que você possa obter o WMATIC, mas também para que tenha os detalhes da transação no explorador de blocos e com eles o endereço do contrato do WMATIC na Mumbai. Recomendo repetir o mesmo processo para obter WETH também e seu endereço, outra moeda muito comum de ser utilizada (Wrapped Ether).
Agora com saldo, vamos pro código.
#3 – Criando o smart contract
Crie um novo arquivo no Remix com o nome de UniswapIntegration.sol, com a seguinte estrutura inicial, onde incluí alguns imports importantes.
1
2
3
4
5
6
7
8
9
|
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import “@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol”;
import “@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol”;
contract UniswapIntegration {
}
|
Depois da licença e da versão de Solidity, eu importei dois arquivos Solidity do próprio repositório da Uniswap v3, mais especificamente o ISwapRouter, que é a interface do Router e o TransferHelper, que é um contrato utilitário para manipular tokens ERC-20 nos swaps. Por fim, apenas declarei o contrato pelo nome.
Agora vamos declarar as variáveis de estado e o constructor.
1
2
3
4
5
6
7
8
9
10
11
|
ISwapRouter public uniswap;
address public constant WMATIC = 0x9c3C9283D3e44854697Cd22D3Faa240Cfb032889;
address public constant WETH = 0xA6FA4fB5f76172d178d61B04b0ecd319C5d1C0aa;
// For this example, we will set the pool fee to 0.3%.
uint24 public constant poolFee = 3000;
constructor(){
uniswap = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564);
}
|
Comecei declarando uma variável uniswap do tipo de ISwapRouter, que mais à frente no constructor nós vamos inicializar apontando para o endereço do Router na Polygon Mumbai. Esse endereço eu peguei na documentação oficial da Uniswap, onde são listados os endereços oficiais em várias redes. Jamais use endereços aleatórios a Internet para não ser roubado.
Depois, declarei as constantes com os endereços dos contratos de WMATIC e WETH, que são as moedas que farei swap neste exemplo. Esses endereços eu obtive analisando as transações realizadas pelo próprio dapp da Uniswap quando conectado na Polygon Mumbai.
E por fim, a taxa do pool, que setei em 3000 e que representa 0.3% ( está na escala x10000, ou seja, com 4 casas decimais). Essa é a taxa média, mas dependendo do pool a taxa pode ir de 0.1% a 1%.
Agora vem a função mais importante, de swap.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
function swap(address tokenIn, address tokenOut, uint amountIn) public returns(uint amountOut) {
TransferHelper.safeTransferFrom(tokenIn, msg.sender, address(this), amountIn);
TransferHelper.safeApprove(tokenIn, address(uniswap), amountIn);
ISwapRouter.ExactInputSingleParams memory params =
ISwapRouter.ExactInputSingleParams({
tokenIn: tokenIn,
tokenOut: tokenOut,
fee: poolFee,
recipient: msg.sender,
deadline: block.timestamp,
amountIn: amountIn,
amountOutMinimum: 0,
sqrtPriceLimitX96: 0
});
amountOut = uniswap.exactInputSingle(params);
}
|
Vou começar explicando pelos parâmetros:
- tokenIn: endereço do token ERC-20 que você vai dar no swap;
- tokenOut: endereço do token ERC-20 que você quer receber no swap;
- amountIn: quantidade de tokens que você vai dar no swap (tokenIn);
- amountOut: variável de retorno com a quantidade recebida no swap (tokenOut);
É importante frisar que ANTES de poder chamar este swap o usuário deverá aprovar transferências da sua carteira para o endereço do seu smart contract, caso contrário irá dar erro logo na primeira instrução de safeTransferFrom (erro STF), onde o contrato captura os fundos da carteira do usuário para sua própria carteira e depois aprova a Uniswap fazer o mesmo a partir dele. Mas falarei melhor disso mais adiante.
Após essa etapa inicial de protocolo ERC-20, a seguir nós configuramos os parâmetros do swap usando uma struct chamada ExactInputSingleParams da Uniswap, cujas propriedades são:
- tokenIn: endereço do token ERC-20 que você vai dar no swap;
- tokenOut: endereço do token ERC-20 que você quer receber no swap;
- fee: a taxa a ser paga;
- recipient: quem vai receber os tokens;
- deadline: prazo para conclusão do swap, geralmente colocamos o timestamp do bloco atual;
- amountIn: quantidade de tokens que você vai dar no swap (tokenIn);
- amountOutMinimum: o mínimo de tokenOut que você aceita receber (zero quer dizer qualquer quantia);
- sqrtPriceLimitX96: um parâmetro mais avançado para limitar o preço a ser pago pela moeda (zero quer dizer qualquer preço);
Por fim, chamamos a função exactInputSingle do router usando os parâmetros acima e guardamos a saída na variável de retorno.
Como no meu teste eu vou negociar apenas WETH e WMATIC, seguem mais duas funções auxiliares que usam as constantes de endereço que passei antes.
1
2
3
4
5
6
7
|
function swapMaticToEth(uint amountIn) external returns(uint amountOut) {
amountOut = swap(WMATIC, WETH);
}
function swapEthToMatic(uint amountIn) external returns(uint amountOut) {
amountOut = swap(WETH, WMATIC);
}
|
#4 – Testando o smart contract
Agora que temos o contrato finalizado, certifique-se de que a sua versão está compilando e faça o deploy dela usando a opção Injected Provider MetaMask no Remix. Isso lhe dará um endereço de contrato na blockchain.
Com esse endereço do UniswapIntegration, vá no contrato de WETH pelo explorador de blocos e na aba contract, se autentique com sua MetaMask e rode um approve fornecendo permissão para negociar a quantia que quiser. Depois faça o mesmo no contrato de WMATIC.
Agora volte ao Remix e usa as funções de swapMaticToEth ou swapEthToMatic para ver o swap acontecendo através da integração que codificamos.
E é isso, agora você sabe como criar contratos Solidity que se integram com a Uniswap.
Até a próxima!
*O conteúdo deste artigo é de responsabilidade do(a) autor(a) e não reflete necessariamente a opinião do iMasters.