체인의정석

UniswapV2 정리3 - periphery, Router 뜯어보기 본문

블록체인/디파이

UniswapV2 정리3 - periphery, Router 뜯어보기

체인의정석 2022. 10. 26. 18:24
728x90
반응형

소스 코드 위치

https://github.com/Uniswap/v2-periphery

 

GitHub - Uniswap/v2-periphery: 🎚 Peripheral smart contracts for interacting with Uniswap V2

🎚 Peripheral smart contracts for interacting with Uniswap V2 - GitHub - Uniswap/v2-periphery: 🎚 Peripheral smart contracts for interacting with Uniswap V2

github.com

 

일단 여기서 파일은 크게 3개가 있는데

Migrater와 Router 1, Router 2 이렇게 3개가 여기에 해당된다.

 

UniswapV2Migrator

 

Migrate를 보자면 버전 1에서 마이그레이션을 해오는 코드 같은데 데이터 마이그레이션을 이런 식으로 할 수 있다니...

이걸 미리 알았다면 좋았겠지만 앞으로 마이그레이션 계획이 있을 때는 유니스왑처럼 컨트렉트 형태로 하는게 좋을거 같다는 생각이 들엇다.

 

(실제 서비스에서 마이그레이션 시나리오를 문서로 작성해서 스크립트 문으로 준비했었는데 이런식으로 컨트렉트로 만들면 더 좋은거 같다.)

    function migrate(address token, uint amountTokenMin, uint amountETHMin, address to, uint deadline)
        external
        override
    {
        IUniswapV1Exchange exchangeV1 = IUniswapV1Exchange(factoryV1.getExchange(token));
        uint liquidityV1 = exchangeV1.balanceOf(msg.sender);
        require(exchangeV1.transferFrom(msg.sender, address(this), liquidityV1), 'TRANSFER_FROM_FAILED');
        (uint amountETHV1, uint amountTokenV1) = exchangeV1.removeLiquidity(liquidityV1, 1, 1, uint(-1));
        TransferHelper.safeApprove(token, address(router), amountTokenV1);
        (uint amountTokenV2, uint amountETHV2,) = router.addLiquidityETH{value: amountETHV1}(
            token,
            amountTokenV1,
            amountTokenMin,
            amountETHMin,
            to,
            deadline
        );
        if (amountTokenV1 > amountTokenV2) {
            TransferHelper.safeApprove(token, address(router), 0); // be a good blockchain citizen, reset allowance to 0
            TransferHelper.safeTransfer(token, msg.sender, amountTokenV1 - amountTokenV2);
        } else if (amountETHV1 > amountETHV2) {
            // addLiquidityETH guarantees that all of amountETHV1 or amountTokenV1 will be used, hence this else is safe
            TransferHelper.safeTransferETH(msg.sender, amountETHV1 - amountETHV2);
        }
    }

일단 이건 유저가 직접 실행시키는 코드라는 생각이 든다.

 

유저가 특정 lp 풀에 들어가서 migrate를 실행시키면 msg.sender는 유저가 된다.

그럼 유저가 가지고 있던버전 1의  lp 풀에서 2의 lp 풀로 마이그레이션이 된다.

 

일단 요 부분은 V1 코드 까지 봐주어야 하기 때문에 지금으로선 생략을 하고 나중에 마이그레이션 전략을 짜야 할때 참고차 보면 될거 같다.

 

그럼 내가 알아야 할 핵심 로직은

Router 01과 Router 02가 남는다.

 

UniswapV2Router01

정말 생각보다 긴 걸 볼 수 있었는데 이 부분은 

https://docs.uniswap.org/protocol/V2/reference/smart-contracts/router-01

 

Router01 | Uniswap

UniswapV2Router01 should not be used any longer, because of the discovery of a low severity bug and the fact that some methods do not work with tokens that take fees on transfer. The current recommendation is to use UniswapV2Router02.

docs.uniswap.org

여길 보니까 문제가 생겨서 더이상은 쓰지 않는다고 한다. 그럼으로 Router02만 보면 될것 같다.

 

UniswapV2Router02

