DeFi: DEX 정리 1 - AMM

JJVoiture·2023년 1월 28일
0

AMM(Automated Market Maker)

매수자와 매도자 간의 거래를 이어주는 방법을 탈중앙화된 방식으로 자동화.
AMM은 이용자로 하여금 임의의 두 가지 토큰(지금은 토큰 X와 토큰 Y라고 하자)의 유동성 풀을 만들 수 있다.
두 토큰의 초기 비율은 liquidity curve에 의해 결정된다.
이용자들은 적은 fee를 내고 토큰 X를 토큰 Y로 환전할 수 있다. 유동성 공급자(LP, Liquidity Provider)는 X와 Y 토큰 둘 다를 공급하고 유동성 풀에서 일어나는 거래에 대한 수수료를 받는다. 이 때 수수료는 LP 토큰이라는 형태로 지급된다.

토큰 X와 토큰 Y가 교환되는 비율은 CPMM(Constant Product Market Maker)라고 하는 알고리즘에 의해 결정된다.

CPMM (Constant Product Market Maker)

x * y = k

x, y는 각각 토큰 X, Y의 수량을 의미한다. CPMM에 따르면 풀의 토큰 개수는 상수 k 값이 일정하게 유지되어야 한다.

풀의 가격 설정

X * Y = L^2 = K
Lx = X * Px, Ly = Y * Py, Lv = L * sqrt(P)

Lx: 토큰 x의 유동성 가치
X: 토큰 x의 리저브
Px: 토큰 x의 가치

Lx = Ly = Lv가 일정하게 유지되어야 한다.

Lv^2 = Lx * Ly = (X * Px) * (Y * Py) = (X*Y) * (Px*Py)
Lv = sqrt(X*Y) * sqrt(Px*Py)
L * sqrt(P) = L = sqrt(Px*Py)
P = Px * Py
-> Px = sqrt(P) or Py = sqrt(P)
L = Lx = Ly
sqrt(P) = Px = Lx / X = sqrt(X*Y) / X = sqrt(Y/X)
sqrt(P) = sqrt(Y/X)
// 양변에 X * 1 / sqrt(P) 곱하기
X = sqrt(X*Y) / sqrt(P) = L / sqrt(P)

sqrt(P) = sqrt(Y/X)
// 양변에 Y * 1 / sqrt(P) 곱하기
Y = sqrt(Y/X) * Y / sqrt(P) = sqrt(Y)/sqrt(X) * Y * sqrt(X)/sqrt(Y) 
= sqrt(X*Y) * sqrt(Y) / sqrt(X) = L * sqrt(P)

위 수식에 기반한 swap 방식은 USDT, USDC와 같은 stable coin 간의 swap에서 slippage(실제 가격과의 갭)을 발생시킨다. 뿐만 아니라 토큰 간 가격 연관이 있는 BTC/WBTC나 ETH/WETH와 같은 페어의 풀에서도 같은 현상이 일어난다.

x^3*y + x*y^3 = k

liquidswap은 서로 상호 연관이 있는 토큰의 경우에는 아래와 같은 식을 활용하여 slippage를 최소화한다.

stable coin 간의 slippage 뿐만 아니라 Uniswap V2 기존 CPMM에는 또다른 문제가 있는데, 유동성이 [0, infinite) 까지 고르게 공급되어 있어 swap에 사용되는 금액은 전체 풀의 일부분 밖에 되지 않는다. 이는 LP들에게 돌아가는 수수료가 크게 감소하고, 거래자들 입장에서도 수수료가 그렇게 낮지 않아 해당 풀에 참여하는 이용자들 모두에게 득보다 실이 많은 구조이다.

이를 해결하기 위해서 Uniswap V3에서는 concentraded liquidity(집중화된 유동성)이라는 개념을 도입한다.

위 그림에서 유동성의 범위를 [a,b]로 한정시키게 되는 경우 그래프는 x축으로 -Xb만큼, y축으로 -Ya만큼 이동한 Real reserve가 생긴다. 공급해야 하는 유동성의 범위를 제한시킴으로써 유동성 풀의 수익률과 효율성을 증가시킨다.

L^2 = (X + Xb) * (Y + Ya)
Xb = L / sqrt(Pb), Ya = L * sqrt(Pa)

L^2 = (X + L / sqrt(Pb)) * (Y + L * sqrt(Pa))

실제 swap 구현 코드 예시: liquidswap

Move 언어로 Uniswap V3의 CPMM을 구현한 프로젝트인 liquidswap의 코드를 참조해 보자.

/source/swap/liquidity_pool.move

