WEB3DEV

Cover image for Uniswap v3. Usando em seus contratos
Panegali
Panegali

Posted on

Uniswap v3. Usando em seus contratos

O que há de novo na Uniswap v3 e como integrá-la

Se você ainda não está familiarizado com a Uniswap, é um protocolo totalmente descentralizado para fornecimento automatizado de liquidez na Ethereum. Uma descrição mais fácil de entender seria que é uma exchange (corretora) descentralizada (DEX) que depende de provedores externos de liquidez que podem adicionar tokens em pools de contratos inteligentes e os usuários podem negociá-los diretamente.

Como está rodando na Ethereum, o que podemos negociar são os tokens ERC-20 Ethereum. Para cada token, há seu próprio contrato inteligente e pool de liquidez. A Uniswap — sendo totalmente descentralizada — não tem restrições às quais os tokens podem ser adicionados. Se ainda não existirem contratos para um par de tokens, qualquer um pode criar um usando sua fábrica e qualquer um pode fornecer liquidez a um pool. Uma taxa de 0,3% para cada negociação é dada a esses provedores de liquidez como incentivo.

O preço de um token é determinado pela liquidez em um pool. Por exemplo, se um usuário está comprando TOKEN1 com TOKEN2, a oferta de TOKEN1 no pool diminuirá enquanto a oferta de TOKEN2 aumentará e o preço de TOKEN1 aumentará. Da mesma forma, se um usuário estiver vendendo TOKEN1, o preço do TOKEN1 diminuirá. Portanto, o preço do token sempre reflete a oferta e a demanda.

E, claro, um usuário não precisa ser uma pessoa, pode ser um contrato inteligente. Isso nos permite adicionar a Uniswap aos nossos próprios contratos para adicionar opções de pagamento adicionais para os usuários de nossos contratos. A Uniswap torna esse processo muito conveniente, veja abaixo como integrá-lo.

1

O que há de novo na UniSwap v3?

Você pode ler mais sobre a Uniswap v2 aqui, mas agora vejamos o que há de novo na Uniswap v3:

  • Uma nova funcionalidade para provedores de liquidez que permite definir uma faixa de preço válida. Sempre que o pool estiver fora do intervalo, sua liquidez é ignorada. Isso não apenas reduz o risco de perda impermanente para os provedores de liquidez, mas também é muito mais eficiente em termos de capital, pois...
  • Diferentes níveis de taxas, que são determinadas pelo nível de risco do pool, existem três níveis diferentes:
  1. Pares estáveis: 0,05%. Estas taxas devem ser para pares com baixo risco de flutuações como USDT/DAI. Como ambas são moedas estáveis, a perda potencial impermanente delas é muito baixa. Isso é particularmente interessante para os comerciantes, pois permitirá trocas muito baratas entre moedas estáveis.
  2. Pares de Risco Médio: 0,30%. O risco médio é considerado qualquer par não relacionado que tenha um alto volume/popularidade de negociação, os pares populares tendem a ter um risco ligeiramente menor de volatilidade.
  3. Pares de Risco Alto: 1,00%. Quaisquer outros pares exóticos serão considerados de risco alto para provedores de liquidez e incorrerão na taxa de negociação mais alta de 1%.
  • Mecanismo oráculo Uniswap v2 TWAP aprimorado, onde uma única chamada on-chain pode recuperar o preço TWAP com os últimos 9 dias. Para conseguir isso, em vez de armazenar apenas uma soma de preços cumulativos, todos os relevantes são armazenados em uma matriz de tamanho fixo. Isso sem dúvida aumenta um pouco os custos do gás, mas no geral vale a pena pelo grande aprimoramento do oráculo.

Recursos adicionais da Uniswap v3

O que acontece com a UniSwap v2?
“A Uniswap é um conjunto automatizado e descentralizado de contratos inteligentes. Ela continuará funcionando enquanto a Ethereum existir.” —Hayden Adams.

Integrando a UniSwap v3