도입부를 보면 라우터의 경우 어차피 토큰 잔고를 안들고 있기 때문에 원할 경우 언제든지 바꿀 수 있다고 한다. 어차피 경로를 찾아주는 부분이기 때문에 딱히 더 필요가 없는것 같다.

https://docs.uniswap.org/protocol/V2/reference/smart-contracts/router-02

 

Router02 | Uniswap

Because routers are stateless and do not hold token balances, they can be replaced safely and trustlessly, if necessary. This may happen if more efficient smart contract patterns are discovered, or if additional functionality is desired. For this reason, r

docs.uniswap.org

조회 함수는 일단 생략하고 주요 상태변화 함수부터 살펴보도록 하겠다.

 

addLiquidity

먼저 Liquidity를 추가하는 addLiquidity 함수가 있다.

모든 가능한 경우에 대처하기 위하여 msg.sender는 라우터 컨트렉트에 amountADesired/amountBDesired 에 대한 양만큼을 각각 allownace로 넘겨주어야 한다는 전제 조건이 붙는다.

 

트랜잭션이 실행된때 이상정인 비율로 자산이 예치되어야 한다.

 

만약 넘겨받은 토큰에 대한 풀이 존재하지 않는다면 풀이 자동적으로 생성되게 된다.

 

입력값을 보자면 스왑을 할때 ADesired는 원하는 만큼의 숫자를 amountAmin은 원하는 만큼의 가격이 넘어갈 시 오류를 리턴해 주기위해 입력을 받게 된다.

 

여기까지 되었다면 이제 코드를 봐보도록 하겠다.

 

    // **** ADD LIQUIDITY ****
    function _addLiquidity(
        address tokenA,
        address tokenB,
        uint amountADesired,
        uint amountBDesired,
        uint amountAMin,
        uint amountBMin
    ) private returns (uint amountA, uint amountB) {
        // create the pair if it doesn't exist yet
        if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
            IUniswapV2Factory(factory).createPair(tokenA, tokenB);
        }
        (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
        if (reserveA == 0 && reserveB == 0) {
            (amountA, amountB) = (amountADesired, amountBDesired);
        } else {
            uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
            if (amountBOptimal <= amountBDesired) {
                require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
                (amountA, amountB) = (amountADesired, amountBOptimal);
            } else {
                uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
                assert(amountAOptimal <= amountADesired);
                require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
                (amountA, amountB) = (amountAOptimal, amountBDesired);
            }
        }
    }

먼저 첫번째 조건문은 pair가 없는 경우에는 생성을 해주는 경우이기 때문에 pair를 생성하는 부분이 여기에 해당되게 된다.

만약 getPair의 응답값이 0이라면 createPair를 바로 해버린다.

 

만약 풀이 있다면 첫번째 if 문은 통과가 되게 된다.

 

그 후에 변수를 선언 해주는 부분이 있는데

        (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);

요건 가스비를 아끼려고 이렇게 만든건데 아예 라이브러리화를 시켜서 하고 있다.

 

