# Ethereum Dapp 签名验签中的安全问题

Original: <https://lifelonglearn.ing/ethereum-dapp-security-tips-in-sign-verify>

Author: [奶爸](https://lifelonglearn.ing/users/3b04650c-8231-499b-8ef0-381fc4d7efb0)

Published: 2020-09-16T04:02:50Z

Updated: 2026-09-27T06:57:43Z

Version: 1

#### 2023 年 11 月 19 日 · 更新

因为我们最近在做 ERC4337 账户抽象相关的东西，根据 EntryPoint 校验签名的几个条件补充一下，除了 ​**地址**​、​**次数**​、**链** 这些维度之外还有 ​**时间维度**​，如果一个有个硬分叉的链，只有旧数据没有新数据，恶意攻击者会拿着新的签名去旧数据中使用。

针对时间维度的防攻击就是加上 ​**validUntil**​、**validAfter**

#### 2021 年 03 月 22 日 · 更新

在一些审计报告和 Uniswap 中学到其实可以设置一个 deadline（block.timestamp）来取代下文 nonce 字段。因为使用 nonce 字段你需要记录和知道当前 nonce 到哪里了。

## 关键点

钱包的签名可以

* **任何地址** D 用户拿着 C 用户的签名去验证
* **任意次数** 重复调用
* ​**任何网络**​（主网、测试网）跨链攻击
* **调用合约** 给 A 合约的签名被拿去给 B 使用
* **任何时间** 一个签名一旦签出，这个签名在任何时间都是合法签名

来使用。

## 举例

拿一个 Solidity 官方文档中的例子来说：

```javascript
// recipient is the address that should be paid.
// amount, in wei, specifies how much ether should be sent.
// nonce can be any unique number to prevent replay attacks
// contractAddress is used to prevent cross-contract replay attacks
function signPayment(recipient, amount, nonce, contractAddress, callback) {
    var hash = "0x" + abi.soliditySHA3(
        ["address", "uint256", "uint256", "address"],
        [recipient, amount, nonce, contractAddress]
    ).toString("hex");

    web3.eth.personal.sign(hash, web3.eth.defaultAccount, callback);
}
```

其实这个例子不算特别好，这是一个支付相关的签名示例，包含了下面的字段：

* recipient 收款地址，验签固定收款的钱包
  延伸解释：B 对「B 支付给 A 3 ETH」这个数据进行签名，然后 C 偷盗了签名数据拿去调用，这样 ETH 也不会划转给 C 地址。
* amount 付款金额，验签固定付款金额
* nonce 唯一ID，防止签名被多次使用
  延伸解释：B 对「B 支付给 A 3 ETH」这个数据进行签名，然后 A 去使用签名多次领取，这样 B 就付给了 A 多次 ETH，如果使用了 唯一ID，即可限制领取一次
* contractAddress 当前合约的地址（加一个合约地址可以有效防止签名被用在其他合约中）
  延伸解释：B 对「B 支付给 A 3 ETH」这个数据进行签名，原本是在二手手机中介那里记的帐，A 已经领取了 ETH，结果 A 转头又去二手车中介那里说 B 应该付款 3 ETH 又收了 B 的钱。

所以通过这几个参数，我们发现

* recipient 对应 关键点里面的 「任意地址」
* nonce 对应 「任意次数」
* contractAddress 对应 「任意合约」

唯独漏了一点「任意网络」，这样的话你在测试网上面给 「向 A 转账 3 ETH」，A 可以拿到 mainnet 上面相同 contractAddress 的合约里面调用来领取 3 ETH。

所以这个例子应该改为：

```javascript
// recipient is the address that should be paid.
// amount, in wei, specifies how much ether should be sent.
// nonce can be any unique number to prevent replay attacks
// chainId is used to prevent cross-contract replay attacks
// contractAddress is also used to prevent cross-contract replay attacks
function signPayment(recipient, amount, nonce, chainId, contractAddress, callback) {
    var hash = "0x" + abi.soliditySHA3(
        ["address", "uint256", "uint256", "uint256", "address"],
        [recipient, amount, nonce, chainId, contractAddress]
    ).toString("hex");

    web3.eth.personal.sign(hash, web3.eth.defaultAccount, callback);
}
```
