uniswap ERC20代码学习
目錄
- 1、合約源碼
- 2、代碼逐行解讀
- 3、知識拓展
- 3.1 鏈下簽名消息
- 3.2 EIP-712
- 3.3 為什么存在permit函數
- 3.4 代幣元數據
UniswapV2ERC20.sol是交易對合約的父合約,主要實現了ERC20代幣功能并增加了對線下簽名消息進行授權的支持。它除了標準的ERC20接口外還有自己的接口,因此取名為UniswapV2ERC20。
1、合約源碼
pragma solidity =0.5.16;import './interfaces/IUniswapV2ERC20.sol'; import './libraries/SafeMath.sol';contract UniswapV2ERC20 is IUniswapV2ERC20 {using SafeMath for uint;string public constant name = 'Uniswap V2';string public constant symbol = 'UNI-V2';uint8 public constant decimals = 18;uint public totalSupply;mapping(address => uint) public balanceOf;mapping(address => mapping(address => uint)) public allowance;bytes32 public DOMAIN_SEPARATOR;// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;mapping(address => uint) public nonces;event Approval(address indexed owner, address indexed spender, uint value);event Transfer(address indexed from, address indexed to, uint value);constructor() public {uint chainId;assembly {chainId := chainid}DOMAIN_SEPARATOR = keccak256(abi.encode(keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),keccak256(bytes(name)),keccak256(bytes('1')),chainId,address(this)));}function _mint(address to, uint value) internal {totalSupply = totalSupply.add(value);balanceOf[to] = balanceOf[to].add(value);emit Transfer(address(0), to, value);}function _burn(address from, uint value) internal {balanceOf[from] = balanceOf[from].sub(value);totalSupply = totalSupply.sub(value);emit Transfer(from, address(0), value);}function _approve(address owner, address spender, uint value) private {allowance[owner][spender] = value;emit Approval(owner, spender, value);}function _transfer(address from, address to, uint value) private {balanceOf[from] = balanceOf[from].sub(value);balanceOf[to] = balanceOf[to].add(value);emit Transfer(from, to, value);}function approve(address spender, uint value) external returns (bool) {_approve(msg.sender, spender, value);return true;}function transfer(address to, uint value) external returns (bool) {_transfer(msg.sender, to, value);return true;}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;}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);} }2、代碼逐行解讀
- string name 可讀的簽名域的名稱,例如Dapp的名稱,在本例中為代幣名稱。
- string version當前簽名域的版本,本例中為"1"。
- uint256 chainId。當前鏈的ID,注意因為Solidity不支持直接獲取該值,所以使用了內嵌匯編來獲取。
- address verifyingContract驗證合約的地址,在本例中就是本合約地址了。
- bytes32 salt用來消除歧義的salt,它可以用來作為DOMAIN_SEPARATOR的最后措施。在本例中對’EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'進行keccak256運算后得到的哈希值。
注意:結構體本身無法直接進行hash運算,所以構造器中先進行了轉換,hashStruct就是指將結構體轉換并計算最終hash的過程。
3、知識拓展
3.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。
3.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.3 為什么存在permit函數
現在我們來弄明白為什么存豐permit函數。UniswapV2的核心合約雖然功能完整,但對用戶不友好,用戶需要借助它的周邊合約才能和核心合約交互。但是在涉及到流動性供給時,比如用戶減少流動性,此時用戶需要將自己的流動性代幣(一種ERC20代幣)燃燒掉。由于用戶調用的是周邊合約,周邊合約未經授權是無法進行燃燒操作的( 上面提到過)。此時,如果按照常規操作,用戶需要首先調用交易對合約對周邊合約進行授權,再調用周邊合約進行燃燒,這個過程實質上是調用兩個不同合約的兩個交易(無法合并到一個交易中),它分成了兩步,用戶需要交易兩次才能完成。
使用線下消息簽名后,可以減少其中一個交易,將所有操作放在一個交易里執行,確保了交易的原子性。在周邊合約里,減小流動性來提取資產時,周邊合約在一個函數內先調用交易對的permit函數進行授權,接著再進行轉移流動性代幣到交易對合約,提取代幣等操作。所有操作都在周邊合約的同一個函數中進行,達成了交易的原子性和對用戶的友好性。
因此permit函數存在并且執行了授權操作的原因:
第三方合約在進行ERC20代幣轉移時(代幣交易),用戶首先需要調用代幣合約進行授權(授權交易),然后才能調用第三方合約進行轉移。這樣整個過程將構成分階段的兩個交易,用戶必須交易兩次,失去了交易的原子性。使用線下消息簽名線上驗證的方式可以消除對授權交易的需求,permit就是進行線上驗證并同時執行授權的函數。
當然如果用戶會操作的話,也可以手動授權,不使用permit函數相關的周邊合約接口進行交易。
3.4 代幣元數據
什么叫代幣元數據,指的是代幣名稱,符號(簡寫)和精度。這三種元數據雖然存在于標準的ERC20協議中,必須得到實現,但是對于代幣轉移本身來講卻是沒有任何作用或者意義的(代幣轉移函數transfer和transferFrom并未使用到它們)。它們屬于對外展示的屬性,所以在ERC1155協議中,不管是同質代幣還是非同質代幣(例如ERC721藏品)已經取消了這三種元數據,設法將它們放到了鏈下(不過放到鏈下就意味著需要一個額外的存儲媒介)。然而當前錢包對ERC1155的支持并不太友好,并且ERC1155代幣統一處理各種資產,無法同時滿足多種場景需求。ERC1155提案雖然已變成Final狀態兩年了,始終未得到大規模應用。
總結
以上是生活随笔為你收集整理的uniswap ERC20代码学习的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: windows 10 宽带拨号时无法开启
- 下一篇: html类选择器和id选择器,类和ID选