0%

StudyRecord-使用EIP-712签名进行委托和投票

使用ECDSA签名并验证:

什么是ECDSA:

ECDSA可理解为以太坊、比特币对消息、交易进行签名与验证的算法与流程。

流程:

  • 签名即正向算法(消息 + 私钥 + 随机数)= 签名,其中消息是公开的,私钥是隐私的,经过ECDSA正向算法可得到签名,即r、s、v

  • 验证即反向算法(消息 + 签名)= 公钥,其中消息是公开的,签名是公开的,经过ECDSA反向算法可得到公钥,然后对比已公开的公钥。

签名交易:

20221116112417

签名方法分类:

可以把签名方法划分为三种:

  • 通用消息签名方法;

  • EIP-191标准签名方法;

  • EIP-712标准签名方法;

通用签名方法就是添加了"\x19Ethereum Signed Message:\n"这个字符串的签名,如metamask的personal_sign方法和ECDSA库的toEthSignedMessageHash方法,添加这个字符串只是单纯的为了表明这是以太坊的签名。

EIP-191提出了在签名中加入合约自身的address参数,以防止重放攻击的手法。

主要学习一下EIP-712

EIP-712:

为什么要使用EIP712:

该EIP主要针对两个问题:

  1. 提高链下消息签名在链上使用的可用性,节省gas;

  2. 让用户知道他们在给什么数据进行签名。

在传统的dapp签名中,用户看到的往往是一串十六进制的数据,如下图:
20221116112629
而EIP712强调了一种对数据及其结构进行编码的方案,该方案允许在签名时将其显示给用户进行验证,让用户清楚的知道他们将要签署什么样的数据,如下图所示:

20221116112649

EIP-712 结构解析:

对比我们上述"通用消息签名方法"中只是对要签名的参数进行序列化、keccak256、添加"\x19Ethereum Signed Message:\n32"后再次序列化与keccak256、签名相比,EIP-712是有着结构化上的要求的。

20221116114338

EIP712最终的可签名的hash生成公式:

1
encode(domainSeparator : bytes32, message : Struct) = "x19x01" ‖ domainSeparator ‖ hashStruct(message)

这里的encode处理就是将"x19x01"domainSeparatorhashStruct(message)拼接在一起。

看看domainSeparatorhashStruct(meaasge)具体实现。

签名域domainSeparator:

  • 作用主要是保证不同的合约和链上的签名是不同的、隔离的。

两个 DApp 可能会产生相同的结构,这样Transfer(address from,address to,uint256 amount)就不应该兼容。通过引入域分隔符,dApp 开发人员可以保证不会出现签名冲突。

  • domainSeparator由两部分组成,第一部分为对结构体的keccak256(encodeType),第二部分为结构体的具体实现(encodeData);

  • domainSeparator结构体如下所示,一般来说salt(随机数)会省略;

1
2
3
4
5
6
7
struct EIP712Domain{
string name, //用户可读的域名,如DAPP的名字
string version, // 目前签名的域的版本号
uint256 chainId, // EIP-155中定义的chain ID, 如以太坊主网为1
address verifyingContract, // 用于验证签名的合约地址
bytes32 salt // 随机数
}
  • encodeTypeencodeData都要按照如上的结构体顺序来实现,其中字段可以省略,但不可以颠倒顺序

  • 针对string 或者 bytes 等动态类型,即长度不定的类型,其取值为 keccak256(string) 、 keccak256(bytes) 即内容的hash值;

  • 我们可以看到这里用的是abi.encode而非abi.encodePacked,这是因为domainSeparator结构体要求每个字段占据256位,以便于前端分割。

1
2
3
4
5
6
7
8
9
10
11
DOMAIN_SEPARATOR = keccak256(
abi.encode(
// encodeType
keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
// encodeData
keccak256(bytes(name)),
keccak256(bytes('1')),
chainId,
address(this)
)
);