라이브러리에 가서 한번 살펴보자면 

    function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {
        (address token0,) = sortTokens(tokenA, tokenB);
        (uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();
        (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
    }

이렇게 나오게 되는데 이 부분을 살펴보자면 getReserves를 통하여서 

먼저 토큰을 주소값의 크기를 기준으로 순서대로 맞춰서 token0을 지정해주고 Uniswap V2에 call을 보내서 getReserves를 사용한다.

reserveA와 reserveB의 경우에는 sortTokens를 한 값과 마지막으로 비교를 한 후에 만약 같은 값이 나오면 reserveA와 B를 각각 reserve 0, 1로 두고 다른 값이 나오면 그 반대로 둔다.

 

요 부분은 결국 토큰 페어의 주소값을 통해서 오름차순으로 정렬하기 위한 방법이다.

 

 

그리고 만약 예치된 양이 없다면 예치만 하고 끝낸다.

        if (reserveA == 0 && reserveB == 0) {
            (amountA, amountB) = (amountADesired, amountBDesired);
        }

만약 첫 예치가 아닌 경우 그 다음 조건이 실행되게 된다.

else {
            uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
            if (amountBOptimal <= amountBDesired) {
                require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
                (amountA, amountB) = (amountADesired, amountBOptimal);
            } else {
                uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
                assert(amountAOptimal <= amountADesired);
                require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
                (amountA, amountB) = (amountAOptimal, amountBDesired);
            }
        }

또 라이브러리에서 quote라는걸 써서 살펴봐야 된다.

   // given some amount of an asset and pair reserves, returns an equivalent amount of the other asset
    function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
        require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');
        require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
        amountB = amountA.mul(reserveB) / reserveA;
    }

요걸 보면 결국 Reserve (총 예치량) 과 amount (LP 토큰의 잔고)가 같으며 이를 이용하여 한쪽의 자산 정보와 잔고 정보를 가지고서 스왑 했을 때 받을 만큼의 값을 미리 알아볼 수 있게 된다. amount의 경우 실제 유니스왑 컨트렉트에서 형성된 가격 정보가 들어오게 되며 입력받은 값과의 비교를 통하여서 Optimal(최적의) 값을 구하고 해당 가격에 따라서 전송할량을 정하는 것이다.

 

요 부분이 햇갈려 한번 곰곰히 생각해봤는데

 

desired A : desired B = reserveA : reserveB

요게 성립한다고 생각하고 비율을 잡는다.

근데 이 비율은.. 유저가 호출하는건데 왜 성립한다고 보는 걸까?

 

그 이유는 어차피 lp 토큰 민트를 할때 최소 기준 단위 기준으로 mint를 하기 때문에 유저가 잘못 넣는다면 작은 값으로 비율이 알맞게 들어가며 나머지는 그냥 기부하는 샘이 되기 때문에 (lp 토큰 민트 없이 그냥 들어오면 전체 총량 k만 올라갈 것이다)

실제로 솔리디티 코드에서 막을 필요가 없는 것이다.

 

근데 UI를 설계한다면? 당연히 이건 프론트 단에서 계산을 미리 해주고 알맞은 수치를 넣어주어야 할 것이다.

프론트엔드 데이터와 컨트렉트를 같이 알아야 하는 부분이 이런 곳에서 나오겠구나.. 이런 생각이 든다.

 

(이 사실은 옆자리의 LukePark라는 필명으로 활동중인 분에게 물어보고 설명을 듣고 이해를 하게 되었다.)

 

그럼 Optimal한 값을 계산해 보면 실제로 유동성이 들어왔을때의 수정된 가격이 나오게 될 것이고 이제 교환을 하기 원하는 양 및 교환을 하려하는 최소 범위를 미리 알아낸다. 여기서 또 멘붕이 왔는데 ... 왜 값을 둘다 비교 안하고 하나만 비교하지?

 

else {
            uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
            if (amountBOptimal <= amountBDesired) {
                require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
                (amountA, amountB) = (amountADesired, amountBOptimal);
            } else {
                uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
                assert(amountAOptimal <= amountADesired);
                require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
                (amountA, amountB) = (amountAOptimal, amountBDesired);
            }

같은 이유에서이다. 그냥 한가지 기준점만 맞추게 되면 최악의 경우에도 맞는 기준점을 기준으로 lp토큰이 mint되기 때문에 굳이 양쪽을 검사할 필요가 없는 것이다. 그냥 하나만 기준점이랑 예측치가 잘 맞는지 보고 만약 맞다면 그게 그냥 Lp 토큰의 값이 되거나 그거보다 더 작은 값이 lp토큰의 값이 되는 것이다. lp 토큰을 덜 mint해주는건 로직에 상관이 없기 때문이다.

 

이를 통해 느낀바는 이벤트 로그로만 합산을 하게 되면 디파이는 어려운 경우가 많겠다는 생각이 들었다. 이런식으로 오입금?되거나 기부되는 형태로 취급되는 자산이 발생하는 케이스가 고의로든 실수로든 발생이 가능한 구조이기 때문에 기준점은 항상 현재 컨트렉트의 총 잔고로 봐야 되는 것 같다.

 

이제 해당 인터널 함수를 external로 뺀 부분을 보자면

    function addLiquidity(
        address tokenA,
        address tokenB,
        uint amountADesired,
        uint amountBDesired,
        uint amountAMin,
        uint amountBMin,
        address to,
        uint deadline
    ) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
        (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
        address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
        TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
        TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
        liquidity = IUniswapV2Pair(pair).mint(to);
    }

요거다. 보면 위에서 살펴본 리퀴디티를 가져와서 리턴값을 받아오면 해당 값을 변수로 바로 설정한 후에 pair를 바로 받아와서 lp토큰의 주소를 검색해버리고 각 토큰을 lp 토큰 컨트렉트 주소에 넣은 후에 mint 함수를 실행시킨다.

 

이전에 배웠던 mint 함수는 바로 여기서 쓰이는 것이였다. 

그리고 이더리움용 유동성 풀이 하나 더 있었는데

   function addLiquidityETH(
        address token,
        uint amountTokenDesired,
        uint amountTokenMin,
        uint amountETHMin,
        address to,
        uint deadline
    ) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {
        (amountToken, amountETH) = _addLiquidity(
            token,
            WETH,
            amountTokenDesired,
            msg.value,
            amountTokenMin,
            amountETHMin
        );
        address pair = UniswapV2Library.pairFor(factory, token, WETH);
        TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);
        IWETH(WETH).deposit{value: amountETH}();
        assert(IWETH(WETH).transfer(pair, amountETH));
        liquidity = IUniswapV2Pair(pair).mint(to);
        // refund dust eth, if any
        if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
    }

보아하니 payable이 달려있고 msg.value가 있다. 이건 이더리움을 입금 받아서 그만큼을 토큰 수량으로 치환하여 다루겠다는 의미이고 토큰 주소는 WETH로 되어 있는걸로 봐서 역시 밑 부분을 보아하니 deposit이 있다. 랩드 이더에서 deposit을 유동성이 공급된 결과 값 만큼 리턴을 해준다. 어차피 호출하는 인터널 트랜잭션을 같고 이제 그 후에 랩드 ㅇ이더리움 토큰을 민트를 해주게 되면 lp 토큰 주소에서 mint를 해주면서 끝이 나게 된다. 그리고 만약 민트까지 했는데 이더리움이 남았다면? 그 만큼의 이더리움은 다시 보내주게된다.

removeLiquidity

인출에 대한 로직은? 바로 반대로 하면 된다.

    function removeLiquidity(
        address tokenA,
        address tokenB,
        uint liquidity,
        uint amountAMin,
        uint amountBMin,
        address to,
        uint deadline
    ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
        address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
        IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair
        (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
        (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
        (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
        require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
        require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
    }

	function removeLiquidityETH(
        address token,
        uint liquidity,
        uint amountTokenMin,
        uint amountETHMin,
        address to,
        uint deadline
    ) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) {
        (amountToken, amountETH) = removeLiquidity(
            token,
            WETH,
            liquidity,
            amountTokenMin,
            amountETHMin,
            address(this),
            deadline
        );
        TransferHelper.safeTransfer(token, to, amountToken);
        IWETH(WETH).withdraw(amountETH);
        TransferHelper.safeTransferETH(to, amountETH);
    }

여기서 바로 lp 토큰을 burn 해준다. 근데 burn의 경우에는 위의 2개를 불러주는 함수가 따로 존재한다. 그게 바로 아래의 함수들이다.

    function removeLiquidityWithPermit(
        address tokenA,
        address tokenB,
        uint liquidity,
        uint amountAMin,
        uint amountBMin,
        address to,
        uint deadline,
        bool approveMax, uint8 v, bytes32 r, bytes32 s
    ) external virtual override returns (uint amountA, uint amountB) {
        address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
        uint value = approveMax ? uint(-1) : liquidity;
        IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
        (amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline);
    }
    function removeLiquidityETHWithPermit(
        address token,
        uint liquidity,
        uint amountTokenMin,
        uint amountETHMin,
        address to,
        uint deadline,
        bool approveMax, uint8 v, bytes32 r, bytes32 s
    ) external virtual override returns (uint amountToken, uint amountETH) {
        address pair = UniswapV2Library.pairFor(factory, token, WETH);
        uint value = approveMax ? uint(-1) : liquidity;
        IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
        (amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline);
    }

여기선 permit을 쓴다. 미리 오프체인에 서명 정보를 받아놧다가 메타 트랜잭션을 보내는 부분인데 그냥 approve라고 보면된다. 아무튼 해당 컨트렉트에 권한이 이미 있기 때문에 이땐 바로 자산을 받아와서 remove를 호출하면 된다. 그리고 approveMax 부분은 그냥 infinite approve 적용여부를 의미하는걸로 보인다.

 

코드가 여기서 더잇었는데 이건 그냥 수수료가 적용된 버전이라고 한다. 

 

=> 찾아보니 로직이 달라서 다시 정리해두었다.

https://it-timehacker.tistory.com/342

    function removeLiquidityETHSupportingFeeOnTransferTokens(
        address token,
        uint liquidity,
        uint amountTokenMin,
        uint amountETHMin,
        address to,
        uint deadline
    ) public virtual override ensure(deadline) returns (uint amountETH) {
        (, amountETH) = removeLiquidity(
            token,
            WETH,
            liquidity,
            amountTokenMin,
            amountETHMin,
            address(this),
            deadline
        );
        TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this)));
        IWETH(WETH).withdraw(amountETH);
        TransferHelper.safeTransferETH(to, amountETH);
    }
    function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(
        address token,
        uint liquidity,
        uint amountTokenMin,
        uint amountETHMin,
        address to,
        uint deadline,
        bool approveMax, uint8 v, bytes32 r, bytes32 s
    ) external virtual override returns (uint amountETH) {
        address pair = UniswapV2Library.pairFor(factory, token, WETH);
        uint value = approveMax ? uint(-1) : liquidity;
        IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
        amountETH = removeLiquidityETHSupportingFeeOnTransferTokens(
            token, liquidity, amountTokenMin, amountETHMin, to, deadline
        );
    }

