Pular para o conteúdo principal

Demostração de contratos Uniswap-v2

solidity
Intermediário
Ori Pomerantz
1 de maio de 2021
60 minutos de leitura minute read

Introdução

Uniswap v2(opens in a new tab) pode criar um mercado de câmbio entre quaisquer dos dois tokens ERC-20. Neste artigo, analisaremos o código-fonte dos contratos que implementam este protocolo e entederemos porque eles foram escritos dessa forma.

O que a Uniswap faz?

Basicamente, existem dois tipos de usuários: fornecedores de liquidez e traders.

Os provedores de liquidez fornecem um pool com o par de tokens que podem ser trocados (vamos chamá-los de Token0 e Token1). Em troca, eles recebem um terceiro token que representa a propriedade parcial do pool, chamado de token de liquidez.

Os traders enviam um dos tipos de token para o pool e recebem outro em troca (por exemplo, enviam o Token0 e recebem o Token1) do pool criado pelos provedores de liquidez. A taxa de câmbio é determinada pelo número relativo de Token0 e Token1 que o pool possui. Além disso, o pool recolhe uma pequena porcentagem como recompença por prover liquidez.

Quando provedores de liquidez querem seus ativos de volta, eles podem queimar os tokens do pool e receber seus tokens originais, incluindo sua parcela das recompensas.

Clique aqui para acessar a explicação completa(opens in a new tab).

Por que v2? Por que não v3?

Uniswap v3(opens in a new tab) é uma atualização muito mais complicada do que a v2. Portanto, é mais fácil aprender primeiro a v2 e depois ir para a v3.

Contratos Principais vs Contratos Periféricos

O Uniswap v2 é dividido em dois componentes, um principal e um periférico. Essa divisão permite os contratos principais, que mantêm os ativos e, portanto precisam ser seguros, para serem simples e fáceis de auditar. Toda a funcionalidade extra exigida pelos traders é assegurada pelos contratos periféricos.

Dados e Fluxos de Controle

Este é o fluxo de dados e controle que ocorre quando você executa as três principais ações da Uniswap:

  1. Troca entre diferentes tokens
  2. Adicionar liquidez ao mercado e obter recompensas trocando seus tokens pelo token de liquidez ERC-20
  3. Queimar tokens de liquidez ERC-20 e receber de volta os tokens ERC-20 que o par de troca permite aos traders trocar

Câmbio

Este é o fluxo mais comum usado pelos traders:

Usuário

  1. Fornecer à conta periférica uma provisão correspondente ao valor a ser trocado.
  2. Chamar uma das várias funções de troca contidas nos contratos periféricos (cada uma depende se há ETH envolvido ou não, se o trader especifica a quantidade de tokens depositados ou a quantidade de tokens a receber, etc.). Toda função de troca aceita uma rota, uma matriz de câmbios que devem ser executadas para chegar ao token final.

No contrato periférico (UniswapV2Router02.sol)

  1. Identifica os valores que precisam ser negociados em cada câmbio ao longo da rota.
  2. Itera sobre a rota. Para cada câmbio ao longo da rota, o contrato envia o token de entrada e chama a função swap. Na maioria dos casos, o endereço de destino dos tokens é o próximo par de troca na rota. Ao final do câmbio, é o endereço fornecido pelo trader.

No contrato principal (UniswapV2Pair.sol)

  1. Verifique se o contrato principal não está sendo trapaceado e pode manter liquidez suficiente após a troca.
  2. Veja quantos tokens extras existem além das reservas conhecidas. Essa quantidade é o número de tokens de entrada que recebemos para câmbio.
  3. Enviar os tokens de saída para o destino.
  4. Chamar a função _update para atualizar os valores da reserva

De volta ao contrato periférico (UniswapV2Router02.sol)

  1. Executar qualquer limpeza necessária (por exemplo, queimar tokens WETH para recuperar o ETH e enviar ao negociante)

Adicionar liquidez

Usuário

  1. Permita que a conta periférica acesse a quantidade de tokens que serão adicionados ao pool de liquidez.
  2. Chamar em um dos contratos periféricos a função addLiquidity.

No contrato satélite (UniswapV2Router02.sol)

  1. Criar um par de troca, caso necessário
  2. Caso exista um par de troca, calcule a quantidade de tokens que será adicionada. Supõe-se que esse valor seja idêntico para ambos os tokens, ou seja, a mesma proporção de novos tokens em relação aos tokens existentes.
  3. Verifique se os valores são aceitáveis (os provedores de liquidez podem especificar um valor mínimo, abaixo disso é melhor eles não adicionarem liquidez)
  4. Chame o contrato principal.

No contrato satélite (UniswapV2Pair.sol)

  1. Minerar tokens de liquidez e enviar para o usuário
  2. Chame a função _update para atualizar os valores da reserva

Remover liquidez

Usuário

  1. Permita que a conta periférica queime o par de tokens de liquidez em troca dos tokens inicialmente fornecidos.
  2. Chame em um dos contratos periféricos a função removeLiquidity.

No contrato satélite (UniswapV2Router02.sol)

  1. Enviar os tokens de liquidez para o par de troca

No contrato principal (UniswapV2Pair.sol)

  1. Enviar ao endereço de destino os tokens inicialmente fornecidos em proporção aos tokens queimados. Por exemplo, se existem 1.000 tokens A no pool, 500 tokens B e 90 tokens de liquidez, e nós recebemos 9 tokens para queimar, então, vamos queimar 10% dos tokens de liquidez e enviaremos de volta para o usuário 100 tokens A e 50 tokens B.
  2. Queima os tokens de liquidez
  3. Chamar a função _update para atualizar os valores da reserva

Os contratos principais

Esses são os contratos seguros que detêm a liquidez.

UniswapV2Pair.sol

Esse contrato(opens in a new tab) implementa o pool real que troca os tokens. É a funcionalidade principal do Uniswap.

1pragma solidity =0.5.16;
2
3import './interfaces/IUniswapV2Pair.sol';
4import './UniswapV2ERC20.sol';
5import './libraries/Math.sol';
6import './libraries/UQ112x112.sol';
7import './interfaces/IERC20.sol';
8import './interfaces/IUniswapV2Factory.sol';
9import './interfaces/IUniswapV2Callee.sol';
Exibir tudo
Copiar

Estas são todas as interfaces que o contrato precisa conhecer, ou porque o contrato os implementa (IUniswapV2Pair e UniswapV2ERC20) ou porque eles chamam contratos que os implementam.

1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {
Copiar

Este contrato herda do UniswapV2ERC20, que fornece as funções do ERC-20 para os tokens de liquidez.

1 using SafeMath for uint;
Copiar

A biblioteca SafeMath(opens in a new tab) é usada para evitar excesso de fluxo e fluxo insuficiente. Isso é importante porque, caso contrário, podemos acabar em uma situação em que um valor deve ser -1, mas em vez disso é 2^256-1.

1 using UQ112x112 for uint224;
Copiar

Muitos dos cálculos do contrato do pool requerem frações. Contudo, as frações não são suportadas pelo EVM. A solução encontrada pelo Uniswap é usar valores de 224 bits, com 112 bits para a parte inteira, e 112 bits para a fração. Então 1.0 é representado como 2^112, 1.5 é representado como 2^112 + 2^111, etc.

Mais detalhes sobre essa biblioteca estão disponíveis no final do documento.

Variáveis

1 uint public constant MINIMUM_LIQUIDITY = 10**3;
Copiar

Para evitar casos de divisão por zero, existe um número minímo de tokens de liquidez que sempre existirão (mas são de propriedade da conta zero). Esse número é MINIMUM_LIQUIDITY, mil.

1 bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));
Copiar

Esse é o seletor ABI para a função de transferência ERC-20. É usado para transferir tokens ERC-20 nas duas contas de token.

1 address public factory;
Copiar

Este é o contrato fábrica que criou este pool. Todo pool é um câmbio entre dois tokens ERC-20, a fábrica é o ponto central que conecta todos esses pools.

1 address public token0;
2 address public token1;
Copiar

Existem os endereços dos contratos para os dois tipos de tokens ERC-20 que podem ser trocados por esse pool.

1 uint112 private reserve0; // uses single storage slot, accessible via getReserves
2 uint112 private reserve1; // uses single storage slot, accessible via getReserves
Copiar

A reserva que o pool tem para cada tipo de token. Assumimos que os dois representam a mesma quantidade em valor e, portanto, cada token0 vale o token1 de reserve1/reserve0.

1 uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves
Copiar

O carimbo de data/hora para o último bloco em que ocorreu uma troca, usada para acompanhar as taxas de câmbio ao longo do tempo.

Um dos maiores gastos de gás nos contratos Ethereum é o armazenamento, que persiste de um chamado do contrato para o próximo. Cada célula de armazenamento tem 256 bits de comprimento. Então três variáveis, reserve0, reserve1 e blockTimestampLast, são alocados de forma que uma única célula de armazenamento inclua todas as três juntas (112+112+32=256).

1 uint public price0CumulativeLast;
2 uint public price1CumulativeLast;
Copiar

Essas variáveis possuem os custos cumulativos para cada token (um em relação aos outros). Elas podem ser usadas para calcular a taxa de câmbio médio ao longo de um período de tempo.

1 uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event
Copiar

A forma como o par de troca decide sobre a taxa de câmbio entre o token0 e o token1 é mantendo o múltiplo das reservas constantes durante as negociações. kLast é esse valor. Ela muda quando um provedor de liquidez deposita ou retira tokens, e aumenta ligeiramente devido à taxa de mercado de 0,3%.

