Inaccuracy in liquidswap::stable_curve
computations
Description
Liquidswap provides peripheral modules for interacting with the protocol. The liquidswap::stable_curve
module exposes helper functions for computing exchange amounts.
Liquidity pools for correlated coins utilize a different curve. Specifically, if the reserves of two coins are x
and y
, then it maintains that c = x ^ 3 y + y ^ 3 x
must increase across exchanges. To help compute quantities, the internal function stable_curve::get_y
is used to find y
given x
and c
.
fun get_y(x0: U256, xy: U256, y: U256): U256 {
let i = 0;
let one_u256 = u256::from_u128(1);
while (i < 255) {
let k = f(x0, y);
let _dy = u256::zero();
let cmp = u256::compare(&k, &xy);
if (cmp == 1) {
_dy = u256::add(
u256::div(
u256::sub(xy, k),
d(x0, y),
),
one_u256 // Round up
);
y = u256::add(y, _dy);
} else {
_dy = u256::div(
u256::sub(k, xy),
d(x0, y),
);
y = u256::sub(y, _dy);
};
cmp = u256::compare(&_dy, &one_u256);
if (cmp == 0 || cmp == 1) {
return y
};
i = i + 1;
};
y
}
This implementation uses Newton's method to iteratively find a y
value given an initial guess. However, the criteria the function uses to find when it converges will result in slightly unstable outputs: The result of get_y
differs slightly on different starting conditions. Consider the following:
get_y(u256::from_u128(138), u256::from_u128(200000000), u256::from_u128(40));
get_y(u256::from_u128(138), u256::from_u128(200000000), u256::from_u128(50));
get_y(u256::from_u128(138), u256::from_u128(200000000), u256::from_u128(60));
While the first and third return 63
, the second returns 64
. The true result should be approximately 62.98
; the initial condition of 50
results in an incorrect result, even if we suppose get_y
should round upwards.
Impact
The incorrect get_y
values lead to slightly incorrect calculations by coin_in
and coin_out
. The goal of coin_out
is to return the amount of output a user should receive given reserve states and an input quantity. However, the value it returns can be too low:
coin_out(47, 100000000, 100000000, 10000, 15674);
This call returns 47, but a user could actually extract 48 coins from the pool while still increasing the liquidity pool value.
Recommendations
First, the desired behavior of get_y
should be better documented. At the moment, it is unclear whether it should round up or round down. Based on this decision, the update and stopping criteria for get_y
should be adjusted. Currently, if the adjustment for y
is less than or equal to one in a given iteration, the function assumes it has converged and returns.
Remediation
Pontem Network fixed this issue in commit 0b01ed6
↗