요거는 근데 정말 왜 같은 기능을 이름만 다르게? 

supprot fee-0n transfer tokens라는 말이 있는데 이게 보낼때마다 디플레이션이 발생하는 토큰이 잇따고 한다.

그런경우 이걸 상속받아서 알아서 고쳐쓰라고 이렇게 정의해 두었다고 한다.

(참 복잡하다..)

 

swap

드디어 스왑이다. 코어 보다 훨씬 코드가 길다. 구조를 좀 더 나눴으면 어떨까 싶은 생각인데. 씨포트랑 유니스왑 v2 중간 수준으로 구분해주면 참 좋을거 같다.

 

일단 첫번째 양은 이미 첫번째 페어로 들어간 것을 가정하는 걸로봐서는 이건 2번째 스왑부터의 페어인거 같다.

 

따라서 여기서의 swap은 단일이 아닌 경로를 배열로 받게 된다. 그래서 배열의 요소대로 반복하면서 스왑을 실행시켜 주게 된다. amounts는 마찬가지로 배열로 그 양만큼을 입력받아서 스왑할때 넣어주는 부분이다.

 

wap의 경우에는 sort를 해서 정렬을 해준면서 시작한다. 예전에 비슷한 상황에서 이중 매핑을 썼었는데 이래서 cs가 중요하구나 싶다. 아무튼 순서를 해서 페어에 맞게 딱 지정해 준다.

            (address input, address output) = (path[i], path[i + 1]);
            (address token0,) = UniswapV2Library.sortTokens(input, output);
            uint amountOut = amounts[i + 1];
            (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));