Veja um exemplo. Note que para manter a simplicidade a tabela mostra apenas três dígitos após a vírgula decimal e ignoramos a taxa de negociação de 0,3%, portanto os números não são precisos.

Eventoreserve0reserve1reserve0 * reserve1Taxa de câmbio média (token1 / token0)
Configuração Inicial1.000,0001.000,0001.000.000
Trader A troca 50 token0 por 47,619 token11.050,000952,3811.000.0000,952
Trader B troca 10 token0 por 8,984 token11.060,000943,3961.000.0000,898
Trader C troca 40 token0 por 34,305 token11.100,000909,0901.000.0000,858
Trader D troca 100 token1 por 109,01 token0990,9901.009,0901.000.0000,917
Trader E troca 10 token0 por 10,079 token11.000,990999,0101.000.0001,008

À medida que os traders fornecem mais token0, o valor relativo do token1 aumenta, e vice-versa, baseado na oferta e na demanda.

Bloqueio

1 uint private unlocked = 1;
Copiar

Há uma classe de vulnerabilidades de segurança baseadas no abuso de reentrância(opens in a new tab). O Uniswap precisa transferir tokens ERC-20 arbitrários, o que significa chamar o contrato ERC-20, que pode tentar abusar do mercado do Uniswap que os chama. Tendo uma variável unlocked como parte do contrato, podemos impedir que funções sejam chamadas enquanto elas estão sendo executadas (dentro de uma mesma transação).

1 modifier lock() {
Copiar

Essa função é um modificador(opens in a new tab), uma função que envolve uma função normal e muda seu comportamento de alguma forma.

1 require(unlocked == 1, 'UniswapV2: LOCKED');
2 unlocked = 0;
Copiar

Se unlocked é igual a um, defina-a como zero. Se ela já for zero, reverta a chamada e faça-a falhar.

1 _;
Copiar

Em um modificador, _; é a chamada original da função (com todos os parâmetros). Aqui, isso significa que a execução da função só acontece se unlocked era um quando a função foi chamada, e enquanto ela estiver sendo executada, o valor de unlocked é zero.

1 unlocked = 1;
2 }
Copiar

Após o retorno da função principal, libere o bloqueio.

Outras funções

1 function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
2 _reserve0 = reserve0;
3 _reserve1 = reserve1;
4 _blockTimestampLast = blockTimestampLast;
5 }
Copiar

Esta função fornece aos chamadores o estado atual do câmbio. Observe que as funções do Solidity podem retornar multiplos valores(opens in a new tab).

1 function _safeTransfer(address token, address to, uint value) private {
2 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));
Copiar

Essa função interna transfere uma quantidade de tokens ERC20 do câmbio para outra pessoa. SELECTOR especifica que a função que estamos chamando é transfer(address,uint) (veja a definição acima).

Para evitar ter que importar uma interface para a função do token, nós "manualmente" criamos o chamado usando uma das funções ABI(opens in a new tab).

1 require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');
2 }
Copiar

Uma chamada de transferência ERC-20 pode reportar uma falha de duas maneiras:

  1. Reverter. Se a execução em um contrato externo for revertida, o valor de retorno booleano será false
  2. Finaliza normalmente, mas reporta uma falha. Nesse caso, o buffer de valor de retorno tem um comprimento diferente de zero e quando decodificado como um valor booleano, ele é false

Se alguma dessas condições ocorrer, reverta a execução.

Eventos

1 event Mint(address indexed sender, uint amount0, uint amount1);
2 event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
Copiar

Esses dois eventos são emitidos quando um provedor de liquidez deposita liquidez (Mint) ou a retira (Burn). Em ambos casos, a quantidade de token0 e token1 que são depositados ou sacados são parte do evento, bem como a identidade da conta que os chamou (sender). No caso de um saque, o evento também inclui o endereço que recebe os tokens (to), que pode não ser o mesmo do remetente.

1 event Swap(
2 address indexed sender,
3 uint amount0In,
4 uint amount1In,
5 uint amount0Out,
6 uint amount1Out,
7 address indexed to
8 );
Copiar

Esse evento é emitido quando o negociante troca um token pelo outro. Mais uma vez, o remetente e o destinatário podem ser diferentes. Cada token pode ser enviado para o câmbio ou é recebido dele.

1 event Sync(uint112 reserve0, uint112 reserve1);
Copiar

Por fim, Sync é emitido toda vez que os tokens são adicionados ou sacados, independentemente do motivo, para fornecer as informações mais recentes das reservas dos tokens (e, portanto, a taxa de câmbio).

Funções de Configuração

Essa função deve ser executada uma única vez, quando um novo par de troca é criado.

1 constructor() public {
2 factory = msg.sender;
3 }
Copiar

O construtor certifica-se de que manteremos a rastreabilidade do endereço do contrato da fábrica que criou o par. Essa informação é necessária para a função initialize e para a taxa de fábrica (se existir uma)

1 // called once by the factory at time of deployment
2 function initialize(address _token0, address _token1) external {
3 require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
4 token0 = _token0;
5 token1 = _token1;
6 }
Copiar

Essa função permite que a fábrica (e apenas a fábrica) especifique os dois tokens ERC-20 que esse par irá trocar.

Funções de atualização interna

_update
1 // update reserves and, on the first call per block, price accumulators
2 function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
Copiar

Essa função é chamada toda vez que os tokens são depositados ou sacados.

1 require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');
Copiar

Se balance0 ou balance1 (uint256) for maior que uint112(-1) (=2^112-1) (então, ele excede o fluxo e retornará para 0 quando for convertido em uint112), recuse a continuar a _update para evitar excesso de fluxo. Com um token normal que pode ser subdividido em 10^18 unidades, isso significa que cada câmbio está limitado a cerca de 5,1*10^15 de cada token. Até o momento, isso não tem sido um problema.

1 uint32 blockTimestamp = uint32(block.timestamp % 2**32);
2 uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
3 if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
Copiar

Se o tempo decorrido não for zero, isso significa que somos a primeira transação de câmbio nesse bloco. Nesse caso, precisamos atualizar os acumuladores de custo.

1 // * never overflows, and + overflow is desired
2 price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
3 price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
4 }
Copiar

Cada acumulador de custo é atualizado com o último custo (reserva do outro token/reserva desse token) vezes o tempo decorrido em segundos. Para obter um preço médio, tome o preço acumulado em dois pontos no tempo e divida-o pela diferença de tempo entre eles. Por exemplo, suponha esta sequência de eventos:

Eventoreserva0reserva1carimbo de data/horaTaxa de câmbio marginal (reserve1 / reserve0)price0CumulativeLast
Configuração Inicial1.000,0001.000,0005.0001.0000
Trader A deposita 50 token0 e recebe 47,619 token11.050,000952,3815.0200,90720
Trader B deposita 10 token0 e recebe 8,984 token11.060,000943,3965.0300,89020+10*0,907 = 29,07
Trader C deposita 40 token0 e recebe 34,305 token11.100,000909,0905.1000,82629,07+70*0,890 = 91,37
Trader D deposita 100 token1 e recupera 109,01 token0990,9901.009,0905.1101.01891,37+10*0,826 = 99,63
Trader E deposita 10 token0 e recupera 10,079 token11.000,990999,0105.1500,99899,63+40*1,1018 = 143,702

Digamos que queremos calcular o preço médio de Token0 entre os carimbos de data/hora 5,030 e 5,150. A diferença no valor de price0Cumulative é de 143,702-29,07=114,632. Essa é a média em dois minutos (120 segundos). Portanto, o preço médio é 114,632/120 = 0,955.

Esse cálculo de preço é a razão pela qual precisamos conhecer os tamanhos de reserva antigos.

1 reserve0 = uint112(balance0);
2 reserve1 = uint112(balance1);
3 blockTimestampLast = blockTimestamp;
4 emit Sync(reserve0, reserve1);
5 }
Copiar

Por fim, atualize as variáveis globais e emita um evento Sync.

_mintFee
1 // if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k)
2 function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
Copiar

No Uniswap 2.0, os traders pagam uma taxa de 0,30% para usar o mercado. A maior parte da taxa (0,25% da operação) sempre vai para os provedores de liquidez. O restante 0,05% pode ir tanto para o provedor de liquidez quanto para o endereço especificado pela fábrica como a taxa de protocolo, que paga a Uniswap pelo seu esfoço de desenvolvimento.

Para redução de cálculos (e, portanto, custos de gás), essa taxa é calculada apenas quando liquidez é adicionada ou removida do pool, em vez de ser calculada a cada transação.

1 address feeTo = IUniswapV2Factory(factory).feeTo();
2 feeOn = feeTo != address(0);
Copiar

Leia o destino de taxa da fábrica. Se for zero, não haverá nehuma taxa de protocolo e, portanto, não há necessidade de calculá-la.

1 uint _kLast = kLast; // gas savings
Copiar

A variável kLast do estado está localizada no armazenamento, portanto, ela terá um valor entre chamadas diferentes para o contrato. Acessar o armazenamento é muito mais caro do que acessar a memória volátil, que é liberada quando a chamada de função para o contrato termina. Por isso, usamos uma variável interna para economizar gás.

