BlockBaseのエンジニアの@suhara_pontaです。
これまでAMMの概要、コントラクトの全体像を見てきました。
今回はコントラクトの詳細、特に価格算定ロジックを見ていきたいと思います。
レポジトリはこちらです。
流動性供給 = LPトークンのmint
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
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が計算される部分です。
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;
}
一度手数料を考えずに式を立てると、こうなりますが、
変形させると以下のようになって、Solidityの式通りになります(久しぶりに通分とかしました笑)
amountIn.mul(998);
とreserveIn.mul(1000)
というのは手数料だと考えています。
0.17%と0.03%の合計で0.2%かなと推測していますが自信がないのでどなたかわかる方、教えてください。
上記で計算してきたamountをcoreのPancakePair.solのswapのinputとして使っています。
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