위의 코드 부분이다. token0으로 기준을 잡아 주었으니 조건문을 통하여서 교환의 대가로 받는 토큰을 체크해서 주는 토큰이 뭔지 알아낸다. 그럼 주는 토큰을 알아냈으니 누가 받아야되는지도 알아내면 보낼 수 있을 것이다.

 

누가보내는 지는 마지막 단계에서는 알 필요가 없기에 현재 카운터 i의 값을 가지고 유효한지 체크해준 후에 만약 유효하다면 output과 그 다음 output 값을 넣어서 본다. 

 

아까 생략을 생략하긴 했는데 input과 output은

            (address input, address output) = (path[i], path[i + 1]);

이런 식으로 경로의 값을 하나씩 순회하며 정해지게 된다. 어차피 나온 결과값의 토큰을 다음 입력값으로 넣어주기 때문이다. 아무튼 이런식으로 하기 때문에 PairFor를 넣을 때 현재 받은 output기준의 다음값을 지정하려면 path[i+2]를 지정해 주어야 하고 만약 이게 마지막 값이라면 그냥 입력받은 _to를 써서 끝내버리면 된다.

 

그후는 그냥 이전에 공부한 값에다가 swap 명령어를 내리는 것이다.

    // **** SWAP ****
    // requires the initial amount to have already been sent to the first pair
    function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {
        for (uint i; i < path.length - 1; i++) {
            (address input, address output) = (path[i], path[i + 1]);
            (address token0,) = UniswapV2Library.sortTokens(input, output);
            uint amountOut = amounts[i + 1];
            (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
            address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
            IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
                amount0Out, amount1Out, to, new bytes(0)
            );
        }
    }

 그럼 이 swap은 어디서 호출해서 쓰일까??

    function swapExactTokensForTokens(
        uint amountIn,
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    ) external virtual override ensure(deadline) returns (uint[] memory amounts) {
        amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
        require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
        TransferHelper.safeTransferFrom(
            path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
        );
        _swap(amounts, path, to);
    }
    function swapTokensForExactTokens(
        uint amountOut,
        uint amountInMax,
        address[] calldata path,
        address to,
        uint deadline
    ) external virtual override ensure(deadline) returns (uint[] memory amounts) {
        amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
        require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
        TransferHelper.safeTransferFrom(
            path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
        );
        _swap(amounts, path, to);
    }