1 if (feeOn) {
2 if (_kLast != 0) {
Copiar

Os provedores de liquidez recebem sua parte simplesmente pela valorização de seus tokens de liquidez. No entanto, a taxa do protoclo requer que novos tokens de liquidez sejam cunhados e fornecidos ao endereço feeTo.

1 uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
2 uint rootKLast = Math.sqrt(_kLast);
3 if (rootK > rootKLast) {
Copiar

Se houver uma nova liquidez para coletar a taxa de protocolo. Você pode ver a função raiz quadrada mais tarde neste artigo

1 uint numerator = totalSupply.mul(rootK.sub(rootKLast));
2 uint denominator = rootK.mul(5).add(rootKLast);
3 uint liquidity = numerator / denominator;
Copiar

Este cálculo complicado das taxas é explicado no whitepaper(opens in a new tab) na página 5. Sabemos que entre o tempo kLast calculado e o presente, não foi adicionada ou removida liquidez (porque executamos esse cálculo toda vez que a liquidez é adicionada ou removida, antes que ele realmente mude), por isso, qualquer mudança em reserve0 * reserve1 tem que vir de taxas de transação (sem elas, manteríamos reserve0 * reserve1 constante).

1 if (liquidity > 0) _mint(feeTo, liquidity);
2 }
3 }
Copiar

Use a função UniswapV2ERC20._mint para criar os tokens de liquidez adicionais e atribuí-los a feeTo.

1 } else if (_kLast != 0) {
2 kLast = 0;
3 }
4 }
Copiar

Se não houver nenhuma taxa, defina kLast como zero (caso ainda não esteja definido). Quando esse contrato foi escrito, havia um recurso de reembolso de gás(opens in a new tab) que incentivava os contratos a reduzir o tamanho geral do estado Ethereum, zerando o armazenamento que não era necessário. Esse código recebe o reembolso quando possível.

Funções acessíveis externamente

Observe que, embora qualquer transação ou contrato possa chamar essas funções, elas foram concebidas para serem chamadas a partir do contrato periférico. Se você as chamar diretamente, não conseguirá trapacear com o par de troca, mas poderá perder o valor por meio de um erro.

cunhar
1 // this low-level function should be called from a contract which performs important safety checks
2 function mint(address to) external lock returns (uint liquidity) {
Copiar

Essa função é chamada quando um provedor de liquidez adiciona liquidez ao pool. Ele cunha tokens de liquidez adicionais como recompensa. Ela deverá ser chamada de contrato periférico, que a chama após adicionar a liquidez na mesma transação (por isso, ninguém poderia enviar uma transação que revindica a nova liquidez antes do dono legítimo).

1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
Copiar

Essa é a maneira de ler os resultados de uma função Solidity que retorna múltiplos valores. Descartamos os últimos valores retornados, o carimbo de data-hoira do bloco, por que não precisamos deles.

1 uint balance0 = IERC20(token0).balanceOf(address(this));
2 uint balance1 = IERC20(token1).balanceOf(address(this));
3 uint amount0 = balance0.sub(_reserve0);
4 uint amount1 = balance1.sub(_reserve1);
Copiar

Obtenha os saldos atuais e veja o quanto foi adicionado de cada tipo de token.

1 bool feeOn = _mintFee(_reserve0, _reserve1);
Copiar

Calcule as taxas de protocolo a serem coletadas, se houver, e crie os respectivos tokens de liquidez. Como os parâmetros para _mintFee são os valores de reserva antigos, a taxa é calculada com precisão com base unicamente nas alterações do pool decorrentes das taxas.

1 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
2 if (_totalSupply == 0) {
3 liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
4 _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
Copiar

Se este for o primeiro depósito, crie tokens MINIMUM_LIQUIDITY e envie-os para o endereço zero para bloqueá-los. Eles nunca podem ser resgatados, o que significa que o pool nunca ficará completamente vazio (o que nos salva da divisão por zero em alguns casos). O valor de MINIMUM_LIQUIDITY é mil, considerando que a maioria dos ERC-20 são subdivididos em unidades de 10^-18'th de um token, visto que o ETH é dividido em wei e equivale a 10^-15 do valor de um único token. Não é um custo alto.

No momento do primeiro depósito, não sabemos o valor relativo dos dois tokens, por isso, multiplicamos as quantidades e aplicamos a raiz quadrada, supondo que o depósito nos fornece o mesmo valor em ambos os tokens.

Podemos confiar nisso, pois é do interesse do depositante oferecer o mesmo valor para evitar perda de valor por arbitragem. Digamos que o valor dos dois tokens é idêntico, mas nosso depositante depositou quatro vezes mais o Token1 do que o Token0. Um trader pode usar o fato de que o par de troca pensa que o Token0 é mais valioso para extrair valor dessa situação.

Eventoreserva0reserva1reserva0 * reserva1Valor do pool (reserve0 + reserve1)
Configuração Inicial83225640
O trader deposita 8 tokens Token0 e recupera 16 Token1161625632

Como você pode ver, o trader ganhou 8 tokens extra, que vêm de uma redução do valor do pool, prejudicando o depositante que a possui.

1 } else {
2 liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
Copiar

Em todos os depósitos subsequentes, já conhecemos a taxa de câmbio entre os dois ativos e esperamos que os provedores de liquidez forneçam um valor igual em ambos. Caso contrário, daremos a eles tokens de liquidez com base no menor valor que eles forneceram como punição.

Seja um depósito inicial, seja um depósito subsequente, o número de tokens de liquidez que fornecemos é igual à raiz quadradada da alteração em reserve0*reserve1 e o valor do token de liquidez não muda (a menos que obtenhamos um depósito com valores diferentes nos dois tipos, então, neste caso, a "multa" é distribuída). Aqui está outro exemplo com dois tokens que têm o mesmo valor, com três depósitos bons e um ruim (depósito de apenas um tipo de token, portanto, ele não produz nenhum token de liquidez).

Eventoreserva0reserva1reserva0 * reserva1Valor do Pool (reserve0 + reserve1)Tokens de liquidez cunhados para este depósitoTotal de tokens de liquidezvalor de cada token de liquidez
Configuração Inicial8,0008,0006416,000882,000
Depósito de quatro de cada tipo12,00012,00014424,0004122,000
Depósito de dois de cada tipo14,00014,00019628,0002142,000
Depósito de valores desiguais18,00014,00025232,000014~2,286
Após a arbitragem~15,874~15,874252~31,748014~2,267
1 }
2 require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
3 _mint(to, liquidity);
Copiar

Use a função UniswapV2ERC20._mint para criar os tokens de liquidez adicionais e fornecê-los para a conta correta.

1
2 _update(balance0, balance1, _reserve0, _reserve1);
3 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
4 emit Mint(msg.sender, amount0, amount1);
5 }
Copiar

Atualize as variáveis de estado (reserve0, reserve1, e se necessário kLast) e emita o evento apropriado.

queimar
1 // this low-level function should be called from a contract which performs important safety checks
2 function burn(address to) external lock returns (uint amount0, uint amount1) {
Copiar

Essa função é chamada quando a liquidez é retirada e os tokens de liquidez apropriados precisam ser queimados. Ela também deve ser chamada a partir de uma conta periférica.

1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
2 address _token0 = token0; // gas savings
3 address _token1 = token1; // gas savings
4 uint balance0 = IERC20(_token0).balanceOf(address(this));
5 uint balance1 = IERC20(_token1).balanceOf(address(this));
6 uint liquidity = balanceOf[address(this)];
Copiar

O contrato periférico transferiu a liquidez que será queimada para este contrato antes da chamada. Dessa forma, sabemos quanta liquidez queimar, e podemos garantir que ela seja queimada.

1 bool feeOn = _mintFee(_reserve0, _reserve1);
2 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
3 amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
4 amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
5 require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');
Copiar

O provedor de liquidez recebe um valor igual de ambos os tokens. Dessa forma, não mudamos a taxa de câmbio.

1 _burn(address(this), liquidity);
2 _safeTransfer(_token0, to, amount0);
3 _safeTransfer(_token1, to, amount1);
4 balance0 = IERC20(_token0).balanceOf(address(this));
5 balance1 = IERC20(_token1).balanceOf(address(this));
6
7 _update(balance0, balance1, _reserve0, _reserve1);
8 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
9 emit Burn(msg.sender, amount0, amount1, to);
10 }
11
Exibir tudo
Copiar

O resto da função burn é o espelho da função mint acima.

troca
1 // this low-level function should be called from a contract which performs important safety checks
2 function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
Copiar

Essa função também deve ser chamada no contrato periférico.

1 require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
2 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
3 require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
4
5 uint balance0;
6 uint balance1;
7 { // scope for _token{0,1}, avoids stack too deep errors
Copiar

Variáveis locais podem ser armazenadas na memória ou, se não houver muitas delas, diretamente na pilha. Se pudermos limitar o número em que usamos a pilha, gastaremos menos gás. Para mais detalhes, confira o yellow paper, as especificações formais do Ethereum(opens in a new tab), p. 26, equação 298.

1 address _token0 = token0;
2 address _token1 = token1;
3 require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
4 if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
5 if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
Copiar

Essa transferência é optimista, porque transferimos antes de termos certeza de que todas as condições estão preenchidas. Isso é aceitável no Ethereum porque, se as condições não forem atendidas mais tarde na chamada, anularemos a função e todas as alterações criadas.

1 if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
Copiar

Informe o destinatário sobre a troca, se solicitado.

1 balance0 = IERC20(_token0).balanceOf(address(this));
2 balance1 = IERC20(_token1).balanceOf(address(this));
3 }
Copiar

Obtenha os saldos atuais. O contrato periférico envia-nos os tokens antes de nos chamar para a troca. Isso facilita para o contrato verificar se não está sendo trapaceado, uma verificação que tem que acontecer no contrato principal (porque podemos ser chamados por outras entidades além do nosso contrato periférico).

1 uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
2 uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
3 require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
4 { // scope for reserve{0,1}Adjusted, avoids stack too deep errors
5 uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
6 uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
7 require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
Copiar

Esta é uma verificação de integridade para garantir que não haja perdas durante a troca. Em nenhuma circunstância, a troca deverá reduzir reserve0*reserve1. É também aqui que asseguramos que uma taxa igual a 0,3% está sendo efetuada na troca; antes da verificação de integridade do valor de K, multiplicamos os dois saldos por 1.000 e subtraímos do montante multiplicado por 3, ou seja, 0,3% (3/1000 = 0,003 = 0,3%) está sendo deduzido do saldo antes de comparar seu valor K com o valor atual das reservas K.

1 }
2
3 _update(balance0, balance1, _reserve0, _reserve1);
4 emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
5 }
Copiar

Atualize reserve0, reserve1 e, se necessário, os acumuladores de preço, o carimbo de data-hora e emita um evento.

Sincronizar ou Examinar

É possível que os saldos reais fiquem dessincronizados com as reservas que o par de troca pensa que tem. Não há nenhuma forma de retirar os tokens sem o consentimento do contrato, mas os depósitos são uma questão diferente. Uma conta pode transferir tokens para a corretora sem chamar mint ou swap.

Nesse caso, há duas soluções:

  • sync, atualizar as reservas para os saldos atuais
  • skim, sacar o valor extra. Observe que qualquer conta tem permissão para chamar skim porque não sabemos quem depositou os tokens. Essa informação é emitida em um evento, mas os eventos não são acessíveis na blockchain.
1 // force balances to match reserves
2 function skim(address to) external lock {
3 address _token0 = token0; // gas savings
4 address _token1 = token1; // gas savings
5 _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
6 _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
7 }
8
9
10
11 // force reserves to match balances
12 function sync() external lock {
13 _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
14 }
15}
Exibir tudo
Copiar

UniswapV2Factory.sol

Este contrato(opens in a new tab) cria o par de troca.

1pragma solidity =0.5.16;
2
3import './interfaces/IUniswapV2Factory.sol';
4import './UniswapV2Pair.sol';
5
6contract UniswapV2Factory is IUniswapV2Factory {
7 address public feeTo;
8 address public feeToSetter;
Copiar

Essas variáveis de estado são necessárias para implementar a taxa do protocolo (consulte o whitepaper(opens in a new tab), p. 5). O endereço taxa acumula os tokens de liquidez pela taxa do protocolo, e feeToSetter é o endereço autorizado a mudar taxaPara para um endereço diferente.

1 mapping(address => mapping(address => address)) public getPair;
2 address[] public allPairs;
Copiar

Essas variáveis rastreiam os pares e as trocas entre os dois tipos de token.

O primeiro, getPair, é um mapeamento que identifica um contrato de par de troca baseado nos dois tokens ERC-20 que ele troca. Tokens ERC-20 são identificados pelo endereço dos contratos que os implementam, então as chaves e os valores são todos endereços. Para obter o endereço do par de troca que permite a troca do tokenA para o tokenB, você utiliza getPair[<tokenA address>][<tokenB address>] (ou o contrário).

A segunda variável, allPairs, é uma matriz que inclui todos os endereços dos pares de troca criados por essa fábrica. No Ethereum, você não pode iterar sobre o conteúdo de um mapeamento ou obter uma lista de todas as chaves, então, essa variável é a única maneira de saber qual troca esta fábrica gerencia.

Observação: a razão pela qual você não pode iterar sobre todas as chaves de um mapeamento é que os dados do contrato de armazenamento são caros, portanto, quanto menos os usarmos e os mudarmos, melhor. Você pode criar mapeamentos que suportam iteração(opens in a new tab), mas eles requerem armazenamento extra para uma lista de chaves. Na maioria das aplicações, você não precisa disso.

1 event PairCreated(address indexed token0, address indexed token1, address pair, uint);
Copiar

Esse evento é emitido quando um novo par de troca é criado. Ele inclui os endereços dos tokens, o endereço do par de troca e o número total de trocas gerenciadas pela fábrica.

1 constructor(address _feeToSetter) public {
2 feeToSetter = _feeToSetter;
3 }
Copiar

A única coisa que o construtor faz é especificar o feeToSetter. As fábricas começam sem taxa, e somente feeSetter pode mudar isso.

1 function allPairsLength() external view returns (uint) {
2 return allPairs.length;
3 }
Copiar

Essa função retorna o número de pares de troca.

1 function createPair(address tokenA, address tokenB) external returns (address pair) {
Copiar

Essa é a função principal da fábrica, para criar um par de troca entre dois tokens ERC-20. Note que qualquer um pode chamar esta função. Você não precisa de permissão do Uniswap para criar um novo par de troca.

1 require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
2 (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
Copiar

Queremos que o endereço da nova troca seja determinante, para poder ser calculado antecipadamente fora da cadeia (isso pode ser útil para transações com camada 2). Para isso, precisamos ter uma ordem consistente dos endereços dos tokens, independente da ordem na qual nós os recebemos, então os classificamos aqui.

1 require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
2 require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
Copiar

Os grandes pools de liquidez são melhores do que os pequenos, porque têm preços mais estáveis. Não queremos ter mais do que um único pool de liquidez por par de tokens. Se já existe uma troca, não há necessidade de criar outra para o mesmo par de troca.

1 bytes memory bytecode = type(UniswapV2Pair).creationCode;
Copiar

Para criar um novo contrato, precisamos do código que o cria (tanto do construtor quanto o código que escreve para a memória do bytecode EVM do contrato atual). Normalmente, no Solidity, só usamosaddr = new <name of contract>(<constructor parameters>) e o compilador cuida de tudo para nós, mas para termos um endereço de contrato determinístico, precisamos usar o opcode CREATE2(opens in a new tab). Quando este código foi escrito, esse opcode ainda não era suportado pelo Solidity, então foi necessário obter manualmente o código. Isso não é mais um problema, porque Solidity agora suporta CREATE2(opens in a new tab).

1 bytes32 salt = keccak256(abi.encodePacked(token0, token1));
2 assembly {
3 pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
4 }
Copiar

Quando um opcode não é suportado pelo Solidity, ainda podemos chamá-lo usando o assembly embutido(opens in a new tab).

1 IUniswapV2Pair(pair).initialize(token0, token1);
Copiar

Chame a função inicialize para dizer à nova troca quais são os dois tokens que serão trocados.

1 getPair[token0][token1] = pair;
2 getPair[token1][token0] = pair; // populate mapping in the reverse direction
3 allPairs.push(pair);
4 emit PairCreated(token0, token1, pair, allPairs.length);
5 }
Copiar

Salve as novas informações sobre pares nas variáveis de estado e emita um evento para informar o mundo do novo par de troca.

1 function setFeeTo(address _feeTo) external {
2 require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
3 feeTo = _feeTo;
4 }
5
6 function setFeeToSetter(address _feeToSetter) external {
7 require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
8 feeToSetter = _feeToSetter;
9 }
10}
Exibir tudo
Copiar

Essas duas funções permitem feeSetter controlar o destinatário da taxa (se houver), e alterar feeSetter para um novo endereço.

UniswapV2ERC20.sol

Este contrato(opens in a new tab) implementa o token ERC-20 de liquidez. É semelhante ao contrato do contrato OpenZeppelin ERC-20, então vou apenas explicar a parte que é diferente, a funcionalidade permit.

As transações no Ethereum custam ether (ETH), que é equivalente a dinheiro real. Se você tem tokens ERC-20, mas não tem ETH, você não pode fazer transações, então você não pode fazer nada com eles. Uma solução para evitar esse problema são asmeta-transações(opens in a new tab). O proprietário dos tokens assina uma transação que permite outra pessoa retirar os tokens da cadeia e enviá-los usando a Internet para o destinatário. O destinatário, que efetivamente possui ETH, envia depois a autorização em nome do proprietário.

1 bytes32 public DOMAIN_SEPARATOR;
2 // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
3 bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
Copiar

Este hash é o identificador para o tipo de transação(opens in a new tab). O único que suportamos aqui é Permit com esses parâmetros.

1 mapping(address => uint) public nonces;
Copiar

Não é viável que um destinatário falsifique uma assinatura digital. No entanto, é comum enviar a mesma transação duas vezes (esta é uma forma de ataque de repetição(opens in a new tab)). Para evitar isso, usamos nonce(opens in a new tab). Se o nonce de uma nova Permit não é uma unidade maior do que o último usado, presumimos que ele seja inválido.

1 constructor() public {
2 uint chainId;
3 assembly {
4 chainId := chainid
5 }
Copiar

Este é o código para recuperar o identificador da cadeia(opens in a new tab). Ele usa uma linguagem em assembly do EVM chamado Yul(opens in a new tab). Observe que, na versão atual do Yul, você tem que usar chainid(), e não chainid.

1 DOMAIN_SEPARATOR = keccak256(
2 abi.encode(
3 keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
4 keccak256(bytes(name)),
5 keccak256(bytes('1')),
6 chainId,
7 address(this)
8 )
9 );
10 }
Exibir tudo
Copiar

Calcule o separador de domínio(opens in a new tab) para EIP-712.

1 function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
Copiar

Esta é a função que implementa as permissões. Ela recebe como parâmetros os campos relevantes, e os três valores escalares para a assinatura(opens in a new tab) (v, r, e s).

1 require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
Copiar

Não aceite transações após o prazo.

1 bytes32 digest = keccak256(
2 abi.encodePacked(
3 '\x19\x01',
4 DOMAIN_SEPARATOR,
5 keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
6 )
7 );
Copiar

abi.encodePacked(...) é a mensagem que esperamos receber. Nós sabemos qual deve ser o nonce, então não há necessidade de obtê-lo como um parâmetro.

O algoritmo de assinatura Ethereum espera obter 256 bits para assinar, então usamos a função hash keccak256.

1 address recoveredAddress = ecrecover(digest, v, r, s);
Copiar

Do digest e da assinatura, podemos obter o endereço que o assinou usando ecrecover(opens in a new tab).

1 require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
2 _approve(owner, spender, value);
3 }
4
Copiar

Se tudo estiver OK, trate isso como uma aprovação ERC-20(opens in a new tab).

Os Contratos Periféricos

Os contratos periféricos são a API (interface de programa do aplicativo) do Uniswap. Eles estão disponíveis para chamadas externas, seja de outros contratos ou aplicações descentralizadas. Você poderia chamar os contratos principais diretamente, mas isso é mais complicado e pode perder valor se cometer um erro. Os contratos principais contêm apenas testes para garantir que eles não estejam vulneráveis, e não faz testes de integridade para mais ninguém. Eles se encontram nos periféricos para que possam ser atualizados se necessário.

UniswapV2Router01.sol

Este contrato(opens in a new tab) tem vulnerabilidades e não deve mais ser usado(opens in a new tab). Felizmente, os contratos periféricos não têm estado e não possuem nenhum ativo, então é fácil descontinuá-los e sugerir que os usuários usem seu substituo, o UniswapV2Router02.

UniswapV2Router01.sol

Na maioria dos casos, você usará o Uniswap através deste contrato(opens in a new tab). Você pode ver como usá-lo aqui(opens in a new tab).

1pragma solidity =0.6.6;
2
3import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';
4import '@uniswap/lib/contracts/libraries/TransferHelper.sol';
5
6import './interfaces/IUniswapV2Router02.sol';
7import './libraries/UniswapV2Library.sol';
8import './libraries/SafeMath.sol';
9import './interfaces/IERC20.sol';
10import './interfaces/IWETH.sol';
Exibir tudo
Copiar

A maioria deles já discutimos antes ou são bastante óbvios. A única exceção é IWETH.sol. O Uniswap v2 permite trocas por qualquer par de tokens ERC-20, mas o ether (ETH) em si não é um token ERC-20. Ele é anterior ao padrão e é transferido por meio de mecanismos únicos. Para permitir o uso de ETH em contratos que se aplicam a tokens ERC-20, foi criado o contrato wrapped ether (WETH)(opens in a new tab). Você envia ETH a esse contrato, e ele cunha um valor equivalente de WETH para você. Você também pode queimar WETH e recuperar ETH.

1contract UniswapV2Router02 is IUniswapV2Router02 {
2 using SafeMath for uint;
3
4 address public immutable override factory;
5 address public immutable override WETH;
Copiar

O roteador precisa saber qual fábrica usar, e para transações que exigem WETH, saber qual contrato WETH usar. Estes valores são imutáveis(opens in a new tab), o que significa que eles só podem ser definidos no construtor. Isso dá aos usuários a confiança de que ninguém conseguirá mudá-los para indicar contratos menos honestos.

1 modifier ensure(uint deadline) {
2 require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');
3 _;
4 }
Copiar

Este modificador certifica-se de que as transações limitadas por tempo ("Faça X antes do tempo Y, se puder") não ocorram depois do seu limite de tempo.

1 constructor(address _factory, address _WETH) public {
2 factory = _factory;
3 WETH = _WETH;
4 }
Copiar

O construtor apenas define as variáveis de estado imutáveis.

1 receive() external payable {
2 assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract
3 }
Copiar

Esta função é chamada quando resgatamos tokens do contrato WETH de volta para ETH. Apenas o contrato WETH que usamos está autorizado a fazer isso.

Adicionar Liquidez

Essas funções adicionam tokens ao par de troca, o que aumenta o pool de liquidez.

1
2 // **** ADD LIQUIDITY ****
3 function _addLiquidity(
Copiar

Esta função é usada para calcular a quantidade de tokens A e B que devem ser depositados ao par de troca.

1 address tokenA,
2 address tokenB,
Copiar

Estes são os endereços dos contratos do token ERC-20.

1 uint amountADesired,
2 uint amountBDesired,
Copiar

Estas são as quantidades que o provedor de liquidez quer depositar. Elas também são as quantidades máximas de A e B a serem depositadas.

1 uint amountAMin,
2 uint amountBMin
Copiar

Estas são as quantidades mínimas aceitáveis para o depósito. Se a transação não puder ser feita com esses valores ou mais, cancele-a. Se você não quiser esse recurso, basta especificar zero.

Provedores de liquidez especificam um mínimo, geralmente porque querem limitar a transação a uma taxa de câmbio próxima da atual. Se a taxa de câmbio flutuar demais, pode ser devido a novidades que alteram os valores, e ele querem decidir manualmente o que fazer.

Por exemplo, imagine um caso em que a taxa de câmbio é de um para um, e o provedor de liquidez especifica esses valores:

ParâmetroValor
amountADesired1.000
amountBDesired1.000
amountAMin900
amountBMin800

Enquanto a taxa de câmbio permanecer entre 0,9 e 1,25, a transação será realizada. Se a taxa de câmbio sair desse intervalo, a transação será cancelada.

A razão dessa precaução é que as transações não são imediatas, você as envia e um validador vai incluí-las em um bloco (a menos que seu preço de gás seja muito baixo, nesse caso você precisará enviar outra transação com o mesmo nonce e um preço de gás mais alto para substituí-la). Você não pode controlar o que acontece durante o intervalo entre o envio e a inclusão.

1 ) internal virtual returns (uint amountA, uint amountB) {
Copiar

A função retorna os valores que o provedor de liquidez deve depositar para ter uma proporção igual à atual entre as reservas.

1 // create the pair if it doesn't exist yet
2 if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
3 IUniswapV2Factory(factory).createPair(tokenA, tokenB);
4 }
Copiar

Se ainda não houver par de troca para esses tokens, crie-o.

1 (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
Copiar

Recupere as reservas atuais no par.

1 if (reserveA == 0 && reserveB == 0) {
2 (amountA, amountB) = (amountADesired, amountBDesired);
Copiar

Se as reservas atuais estão vazias, então isso não é um par de troca. Os valores a serem depositados devem ser exatamente iguais àqueles que o provedor de liquidez quer fornecer.

1 } else {
2 uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
Copiar

Se precisarmos ver quais valores serão, obteremos o valor ideal usando esta função(opens in a new tab). Queremos a mesma proporção das reservas atuais.

1 if (amountBOptimal <= amountBDesired) {
2 require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
3 (amountA, amountB) = (amountADesired, amountBOptimal);
Copiar

Se amountBOptimal é menor que a quantidade que o provedor de liquidez quer depositar significa que o token B é mais valioso atualmente do que o depositante de liquidez pensa, portanto, é necessário um valor menor.

1 } else {
2 uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
3 assert(amountAOptimal <= amountADesired);
4 require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
5 (amountA, amountB) = (amountAOptimal, amountBDesired);
Copiar

Se o valor B ideal for maior do que o valor B desejado, isso significa que os tokens B são menos valiosos atualmente do que o depositante de liquidez pensa, portanto, é necessário um valor maior. No entanto, a quantidade desejada é um valor máximo, então não podemos fazer isso. Em vez disso, calculamos o número ideal de tokens A para a quantidade desejada de tokens B.

Colocando tudo isso junto, obtemos este gráfico. Suponha que você esteja tentando depositar mil tokens A (linha azul) e mil tokens B (linha vermelha). O eixo x é a taxa de câmbio, A/B. Se x=1, eles são iguais em valor e você deposita mil de cada um. Se x=2, A é o dobro do valor de B (você ganha dois tokens B para cada token A), então você deposita mil tokens B, mas apenas 500 tokens A. Se x=0,5, a situação é invertida, mil tokens A e quinhentos tokens B.

Grafo

Você pode depositar liquidez diretamente no contrato principal (usandoUniswapV2Pair::mint(opens in a new tab)). No entanto, o contrato principal somente verifica se não está sendo enganado, para que você não corra o risco de perder valor se a taxa de câmbio mudar entre o momento em que envia sua transação e o momento em que ela é executada. Se você usa o contrato periférico, ele calcula o montante que você deve depositar e deposita imediatamente, então a taxa de câmbio não muda e você não perde nada.

1 function addLiquidity(
2 address tokenA,
3 address tokenB,
4 uint amountADesired,
5 uint amountBDesired,
6 uint amountAMin,
7 uint amountBMin,
8 address to,
9 uint deadline
Exibir tudo
Copiar

Esta função pode ser chamada por uma transação para depositar liquidez. A maioria dos parâmetros são os mesmos do _addLiquidity acima, com duas exceções:

. to é o endereço que obtém os novos tokens de liquidez cunhados para mostrar a parte do pool que o provedor de liquidez detém. deadline é o limite de tempo da transação

1 ) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
2 (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
3 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
Copiar

Nós calculamos os valores que realmente depositamos e depois encontramos o endereço do pool de liquidez. Para economizar gás, não fazemos isso perguntando à fábrica, mas usando a função pairFor (veja abaixo nas bibliotecas)

1 TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
2 TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
Copiar

Transfira as quantidades corretas de tokens do usuário para o par de troca.

1 liquidity = IUniswapV2Pair(pair).mint(to);
2 }
Copiar

Em retorno, dê liquidez ao endereço to para a propriedade parcial do pool. A função mint do contrato central vê quantos tokens extras ele tem (comparado com o que ele tinha na última vez que a liquidez mudou) e cunha liquidez por consequência.

1 function addLiquidityETH(
2 address token,
3 uint amountTokenDesired,
Copiar

Quando um provedor de liquidez quer fornecer liquidez a uma troca Token/ETH, existem algumas diferenças. O contrato trata do encapsulamento do ETH para o provedor de liquidez. Não há necessidade de especificar quantos ETH o usuário quer depositar, porque o usuário só os envia com a transação (o valor está disponível em msg.value).

1 uint amountTokenMin,
2 uint amountETHMin,
3 address to,
4 uint deadline
5 ) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {
6 (amountToken, amountETH) = _addLiquidity(
7 token,
8 WETH,
9 amountTokenDesired,
10 msg.value,
11 amountTokenMin,
12 amountETHMin
13 );
14 address pair = UniswapV2Library.pairFor(factory, token, WETH);
15 TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);
16 IWETH(WETH).deposit{value: amountETH}();
17 assert(IWETH(WETH).transfer(pair, amountETH));
Exibir tudo
Copiar

Para depositar o ETH, o contrato primeiro converte em WETH e depois transfere o WETH para o par. Observe que a transferência é envolvida em um assert. Isso significa que se a transferência falhar, essa chamada de contrato também irá falhar, e o encapsulamento do Eth não ocorre.

1 liquidity = IUniswapV2Pair(pair).mint(to);
2 // refund dust eth, if any
3 if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
4 }
Copiar

O usuário já nos enviou o ETH, então, se houver alguma quantidade restante (porque o outro token é menos valioso do que o usuário pensava), precisamos emitir um reembolso.

Remover Liquidez

Essas funções removerão liquidez e pagarão ao provedor de liquidez.

1 // **** REMOVE LIQUIDITY ****
2 function removeLiquidity(
3 address tokenA,
4 address tokenB,
5 uint liquidity,
6 uint amountAMin,
7 uint amountBMin,
8 address to,
9 uint deadline
10 ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
Exibir tudo
Copiar

O caso mais simples de remover liquidez. Há uma quantidade mínima de cada token que o provedor de liquidez concorda em receber, e isso deve acontecer antes do prazo.

1 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
2 IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair
3 (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
Copiar

A função burn do contrato principal lida com o pagamento dos tokens de volta ao usuário.

1 (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
Copiar

Quando uma função retorna vários valores, mas só estamos interessados em alguns deles, é assim que obtemos apenas esses valores. É um pouco mais barato em termos de gás do que ler um valor e nunca o utilizar.

1 (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
Copiar

Traduza os valores do jeito que o contrato principal os retorna (token de endereço inferior primeiro) da maneira esperada pelo usuário (correspondente a tokenA e tokenB).

1 require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
2 require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
3 }
Copiar

É OK fazer a transferência primeiro e depois verificar que ela é legítima, porque se não for, reverteremos todas as alterações de estado.

1 function removeLiquidityETH(
2 address token,
3 uint liquidity,
4 uint amountTokenMin,
5 uint amountETHMin,
6 address to,
7 uint deadline
8 ) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) {
9 (amountToken, amountETH) = removeLiquidity(
10 token,
11 WETH,
12 liquidity,
13 amountTokenMin,
14 amountETHMin,
15 address(this),
16 deadline
17 );
18 TransferHelper.safeTransfer(token, to, amountToken);
19 IWETH(WETH).withdraw(amountETH);
20 TransferHelper.safeTransferETH(to, amountETH);
21 }
Exibir tudo
Copiar

Remover liquidez para ETH é quase a mesma coisa, exceto o fato de recebermos os tokens WETH e, em seguida, resgatá-los para ETH e devolver ao provedor de liquidez.

1 function removeLiquidityWithPermit(
2 address tokenA,
3 address tokenB,
4 uint liquidity,
5 uint amountAMin,
6 uint amountBMin,
7 address to,
8 uint deadline,
9 bool approveMax, uint8 v, bytes32 r, bytes32 s
10 ) external virtual override returns (uint amountA, uint amountB) {
11 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
12 uint value = approveMax ? uint(-1) : liquidity;
13 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
14 (amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline);
15 }
16
17
18 function removeLiquidityETHWithPermit(
19 address token,
20 uint liquidity,
21 uint amountTokenMin,
22 uint amountETHMin,
23 address to,
24 uint deadline,
25 bool approveMax, uint8 v, bytes32 r, bytes32 s
26 ) external virtual override returns (uint amountToken, uint amountETH) {
27 address pair = UniswapV2Library.pairFor(factory, token, WETH);
28 uint value = approveMax ? uint(-1) : liquidity;
29 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
30 (amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline);
31 }
Exibir tudo
Copiar

Essas funções retransmitem meta-transações para permitir que usuários sem ether se retirem do pool, usando o mecanismo de permissão.

1
2 // **** REMOVE LIQUIDITY (supporting fee-on-transfer tokens) ****
3 function removeLiquidityETHSupportingFeeOnTransferTokens(
4 address token,
5 uint liquidity,
6 uint amountTokenMin,
7 uint amountETHMin,
8 address to,
9 uint deadline
10 ) public virtual override ensure(deadline) returns (uint amountETH) {
11 (, amountETH) = removeLiquidity(
12 token,
13 WETH,
14 liquidity,
15 amountTokenMin,
16 amountETHMin,
17 address(this),
18 deadline
19 );
20 TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this)));
21 IWETH(WETH).withdraw(amountETH);
22 TransferHelper.safeTransferETH(to, amountETH);
23 }
24
Exibir tudo
Copiar

Esta função pode ser usada para tokens que têm taxas de transferência ou de armazenamento. Quando um token tem tais taxas, não podemos confiar na função removeLiquidity para nos dizer quanto do token nós recuperaremos. Por isso, primeiro temos que sacar e depois obter o saldo.

1
2
3 function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(
4 address token,
5 uint liquidity,
6 uint amountTokenMin,
7 uint amountETHMin,
8 address to,
9 uint deadline,
10 bool approveMax, uint8 v, bytes32 r, bytes32 s
11 ) external virtual override returns (uint amountETH) {
12 address pair = UniswapV2Library.pairFor(factory, token, WETH);
13 uint value = approveMax ? uint(-1) : liquidity;
14 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
15 amountETH = removeLiquidityETHSupportingFeeOnTransferTokens(
16 token, liquidity, amountTokenMin, amountETHMin, to, deadline
17 );
18 }
Exibir tudo
Copiar

A função final combina taxas de armazenamento com meta-transações.

Negociação

1 // **** SWAP ****
2 // requires the initial amount to have already been sent to the first pair
3 function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {
Copiar

Essa função executa um processamento interno, necessário às funções expostas aos traders.

1 for (uint i; i < path.length - 1; i++) {
Copiar

Enquanto estou escrevendo isso, existem 388.160 tokens ERC-20(opens in a new tab). Se houvesse um par de troca para cada par de tokens, existiriam mais de 150 bilhões de pares de troca. A cadeia inteira, no momento, tem apenas 0,1% desse número de contas(opens in a new tab). Em vez disso, as funções de troca suportam o conceito de caminho. Um trader pode trocar A por B, B por C e C por D, portanto, não há necessidade de uma troca direta do par A-D.

Os preços nesses mercados tendem a estar sincronizados, porque quando eles estão dessincronizados, cria-se uma oportunidade de arbitragem. Imagine, por exemplo, três tokens, A, B e C. Existem três pares de troca, um para cada par.

  1. A situação inicial
  2. Um trader vende 24,695 tokens A e recebe 25,305 tokens B.
  3. O trader vende 24,695 tokens B para 25,305 tokens C, obtendo aproximadamente 0,61 tokens B de lucro.
  4. Em seguida, o trader vende 24,695 tokens B por 25,305 tokens C, obtendo aproximadamente 0,61 tokens B de lucro. O trader também tem tokens adicionais de 0,61 A (os 25,305 obtidos pelo trader, menos o investimento original de 24,695).
EtapaTroca A-BTroca B-CTroca A-C
1A:1.000 B:1.050 A/B=1,05B:1.000 C:1.050 B/C=1,05B:1.050 C:1.000 B/C=1,05
2A:1.024,695 B:1.024,695 A/B=1B:1.000 C:1.050 B/C=1,05B:1.050 C:1.000 B/C=1,05
3A:1.024,695 B:1.024,695 A/B=1B:1.024,695 C:1.024,695 B/C=1,05B:1.050 C:1.000 B/C=1,05
4A:1.024,695 B:1.024,695 A/B=1B:1.024,695 C:1.024,695 B/C=1,05B:1.024,695 C:1.024,695 B/C=1,05
1 (address input, address output) = (path[i], path[i + 1]);
2 (address token0,) = UniswapV2Library.sortTokens(input, output);
3 uint amountOut = amounts[i + 1];
Copiar

Obtenha o par que estamos tratando no momento, classifique-o (para uso com o par) e obtenha o valor de saída esperado.

1 (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
Copiar

Obtenha os valores esperados, classificados da maneira desejada para o par de troca.

1 address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
Copiar

Esta é a última troca? Em caso afirmativo, envie os tokens recebidos para troca para o destino. Caso contrário, envie para o próximo par de troca.

1
2 IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
3 amount0Out, amount1Out, to, new bytes(0)
4 );
5 }
6 }
Copiar

Na verdade, chame a troca de par para trocar os tokens. Não precisamos de um retorno de chamada para ser informado sobre a troca, por isso, não enviamos nenhum byte para esse campo.

1 function swapExactTokensForTokens(
Copiar

Essa função é usada diretamente pelos negociantes para trocar um token pelo outro.

1 uint amountIn,
2 uint amountOutMin,
3 address[] calldata path,
Copiar

Este parâmetro contém os endereços dos contratos ERC-20. Como explicado acima, isso é uma matriz porque você poderá precisar passar por vários pares de troca para obter o ativo que deseja.

Um parâmetro de função no Solidity pode ser armazenado tanto em memory quanto em calldata. Se a função for um ponto de entrada do contrato, chamado diretamente de um usuário (usando uma transação) ou por um contrato diferente, o valor do parâmetro poderá ser retirado diretamente dos dados de chamada. Se a função for chamada internamente, como no _swap acima, os parâmetros deverão ser armazenados em memory. Do ponto de vista do contrato chamado, calldata é somente leitura.

Com tipos escalares como uint ou address, o compilador lida com a escolha do armazenamento para nós, mas com as matrizes, que são mais longas e mais caras, nós especificamos o tipo de armazenamento a ser usado.

1 address to,
2 uint deadline
3 ) external virtual override ensure(deadline) returns (uint[] memory amounts) {
Copiar

Valores de retorno são sempre retornados na memória.

1 amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
2 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
Copiar

Calcula a quantidade a ser comprada em cada troca. Se o resultado for menor do que o mínimo que o trader está disposto a aceitar, reverta a transação.

1 TransferHelper.safeTransferFrom(
2 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
3 );
4 _swap(amounts, path, to);
5 }
Copiar

Por fim, transfira o token ERC-21 inicial para a conta do primeiro par de troca e chame _swap. Tudo isso está acontecendo na mesma transação, então, o par de troca sabe que quaisquer tokens inesperados fazem parte dessa transferência.

1 function swapTokensForExactTokens(
2 uint amountOut,
3 uint amountInMax,
4 address[] calldata path,
5 address to,
6 uint deadline
7 ) external virtual override ensure(deadline) returns (uint[] memory amounts) {
8 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
9 require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
10 TransferHelper.safeTransferFrom(
11 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
12 );
13 _swap(amounts, path, to);
14 }
Exibir tudo
Copiar

A função anterior, swapTokensForTokens, permite que um trader especifique o número exato de tokens de entrada que ele está disposto a dar e o número mínimo de tokens de saída que ele está disposto a receber em troca. Esta função faz a troca inversa, permite que um trader especifique o número de tokens de saída que ele quer, e o número máximo de tokens de entrada que ele está disposto a pagar por eles.

Em ambos os casos, o trader tem que primeiro conceder ao contrato periférico um subsídio que lhe permita transferi-los.

1 function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)
2 external
3 virtual
4 override
5 payable
6 ensure(deadline)
7 returns (uint[] memory amounts)
8 {
9 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
10 amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path);
11 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
12 IWETH(WETH).deposit{value: amounts[0]}();
13 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
14 _swap(amounts, path, to);
15 }
16
17
18 function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
19 external
20 virtual
21 override
22 ensure(deadline)
23 returns (uint[] memory amounts)
24 {
25 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
26 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
27 require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
28 TransferHelper.safeTransferFrom(
29 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
30 );
31 _swap(amounts, path, address(this));
32 IWETH(WETH).withdraw(amounts[amounts.length - 1]);
33 TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
34 }
35
36
37
38 function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
39 external
40 virtual
41 override
42 ensure(deadline)
43 returns (uint[] memory amounts)
44 {
45 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
46 amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
47 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
48 TransferHelper.safeTransferFrom(
49 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
50 );
51 _swap(amounts, path, address(this));
52 IWETH(WETH).withdraw(amounts[amounts.length - 1]);
53 TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
54 }
55
56
57 function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)
58 external
59 virtual
60 override
61 payable
62 ensure(deadline)
63 returns (uint[] memory amounts)
64 {
65 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
66 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
67 require(amounts[0] <= msg.value, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
68 IWETH(WETH).deposit{value: amounts[0]}();
69 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
70 _swap(amounts, path, to);
71 // refund dust eth, if any
72 if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]);
73 }
Exibir tudo
Copiar

