추가 공부 [Uniswap]

🔥 저번글 의문점 해결

🔨 ERC-20 : transferFrom

일단 제가 저번에 생각하고 고민했던 내용은 크게 틀리지 않았습니다.

transferFrom자체가 서로가 공동으로 인지할수 있는?? 중간 지점에 돈을 저장하고

저장한 금액에 한에서 다른 사용자가 인출하는 시스템입니다.

예를들면 우리가 통신비를 낼떄가 있습니다.

통장에 100만원정도를 보관한다는 것이 allowance를 설정한다는 것이고

해당 100만원 한에서 저희는 핸드폰 비용이 빠져나가게 됩니다.

이와 같은 원리로 이해를 해보자면

저번글에서 적은 스테이킹함수에서 굳이 transferFrom을 사용할 필요는 없다고 생각을 합니다.

  • 왜냐하면 사용자가 스테이킹하겠다고 함수를 실행시키는 것이기 떄문에 이후 다른 사용자가 해당 금액을 가져가서 처리할 필요는 없기 떄문이죠

🔨 Uniswap

처음 Swap을 공부를 하였을떄에는 단순히 Uniswap의 github를 참고하였습니다.

core, Factory등등 다양한 컨트랙트들이 있었고 이 컨트랙트를 보아서는 도저히 이해가 안가더군요...

그래서 백서도 참고하고 youtube도 참고하여 Swap에 대하여 공부를 하였습니다.

  • 상당히 긴 글이 될꺼 같습니다..

V2 이전

일단 v2이전에는 어떻게 작동을 하였는지 살펴보았습니다.

  • 딱 초기의 유니스왑에 대해서 알아보았습니다.

일단 유니스왑은 오더북 형식이 아닌 AMM형식을 사용합니다.

  • 그중에서 CPMM이라는 AMM을 사용합니다.

AMM자체가 DEX를 의미하기 떄문에 탈중앙화 거래소라고 이해를 하면 될것 같습니다.

CPMM은 X * Y = K라는 것과 같이 K라는 고정상수값을 이용해 가격을 유지하는 방법을 말합니다.

만약 X가 ETH, Y가 Klaytn이라면

ETH를 컨트랙트에 스왑함으로써 받게되는 클레이튼은 K/X가 될 것입니다.

이는 더 많은 ETH를 스왑할수록 더 적은 Klaytn을 받게된다는 의미가 되고

또 이 의미는 Klaytn의 가격이 상승했다는 의미가 됩니다.

이와 같이 오더북이 아닌 유동성을 통해 거래가 이루어 지고 이 유동성은 차익거래를 실현하고자 하는 거래자들로 인해서
일정하게 유지가 됩니다.

오더북이 블록체인이 어울리지 않는 이유

해당 부분은 굉장히 간단합니다.

블록체인은 모든 행위에 대해서 수수료라는 것을 지불해야 합니다.

그러기 떄문에 비용이 발생하고 대신 보안적으로 굉장히 안전하다는 장점이 있습니다.

만약 오더북 형태를 사용하게 된다면 거래를 등록 취소 하는 일련의 모든 과정이 수수료가 발생을 할 것이고 사용자들 입장에서는 단순히 거래를 등록하는데에 자신의 자금이 빠져나가니 좋은 형태는 아니겠죠

그러다 보니 오더북 보다는 이와 같이 알고리즘을 활용하여 거래를 유발하는 것 입니다.

유니스왑은 기본적으로 Pair Pool이라는 것을 형성합니다.

pair pool은 쉽게 말하면 토큰 거래를 지원하는 길?? 이라고 이해를 하면 될것 같습니다.

pair pool은 Factory컨트랙트라는 곳에서 함수 실행을 통하여 작동을 하게 되며

이후 거래가 정상적으로 이루어지기 위해서는 충분한 차익거래 및 사용자가 있어야 합니다.

예를들면

ERC-20토큰 한개와 ETH를 스왑하기 위해서는 pair pool이 만들어 져야 하며

이 pair pool을 통해서 스왑이 이루어 집니다.

이후 다른 ERC-20과 또 ETH를 스왑하기 위해서는 다른 pair pool이 만들어 져야 합니다.

이러한 방식이 다양한 스왑을 지원한다는 장점이 있지만 수많은 CA주소가 만들어 지게 될 것이며 컨트랙트 주소면에서는 효율적인 방법이 아닙니다.

  • 이런 CA주소 값들은 모두 Factory컨트랙트에서 보관을 하게 됩니다.

Uniswap이 굉장히 블록체인 스러운 이유

일단 기본적으로 제 3자를 통해서 거래가 이루어 지지 않습니다.

모든것이 blockChain에서 작동을 하기 떄문에 보안적으로 우수하며 스왑떄 발생하는 수수료를 통해서 유동성을 유지합니다.

  • 제 3자가 독식을 하는 구조가 아닙니다.

유동성을 공급하기 위해서는 유동성 공급자들이라는 역할이 존재해야 하며 이 공급자들은 스테이킹과 같이 자신의 자산을 컨트랙트에 예치를 시킵니다.

ETH-Klaytn 이라는 pair pool에 자신의 ETH와 Klaytn을 예치시키는 것과 같습니다.

이런 예치하는 작업이 끝나면 예치하는 코인의 금액만큼 LP토큰이라는 것을 발행받고 해당 LP토큰에 비례하여 스왑할떄 발생하는 수수료를 받게 됩니다.

즉 스왑이 많이 발생하는 pair pool인 경우에는 많은 사람들이 수수료를 받기위해 자신의 토큰을 예치할 것이며

이로 인해 더 견고한 유동성 풀이 형성이 됩니다.

견고한 유동성풀은 이와 같습니다.

특정 사용자로 인해 가격이 크게 변동이 이루어 지지 않는다.
- 차익거래로 인한 수익을 많이 받지 못한다.

많은 사용자들이 해당 스왑에 대하여 관심이 있다.

이런 부분에 대하여 공부를 하면서 제가 느낀점은

Uniswap든 다른 모든 스왑이든 일단 사람들이 어느정도 몰려야 유지가 된다는 점을 꺠닫게 되었습니다.

  • 왜냐하면 견고한 유동성 풀을 형성해야 가격이 실제 시장가격과 비슷하게 유지가 되기 떄문에

V2 이후

Uniswap백서를 살펴보면서 어떠한 부분이 달라졌는지에 대하여 학습을 하였습니다.

일단 V2이전에는 ETH를 기축통화로 사용을 하였습니다.

Klaytn을 bora코인으로 바꾸고자 하면

Klaytn -> ETH -> bora와 같이

중간 단계에 ETH라는 것을 통해 이루어 져야 했습니다.

이런 방법은 만약 단순히 스테이클 코인을 스왑하고자 하는 사람에게는 ETH가 일시적으로 하락을 하게 되었을떄 일시적인 손해를 겪게 하였습니다.

이 부분은 개선하기 위해 V2에서는 ERC-20토큰 간에 거래를 지원하게 수정하였습니다.

하지만 이 방식또한 문제가 있습니다.

ERC-20토큰 간에 거래를 가능하게 하면

ERC-20토큰과 ETH간에도 거래가 가능해야합니다.

이 방식은 같은 로직을 두번 사용을 하기 떄문에 코드를 두번 짜게 만듭니다.

이 문제점을 해결하기 위해서 V2에서는 WETH를 사용합니다.

  • ETH를 안쓰고 ETH를 ERC-20토큰의 형태로 Wrapping한 토큰을 말합니다.

즉 V2에서는 ETH를 사용하지 않고 무조건 WETH를 통해서 ERC-20토큰 거래를 유발합니다.

또 달라진 점은 수수료 입니다.

V2이전에는 약 0.3%가 수수료로 부과가 되었지만

V2이후에는 0.25%는 일반적인 수수료, 0.05%는 프로토콜fee로 바뀌어서 따로 걷고 있습니다.

