【3部作】これであなたもUniswapV2チョットデキル【中編】:君が主役だ!LPトークンスペシャル!!
導入:前編の続きです。
基本的な用語や前提は以下の前編で全て整理していますので、
本記事を読まれる方は前編から確認していただくことを推奨いたします。
本題
流動性ライフサイクル
前回の前編では、流動性
今回からは、いよいよ、ここまで整理して来た理解を元に、
UniswapV2の各機能をみていくことなります。
しかし、前回同様中心テーマは流動性です。
流動性の誕生(作成)、成長(報酬・追加による拡大)、衰退(引出による縮小)を念頭に、
整理して理解していきます。
前編を思い出してください。
あなたは、2つのトークンの入ったボトルの管理者でした。
そして、上のグラフを見て来たように、
ボトルの中の流動性
ユーザーは損をしにくいということでした。
さてさて、しかし、実際はどうでしょうか?
実際のあなたは、ボトルの管理者ではなく、
流動性を提供する1ユーザーです。
そして、流動性を提供するのは、
利益を得ること、ヘッジすることなどを目的とした、
あなたやユーザーの皆さんです。
みんなのために流動性を多くしよう!なんて考えますか?
(ガバナンストークンの発行主体ならまあ...)
あなたはプールに、大切な資産を寄付したいわけではありません。
流動性提供者として利益を得るために、
大事な資産を、危険いっぱい、リスクいっぱいの
UniswapV2のUniswapV2Pairコントラクトに預けるわけです。
善意で大量に預けようとは到底思いませんね。
というわけでここからは、
より良いサービスを提供したい(より良い価格を提供し、人を集め、利益を得たい)
ボトルの管理者というロールと
これとはほぼ反対に位置する、
単に利益を求めるユーザーというロールの
2つの視点を同居させながら、
UniswapV2の理解を深めていきます。
その2つの視点から、
流動性を提供するとはどういうことか、
また、流動性を提供することで、
どのような利益が得られ、
どのようなリスクを被るかなどを整理していきましょう。
【誕生】流動性の初期化とLPトークン
UniswapV2では、
様々なトークンの交換場所(ボトル)を1から作る、
つまり、プールを生成し、流動性を初期化することができます。
また、すでに作られているペアに流動性を追加することができます。
具体的には、すでに整理して来た、
UniswapV2Router02コントラクト
UniswapV2Pairコントラクト
を介して流動性の初期化・追加を行うことができます。
復習として、再度UniswapV2のコントラクトの構成イメージを共有しておきます。
(出典:https://github.com/adshao/publications/blob/master/uniswap/dive-into-uniswap-v2-contracts/README.md)
また、流動性の初期化・追加に該当するUniswapV2の具体的な実装は以下です。
v2-periphery/contracts /UniswapV2Router02.sol > _addLiquidity, addLiquidity
// **** ADD LIQUIDITY ****
function _addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin
) internal virtual returns (uint amountA, uint amountB) {
// create the pair if it doesn't exist yet
if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
IUniswapV2Factory(factory).createPair(tokenA, tokenB);
}
(uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
if (reserveA == 0 && reserveB == 0) {
(amountA, amountB) = (amountADesired, amountBDesired);
} else {
uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
if (amountBOptimal <= amountBDesired) {
require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
(amountA, amountB) = (amountADesired, amountBOptimal);
} else {
uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
assert(amountAOptimal <= amountADesired);
require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
(amountA, amountB) = (amountAOptimal, amountBDesired);
}
}
}
function addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
liquidity = IUniswapV2Pair(pair).mint(to);
}
v2-core/contracts /UniswapV2Pair.sol
// this low-level function should be called from a contract which performs important safety checks
// この低レベル関数は、重要な安全確認を実行するコントラクトから呼び出される必要があります
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
// ガスの節約、_mintFee内でtotalSupplyが更新される可能性があるため、ここで定義する必要があります
if (_totalSupply == 0) {
liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
_mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
// 最初のMINIMUM_LIQUIDITYトークンを永久にロック
} else {
liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
}
require(liquidity > 0, 'UniswapV2: 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
// reserve0とreserve1は最新の状態です
emit Mint(msg.sender, amount0, amount1);
}
ERC20の簡単な整理
さて、ここでERC20を前提とするコードがあるので、整理しておきます。
まず、UniswapV2Pairコントラクトは、ERC20に準拠しているトークンです。
そして、様々な基礎トークンをペアに持ち、
それらの情報を管理する各UniswapV2Pairコントラクトの別名が、
LPトークンです。
より詳しくいうと、
UniswapV2Factoryコントラクトという設計図実体化工場が、
UniswapV2Pairコントラクトという設計図のようなものを持っており、
それに任意の基礎トークン2つのアドレスを設定して、
createPairメソッドを実行することで、LPトークンを生み出します。
(※LPトークンの発行とは別で、
LPトークン(コントラクト)自体のデプロイの話)
これをインスタンス化といったりします。
(魂を入れるみたいな?
メドロットとそのメダルとか?
レプリカントにゲシュタルトとか?)
再度全体の構成図です。
(出典:https://github.com/adshao/publications/blob/master/uniswap/dive-into-uniswap-v2-contracts/README.md)
実装では、以下UniswapV2FactoryコントラクトのcreatePair
に、
tokenA、tokenBが設定されたのち実行され、
UniswapV2Pairコントラクトのconstructor
が実行されることにより、
魂をもった?UniswapV2Pairコントラクトが実体化します。
contracts/UniswapV2Factory.sol
pragma solidity =0.5.16;
import './interfaces/IUniswapV2Factory.sol';
import './UniswapV2Pair.sol';
contract UniswapV2Factory is IUniswapV2Factory {
address public feeTo;
address public feeToSetter;
mapping(address => mapping(address => address)) public getPair;
address[] public allPairs;
event PairCreated(address indexed token0, address indexed token1, address pair, uint);
constructor(address _feeToSetter) public {
feeToSetter = _feeToSetter;
}
function allPairsLength() external view returns (uint) {
return allPairs.length;
}
function createPair(address tokenA, address tokenB) external returns (address pair) {
require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
// 単一のチェックで十分である
bytes memory bytecode = type(UniswapV2Pair).creationCode;
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
IUniswapV2Pair(pair).initialize(token0, token1);
getPair[token0][token1] = pair;
getPair[token1][token0] = pair; // populate mapping in the reverse direction
// 逆方向のマッピングを埋める
allPairs.push(pair);
emit PairCreated(token0, token1, pair, allPairs.length);
}
function setFeeTo(address _feeTo) external {
require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
feeTo = _feeTo;
}
function setFeeToSetter(address _feeToSetter) external {
require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
feeToSetter = _feeToSetter;
}
}
v2-core/contracts /UniswapV2Pair.sol > constructor
constructor() public {
factory = msg.sender;
}
このLPトークンは、
ERC20に準拠しているのですが、
それはどういうことかというと、
以下のような例えば、OpenZeppelin作成のERC20.solのような実装を持っているということです。
contracts/token/ERC20/ERC20.sol
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/ERC20.sol)
pragma solidity ^0.8.20;
import {IERC20} from "./IERC20.sol";
import {IERC20Metadata} from "./extensions/IERC20Metadata.sol";
import {Context} from "../../utils/Context.sol";
import {IERC20Errors} from "../../interfaces/draft-IERC6093.sol";
/**
* @dev {IERC20}インターフェースの実装。
*
* この実装はトークンの作成方法には依存しません。つまり、供給メカニズムは
* {_mint}を使用して派生契約で追加する必要があります。
*
* TIP: 詳細な解説については、次のガイドを参照してください
* https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[供給メカニズムの実装方法]。
*
* {decimals}のデフォルト値は18です。これを変更するには、この関数を
* オーバーライドして別の値を返すようにする必要があります。
*
* OpenZeppelin Contractsの一般的なガイドラインに従っています:関数が失敗した場合に
* `false`を返す代わりにリバートします。この動作は一般的であり、
* ERC-20アプリケーションの期待に反するものではありません。
*/
abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors {
mapping(address account => uint256) private _balances;
mapping(address account => mapping(address spender => uint256)) private _allowances;
uint256 private _totalSupply;
string private _name;
string private _symbol;
/**
* @dev {name}および{symbol}の値を設定します。
*
* これら2つの値は不変です: それらは構築中に一度だけ設定できます。
*/
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
/**
* @dev トークンの名前を返します。
*/
function name() public view virtual returns (string memory) {
return _name;
}
/**
* @dev トークンのシンボルを返します。通常は名前の短縮バージョンです。
*/
function symbol() public view virtual returns (string memory) {
return _symbol;
}
/**
* @dev ユーザー表現に使用される小数点以下の桁数を返します。
* 例えば、`decimals`が`2`の場合、`505`トークンの残高はユーザーに`5.05`として表示されるべきです(`505 / 10 ** 2`)。
*
* トークンは通常、EtherとWeiの関係を模倣して18の値を選択します。これは、この関数で返されるデフォルト値であり、
* オーバーライドしない限り変更されません。
*
* 注意: この情報は表示目的でのみ使用され、契約の算術計算には一切影響しません。
* {IERC20-balanceOf}や{IERC20-transfer}なども含まれます。
*/
function decimals() public view virtual returns (uint8) {
return 18;
}
/**
* @dev {IERC20-totalSupply}を参照してください。
*/
function totalSupply() public view virtual returns (uint256) {
return _totalSupply;
}
/**
* @dev {IERC20-balanceOf}を参照してください。
*/
function balanceOf(address account) public view virtual returns (uint256) {
return _balances[account];
}
/**
* @dev {IERC20-transfer}を参照してください。
*
* 要件:
*
* - `to`はゼロアドレスではない必要があります。
* - 呼び出し者は少なくとも`value`の残高を持っている必要があります。
*/
function transfer(address to, uint256 value) public virtual returns (bool) {
address owner = _msgSender();
_transfer(owner, to, value);
return true;
}
/**
* @dev {IERC20-allowance}を参照してください。
*/
function allowance(address owner, address spender) public view virtual returns (uint256) {
return _allowances[owner][spender];
}
/**
* @dev {IERC20-approve}を参照してください。
*
* 注意: `value`が最大の`uint256`の場合、`transferFrom`時に許可は更新されません。
* これは、無限の承認と同義です。
*
* 要件:
*
* - `spender`はゼロアドレスではない必要があります。
*/
function approve(address spender, uint256 value) public virtual returns (bool) {
address owner = _msgSender();
_approve(owner, spender, value);
return true;
}
/**
* @dev {IERC20-transferFrom}を参照してください。
*
* 承認の更新を示す{Approval}イベントをスキップします。これはERCによって
* 必須ではありません。{xref-ERC20-_approve-address-address-uint256-bool-}[_approve]を参照してください。
*
* 注意: 現在の許可が最大`uint256`の場合、許可は更新されません。
*
* 要件:
*
* - `from`および`to`はゼロアドレスではない必要があります。
* - `from`は少なくとも`value`の残高を持っている必要があります。
* - 呼び出し者は、`from`のトークンに対して少なくとも`value`の許可を持っている必要があります。
*/
function transferFrom(address from, address to, uint256 value) public virtual returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, value);
_transfer(from, to, value);
return true;
}
/**
* @dev `from`から`to`に指定された`value`のトークンを移動します。
*
* この内部関数は{transfer}と同等で、自動トークン手数料、スラッシングメカニズムなどを
* 実装するために使用できます。
*
* {Transfer}イベントを発行します。
*
* 注意: この関数は仮想ではないため、{_update}をオーバーライドする必要があります。
*/
function _transfer(address from, address to, uint256 value) internal {
if (from == address(0)) {
revert ERC20InvalidSender(address(0));
}
if (to == address(0)) {
revert ERC20InvalidReceiver(address(0));
}
_update(from, to, value);
}
/**
* @dev `from`から`to`に指定された`value`のトークンを転送するか、`from`
* (または`to`)がゼロアドレスの場合にはミント(またはバーン)します。転送、ミント、およびバーンに対するすべてのカスタマイズは、この関数をオーバーライドして行う必要があります。
*
* {Transfer}イベントを発行します。
*/
function _update(address from, address to, uint256 value) internal virtual {
if (from == address(0)) {
// オーバーフローチェックが必要: これ以降のコードはtotalSupplyがオーバーフローしないことを前提としています
_totalSupply += value;
} else {
uint256 fromBalance = _balances[from];
if (fromBalance < value) {
revert ERC20InsufficientBalance(from, fromBalance, value);
}
unchecked {
// オーバーフローは発生しません: value <= fromBalance <= totalSupply。
_balances[from] = fromBalance - value;
}
}
if (to == address(0)) {
unchecked {
// オーバーフローは発生しません: value <= totalSupplyまたはvalue <= fromBalance <= totalSupply。
_totalSupply -= value;
}
} else {
unchecked {
// オーバーフローは発生しません: balance + valueは最大でtotalSupplyであり、それはuint256に収まることが確認されています。
_balances[to] += value;
}
}
emit Transfer(from, to, value);
}
/**
* @dev `account`に`value`分のトークンを作成し、address(0)から転送することで割り当てます。
* `_update`メカニズムに依存しています。
*
* `from`がゼロアドレスに設定された{Transfer}イベントを発行します。
*
* 注意: この関数は仮想ではないため、{_update}をオーバーライドする必要があります。
*/
function _mint(address account, uint256 value) internal {
if (account == address(0)) {
revert ERC20InvalidReceiver(address(0));
}
_update(address(0), account, value);
}
/**
* @dev `account`から`value`分のトークンを破棄し、総供給量を減少させます。
* `_update`メカニズムに依存しています。
*
* `to`がゼロアドレスに設定された{Transfer}イベントを発行します。
*
* 注意: この関数は仮想ではないため、{_update}をオーバーライドする必要があります。
*/
function _burn(address account, uint256 value) internal {
if (account == address(0)) {
revert ERC20InvalidSender(address(0));
}
_update(account, address(0), value);
}
/**
* @dev `owner`のトークンに対する`spender`の許可額を`value`として設定します。
*
* この内部関数は`approve`と同等で、特定のサブシステムの自動許可などに使用できます。
*
* {Approval}イベントを発行します。
*
* 要件:
*
* - `owner`はゼロアドレスではない必要があります。
* - `spender`はゼロアドレスではない必要があります。
*
* このロジックをオーバーライドする場合は、追加の`bool emitEvent`引数を持つバリアントを使用してください。
*/
function _approve(address owner, address spender, uint256 value) internal {
_approve(owner, spender, value, true);
}
/**
* @dev {Approval}イベントを有効または無効にするオプションフラグ付きの{_approve}のバリアント。
*
* デフォルトでは({_approve}を呼び出す場合)、フラグはtrueに設定されます。一方、`transferFrom`操作中に`_spendAllowance`によって行われる承認の変更はフラグをfalseに設定します。これにより、`transferFrom`操作中に`Approval`イベントを発行せずにガスを節約します。
*
* `transferFrom`操作中に`Approval`イベントを発行し続けたい場合は、次のオーバーライドを使用してフラグを強制的にtrueに設定できます:
*
* ```solidity
* function _approve(address owner, address spender, uint256 value, bool) internal virtual override {
* super._approve(owner, spender, value, true);
* }
* ```
*
* 要件は{_approve}と同じです。
*/
function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual {
if (owner == address(0)) {
revert ERC20InvalidApprover(address(0));
}
if (spender == address(0)) {
revert ERC20InvalidSpender(address(0));
}
_allowances[owner][spender] = value;
if (emitEvent) {
emit Approval(owner, spender, value);
}
}
/**
* @dev 使用された`value`に基づいて`owner`の`spender`に対する許可を更新します。
*
* 無限の許可がある場合、許可値は更新されません。
* 利用可能な許可が不足している場合はリバートします。
*
* {Approval}イベントは発行しません。
*/
function _spendAllowance(address owner, address spender, uint256 value) internal virtual {
uint256 currentAllowance = allowance(owner, spender);
if (currentAllowance != type(uint256).max) {
if (currentAllowance < value) {
revert ERC20InsufficientAllowance(spender, currentAllowance, value);
}
unchecked {
_approve(owner, spender, currentAllowance - value, false);
}
}
}
}
以下も関連するので置いておきます。
penzeppelin-contracts/contracts/token/ERC20/utils /SafeERC20.sol
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/utils/SafeERC20.sol)
pragma solidity ^0.8.20;
import {IERC20} from "../IERC20.sol";
import {IERC1363} from "../../../interfaces/IERC1363.sol";
import {Address} from "../../../utils/Address.sol";
/**
* @title SafeERC20
* @dev Wrappers around ERC-20 operations that throw on failure (when the token
* contract returns false). Tokens that return no value (and instead revert or
* throw on failure) are also supported, non-reverting calls are assumed to be
* successful.
* To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
* which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
*/
/**
* @title SafeERC20
* @dev ERC-20の操作をラップして、失敗時にエラーをスローする(トークンコントラクトがfalseを返す場合)。値を返さないトークン(失敗時にリバートまたはスローするもの)もサポートされており、リバートしない呼び出しは成功とみなされます。
* このライブラリを使用するには、コントラクトに`using SafeERC20 for IERC20;`というステートメントを追加することで、`token.safeTransfer(...)`などの安全な操作を呼び出すことができます。
*/
library SafeERC20 {
/**
* @dev An operation with an ERC-20 token failed.
*/
/**
* @dev ERC-20トークンでの操作が失敗しました。
*/
error SafeERC20FailedOperation(address token);
/**
* @dev Indicates a failed `decreaseAllowance` request.
*/
/**
* @dev `decreaseAllowance`リクエストが失敗したことを示します。
*/
error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease);
/**
* @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value,
* non-reverting calls are assumed to be successful.
*/
/**
* @dev 呼び出し元コントラクトから`to`へ`token`の`value`量を転送します。`token`が値を返さない場合、リバートしない呼び出しは成功とみなされます。
*/
function safeTransfer(IERC20 token, address to, uint256 value) internal {
_callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value)));
}
/**
* @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the
* calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.
*/
/**
* @dev `from`から`to`へ`token`の`value`量を転送し、`from`によって呼び出し元コントラクトに与えられた承認を使用します。`token`が値を返さない場合、リバートしない呼び出しは成功とみなされます。
*/
function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
_callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value)));
}
/**
* @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful.
*/
/**
* @dev 呼び出し元コントラクトの`spender`に対する許可を`value`だけ増加させます。`token`が値を返さない場合、リバートしない呼び出しは成功とみなされます。
*/
function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {
uint256 oldAllowance = token.allowance(address(this), spender);
forceApprove(token, spender, oldAllowance + value);
}
/**
* @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no
* value, non-reverting calls are assumed to be successful.
*/
/**
* @dev 呼び出し元コントラクトの`spender`に対する許可を`requestedDecrease`だけ減少させます。`token`が値を返さない場合、リバートしない呼び出しは成功とみなされます。
*/
function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal {
unchecked {
uint256 currentAllowance = token.allowance(address(this), spender);
if (currentAllowance < requestedDecrease) {
revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease);
}
forceApprove(token, spender, currentAllowance - requestedDecrease);
}
}
/**
* @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval
* to be set to zero before setting it to a non-zero value, such as USDT.
*/
/**
* @dev 呼び出し元コントラクトの`spender`に対する許可を`value`に設定します。`token`が値を返さない場合、リバートしない呼び出しは成功とみなされます。USDTのように、非ゼロの値に設定する前に許可をゼロに設定する必要があるトークンで使用されることを目的としています。
*/
function forceApprove(IERC20 token, address spender, uint256 value) internal {
bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value));
if (!_callOptionalReturnBool(token, approvalCall)) {
_callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0)));
_callOptionalReturn(token, approvalCall);
}
}
/**
* @dev Performs an {ERC1363} transferAndCall, with a fallback to the simple {ERC20} transfer if the target has no
* code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
* targeting contracts.
*
* Reverts if the returned value is other than `true`.
*/
/**
* @dev {ERC1363}のtransferAndCallを実行し、対象がコードを持たない場合は単純な{ERC20}の転送にフォールバックします。これは、{ERC1363}チェックに依存する{ERC721}のような安全な転送を実装するために使用できます。
*
* 返される値が`true`以外の場合はリバートします。
*/
function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {
if (to.code.length == 0) {
safeTransfer(token, to, value);
} else if (!token.transferAndCall(to, value, data)) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @dev Performs an {ERC1363} transferFromAndCall, with a fallback to the simple {ERC20} transferFrom if the target
* has no code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
* targeting contracts.
*
* Reverts if the returned value is other than `true`.
*/
/**
* @dev {ERC1363}のtransferFromAndCallを実行し、対象がコードを持たない場合は単純な{ERC20}のtransferFromにフォールバックします。これは、{ERC1363}チェックに依存する{ERC721}のような安全な転送を実装するために使用できます。
*
* 返される値が`true`以外の場合はリバートします。
*/
function transferFromAndCallRelaxed(
IERC1363 token,
address from,
address to,
uint256 value,
bytes memory data
) internal {
if (to.code.length == 0) {
safeTransferFrom(token, from, to, value);
} else if (!token.transferFromAndCall(from, to, value, data)) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @dev Performs an {ERC1363} approveAndCall, with a fallback to the simple {ERC20} approve if the target has no
* code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
* targeting contracts.
*
* NOTE: When the recipient address (`to`) has no code (i.e. is an EOA), this function behaves as {forceApprove}.
* Opposedly, when the recipient address (`to`) has code, this function only attempts to call {ERC1363-approveAndCall}
* once without retrying, and relies on the returned value to be true.
*
* Reverts if the returned value is other than `true`.
*/
/**
* @dev {ERC1363}のapproveAndCallを実行し、対象がコードを持たない場合は単純な{ERC20}のapproveにフォールバックします。これは、{ERC1363}チェックに依存する{ERC721}のような安全な転送を実装するために使用できます。
*
* 注: 受信アドレス(`to`)がコードを持たない場合(例えば、EOAの場合)、この関数は{forceApprove}として機能します。反対に、受信アドレス(`to`)がコードを持っている場合、この関数は{ERC1363-approveAndCall}を一度だけ呼び出し、値が`true`であることに依存します。
*
* 返される値が`true`以外の場合はリバートします。
*/
function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {
if (to.code.length == 0) {
forceApprove(token, to, value);
} else if (!token.approveAndCall(to, value, data)) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
* on the return value: the return value is optional (but if data is returned, it must not be false).
* @param token The token targeted by the call.
* @param data The call data (encoded using abi.encode or one of its variants).
*
* This is a variant of {_callOptionalReturnBool} that reverts if call fails to meet the requirements.
*/
/**
* @dev Solidityのハイレベルコール(つまり、コントラクトへの通常の関数呼び出し)を模倣し、返される値に対する要件を緩和します。返される値はオプションです(データが返される場合、それがfalseであってはなりません)。
* @param token 呼び出しの対象となるトークン。
* @param data 呼び出しデータ(abi.encodeまたはそのバリアントのいずれかを使用してエンコード)。
*
* これは、要求を満たさない場合にリバートする、{_callOptionalReturnBool}のバリアントです。
*/
function _callOptionalReturn(IERC20 token, bytes memory data) private {
uint256 returnSize;
uint256 returnValue;
assembly ("memory-safe") {
let success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
// bubble errors
if iszero(success) {
let ptr := mload(0x40)
returndatacopy(ptr, 0, returndatasize())
revert(ptr, returndatasize())
}
returnSize := returndatasize()
returnValue := mload(0)
}
if (returnSize == 0 ? address(token).code.length == 0 : returnValue != 1) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
* on the return value: the return value is optional (but if data is returned, it must not be false).
* @param token The token targeted by the call.
* @param data The call data (encoded using abi.encode or one of its variants).
*
* This is a variant of {_callOptionalReturn} that silently catches all reverts and returns a bool instead.
*/
/**
* @dev Solidityのハイレベルコール(つまり、コントラクトへの通常の関数呼び出し)を模倣し、返される値に対する要件を緩和します。返される値はオプションです(データが返される場合、それがfalseであってはなりません)。
* @param token 呼び出しの対象となるトークン。
* @param data 呼び出しデータ(abi.encodeまたはそのバリアントのいずれかを使用してエンコード)。
*
* これは、すべてのリバートを静かにキャッチし、代わりにboolを返す{_callOptionalReturn}のバリアントです。
*/
function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {
bool success;
uint256 returnSize;
uint256 returnValue;
assembly ("memory-safe") {
success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
returnSize := returndatasize()
returnValue := mload(0)
}
return success && (returnSize == 0 ? address(token).code.length > 0 : returnValue == 1);
}
}
今回は、ERC20についてはあまり詳しく触れませんが(今後記事にするとは思いますが...)、
ERC20に準拠したコントラクトは、
それ自体がトークンというよりは、
トークンの管理システムというイメージの方が合っていると思います。
その管理システムに問い合わせると、
トークンの発行や転送、
誰がどれだけのトークンを持っているか、
そもそもどれほど発行して流通している状態なのかなど、
あらかじめ決められたその操作と決められた操作の名前を持ち、
コインのようなトークンを管理しています。
そして、UniswapV2や他のDefiのプロジェクトにおいては、
ほぼ当たり前のように、ERC20の実装コードが使用されています。
これは当前で、Defiの機能は基本的に資産の運用であると考えられ、
その資産は基本的にトークンの形をしており、
トークンは基本的にERC20に準拠してますから、
当たり前のようにERC20の実装を使わなければ、
まともなシステムになりません。
というか、Defiプロジェクト側も、
ERC20に準拠していない、オリジナルのトークン、つまり、
何のルールにもしたがっていない、名前も統一されていないような、
独自の「状態」と「状態を操作するもの」のセットを持っており、
また、それらの名前も統一されていないトークンなど扱いたくありません。
話を戻すと、
今回注目している流動性の初期化・追加の文脈においては、
LPトークンが、先ほど言及したトークンの管理システムであり、
各基礎トークンペアごとに、
このトークンの管理システムが存在していて、
各ペアのLPトークンが流通しているというわけです。
そして、UniswapV2においては、
下記で紹介するような、
LPトークンの持つERC20準拠の_mintメソッドを使うことで、自身を新規発行したり、
ERC準拠の操作を少し改造したsafeTransferFromメソッドを使って、
基礎トークンのRC20準拠のメソッドを使ってトークンを転送したりしています。
実装を見てみると、_mintメソッドでは以下のように、
account
のアドレスに、value
分新規で発行します。
contracts/token/ERC20/ERC20.sol > _mint
/**
* @dev `account`に`value`分のトークンを作成し、address(0)から転送することで割り当てます。
* `_update`メカニズムに依存しています。
*
* `from`がゼロアドレスに設定された{Transfer}イベントを発行します。
*
* 注意: この関数は仮想ではないため、{_update}をオーバーライドする必要があります。
*/
function _mint(address account, uint256 value) internal {
if (account == address(0)) {
revert ERC20InvalidReceiver(address(0));
}
_update(address(0), account, value);
}
また、safeTransferFromメソッドでは、
fromからtoへtoken
のvalue
量を転送します。
penzeppelin-contracts/contracts/token/ERC20/utils /SafeERC20.sol > safeTransferFrom
/**
* @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the
* calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.
*/
/**
* @dev `from`から`to`へ`token`の`value`量を転送し、`from`によって呼び出し元コントラクトに与えられた承認を使用します。`token`が値を返さない場合、リバートしない呼び出しは成功とみなされます。
*/
function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
_callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value)));
}
safeTransferFromに関しては、
いろいろ言いたいことはありますが、(認証認可周りがとても重要なので)
ひとまず今回は、単に、
tokenに指定されたアドレスを、
valueで指定された分だけ、
fromのアドレスから
toのアドレスに送るという役割を担うとだけ覚えておいてください。
念の為、UniswapV2では、safeTransferFromメソッドと
似たような実装を自前で実装していることに触れておきます。
ここを深掘りしても仕方がないので、
先ほどのsafeTransferFromメソッドと、
以下のUniswapV2自前のsafeTransferFromメソッドは
基本的に同じものと考えてください。別の機会にでも詳しく整理しましょう。
solidity-lib/contracts/libraries /TransferHelper.sol > safeTransferFrom
function safeTransferFrom(
address token,
address from,
address to,
uint256 value
) internal {
// bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));
// bytes4(keccak256(bytes('transferFrom(address,address,uint256)'))) -> 'transferFrom' 関数のシグネチャ
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));
require(
success && (data.length == 0 || abi.decode(data, (bool))),
'TransferHelper::transferFrom: transferFrom failed'
);
}
では、先ほど触れた、
_mintメソッドやsafeTransferFromメソッドが
実際にどこで使われているか紹介しておきます。
_mintメソッドは、
例えば以下のUniswapV2Router02コントラクトにおける、
addLiquidityメソッドで使用されています。
v2-periphery/contracts /UniswapV2Router02.sol > addLiquidity
function addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
liquidity = IUniswapV2Pair(pair).mint(to);
}
また、同様の箇所にsafeTransferFromメソッドも使われており、
流動性の初期化・追加の際、ユーザーが持っている2つの基礎トークンを
UniswapV2Pairコントラクトに転送していることがわかります。
v2-periphery/contracts /UniswapV2Router02.sol > _addLiquidity, addLiquidity
(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
liquidity = IUniswapV2Pair(pair).mint(to);
LPトークンを初期化する
では、話を戻して、
まずは、新しく1から流動性を作り、
LPトークンを発行する流れを見ていきましょう。
現在私たちは、トークンの設計をしているような状況です。
そのようなとき、まずは何を考えるでしょう。
特に今回のトークン、つまり、LPトークンは、
2つの基礎トークンからなる原資産から派生して誕生した金融商品、
デリバティブです。
しかも、LPトークンの導入目的は明確で、流動性の管理です。
より端的に言えば、
誰がどれだけの流動性のシェアを持っているか
齟齬なく運用管理するという目的があります。
というわけで、LPトークンはその発行に、
原資産の量が密接に関連してくるでしょう。
そう言えば、
前回、基礎トークン、原資産と流動性
というのは、リザーブの乗算的な合計であり、 k
幾何的には、における座標と原点までの正方形の面積である。 k
これはボトルの中の基礎トークンに何も重み付けをしないで、
数量だけ計算した場合の平均した1基礎トークンあたりの量を表しているといえます。
平均した1基礎トークンあたりの量は、
\sqrt{k}
と表せます。
つまり、以下でした。
LPトークンの初回発行時点では、
例えば、100MTK, 200USDC入れたとして、
どちらの方がか価値が高い低いとか言及できますでしょうか?
最初はわからないという前提を置くのが、まあ妥当ではないでしょうか。
したがって、このMTKとUSDCを
LPトークンの量は、
と定義するのが良さそうではないでしょうか。
つまり、
となります。
該当コードは以下です。
v2-core/contracts /UniswapV2Pair.sol > mint
liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
_mint(address(0), MINIMUM_LIQUIDITY); // permanently
より広く見ると以下になります。
v2-core/contracts /UniswapV2Pair.sol > mint
if (_totalSupply == 0) {
liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
_mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
// 最初のMINIMUM_LIQUIDITYトークンを永久にロック
} else {
//今回は不要
}
require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
_mint(to, liquidity);
_update(balance0, balance1, _reserve0, _reserve1);
初期化の制限
「なんだ、簡単だ」となりそうですが、そうでもないです。
よくコードを見てみてください。
よく見るとMINIMUM_LIQUIDITYという見慣れないものが
.subという引き算(subtraction)によって、
マイナスされています。
基本的に、LPトークンの初回の新規発行において、
基礎トークンの量的な制限はないのですが、
つまり好きな量で流動性提供を開始できるのですが、
より正確には、LPトークンの初回新規発行量が、
MINIMUM_LIQUIDITY(
よりも大きくなければ、発行ができません。
もし、MINIMUM_LIQUIDITYよりも小さな値で流動性提供をしてしまうと、
LPトークンの値、つまり、
requireというルールで弾かれ、
そこまでで消費したGasは帰って来ません。(残りのGasは返却されます。)
順調に流動性を提供できた場合、
このMINIMUM_LIQUIDITYのLPトークンの発行分は、
イメージとしては、以下のようになります。
このようにする理由は、
攻撃者による流動性の占有を防ぐためであると考えられます。
少し難しいので、
詳しくは、流動性の追加について整理した後で確認しましょう。
【成長1】流動性の追加
さて、あなたがLPトークンの初回の新規発行を行った後、
さらにあなたは、流動性を追加することにしました。
具体的には以下の箇所です。
いつものように、
UniswapV2Router02コントラクトから
UniswapV2Pairコントラクトの流れで実行されていきます。
v2-periphery/contracts /UniswapV2Router02.sol > _addLiquidity
} else {
uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
if (amountBOptimal <= amountBDesired) {
require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
(amountA, amountB) = (amountADesired, amountBOptimal);
} else {
uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
assert(amountAOptimal <= amountADesired);
require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
(amountA, amountB) = (amountAOptimal, amountBDesired);
}
}
v2-core/contracts /UniswapV2Pair.sol > mint
} else {
liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
}
require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
_mint(to, liquidity);
_update(balance0, balance1, _reserve0, _reserve1);
その際、UniswapV2Router02コントラクトにて、
以下のquoteメソッドの実行が行われます。
一体何を行っているのでしょう?
v2-periphery/contracts/libraries /UniswapV2Library.sol > quote
// given some amount of an asset and pair reserves, returns an equivalent amount of the other asset
// ある資産の量とペアのリザーブが与えられた場合、他の資産の同等量を返す
function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');
require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
amountB = amountA.mul(reserveB) / reserveA;
}
LPトークンの初回の新規発行においては、
基本的に好きな基礎トークンの量でLPトークンを発行していました。
てきとーに量を入れてたわけですね。
それは、基礎トークン間の価値の差が定まっていなかったからできたことです。
結局、最初はよくわからないから、
「エイヤっ!」
で適当に考えなくてもいいわけです。
どっちの方が価値高いか低いかがわからないんで。
(まあ同じぐらいの価値だと思う量を入れた方がいいと思いますが...)
しかし、初回以降は話が違って来ます。
前回の前編にて例え話をしましたが、
すでに各ユーザがそれぞれの価格の想定を元に、
売り買いを繰り返し、ある程度の範囲でリザーブは固定されていき、
そのリザーブ間の量の比、つまり、価格もある程度固定されていきます。
したがって、
最初にLPを発行した時点から、
そこにプールされているリザーブは、
そのトークン間の価値の関係性を表したもので、
アービトラージャー等の取引対象として
既に晒されているわけです。
つまり、
もう基準は目の前のリザーブ比として明確に存在し、
他のDEXやCEXなどの価格や投資家の評価に晒され、
調整されうる対象になってしまったのでから、
もしくは、現在進行形で調整されているのだから、
それを前提にLPトークンを発行する必要があります。
(アービトラージャーによって右へ左へ)
さて、初回のLPトークンの発行と、
それ以降の発行とでは全く毛色が違っています。
しかし、この2つで共通するルールがあります。
それは、提供するペアを同じ価値で提供しておかないと、
正確に価格を判断できるユーザーや
アービトラージャーの餌食になるということです。
ただ、前者は、
新規ガバナンストークンなど未知のものを初期化する場合、
確定できるようなリスクが大きくないというだけです。
(まあ、そうとも言い切れませんが...)
例えば、悪い例として、
大幅に価格と乖離した量をてきとーに追加するとします。
すると、既存の価格がずれます。
その後、それを狙っていたアービトラージャーが取引して、
元の価格に戻ります。
この時損害を受けるのはあなたです。
この損害をインパーマネントロス(IL)といいますが、
投入時の価格からズレるほど、指数関数的に増加する損害を受けます。
詳しくは、後編で整理します。
したがって、基本的には、
価格を変動させない、現時点でのリザーブ比、価格を前提に、
LPトークンの発行を進めていきたいという考えがベースとなります。
さて、quoteメソッド実行の結論としては、
現時点でのリザーブ比、価格を前提に、
LPトークンの発行を進めるためであるといえます。
以下、再掲します。
v2-periphery/contracts/libraries /UniswapV2Library.sol > quote
// given some amount of an asset and pair reserves, returns an equivalent amount of the other asset
// ある資産の量とペアのリザーブが与えられた場合、他の資産の同等量を返す
function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');
require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
amountB = amountA.mul(reserveB) / reserveA;
}
現時点でのリザーブ比、価格を前提にといいましたが、
基本的には、自分が提供したい基礎トークンの量をベースとして計算されるので、
提供したい量が大きく制限されるわけではありません。
あくまでも、提供したい量を元に、
相方のトークンの量が制限されるということです。
では、UniswapV2Router02コントラクトにて、
quoteメソッドを実行し、流動性の追加を行います。
具体的には、以下のような計算になります。
現在のトークンAの1単位あたりの
トークンBのリザーブの量を以下とします。
また、
提供したい基礎トークンAの追加希望量 amountADesiredとします。
この量をトークンBベースに換算すると、
となります。
v2-periphery/contracts /UniswapV2Router02.sol > _addLiquidity
これが、以下のUniswapV2Router02コントラクトで行われていることです。
} else {
uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
if (amountBOptimal <= amountBDesired) {
require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
(amountA, amountB) = (amountADesired, amountBOptimal);
} else {
uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
assert(amountAOptimal <= amountADesired);
require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
(amountA, amountB) = (amountAOptimal, amountBDesired);
}
}
より丁寧に整理すると、
まず、提供したいトークンAの量 amountADesiredを元に、
そのときのリザーブ比から価格に影響を与えない
最適なトークンBの量 amountBOptimalを導きます。
その際、
つまり、追加したいトークンBの量
最適なトークンBの量$\text{amountBOptimal} $を超えてしまっていた場合は、
逆からのアプローチを行います。
したがって、
提供したいトークンBの量 amountBDesiredを元に、
最適なトークンAの量 amountAOptimalを計算します。
それでも
assertで全額Gasを回収され、無かったことになります。
この計算が終わり、無事価格に影響を与えることなく
基礎トークンのそれぞれの量が計算できたとします。
その結果は、UniswapV2Pairコントラクトのmintメソッドに渡されます。
渡す方法としては、先ほど計算したトークン量を
つまり、実際にユーザが追加したいと希望するトークンの量を
魂の入っているUniswapV2Pairコントラクトの実体に転送します。
v2-periphery/contracts /UniswapV2Router02.sol > _addLiquidity, addLiquidity
(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
liquidity = IUniswapV2Pair(pair).mint(to);
LPトークンの初回新規発行
その後、mintメソッドが実行されていきます。
v2-core/contracts /UniswapV2Pair.sol > mint
// this low-level function should be called from a contract which performs important safety checks
// この低レベル関数は、重要な安全確認を実行するコントラクトから呼び出される必要があります
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
// ガスの節約、_mintFee内でtotalSupplyが更新される可能性があるため、ここで定義する必要があります
if (_totalSupply == 0) {
liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
_mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
// 最初のMINIMUM_LIQUIDITYトークンを永久にロック
} else {
liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
}
require(liquidity > 0, 'UniswapV2: 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
// reserve0とreserve1は最新の状態です
emit Mint(msg.sender, amount0, amount1);
}
ここからも少しややこしいです。
まず、UniswapV2Router02コントラクトを介して送られてきた
トークンの量のチェックを行います。
v2-core/contracts /UniswapV2Pair.sol
uint balance0 = IERC20(token0).balanceOf(address(this));
uint balance1 = IERC20(token1).balanceOf(address(this));
uint amount0 = balance0.sub(_reserve0);
uint amount1 = balance1.sub(_reserve1);
ここでは、既に紹介したERC20特有のメソッド、操作を用いて、
UniswapV2Pairコントラクト自分自身が持っている基礎トークンの残高を照会し、
そこから、現在のリザーブをマイナスすることで、送られてきたトークンの量を把握します。
問題なく処理が進むと、以下のように処理が進みます。
ここがいよいよ流動性追加の最後の処理となります。
これらは、既存LPシェアをキソンすることなく、
適切に割り当てるための処理です。
順に確認していきましょう。
UniswapV2Router02コントラクトを介して、
UniswapV2Pairコントラクトに流動性追加用のトークンが送られてくるわけですが、
その量が _reserve0 _reserve1のそれぞれに対して
どれぐらいの割合を占めるかをまず確認します。
また、プロトコル手数料という、まあ、無視していいものを除くと、
LPトークンは、純粋に流動性提供以外で発行されないことから、
LPトークンの総供給量 _totalSupplyは、
全て流動性提供者に割り当てられたものであるといえます。
したがって、 _totalSupplyに、
追加量が _reserve0 _reserve1のそれぞれに対して、
どれぐらいの割合を占めるかの比を乗じると、
以下のように、
新規流動性提供者に割り当てられるべきトークン量が、
各トークンベースで導けます。
最後に以下のように小さい方を流動性として、
LPトークンを新規発行するわけですが、
このようなルールを決めておく理由は、
片方の amountを極端に大きくして、
不当に既存のシェアを奪うことができる余地をなくすためと考えられます。
しかし、個人的にはいまだに理解しきれてないことがあり、
以下の2つのモデルにおいて、
_totalSupplyは、
純粋にLPユーザーに割り当てられているのに対して、
_reserve0 _reserve1は、
スワップの報酬により増加していきます。(後編)
長期化したプールにおいて、
新たな参加者は、
流動性の割り当てが初期より小さくなっていく気がするのですがどうなんでしょうか。
詳しい方詳細をおしえてください。
MINIMUM_LIQUIDITYを導入する理由
さて、色々と方がつきましたので、
未解決話題を整理して、次の話題にいきましょう。
具体的には以下の件です。
基本的に、LPトークンの初回の新規発行において、
基礎トークンの量的な制限はないのですが、
つまり好きな量で流動性提供を開始できるのですが、
より正確には、LPトークンの初回新規発行量が、
MINIMUM_LIQUIDITY((decimals=18表記) or 1000 (10進数表記)) 1e-15
よりも大きくなければ、発行ができません。
なぜこのような実装をしているのでしょうか。
詳しく具体例を用いて解説します。
アプローチとしては、
MINIMUM_LIQUIDITYが存在しない場合、
どのような困ったことが起こるかを見ていきます。
前提として、
MINIMUM_LIQUIDITYという量のLPトークンをロックする仕組みがないと仮定します。
攻撃者が流動性プールを操作して、小規模な流動性提供者がプールに参加するのを困難にし、流動性提供者を自分だけにする攻撃を考えます。
LPトークンの decimalsは
- 初回LPトークン発行
攻撃者は非常に小さな量のWETHとDAI、
例えば、
および
を使用してプールを初期化します。
以下のような初期供給量となります。
または、
- 大量の流動性の寄付
その後、攻撃者はプールに、寄付として、
100 WETH($100 * 10^{18} $wei)と100 DAI(
_updateメソッドを呼び出してプールの状態を更新します。
(swapなどすると更新できます)
_updateメソッドをは以下のようなものですが、
今回においては、単に残高をリザーブの値として反映するだけです。
v2-core/contracts /UniswapV2Pair.sol > _update, sync
// update reserves and, on the first call per block, price accumulators
// リザーブを更新し、各ブロックの最初の呼び出しで価格累積値を更新する
function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');
uint32 blockTimestamp = uint32(block.timestamp % 2**32);
uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
// オーバーフローが意図的に発生することを想定
if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
// * never overflows, and + overflow is desired
// * はオーバーフローしない、+ オーバーフローが意図されている
price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
}
reserve0 = uint112(balance0);
reserve1 = uint112(balance1);
blockTimestampLast = blockTimestamp;
emit Sync(reserve0, reserve1);
}
// force reserves to match balances
// リザーブを強制的に残高に一致させる
function sync() external lock {
_update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
}
では、ここで、現時点での1LPトークンの価値を考えます。
LPトークンの価値、価格評価はかなり深い内容なのでまた別で記事を書きますが、
今回は詳細を飛ばしたものであることに注意してください。
LPの価格はまず、以下のように考えられます。
ここで、今回は、
したがって以下になります。
これ、半端ない金額になります。
しかも、これ以上分割できない、最小単位がこの価格になります。
この高価な1LPを作ることについて、
ここまで整理してきた知識を使って考えてみましょう。
先ほど以下であることを理解しました。
これは、今回の場合、具体的には以下になります。
したがって、
LPトークンを発行する際に、
UniswapV2Pairコントラクトに転送する基礎トークンの量でした。
ここで、quoteメソッドも加味しても、100WETH+100DAIを用意して、
を転送する必要があります。
恐ろしいですね🥶
- 小規模な流動性提供者に対する影響
上述のように、
新たな流動性提供者がプールに流動性を提供するためには、
少なくとも100 WETHと100 DAIを提供する必要があり、
小規模な流動性提供者が参加しにくくなります。
攻撃者の流動性プール独り占め作戦の完了です。
- MINIMUM_LIQUIDITYの導入
そういうわけで、LPトークンを最低限、
MINIMUM_LIQUIDITY分を強制的に、システムで発行し、
それをロックすることで、常に存在させ、
これ以上分割できない1LPを超高価にするようなことを実現しにくくして、
攻撃者に立ち向かいます。
これらの式は、具体的に以下になります。
したがって、
この結果を見るだけでもかなり有効であることがわかります。
【衰退】流動性の解除と報酬の確定
さて、長々と、流動性の初期化と追加について見てきましたが、
次は、流動性の解除について見ていきます。
これまでの初期化や、追加についての整理を前提に流動性の解除を考えると、
本項の内容はそれほどヘヴィーではないと思います。
サッと確認してしまいましょう。
ひとまず、いつものようにUniswapV2Router02コントラクトから実行します。
v2-periphery/contracts /UniswapV2Router02.sol > removeLiquidity
// **** REMOVE LIQUIDITY ****
// **** 流動性の削除 ****
function removeLiquidity(
address tokenA,
address tokenB,
uint liquidity,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair
// ペアに流動性を送る
(uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
(address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
(amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
// amountAがamountAMin以上であることを確認
require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
// amountBがamountBMin以上であることを確認
}
まず、解除したい量のLPトークンを、
既に見慣れた下記のメソッドでユーザーからUniswapV2Pairコントラクトに転送します。
v2-periphery/contracts /UniswapV2Router02.sol > removeLiquidity etc.
IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair
// ペアに流動性を送る
v2-core/contracts /UniswapV2ERC20.sol > transferFrom
function transferFrom(address from, address to, uint value) external returns (bool) {
if (allowance[from][msg.sender] != uint(-1)) {
allowance[from][msg.sender] = allowance[from][msg.sender].sub(value);
}
_transfer(from, to, value);
return true;
}
転送後、以下の箇所でUniswapV2Router02コントラクトが、
UniswapV2Pairコントラクトのburnメソッドに実行をパスします。
v2-periphery/contracts /UniswapV2Router02.sol > removeLiquidity etc.
(uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
burnメソッドの具体的な実装は以下です。
v2-core/contracts /UniswapV2Pair.sol > burn
// this low-level function should be called from a contract which performs important safety checks
// この低レベル関数は、重要な安全チェックを行うコントラクトから呼び出されるべきです
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
// ガス節約、_mintFee内でtotalSupplyが更新される可能性があるためここで定義する必要があります
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, 'UniswapV2: 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
// reserve0とreserve1は最新の状態です
emit Burn(msg.sender, amount0, amount1, to);
}
ここではまず、
UniswapV2Pairコントラクト自身の、
基礎トークンの所有分と流動性の所有分を計算しています。
_mintメソッドでも似たようなものを見ましたね。
v2-core/contracts /UniswapV2Pair.sol > burn
uint balance0 = IERC20(_token0).balanceOf(address(this));
uint balance1 = IERC20(_token1).balanceOf(address(this));
uint liquidity = balanceOf[address(this)];
ただし、ここは少し難しい気がするので、注意深く確認しておきます。
リザーブの残高とは違って、
UniswapV2Pairコントラクト自身のLPトークンの所持量は
この処理のときだけユーザが転送することで
暫定的に所持して処理します。
わかりづらいです...😓
しかし、結構面白いところでもあります。
たとえば、直接UniswapV2Pairコントラクトにあらかじめ
該当のLPトークン送っておけば、
burnの際、ちょっと多くの量報酬がもらえるでしょう。
まあ、意味ないし、実際に検証してないので想像なんですが...
あと、そんなこと実際に検証してると誰かにバーンされるでしょう。
監視してる人いるかわかりませんが😂
さて、次の箇所が肝となります。
これはもう見たまんまで、LPはシェアを表しますから、
全シェアである _totalSupplyを liquidityがどれだけ占めるのかを計算し、
UniswapV2Pairコントラクトが所持する基礎トークンの量に乗じて報酬を確定します。
v2-core/contracts /UniswapV2Pair.sol > burn
amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
// 残高を使用して按分分配を保証
amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
// 残高を使用して按分分配を保証
そして、実際にLPトークンもバーンして(総供給を減じて)、
計算した基礎トークンをLPユーザーに転送して完了という感じです。
v2-core/contracts /UniswapV2Pair.sol > burn
_burn(address(this), liquidity);
_safeTransfer(_token0, to, amount0);
_safeTransfer(_token1, to, amount1);
_burnメソッドに関しては、ERC20のものなので、
以下、OpenZeppelinの実装とUniswapV2の独自実装掲載しておきます。
contracts/token/ERC20/ERC20.sol > _burn
/**
* @dev `account`から`value`分のトークンを破棄し、総供給量を減少させます。
* `_update`メカニズムに依存しています。
*
* `to`がゼロアドレスに設定された{Transfer}イベントを発行します。
*
* 注意: この関数は仮想ではないため、{_update}をオーバーライドする必要があります。
*/
function _burn(address account, uint256 value) internal {
if (account == address(0)) {
revert ERC20InvalidSender(address(0));
}
_update(account, address(0), value);
}
/**
* @dev `from`から`to`に指定された`value`のトークンを転送するか、`from`
* (または`to`)がゼロアドレスの場合にはミント(またはバーン)します。転送、ミント、およびバーンに対するすべてのカスタマイズは、この関数をオーバーライドして行う必要があります。
*
* {Transfer}イベントを発行します。
*/
function _update(address from, address to, uint256 value) internal virtual {
if (from == address(0)) {
// オーバーフローチェックが必要: これ以降のコードはtotalSupplyがオーバーフローしないことを前提としています
_totalSupply += value;
} else {
uint256 fromBalance = _balances[from];
if (fromBalance < value) {
revert ERC20InsufficientBalance(from, fromBalance, value);
}
unchecked {
// オーバーフローは発生しません: value <= fromBalance <= totalSupply。
_balances[from] = fromBalance - value;
}
}
if (to == address(0)) {
unchecked {
// オーバーフローは発生しません: value <= totalSupplyまたはvalue <= fromBalance <= totalSupply。
_totalSupply -= value;
}
} else {
unchecked {
// オーバーフローは発生しません: balance + valueは最大でtotalSupplyであり、それはuint256に収まることが確認されています。
_balances[to] += value;
}
}
emit Transfer(from, to, value);
}
v2-core/contracts /UniswapV2ERC20.sol > _burn
function _burn(address from, uint value) internal {
balanceOf[from] = balanceOf[from].sub(value);
totalSupply = totalSupply.sub(value);
emit Transfer(from, address(0), value);
}
k の拡大縮小メカニズム
流動性ではここまで、
流動性の初期化と追加と削除について整理して来ました。
したがって、流動性を管理する基本的な機能は、
確認し終わったということになります。
それでは、まとめとして、
ここまでの情報をもとにして、
流動性
報酬が流動性に与える影響を確認しておきます。
-
流動性
の拡大要因k - ユーザがスワップすることによる手数料(流動性提供者への報酬)
- 流動性提供者による流動性の追加
- 転送された基礎トークンはそのままリザーブへ
-
流動性
の縮小要因k - 流動性提供者による流動性の解除
- 転送されたLPトークンはバーンされ、基礎トークンは流動性提供者の元へ
- 転送されたLPトークンはバーンされ、基礎トークンは流動性提供者の元へ
- 流動性提供者による流動性の解除
流動性の追加と削除の関係式
さて、ざっと、ここまでの流動性の関係を整理しました。
しかし、私は、いまいちまだ理解がふわっとしています。
例えば、流動性の追加と解除は、対称的なものなのか?
また、追加と削除は密接に関わっているにも関わらず、
その関係性がいまだに見えて来ません。
したがって、ここまで整理してきたモデルを触って、
なにかヒントを得たいと思います。
ここでは、報酬による拡大に関しては、
すぐ後に確認するので、一旦置いておきます。
では、まず、手始めに、
流動性追加時のモデルをいじってみようと思います。
これまでの整理で以下のモデルを得てましたが、
これを
また、
したがって、流動性の追加によって、
また、流動性の解除は以下のようなモデルで実装されていました。
少し書き換えると、以下のようになり、
ここでさらに
$ \text{balance0} \approx \text{_reserve0}$ $ \text{balance1} \approx \text{_reserve1}$なので、
さて、ここまでを整理すると、以下のようになり、とても綺麗な対称性を持っています。
つまり、追加と解除では同じルールで実装されていることになります。
追加
解除
ただ、注意したいのは、
流動性の追加は、
流動性の解除は、
あくまでも、追加と解除が同じルールをで動いているのか確認したかっただけで、
モデルの形式としては以下の方が正確でしょう。
もうすこしこのモデルで遊んでみます。
先ほどのモデルを、流動性の追加(
(※ before, afterは、
以下のように書き換えられます。
したがって、この時間軸をもとに、追加した後すぐに解除した場合のモデルは、
したがって、
これをさらに整理します。
最後の処理として、流動性を解除したします。
そこから、時間の巻き戻していくと、
解除→追加→...→初回流動性となりますが、これを簡略化し、
→...→をなくして、解除→追加→初回流動性となる場合を仮定し、
また、スワップも行われないと仮定すると
(流動性の手数料による成長なしを仮定)、
初回の流動性は、
そのため、初回の流動性提供時における流動性のモデルは次のようになります。
または、
したがって、
さらに、スワップが行われない仮定から、
つまり、ここまで落とし込めます。
なんとなく追加と解除の関係に、
親近感を持ててきたのではないでしょうか😇
ここで整理しておくと、
流動性提供は、
追加時、総供給を基準に liquidityを新規発行しますが、
これは、スワップがないという仮定では、
になるわけなんで、
結局、
そして、
総供給を基準に発行し、その量がさらに、総供給につかされるので、
総供給以上の流動性提供解除などは行われず、
あくまでもスワップが行われない環境においては、
全員が流動性を解除しない限り、リザーブがなくなることはないです。
実験してみます。
以下はここまで整理して来た、流動性解除のモデルですが、
ここで、
つまり、リザーブ全量抜けることとなり、
あくまでもスワップがなかった場合ですが、
流動性が空になることがわかります。
再度流動性の拡大縮小メカニズムを確認する
したがって、
流動性の追加・解除において、合計すると、
本整理においては、あまり気にすることではないため、
基本的には、スワップの報酬に注目していればよさそうに思います。
ちなみに、 以下の実際の実装のように、
if (_totalSupply == 0) {
とあるので、流動性が空なら、また次の人が初回でなくても初期化を行えます。
v2-core/contracts /UniswapV2Pair.sol > mint
// this low-level function should be called from a contract which performs important safety checks
// この低レベル関数は、重要な安全確認を実行するコントラクトから呼び出される必要があります
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
// ガスの節約、_mintFee内でtotalSupplyが更新される可能性があるため、ここで定義する必要があります
if (_totalSupply == 0) {
liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
_mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
// 最初のMINIMUM_LIQUIDITYトークンを永久にロック
} else {
liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
}
require(liquidity > 0, 'UniswapV2: 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
// reserve0とreserve1は最新の状態です
emit Mint(msg.sender, amount0, amount1);
}
さて、流動性の拡大要因、縮小要因を見ていたわけですが、
再掲すると以下でした。
-
流動性
の拡大要因k - ユーザがスワップすることによる手数料(流動性提供者への報酬)
- 流動性提供者による流動性の追加
- 転送された基礎トークンはそのままリザーブへ
-
流動性
の縮小要因k - 流動性提供者による流動性の解除
- 転送されたLPトークンはバーンされ、基礎トークンは流動性提供
- 流動性提供者による流動性の解除
しかし、ここまで整理してきた情報でいえそうなことは、
繰り返しになりますが、
流動性の追加による拡大は一時的なものであるということです。
全部抜けば、以下のようになるわけですから、
ここで、
結局は
とすると、メカニズムに肝は、
拡大要因のみ、さらに、
スワップによる手数料のみ、
ということになります。
また、先ほど確認した以下のモデルですが、
における、
本来、
であり、この
スワップの報酬による流動性
基礎トークンリザーブの増加は当然含まれていないため、
スワップが実行されない場合を仮定した以下は、
とはならず、
のようになるはずです。
締め:後編に続く
終わりませんでした。
スワップ報酬に流動性の成長は次にやります。
参考文献
以下記事の最下部をご確認ください。
Discussion