title | tags | |||
---|---|---|---|---|
56. Exchange Descentralizada |
|
Recentemente, tenho estudado Solidity novamente para revisar os detalhes e escrever um "WTF Introdução Simples ao Solidity" para iniciantes (programadores experientes podem procurar outros tutoriais). Serão lançadas de 1 a 3 aulas por semana.
Twitter: @0xAA_Science
Comunidade: Discord|Grupo WeChat|Site oficial wtf.academy
Todo o código e tutoriais estão disponíveis no GitHub: github.com/AmazingAng/WTFSolidity
Nesta aula, vamos apresentar o Constant Product Automated Market Maker (CPAMM), que é o mecanismo central das exchanges descentralizadas (DEX) como Uniswap, PancakeSwap, entre outras. O contrato de ensino é uma simplificação do contrato Uniswap-v2, incluindo as funcionalidades centrais do CPAMM.
O Market Maker Automatizado (Automated Market Maker, AMM) é um algoritmo ou um contrato inteligente que permite a negociação descentralizada de ativos digitais. A introdução do AMM criou uma nova forma de negociação, sem a necessidade de correspondência de pedidos entre compradores e vendedores tradicionais, mas sim através de uma fórmula matemática predefinida (como a fórmula do produto constante) que cria uma pool de liquidez, permitindo que os usuários negociem a qualquer momento.
A seguir, vamos usar o mercado de Coca-Cola ($COLA) e dólar ($USD) como exemplo para explicar o AMM. Para facilitar, vamos definir os seguintes símbolos:
O Market Maker Automatizado com Soma Constante (Constant Sum Automated Market Maker, CSAMM) é o modelo mais simples de AMM, e vamos começar por ele. A restrição durante a negociação é:
onde
A vantagem do CSAMM é que ele garante que o preço relativo dos tokens permaneça constante, o que é importante em trocas de stablecoins, onde todos esperam que 1 USDT possa sempre ser trocado por 1 USDC. No entanto, a desvantagem é que a liquidez é facilmente esgotada: eu só preciso de $10 para esgotar a liquidez de Coca-Cola no mercado, e outros usuários que queiram comprar Coca-Cola não poderão mais fazer isso.
A seguir, vamos apresentar o Market Maker Automatizado com Produto Constante, que possui "liquidez infinita".
O Market Maker Automatizado com Produto Constante (Constant Product Automated Market Maker, CPAMM) é o modelo mais popular de AMM, e foi adotado pela primeira vez pelo Uniswap. A restrição durante a negociação é:
onde
A vantagem do CPAMM é que ele possui "liquidez infinita": o preço relativo dos tokens varia de acordo com as compras e vendas, e quanto mais escasso for um token, maior será o seu preço relativo, evitando que a liquidez seja esgotada. No exemplo acima, a negociação fez com que o preço da Coca-Cola subisse de $1 para $4 por garrafa, evitando que a Coca-Cola fosse comprada até esgotar a liquidez do mercado.
A seguir, vamos construir uma exchange descentralizada extremamente simples baseada no CPAMM.
Agora, vamos escrever um contrato chamado SimpleSwap
que representa uma exchange descentralizada, permitindo que os usuários negociem um par de tokens.
SimpleSwap
herda o padrão de contrato ERC20 para facilitar o registro da liquidez fornecida pelos provedores de liquidez. No construtor, especificamos os endereços dos dois tokens que a exchange suporta. reserve0
e reserve1
registram as reservas dos tokens no contrato.
contract SimpleSwap is ERC20 {
// Contrato do token
IERC20 public token0;
IERC20 public token1;
// Reservas dos tokens
uint public reserve0;
uint public reserve1;
// Construtor, inicializa os endereços dos tokens
constructor(IERC20 _token0, IERC20 _token1) ERC20("SimpleSwap", "SS") {
token0 = _token0;
token1 = _token1;
}
}
A exchange tem dois tipos de participantes: provedores de liquidez (Liquidity Providers, LP) e traders. A seguir, vamos implementar as funcionalidades para cada um desses participantes.
Os provedores de liquidez fornecem liquidez ao mercado, permitindo que os traders obtenham melhores preços e liquidez, e recebem uma taxa em troca.
Primeiro, precisamos implementar a funcionalidade de adicionar liquidez. Quando um usuário adiciona liquidez à pool de tokens, o contrato deve registrar a participação do LP. De acordo com o Uniswap V2, a participação do LP é calculada da seguinte forma:
-
Quando a pool de tokens é adicionada pela primeira vez, a participação do LP
$\Delta{L}$ é determinada pela raiz quadrada do produto das quantidades de tokens adicionadas:$$\Delta{L}=\sqrt{\Delta{x} *\Delta{y}}$$ -
Quando a liquidez é adicionada posteriormente, a participação do LP é determinada pela proporção das quantidades de tokens adicionadas em relação às reservas dos tokens (a proporção é calculada para cada token e a menor proporção é usada):
$$\Delta{L}=L*\min{(\frac{\Delta{x}}{x}, \frac{\Delta{y}}{y})}$$
Como o contrato SimpleSwap
herda o padrão ERC20, após calcular a participação do LP, podemos emitir tokens para o usuário representando sua participação.
A função addLiquidity()
a seguir implementa a funcionalidade de adicionar liquidez, com as seguintes etapas:
- Transferir os tokens adicionados pelo usuário para o contrato. O usuário precisa aprovar o contrato antecipadamente.
- Calcular a participação de liquidez adicionada e verificar a quantidade de tokens a serem emitidos.
- Atualizar as reservas dos tokens no contrato.
- Emitir tokens LP para o provedor de liquidez.
- Emitir o evento
Mint
.
event Mint(address indexed sender, uint amount0, uint amount1);
// Adicionar liquidez, transferir tokens e emitir tokens LP
// @param amount0Desired Quantidade de token0 a ser adicionada
// @param amount1Desired Quantidade de token1 a ser adicionada
function addLiquidity(uint amount0Desired, uint amount1Desired) public returns(uint liquidity){
// Transferir a liquidez adicionada para o contrato Swap, o usuário precisa aprovar o contrato Swap antecipadamente
token0.transferFrom(msg.sender, address(this), amount0Desired);
token1.transferFrom(msg.sender, address(this), amount1Desired);
// Calcular a liquidez adicionada
uint _totalSupply = totalSupply();
if (_totalSupply == 0) {
// Se for a primeira vez que a liquidez é adicionada, emitir tokens LP (liquidity provider) na quantidade de L = sqrt(x * y)
liquidity = sqrt(amount0Desired * amount1Desired);
} else {
// Se não for a primeira vez que a liquidez é adicionada, emitir tokens LP com base na proporção das quantidades de tokens adicionadas, usando a menor proporção entre os dois tokens
liquidity = min(amount0Desired * _totalSupply / reserve0, amount1Desired * _totalSupply /reserve1);
}
// Verificar a quantidade de tokens LP emitidos
require(liquidity > 0, 'INSUFFICIENT_LIQUIDITY_MINTED');
// Atualizar as reservas dos tokens
reserve0 = token0.balanceOf(address(this));
reserve1 = token1.balanceOf(address(this));
// Emitir tokens LP para o provedor de liquidez, representando a liquidez fornecida
_mint(msg.sender, liquidity);
emit Mint(msg.sender, amount0Desired, amount1Desired);
}
A seguir, precisamos implementar a funcionalidade de remover liquidez. Quando um usuário remove uma quantidade
A função removeLiquidity()
a seguir implementa a funcionalidade de remover liquidez, com as seguintes etapas:
- Obter o saldo dos tokens no contrato.
- Calcular a quantidade de tokens a serem transferidos com base na proporção dos tokens LP.
- Verificar a quantidade de tokens.
- Queimar os tokens LP.
- Transferir os tokens correspondentes para o usuário.
- Atualizar as reservas dos tokens no contrato.
- Emitir o evento
Burn
.
// Remover liquidez, queimar tokens LP e transferir tokens
// Quantidade a ser transferida = (liquidity / totalSupply_LP) * reserve
// @param liquidity Quantidade de liquidez a ser removida
function removeLiquidity(uint liquidity) external returns (uint amount0, uint amount1) {
// Obter o saldo
uint balance0 = token0.balanceOf(address(this));
uint balance1 = token1.balanceOf(address(this));
// Calcular a quantidade de tokens a serem transferidos com base na proporção dos tokens LP
uint _totalSupply = totalSupply();
amount0 = liquidity * balance0 / _totalSupply;
amount1 = liquidity * balance1 / _totalSupply;
// Verificar a quantidade de tokens
require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED');
// Queimar os tokens LP
_burn(msg.sender, liquidity);
// Transferir os tokens
token0.transfer(msg.sender, amount0);
token1.transfer(msg.sender, amount1);
// Atualizar as reservas dos tokens
reserve0 = token0.balanceOf(address(this));
reserve1 = token1.balanceOf(address(this));
emit Burn(msg.sender, amount0, amount1);
}
Agora, as funcionalidades relacionadas aos provedores de liquidez estão concluídas. A seguir, vamos implementar as funcionalidades de negociação.
Na exchange SimpleSwap
, os usuários podem trocar um token por outro. Quanto de token1 posso obter ao trocar
De acordo com a fórmula do produto constante, antes da negociação:
Após a negociação, temos:
Como o valor de
Portanto, a quantidade de token1
A função getAmountOut()
a seguir implementa o cálculo da quantidade de tokens a serem obtidos, dado um valor de entrada e as reservas dos tokens.
// Dado um valor de entrada e as reservas dos tokens, calcular a quantidade de tokens a serem obtidos
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) public pure returns (uint amountOut) {
require(amountIn > 0, 'INSUFFICIENT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'INSUFFICIENT_LIQUIDITY');
amountOut = amountIn * reserveOut / (reserveIn + amountIn);
}
Com essa fórmula central, podemos implementar a funcionalidade de negociação. A função swap()
a seguir implementa a funcionalidade de troca de tokens, com as seguintes etapas:
- O usuário especifica a quantidade de tokens a serem trocados, o endereço do token a ser trocado e a quantidade mínima do outro token a ser obtida.
- Verificar se é uma troca de token0 por token1 ou de token1 por token0.
- Usar a fórmula acima para calcular a quantidade de tokens a serem obtidos.
- Verificar se a quantidade de tokens obtidos atende à quantidade mínima especificada pelo usuário (semelhante ao slippage em uma negociação).
- Transferir os tokens do usuário para o contrato.
- Transferir os tokens trocados do contrato para o usuário.
- Atualizar as reservas dos tokens no contrato.
- Emitir o evento
Swap
.
// Trocar tokens
// @param amountIn Quantidade de tokens a serem trocados
// @param tokenIn Endereço do token a ser trocado
// @param amountOutMin Quantidade mínima do outro token a ser obtida
function swap(uint amountIn, IERC20 tokenIn, uint amountOutMin) external returns (uint amountOut, IERC20 tokenOut){
require(amountIn > 0, 'INSUFFICIENT_OUTPUT_AMOUNT');
require(tokenIn == token0 || tokenIn == token1, 'INVALID_TOKEN');
uint balance0 = token0.balanceOf(address(this));
uint balance1 = token1.balanceOf(address(this));
if(tokenIn == token0){
// Se for uma troca de token0 por token1
tokenOut = token1;
// Calcular a quantidade de token1 a ser obtida
amountOut = getAmountOut(amountIn, balance0, balance1);
require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT');
// Realizar a troca
tokenIn.transferFrom(msg.sender, address(this), amountIn);
tokenOut.transfer(msg.sender, amountOut);
}else{
// Se for uma troca de token1 por token0
tokenOut = token0;
// Calcular a quantidade de token1 a ser obtida
amountOut = getAmountOut(amountIn, balance1, balance0);
require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT');
// Realizar a troca
tokenIn.transferFrom(msg.sender, address(this), amountIn);
tokenOut.transfer(msg.sender, amountOut);
}
// Atualizar as reservas dos tokens
reserve0 = token0.balanceOf(address(this));
reserve1 = token1.balanceOf(address(this));
(tokenIn), amountOut, address(tokenOut));
}
O código completo do SimpleSwap
é o seguinte:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract SimpleSwap is ERC20 {
// Contrato do token
IERC20 public token0;
IERC20 public token1;
// Reservas dos tokens
uint public reserve0;
uint public reserve1;
// Eventos
event Mint(address indexed sender, uint amount0, uint amount1);
event Burn(address indexed sender, uint amount0, uint amount1);
event Swap(
address indexed sender,
uint amountIn,
address tokenIn,
uint amountOut,
address tokenOut
);
// Construtor, inicializa os endereços dos tokens
constructor(IERC20 _token0, IERC20 _token1) ERC20("SimpleSwap", "SS") {
token0 = _token0;
token1 = _token1;
}
// Função auxiliar para retornar o menor valor entre dois números
function min(uint x, uint y) internal pure returns (uint z) {
z = x < y ? x : y;
}
// Função auxiliar para calcular a raiz quadrada usando o método babilônico (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)
function sqrt(uint y) internal pure returns (uint z) {
if (y > 3) {
z = y;
uint x = y / 2 + 1;
while (x < z) {
z = x;
x = (y / x + x) / 2;
}
} else if (y != 0) {
z = 1;
}
}
// Adicionar liquidez, transferir tokens e emitir tokens LP
// @param amount0Desired Quantidade de token0 a ser adicionada
// @param amount1Desired Quantidade de token1 a ser adicionada
function addLiquidity(uint amount0Desired, uint amount1Desired) public returns(uint liquidity){
// Transferir a liquidez adicionada para o contrato Swap, o usuário precisa aprovar o contrato Swap antecipadamente
token0.transferFrom(msg.sender, address(this), amount0Desired);
token1.transferFrom(msg.sender, address(this), amount1Desired);
// Calcular a liquidez adicionada
uint _totalSupply = totalSupply();
if (_totalSupply == 0) {
// Se for a primeira vez que a liquidez é adicionada, emitir tokens LP (liquidity provider) na quantidade de L = sqrt(x * y)
liquidity = sqrt(amount0Desired * amount1Desired);
} else {
// Se não for a primeira vez que a liquidez é adicionada, emitir tokens LP com base na proporção das quantidades de tokens adicionadas, usando a menor proporção entre os dois tokens
liquidity = min(amount0Desired * _totalSupply / reserve0, amount1Desired * _totalSupply /reserve1);
}
// Verificar a quantidade de tokens LP emitidos
require(liquidity > 0, 'INSUFFICIENT_LIQUIDITY_MINTED');
// Atualizar as reservas dos tokens
reserve0 = token0.balanceOf(address(this));
reserve1 = token1.balanceOf(address(this));
// Emitir tokens LP para o provedor de liquidez, representando a liquidez fornecida
_mint(msg.sender, liquidity);
emit Mint(msg.sender, amount0Desired, amount1Desired);
}
// Remover liquidez, queimar tokens LP e transferir tokens
// Quantidade a ser transferida = (liquidity / totalSupply_LP) * reserve
// @param liquidity Quantidade de liquidez a ser removida
function removeLiquidity(uint liquidity) external returns (uint amount0, uint amount1) {
// Obter o saldo
uint balance0 = token0.balanceOf(address(this));
uint balance1 = token1.balanceOf(address(this));
// Calcular a quantidade de tokens a serem transferidos com base na proporção dos tokens LP
uint _totalSupply = totalSupply();
amount0 = liquidity * balance0 / _totalSupply;
amount1 = liquidity * balance1 / _totalSupply;
// Verificar a quantidade de tokens
require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED');
// Queimar os tokens LP
_burn(msg.sender, liquidity);
// Transferir os tokens
token0.transfer(msg.sender, amount0);
token1.transfer(msg.sender, amount1);
// Atualizar as reservas dos tokens
reserve0 = token0.balanceOf(address(this));
reserve1 = token1.balanceOf(address(this));
emit Burn(msg.sender, amount0, amount1);
}
// Dado um valor de entrada e as reservas dos tokens, calcular a quantidade de tokens a serem obtidos
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) public pure returns (uint amountOut) {
require(amountIn > 0, 'INSUFFICIENT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'INSUFFICIENT_LIQUIDITY');
amountOut = amountIn * reserveOut / (reserveIn + amountIn);
}
// Trocar tokens
// @param amountIn Quantidade de tokens a serem trocados
// @param tokenIn Endereço do token a ser trocado
// @param amountOutMin Quantidade mínima do outro token a ser obtida
function swap(uint amountIn, IERC20 tokenIn, uint amountOutMin) external returns (uint amountOut, IERC20 tokenOut){
require(amountIn > 0, 'INSUFFICIENT_OUTPUT_AMOUNT');
require(tokenIn == token0 || tokenIn == token1, 'INVALID_TOKEN');
uint balance0 = token0.balanceOf(address(this));
uint balance1 = token1.balanceOf(address(this));
if(tokenIn == token0){
// Se for uma troca de token0 por token1
tokenOut = token1;
// Calcular a quantidade de token1 a ser obtida
amountOut = getAmountOut(amountIn, balance0, balance1);
require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT');
// Realizar a troca
tokenIn.transferFrom(msg.sender, address(this), amountIn);
tokenOut.transfer(msg.sender, amountOut);
}else{
// Se for uma troca de token1 por token0
tokenOut = token0;
// Calcular a quantidade de token1 a ser obtida
amountOut = getAmountOut(amountIn, balance1, balance0);
require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT');
// Realizar a troca
tokenIn.transferFrom(msg.sender, address(this), amountIn);
tokenOut.transfer(msg.sender, amountOut);
}
// Atualizar as reservas dos tokens
reserve0 = token0.balanceOf(address(this));
reserve1 = token1.balanceOf(address(this));
emit Swap(msg.sender, amountIn, address(tokenIn), amountOut, address(tokenOut));
}
}
-
Implante dois contratos ERC20 (token0 e token1) e registre seus endereços de contrato.
-
Implante o contrato
SimpleSwap
e preencha os endereços dos tokens acima. -
Chame a função
approve()
dos contratos ERC20 para permitir que o contratoSimpleSwap
gaste 1000 unidades de cada token. -
Chame a função
addLiquidity()
do contratoSimpleSwap
para adicionar liquidez à exchange. Adicione 100 unidades de cada token. -
Chame a função
balanceOf()
do contratoSimpleSwap
para verificar a participação do LP. Deve ser 100. ($\sqrt{100*100}=100$ ) -
Chame a função
swap()
do contratoSimpleSwap
para realizar uma troca de tokens. Use 100 unidades do token0. -
Chame as funções
reserve0
ereserve1
do contratoSimpleSwap
para verificar as reservas de tokens no contrato. Deve ser 200 e 50, respectivamente. Na etapa anterior, usamos 100 unidades do token0 para trocar por 50 unidades do token1 ($\frac{100*100}{100+100}=50$ ).
Nesta aula, apresentamos o Market Maker Automatizado com Produto Constante e escrevemos uma exchange descentralizada extremamente simples. No contrato SimpleSwap
, há muitos aspectos que não foram considerados, como taxas de negociação e governança. Se você estiver interessado em exchanges descentralizadas, recomendamos a leitura de Programming DeFi: Uniswap V2 e Uniswap v3 book para um estudo mais aprofundado.