签名对象hashStruct(message):

一般来讲,hashStruct(message)domainSeparator格式相同,也是由两部分组成,第一部分为对自定义结构体的keccak256(encodeType),第二部分为自定义结构体的具体实现(encodeData)

由注释可知,PERMIT_TYPEHASH就是Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)的hash,注意自定义对象名要首字母大写;
encodeData与encodeType顺序要相同;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2ERC20.sol

// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;

function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
// 签名摘要
bytes32 digest = keccak256(
abi.encodePacked(
'\x19\x01', // 固定前缀
DOMAIN_SEPARATOR, // 域名分隔符
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)) //签名对象
)
);
address recoveredAddress = ecrecover(digest, v, r, s);
require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
_approve(owner, spender, value);
}

小结:

  • 签名域domainSeparator: 该签名可用于哪个链的哪个合约,限定的范围

  • 签名对象hashStruct(message): 该msg用于哪个具体的函数

其中r,s,v就相当于是签名

1
2
3
4
5
const sig = signature.value;

const r = '0x' + sig.substring(2).substring(0, 64);
const s = '0x' + sig.substring(2).substring(64, 128);
const v = '0x' + sig.substring(2).substring(128, 130);

Compound委托实例:

通过实例来加深理解。教程: https://medium.com/compound-finance/delegation-and-voting-with-eip-712-signatures-a636c9dfec5e

https://github.com/compound-developers/compound-governance-examples/blob/master/signature-examples/batch-publish-examples/README.md

通过签名功能的用户的一个主要好处是他们可以免费创建签名委托或投票交易,并让受信任的第三方花费 ETH 支付gas费并将其写入区块链。

步骤:

测试代码:https://github.com/jerrychan807/my-awesome-solidity/blob/main/sign/README.md

部署Compound代币合约:

1
2
3
4
# 启动本地节点
yarn hardhat node
# 部署在本地
yarn hardhat deploy --network localhost

部署完Compound代币合约后,编写转账用例,AB用户均持有comp代币

启动http服务器:

1
python -m http.server

用户A在链下创建签名
20221115174725
20221115184807

接着把签名数据给用户B,由用户B去支付gas写入链上。

1
2
3
4
5
6
7
# 测试结果
hardhat test test/Token.test.ts --network localhost
Token contract
contract tests1
normalAddr1DelegatesBalance : 0.0
normalAddr2DelegatesBalance : 642829559307850963015472508762.062935916233390536
adminDelegatesBalance : 0.0

实现了用户A在链上免费生成签名,再由别的用户付gas fee写到链上。

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// https://etherscan.io/address/0xc00e94Cb662C3520282E6f5717214004A7f26888#code

/// @notice The EIP-712 typehash for the delegation struct used by the contract
bytes32 public constant DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)");

/**
* @notice Delegates votes from signatory to `delegatee`
* @param delegatee The address to delegate votes to 委托目的地址
* @param nonce The contract state required to match the signature
* @param expiry The time at which to expire the signature 过期时间
* @param v The recovery byte of the signature
* @param r Half of the ECDSA signature pair
* @param s Half of the ECDSA signature pair
*/
function delegateBySig(address delegatee, uint nonce, uint expiry, uint8 v, bytes32 r, bytes32 s) public {
bytes32 domainSeparator = keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name)), getChainId(), address(this)));
bytes32 structHash = keccak256(abi.encode(DELEGATION_TYPEHASH, delegatee, nonce, expiry));
// 签名
bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash));
// 签名反向算法得出 委托人地址
address signatory = ecrecover(digest, v, r, s);
require(signatory != address(0), "Comp::delegateBySig: invalid signature");
require(nonce == nonces[signatory]++, "Comp::delegateBySig: invalid nonce");
require(now <= expiry, "Comp::delegateBySig: signature expired");
// 委托人 委托给 目的地址
return _delegate(signatory, delegatee);
}

Refs: