EIP712实现无Gas以太坊交易【含源码】

Stella981
• 阅读 944

每个人都在讨论无gas以太坊交易,因为没有人喜欢支付gas费用。但是以太坊网络能够精准地运转恰恰是因为交易需要手续费。那么如何实现无gas交易呢?让我们一起学习无gas以太坊交易的魔法!

在这篇文章中,我们将学习如何实现无gas交易模式。你会发现虽然在以太坊上没有免费的午餐,但可以用有趣的方式来转移gas成本。利用本文中学到的知识,你的DApp用户就可以省掉gas,获得更好的用户体验,或者在你的智能合约中构建新颖的代理模式。

不过等一下!还不止这些!为了方便你的使用,我已经将相关工具放到这个Github仓库了。因此现在你要实现无gas以太坊交易的门槛已经大大降低了。

现在让我们开始吧!

用自己熟悉的语言学习 以太坊DApp开发Java | Php | Python | .Net / C# | Golang | Node.JS | Flutter / Dart

1、一些背景知识

我不得不承认,虽然我了解如何在智能合约中实现无gas交易,但是并不太了解背后的密码学知识。不过对我而言这算不上大的障碍,因此如果你也不太熟悉密码学,相信也不会影响你实现无gas以太坊交易。

据我所知,我的私钥被用来签名发送到以太坊网络的交易,在这个过程中运用了一些密码学技术来识别我的身份并存入变量msg.sender,这是以太坊中访问控制的基石。

无gas交易背后的魔法在于,我们可以用自己的私钥为希望执行的合约交易 制作一个签名。

签名是链下生成的,无需消耗任何gas。一旦签名完成,就可以将交易发送给其他人替我们执行,同时也替我们支付gas费用。

使用签名的合约函数通常就是一个普通的函数,不过支持传入额外的签名参数。例如在dai.sol中,我们可以看到如下的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账号操作不超过wad个你持有的代币。

是不是一样?

因此基本上来说,无gas交易背后的模式就是制作一个签名,别人用这个签名就可以用你的身份安全地执行一个特殊的交易,就像你授权别人执行一个方法。

这其实就是一种代理模式。

2、无gas交易规范

如果你和我一样,那你可能马上就会深入研究代码。我立刻注意到了一个注释:

// — — EIP712 niceties — -

看起来是一个以太坊规范,因此我就研究了一下,不过当时并没有理解。现在我已经理解,并且可以用浅显的话语来解释了。

EIP712描述了为合约方法生成签名的通用方式。其他的EIP则描述如何在特定的用例中运用EIP712。例如EIP2612描述如何将EIP712签名用于permit方法,该方法和ERC20代币中的approve方法实现相同的功能,就像我们在前面看到的。

如果你只是想实现一个已经定义过的签名方法,比如为你的MetaCoin合约添加支持签名的approve方法,那么阅读EIP2612就够了。更简单的办法就是直接继承一个已经实现了EIP2612的合约。

在这篇文章中,我们将研究dai.sol中的一种无gas交易实现,这会帮助我们更清晰地理解其内部机制。dai.sol的无gas实现是在EIP2612之前完成的,因此有一些区别。不过这不是大问题。

3、签名构成

在dai.sol中可以看到EIP712的一个早期实现,它允许dai持有者在链下计算签名并交由spender代为执行approve方法,而不是由dai持有者直接调用approve方法。

整个实现包含4个部分:

  • 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域(合约名称、版本、链ID、部署地址)的字符串构造的。

所有这些信息在构造函数中进行哈希并存入DOMAIN_SEPARATOR变量,dai持有者在生成签名时需要使用这个变量值,并且在执行permit方法时需要匹配。DOMAIN_SEPARATOR可以确保一个签名仅对单一合约有效。

下图是PERMIT_TYPEHASH:

EIP712实现无Gas以太坊交易【含源码】

PERMIT_TYPEHASH是函数名(首字母大写)以及全部参数(包括类型和参数名)的哈希,其目的是清晰界定签名的适用方法。

在permit方法中需要处理签名,如果适用的PERMIT_TYPEHASH并不是针对这个方法的,交易就会回滚。这样就确保了一个签名仅可以用于特定的方法。

下面是nonces映射:

mapping (address => uint) public nonces;

nonces应用用来注册一个特定的dai持有者已经使用的签名数量。当创建签名时,需要包含一个nonces值,当执行permit方法时,nonce必须匹配该持有者已经使用的签名数量。这一措施用来确保签名仅使用一次。