Essas quatro variantes envolvem negociação entre ETH e tokens. A única diferença é que recebemos ETH do trader e usamos para cunhar WETH, ou recebemos WETH da última troca no caminho e o queimamos, devolvendo ao trader o ETH resultante.

1 // **** SWAP (supporting fee-on-transfer tokens) ****
2 // requires the initial amount to have already been sent to the first pair
3 function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {
Copiar

Esta é a função interna para trocar tokens que têm taxas de transferência ou armazenamento para resolver (esse problema(opens in a new tab)).

1 for (uint i; i < path.length - 1; i++) {
2 (address input, address output) = (path[i], path[i + 1]);
3 (address token0,) = UniswapV2Library.sortTokens(input, output);
4 IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output));
5 uint amountInput;
6 uint amountOutput;
7 { // scope to avoid stack too deep errors
8 (uint reserve0, uint reserve1,) = pair.getReserves();
9 (uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
10 amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput);
11 amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput);
Exibir tudo
Copiar

Devido às taxas de transferência, não podemos confiar na função getAmountsOut para nos informar quanto recebemos de cada transferência (a forma como fazemos antes de chamar o _swap). Em vez disso, primeiro precisamos transferir e ver quantos tokens recebemos de volta.

Observação: em teoria, poderíamos simplesmente usar essa função em vez de _swap, mas em certos casos (por exemplo, se a transferência acabar sendo cancelada porque não há quantidade suficiente no final para atender ao mínimo necessário) que acabaria custando mais gás. Os tokens de taxa de transferência são bastante raros, portanto, embora precisemos acomodá-los, não há necessidade de todas as trocas assumirem que eles passam por pelo menos uma delas.

1 }
2 (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0));
3 address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
4 pair.swap(amount0Out, amount1Out, to, new bytes(0));
5 }
6 }
7
8
9 function swapExactTokensForTokensSupportingFeeOnTransferTokens(
10 uint amountIn,
11 uint amountOutMin,
12 address[] calldata path,
13 address to,
14 uint deadline
15 ) external virtual override ensure(deadline) {
16 TransferHelper.safeTransferFrom(
17 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
18 );
19 uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
20 _swapSupportingFeeOnTransferTokens(path, to);
21 require(
22 IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
23 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
24 );
25 }
26
27
28 function swapExactETHForTokensSupportingFeeOnTransferTokens(
29 uint amountOutMin,
30 address[] calldata path,
31 address to,
32 uint deadline
33 )
34 external
35 virtual
36 override
37 payable
38 ensure(deadline)
39 {
40 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
41 uint amountIn = msg.value;
42 IWETH(WETH).deposit{value: amountIn}();
43 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn));
44 uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
45 _swapSupportingFeeOnTransferTokens(path, to);
46 require(
47 IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
48 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
49 );
50 }
51
52
53 function swapExactTokensForETHSupportingFeeOnTransferTokens(
54 uint amountIn,
55 uint amountOutMin,
56 address[] calldata path,
57 address to,
58 uint deadline
59 )
60 external
61 virtual
62 override
63 ensure(deadline)
64 {
65 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
66 TransferHelper.safeTransferFrom(
67 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
68 );
69 _swapSupportingFeeOnTransferTokens(path, address(this));
70 uint amountOut = IERC20(WETH).balanceOf(address(this));
71 require(amountOut >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
72 IWETH(WETH).withdraw(amountOut);
73 TransferHelper.safeTransferETH(to, amountOut);
74 }
Exibir tudo
Copiar

Essas são as mesmas variantes usadas para tokens normais, mas chamam _swapSupportingFeeOnTransferTokens.

1 // **** LIBRARY FUNCTIONS ****
2 function quote(uint amountA, uint reserveA, uint reserveB) public pure virtual override returns (uint amountB) {
3 return UniswapV2Library.quote(amountA, reserveA, reserveB);
4 }
5
6 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut)
7 public
8 pure
9 virtual
10 override
11 returns (uint amountOut)
12 {
13 return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut);
14 }
15
16 function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut)
17 public
18 pure
19 virtual
20 override
21 returns (uint amountIn)
22 {
23 return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut);
24 }
25
26 function getAmountsOut(uint amountIn, address[] memory path)
27 public
28 view
29 virtual
30 override
31 returns (uint[] memory amounts)
32 {
33 return UniswapV2Library.getAmountsOut(factory, amountIn, path);
34 }
35
36 function getAmountsIn(uint amountOut, address[] memory path)
37 public
38 view
39 virtual
40 override
41 returns (uint[] memory amounts)
42 {
43 return UniswapV2Library.getAmountsIn(factory, amountOut, path);
44 }
45}
Exibir tudo
Copiar

Estas funções são apenas proxies que chamam as funções UniswapV2Library.

UniswapV2Migrator.sol

Este contrato foi usado para migrar trocas da antiga v1 para v2. Agora que eles foram migrados, isso já não é mais relevante.

As bibliotecas

A biblioteca SafeMath(opens in a new tab) está bem documentada, então não há necessidade de documentá-la aqui.

Matemática

Esta biblioteca contém algumas funções matemáticas que normalmente não são necessárias no código do Solidity, portanto, elas não fazem parte da linguagem.

