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)
- Invariante
- Trocar
- Calcular Y
- Calcular D
- Obtenha o preço virtual
- Adicione liquidez
- Taxa de desequilíbrio
- Remova a liquidez
- Remova um token de liquidez
- Calcular retirar um token
- getYD
// SPDX-License-Identifier: MITpragma 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 tokensuint 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 AMMuint 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 liquidezuint 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 decimalsuint 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 decimaisfunction _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) - Df'(D) = An^n + (n + 1) D^n / (n^n prod(x_i)) - 1(as + np)D_nD_(n+1) = -----------------------(a - 1)D_n + (n + 1)pa = An^ns = sum(x_i)p = (D_n)^(n + 1) / (n^n prod(x_i))*/uint a = A * N; // An^nuint 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 <= suint 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_jf(y) = y^2 + y(b - D) - cy_n^2 + cy_(n+1) = --------------2y_n + b - Dondes = sum(x_k), k != jp = prod(x_k), k != jb = 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 Newtonuint y_prev;// Suposição inicial, y <= duint 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 Newtonuint y_prev;// Suposição inicial, y <= duint 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 dyuint[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 baixody = (y0 - y1 - 1) / multipliers[j];// Subtrair taxa (fee) de dyuint 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)externalreturns (uint shares){// calcular a liquidez atual d0uint _totalSupply = totalSupply;uint d0;uint[N] memory old_xs = _xp();if (_totalSupply > 0) {d0 = _getD(old_xs);}// Transferir tokens emuint[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 d1uint d1 = _getD(new_xs);require(d1 > d0, "a liquidez não aumentou");// Recalcular D contabilizando a taxa de desequilíbriouint 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 saldosfor (uint i; i < N; ++i) {balances[i] += amounts[i];}// Shares para cunhar = (d2 - d0) / d0 * total supply// d1 >= d2 >= d0if (_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)externalreturns (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)privateviewreturns (uint dy, uint fee){uint _totalSupply = totalSupply;uint[N] memory xp = _xp();// Calcule d0 e d1uint d0 = _getD(xp);uint d1 = d0 - (d0 * shares) / _totalSupply;// Calcule a redução em y se D = d1uint 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 taxasuint dx;for (uint j; j < N; ++j) {if (j == i) {dx = (xp[j] * d1) / d0 - y0;} else {// d1 / d0 <= 1dx = xp[j] - (xp[j] * d1) / d0;}xp[j] -= (LIQUIDITY_FEE * dx) / FEE_DENOMINATOR;}// Recalcular y com xp incluindo taxas de desequilíbriouint y1 = _getYD(i, xp, d1);// - 1 arredondar para baixody = (xp[i] - y1 - 1) / multipliers[i];fee = dy0 - dy;}function calcWithdrawOneToken(uint shares, uint i)externalviewreturns (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);}