Uniswap v2 아비트라지 최적화

아비트라지는 가격 차이를 활용해 무위험 수익을 얻는 방법으로, 최적의 이익을 위해서는 정확한 계산이 필수적입니다. DEX(탈중앙화 거래소)에는 다양한 토큰 페어의 유동성 풀이 등록되어 있으며, 유동성을 추가하거나 제거하거나 거래가 이루어질 때 여러 풀 간에 가격 차이가 발생할 수 있습니다. 이러한 가격 차이를 이용해 두 풀 사이에서 무위험 차익거래, 즉 아비트라지를 수행할 수 있습니다.

아비트라지가 가능하다는 점을 확인하고, 각 풀이 얼마나 유동성을 보유하고 있는지도 파악할 수 있습니다. 그렇다면, 아비트라지를 통해 최대 이익을 얻기 위해서는 얼마만큼 거래해야 할까요?

Uniswap v2 AMM

Uniswap v2에서 토큰 $x$개를 풀에 넣었을 때 얻을 수 있는 토큰의 양을 $y$로 정의합니다. 이때 $R_{in}$은 풀에 넣는 토큰과 같은 종류의 토큰이 현재 풀에 얼마나 있는지, $R_{out}$​은 얻고자 하는 토큰의 풀 내 개수를 나타냅니다.

아래는 Uniswap v2의 내장 함수인 getAmountOut 함수입니다.

getAmountOut
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
    require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
    require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
    uint amountInWithFee = amountIn.mul(997);
    uint numerator = amountInWithFee.mul(reserveOut);
    uint denominator = reserveIn.mul(1000).add(amountInWithFee);
    amountOut = numerator / denominator;
Solidity


수식으로 표현하면 다음과 같습니다:

$$y = \frac{(1000 – 3)R_{out} x}{1000 R_{in} + (1000 – 3) x}$$

이 공식은 0.3% 거래 수수료를 반영하여 유동성 풀의 가격 결정 메커니즘을 설명합니다.

아비트라지 공식의 유도

아비트라지는 두 개의 풀을 이용한 거래이므로, 두 개의 공식을 연결해 계산해야 합니다. 아래는 두 공식을 연결한 과정입니다.

  • $x$ = amount in
  • $y_1$ = amount out of 1st pool
  • $y_2$ = amount out of 2nd pool
  • $n$ = 1000 where Uniswap v2
  • $s$ = 3 where Uniswap v2
  • $R_{1, in}$ = reserve in of 1st pool
  • $R_{1, out}$ = reserve out of 1st pool
  • $R_{2, in}$ = reserve in of 2nd pool
  • $R_{2, out}$ = reserve out of 2nd pool
  • $y_1 = \frac{(n – s)R_{1, out}x}{nR_{1, in} + (n – s)x}$
  • $y_2 = \frac{(n – s)R_{2, out}y_1}{nR_{2, in} + (n – s)y_1}$
  • $y_2 = \{ (n – s)R_{2, out} \times \frac{(n – s)R_{1, out}x}{nR_{1, in} + (n – s)x}\} \div \{ nR_{2, in} + (n – s) \frac{(n – s)R_{1, out}x}{nR_{1, in} + (n – s)x} \}$


풀 간의 reserve 값을 알면 입금량에 따라 얻을 수 있는 토큰 수를 계산할 수 있습니다. 그렇다면, 최대 이익을 얻기 위한 최적의 입금량을 어떻게 계산할까요? 이를 근의 공식으로 유도해 보겠습니다.

