체인의정석

UniswapV2 정리4 : fee-on-transfer tokens 함수들 본문

블록체인/디파이

UniswapV2 정리4 : fee-on-transfer tokens 함수들

체인의정석 2022. 10. 27. 11:31
728x90
반응형

uniswapV2Router를 보니 같은 함수가 2개씩 (이름만 다르게) 있는 것을 확인 할 수 있었다.

주석에는 보낼때마다 수수료가 발생하는 토큰의 경우 사용한다고 한다.

 

https://github.com/ethereum/EIPs/issues/3214

 

ERC20 Extending: token with fee on transfer · Issue #3214 · ethereum/EIPs

Simple Summary Extending ERC20 interface for tokens with fee on transfer. Abstract This standard defines new interfaces a token with fee on transfer should implement, while remaining backwards comp...

github.com

https://dex.globiance.com/fott

 

https://dex.globiance.com/fott

 

dex.globiance.com

몇몇개의 토큰의 경우 슬리피지 조정을 위해서 이러한 토큰들을 고려한 코딩을 짜야 한다고 한다.

 

그래서 그걸 고려한 코딩이 뭔지 한번 봤는데

 

 
    function swapExactTokensForTokensSupportingFeeOnTransferTokens(
        uint amountIn,
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    ) external virtual override ensure(deadline) {
        TransferHelper.safeTransferFrom(
            path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
        );
        uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
        _swapSupportingFeeOnTransferTokens(path, to);
        require(
            IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
            'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
        );
    }


    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);
    }

구조는 거의 비슷하지만 내부적으로 호출하는 internal 함수의 경우에는 다른것 같다.

 

그럼 이 차이를 살펴보면

    // **** 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이고

 

   // **** SWAP (supporting fee-on-transfer tokens) ****
    // requires the initial amount to have already been sent to the first pair
    function _swapSupportingFeeOnTransferTokens(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);
            IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output));
            uint amountInput;
            uint amountOutput;
            { // scope to avoid stack too deep errors
            (uint reserve0, uint reserve1,) = pair.getReserves();
            (uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
            amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput);
            amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput);
            }
            (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0));
            address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
            pair.swap(amount0Out, amount1Out, to, new bytes(0));
        }
    }

요건 전송 수수료 모델이 적용된 swap인데

차이가 있는 부분은 결국

 

        { // scope to avoid stack too deep errors
            (uint reserve0, uint reserve1,) = pair.getReserves();
            (uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
            amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput);
            amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput);
        }

이 부분이다.  일단 Input 부분을 계산할 때는 현재 lp토큰의 잔고에서 reserveInput을 가져와서 차감해준 후 가치를 다시 산정해 준다.

만약 가치가 줄어들었다면 amountInput에 반영이 될 것이고 이에 따라서 Uniswap 내부적으로 swap을 할때 min을 통해 적은 가치를 산정해 주기 때문에 정상적으로 작동하게 된다.

 

이 행위는 일단 전송이 이루어지고 나서 실행되는 internal 함수이기 때문에 문제가 없다고 할 수 있다.

    // performs chained getAmountOut calculations on any number of pairs
    function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {
        require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
        amounts = new uint[](path.length);
        amounts[0] = amountIn;
        for (uint i; i < path.length - 1; i++) {
            (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);
            amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
        }
    }

getAmountOut을 보면 getReserves에서 유니스왑의 getReserves를 호출하게 되고 그때 인자값으로 쓰이는 부분이 factory 주소와 2개의 토큰 주소이다.

    function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
        _reserve0 = reserve0;
        _reserve1 = reserve1;
        _blockTimestampLast = blockTimestampLast;
    }

그 결과 값을 256바이트를 쪼개서 호출하게 되며,

받은 값들 중 reserce 0, 1은 유니스왑 라이브러리에서 순서를 정렬해 주기 때문에

해당 순서를 기반으로 하여서 

    // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset
    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;
    }

getAmountOut을 실행시켜 주면된다.

이 부분은 수수료 0.3%를 계산해 주는 부분인데 현재 유니스왑은 lp풀이 0.3%를 다 가져간다고는 한다.

이 식은 정말 복잡했는데 일단 크게 amount In 과 amount out의 곱을 K라고 보면 입력값과 확인한 값을 섞어서 해보면

0.97K 가 나오는것 같다.

(시간관계상 대략적으로만 계산했어서 계산이 정확한지는 모르겠습니다)

728x90
반응형
Comments