欢迎订阅专栏:3分钟Solidity--智能合约--Web3区块链技术必学
如需获取本内容的最新版本,请参见 Cyfrin.io 上的签名重放(代码示例)
在链下签署消息,并通过合约在执行函数前要求该签名是一种实用的技术。
例如,这项技术可用于:
- 减少链上交易数量
- 实现无燃料费交易(称为“元交易”)
漏洞
同一个签名可被多次用于执行某个函数。如果签名者的本意是仅批准一次交易,这可能会造成危害。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "./ECDSA.sol";
contract MultiSigWallet {
using ECDSA for bytes32;
address[2] public owners;
constructor(address[2] memory _owners) payable {
owners = _owners;
}
function deposit() external payable {}
function transfer(address _to, uint256 _amount, bytes[2] memory _sigs)
external
{
bytes32 txHash = getTxHash(_to, _amount);
require(_checkSigs(_sigs, txHash), "invalid sig");
(bool sent,) = _to.call{value: _amount}("");
require(sent, "Failed to send Ether");
}
function getTxHash(address _to, uint256 _amount)
public
view
returns (bytes32)
{
return keccak256(abi.encodePacked(_to, _amount));
}
function _checkSigs(bytes[2] memory _sigs, bytes32 _txHash)
private
view
returns (bool)
{
bytes32 ethSignedHash = _txHash.toEthSignedMessageHash();
for (uint256 i = 0; i < _sigs.length; i++) {
address signer = ethSignedHash.recover(_sigs[i]);
bool valid = signer == owners[i];
if (!valid) {
return false;
}
}
return true;
}
}
预防性技术
使用 nonce和合约地址对消息进行签名。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "./ECDSA.sol";
contract MultiSigWallet {
using ECDSA for bytes32;
address[2] public owners;
mapping(bytes32 => bool) public executed;
constructor(address[2] memory _owners) payable {
owners = _owners;
}
function deposit() external payable {}
function transfer(
address _to,
uint256 _amount,
uint256 _nonce,
bytes[2] memory _sigs
) external {
bytes32 txHash = getTxHash(_to, _amount, _nonce);
require(!executed[txHash], "tx executed");
require(_checkSigs(_sigs, txHash), "invalid sig");
executed[txHash] = true;
(bool sent,) = _to.call{value: _amount}("");
require(sent, "Failed to send Ether");
}
function getTxHash(address _to, uint256 _amount, uint256 _nonce)
public
view
returns (bytes32)
{
return keccak256(abi.encodePacked(address(this), _to, _amount, _nonce));
}
function _checkSigs(bytes[2] memory _sigs, bytes32 _txHash)
private
view
returns (bool)
{
bytes32 ethSignedHash = _txHash.toEthSignedMessageHash();
for (uint256 i = 0; i < _sigs.length; i++) {
address signer = ethSignedHash.recover(_sigs[i]);
bool valid = signer == owners[i];
if (!valid) {
return false;
}
}
return true;
}
}
/*
// owners
0xe19aea93F6C1dBef6A3776848bE099A7c3253ac8
0xfa854FE5339843b3e9Bfd8554B38BD042A42e340
// to
0xe10422cc61030C8B3dBCD36c7e7e8EC3B527E0Ac
// amount
100
// nonce
0
// tx hash
0x12a095462ebfca27dc4d99feef885bfe58344fb6bb42c3c52a7c0d6836d11448
// signatures
0x120f8ed8f2fa55498f2ef0a22f26e39b9b51ed29cc93fe0ef3ed1756f58fad0c6eb5a1d6f3671f8d5163639fdc40bb8720de6d8f2523077ad6d1138a60923b801c
0xa240a487de1eb5bb971e920cb0677a47ddc6421e38f7b048f8aa88266b2c884a10455a52dc76a203a1a9a953418469f9eec2c59e87201bbc8db0e4d9796935cb1b
*/
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
// OpenZeppelin Contracts (last updated v4.5.0) (utils/cryptography/ECDSA.sol)
library ECDSA {
enum RecoverError {
NoError,
InvalidSignature,
InvalidSignatureLength,
InvalidSignatureS,
InvalidSignatureV
}
function _throwError(RecoverError error) private pure {
if (error == RecoverError.NoError) {
return; // no error: do nothing
} else if (error == RecoverError.InvalidSignature) {
revert("ECDSA: invalid signature");
} else if (error == RecoverError.InvalidSignatureLength) {
revert("ECDSA: invalid signature length");
} else if (error == RecoverError.InvalidSignatureS) {
revert("ECDSA: invalid signature 's' value");
} else if (error == RecoverError.InvalidSignatureV) {
revert("ECDSA: invalid signature 'v' value");
}
}
function tryRecover(bytes32 hash, bytes memory signature)
internal
pure
returns (address, RecoverError)
{
// Check the signature length
// - case 65: r,s,v signature (standard)
// - case 64: r,vs signature (cf https://eips.ethereum.org/EIPS/eip-2098) _Available since v4.1._
if (signature.length == 65) {
bytes32 r;
bytes32 s;
uint8 v;
// ecrecover takes the signature parameters, and the only way to get them
// currently is to use assembly.
assembly {
r := mload(add(signature, 0x20))
s := mload(add(signature, 0x40))
v := byte(0, mload(add(signature, 0x60)))
}
return tryRecover(hash, v, r, s);
} else if (signature.length == 64) {
bytes32 r;
bytes32 vs;
// ecrecover takes the signature parameters, and the only way to get them
// currently is to use assembly.
assembly {
r := mload(add(signature, 0x20))
vs := mload(add(signature, 0x40))
}
return tryRecover(hash, r, vs);
} else {
return (address(0), RecoverError.InvalidSignatureLength);
}
}
function recover(bytes32 hash, bytes memory signature)
internal
pure
returns (address)
{
(address recovered, RecoverError error) = tryRecover(hash, signature);
_throwError(error);
return recovered;
}
function tryRecover(bytes32 hash, bytes32 r, bytes32 vs)
internal
pure
returns (address, RecoverError)
{
bytes32 s = vs
& bytes32(
0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
);
uint8 v = uint8((uint256(vs) >> 255) + 27);
return tryRecover(hash, v, r, s);
}
function recover(bytes32 hash, bytes32 r, bytes32 vs)
internal
pure
returns (address)
{
(address recovered, RecoverError error) = tryRecover(hash, r, vs);
_throwError(error);
return recovered;
}
function tryRecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s)
internal
pure
returns (address, RecoverError)
{
// EIP-2仍允许ecrecover()存在签名可塑性。
// 需消除这种可能性,确保签名唯一性。以太坊黄皮书
// (<https://ethereum.github.io/yellowpaper/paper.pdf>)附录F定义了有效范围:
// s值需满足(301)0 < s < secp256k1n ÷ 2 + 1,
// v值需满足(302)v ∈ {27, 28}。
// 当前多数库生成的签名具有唯一性,其s值位于序数下半区。
// 若您的库生成可塑性签名(如s值位于上半区),应通过公式
// 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1
// 计算新s值,并将v值在27/28间切换。
// 若库生成v值为0/1而非27/28的签名,需对v值加27以兼容此类可塑性签名。
if (
uint256(s)
> 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0
) {
return (address(0), RecoverError.InvalidSignatureS);
}
if (v != 27 && v != 28) {
return (address(0), RecoverError.InvalidSignatureV);
}
// If the signature is valid (and not malleable), return the signer address
address signer = ecrecover(hash, v, r, s);
if (signer == address(0)) {
return (address(0), RecoverError.InvalidSignature);
}
return (signer, RecoverError.NoError);
}
function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s)
internal
pure
returns (address)
{
(address recovered, RecoverError error) = tryRecover(hash, v, r, s);
_throwError(error);
return recovered;
}
function toEthSignedMessageHash(bytes32 hash)
internal
pure
returns (bytes32)
{
// 32是哈希的字节长度,
// 由上述类型签名强制规定
return keccak256(
abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)
);
}
}