透過鏈下簽名授權實現更少 Gas 的 ERC20代幣

買賣虛擬貨幣
解鎖消耗到了大量的 gas每個人都在談論 “無gas” 的以太坊交易,因為沒有人喜歡支付gas費用。但是以太坊網路的執行正是因為交易是付費的。那麼,你怎麼才能“無gas”交易呢? 這是什麼法術?在本文中,我將展示如何使用 “無 gas” 交易背後的模式。你會發現,儘管以太坊沒有免費的午餐之類的東西,但是你可以透過有趣的方式改變 gas 成本。透過運用本文中的知識,你的使用者將節省大量 gas,享受更好的使用者體驗,甚至可以在你的智慧合約中構建新穎的委派模式。可是等等!還有更多!為方便起見,我將所需的所有工具都放在了此儲存庫[3]中。因此,現在你實現 “無 gas” 代幣的障礙就突然降低了很多。讓我們開始吧。
背景我不得不承認,即使我知道如何在智慧合約中實現“無 gas”交易,但對於使它們成為可能的密碼學我也知之甚少。那對我來說不是障礙,所以對你也不應該是。據我所知,私鑰用於簽署傳送給以太坊的交易,一些密碼學魔術用於將我(簽名者)識別為msg.sender。這支撐了以太坊中所有訪問控制。“無 gas” 交易背後的法寶是,我可以使用我的私鑰和要執行的智慧合約交易進行簽名。簽名是在鏈下進行的,而無需花費任何 gas。然後,我可以將此簽名交給其他人,以他們的名義代表我執行交易。簽名函式通常就是常規合約方法,但會使用其他簽名引數進行擴充套件。例如,在dai.sol[4]中,我們有授權(approve)函式:
function approve(address usr, uint wad) external returns (bool)我們還具有permit許可函式,該功能與approve函式相同,但是將簽名作為引數。function permit(address holder, address spender, uint256 nonce, uint256 expiry, bool allowed, uint8 v, bytes32 r, bytes32 s) external不用擔心所有這些額外的引數,我們將介紹它們。你需要注意的是這兩個函式都使用allowance對映執行的操作:function approve(address usr, uint wad) external returns (bool){
  allowance[msg.sender][usr] = wad;  …}function permit(  address holder, address spender,  uint256 nonce, uint256 expiry, bool allowed,
  uint8 v, bytes32 r, bytes32 s) external {  …  allowance[holder][spender] = wad;  …}
如果使用approve,則允許spender最多使用wad個代幣。如果你給某人提供有效的簽名,則該人可以呼叫permit以允許spender  使用你的代幣。因此,基本上,“無 gas”交易背後的模式是製作可以提供給某人的簽名,以便他們可以安全地執行特殊交易。這就像授予某人執行函式的許可權。這是一種授權模式。標準如果你像我一樣,那麼你要做的第一件事就是深入研究程式碼。我立即注意到此註釋:
// — — EIP712 niceties — -有了這個,我鑽進了兔子洞[5],卻無望地迷路了。現在,我已經理解了,我可以用簡單的方式來解釋它。EIP712[6]描述瞭如何以通用方式構建函式簽名。其他EIP描述瞭如何將EIP712[7]應用。例如,EIP2612[8]描述瞭如何使用EIP712[9]的簽名應用於permit函式,其功能應與ERC20代幣中的approve功能相同。如果你只想實現之前提到的簽名功能,例如將簽名批准新增到自己的MetaCoin,則可以閱讀EIP2612[10] ,你甚至可以繼承實現過的合約,並減輕生活壓力。在本文中,我們將研究dai.sol[11]中“無 gas”交易的實現。這將使事情變得清晰。dai.sol[12]實現發生在EIP2612[13]之前,會略有不同。那不會有問題。簽名組成
EIP712[14]簽名的早期實現可以在dai.sol[15]原始碼中找到 。它允許Dai持有人透過計算鏈下簽名並將其提供給支出者(spender)來批准轉賬交易,而不是自己呼叫approve函式。它包含下面幾個部分:一個 DOMAIN_SEPARATOR  .一個 PERMIT_TYPEHASH .一個 nonces 變數.一個 permit 函式.
這是DOMAIN_SEPARATOR,和相關變數:string  public constant name     = "Dai Stablecoin";string  public constant version  = "1";bytes32 public DOMAIN_SEPARATOR;constructor(uint256 chainId_) public {  ...
  DOMAIN_SEPARATOR = keccak256(abi.encode(    keccak256(      "EIP712Domain(string name,string version," +       "uint256 chainId,address verifyingContract)"    ),    keccak256(bytes(name)),
    keccak256(bytes(version)),    chainId_,    address(this)  ));}DOMAIN_SEPARATOR只不過是唯一標識智慧合約的雜湊。它是由EIP712域(EIP712Domain)的字串,包含代幣合約的名稱,版本,所在的chainId以及合約部署的地址構成。