/// Swap coins (can swap both x and y in the same time).
/// In the most of situation only X or Y coin argument has value (similar with *_out, only one _out will be non-zero).
/// Because an user usually exchanges only one coin, yet function allow to exchange both coin.
/// * `x_in` - X coins to swap.
/// * `x_out` - expected amount of X coins to get out.
/// * `y_in` - Y coins to swap.
/// * `y_out` - expected amount of Y coins to get out.
/// Returns both exchanged X and Y coins: `(Coin<X>, Coin<Y>)`.

      public fun swap<X, Y, Curve>(
          x_in: Coin<X>,
          x_out: u64,
          y_in: Coin<Y>,
          y_out: u64
      ): (Coin<X>, Coin<Y>) acquires LiquidityPool, EventsStore {
          assert_no_emergency();

          assert!(coin_helper::is_sorted<X, Y>(), ERR_WRONG_PAIR_ORDERING);
          assert!(exists<LiquidityPool<X, Y, Curve>>(@liquidswap_pool_account), ERR_POOL_DOES_NOT_EXIST);

          assert_pool_unlocked<X, Y, Curve>();

          let x_in_val = coin::value(&x_in);
          let y_in_val = coin::value(&y_in);

          assert!(x_in_val > 0 || y_in_val > 0, ERR_EMPTY_COIN_IN);

          let pool = borrow_global_mut<LiquidityPool<X, Y, Curve>>(@liquidswap_pool_account);
          let x_reserve_size = coin::value(&pool.coin_x_reserve);
          let y_reserve_size = coin::value(&pool.coin_y_reserve);

          // Deposit new coins to liquidity pool.
          coin::merge(&mut pool.coin_x_reserve, x_in);
          coin::merge(&mut pool.coin_y_reserve, y_in);

          // Withdraw expected amount from reserves.
          let x_swapped = coin::extract(&mut pool.coin_x_reserve, x_out);
          let y_swapped = coin::extract(&mut pool.coin_y_reserve, y_out);

          // Confirm that lp_value for the pool hasn't been reduced.
          // For that, we compute lp_value with old reserves and lp_value with reserves after swap is done,
          // and make sure lp_value doesn't decrease
          let (x_res_new_after_fee, y_res_new_after_fee) =
              new_reserves_after_fees_scaled<Curve>(
                  coin::value(&pool.coin_x_reserve),
                  coin::value(&pool.coin_y_reserve),
                  x_in_val,
                  y_in_val,
                  pool.fee
              );
          assert_lp_value_is_increased<Curve>(
              pool.x_scale,
              pool.y_scale,
              (x_reserve_size as u128),
              (y_reserve_size as u128),
              (x_res_new_after_fee as u128),
              (y_res_new_after_fee as u128),
          );

          split_fee_to_dao(pool, x_in_val, y_in_val);

          update_oracle<X, Y, Curve>(pool, x_reserve_size, y_reserve_size);

          let events_store = borrow_global_mut<EventsStore<X, Y, Curve>>(@liquidswap_pool_account);
          event::emit_event(
              &mut events_store.swap_handle,
              SwapEvent<X, Y, Curve> {
                  x_in: x_in_val,
                  y_in: y_in_val,
                  x_out,
                  y_out,
              });

          // Return swapped amount.
          (x_swapped, y_swapped)
      }
	acquires LiquidityPool, EventsStore {
        assert!(coin_helper::is_sorted<X, Y>(), ERR_WRONG_PAIR_ORDERING);
        assert!(exists<LiquidityPool<X, Y, Curve>>(@liquidswap_pool_account), ERR_POOL_DOES_NOT_EXIST);

        assert_pool_unlocked<X, Y, Curve>();

        let burned_lp_coins_val = coin::value(&lp_coins);

        let pool = borrow_global_mut<LiquidityPool<X, Y, Curve>>(@liquidswap_pool_account);

        let lp_coins_total = coin_helper::supply<LP<X, Y, Curve>>();
        let x_reserve_val = coin::value(&pool.coin_x_reserve);
        let y_reserve_val = coin::value(&pool.coin_y_reserve);

        // Compute x, y coin values for provided lp_coins value
        let x_to_return_val = math::mul_div_u128((burned_lp_coins_val as u128), (x_reserve_val as u128), lp_coins_total);
        let y_to_return_val = math::mul_div_u128((burned_lp_coins_val as u128), (y_reserve_val as u128), lp_coins_total);
        assert!(x_to_return_val > 0 && y_to_return_val > 0, ERR_INCORRECT_BURN_VALUES);

        // Withdraw those values from reserves
        let x_coin_to_return = coin::extract(&mut pool.coin_x_reserve, x_to_return_val);
        let y_coin_to_return = coin::extract(&mut pool.coin_y_reserve, y_to_return_val);

        update_oracle<X, Y, Curve>(pool, x_reserve_val, y_reserve_val);
        coin::burn(lp_coins, &pool.lp_burn_cap);

        let events_store = borrow_global_mut<EventsStore<X, Y, Curve>>(@liquidswap_pool_account);
        event::emit_event(
            &mut events_store.liquidity_removed_handle,
            LiquidityRemovedEvent<X, Y, Curve> {
                returned_x_val: x_to_return_val,
                returned_y_val: y_to_return_val,
                lp_tokens_burned: burned_lp_coins_val
            });

        (x_coin_to_return, y_coin_to_return)
    }

/source/libs/math.move

public fun mul_div_u128(x: u128, y: u128, z: u128): u64 {
    assert!(z != 0, ERR_DIVIDE_BY_ZERO);
    let r = x * y / z;
    (r as u64)
}

참고

https://docs.liquidswap.com/protocol-overview
https://docs.uniswap.org/concepts/protocol/concentrated-liquidity
https://github.com/pontem-network/liquidswap

profile
안녕하세요.

0개의 댓글