Uniswap V2 — 從程式碼解釋 DeFi 協議
為了理解我們在分析程式碼時將要經歷的不同元件,首先了解哪些是主要概念以及它們的作用是很重要的。所以,和我一起裸露吧,因為這是值得的。
我在 5 個段落中總結了您需要了解的主要重要概念,您將在本文結束時理解這些概念。
Uniswap 是一種去中心化交易協議。該協議是一套持久的、不可升級的智慧合約,它們共同建立了一個自動化的做市商。
Uniswap 生態系統由貢獻流動性的流動性提供者、交換代幣的交易員和與智慧合約互動以開發代幣新互動的開發人員組成。
每個 Uniswap智慧合約或對管理一個由兩個 ERC-20 代幣儲備組成的流動資金池。
每個流動性池重新平衡以保持 50/50 比例的加密貨幣資產,這反過來又決定了資產的價格。
流動性提供者可以是任何能夠向 Uniswap 交易合約提供等值的 ETH 和 ERC-20 代幣的人。作為回報,他們從交易合約中獲得流動性提供者代幣(LP 代幣代表流動性提供者擁有的池的份額),可用於隨時提取其在流動性池中的比例。
他們儲存庫中的主要智慧合約是:
UniswapV2ERC20
— 用於 LP 令牌的擴充套件 ERC20 實現。它還實施了 EIP-2612 以支援鏈下傳輸批准。UniswapV2Factory
— 與 V1 類似,這是一個工廠合約,它建立配對合約並充當它們的登入檔。登入檔使用 create2 來生成對地址——我們將詳細瞭解它是如何工作的。UniswapV2Pair
— 負責核心邏輯的主合約。值得注意的是,工廠只允許建立獨特的貨幣對,以免稀釋流動性。UniswapV2Router
— Uniswap UI 和其他在 Uniswap 之上工作的網路和去中心化應用程式的主要入口點。UniswapV2Library
— 一組實現重要計算的輔助函式。
在這篇文章中,我們將提及所有這些,但我們將主要關注瀏覽UniswapV2Router
和UniswapV2Factory
編碼,儘管UniswapV2Pair
並且UniswapV2Library
會涉及很多。
UniswapV2Router02.sol
該合約使建立貨幣對、新增和刪除流動性、計算所有可能的掉期變化的價格以及執行實際掉期變得更加容易。路由器適用於透過工廠合約部署的所有對
您需要在合約中建立一個例項才能呼叫 addLiquidity、removeLiquidity 和 swapExactTokensForTokens 函式
address private constant ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
IUniswapV2Router02 public uniswapV2Router;
uniswapV2Router = IUniswapV2Router02(ROUTER);
現在讓我們看看流動性管理:
函式 addLiquidity():
function addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) external returns (uint amountA, uint amountB, uint liquidity);
- tokenA和tokenB:是我們需要獲取或建立我們想要增加流動性的貨幣對的代幣。
- amountADesired和amountBDesired是我們要存入流動資金池的金額。
- amountAMin和amountBMin是我們要存入的最小金額。
- to address 是接收 LP 代幣的地址。
- 截止日期,最常見的是
block.timestamp
在內部 _addLiquidity() 中,它將檢查這兩個令牌中的一對是否已經存在,如果不存在,它將建立一個新令牌
if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
IUniswapV2Factory(factory).createPair(tokenA, tokenB);
}
然後它需要獲取現有的代幣數量或也稱為reserveA
and reserveB
,我們可以透過 UniswapV2Pair 合約訪問它
IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves()
現在,外部函式 addLiquidity, 返回(uint amountA, uint amountB, uint liquidity)
,那麼它是如何計算的呢?
透過UniswapV2Library拿到上面提到的reserves之後,還有一系列的檢查
如果該對不存在,並且新建立 amountA
並amountB
返回一個新的,則將amountADesired
作為amountBDesired
引數傳遞(見上文)。
否則,它會做這個操作
amountBOptimal = amountADesired.mul(reserveB) / reserveA;
如果amountB
小於或等於,amountBDesired
那麼它將返回:
(uint amountA, uint amountB) = (amountADesired, amountBOptimal)
否則,它將返回
(uint amountA, uint amountB) = (amountAOptimal, amountBDesired)
其中amountAOptimal
的計算方式與amountBOptimal
然後,要計算liquidity
返回值將經過以下過程:
首先,它將使用現有/新建立的對的地址部署 UniswapV2Pair 合約。
它是如何做到的?它計算一對的 CREATE2 地址而無需進行任何外部呼叫:(閱讀有關 CREATE2 Opcode 的更多資訊)
pair = address(uint(keccak256(abi.encodePacked(address(uint(keccak256(abi.encodePacked(
hex'ff',
factory,
keccak256(abi.encodePacked(token0, token1)),
hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // init code hash
))));
然後,它獲取新部署合約的地址,我們需要用它來從這對代幣中鑄造代幣。
當您向貨幣對新增流動性時,合約會生成 LP 代幣;當你移除流動性時,LP 代幣就會被銷燬。
pairFor
因此,首先我們使用UniswapV2Library獲取地址:
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);UniswapV2Library.pairFor(factory, tokenA, tokenB);
因此,稍後可以鑄造 ERC20 代幣並計算返回的流動性:
liquidity = IUniswapV2Pair(pair).mint(to);
如果您想知道為什麼它最終成為 ERC20,在 mint 函式中它是這樣儲存的https://github.com/Uniswap/v2-core/blob/ee547b17853e71ed4e0101ccfd52e70d5acded58/contracts/UniswapV2Pair.sol#L112)
uint balance0 = IERC20(token0).balanceOf(address(this));
uint balance1 = IERC20(token1).balanceOf(address(this));
****函式removeLiquidity():
function removeLiquidity(
address tokenA,
address tokenB,
uint liquidity,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) external returns (uint amountA, uint amountB);
從池中移除流動性意味著燃燒 LP 代幣以換取一定數量的基礎代幣。
IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity);
然後,外部函式返回兩個值(uint amountA, uint amountB)
,這些值是使用傳遞給函式的引數計算的。
隨提供的流動性返回的代幣數量計算如下:
amount0 = liquidity.mul(balance0) / _totalSupply;
amount1 = liquidity.mul(balance1) / _totalSupply;
然後它將這些數量的代幣轉移到指定的地址
_safeTransfer(_token0, to, amount0);
_safeTransfer(_token1, to, amount1);
您的 LP 代幣份額越大,銷燬後獲得的儲備份額就越大。
上面的這些計算發生在 burn 函式內部
IUniswapV2Pair(對).burn(對)
IUniswapV2Pair(pair).burn(to)
****函式swapExactTokensForTokens()
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external returns (uint[] memory amounts);
Uniswap 的核心功能是交換代幣,所以讓我們弄清楚程式碼中發生了什麼,以便更好地理解它
您很可能聽說過流動資金池中使用的神奇公式
X * Y = K
所以,這將首先發生在 swap 函式內部getAmountOut()
。
裡面用到的關鍵函式有:
TransferHelper.safeTransferFrom().safeTransferFrom()
代幣金額髮送到配對代幣的地方
在 UniswapV2Pair 合約的較低階別交換功能中,它將是
_safeTransfer(_token, to, amountOut);
這將實際轉移回預期地址。
我知道資訊量很大,但您將有足夠的時間閱讀所有內容,直到完全理解為止。所以……
UniswapV2Factory.sol
工廠合約是所有已部署對合約的登入檔。這個合約是必要的,因為我們不希望有成對的相同代幣,這樣流動性就不會分成多個相同的對。
該合約還簡化了配對合約的部署:無需透過任何外部呼叫手動部署配對合約,只需呼叫工廠合約中的方法即可。
好吧,讓我們倒回去,因為在上面的這些行中已經說了非常重要的事情。我們把它們拆分開來分別分析:
該合約是所有已部署對合約的登入檔
只部署了一個工廠合約,該合約用作 Uniswap 交易對的官方註冊處。
現在,我們在程式碼中的什麼地方看到了它以及發生了什麼:
address[] public allPairs;
它有 的陣列allPairs
,如上所述,儲存在這個合約中。這些對被新增到一個方法中,該方法createPair()
透過將新初始化的對推送到陣列來呼叫。
allPairs.push(pair);push(pair);
這個合約是必要的,因為我們不想擁有成對的相同代幣
mapping(address => mapping(address => address)) public getPair;
它具有該對的地址與構成該對的兩個令牌的對映。這用於檢查一對是否已經存在。
require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS');
該合約還簡化了配對合約的部署
這是一個更深層次的話題,但我將嘗試總結一下這裡發生的事情的重要性。
在以太坊中,合約可以部署合約。可以呼叫已部署合約的函式,該函式將部署另一個合約。
您不需要從您的計算機上編譯和部署合約,您可以透過現有合約來執行此操作。
那麼,Uniswap 是如何部署智慧合約的呢?
透過使用操作碼CREATE2
bytes memory bytecode = type(UniswapV2Pair).creationCode;type(UniswapV2Pair).creationCode;
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
在第一行,我們得到建立位元組碼UniswapV2Pair
下一行建立了salt
一個位元組序列,用於確定性地生成新合約的地址。
最後一行是我們呼叫以使用+create2
確定性地建立新地址的地方。部署。bytecode``salt``UniswapV2Pair
並得到對地址,我們可以看到這是createPair()
函式的返回值
function createPair(
address tokenA, address tokenA,
address tokenB
) external returns (address pair)
當提供的標記不是現有的對_addLiquidity()
時,它在內部函式中使用。
所以,這就是關於 Uniswap 程式碼的全部內容。
現在,為了看到我們測試的所有內容,我可以推薦您檢視 Smart Contract Programmer 在他的defi-by-example 內容中實現的程式碼,他已經在影片中進行了解釋。
在這裡你可以看到我們可以增加流動性的方式:
function addLiquidity(
address _tokenA,
address _tokenB,
uint _amountA,
uint _amountB
) external {
IERC20(_tokenA).transferFrom(msg.sender, address(this), _amountA);
IERC20(_tokenB).transferFrom(msg.sender, address(this), _amountB);
IERC20(_tokenA).approve(ROUTER, _amountA);
IERC20(_tokenB).approve(ROUTER, _amountB);
(uint amountA, uint amountB, uint liquidity) =
IUniswapV2Router(ROUTER).addLiquidity(
_tokenA,
_tokenB,
_amountA,
_amountB,
1,
1,
address(this),
block.timestamp
);
emit Log("amountA", amountA);
emit Log("amountB", amountB);
emit Log("liquidity", liquidity);
}
以及我們必須如何考慮消除流動性:
function removeLiquidity(address _tokenA, address _tokenB) external {
address pair = IUniswapV2Factory(FACTORY).getPair(_tokenA, _tokenB);
uint liquidity = IERC20(pair).balanceOf(address(this));
IERC20(pair).approve(ROUTER, liquidity);
(uint amountA, uint amountB) =
IUniswapV2Router(ROUTER).removeLiquidity(
_tokenA,
_tokenB,
liquidity,
1,
1,
address(this),
block.timestamp
);
emit Log("amountA", amountA);
emit Log("amountB", amountB);
}
透過Github 獲取更多區塊鏈學習資料!