所有這些資訊都在建構函式上進行hash 運算賦值到DOMAIN_SEPARATOR變數中,該變數在建立線下簽名時由持有人使用,並且在執行permit時需要匹配。這樣可以確保簽名僅對一個合約有效。

這是PERMIT_TYPEHASH:

PERMIT_TYPEHASH 是函式名稱(大寫開頭)和所有引數(包括型別和名稱)的雜湊。目的是清楚地標誌簽名的函式。

簽名將在permit函式中處理,如果使用的PERMIT_TYPEHASH不是該特定函式的簽名,它將回退交易。這樣可以確保僅將簽名用於預期的功能。

然後是nonces對映:

mapping (address => uint) public nonces;

該對映記錄了特定持有人已使用了多少次簽名。建立簽名時,需要包含一個nonces值。執行permit時,所包含的nonce 值必須與該持有人到目前為止使用的簽名數完全匹配。這樣可以確保每個簽名僅使用一次。

所有這三個條件,即PERMIT_TYPEHASH,DOMAIN_SEPARATOR和nonce,確保每個簽名僅用於預期的合約,預期的函式,並且僅使用一次。

現在,讓我們看看如何在智慧合約中處理簽名。

permit 函式

permit是dai.sol[16]裡實現的函式,允許使用簽名來修改持有人的 allowance對spender授權的數量。

// --- 透過簽名授權 ---
function permit(
  address holder, address spender,
  uint256 nonce, uint256 expiry, bool allowed,
  uint8 v, bytes32 r, bytes32 s
) external;

如你所見,permit有很多引數。它們是計算簽名所需的所有引數,加上簽名本身就是v,r和s。

你需要用引數建立簽名似乎很愚蠢,但是你確實需要。因為僅能從簽名中恢復簽名的地址。我們將使用所有引數和恢復的地址來確保簽名有效。

首先,我們使用確保安全性所需的所有引數來計算digest。作為簽名建立的一部分,holder將需要在鏈下計算出完全相同的digest:

bytes32 digest =
  keccak256(abi.encodePacked(
    "\x19\x01",
    DOMAIN_SEPARATOR,
    keccak256(abi.encode(
      PERMIT_TYPEHASH,
      holder,
      spender,
      nonce,
      expiry,
      allowed
    ))
  ));

使用ecrecover和v,r,s簽名,我們可以恢復地址。如果它是holder的地址,我們知道所有引數都匹配DOMAIN_SEPARATOR,PERMIT_TYPEHASH,nonce,holder,spender,expiry和allowed。哪怕是任何一點內容沒匹配,則簽名被拒絕:

require(holder == ecrecover(digest, v, r, s), "Dai/invalid-permit");

請注意這裡。簽名中有許多引數,其中一些引數有點模糊,例如chainId (它是 DOMAIN_SEPARATOR 的一部分)。它們中的任何一個不匹配都會導致簽名被拒絕,並帶有完全相同的錯誤提示,這讓鏈下除錯簽名很困難。

現在我們知道 holder 批准了這個函式呼叫。接下來,我們將證明簽名沒有被濫用。我們檢查當前時間是否在 expiry(過期)之前,這保證了僅在特定時間內許可有效。

require(expiry == 0 || now <= expiry, "Dai/permit-expired");

我們還會檢查簽名中的 nonce ,以便每個簽名只能使用一次。

require(nonce == nonces[holder]++, "Dai/invalid-nonce");

這些檢查都透過了!dai.sol[17]使spender可以使用的holder的代幣數量設定為最大值(即allowance設定為最大),並觸發一個事件,僅此而已。

uint wad = allowed ? uint(-1) : 0;
allowance[holder][spender] = wad;
emit Approval(holder, spender, wad);

dai.sol[18]合約對 allowance 使用的是二分法設定(譯者注:要麼是最大,要麼是 0), 在程式碼庫[19],有更傳統的方法。

建立鏈下簽名

建立簽名也許需要透過一些實踐才可以掌握它。我們將分三步複製智慧合約中permit的功能:

生成 DOMAIN_SEPARATOR
生成 digest
建立交易簽名

以下函式將生成 DOMAIN_SEPARATOR。它與dai.sol[20]建構函式中的程式碼相同,但在JavaScript中實現,並使用ethers.js[21]的keccak256,defaultAbiCoder和toUtfBytes,它需要代幣名稱和部署地址,以及chainId。假定代幣版本為“1”。

