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

BlockBaseのエンジニアの[@suhara_ponta](https://twitter.com/suhara_ponta)です。
これまでAMMの概要、コントラクトの全体像を見てきました。

https://qiita.com/ksuhara/items/fc23ca9bea203f795054

https://qiita.com/ksuhara/items/8e6fdc4da527e77692de

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

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

https://github.com/pancakeswap/pancake-swap-core

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

```solidity: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
```solidity: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を予測して表示する部分があります。

https://github.com/pancakeswap/pancake-swap-periphery/blob/d769a6d136b74fde82502ec2f9334acc1afc0732/contracts/libraries/PancakeLibrary.sol#L44

以下の画像の部分でFromを入力するとToのestimatedが計算される部分です。
![スクリーンショット 2021-05-13 11.26.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505435/fc8de06d-b253-6682-7287-885446987e11.png)

```solidity: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505435/6ae740a8-636f-599f-9b45-cde8ad56b70f.png)
変形させると以下のようになって、Solidityの式通りになります（久しぶりに通分とかしました笑）
![スクリーンショット 2021-05-13 11.40.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505435/f876de8d-9ab3-1c0b-8430-e57aedca496a.png)
`amountIn.mul(998);`と`reserveIn.mul(1000)`というのは手数料だと考えています。
![スクリーンショット 2021-05-13 11.43.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505435/21730fb7-2872-c52f-adeb-1c08a6f20d1c.png)
0.17%と0.03%の合計で0.2%かなと推測していますが自信がないのでどなたかわかる方、教えてください。

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

```solidity: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