확실하지만 않지만 이런 방향은 0.25%는 유동성풀 공급자들에게 주어지는 수수료이고
0.05%는 유니토큰 홀더들에게 가는 것으로 예상을 하였습니다.

  • 왜냐하면 Uniswap거버넌스에 들어가보면 이런 부분을 추가해달라는 요청이 굉장히 많다고 합니다.

🔨 Uniswap 코드

모든 코드를 다 살펴볼수는 없고 크게 Factory에 createPair부분을 우선 살펴보도록 하겠습니다.

Factory 컨트랙트

 function createPair(address tokenA, address tokenB) external returns (address pair) {
        require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
        (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
        require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
        require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
        bytes memory bytecode = type(UniswapV2Pair).creationCode;
        bytes32 salt = keccak256(abi.encodePacked(token0, token1));
        assembly {
            pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
        }
        IUniswapV2Pair(pair).initialize(token0, token1);
        getPair[token0][token1] = pair;
        getPair[token1][token0] = pair; // populate mapping in the reverse direction
        allPairs.push(pair);
        emit PairCreated(token0, token1, pair, allPairs.length);
    }

전반적으로 require을 통해서 실질적으로 존재하는 token의 주소인지를 확인합니다.

(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);

이 코드의 역할은 어떤 토큰이 앞에 있는지 결정하기 위해서 토큰의 주소값을 비교를 하는 것이고 이후 token0만을 address(0)인지 확인하는 이유는 어차피 token1은 token0보다 큰 주소값을 가지기 떄문입니다.

  • 이 토큰이 위치한 순서가 그냥 token의 주소값 크기에 따라서 배치된 것 입니다.

마지막으로는 기존에 해당 토큰 사이에 pair pool이 형성되어 있는지를 확인합니다.

이후 만들어진 pair pool이 없다면 새로운 pair pool을 만듬으로써 함수가 종료가 됩니다.

이런 과정을 통해서 pair pool이 형성이 되며 이후 pari컨트랙트를 확인하면서 어떻게 pair pool이 형성이 되었는지를 확인해 보겠습니다.

Pair 컨트랙트

constructor() public {
        factory = msg.sender;
    }

기본적으로 factory를 컨트랙트가 만들어 질떄 선정합니다.

  • Factory컨트랙트에서 실행을 시키기 떄문에 msg.sender는 Factory컨트랙트가 됩니다.
function mint(address to) external lock returns (uint liquidity) {
        (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
        uint balance0 = IERC20(token0).balanceOf(address(this));
        uint balance1 = IERC20(token1).balanceOf(address(this));
        uint amount0 = balance0.sub(_reserve0);
        uint amount1 = balance1.sub(_reserve1);

        bool feeOn = _mintFee(_reserve0, _reserve1);
        uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
        if (_totalSupply == 0) {
            liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
           _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
        } else {
            liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
        }
        require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
        _mint(to, liquidity);

        _update(balance0, balance1, _reserve0, _reserve1);
        if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
        emit Mint(msg.sender, amount0, amount1);
    }

mint함수는 LP토큰을 만들어 내는 함수 입니다.

  • 이함수에서는 컨트랙트에 보관하고 있는 잔고와 실제 잔고가 다를수 있기 떄문에
  • 다르면 mint 또는 burn해준다고 합니다.
  • 저도 어떤 경우에 다르다고 하는지는 모르겠습니다...ㅠㅠ
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
        require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
        (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
        require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');

        uint balance0;
        uint balance1;
        { // scope for _token{0,1}, avoids stack too deep errors
        address _token0 = token0;
        address _token1 = token1;
        require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
        if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
        if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
        if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
        balance0 = IERC20(_token0).balanceOf(address(this));
        balance1 = IERC20(_token1).balanceOf(address(this));
        }
        uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
        uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
        require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
        { // scope for reserve{0,1}Adjusted, avoids stack too deep errors
        uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
        uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
        require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
        }

        _update(balance0, balance1, _reserve0, _reserve1);
        emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
    }

그 다음에는 Swap함수 입니다.

실질적으로 스왑을 시전하는 함수이며 인자로는 이상하게도 나가는 금액만을 받고 있습니다.

이런식으로 인자를 받는 이유는 플래시론을 지원하기 위함입니다.

  • 플래신론이란 쉽게 말해 암호화폐 대출을 통해서 스왑을 하고 이후 다른 토큰을 통해 차익거래를 실현하여
  • 이후에 대출한 암호화폐를 갚는 행위를 말합니다.

그러면 스왑할떄 적는 금액은 어디에 적게 되나면

pair pool을 할떄에 해당 토큰에 적게 된다고 합니다.

  • 이런식으로 작동하는 이유에 대해서는 저도 잘 모르겠습니다.
  • 백서를 확인하면 이런방식으로 작동시켜야 누가, 얼마를 스왑을 했는지 확인할수 없기 떄문에 이런방식으로 작동하는것으로 알고 있습니다.

마지막은 라우팅입니다.

우리가 node.js에서 express를 통하여 서버를 구성하면

역할에 맞게 router를 사용하여 서버를 구성합니다.

이것과 비슷하게 Uniswap에서는 스왑을 중간에서 지원하는 역할을 해주는 방법이 있습니다.

기본적으로 Swap하기 위해서는 pair pool을 형성해야 하지만 모든 ERC-20토큰에 대해서 풀을 형성할 수는 없을 것 입니다.

그러면 사용자는 원하는 토큰을 교환하지 못하는 경우도 생기고

이런 경우를 대비하기 위해서 라우팅을 지원합니다.

ERC-20에서 ERC-20사이에 만약 pair pool이 형성되어 있지 않으면 
ERC-20 -> WETH -> ERC-20 이런식으로 중간 과정에 토큰을 추가하여

서로 스왑을 지원합니다.

추가로 라우팅을 지원하는 경우는 한가지가 더 있습니다.

ERC-20과 ERC-20사이에 pair pool은 형성되어 있지만

충분한 유동성 풀이 형성되어 있지 않을시에

이런떄에도 라우팅을 지원하는 것으로 알고 있습니다.

  • 사진에 보이는 것처럼 중간에 WETH를 통해 스왑을 하는것을 확인 할수 있습니다.

🔥 후기 및 느낀점, 이후 활동

Uniswap의 백서를 살펴보면서 코드를 보니 이해가 생각보다 쉬웠다고 생각합니다.

기본적으로 백서를 처음 읽어보았기 떄문에 읽으면서도 이런식으로 공부를 하는 것이 맞는가에 대한 의문이 계속 들었지만

유튜브에서 백서륿 보고 이해를 해야 코드가 왜 작성이 되었는지를 이해할수 있다는 말을 해서 그 말 그대로 따라서 공부를 해보았습니다.

말 그대로 백서는 어떤 생각을 가지고 코드를 작성을 하였는지에 대한 정리글 이였기 떄문에 확실히 도움이 되었다고 생각을 합니다.

  • 물론 영문이라서... 많이 어지러웠습니다 ㅠ

아직 Uniswap에는 더 많은 함수가 있고 제가 모르는 부분이 많습니다.

  • 실제로 github에 보면 정말 많은 컨트랙트 코드가 있다는 것을 확인할수 있습니다.

많이 부족하지만 그래도 Uniswap에 대해서, Swap에 대해서 많이 학습을 했던 시간이였던것 같습니다.

아직까지는 V2에 대해서 학습을 하였지만 현재 Uniswap은 V3가 진행되고 있습니다.

이부분에 대해서는 후에 만약 학습할 시간이 생긴다면 공부를 해보고 싶고

현재에는 어느정도의 Swap에 대하여 알게되었다고 생각을 하기 떄문에 이제는 Golang에 대해 학습을 진행해보고자 합니다.

감사합니다!!

다음에 할일

  • Golang공부
  • Solidity 심화 과정 공부!

좋은 웹페이지 즐겨찾기