4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Liquidity Poolの価格算定ロジックを理解する(PancakeSwap勉強会③)

Posted at

BlockBaseのエンジニアの@suhara_pontaです。
これまでAMMの概要、コントラクトの全体像を見てきました。

今回はコントラクトの詳細、特に価格算定ロジックを見ていきたいと思います。

レポジトリはこちらです。

流動性供給 = LPトークンのmint

PancakePair.sol
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, 'Pancake: 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);
    }

totalSupply == 0のとき、つまり最初にLPが作られるとき、mintされるLPトークンの量はamount0 * amount1の平方根の近似値からMINIMUM_LIQUIDITYを引いたものになります。MINIMUM_LIQUIDITYが必要なのは、LPから全てのトークンが引き出されてしまうと、再度流動性を提供するときに価格が計算できなくなってしまうからだと推測しています(間違っていたらコメントで教えてください🙏)

totalSupply == 0ではないとき、つまりすでにあるLPに追加する場合、
amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);のどちらか小さいほうがmintされるLPトークンの量になります。
ここでは現在のLPトークンの総量 * (LPに入れるtokenの量 / LPに入っているtokenの量)を計算して、現在の総量に対する割合に応じたLPトークンを発行しています。

流動性の引き出し = LPトークンのburn

PancakePair.sol
    function burn(address to) external lock returns (uint amount0, uint amount1) {
        (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
        address _token0 = token0;                                // gas savings
        address _token1 = token1;                                // gas savings
        uint balance0 = IERC20(_token0).balanceOf(address(this));
        uint balance1 = IERC20(_token1).balanceOf(address(this));
        uint liquidity = balanceOf[address(this)];

        bool feeOn = _mintFee(_reserve0, _reserve1);
        uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
        amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
        amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
        require(amount0 > 0 && amount1 > 0, 'Pancake: INSUFFICIENT_LIQUIDITY_BURNED');
        _burn(address(this), liquidity);
        _safeTransfer(_token0, to, amount0);
        _safeTransfer(_token1, to, amount1);
        balance0 = IERC20(_token0).balanceOf(address(this));
        balance1 = IERC20(_token1).balanceOf(address(this));

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

計算ロジックは、現在LPにあるtokenの量 * (burnするLPトークンの量 / 現在発行されているLPトークンの総量)なので以下の部分がそれに該当しています。
amount0 = liquidity.mul(balance0) / _totalSupply;
amount1 = liquidity.mul(balance1) / _totalSupply;

Swap

トークンのswapではPancakePair.solのswap関数を実行する前に、pancake-swap-peripheryレポジトリのLibraryでamountを予測して表示する部分があります。

以下の画像の部分でFromを入力するとToのestimatedが計算される部分です。
スクリーンショット 2021-05-13 11.26.54.png

PancakeLibrary.sol
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
        require(amountIn > 0, 'PancakeLibrary: INSUFFICIENT_INPUT_AMOUNT');
        require(reserveIn > 0 && reserveOut > 0, 'PancakeLibrary: INSUFFICIENT_LIQUIDITY');
        uint amountInWithFee = amountIn.mul(998);
        uint numerator = amountInWithFee.mul(reserveOut);
        uint denominator = reserveIn.mul(1000).add(amountInWithFee);
        amountOut = numerator / denominator;
    }

一度手数料を考えずに式を立てると、こうなりますが、
スクリーンショット 2021-05-13 11.38.35.png
変形させると以下のようになって、Solidityの式通りになります(久しぶりに通分とかしました笑)
スクリーンショット 2021-05-13 11.40.46.png
amountIn.mul(998);reserveIn.mul(1000)というのは手数料だと考えています。
スクリーンショット 2021-05-13 11.43.00.png
0.17%と0.03%の合計で0.2%かなと推測していますが自信がないのでどなたかわかる方、教えてください。

上記で計算してきたamountをcoreのPancakePair.solのswapのinputとして使っています。

PancakePair.sol
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
        require(amount0Out > 0 || amount1Out > 0, 'Pancake: INSUFFICIENT_OUTPUT_AMOUNT');
        (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
        require(amount0Out < _reserve0 && amount1Out < _reserve1, 'Pancake: 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, 'Pancake: 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) IPancakeCallee(to).pancakeCall(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, 'Pancake: INSUFFICIENT_INPUT_AMOUNT');
        { // scope for reserve{0,1}Adjusted, avoids stack too deep errors
        uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(2));
        uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(2));
        require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'Pancake: K');
        }

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

transferを先にしたあとに、不整合がないかを確認して(不整合があればtransactionは巻き戻ります)、balanceとreserveをアップデートしています。

まとめ

PancakeSwapのコードを見ましたが、繰り返しになりますがUniswap V2と同じです。また、世の中に多く出ている〇〇swapのほぼ全てがUniswap v2のクローンなので、それらがどう動いているかを理解できたと思います。

今後はPancakeSwapのほかの機能のコントラクトを見ていきたいと思います。

BlockBaseではこのような勉強会を定期的に開催したり、プロダクト開発の様子などもDiscordで公開しているのでぜひ参加してみてください。
https://discord.com/invite/7P3NChCxhM

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?