前言:uniswap讓我再一次感知到知易行難,2019年的時候就知道這個項目,組內有人分享,但並沒引起我足夠的重視,導致起個大早趕個晚集,所以人的精力還是要專注,長時間專注於一個領域才開花結果。
UniswapV2合約分為核心合約和周邊合約,均使用Solidity語言編寫。其核心合約實現了UniswapV2的完整功能(創建交易對,流動性供給,交易代幣,價格預言機等),但對用戶操作不友好;而周邊合約是用來讓用戶更方便的和核心合約交互。
UniswapV2核心合約主要由factory合約(UniswapV2Factory.sol)、交易對模板合約(UniswapV2Pair.sol)及輔助工具庫與接口定義等三部分組成。
解析版本二:
https://gitee.com/facelay/uniswap-v2-core-master
UniswapV2介紹
https://blog.csdn.net/weixin_39430411/article/details/108665694
UniswapV2核心合約學習(1)— UniswapV2Factory.sol
https://blog.csdn.net/weixin_39430411/article/details/108842197
mapping(address => mapping(address => address)) public getPair;這個狀態變量是一個map(其key為地址類型,其value也是一個map),它用來記錄所有的交易對地址。注意,它的名稱為getPair並且為public的,這樣的目的也是讓默認構建的同名函數來實現相應的接口。注意這行代碼中出現了三個address,前兩個分別為交易對中兩種ERC20代幣合約的地址,最后一個是交易對合約本身的地址。
createPair函數
該函數顧名思義,是用來創建交易對。之所以將該函數放在最后講,是因為該函數相對復雜,並且還有一些知識點拓展。下面開始具體分析該函數,函數代碼在前面源碼部分已經列出了。注意:下文中所說的第幾行均不包含空行(跳過空行)。
該函數接受任意兩個代幣地址為參數,用來創建一個新的交易對合約並返回新合約的地址。注意,它的可見性為external並且沒有任何限定,意味着合約外部的任何賬號(或者合約)都可以調用該函數來創建一個新的ERC20/ERC20交易對(前提是該ERC20/ERC20交易對並未創建)。
UniswapV2核心合約學習(2)——UniswapV2ERC20.sol
https://blog.csdn.net/weixin_39430411/article/details/108965441
bytes32 public DOMAIN_SEPARATOR;
// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
-
bytes32 public DOMAIN_SEPARATOR;
用來在不同Dapp之間區分相同結構和內容的簽名消息,該值也有助於用戶辨識哪些為信任的Dapp,具體可見eip-712提案。
-
bytes32 public constant PERMIT_TYPEHASH
這一行代碼根據事先約定使用permit
函數的部分定義計算哈希值,重建消息簽名時使用。
constructor
構造器。該構造器只做了一件事,計算DOMAIN_SEPARATOR
的值。根據EIP-712的介紹,該值通過domainSeparator = hashStruct(eip712Domain)
計算。
function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
bytes32 digest = keccak256(
abi.encodePacked(
'\x19\x01',
DOMAIN_SEPARATOR,
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
)
);
address recoveredAddress = ecrecover(digest, v, r, s);
require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
_approve(owner, spender, value);
}
permit使用線下簽名消息進行授權操作。
為什么會有使用線下簽名然后再線上驗證操作這種方式呢?
首先線下簽名不需要花費任何gas,然后任何其它賬號或者智能合約可以驗證這個簽名后的消息,
然后再進行相應的操作(這一步可能是需要花費gas的,簽名本身是不花費gas的)。
線下簽名還有一個好處是減少以太坊上交易的數量,UniswapV2中使用線下簽名消息主要是為了消除代幣授權轉移時對授權交易的需求。
1、鏈下簽名消息
鏈下簽名消息相關知識可以參考Solidity官方文檔中的Solidity by Example下的Micropayment Channel示例。根據應用場景的不同,簽名的消息包含不同的內容,但一般都要包含一個防重放攻擊的元素。通常使用和以太坊交易本身相同的技巧,即使用一個nonce記錄賬號進行交易的數量,智能合約檢查該nonce以確保簽名消息不被多次使用。本例中簽名消息的內容包括:[PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline]。從代碼nonces[owner]++中 可以看到,每調用一次permit,相應地址的nonce就會加1,這樣再使用原來的簽名消息就無法再通過驗證了(重建的簽名消息不正確了),也就防止了重放攻擊。
在以太坊中,在ECDSA簽名原有的r和s的基礎上加了一個v,使用它們可以驗證簽名消息的賬號。Solidity中有一個內置的函數ecrecover來獲取消息的簽名地址,它使用簽名消息和r,s,v作為參數。
使用鏈下簽名消息的常用流程是在首先鏈上根據輸入參數重建整個簽名消息,然后將重建的簽名消息和輸入的簽名消息進行處理及比較對照,來進行相關判定和驗證輸入信息未受到篡改。
鏈下簽名計算實質上是模擬的是Solidity中的keccak256及abi.encodePacked函數,因此本合約中消息簽名的計算方式為bytes32 digest = keccak256(這行及接下來的代碼。計算后得到一個hash值digest,利用這個值和函數參數中的,r,s,v,使用ecrecover函數就可以得到消息簽名者的地址。將這個對址和owner相對比,就可以驗證該消息是否由owner簽名的(顯而易見每個賬號只能對本地址進行授權操作)。注意:簽名內容包含了spender和value,如果簽名內容的任意值做了更改,使用原來的r,s,v是無法通過驗證的。
查看了一下UniswapV2的前端,它使用了web3-react中的eth_signTypedData_v4方法來計算簽名消息中的r,s,v的,最終傳遞給了permit函數作為參數。這里V1版本前端直接使用的是Javascript + React,V2版本前端使用的是TypeScript + React。
2、EIP-712
該提案是用來增強鏈下簽名消息在鏈上的可用性的。具體內容參見github上的EIP地址:https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md,它同時提供了一個測試示例Example.sol,本合約中DOMAIN_SEPARATOR的計算方法和示例中是一致的。因為原生的簽名消息對用戶不太友好,用戶無法從中獲取更多信息,使用EIP-712第一可以讓用戶了解消息簽名的大致描述,第二可以讓用戶辨識哪些是信任的Dapp,哪些是高風險的Dapp,從而不隨便簽名消息讓自己遭受損失(比如一個惡意Dapp進行偽裝等)。
3、為什么存在permit函數
現在我們來弄明白為什么存豐permit函數。UniswapV2的核心合約雖然功能完整,但對用戶不友好,用戶需要借助它的周邊合約才能和核心合約交互。但是在涉及到流動性供給時,比如用戶減少流動性,此時用戶需要將自己的流動性代幣(一種ERC20代幣)燃燒掉。由於用戶調用的是周邊合約,周邊合約未經授權是無法進行燃燒操作的( 上面提到過)。此時,如果按照常規操作,用戶需要首先調用交易對合約對周邊合約進行授權,再調用周邊合約進行燃燒,這個過程實質上是調用兩個不同合約的兩個交易(無法合並到一個交易中),它分成了兩步,用戶需要交易兩次才能完成。
使用線下消息簽名后,可以減少其中一個交易,將所有操作放在一個交易里執行,確保了交易的原子性。在周邊合約里,減小流動性來提取資產時,周邊合約在一個函數內先調用交易對的permit函數進行授權,接着再進行轉移流動性代幣到交易對合約,提取代幣等操作。所有操作都在周邊合約的同一個函數中進行,達成了交易的原子性和對用戶的友好性。
因此permit函數存在並且執行了授權操作的原因:
第三方合約在進行ERC20代幣轉移時(代幣交易),用戶首先需要調用代幣合約進行授權(授權交易),然后才能調用第三方合約進行轉移。這樣整個過程將構成分階段的兩個交易,用戶必須交易兩次,失去了交易的原子性。使用線下消息簽名線上驗證的方式可以消除對授權交易的需求,permit就是進行線上驗證並同時執行授權的函數。
當然如果用戶會操作的話,也可以手動授權,不使用permit函數相關的周邊合約接口進行交易。
UniswapV2核心合約學習(3)——UniswapV2Pair.sol
https://blog.csdn.net/weixin_39430411/article/details/108965855