Este artículo muestra algebraicamente paso a paso cómo se deriva el código para get_D() y get_y() a partir del invariante de StableSwap.
Dado el invariante de StableSwap:
Ann∑xi+D=AnnD+nn∏xiDn+1
Hay dos operaciones matemáticas frecuentes que deseamos realizar con él:
Calcular D dados valores fijos para A, y las reservas x1,…,xn. Ten en cuenta que n, el número de monedas que soporta el pool, se fija en el momento en que se despliega el pool. Esto es lo que hace la función get_D().
Dado D, deseamos aumentar el valor de una de las reservas xi a un nuevo valor xi′ y averiguar cuánto necesita disminuir otra reserva xj para mantener la ecuación equilibrada. Esto es lo que hace la función get_y(). Aquí “y” significa xi′.
Estas operaciones se llaman get_D() y get_y(), respectivamente, en Curve StableSwap.
El objetivo de get_D()
En Curve V1 (StableSwap), D se comporta de manera similar a k en Uniswap V2 — cuanto mayor sea D, mayores serán las reservas y más “hacia afuera” estará la curva de precios. D cambia — y necesita ser recalculado — después de que se añade o se retira liquidez, o si una comisión cambia el balance del pool. Para esto sirve la función get_D(). Dadas las reservas actuales del pool, calcula D.
Si un pool de Curve contiene dos tokens, x e y, el invariante de StableSwap es
4A(x+y)+D=4AD+4xyD3
El “factor de amplificación” A, para nuestros propósitos, puede ser tratado como una constante.
El objetivo de get_y()
La función get_y() se utiliza durante un intercambio (swap). De manera similar a k en Uniswap V2, D debe mantenerse constante durante un swap (ignorando las comisiones). Específicamente, dado un nuevo valor para x, calcula el valor de y que mantiene la ecuación equilibrada. Por lo tanto, es una subrutina importante para averiguar “si meto esta cantidad de token x en el pool, ¿qué cantidad de token y se puede sacar?”
Curve puede contener más de 2 tokens en el pool (por ejemplo, 3pool contiene USDT, USDC y DAI). Curve identifica las monedas por un índice en un array. Así que, en este caso, x e y se refieren a monedas particulares en ese array. En este contexto, get_y() significa cambiar el balance de un token particular x, manteniendo los otros balances constantes, pero permitiendo que otro token y cambie de valor. Luego, dado un cambio particular en x, calcula cómo cambia y para mantener el invariante equilibrado.
El invariante para n tokens es:
Ann∑xi+D=AnnD+nn∏xiDn+1
Para mayor simplicidad, utilizaremos S en lugar de sumatoria y P en lugar de productoria en el resto del artículo, de modo que el invariante se convierte en:
AnnS+D=ADnn+nnPDn+1
Donde S es la suma de los balances de los tokens (x0+x1+…+xn), P es el producto de los balances (x0x1...xn), y xi es el balance del token i.
En el whitepaper, S se escribe como ∑xi y P se escribe como ∏xi. La ecuación del whitepaper se replica a continuación:
Ann∑xi+D=ADnn+nn∏xiDn+1
Utilizaremos S y P en lugar de la notación de suma y producto.
Asumimos que los pools pueden contener un número arbitrario n de tokens, por lo que las fórmulas reflejarán eso. En la práctica, sin embargo, n debe ser pequeño, de lo contrario el término Dn+1 es susceptible de sufrir un overflow.
Calculando D con get_D()
En get_D(), se nos presenta un conjunto de balances x_0, x_1, ..., x_n y debemos calcular D.
No es posible despejar algebraicamente
AnnS+D=ADnn+nnPDn+1
para D. En su lugar, necesitamos aplicar el método de Newton para resolverlo numéricamente. Para hacerlo, creamos una función f(D), la cual es 0 cuando la ecuación está equilibrada.
0=ADnn+nnPDn+1−D−AnnS
0=nnPDn+1+ADnn−D−AnnS
f(D)=nnPDn+1+AnnD−D−AnnS
y calculamos la derivada f′(D) con respecto a D como:
f′(D)=nnP(n+1)Dn+Ann−1
Fórmula del Método de Newton
Podemos despejar iterativamente D utilizando:
Dnext=D−f′(D)f(D)
Será útil expresar f′(D) con D en el denominador. Primero multiplicamos la parte superior e inferior de la fracción izquierda que define f′(D) por D.
Podemos reescribir el método de Newton para tener un denominador común:
Dnext=D−f′(D)f(D)=Df′(D)f′(D)−f′(D)f(D)=f′(D)Df′(D)−f(D)// Meˊtodo de Newton// multiplicar D por f′(D)f′(D)// combinar por denominador comuˊn
Al sustituir el f(D) y f′(D) anteriores en la fórmula reescrita del método de Newton obtenemos:
Esto coincide exactamente con lo que hay en el código de Vyper:
La variable Dp se definió como:
D_P: uint256 = D # D_P = Sfor _x in xp: D_P = D_P * D / (_x * N_COINS)
xp es el número de tokens, por lo que el bucle se ejecutará n veces. Por lo tanto, tenemos a D multiplicado por sí mismo n veces en el denominador
Dp=nn∏i=1nxiDn+1
Calculando y con get_y()
La idea es que forzamos a uno de los xi a tomar un nuevo valor (el código llama a esto x) y calculamos el valor correcto para otro xj (donde i=j) de tal manera que la ecuación se mantenga equilibrada. El balance de los otros tokens permanece inalterado. A xj se le denomina y.
Aunque un pool de StableSwap podría tener múltiples tokens, solo es posible intercambiar dos de esos tokens a la vez usando get_y().
De nuevo, tenemos el mismo invariante
AnnS+D=ADnn+nnPDn+1
D, A, y n son fijos, pero estaremos cambiando dos de los valores en S y P
SP=x0+x1+...+xn=x0x1...xn
Por lo tanto, necesitamos ajustar la fórmula un poco, ya que S y P contienen los valores que estamos calculando.
S′ será la suma de todos los balances excepto el nuevo balance del token xi que estamos intentando despejar
P será el producto de los balances de todos los tokens, excepto el del que estamos intentando despejar.
En otras palabras,
SP=S′+y=P′y
Para mantener la consistencia con el código, llamaremos y al token cuyo nuevo balance estamos intentando calcular.
Entonces la fórmula se convierte en
Ann(S′+y)+D=ADnn+nnP′yDn+1
De nuevo, derivamos un f(y) que es 0 cuando la ecuación está equilibrada, y su derivada con respecto a y
Volviendo a nuestro invariante, podemos despejar el término fraccionario en el denominador:
Ann(S′+y)+D=ADnn+nnP′yDn+1
nnP′yDn+1=Ann(S′+y)+D−ADnn
Luego podemos sustituir eso en la ecuación para ynext:
ynext=nnP′yDn+1Ann1+yy2+nnP′AnnDn+1
ynext=(Ann(S′+y)+D−ADnn)Ann1+yy2+nnP′AnnDn+1
Luego podemos distribuir Ann1 y simplificar el denominador
ynext=((S′+y)+AnnD−D)+yy2+nnP′AnnDn+1
ynext=(S′+y+AnnD−D)+yy2+nnP′AnnDn+1
Simplificamos el denominador eliminando los paréntesis y sumando las dos y juntas
ynext=2y+S′+AnnD−Dy2+nnP′AnnDn+1
En el código original, Curve define variables adicionales:
c=nnP′AnnDn+1
b=S′+AnnD
Después de la sustitución en la fórmula para ynext, obtenemos:
ynext=2y+S′+AnnD−Dy2+nnP′AnnDn+1
ynext=2y+b−Dy2+c
Comparación con el código fuente original
Esto coincide exactamente con el código de Curve, como se ve en el recuadro morado a continuación:
Discrepancia entre Ann y Anⁿ
De manera un tanto confusa, el whitepaper de Curve usa el invariante Ann pero la base de código usa Ann. Es decir, la base de código parece estar calculando A * n * n en lugar de A * n ** n. La razón de esta discrepancia es que la base de código almacena A como Ann−1. Dado que n es fijo en el momento del despliegue, precalcular nn−1 permite que el código evite calcular un exponente on-chain, lo cual es una operación más costosa.
Resumen
El invariante central de Curve no permite que las variables D o xi se despejen de manera simbólica. En su lugar, los términos deben resolverse numéricamente.
Una de las lecciones de este ejercicio es que una buena manipulación algebraica es una técnica muy efectiva de optimización de gas. Los desarrolladores de Curve fueron capaces de calcular la fórmula del método de Newton que es mucho más reducida que simplemente insertar f y su derivada y dejarlo así.
Citas y Agradecimientos
Los siguientes recursos fueron consultados al escribir este artículo: