# PancakeSwapの中身を読む **Published by:** [narvik](https://paragraph.com/@narvik/) **Published on:** 2022-02-11 **URL:** https://paragraph.com/@narvik/pancakeswap ## Content ※ 2021年7月19日にnoteで投稿した記事の移植です。はじめにこの記事ではPancakeSwapのコントラクトがswap時にどのような挙動をするかを記載しています。具体的には基本的なswapの種類の説明と、swapExactTokensForTokensに対して内部実装の解説です。誤っている点があったらコメントを頂けると嬉しいです。 またPancakeSwapはUniswap v2のフォークなので事実上Uniswap v2の説明となります。PancakeSwapのドキュメントにもUniswap v2のドキュメントを読めと記載されているので記事中ではUniswap v2のドキュメントを参照します。前提知識swapはざっくりと以下のようなシーケンスで行われます。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 メソッドは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}となり最小出力量が表示される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するためのrouteto: 交換した後に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アドレスを指定した場合、BUSDアドレス,USDTアドレスでgetReservesを呼ぶ得られたreserveIn, reserveOutを使ってBUSD->USDTへのswapで得られる数量をgetAmountOutで取得USDTアドレス,CAKEアドレスでgetReservesを呼ぶ得られたreserveIn, reserveOutを使ってUSDT->CAKEへのswapで得られる数量をgetAmountOutで取得それぞれの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の裏でいろいろなことをやっていますね。 ユーザーに複雑さを感じさせない凄い仕組みだなぁと感じました。コントラクトリンクPancakeRouterPancakeFactory ## Publication Information - [narvik](https://paragraph.com/@narvik/): Publication homepage - [All Posts](https://paragraph.com/@narvik/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@narvik): Subscribe to updates - [Twitter](https://twitter.com/narvik_eth): Follow on Twitter