記得朋友圈看到過一句話,如果Defi是以太坊的皇冠,那么Uniswap就是這頂皇冠中的明珠,Uniswap目前已經是V2版本,相對V1,它的功能更加全面優化,然而其合約原始碼卻并不復雜,本文為個人學習UniswapV2原始碼的系列記錄,
一、UniswapV2合約簡要介紹
UniswapV2合約分為核心合約和周邊合約,均使用Solidity語言撰寫,其核心合約實作了UniswapV2的完整功能(創建交易對,流動性供給,交易代幣,價格預言機等),但對用戶操作不友好;而周邊合約是用來讓用戶更方便的和核心合約互動,
UniswapV2核心合約主要由factory合約(UniswapV2Factory.sol)、交易對模板合約(UniswapV2Pair.sol)及輔助工具庫與介面定義等三部分組成,這次先學習UniswapV2Factory合約,
二、UniswapV2Factory合約原始碼一覽
其檔案名為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;
}
}
該檔案代碼很短,只有49行,我們來逐行學習該代碼,
三、UniswapV2Factory合約原始碼逐行學習
3.1、比較簡單的代碼部分
-
代碼的第一行,設定使用的Solidity編譯器的版本,這里估計是為了更嚴謹,使用了精確的編譯器版本
0.5.16,而不是我們常用的>= 0.5.16或者^0.5.16, -
代碼中的兩個
import陳述句分別匯入了factory所必須實作的介面合約及交易對模板合約,這個也很簡單, -
contract UniswapV2Factory is IUniswapV2Factory定義了UniswapV2Factory合約是一個IUniswapV2Factory,它必須實作其所有介面, -
feeTo這個狀態變數主要是用來切換開發團隊手續費開關,在UniswapV2中,用戶在交易代幣時,會被收取交易額的千分之三的手續費分配給所有流動性供給者,如果feeTo不為零地址,則代表開關打開,此時會在手續費中分1/6給開發團隊,feeTo設定為零地址(默認值),則開關關閉,不從流動性供給者中分走1/6手續費,它的訪問權限設定為public后編譯器會默認構建一個同名public函式,正好用來實作IUniswapV2Factory.sol中定義的相關介面, -
feeToSetter這個狀態變數是用來記錄誰是feeTo設定者,其讀取權限設定為public的主要目的同上, -
mapping(address => mapping(address => address)) public getPair;這個狀態變數是一個map(其key為地址型別,其value也是一個map),它用來記錄所有的交易對地址,注意,它的名稱為getPair并且為public的,這樣的目的也是讓默認構建的同名函式來實作相應的介面,注意這行代碼中出現了三個address,前兩個分別為交易對中兩種ERC20代幣合約的地址,最后一個是交易對合約本身的地址, -
allPairs,記錄所有交易對地址的陣列,雖然交易對址前面已經使用map記錄了,但map無法遍歷,如果想遍歷和索引,必須使用陣列,注意它的名稱和權限,同樣是為了實作介面, -
event PairCreated(address indexed token0, address indexed token1, address pair, uint);交易對被創建時觸發的事件,注意引數中的indexed表明該引數可以被監聽端(輕客戶端)過濾, -
constructor(address _feeToSetter) public { feeToSetter = _feeToSetter; }構造器,很簡單,引數提供了一個初始
feeToSetter地址作為feeTo的設定者地址,不過此時feeTo仍然為默認值零地址,開發團隊手續費未打開, -
function allPairsLength() external view returns (uint) { return allPairs.length; }這個函式非常簡單,回傳所有交易對地址陣列的長度,這樣在合約外部可以方便使用類似
for這樣的形式遍歷該陣列, -
我們先跳過
createPair函式,該函式最后學習,先看setFeeTo函式:function setFeeTo(address _feeTo) external { require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN'); feeTo = _feeTo; }這個函式也很簡單,用來設定新的
feeTo以切換開發團隊手續費開關(可以為開發團隊接收手續費的地址,也可以為零地址),注意,該函式首先使用require函式驗證了呼叫者必須為feeTo的設定者feeToSetter,如果不是則會重置整個交易, -
setFeeToSetter函式function setFeeToSetter(address _feeToSetter) external { require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN'); feeToSetter = _feeToSetter; }該函式用來轉讓
feeToSetter,它首先判定呼叫者必須是原feeToSetter,否則重置整個交易,但這里有可能存在這么一種情況:當原
feeToSetter不小心輸錯了新的設定者地址_feeToSetter時,設定會立即生效,此時feeToSetter為一個錯誤的或者陌生的無控制權的地址,無法再通過該函式設定回來,雖然UniswapV2團隊不會存在這種疏忽,但是我們自己在使用時,還是有可能發生的,有一種方法可以解決這個問題,就是使用一個中間地址值過渡一下,而新的feeToSetter必須再呼叫一個接受方法才能真正成為設定者,如果在接受之前發現設定錯誤,原設定者可以重新設定,具體代碼實作可以參考下面的Owned合約的owner轉讓實作:pragma solidity ^0.4.24; contract Owned { address public owner; address public newOwner; event OwnershipTransferred(address indexed _from, address indexed _to); constructor() public { owner = msg.sender; } modifier onlyOwner { require(msg.sender == owner,"invalid operation"); _; } function transferOwnership(address _newOwner) public onlyOwner { newOwner = _newOwner; } function acceptOwnership() public { require(msg.sender == newOwner,"invalid operation"); emit OwnershipTransferred(owner, newOwner); owner = newOwner; newOwner = address(0); } }
3.2、createPair函式
該函式顧名思義,是用來創建交易對,之所以將該函式放在最后講,是因為該函式相對復雜,并且還有一些知識點拓展,下面開始具體分析該函式,函式代碼在前面原始碼部分已經列出了,注意:下文中所說的第幾行均不包含空行(跳過空行),
該函式接受任意兩個代幣地址為引數,用來創建一個新的交易對合約并回傳新合約的地址,注意,它的可見性為external并且沒有任何限定,意味著合約外部的任何賬號(或者合約)都可以呼叫該函式來創建一個新的ERC20/ERC20交易對(前提是該ERC20/ERC20交易對并未創建),
-
該函式前四行主要是用來進行引數驗證,并且同時將代幣地址從小到大排序,
- 第1行用來驗證兩種代幣的合約地址不能相同,也就交易對必須是兩種不同的ERC20代幣,
- 第2行用來對兩種代幣的合約地址從小到大排序,因為地址型別底層其實是uint160,所以也是有大小可以排序的,
- 第3行用來驗證兩個地址不能為零地址,為什么只驗證了
token0呢,因為token1比它大,它不為零地址,token1肯定也就不為零地址, - 第4行用來驗證交易對并未創建(不能重復創建相同的交易對),
-
該函式第5-10行用來創建交易對合約并初始化,
-
第5行用來獲取交易對模板合約
UniswapV2Pair的創建位元組碼creationCode,注意,它回傳的結果是包含了創建位元組碼的位元組陣列,型別為bytes,類似的,還有運行時的位元組碼runtimeCode,creationCode主要用來在內嵌匯編中自定義合約創建流程,特別是應用于create2操作碼中,這里create2是相對于create操作碼來講的,注意該值無法在合約本身或者繼承合約中獲取,因為這樣會導致自回圈參考, -
第6行用來計算一個
salt,注意,它使用了兩個代幣地址作為計算源,這就意味著,對于任意交易對,該salt是固定值并且可以線下計算出來, -
第7行中的
assembly代表這是一段內嵌匯編代碼,Solidity中內嵌匯編語言為Yul語言,在Yul中,使用同名的內置函式來代替直接使用操作碼,這樣更易讀,后面的左括號代表內嵌匯編作用域開始, -
第8行在Yul代碼中使用了
create2函式(該函式名表明使用了create2操作碼)來創建新合約,我們看一下該函式的定義:create2(v, p, n, s) C create new contract with code mem[p…(p+n)) at address keccak256(0xff . this . s . keccak256(mem[p…(p+n))) and send v wei and return the new address, where 0xffis a 1 byte value,thisis the current contract’s address as a 20 byte value andsis a big-endian 256-bit value-
第一欄為函式定義,可以看到它有四個引數,
-
第二欄代表開始適用的以太坊的版本,
C代表Constantinople,也就是從君士坦丁堡版本開始可用,相應的還有F–前沿版本,H–家園版本,B–拜占庭版本,I–伊斯坦布爾版本,在時間軸上,不同版本由舊到新分別為:F => H => B => C => I,也就是 前沿 => 家園 => 拜占庭 => 君士坦丁堡 => 伊斯坦布爾 ,
使用該函式時注意對應的以太坊版本,
-
第三欄是解釋,從中可以看到
v代表發送到新合約的eth數量(以wei為單位),p代表代碼的起始記憶體地址,n代表代碼的長度,s代表salt,另外它還給出了新合約地址的計算公式,
-
-
第9行是內嵌匯編作用域結束,
-
第10行是呼叫新創建的交易對合約的一個初始化方法,將排序后的代幣地址傳遞過去,為什么要這樣做呢,因為使用
create2函式創建合約時無法提供構造器引數,
-
-
該函式的第11-14行用來記錄新創建的交易對地址并觸發交易對創建事件,
- 第11行和第12行用來將交易對地址記錄到map中去,因為:1、A/B交易對同時也是B/A交易對;2、但在查詢交易對時,用戶提供的兩個代幣地址并沒有排序,所以需要記錄兩次,
- 第13行將交易對地址記錄到陣列中去,便于合約外部索引和遍歷,
- 第14行觸發交易對創建事件,
-
create2函式中知識點拓展,-
這里我們先稍微提一下以太坊虛擬機中賬號的記憶體管理,每個賬號(包含合約)都有一個記憶體區域,該記憶體區域是線性的并且在位元組等級上尋址,但是讀取限定為256位(32位元組)大小,寫的時候可以為8位(1位元組)或者256位(32位元組)大小,
-
Solidity中內嵌匯編訪問本地變數時,如果本地變數是值型別,直接使用該值 ;如果本地變數是參考型別(對記憶體或者calldata的參考),那么會使用它在記憶體或者calldata中的地址,而不是值本身,在Solidity中,
bytes為動態大小的位元組陣列,它不是值型別而是參考型別,類似的string也是參考型別,
注意到
create2函式呼叫時使用了型別資訊creationCode,結合上面的知識拓展,從該函式代碼中我們可以得到:bytecode為記憶體中包含創建位元組碼的位元組陣列,它的型別為bytes,是參考型別,根據上述提到的記憶體讀取限制和內嵌匯編訪問本地參考型別的變數的規則,它在內嵌匯編中的實際值為該位元組陣列的記憶體地址,函式中首先讀取了該記憶體地址起始的256位(32位元組),它存盤了creationCode的長度,具體的獲取方法為mload(bytecode),- 記憶體中
creationCode的實際內容的起始地址為add(bytecode, 32),為什么會在bytecode上加32呢?因為剛才提到從bytecode開始的32位元組存盤的是creationCode的長度,從第二個32位元組開始才是存的實際creationCode內容, create2函式解釋中的p對應代碼中的add(bytecode, 32),解釋中的n對應為mload(bytecode),
-
其實以太坊中這樣的方式很常見,比如某函式呼叫的引數為陣列時(calldata型別),引數部分編碼后,首先第一個單元(32位元組)記錄的是陣列長度,接下來才是陣列元素,每個元素(值型別)一個單元(32位元組),
因為使用內嵌匯編會增加閱讀難度,所以在Solidity0.6.2版本以后,提供了新語法來實作create2函式的功能,直接在語言級別上支持使用salt創建合約,參見下面示例代碼中的合約d的創建程序:
pragma solidity >0.6.1 <0.7.0;
contract D {
uint public x;
constructor(uint a) public {
x = a;
}
}
contract C {
function createDSalted(bytes32 salt, uint arg) public {
/// This complicated expression just tells you how the address
/// can be pre-computed. It is just there for illustration.
/// You actually only need ``new D{salt: salt}(arg)``.
address predictedAddress = address(bytes20(keccak256(abi.encodePacked(
byte(0xff),
address(this),
salt,
keccak256(abi.encodePacked(
type(D).creationCode,
arg
))
))));
D d = new D{salt: salt}(arg);
require(address(d) == predictedAddress);
}
}
該代碼中通過直接在new的D合約型別后面加上salt選項的方式進行自定義的合約創建,等效使用Yul中的create2函式,注意該示例中predictedAddress的計算方法和create2函式解釋中的地址計算方法是一致的,
注意,使用示例中的語法創建新合約還可以提供構造器引數,并不存在create2函式中無法使用構造器引數的問題,因此它也移除了新合約初始化函式的部分需求(初始化在構建器中進行),但是UniswapV2指定了Solidity的編譯器版本為0.5.16,所以無法使用該語法,如果我們自己要使用,需要將編譯器版本指定為0.6.2以上,同時需要注意Solidity0.6.2以上的具體某個版本和0.5.16版本有哪些不同并加以修改,
至此,UniswapV2核心合約中的第一個合約UniswapV2Factory.sol的學習就結束了,由于個人能力有限,難免有理解錯誤或者不正確的地方,還請大家留言多多指正,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/141897.html
標籤:其他
上一篇:全國第一所螞蟻鏈大學落地江西 2020區塊鏈創新應用高峰論壇圓滿落幕
下一篇:花火交易所系統開發搭建
