Como criar um sistema de Swap estável AMM

porMatheusem23/07/2022

Nesse artigo iremos aprender a como criar um sistema de Swap estável AMM através de contrato inteligente.

Sistema de Swap estável AMM

Versão simplificada do AMM de swap estável da Curve

Como funciona?
Invariante - preço de negociação e quantidade de liquidez são determinados por esta equação.

An^n soma(x_i) + D = ADn^n + D^(n + 1) / (n^n prod(x_i))

Tópicos:
0. Método de Newton x_(n + 1) = x_n - f(x_n) / f'(x_n)

  1. Invariante
  2. Trocar
    • Calcular Y
    • Calcular D
  3. Obtenha o preço virtual
  4. Adicione liquidez
    • Taxa de desequilíbrio
  5. Remova a liquidez
  6. Remova um token de liquidez
    • Calcular retirar um token
    • getYD
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
library Math {
function abs(uint x, uint y) internal pure returns (uint) {
return x >= y ? x - y : y - x;
}
}
contract StableSwap {
// Número de tokens
uint private constant N = 3;
// Coeficiente de amplificação multiplicado por N^(N - 1)
// Maior valor torna a curva mais plana
// O valor mais baixo torna a curva mais parecida com um produto constante AMM
uint private constant A = 1000 * (N**(N - 1));
// 0.03%
uint private constant SWAP_FEE = 300;
// A taxa de liquidez é derivada de 2 restrições:
// 1. A taxa é 0 para adicionar/remover liquidez que resulta em um pool equilibrado
// 2. Trocar em um pool equilibrado é como adicionar e remover liquidez
// de um pool balanceado
// taxa de swap = adiciona taxa de liquidez + remove taxa de liquidez
uint private constant LIQUIDITY_FEE = (SWAP_FEE * N) / (4 * (N - 1));
uint private constant FEE_DENOMINATOR = 1e6;
address[N] public tokens;
// Normalize cada token para 18 decimais
// Exemplo - DAI (18 decimals), USDC (6 decimals), USDT (6 decimals)
uint[N] private multipliers = [1, 1e12, 1e12];
uint[N] public balances;
// 1 share = 1e18, 18 decimals
uint private constant DECIMALS = 18;
uint public totalSupply;
mapping(address => uint) public balanceOf;
function _mint(address _to, uint _amount) private {
balanceOf[_to] += _amount;
totalSupply += _amount;
}
function _burn(address _from, uint _amount) private {
balanceOf[_from] -= _amount;
totalSupply -= _amount;
}
// Retorna saldos ajustados com precisão, ajustados para 18 casas decimais
function _xp() private view returns (uint[N] memory xp) {
for (uint i; i < N; ++i) {
xp[i] = balances[i] * multipliers[i];
}
}
/**
* @notice Calcule D, soma dos saldos em um pool perfeitamente equilibrado
* Se os saldos de x_0, x_1, ... x_(n-1) então soma(x_i) = D
* @param xp Saldos ajustados com precisão
* @return D
*/
function _getD(uint[N] memory xp) private pure returns (uint) {
/*
O método de Newton para calcular D
-----------------------------
f(D) = ADn^n + D^(n + 1) / (n^n prod(x_i)) - An^n sum(x_i) - D
f'(D) = An^n + (n + 1) D^n / (n^n prod(x_i)) - 1
(as + np)D_n
D_(n+1) = -----------------------
(a - 1)D_n + (n + 1)p
a = An^n
s = sum(x_i)
p = (D_n)^(n + 1) / (n^n prod(x_i))
*/
uint a = A * N; // An^n
uint s; // x_0 + x_1 + ... + x_(n-1)
for (uint i; i < N; ++i) {
s += xp[i];
}
// O método de Newton
// Suposição inicial, d <= s
uint d = s;
uint d_prev;
for (uint i; i < 255; ++i) {
// p = D^(n + 1) / (n^n * x_0 * ... * x_(n-1))
uint p = d;
for (uint j; j < N; ++j) {
p = (p * d) / (N * xp[j]);
}
d_prev = d;
d = ((a * s + N * p) * d) / ((a - 1) * d + (N + 1) * p);
if (Math.abs(d, d_prev) <= 1) {
return d;
}
}
revert("D não convergiu");
}
/**
* @notice Calcule o novo saldo do token j dado o novo saldo do token i
* @param i Índice do token de entrada
* @param j Índice de token de saída
* @param x Novo saldo do token i
* @param xp Saldos atuais ajustados com precisão
*/
function _getY(
uint i,
uint j,
uint x,
uint[N] memory xp
) private pure returns (uint) {
/*
Método de Newton para calcular y
-----------------------------
y = x_j
f(y) = y^2 + y(b - D) - c
y_n^2 + c
y_(n+1) = --------------
2y_n + b - D
onde
s = sum(x_k), k != j
p = prod(x_k), k != j
b = s + D / (An^n)
c = D^(n + 1) / (n^n * p * An^n)
*/
uint a = A * N;
uint d = _getD(xp);
uint s;
uint c = d;
uint _x;
for (uint k; k < N; ++k) {
if (k == i) {
_x = x;
} else if (k == j) {
continue;
} else {
_x = xp[k];
}
s += _x;
c = (c * d) / (N * _x);
}
c = (c * d) / (N * a);
uint b = s + d / a;
// O método de Newton
uint y_prev;
// Suposição inicial, y <= d
uint y = d;
for (uint _i; _i < 255; ++_i) {
y_prev = y;
y = (y * y + c) / (2 * y + b - d);
if (Math.abs(y, y_prev) <= 1) {
return y;
}
}
revert("vc não convergiu");
}
/**
* @notice Calcule o novo saldo do token i dado ajustado com precisão
* saldos xp e liquidez d
* @dev Equação é calcular y é igual a _getY
* @param i Índice do token para calcular o novo saldo
* @param xp Saldos ajustados com precisão
* @param d Liquidez d
* @return Novo saldo do token i
*/
function _getYD(
uint i,
uint[N] memory xp,
uint d
) private pure returns (uint) {
uint a = A * N;
uint s;
uint c = d;
uint _x;
for (uint k; k < N; ++k) {
if (k != i) {
_x = xp[k];
} else {
continue;
}
s += _x;
c = (c * d) / (N * _x);
}
c = (c * d) / (N * a);
uint b = s + d / a;
// O método de Newton
uint y_prev;
// Suposição inicial, y <= d
uint y = d;
for (uint _i; _i < 255; ++_i) {
y_prev = y;
y = (y * y + c) / (2 * y + b - d);
if (Math.abs(y, y_prev) <= 1) {
return y;
}
}
revert("vc não convergiu");
}
// Valor estimado de 1 ação
// Quantos tokens vale uma ação?
function getVirtualPrice() external view returns (uint) {
uint d = _getD(_xp());
uint _totalSupply = totalSupply;
if (_totalSupply > 0) {
return (d * 10**DECIMALS) / _totalSupply;
}
return 0;
}
/**
* @notice Troque a quantidade dx do token i pelo token j
* @param i Índice do token em
* @param j Índice de token out
* @param dx Token em quantidade
* @param minDy Saída mínima do token
*/
function swap(
uint i,
uint j,
uint dx,
uint minDy
) external returns (uint dy) {
require(i != j, "i = j");
IERC20(tokens[i]).transferFrom(msg.sender, address(this), dx);
// Calcula dy
uint[N] memory xp = _xp();
uint x = xp[i] + dx * multipliers[i];
uint y0 = xp[j];
uint y1 = _getY(i, j, x, xp);
// y0 deve ser >= y1, pois x aumentou
// -1 para arredondar para baixo
dy = (y0 - y1 - 1) / multipliers[j];
// Subtrair taxa (fee) de dy
uint fee = (dy * SWAP_FEE) / FEE_DENOMINATOR;
dy -= fee;
require(dy >= minDy, "dy < min");
balances[i] += dx;
balances[j] -= dy;
IERC20(tokens[j]).transfer(msg.sender, dy);
}
function addLiquidity(uint[N] calldata amounts, uint minShares)
external
returns (uint shares)
{
// calcular a liquidez atual d0
uint _totalSupply = totalSupply;
uint d0;
uint[N] memory old_xs = _xp();
if (_totalSupply > 0) {
d0 = _getD(old_xs);
}
// Transferir tokens em
uint[N] memory new_xs;
for (uint i; i < N; ++i) {
uint amount = amounts[i];
if (amount > 0) {
IERC20(tokens[i]).transferFrom(msg.sender, address(this), amount);
new_xs[i] = old_xs[i] + amount * multipliers[i];
} else {
new_xs[i] = old_xs[i];
}
}
// Calcular nova liquidez d1
uint d1 = _getD(new_xs);
require(d1 > d0, "a liquidez não aumentou");
// Recalcular D contabilizando a taxa de desequilíbrio
uint d2;
if (_totalSupply > 0) {
for (uint i; i < N; ++i) {
// TODO: por que old_xs[i] * d1 / d0? por que não d1 / N?
uint idealBalance = (old_xs[i] * d1) / d0;
uint diff = Math.abs(new_xs[i], idealBalance);
new_xs[i] -= (LIQUIDITY_FEE * diff) / FEE_DENOMINATOR;
}
d2 = _getD(new_xs);
} else {
d2 = d1;
}
// Atualizar saldos
for (uint i; i < N; ++i) {
balances[i] += amounts[i];
}
// Shares para cunhar = (d2 - d0) / d0 * total supply
// d1 >= d2 >= d0
if (_totalSupply > 0) {
shares = ((d2 - d0) * _totalSupply) / d0;
} else {
shares = d2;
}
require(shares >= minShares, "shares < min");
_mint(msg.sender, shares);
}
function removeLiquidity(uint shares, uint[N] calldata minAmountsOut)
external
returns (uint[N] memory amountsOut)
{
uint _totalSupply = totalSupply;
for (uint i; i < N; ++i) {
uint amountOut = (balances[i] * shares) / _totalSupply;
require(amountOut >= minAmountsOut[i], "amountOut < min");
balances[i] -= amountOut;
amountsOut[i] = amountOut;
IERC20(tokens[i]).transfer(msg.sender, amountOut);
}
_burn(msg.sender, shares);
}
/**
* @notice Calcular a quantidade de token i para receber por ações
* @param shares Ações para queimar
* @param i Índice do token a ser retirado
* @return dy Quantidade de token i a receber
* fee: Taxa para retirada. Taxa já incluída no dy
*/
function _calcWithdrawOneToken(uint shares, uint i)
private
view
returns (uint dy, uint fee)
{
uint _totalSupply = totalSupply;
uint[N] memory xp = _xp();
// Calcule d0 e d1
uint d0 = _getD(xp);
uint d1 = d0 - (d0 * shares) / _totalSupply;
// Calcule a redução em y se D = d1
uint y0 = _getYD(i, xp, d1);
// d1 <= d0 então vc deve ser <= xp[i]
uint dy0 = (xp[i] - y0) / multipliers[i];
// Calcule a taxa de desequilíbrio, atualize o xp com taxas
uint dx;
for (uint j; j < N; ++j) {
if (j == i) {
dx = (xp[j] * d1) / d0 - y0;
} else {
// d1 / d0 <= 1
dx = xp[j] - (xp[j] * d1) / d0;
}
xp[j] -= (LIQUIDITY_FEE * dx) / FEE_DENOMINATOR;
}
// Recalcular y com xp incluindo taxas de desequilíbrio
uint y1 = _getYD(i, xp, d1);
// - 1 arredondar para baixo
dy = (xp[i] - y1 - 1) / multipliers[i];
fee = dy0 - dy;
}
function calcWithdrawOneToken(uint shares, uint i)
external
view
returns (uint dy, uint fee)
{
return _calcWithdrawOneToken(shares, i);
}
/**
* @notice Retirar liquidez no token i
* @param shares Ações para queimar
* @param i Token para retirar
* @param minAmountOut Quantidade mínima de token i que deve ser retirada
*/
function removeLiquidityOneToken(
uint shares,
uint i,
uint minAmountOut
) external returns (uint amountOut) {
(amountOut, ) = _calcWithdrawOneToken(shares, i);
require(amountOut >= minAmountOut, "amountOut < min");
balances[i] -= amountOut;
_burn(msg.sender, shares);
IERC20(tokens[i]).transfer(msg.sender, amountOut);
}
}
interface IERC20 {
function totalSupply() external view returns (uint);
function balanceOf(address account) external view returns (uint);
function transfer(address recipient, uint amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint);
function approve(address spender, uint amount) external returns (bool);
function transferFrom(
address sender,
address recipient,
uint amount
) external returns (bool);
event Transfer(address indexed from, address indexed to, uint amount);
event Approval(address indexed owner, address indexed spender, uint amount);
}

Testar no Remix