$𝑦_2$에 대한 $𝑥$의 해를 찾아야 합니다. 근의 공식으로 유도해 보겠습니다.

  • $y_1 = \frac{(n – s)R_{1, out}x} {nR_{1, in} + (n – s)x}$
  • $y_2 = \frac{(n – s)R_{2, out}y_1} {nR_{2, in} + (n – s)y_1}$
  • $y_2 = \{ (n – s)R_{2, out} \times \frac{(n – s)R_{1, out}x}{\{nR_{1, in} + (n – s)x\}} \} \div \{ nR_{2, in} + (n – s) \frac{(n – s)R_{1, out}x}{\{nR_{1, in} + (n – s)x\}} \}$
  • $y_2 = \{ (n – s)R_{2, out} \times (n – s)R_{1, out}x \} \div [ nR_{2, in} \{ nR_{1, in} + (n – s)x \} + (n – s) (n – s)R_{1, out}x ]$
  • $y_2 = \{ (n – s)^2R_{1, out}R_{2, out}x \} \div \{ n^2R_{1, in}R_{2, in} + (n – s)nR_{2, in}x + (n – s)^2R_{1, out}x \}$
  • $y_2 = \frac {(n – s)^2R_{1, out}R_{2, out}x} {n^2R_{1, in}R_{2, in} + x { (n – s)nR_{2, in} + (n – s)^2R_{1, out} }}$
  • $F(x) = y_2 – x$
  • $F^{\prime}(x) = y^{\prime}_1 – 1$
  • $f = (n – s)^2R_{1, out}R_{2, out}x$
  • $g = n^2R_{1, in}R_{2, in} + x \{ (n – s)nR_{2, in} + (n – s)^2R_{1, out} \}$
  • $y^{\prime}_1 = \frac {f^{\prime}g – fg^{\prime}} {g^2}$
  • $g^2 = f^{\prime}g – fg^{\prime}$
  • $f^{\prime}g – fg^{\prime} = (n – s)^2R_{1, out}R_{2, out} [ n^2R_{1, in}R_{2, in} + x \{ (n – s)nR_{2, in} + (n – s)^2R_{1, out} \} ] – (n – s)^2R_{1, out}R_{2, out}x \{ (n – s)nR_{2, in} + (n – s)^2R_{1, out} \}$
  • $f^{\prime}g – fg^{\prime} = (n – s)^2R_{1, out}R_{2, out} \{ n^2R_{1, in}R_{2, in} + (n – s)nR_{2, in} x \} + (n – s)^4R^2_{1, out}R_{2, out} x – (n – s)^3nR_{2, in}R_{1, out}R_{2, out}x – (n – s)^4R^2_{1, out}R_{2, out}x$
  • $f^{\prime}g – fg^{\prime} = (n – s)^2R_{1, out}R_{2, out} \{ n^2R_{1, in}R_{2, in} + (n – s)nR_{2, in} x \} – (n – s)^3nR_{2, in}R_{1, out}R_{2, out}x$
  • $f^{\prime}g – fg^{\prime} = (n – s)^2n^2R_{1, in}R_{2, in}R_{1, out}R_{2, out}$
  • $g^2 = [ n^2R_{1, in}R_{2, in} + x \{ (n – s)nR_{2, in} + (n – s)^2R_{1, out} \}]^2$
  • $k = (n – s)nR_{2, in} + (n – s)^2R_{1, out}$
  • $g^2 = (n^2R_{1, in}R_{2, in} + kx)^2$
  • $g^2 = (n^2R_{1, in}R_{2, in})^2 + 2n^2R_{1, in}R_{2, in} kx + (kx)^2$
  • $(n^2R_{1, in}R_{2, in})^2 + 2n^2R_{1, in}R_{2, in} kx + (kx)^2 = (n – s)^2n^2R_{1, in}R_{2, in}R_{1, out}R_{2, out}$
  • $k^2x^2 + 2n^2R_{1, in}R_{2, in} kx + (n^2R_{1, in}R_{2, in})^2 – (n – s)^2n^2R_{1, in}R_{2, in}R_{1, out}R_{2, out} = 0$
  • $a = k^2$
  • $b = 2n^2R_{1, in}R_{2, in} k$
  • $c = (n^2R_{1, in}R_{2, in})^2 – (n – s)^2n^2R_{1, in}R_{2, in}R_{1, out}R_{2, out}$
  • $x^* = \frac {-b + \sqrt {b^2 – 4ac}} {2a}$

복잡했지만 근의 공식을 사용하여 유도했습니다. 이제 실수가 없었는지 검증해 보겠습니다.

두 풀 사이의 가격이 2배 차이 나는 상황을 가정해 보겠습니다.

  • $n = 1000$
  • $s = 3$
  • $R_{1, in}=100 * 10^{18}$
  • $R_{1, out}=1000 * 10^{18}$
  • $R_{2, in}=1000 * 10^{18}$
  • $R_{2, out}=200 * 10^{18}$

아래 그래프에서, arbitrage 이익의 기댓값은 $8.44176 \times 10^{18}$으로 나타납니다. 또한, 최대 이익을 얻기 위해 첫 번째 pool에 넣어야 하는 토큰의 수량은 $20.5911 \times 10^{18}$임을 확인할 수 있습니다. 이 값은 근의 공식을 통해 도출된 결과와 일치하므로, 공식이 검증되었다고 할 수 있습니다.

계산과 그래프 on desmos

공식 일반화

두 풀 간의 아비트라지뿐만 아니라, 여러 풀을 거쳐가며 이루어지는 multi-hop 아비트라지도 존재합니다. 이를 통해 더 복잡한 아비트라지 기회를 탐색할 수 있습니다.
만약 두 개의 풀이 아닌 $n$개의 풀을 통해 아비트라지를 수행한다면 어떻게 해야 할까요? 다음은 $n-hop$ 경우의 일반화된 공식입니다.


