目錄
- 1、核心流程
- 2、合約結構
- 3、創建流動性
- 3、交易
- 4、代碼注釋
- 5、代碼逐行解讀
- 5.1 簡單代碼部分
- 5.2 創建交易對代碼:createPair函式
1、核心流程
- uniswap核心合約分為3個合約,工廠合約,配對合約,ERC20合約
- 核心合約布署時只需要布署工廠合約
- 工廠合約布署時建構式只需要設定一個手續費管理員
- 在工廠合約布署之后,就可以進行創建配對的操作
- 要在交易所中進行交易,操作順序是:創建交易對,添加流動性,交易
- 添加配對時需要提供兩個token的地址,隨后工廠合約會為這個交易對布署一個新的配對合約
- 配對合約的布署是通過create2的方法
- 兩個token地址按2進制大小排序后一起進行hash,以這個hash值作為create2的salt進行布署
- 所以配對合約的地址是可以通過兩個token地址進行create2計算的
- 用戶可以將兩個token存入到配對合約中,然后在配對合約中為用戶生成一種兼容ERC20的token
- 配對合約中生成的erc20Token可以成為流動性
- 流動性的token可以進行erc20的操作,可以將流動性token傳送給其他用戶
- 用戶也可以取出流動性,配對合約將銷毀流動性,并將兩種token同時返還用戶
- 返還的數量將根據流動性數量和兩種token的儲備量重新計算,如果有手續費收益,用戶也將得到收益
- 用戶可以通過一種token交換另一種token,配對合約將扣除千分之3的手續費
- 在uniswap核心合約基礎上,還有一個路由合約用來更好的操作核心合約
- 路由合約擁有3部分操作方法,添加流動性,移除流動性,交換
- 雖然配對合約已經可以完成所有的交易操作,但路由合約將所有操作整合,配合前端更好的完成交易
- 因為路由合約的代碼量較多,布署時會超過gas限制,所以路由合約被分為兩個版本,功能互相補充
2、合約結構

3、創建流動性

3、交易