这三者结合在一起,PERMIT_TYPEHASH、DOMAIN_SEPARATOR以及nonce,就可以确保一个签名仅可以用于特定的合约、特定的方法,并且只可以使用一次。

现在让我们看看在智能合约中是如何处理签名的。

4、permit方法

permit方法是dai.sol中实现的一个函数,它允许使用签名来实现approve相同的功能。

// --- Approve by signature ---
function permit(
  address holder, address spender,
  uint256 nonce, uint256 expiry, bool allowed,
  uint8 v, bytes32 r, bytes32 s
) external

正如你看到的,permit方法包含很多参数。这些参数是计算签名需要的数据,以及签名数据v、r和s。

传入创建签名的参数看起来很傻,但是这是必须的。因为从签名中能够恢复出来的只有签名创建者的地址。我们需要所有这些参数以及恢复出来的创建者地址来确保签名的有效性。

首先我们利用这些参数计算一个摘要数据。dai持有者需要在链下进行同样的计算,这是生成签名的必要环节:

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

使用ecrecover方法以及v、r和s,我们可以从签名中恢复出地址。如果这就是dai持有者的地址,那么我们就知道参数对上了,也就是说DOMAIN_SEPARATOR、PERMIT_TYPEHASH、nonce、holder、spender、expiry以及allowed都对。如何对不上,就拒绝这个签名:

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

这个地方需要注意。签名涉及很多参数,其中有些参数比较晦涩,例如链ID(DOMAIN_SEPARATOR的一部分)。其中任何参数对不上都会导致签名被拒绝,这使得链下签名的调试非常困难。

现在我们指导持有者已经授权了这个方法调用。接下来我们需要确认签名没有被滥用。

首先检查当前时间是否在expiry之前,这样可以让授权仅在特定时间点之前有效。

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

我们也可以检查具有这个nonce的签名还没有使用过,这样就可以确保一个签名只能使用一次。

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

现在通过了!dai.sol更新allowance,触发事件,就这些简单的工作了。

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

dai.sol合约使用二进制方式处理allowance,在我们提供的代码 中则使用了更传统的方式来处理allowance。

5、创建链下签名

创建签名不适合胆小的人,不过只需要一点练习和耐心,其实也容易掌握。我们用三个步骤来复制智能合约的permit方法中的逻辑:

  • 生成DOMAIN_SEPARATOR
  • 生成摘要
  • 生成交易签名

下面的函数将创建DOMAIN_SEPARATOR。它和dai.sol构造函数中的代码功能一样,不过使用的是javascript,以及ethers.js中的keccak256、defaultAbiCoder和toUtfBytes。这个函数需要代币名称、部署地址以及链ID,并假设代币版本为"1":

EIP712实现无Gas以太坊交易【含源码】

下面的函数将为特定的permit调用创建摘要。注意holder、spender、nonce和expiry都作为参数传入。同时传入一个approve.allowed参数,虽然你可以始终将其设置为true。注意这里的PERMIT_TYPEHASH我们是直接从dai.sol拷贝过来的。

EIP712实现无Gas以太坊交易【含源码】

一旦我们得到摘要,那么进行签名就相对容易多了。我们使用ethereumjs-util中的ecsign对移除0x前缀的摘要数据进行签名。注意这个步骤我们需要用户私钥。

EIP712实现无Gas以太坊交易【含源码】

上述js函数的调用方法如下:

EIP712实现无Gas以太坊交易【含源码】

注意我们在调用permit时是如何使用之前创建摘要的那些参数的。只有这样签名才会有效。

另一点需要注意的是,在这个代码片段中user2只调用两个交易。user1表示dai持有者,他是创建摘要并进行签名的账号。然而user1并不需要消耗任何gas。

user1将签名给user2,user2使用这个签名来执行permit方法以及transferFrom方法。

在user1看来,这就是一个无gas交易,他不需要消耗任何wei。

6、结论

本文展示了如何使用无gas交易,澄清了无gas实际上意味着将gas成本转嫁给了其他人。为此我们需要智能合约中的方法能够处理预签名交易。

不过使用这一模式有显著的好处,因此无gas交易已经被广泛使用。签名允许交易的gas成本从用户转移到服务提供商,从而消除了很多场景中的用户进入障碍。无gas交易也支持更加高级的代码模式实现,通常都会带来显著的用户体验改善。


原文链接:如何实现无Gas以太坊交易 — 汇智网