1pragma solidity =0.5.16;
2
3// a library for performing various math operations
4
5library Math {
6 function min(uint x, uint y) internal pure returns (uint z) {
7 z = x < y ? x : y;
8 }
9
10 // babylonian method (https://wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)
11 function sqrt(uint y) internal pure returns (uint z) {
12 if (y > 3) {
13 z = y;
14 uint x = y / 2 + 1;
Exibir tudo
Copiar

Comece com x como uma estimativa maior do que a raiz quadrada (razão pela qual precisamos tratar 1 – 3 como casos especiais).

1 while (x < z) {
2 z = x;
3 x = (y / x + x) / 2;
Copiar

Faça uma estimativa mais próxima, a média da estimativa anterior e o número da raiz quadrada a qual estamos tentando encontrar dividida pela estimativa anterior. Repita até a nova estimativa não ser menor do que a existente. Para mais detalhes, veja aqui(opens in a new tab).

1 }
2 } else if (y != 0) {
3 z = 1;
Copiar

Nunca devemos precisar da raiz quadrada de zero. As raízes quadradas de um, dois e três são aproximadamente um (usamos inteiros, portanto ignoramos a fração).

1 }
2 }
3}
Copiar

Frações de Ponto Fixo (UQ112x112)

Essa biblioteca lida com frações, que normalmente não fazem parte da aritmética do Ethereum. Ele faz isso codificando o número x como x*2^112. Isso nos permite usar os opcodes originais de adição e subtração sem alterações.

1pragma solidity =0.5.16;
2
3// a library for handling binary fixed point numbers (https://wikipedia.org/wiki/Q_(number_format))
4
5// range: [0, 2**112 - 1]
6// resolution: 1 / 2**112
7
8library UQ112x112 {
9 uint224 constant Q112 = 2**112;
Exibir tudo
Copiar

Q112 é a codificação para um.

1 // encode a uint112 as a UQ112x112
2 function encode(uint112 y) internal pure returns (uint224 z) {
3 z = uint224(y) * Q112; // never overflows
4 }
Copiar

Porque y é uint112, o máximo pode ser 2^112-1. Esse número ainda pode ser codificado como um UQ112x112.

1 // divide a UQ112x112 by a uint112, returning a UQ112x112
2 function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {
3 z = x / uint224(y);
4 }
5}
Copiar

Se dividirmos dois valores UQ112x112, o resultado não será mais multiplicado por 2^112. Então, em vez disso, pegamos um inteiro como denominador. Teríamos que usar um truque semelhante para fazer a multiplicação, mas não precisamos fazer a multiplicação dos valores UQ112x112.

UniswapV2Library

Esta biblioteca é usada somente pelos contratos periféricos

1pragma solidity >=0.5.0;
2
3import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';
4
5import "./SafeMath.sol";
6
7library UniswapV2Library {
8 using SafeMath for uint;
9
10 // returns sorted token addresses, used to handle return values from pairs sorted in this order
11 function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
12 require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES');
13 (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
14 require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS');
15 }
Exibir tudo
Copiar

Ordene os dois tokens por endereço, a fim de obter o endereço do par de troca para eles. Isso é necessário porque, caso contrário, teríamos duas possibilidades, uma para os parâmetros A,B e outro para os parâmetros B,A, levando a duas trocas em vez de uma.

1 // calculates the CREATE2 address for a pair without making any external calls
2 function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {
3 (address token0, address token1) = sortTokens(tokenA, tokenB);
4 pair = address(uint(keccak256(abi.encodePacked(
5 hex'ff',
6 factory,
7 keccak256(abi.encodePacked(token0, token1)),
8 hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // init code hash
9 ))));
10 }
Exibir tudo
Copiar

Essa função calcula o endereço do par de troca para os dois tokens. Este contrato é criado usando o opcode CREATE2(opens in a new tab), para que possamos calcular o endereço usando o mesmo algoritmo se soubermos os parâmetros que ele usa. Isso é muito mais barato do que pedir à fábrica, e

1 // fetches and sorts the reserves for a pair
2 function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {
3 (address token0,) = sortTokens(tokenA, tokenB);
4 (uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();
5 (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
6 }
Copiar

Essa função retorna as reservas dos dois tokens que o par de troca tem. Observe que ele pode receber os tokens em qualquer uma das ordens e classificá-los para uso interno.

1 // given some amount of an asset and pair reserves, returns an equivalent amount of the other asset
2 function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
3 require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');
4 require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
5 amountB = amountA.mul(reserveB) / reserveA;
6 }
Copiar

Esta função fornece a quantidade de tokens B que você receberá em retorno pelo token A se não houver taxas envolvidas. Este cálculo considera que a transferência altera a taxa de câmbio.

1 // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset
2 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
Copiar

A função quote acima funciona muito bem se não houver nenhuma taxa para usar o par de troca. No entanto, se houver uma taxa de câmbio de 0,3%, a quantidade que você realmente receberá é menor. Essa função calcula o valor após a taxa de câmbio.

1
2 require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
3 require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
4 uint amountInWithFee = amountIn.mul(997);
5 uint numerator = amountInWithFee.mul(reserveOut);
6 uint denominator = reserveIn.mul(1000).add(amountInWithFee);
7 amountOut = numerator / denominator;
8 }
Copiar

O Solidity não lida com funções nativamente, por isso, não podemos multiplicar a quantia por 0,997. Em vez disso, multiplicamos o numerador por 997 e o denominador por 1.000, atingindo o mesmo efeito.

1 // given an output amount of an asset and pair reserves, returns a required input amount of the other asset
2 function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {
3 require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');
4 require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
5 uint numerator = reserveIn.mul(amountOut).mul(1000);
6 uint denominator = reserveOut.sub(amountOut).mul(997);
7 amountIn = (numerator / denominator).add(1);
8 }
Copiar

Essa função faz aproximadamente a mesma coisa, mas obtém o valor de saída e fornece a entrada.

1
2 // performs chained getAmountOut calculations on any number of pairs
3 function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {
4 require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
5 amounts = new uint[](path.length);
6 amounts[0] = amountIn;
7 for (uint i; i < path.length - 1; i++) {
8 (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);
9 amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
10 }
11 }
12
13 // performs chained getAmountIn calculations on any number of pairs
14 function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) {
15 require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
16 amounts = new uint[](path.length);
17 amounts[amounts.length - 1] = amountOut;
18 for (uint i = path.length - 1; i > 0; i--) {
19 (uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]);
20 amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);
21 }
22 }
23}
Exibir tudo
Copiar

Essas duas funções lidam com a identificação dos valores quando é necessário passar por vários pares de troca.

Auxiliar de Transferência

Esta biblioteca(opens in a new tab) adiciona verificações de sucesso em torno das transferências ERC-20 e Ethereum para tratar uma reversão e um valor de retorno false da mesma maneira.

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3pragma solidity >=0.6.0;
4
5// helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return true/false
6library TransferHelper {
7 function safeApprove(
8 address token,
9 address to,
10 uint256 value
11 ) internal {
12 // bytes4(keccak256(bytes('approve(address,uint256)')));
13 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value));
14
Exibir tudo
Copiar

Podemos chamar um contrato diferente de uma das duas maneiras:

1 require(
2 success && (data.length == 0 || abi.decode(data, (bool))),
3 'TransferHelper::safeApprove: approve failed'
4 );
5 }
Copiar

Por uma questão de compatibilidade com versões anteriores dos tokens criados antes do padrão ERC-20, uma chamada ERC-20 pode falhar revertendo (nesse caso sucess é false) ou sendo bem-sucedido e retornando um valor false (nesse caso, há dados de saída e, se você decodificá-los como um booleano, obterá false).

1
2
3 function safeTransfer(
4 address token,
5 address to,
6 uint256 value
7 ) internal {
8 // bytes4(keccak256(bytes('transfer(address,uint256)')));
9 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value));
10 require(
11 success && (data.length == 0 || abi.decode(data, (bool))),
12 'TransferHelper::safeTransfer: transfer failed'
13 );
14 }
Exibir tudo
Copiar

Essa função implementa a funcionalidade de transferência do ERC-20(opens in a new tab), que permite que uma conta gaste o valor permitido por uma conta diferente.

1
2 function safeTransferFrom(
3 address token,
4 address from,
5 address to,
6 uint256 value
7 ) internal {
8 // bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));
9 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));
10 require(
11 success && (data.length == 0 || abi.decode(data, (bool))),
12 'TransferHelper::transferFrom: transferFrom failed'
13 );
14 }
Exibir tudo
Copiar

Essa função implementa a funcionalidade de transferência do ERC-20(opens in a new tab), que permite que uma conta gaste o valor permitido por uma conta diferente.

1
2 function safeTransferETH(address to, uint256 value) internal {
3 (bool success, ) = to.call{value: value}(new bytes(0));
4 require(success, 'TransferHelper::safeTransferETH: ETH transfer failed');
5 }
6}
Copiar

Essa função transfere ether para uma conta. Qualquer chamada a um contrato diferente pode tentar enviar ether. Como nós não precisamos chamar nenhuma função, nós não enviamos nenhum dado com a chamada.

Conclusão

Este é um longo artigo de cerca de 50 páginas. Se você chegou até aqui, parabéns! Esperamos que agora você tenha entendido as considerações a ter em mente ao escrever um aplicativo real (em oposição aos curtos programas de exemplo) e consiga melhor escrever contratos para seus próprios casos de uso.

Agora vá escrever algo interessante e nos surpreenda.

Última edição: @Fritzhoy(opens in a new tab), 2 de abril de 2024

Este tutorial foi útil?