4、代碼注釋
pragma solidity =0.5.16;
import './interfaces/IUniswapV2Factory.sol';
import './UniswapV2Pair.sol';
//uniswap工廠合約,實作了IUniswapV2Factory介面
contract UniswapV2Factory is IUniswapV2Factory {
// 這個狀態變數主要是用來切換開發團隊手續費開關,在UniswapV2中,用戶在交易代幣時,會被收取交易額的千分之三的手續費分配給所有流動性供給者,如果feeTo不為零地址,則代表開關打開,此時會在手續費中分1/6給開發團隊,feeTo設定為零地址(默認值),則開關關閉,不從流動性供給者中分走1/6手續費,也就是總的千分之五,它的訪問權限設定為public后編譯器會默認構建一個同名public函式,正好用來實作IUniswapV2Factory.sol中定義的相關介面,
address public feeTo;
// 這個狀態變數是用來記錄誰是feeTo設定者,其讀取權限設定為public的主要目的同上,
address public feeToSetter;
// 這個狀態變數是用來記錄所有的交易對地址,實作了IUniswapV2Factory的getPair方法,前兩個分別為交易對中兩種ERC20代幣合約的地址,最后一個是交易對合約本身的地址,
mapping(address => mapping(address => address)) public getPair;
// 記錄所有交易對地址的陣列,實作了IUniswapV2Factory的allPairs方法,因為上面map雖然存盤了所有交易對,但是無法遍歷
address[] public allPairs;
// 配對合約的Bytecode的hash
bytes32 public constant INIT_CODE_PAIR_HASH = keccak256(abi.encodePacked(type(UniswapV2Pair).creationCode));
// 交易對被創建時觸發的事件,注意引數中的indexed表明該引數可以被監聽端(輕客戶端)過濾,
event PairCreated(address indexed token0, address indexed token1, address pair, uint);
/**
* @dev 建構式,引數提供了一個初始feeToSetter地址作為feeTo的設定者地址,不過此時feeTo仍然為默認值零地址,開發團隊手續費未打開,
* @param _feeToSetter 收稅開關權限控制
*/
constructor(address _feeToSetter) public {
feeToSetter = _feeToSetter;
}
/**
* @dev 回傳所有交易對地址陣列的長度
*/
function allPairsLength() external view returns (uint) {
return allPairs.length;
}
/**
* 該函式接受任意兩個代幣地址為引數,用來創建一個新的交易對合約并回傳新合約的地址,注意,它的可見性為external并且沒有任何限定,意味著合約外部的任何賬號(或者合約)都可以呼叫該函式來創建一個新的ERC20/ERC20交易對(前提是該ERC20/ERC20交易對并未創建),
* @param tokenA TokenA
* @param tokenB TokenB
* @return pair 配對地址
* @dev 創建配對
*/
function createPair(address tokenA, address tokenB) external returns (address pair) {
// 來驗證兩種代幣的合約地址不能相同,也就交易對必須是兩種不同的ERC20代幣
require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
// 將tokenA和tokenB進行大小排序,確保tokenA小于tokenB
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
// 確認token0不等于0地址,用來驗證兩個地址不能為零地址,為什么只驗證了token0呢,因為token1比它大,它不為零地址,token1肯定也就不為零地址
require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
// 驗證交易對未被創建
require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
// 給bytecode變數賦值"UniswapV2Pair"合約的創建位元組碼
bytes memory bytecode = type(UniswapV2Pair).creationCode;
// 將token0和token1打包后創建哈希
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
// 行內匯編
// solium-disable-next-line
assembly {
//通過create2方法布署合約,并且加鹽,回傳地址到pair變數
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
// 呼叫pair地址的合約中的"initialize"方法,傳入變數token0,token1
IUniswapV2Pair(pair).initialize(token0, token1);
// 配對映射中設定token0=>token1=pair
getPair[token0][token1] = pair;
// 配對映射中設定token1=>token0=pair
getPair[token1][token0] = pair; // populate mapping in the reverse direction
// 配對陣列中推入pair地址
allPairs.push(pair);
// 觸發配對成功事件
emit PairCreated(token0, token1, pair, allPairs.length);
}
/**
* @dev 用來設定新的feeTo以切換開發團隊手續費開關(可以為開發團隊接收手續費的地址,也可以為零地址),注意,該函式首先使用require函式驗證了呼叫者必須為feeTo的設定者feeToSetter,如果不是則會重置整個交易,
* @param _feeTo 收稅地址
*/
function setFeeTo(address _feeTo) external {
require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
feeTo = _feeTo;
}
/**
* @dev 該函式用來轉讓feeToSetter,它首先判定呼叫者必須是原feeToSetter,否則重置整個交易,
* @param _feeToSetter 收稅權限控制
*/
function setFeeToSetter(address _feeToSetter) external {
require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
feeToSetter = _feeToSetter;
}
}
5、代碼逐行解讀
5.1 簡單代碼部分
- 代碼的第一行,設定使用的Solidity編譯器的版本,這里估計是為了更嚴謹,使用了精確的編譯器版本0.5.16,而不是我們常用的>= 0.5.16或者^0.5.16
pragma solidity =0.5.16;
- 代碼中的兩個import陳述句分別匯入了factory所必須實作的介面合約及交易對模板合約
import './interfaces/IUniswapV2Factory.sol';
import './UniswapV2Pair.sol';
- contract UniswapV2Factory is IUniswapV2Factory 定義了UniswapV2Factory合約是一個IUniswapV2Factory,它必須實作其所有介面,
contract UniswapV2Factory is IUniswapV2Factory
- feeTo這個狀態變數主要是用來切換開發團隊手續費開關,在UniswapV2中,用戶在交易代幣時,會被收取交易額的千分之三的手續費分配給所有流動性供給者,如果feeTo不為零地址,則代表開關打開,此時會在手續費中分1/6給開發團隊,feeTo設定為零地址(默認值),則開關關閉,不從流動性供給者中分走1/6手續費,它的訪問權限設定為public后編譯器會默認構建一個同名public函式,正好用來實作IUniswapV2Factory.sol中定義的相關介面,
address public feeTo;
- feeToSetter這個狀態變數是用來記錄誰是feeTo設定者,其讀取權限設定為public的主要目的同上,
address public feeToSetter;
- mapping(address => mapping(address => address)) public getPair;這個狀態變數是一個map(其key為地址型別,其value也是一個map),它用來記錄所有的交易對地址,注意,它的名稱為getPair并且為public的,這樣的目的也是讓默認構建的同名函式來實作相應的介面,注意這行代碼中出現了三個address,前兩個分別為交易對中兩種ERC20代幣合約的地址,最后一個是交易對合約本身的地址,
mapping(address => mapping(address => address)) public getPair;
- allPairs,記錄所有交易對地址的陣列,雖然交易對址前面已經使用map記錄了,但map無法遍歷,如果想遍歷和索引,必須使用陣列,注意它的名稱和權限,同樣是為了實作介面,
address[] public allPairs;
- 配對合約的Bytecode的hash
bytes32 public constant INIT_CODE_PAIR_HASH = keccak256(abi.encodePacked(type(UniswapV2Pair).creationCode));
- event PairCreated(address indexed token0, address indexed token1, address pair, uint);交易對被創建時觸發的事件,注意引數中的indexed表明該引數可以被監聽端(輕客戶端)過濾,
event PairCreated(address indexed token0, address indexed token1, address pair, uint);
- 構造器,很簡單,引數提供了一個初始feeToSetter地址作為feeTo的設定者地址,不過此時feeTo仍然為默認值零地址,開發團隊手續費未打開,
constructor(address _feeToSetter) public {
feeToSetter = _feeToSetter;
}
- 這個函式非常簡單,回傳所有交易對地址陣列的長度,這樣在合約外部可以方便使用類似for這樣的形式遍歷該陣列,
function allPairsLength() external view returns (uint) {
return allPairs.length;
}
- 用來設定新的feeTo以切換開發團隊手續費開關(可以為開發團隊接收手續費的地址,也可以為零地址),注意,該函式首先使用require函式驗證了呼叫者必須為feeTo的設定者feeToSetter,如果不是則會重置整個交易,
function setFeeTo(address _feeTo) external {
require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
feeTo = _feeTo;
}
- 該函式用來轉讓feeToSetter,它首先判定呼叫者必須是原feeToSetter,否則重置整個交易,
function setFeeToSetter(address _feeToSetter) external {
require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
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);
}
}
5.2 創建交易對代碼:createPair函式
該函式接受任意兩個代幣地址為引數,用來創建一個新的交易對合約并回傳新合約的地址,注意,它的可見性為external并且沒有任何限定,意味著合約外部的任何賬號(或者合約)都可以呼叫該函式來創建一個新的ERC20/ERC20交易對(前提是該ERC20/ERC20交易對并未創建)
- 用來驗證兩種代幣的合約地址不能相同,也就交易對必須是兩種不同的ERC20代幣,
require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
- 用來對兩種代幣的合約地址從小到大排序,因為地址型別底層其實是uint160,所以也是有大小可以排序的,
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
- 用來驗證兩個地址不能為零地址,為什么只驗證了token0呢,因為token1比它大,它不為零地址,token1肯定也就不為零地址
require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
- 驗證交易對并未創建(不能重復創建相同的交易對)
require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS');
- 用來獲取交易對模板合約UniswapV2Pair的創建位元組碼creationCode,注意,它回傳的結果是包含了創建位元組碼的位元組陣列,型別為bytes,類似的,還有運行時的位元組碼runtimeCode,creationCode主要用來在內嵌匯編中自定義合約創建流程,特別是應用于create2操作碼中,這里create2是相對于create操作碼來講的,注意該值無法在合約本身或者繼承合約中獲取,因為這樣會導致自回圈參考
bytes memory bytecode = type(UniswapV2Pair).creationCode;
- 第6行用來計算一個salt,注意,它使用了兩個代幣地址作為計算源,這就意味著,對于任意交易對,該salt是固定值并且可以線下計算出來
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
- assembly代表這是一段內嵌匯編代碼,Solidity中內嵌匯編語言為Yul語言,在Yul中,使用同名的內置函式來代替直接使用操作碼,這樣更易讀,后面的左括號代表內嵌匯編作用域開始,
assembly {
- 在Yul代碼中使用了create2函式(該函式名表明使用了create2操作碼)來創建新合約,
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
- 呼叫新創建的交易對合約的一個初始化方法,將排序后的代幣地址傳遞過去,為什么要這樣做呢,因為使用create2函式創建合約時無法提供構造器引數,
IUniswapV2Pair(pair).initialize(token0, token1);
- 用來將交易對地址記錄到map中去,因為:1、A/B交易對同時也是B/A交易對;2、但在查詢交易對時,用戶提供的兩個代幣地址并沒有排序,所以需要記錄兩次
getPair[token0][token1] = pair;
getPair[token1][token0] = pair;
- 交易對地址記錄到陣列中去,便于合約外部索引和遍歷,
allPairs.push(pair);
- 觸發交易對創建事件
emit PairCreated(token0, token1, pair, allPairs.length);
轉載請註明出處,本文鏈接:https://www.uj5u.com/qukuanlian/298422.html
標籤:區塊鏈