3-hop:

  • $k = (n – s)n^2R_{2, in}R_{3, in} + (n – s)^2nR_{1, out}R_{3, in} + (n-s)^3R_{1, out}R_{2, out}$
  • $a = k^2$
  • $b = 2n^3R_{0, in}R_{1, in}R_{2, in} k$
  • $c = (n^3R_{1, in}R_{2, in}R_{3, in})^2 – (n – s)^3n^3R_{1, in}R_{2, in}R_{3, in}R_{1, out}R_{2, out}R_{3, out}$
  • $x^* = \frac {-b + \sqrt {b^2 – 4ac}} {2a}$


4-hop:

  • $k = (n – s)n^3R_{2, in}R_{3, in}R_{4, in} + (n – s)^2n^2R_{1, out}R_{3, in}R_{4, in} + (n-s)^3nR_{1, out}R_{2, out}R_{4, in} + (n – s)^4R_{1, out}R_{2, out}R_{3, out}$
  • $a = k^2$
  • $b = 2n^4R_{1, in}R_{2, in}R_{3, in}R_{4, in} k$
  • $c = (n^4R_{1, in}R_{2, in}R_{3, in}R_{4, in})^2 – (n – s)^4n^4R_{1, in}R_{2, in}R_{23 in}R_{4, in}R_{1, out}R_{2, out}R_{3, out}R_{4, out}$
  • $x^* = \frac {-b + \sqrt {b^2 – 4ac}} {2a}$


Generalize the formula:

  • $h$ = hops
  • $k = (n – s)n^h \prod_{i=2}^{h} R_{i, in} + \sum_{j=2}^{h} [ (n – s)^{j}n^{h-j} \prod_{i=1}^{j – 1} R_{i, out} \prod_{i=1}^{h-j} R_{i + j, in} ]$
  • $a = k^2$
  • $b = 2n^{h} \prod_{i=1}^{h} R_{i, in} k$
  • $c = (n^{h} \prod_{i=1}^{h} R_{i, in})^2 – (n – s)^{h}n^{h} \prod_{i=1}^{h} R_{i, in} \prod_{i=1}^{h} R_{i, out}$
  • $x^* = \frac {-b + \sqrt {b^2 – 4ac}} {2a}$

Formula implement
get_multi_hop_optimal_amount_in(data: List[Tuple[int, int, int, int]]):
    """
    data: List[Tuple[int, int, int, int]]
        Tuple of (N, S, reserve_in, reserve_out)
    """
    h = len(data)
    n = 0
    s = 0
    prod_reserve_in_from_second = 1
    prod_reserve_in_all = 1
    prod_reserve_out_all = 1
    for idx, (N, S, reserve_in, reserve_out) in enumerate(data):
        if S > s:
          n = N
          s = S
        if idx > 0:
          prod_reserve_in_from_second *= reserve_in
        prod_reserve_in_all *= reserve_in
        prod_reserve_out_all *= reserve_out
    sum_k_value = 0
    for j in range(1, h):
      prod_reserve_out_without_latest = prod([r[3] for r in data[:-1]])
      prod_reserve_in_ = 1
      for i in range(0, h-j - 1):
        prod_reserve_in_ *= data[i + j + 1][2]
      sum_k_value += (n - s) ** (j + 1) * n ** (h - j - 1) * prod_reserve_out_without_latest * prod_reserve_in_
    k = (n - s) * n ** (h - 1) * prod_reserve_in_from_second + sum_k_value
    a = k ** 2
    b = 2 * n ** h * prod_reserve_in_all * k
    c = (n ** h * prod_reserve_in_all ) ** 2 - (n - s) ** h * n ** h * prod_reserve_in_all * prod_reserve_out_all
    numerator = -b + math.sqrt(b ** 2 - 4 * a * c)
    denominator = 2 * a
    return math.floor(numerator / denominator)
Python

결론 및 유의사

위의 수식과 알고리즘을 통해 최적의 아비트라지 기회를 탐색할 수 있습니다. 그러나 실제 거래에서는 슬리피지(Slippage), 거래 수수료, 네트워크 지연 등의 변수를 고려해야 하므로 실시간 데이터를 기반으로 전략을 세우는 것이 중요합니다.

DEX 아비트라지는 진입 장벽이 낮아 경쟁이 치열하기 때문에, 다른 봇들과의 경쟁에서 승리하는 것은 매우 어렵습니다.

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다