아비트라지는 가격 차이를 활용해 무위험 수익을 얻는 방법으로, 최적의 이익을 위해서는 정확한 계산이 필수적입니다. DEX(탈중앙화 거래소)에는 다양한 토큰 페어의 유동성 풀이 등록되어 있으며, 유동성을 추가하거나 제거하거나 거래가 이루어질 때 여러 풀 간에 가격 차이가 발생할 수 있습니다. 이러한 가격 차이를 이용해 두 풀 사이에서 무위험 차익거래, 즉 아비트라지를 수행할 수 있습니다.
아비트라지가 가능하다는 점을 확인하고, 각 풀이 얼마나 유동성을 보유하고 있는지도 파악할 수 있습니다. 그렇다면, 아비트라지를 통해 최대 이익을 얻기 위해서는 얼마만큼 거래해야 할까요?
Uniswap v2 AMM
Uniswap v2에서 토큰 $x$개를 풀에 넣었을 때 얻을 수 있는 토큰의 양을 $y$로 정의합니다. 이때 $R_{in}$은 풀에 넣는 토큰과 같은 종류의 토큰이 현재 풀에 얼마나 있는지, $R_{out}$은 얻고자 하는 토큰의 풀 내 개수를 나타냅니다.
아래는 Uniswap v2의 내장 함수인 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}$임을 확인할 수 있습니다. 이 값은 근의 공식을 통해 도출된 결과와 일치하므로, 공식이 검증되었다고 할 수 있습니다.

공식 일반화
두 풀 간의 아비트라지뿐만 아니라, 여러 풀을 거쳐가며 이루어지는 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}$
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 아비트라지는 진입 장벽이 낮아 경쟁이 치열하기 때문에, 다른 봇들과의 경쟁에서 승리하는 것은 매우 어렵습니다.