元交易和签名重放
有些时候,你希望你的 DApp 用户有一个无气的体验,或者也许在没有真正把东西放在链上的情况下进行交易。这些类型的交易被称为元交易,在这个层面上,我们将深入探讨如何设计元交易,以及如果不仔细设计,它们如何被利用。
对于那些使用过 OpenSea 的人来说,有没有注意到 OpenSea 如何让你的 NFT 免费上市?无论你想以什么价格出售你的 NFT,不知何故,它从来没有在最初的 NFT 批准交易之外收取气体?答案是,Meta 交易。
元交易也常用于无气体的交易体验,例如要求用户签署一个信息来认领一个 NFT,而不是支付气体来发送一个交易来认领一个 NFT。
还有其他用例,例如让用户用任何代币,甚至是法币来支付燃气费,而不需要转换为加密货币。Multisig 钱包也只要求最后一个签名者为正在从 multisig 钱包进行的交易支付汽油费,其他用户只是签署一些信息。
这难道不是非常酷吗?🎉
它是如何工作的?
元交易只是一个花哨的词,即由第三方(中继者)代表用户支付交易的气体。用户只需要签署包含他们想要执行的交易信息的消息(而不是发送交易),并将其交给中继者。然后,中继者负责使用这些数据创建有效的交易,并自己支付燃气费用。
中继者可以是,例如,你的 DApp 中的代码,让你的用户体验无气的交易,也可以是你用法币支付的第三方公司,在以太坊上执行交易,等等。
在这个层面上,我们将做两件事--学习元交易以及如何构建一个支持元交易的简单智能合约,同时学习我们构建的第一个合约中的一个安全漏洞以及如何修复它。一石二鸟 😎。
数字签名
在前面的 IPFS 层面,我们已经解释了什么是散列。在继续之前,我们需要了解密码学的第二个基本概念--数字签名。
数字签名是一种验证信息真实性的数学方法。给定一些数据,可以用私钥对该数据进行签名,并产生一个签名信息。
然后,其他相信自己拥有原始输入数据的人可以将其与已签署的信息进行比较,以检索签署该信息的公共密钥。由于发送者的公钥必须事先知道,他们可以保证数据没有被篡改,其完整性得到维护。
所以在上面的例子中,Alice 用她的私钥签署了信息 Hello Bob!并将其发送给了 Bob。Bob 已经知道内容是 Hello Bob!,现在他想验证该信息确实是由 Alice 发送的,所以他试图从签名的信息中检索出公钥。如果公钥是 Alice 的公钥,那么该信息就没有被篡改,是安全传输的。
数字签名的使用案例
数字签名通常用于希望安全通信和确保信息内容在通信过程中没有被篡改。你可以发送原始的明文信息,以及来自已知发送者的签名信息,接收者可以通过比较明文信息和签名信息得出签名者的公钥来验证信息的完整性。如果签名者的公钥与预期的发送者的公钥 相吻合,则该信息是安全传输的!
以太坊上的数字签名
在以太坊的情况下,用户钱包可以签署消息,然后原始输入数据可以与签署的消息进行验证,以检索签署该消息的用户的地址。签名验证既可以在链外进行,也可以在 Solidity 内部进行。在此情况下,用户的地址就是公钥。
理解这些概念
假设我们想建立一个 DApp,用户可以将代币转移到其他地址,而不需要支付汽油,只有一次批准。由于用户自己不会发送交易,我们将为他们支付汽油 🤑。
这种设计非常类似于 OpenSea 的工作方式,一旦你支付汽油以提供对 ERC-721 或 ERC-1155 集合的批准,OpenSea 上的列表和销售对卖家来说是免费的。OpenSea 只要求卖家在挂牌时签署交易,而当买家出现时,卖家的签名会与买家的交易一起提交,从而将 NFT 转移给买家,将 ETH 转移给卖家--而买家为这一切支付了所有的气体。
在我们的案例中,我们将只是有一个中继器,让你在初步批准后将 ERC-20 代币转移到其他地址 🎉。
让我们想一想,为了实现这一目标,需要什么所有的信息。智能合约需要知道关于代币转移的所有信息,所以我们至少需要将 4 项内容纳入签名信息中。
- 发件人地址
- 收件人地址
- 要转移的代币数量
- ERC-20 智能合约的 tokenContract 地址
流程看起来会是这样的。
- 用户首先批准 TokenSender 合约进行无限的代币转移(使用 ERC20 批准功能)
- 用户签署一个包含上述信息的消息
- 中继器调用智能合约,传递已签署的信息,并支付气体费用
- 智能合约验证签名并解码消息数据,并将代币从发送方转移到接收方
值得庆幸的是,ethers.js
自带了一个名为 signMessage
的函数,让我们可以轻松地签署消息。然而,为了避免产生任意长度的消息,我们将首先对所需的数据进行散列,然后签名。在智能合约中,我们也将首先对给定的参数进行散列,然后尝试根据散列来验证签名。
我们将把这个编码作为一个 Hardhat 测试,所以首先,我们需要编写智能合约。
构建
让我们建立一个例子,在那里你可以体验到元交易的作用,稍后我们将扩展该测试以显示漏洞。
创建项目
要建立一个 Hardhat 项目,打开终端并执行这些命令
npm init --yes npm install --save-dev hardhat 如果你是在 Windows 上,请做这个额外的步骤,同时安装这些库:)
npm install --save-dev @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers 在你安装 Hardhat 的同一目录下运行。
npx hardhat 选择创建一个基本样本项目 对已经指定的 Hardhat 项目根按回车键 在是否要添加.gitignore 的问题上按回车键 在 "是否要用 npm 安装这个样本项目的依赖项(@nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers)? 现在你已经有了一个准备好的 hardhat 项目!
编写合约
让我们也在同一个终端上安装 OpenZeppelin 合约。
npm install @openzeppelin/contracts
我们将创建两个智能合约。第一个是一个超级简单的 ERC-20 实现,第二个是我们的 TokenSender 合约。为了简单起见,我们将在同一个.sol 文件中创建它们。
让我们在合约/目录下创建一个名为 MetaTokenSender.sol 的 Solidity 文件,代码如下
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
contract RandomToken is ERC20 {
constructor() ERC20("", "") {}
function freeMint(uint amount) public {
_mint(msg.sender, amount);
}
}
contract TokenSender {
using ECDSA for bytes32;
function transfer(address sender, uint amount, address recipient, address tokenContract, bytes memory signature) public {
bytes32 messageHash = getHash(sender, amount, recipient, tokenContract);
bytes32 signedMessageHash = messageHash.toEthSignedMessageHash();
address signer = signedMessageHash.recover(signature);
require(signer == sender, "Signature does not come from sender");
bool sent = ERC20(tokenContract).transferFrom(sender, recipient, amount);
require(sent, "Transfer failed");
}
function getHash(address sender, uint amount, address recipient, address tokenContract) public pure returns (bytes32) {
return keccak256(abi.encodePacked(sender, amount, recipient, tokenContract));
}
}
让我们来看看发生了什么。首先,让我们看一下进口。
导入的内容
ERC20.sol 导入是为了继承 OpenZeppelin 的基本 ERC-20 合约实现,我们在以前的轨道中已经了解了很多。
ECDSA
代表椭圆曲线数字签名算法
--它是 Ethereum 使用的签名算法,ECDSA.sol
的 OpenZeppelin 库包含一些用于 Solidity 中数字签名的辅助函数。