[멋쟁이 사자처럼 블록체인 스쿨 3기] 23-06-27

임형석·2023년 6월 27일
0

Defi


CPMM

Uniswap V3

CPMM 은 constant product market makers 의 약자이다.

uniswap 에서 처음 제시한 함수로, 토큰 스왑 거래를 지원하기 위해 도입했다.

유니스왑은 X * Y = K 수식으로 풀을 설명할 수 있다.

X 토큰의 갯수, Y 토큰의 갯수가 K 인 풀의 크기를 결정한다.

CPMM 함수
흔히 x*y = k로 정의되며 위 함수는 거래가 발생하는 시점의 함수를 정의한 모습이다.
여기서 R은 리저브를 의미하며 R_a와 R_b는 교환 대상이 되는 두 토큰이다.
γ는 트랜잭션 수수료이며 k를 유지하기 위한 변화량이 각 풀 내 리저브양에 더해지고 감소한다.
두 토큰 리저브 수량의 곱이 항상 일정한 k를 유지해야 하기 때문에 토큰의 가격은 토큰 수량의 반비례 그래프를 형성한다.

이러한 반비례 그래프를 형성한다.

CPMM 은 두 가지의 특징이 있다.

  1. 먼저 교환하고자 하는 토큰 수량이 많을수록 더 큰 손해를 보게된다.
    이를 전문용어로는 슬리피지(Slippage)라고도 표현한다.
    오더북에서는 판매자만 있다면 내가 원하는 가격에 원하는 양만큼 구입할 수 있지만 CPMM에서는 원하는 가격에 구매할 수 없다. 다시 말해서 Limit Order를 할 수 없고 시장가로 다 긁어야 한다.
  1. 반비례 그래프 특성상 양쪽으로 무한히 발산하기 때문에 이론상 무한대의 유동성(Infinite Liquidity)을 제공한다. 풀 내에 유동성만 예치되어 있다면 구매자는 항상 상품을 구입할 수 있다.
    반면, 오더북 기반 거래소에서는 팔고 싶어도 구매자가 없어 팔지 못하는 상황이 연출될 수 있다.

자료 출처


CPMM in solidity

위의 링크에서 유니스왑의 CPMM 에 대해 읽어보았으나, 역시 어려웠다.

그래서 수업시간 중 배운 CPMM 컨트랙트 코드를 다시 보았다.

코드는 아래와 같다.

    function balanceOfAB() public view returns(uint, uint) {
        return (token_a.balanceOf(address(this)), token_b.balanceOf(address(this)));
    }

    // 공급
    function provideLiquidity(address _token, uint _a) public isitAorB(_token) {
        (uint bal_a, uint bal_b) = balanceOfAB();

        uint mint_lp;

        if(_token == address(token_a)) {
            uint _b = _a * bal_b / bal_a;

            token_a.transferFrom(tx.origin, address(this), _a);
            token_b.transferFrom(tx.origin, address(this), _b);

            mint_lp = _a * totalSupply() / bal_a;
        } else {
            uint _b = _a * bal_a / bal_b;

            token_a.transferFrom(tx.origin, address(this), _b);
            token_b.transferFrom(tx.origin, address(this), _a);

            mint_lp = _a * totalSupply() / bal_b;
        }

        // lp 토큰 지급
        _mint(msg.sender, mint_lp);
        // k = (bal_a + _a) * (bal_b + _b) ;
    }

먼저, balanceOfAB() 함수로 현재의 풀을 운영하고 있는 컨트랙트가 가지고 있는 A,B 토큰의 비율을 가져온다.

예치할 A 토큰을 입력하면, 자동으로 B 토큰을 계산하는데, 방금 가져온 A, B 토큰 비율로 예치할 B 토큰을 계산한다.

uint _b = _a * bal_b / bal_a;

반대로, 예치할 B 토큰을 입력하면, 자동으로 A 토큰을 계산하며, 현재 풀의 A, B 토큰 비율로 예치할 A 토큰을 계산한다.

uint _b = _a * bal_a / bal_b;

그리고 A, B 토큰을 예치하며 풀에 유동성을 공급한 사람에게 LP(liquidity pool) 토큰을 지급한다.

풀에 예치한 A, B 토큰을 찾아갈 때는 이때 받은 LP 토큰 만큼을 반납해야 출금 받을 수 있다.


풀에 유동성을 공급하는 함수를 만들었으니, 반대로 유동성을 회수하는 함수도 만들어야 한다.

    // 제거
    function withdrawLiquidity(uint _a) public {
        require(balanceOf(msg.sender) >= _a, "not enough");
        (uint bal_a, uint bal_b) = balanceOfAB();
        // lp 토큰 수령 후 확인

        uint a_out = _a * bal_a / totalSupply();
        uint b_out = _a * bal_b / totalSupply();

        token_a.transfer(msg.sender, a_out);
        token_b.transfer(msg.sender, b_out);
        // k =  (bal_a - _a) * (bal_b - _b);
    }

먼저, balanceOfAB() 함수로 현재의 풀을 운영하고 있는 컨트랙트가 가지고 있는 A,B 토큰의 비율을 가져온다.

그리고 유동성 공급 시 지급하였던 LP 토큰을 회수한다.

회수가 확인되면 현재 풀에 남아있는 A,B 토큰을 총 공급량으로 나누어 A, B 토큰을 지급한다.

uint a_out = _a * bal_a / totalSupply();
uint b_out = _a * bal_b / totalSupply();

이때, K(풀의 크기) 는 K = X * Y 공식에 따라, (bal_a - _a) * (bal_b - _b) 로 변화할 것이다.


다음은 거래이다.

A 또는 B 토큰 일정량을 가져와서 다른 토큰으로 교환한다고 했을 때, 아래의 코드대로 실행된다.

// 거래
function swapToken(address _token, uint _amount) public isitAorB(_token) {
    (uint bal_a, uint bal_b) = balanceOfAB();
    uint k = bal_a * bal_b;

    if(_token == address(token_a)) {
        uint out = bal_b - k / (bal_a + _amount);
        token_a.transferFrom(tx.origin, address(this), _amount);
        token_b.transfer(msg.sender, out);
    } else {
        uint out = bal_a - k / (bal_b + _amount);
        token_b.transferFrom(tx.origin, address(this), _amount);
        token_a.transfer(msg.sender, out);
    }
}

먼저, 풀의 A ,B 비율을 가져온다.
(uint bal_a, uint bal_b) = balanceOfAB();

가져온 비율로 K(풀의 크기) 를 정의해준다.
uint k = bal_a * bal_b;

가져온 토큰을 확인하고, 받아갈 토큰B 에 대한 수식을 작성한다.
uint out = bal_b - k / (bal_a + _amount);

수식에 따라 토큰을 회수한다.
token_a.transferFrom(tx.origin, address(this), _amount);


아까 보았던 반비례 그래프다.

유동성 풀에 유동성을 공급, 회수 할때는 이 그래프가 Y 축을 기준으로 위아래로 움직이게 되고,

A 토큰을 B 토큰으로 스왑했을 경우에는 X 축을 기준으로 오른쪽으로.

B 토큰을 A 토큰으로 스왑하는 경우엔 X 축 기준으로 왼쪽으로 이동하게 된다.


0개의 댓글