바로 여기다. 보면 이제 하나는 입력값에서 최대값을 설정해주고 out을 받는다고 치면 

        require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');

요 식을 추가해서 최대 교환하려는 토큰 값을 벗어날 경우 예외처리를 해주고

 

반대로 기준점을 인출값으로 잡는다고 치면

        require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');

이런예외값을 너어주면 된다.

 

마찬가지로 이것도 이더리움용에다가 디플레이션 토큰 용까지 함수가 또 중복된다./// (제발 분리좀 시켜줘 ㅠㅠ)

 

Library Fuctions

마지막 값들은 바로 라이브러리 함수들인데

    // **** LIBRARY FUNCTIONS ****
    function quote(uint amountA, uint reserveA, uint reserveB) public pure virtual override returns (uint amountB) {
        return UniswapV2Library.quote(amountA, reserveA, reserveB);
    }

    function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut)
        public
        pure
        virtual
        override
        returns (uint amountOut)
    {
        return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut);
    }

    function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut)
        public
        pure
        virtual
        override
        returns (uint amountIn)
    {
        return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut);
    }

    function getAmountsOut(uint amountIn, address[] memory path)
        public
        view
        virtual
        override
        returns (uint[] memory amounts)
    {
        return UniswapV2Library.getAmountsOut(factory, amountIn, path);
    }

    function getAmountsIn(uint amountOut, address[] memory path)
        public
        view
        virtual
        override
        returns (uint[] memory amounts)
    {
        return UniswapV2Library.getAmountsIn(factory, amountOut, path);
    }

이렇게 라이브러리화를 시킨걸 또 이쁘게 조회 함수로 만들어서 사용하고 있다.

 

이런 구조도 정말 좋은거 같다. 뭔가 코어 부분을 만들고 거기에 대한 라이브러리를 만들고 확장 버전에서는 라이브러리를 통해서 코어와 주고 받는 부분.

 

내가 만약 이전 프로젝트들을 유니스왑 코드를 보고 찠다면 더 효율적으로 짤 수 있지 않았을까 싶다.

 

이렇게 유니스왑 분석의 대장정을 마치고 싶지만..

사실 Zap이라는게 하나 더 남았다고 한다.

 

그건 내일이어서봐야겠다 ㅎㅎㅎ

728x90
반응형
Comments