点赞
收藏
评论区
推荐文章
Souleigh ✨ Souleigh ✨
2年前
Web3 - 保护你的钱包
拥有像MetaMask这样的以太坊钱包最好的部分之一就是你可以完全控制你的钱。与WellsFargo的银行账户或Coinbase等加密货币交易所不同,我们不会代表您持有您的资产。相反,由于密码学的强大功能,您的钱实际上是您的。您可以随时随
Wesley13 Wesley13
3年前
ETH挖矿软件最新评测对比【2021】
以太坊近来高出天际的交易手续费对于DeFi等领域的以太坊开发人员而言并不全是坏消息——如果手头恰好有NVIDIA或AMD显卡,不妨利用这些闲置资源参与ETH挖矿,进而从高昂的gas手续费中获取一些额外的被动收益。在这个教程中,我们将对GMiner、TRex、EthMiner、lolMiner、PhoenixMiner、NBMiner、TeamRed
Stella981 Stella981
3年前
Defi数据引擎The Graph调用方法【含源码】
当你尝试访问以太坊智能合约以及DApp产生的区块链数据时,可能会发现很难将数据转换为一种可读的格式。TheGraph提供了一种用于查询以太坊和IPFS网络数据的索引协议,任何人都可以基于其提供的开放API创建并发布索引数据,即subgraph,这使得区块链数据更容易访问。在这个教程中,我们将学习如何使用TheGraph来查询Aave协议数据,使用的技术
Stella981 Stella981
3年前
ERC20接入Uniswap教程【DeFi】
Uniswap是基于以太坊的去中心化数字加密货币交易协议,它为代币持有者提供了简洁的接口来将一种ERC20代币兑换为另一种,并且交易所需的gas成本很低。区块链开发者可以自己的ERC20代币接入Uniswap以增强其流动性。在这个教程中,我们将学习如何将一个ERC20代币接入Uniswap协议,并且在教程的最后提供完整的实现源代码。用自己熟悉的语言学
Stella981 Stella981
3年前
Solidity合约安全漏洞分析教程【Mythril】
在这个教程中,我们将学习Mythril的安装与使用方法,了解Mythril的工作原理,掌握如何利用EtherThief和Suicide模块分析合约的安全漏洞,以及如何配置Mythril安全分析的交易数量参数和执行超时参数。用自己熟悉的语言学习以太坊DApp开发:Java(https://www.oschina.net/action/GoToLi
Wesley13 Wesley13
3年前
37个常见的智能合约安全问题【以太坊】
SWCRegistry是以太坊安全人员和开发人员的必备知识库。它是SmartContractSecurity提供的以太坊智能合约安全漏洞分类及测试用例集,其中包含了37种以太坊Solidity智能合约开发中常见的安全问题的描述及后果,例如重入、溢出等,同时也给出了CWE漏洞分类、解决方案和作为示例的合约程序代码。用自己熟悉的语言学习以太坊DA
Stella981 Stella981
3年前
DeFi研究资源大全,超过100个!
本文为DeFiPulse提供的去中心化金融DeFi协议及应用大全的中文翻译版,内容涵盖借贷产品、DeFi交易、去中心化支付、DeFi钱包、用户接口、DeFi基础设施、数据分析等11个分类,是区块链开发人员及金融从业者研究去中心化金融的必备资源。用自己熟悉的语言学习以太坊DApp开发:Java(https://www.oschina.
Stella981 Stella981
3年前
EthSnarks以太坊混币器【零知识证明】
Miximus是一个用于以太坊区块链的去中心化混币器和匿名转账应用,由EthSnarks作者开发,用于展示零知识证明在以太坊上的实用性。本文介绍Miximus以太坊混币应用的安装使用方法、工作原理和实现细节。用自己熟悉的语言学习以太坊DApp开发:Java(https://www.oschina.net/action/GoToLink?urlh
Stella981 Stella981
3年前
DeFi Compound开发REST API
在这个教程中,我们将学习如何为DeFi协议Compound的智能合约创建一个可以通过HTTP访问的API开发接口,并学习如何使用Infura作为以太坊网络和应用之间的桥梁。Compound是一个基于以太坊的数字资产借贷利率协议。Infura(https://www.oschina.net/action/GoToLink?urlhttp%3A%2F%
Stella981 Stella981
3年前
Solidity内联汇编简明指南
在用Solidity开发以太坊智能合约时,使用汇编可以直接与EVM交互,降低gas开销成本,更精细的控制智能合约的行为,因此值得Solidity开发者学习并加以利用。本文是Solidity汇编开发的简明教程,旨在帮助你快速熟悉如何在Solidity智能合约代码中嵌入汇编代码。以太坊教程链接:Dapp入门(https://www.oschina.n