以下函式將為特定的permit呼叫生成digest。注意,holder,spender,nonce 和 expiry作為引數傳遞。為了清楚起見,它還傳遞了一個 approve.allowed 引數,儘管你可以將其始終設定為 true,否則簽名將被拒絕。從剛剛dai.sol[22]複製PERMIT_TYPEHASH。

一旦我們有了digest,對其進行簽名就相對容易了。我們從[digest]中刪除0x字首後,使用ethereumjs-util[23]中的ecsign。請注意,我們需要使用者私鑰才能執行此操作。

在程式碼中,我們將按以下方式呼叫這些函式:

請注意,對permit的呼叫需要重用用於建立digest的所有引數。只有在這種情況下,簽名才有效。

還要注意的是,此程式碼段中僅有的兩個交易是由user2呼叫的。user1是holder,是建立digest並簽名的使用者。但是,user1並沒有花費任何gas。

user1將簽名提供給user2,後者使用它來執行user1授權的 permit 和transferFrom。

從 user1的角度來看,這是一次“無 gas”交易, 他沒有花一分錢。

結論

本文介紹瞭如何使用“無Gas”交易,闡明瞭“無Gas”實際上意味著將Gas成本轉移給其他人。為此,我們需要一個智慧合約中的功能,該功能可以處理預先簽署的交易,並且需要進行大量的資料檢驗以確保一切安全。

但是,使用此模式有很多好處,因此,它被廣泛使用。簽名允許將交易 gas 成本從使用者轉移到服務提供商,從而在許多情況下消除了相當大的障礙。它還允許實現更高階的委派模式,通常會對UX進行相當大的改進。

已為您提供入門程式碼庫[24],請使用它。

參考資料

[1]登鏈翻譯計劃: https://github.com/lbc-team/Pioneer

[2]Tiny熊: https://learnblockchain.cn/people/15

[3]此儲存庫: https://github.com/albertocuestacanada/ERC20Permit?ref=learnblockchain.cn

[4]dai.sol: https://github.com/makerdao/dss/blob/master/src/dai.sol?ref=learnblockchain.cn

[5]鑽進了兔子洞: https://learnblockchain.cn/docs/eips/eip-712.html

[6]EIP712: https://learnblockchain.cn/docs/eips/eip-712.html

[7]EIP712: https://learnblockchain.cn/docs/eips/eip-712.html

[8]EIP2612: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2612.md?ref=learnblockchain.cn

[9]EIP712: https://learnblockchain.cn/docs/eips/eip-712.html

[10]EIP2612: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2612.md?ref=learnblockchain.cn

[11]dai.sol: https://github.com/makerdao/dss/blob/master/src/dai.sol?ref=learnblockchain.cn

[12]dai.sol: https://github.com/makerdao/dss/blob/master/src/dai.sol?ref=learnblockchain.cn

[13]EIP2612: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2612.md?ref=learnblockchain.cn

[14]EIP712: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md?ref=learnblockchain.cn

[15]dai.sol: https://github.com/makerdao/dss/blob/master/src/dai.sol

[16]dai.sol: https://github.com/makerdao/dss/blob/master/src/dai.sol?ref=learnblockchain.cn

[17]dai.sol: https://github.com/makerdao/dss/blob/master/src/dai.sol?ref=learnblockchain.cn

[18]dai.sol: https://github.com/makerdao/dss/blob/master/src/dai.sol?ref=learnblockchain.cn

[19]程式碼庫: https://github.com/albertocuestacanada/ERC20Permit?ref=learnblockchain.cn

[20]dai.sol: https://github.com/makerdao/dss/blob/master/src/dai.sol?ref=learnblockchain.cn

[21]ethers.js: https://learnblockchain.cn/docs/ethers.js/

[22]dai.sol: https://github.com/makerdao/dss/blob/master/src/dai.sol?ref=learnblockchain.cn

[23]digest]中刪除0x字首後,使用[ethereumjs-util: https://github.com/ethereumjs/ethereumjs-util?ref=learnblockchain.cn

[24]程式碼庫: https://github.com/albertocuestacanada/ERC20Permit?ref=learnblockchain.cn

[25]Cell Network: https://www.cellnetwork.io/?utm_souce=learnblockchain

[26]@albertocuestacanada: https://hackernoon.com/u/albertocuestacanada

免責聲明:

  1. 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
  2. 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
  3. 鏈報僅提供相關項目信息,不構成任何投資建議

推荐阅读

;