Uma das razões pelas quais a Uniswap é tão popular pode ser a maneira simples de integrá-la ao seu próprio contrato inteligente. Digamos que você tenha um sistema onde os usuários pagam com DAI. Com a Uniswap em apenas algumas linhas de código, você pode adicionar a opção de eles também pagarem em ETH. O ETH pode ser convertido automaticamente em DAI antes da lógica atual. Seria algo parecido com isto

pragma solidity ^0.8.0;

contract MyContract {
      // ...
      function pay(uint paymentAmountInDai) public payable {
            if (msg.value > 0) {
                convertEthToExactDai(paymentAmountInDai);
            } else {
                require(daiToken.transferFrom(msg.sender, address(this), paymentAmountInDai);
            }
            // fazer alguma coisa com aquele DAI
            // ...
      }
}
Enter fullscreen mode Exit fullscreen mode

Uma simples verificação no início de sua função será suficiente. Agora, quanto à função convertEthToExactDai, ela se parecerá com algo assim:

pragma solidity ^0.8.0;

contract MyContract {
  // ...
  function convertEthToExactDai(uint256 daiAmount) external payable {
      require(daiAmount > 0, "Must pass non 0 DAI amount");
      require(msg.value > 0, "Must pass non 0 ETH amount");

      uint256 deadline = block.timestamp + 15; // usando "agora" por conveniência, para o prazo de passagem da rede principal a partir do frontend!
      address tokenIn = WETH9;
      address tokenOut = multiDaiKovan;
      uint24 fee = 3000;
      address recipient = msg.sender;
      uint256 amountOut = daiAmount;
      uint256 amountInMaximum = msg.value;
      uint160 sqrtPriceLimitX96 = 0;

      ISwapRouter.ExactOutputSingleParams memory params = ISwapRouter.ExactOutputSingleParams(
          tokenIn,
          tokenOut,
          fee,
          recipient,
          deadline,
          amountOut,
          amountInMaximum,
          sqrtPriceLimitX96
      );

      uniswapRouter.exactOutputSingle{ value: msg.value }(params);
      uniswapRouter.refundETH();

      // reembolsar os restos de ETH ao usuário
      (bool success,) = msg.sender.call{ value: address(this).balance }("");
      require(success, "refund failed");
  }
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Há várias coisas para desempacotar aqui.

  • Swap Router: O SwapRouter será um contrato wrapper (embrulhador) fornecido pela Uniswap que possui vários mecanismos de segurança e funções de conveniência. Você pode instanciá-lo usando ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564 para qualquer rede principal ou de teste. O código da interface pode ser encontrado aqui.
  • WETH: Você pode notar que estamos usando ETH aqui. Na Uniswap não há mais pares ETH diretos, todos os ETH devem ser convertidos para WETH (que é ETH embrulhado como ERC-20) primeiro. No nosso caso, isso é feito pelo roteador.
  • exactOutputSingle: Esta função pode ser usada para usar o ETH e receber a quantidade exata de tokens para ele. Qualquer ETH restante será reembolsado, mas não automaticamente! Eu não percebi isso primeiro e o ETH acabou no contrato de troca do roteador. Então não se esqueça de chamar uniswapRouter.refundETH() depois de uma troca! E certifique-se de ter uma função de fallback em seu contrato para receber ETH: receive() payable external {}. O parâmetro deadline garantirá que os mineradores não possam reter uma troca e usá-la posteriormente, em um momento mais lucrativo. Certifique-se de passar este carimbo de data/hora (timestamp) UNIX do seu frontend, não use now dentro do contrato.
  • Refund: Assim que a negociação for concluída, podemos devolver qualquer ETH restante ao usuário. Isso envia todo o ETH do contrato, portanto, se o seu contrato puder ter um saldo de ETH por outros motivos, certifique-se de alterá-lo.
  • Fee: é um par não estável, mas popular, então a taxa que estamos usando aqui é de 0,3% (veja a seção de taxas acima).
  • sqrtPriceLimitX96: Pode ser usado para determinar limites nos preços do pool que não podem ser excedidos pela troca. Se você defini-lo como 0, ele será ignorado.

Como utilizá-lo no front-end

Um problema que temos agora é quando um usuário chama a função de pagamento e quer pagar em ETH, não sabemos quanto ETH ele precisa. Podemos usar a função quoteExactOutputSingle para calcular exatamente isso.

pragma solidity ^0.8.0;

contract MyContract {
  // ...
  function getEstimatedETHforDAI(uint daiAmount) external payable returns (uint256) {
    address tokenIn = WETH9;
    address tokenOut = multiDaiKovan;
    uint24 fee = 500;
    uint160 sqrtPriceLimitX96 = 0;

    return quoter.quoteExactOutputSingle(
        tokenIn,
        tokenOut,
        fee,
        daiAmount,
        sqrtPriceLimitX96
    );
 }
 // ...
}
Enter fullscreen mode Exit fullscreen mode

Mas observe que não declaramos isso como uma função de exibição, mas não chame essa função on-chain. Ela ainda deve ser chamada como uma função de exibição, mas como está usando funções que não são de exibição para calcular o resultado, não é possível declará-la como uma função de exibição (solicitação de recurso Solidity?). Use, por exemplo, a funcionalidade call() do Web3 para ler o resultado no frontend.

Agora podemos chamar getEstimatedETHforDAI no nosso frontend. Para garantir que estamos enviando ETH suficiente e que a transação não será revertida, podemos aumentar um pouco a quantidade estimada de ETH:

const requiredEth = (await myContract.getEstimatedETHforDAI(daiAmount).call())[0];
const sendEth = requiredEth * 1.1;
Enter fullscreen mode Exit fullscreen mode

E se não houver pool direto para uma troca disponível?

Nesse caso, você pode usar as funções exactInput e exactOutput que recebem um path (caminho) como parâmetro. Este caminho são dados codificados em bytes (codificados para eficiência de gás) dos endereços de token.

Qualquer troca precisa ter um caminho inicial e final. Embora na Uniswap você possa ter token direto para pares de token, nem sempre é garantido que tal pool realmente exista. Mas você ainda pode trocá-los, desde que encontre um caminho, por exemplo, Token1Token2WETHToken3. Nesse caso, você ainda pode trocar Token1 por Token3, custará apenas um pouco mais de gás do que uma troca direta.

Abaixo, você pode ver o código de exemplo da Uniswap para saber como calcular esse caminho no frontend.

function encodePath(tokenAddresses, fees) {
  const FEE_SIZE = 3

  if (path.length != fees.length + 1) {
    throw new Error('path/fee lengths do not match')
  }

  let encoded = '0x'
  for (let i = 0; i < fees.length; i++) {
    // Codificação do endereço em 20 bytes
    encoded += path[i].slice(2)
    // Codificação de 3 bytes da taxa
    encoded += fees[i].toString(16).padStart(2 * FEE_SIZE, '0')
  }
  // codificar o token final
  encoded += path[path.length - 1].slice(2)

  return encoded.toLowerCase()
}
Enter fullscreen mode Exit fullscreen mode

Exemplo de trabalho completo para Remix

Aqui está um exemplo de trabalho completo que você pode usar diretamente no Remix. Ele permite que você troque ETH por Kovan DAI Multi-collaterizado. Ele inclui ainda a alternativa para exactOutputSingle, que é exactInputSingle e permite que você negocie ETH por mais DAI que você receberá por isso.

// SPDX-License-Identifier: MIT
pragma solidity =0.7.6;
pragma abicoder v2;

import "https://github.com/Uniswap/uniswap-v3-periphery/blob/main/contracts/interfaces/ISwapRouter.sol";
import "https://github.com/Uniswap/uniswap-v3-periphery/blob/main/contracts/interfaces/IQuoter.sol";

interface IUniswapRouter is ISwapRouter {
    function refundETH() external payable;
}

contract Uniswap3 {
  IUniswapRouter public constant uniswapRouter = IUniswapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564);
  IQuoter public constant quoter = IQuoter(0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6);
  address private constant DaiTestnet = 0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa;  // adicionar o endereço do token DAI na rede de teste necessária
  address private constant WETH9 = 0xd0A1E359811322d97991E03f863a0C30C2cF029C;   // adicionar o endereço WETH na rede de teste necessária

  function convertExactEthToDai() external payable {
    require(msg.value > 0, "Must pass non 0 ETH amount");

    uint256 deadline = block.timestamp + 15; // usando "agora" por conveniência, para o prazo de passagem da rede principal a partir do frontend!
    address tokenIn = WETH9;
    address tokenOut = multiDaiKovan;
    uint24 fee = 3000;
    address recipient = msg.sender;
    uint256 amountIn = msg.value;
    uint256 amountOutMinimum = 1;
    uint160 sqrtPriceLimitX96 = 0;

    ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams(
        tokenIn,
        tokenOut,
        fee,
        recipient,
        deadline,
        amountIn,
        amountOutMinimum,
        sqrtPriceLimitX96
    );

    uniswapRouter.exactInputSingle{ value: msg.value }(params);
    uniswapRouter.refundETH();

    // reembolsar os restos de ETH ao usuário
    (bool success,) = msg.sender.call{ value: address(this).balance }("");
    require(success, "refund failed");
  }

  function convertEthToExactDai(uint256 daiAmount) external payable {
    require(daiAmount > 0, "Must pass non 0 DAI amount");
    require(msg.value > 0, "Must pass non 0 ETH amount");

    uint256 deadline = block.timestamp + 15; // usando "agora" por conveniência, para o prazo de passagem da rede principal a partir do frontend!
    address tokenIn = WETH9;
    address tokenOut = multiDaiKovan;
    uint24 fee = 3000;
    address recipient = msg.sender;
    uint256 amountOut = daiAmount;
    uint256 amountInMaximum = msg.value;
    uint160 sqrtPriceLimitX96 = 0;

    ISwapRouter.ExactOutputSingleParams memory params = ISwapRouter.ExactOutputSingleParams(
        tokenIn,
        tokenOut,
        fee,
        recipient,
        deadline,
        amountOut,
        amountInMaximum,
        sqrtPriceLimitX96
    );

    uniswapRouter.exactOutputSingle{ value: msg.value }(params);
    uniswapRouter.refundETH();

    // reembolsar os restos de ETH ao usuário
    (bool success,) = msg.sender.call{ value: address(this).balance }("");
    require(success, "refund failed");
  }

  // não usar on-chain, gás ineficiente!
  function getEstimatedETHforDAI(uint daiAmount) external payable returns (uint256) {
    address tokenIn = WETH9;
    address tokenOut = multiDaiKovan;
    uint24 fee = 3000;
    uint160 sqrtPriceLimitX96 = 0;

    return quoter.quoteExactOutputSingle(
        tokenIn,
        tokenOut,
        fee,
        daiAmount,
        sqrtPriceLimitX96
    );
  }

  // importante para receber ETH
  receive() payable external {}
}
Enter fullscreen mode Exit fullscreen mode

A diferença entre exactInput e exactOutput

Depois de executar as funções e examiná-las no Etherscan, a diferença se torna imediatamente óbvia. Aqui estamos negociando com exactOutput. Fornecemos 1 ETH e queremos receber 100 DAI em troca. Qualquer quantidade excedente de ETH é reembolsada para nós.

2

E aqui estamos negociando usando exactInput. Estamos fornecendo 1 ETH e queremos receber quanto DAI pudermos obter por isso, que é 196 DAI.

3

Aqui usei uma rede de testes antiga. Para implementar isso em outra rede de teste (como Goerli), encontre uma torneira (faucet) ou crie seus próprios tokens DAI e WETH para trocar.

Obrigado pela atenção e até breve!


Artigo escrito por Alexandr Kumancev e traduzido por Marcelo Panegali.

Latest comments (0)