# PancakeSwapの中身を読む

By [narvik](https://paragraph.com/@narvik) · 2022-02-11

---

※ 2021年7月19日にnoteで投稿した記事の移植です。

はじめに
----

この記事ではPancakeSwapのコントラクトがswap時にどのような挙動をするかを記載しています。具体的には基本的なswapの種類の説明と、swapExactTokensForTokensに対して内部実装の解説です。誤っている点があったらコメントを頂けると嬉しいです。

またPancakeSwapはUniswap v2のフォークなので事実上Uniswap v2の説明となります。PancakeSwapのドキュメントにもUniswap v2のドキュメントを読めと記載されているので記事中ではUniswap v2のドキュメントを参照します。

前提知識
----

swapはざっくりと以下のようなシーケンスで行われます。

![](https://storage.googleapis.com/papyrus_images/2006b0d15b145d6880f4af74ac026322b977a6ff3bdbb9bc55a84805817b2990.png)

swap前にトークンにapproveが必要な理由はswap時にPancakeRouterが対象トークンのコントラクトに対してtransferFromというメソッドを利用してトークンを移動させますが、この際にapproveで指定された量までしか移動できないためです。

approveは小切手を発行するメソッドです。指定したアドレスに対し、approveで指定した量までtransferFrom経由で移動させることを許可します。例えばapproveで数量: 100を指定した場合、対象者が100の数量をtransferFromで移動させると対象者はそれ以上transferFromで移動させることができません。利便性の観点からrouterにapproveするときはかなり大きな数値を指定するようになっているため、小切手が尽きないようになっています。

Swapの種類
-------

Uniswapのswap系メソッドは以下にまとまっています。

[https://docs.uniswap.org/protocol/V2/reference/smart-contracts/router-02](https://docs.uniswap.org/protocol/V2/reference/smart-contracts/router-02)

メソッドはswap{A}For{B}の命名規則になっています。Aを入力トークン、Bを出力トークンを呼びます。またETHは基軸通貨と呼びます。

まずExactの説明です。ExactがAの前にある場合は出力トークンの最小量を指定します。Bの前にある場合は入力トークンの最大量を指定します。UIから指定する際に入力トークンの量を指定して購入する場合はswapExact{A}For{B}となり、出力トークンの量を指定して購入する場合はswap{A}ForExact{B}となります。UIでは以下のように変化します。

![Fromに入力した場合はswapExact{A}For{B}となり最小出力量が表示される](https://storage.googleapis.com/papyrus_images/8c7dc1c3d226996d4f3510b5a8dd4d0a3196be185a28e16afa5bede70e7fa7d1.png)

Fromに入力した場合はswapExact{A}For{B}となり最小出力量が表示される

![Toに入力した場合はswap{A}ForExact{B}となり最大入力量が表示される](https://storage.googleapis.com/papyrus_images/a4a9978d5fbd9223c7207b60fa97ae212f7a77351def311faa17818d186fef4f.png)

Toに入力した場合はswap{A}ForExact{B}となり最大入力量が表示される

Toに入力した場合はswap{A}ForExact{B}となり最大入力量が表示される

slippageを変化させるとMinimum received, Maximum soldの値が変化します。また、transfer taxがかかるトークンとswap{A}ForExact{B}は相性が悪いです。To側の数量を指定して売買をするためには自分でtransfer taxを加味した数値にする必要があるためです。

次にETHの説明です。swap{A}For{B}のAにETHが来るパターンとBにETHが来るパターンがあります。これは入力トークンに基軸通貨を使うか、出力に基軸通貨を使う場合に利用されます。基軸通貨は他のトークンを扱いが違うためメソッドも分かれています。端的に他のトークンとの違いを言うと基軸通貨にはアドレスが存在しません。

基軸通貨をWrapし、WETH、WBNBなどにすることで他のトークンと同様の扱いが行えるようになります。swapETHFor{B}の場合は最初のRouteがWrapped Tokenとなり、swap{A}ForETHの場合は最後のRouteがWrapped Tokenとなります。

最後にswap{A}For{B}SupportingFeeOnTransferTokensについてです。これは転送料がかかるトークンに対して利用します。前提知識で述べた通り、swapするためには一度PancakePairのLPアドレスにトークンをtransferする必要があります。transfer taxがかかるトークンの場合、PancakePairのLPアドレスにトークンが到着した時点でFromに指定した値より小さくなっているケースが考えられます。それを考慮したswapが行えるメソッドはSupportingFeeOnTransferTokensのsuffixがついています。

内部挙動
----

ここからが本題です。swapExactTokensForTokensのコードは以下のようになっています。

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

メソッドの引数はamountIn, amountOutMin, path, to, deadlineの5つです。それぞれ以下のような意味です。

*   amountIn: 入力トークンの数量
    
*   amountOutMin: 出力トークンの最低量(slippage考慮後の値)
    
*   path: トークンをswapするためのroute
    
*   to: 交換した後にtransferする対象、基本的に自分のアドレスが入る
    
*   deadline: 指定された時刻を過ぎた場合はswapを中止する
    

pathについて説明すると、トークンをswapするためにどのLP(流動性)を使ってswapするかを指定します。例えばBUSDをCAKEにswapしたい場合、

*   BUSD-CAKEのLPだけ存在する
    
    *   BUSDアドレス,CAKEアドレスがrouteになる
        
*   USDT-BUSD、USDT-CAKEのLPだけ存在する
    
    *   BUSDアドレス,USDTアドレス,CAKEアドレスがrouteになる
        
    *   BUSDアドレス,CAKEアドレスを指定してもLPのrouteがないのでNG
        

といった形で存在するLPを元にrouteを組み立てる必要があります。PancakeSwapはUI側でrouteを特定のトークンに制限して総当りで検証し、最も良いレートのrouteが選ばれるようになっています。反面、試行ルートが多いのでレートの反映が遅いケースもあります。

最初に PancakeLibrary.getAmountsOut(factory, amountIn, path) を行っています。これは一言で言うと指定されたroute, amountInで取得できるトークン量を計算しています。実際の処理を見てみます。

    function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {
       require(path.length >= 2, 'PancakeLibrary: INVALID_PATH');
       amounts = new uintUnsupported embed;
       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);
       }
    }
    

for文を利用して繰り返しgetReservesとgetAmountOutを呼び出しています。例えばrouteにBUSDアドレス,USDTアドレス,CAKEアドレスを指定した場合、

1.  BUSDアドレス,USDTアドレスでgetReservesを呼ぶ
    
2.  得られたreserveIn, reserveOutを使ってBUSD->USDTへのswapで得られる数量をgetAmountOutで取得
    
3.  USDTアドレス,CAKEアドレスでgetReservesを呼ぶ
    
4.  得られたreserveIn, reserveOutを使ってUSDT->CAKEへのswapで得られる数量をgetAmountOutで取得
    
5.  それぞれのpathでswapした結果を返す
    

という挙動になります。getReservesとgetAmountOutの処理を見てみます。

    // Router側
    function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {
       (address token0,) = sortTokens(tokenA, tokenB);
       pairFor(factory, tokenA, tokenB);
       (uint reserve0, uint reserve1,) = IPancakePair(pairFor(factory, tokenA, tokenB)).getReserves();
       (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
    }
       
    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(9975);
       uint numerator = amountInWithFee.mul(reserveOut);
       uint denominator = reserveIn.mul(10000).add(amountInWithFee);
       amountOut = numerator / denominator;
    }
    
    // Factory側(PancakePair)
    function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
       _reserve0 = reserve0;
       _reserve1 = reserve1;
       _blockTimestampLast = blockTimestampLast;
    }
    

まずRouter側ではgetReserves内でトークンA,トークンBのLPアドレスを計算し、LPアドレス内のトークン量を取得します。その後getAmountOutでは流動性の状態とamountInの値を使ってamountOutを算出します。uint amountInWithFee = amountIn.mul(9975) と記述されている部分はPancakeSwapの取引手数料です。ドキュメントに記載されている通り、0.25%がtrading feeとして徴収されます。

上記の計算をした結果、得られるトークン量が指定されたamountOutMinを下回る場合、その時点でswapが失敗します。上回った場合はTransferHelper.safeTransferFromを利用し、Fromに指定されたトークン量をLPアドレスに移動し、\_swapを行います。

    // Router側
    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,) = PancakeLibrary.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 ? PancakeLibrary.pairFor(factory, output, path[i + 2]) : _to;
           IPancakePair(PancakeLibrary.pairFor(factory, input, output)).swap(
               amount0Out, amount1Out, to, new bytes(0)
           );
       }
    }
    
    // Factory側(PancakePair)
    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(10000).sub(amount0In.mul(25)));
       uint balance1Adjusted = (balance1.mul(10000).sub(amount1In.mul(25)));
       require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(10000**2), 'Pancake: K');
       }
    
       _update(balance0, balance1, _reserve0, _reserve1);
       emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
    }
    

swapではLPのトークン保有量に不整合が発生しないようにしたり、FlashSwapの補助を行ったりしています。Router側で\_swap(..., new bytes(0))という記述がありますが、何らかの値を入力するとFlashSwap扱いになります。これらの処理をpathごとに行うことでswapが完了します。

まとめ
---

*   swapには入力トークン、出力トークンに応じていろいろなメソッドが使い分けられている
    
*   swap時にはrouteを辿り、複数回のswapが行われている(route毎にtrading feeがかかる)
    

シンプルなUIの裏でいろいろなことをやっていますね。 ユーザーに複雑さを感じさせない凄い仕組みだなぁと感じました。

コントラクトリンク
---------

*   PancakeRouter
    
*   PancakeFactory

---

*Originally published on [narvik](https://paragraph.com/@narvik/pancakeswap)*
