[Bug]: Failed to Verify Signs by Multiple Structured Data Nested through EIP712
VictorZhang2014 opened this issue · comments
Pull Request
No response
What happened?
Hi Ethereum dev team, I came across a bug inside solidity abi.encode/abi.encodePacked. The question is that I nested over 3 struct data via EIP712 structure, whatever I encoded the structs data, it failed every time.
I just used the Ether Mail
solidity code and its related javascript code. Like I took a screenshot of the structured data, Mail
and Person
are just two nested struct data, and I added one more struct called HomeAddress
to the Person
struct, it cannot pass the test.
This is my Solidity code snippet:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
contract EIP712EtherMail {
struct EIP712Domain {
string name;
string version;
uint256 chainId;
address verifyingContract;
}
struct HomeAddress {
string home;
string phone;
uint256 age;
}
struct Person {
string name;
address wallet;
HomeAddress addr;
}
struct Mail {
Person from;
Person to;
string contents;
}
bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);
bytes32 constant HOME_ADDRESS_TYPEHASH = keccak256(
"HomeAddress(string home,string phone,uint256 age)"
);
bytes32 constant PERSON_TYPEHASH = keccak256(
"Person(string name,address wallet,HomeAddress addr)HomeAddress(string home,string phone,uint256 age)"
);
bytes32 constant MAIL_TYPEHASH = keccak256(
"Mail(Person from,Person to,string contents)Person(string name,address wallet,HomeAddress addr)HomeAddress(string home,string phone,uint256 age)"
);
bytes32 DOMAIN_SEPARATOR;
constructor () {
DOMAIN_SEPARATOR = hash(EIP712Domain({
name: "Ether Mail",
version: '1',
chainId: block.chainid,
// verifyingContract: this
verifyingContract: 0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC
}));
}
function hash(EIP712Domain memory eip712Domain) public pure returns (bytes32) {
return keccak256(abi.encode(
EIP712DOMAIN_TYPEHASH,
keccak256(bytes(eip712Domain.name)),
keccak256(bytes(eip712Domain.version)),
eip712Domain.chainId,
eip712Domain.verifyingContract
));
}
function hash(HomeAddress memory addr) public pure returns (bytes32) {
return keccak256(abi.encode(
HOME_ADDRESS_TYPEHASH,
keccak256(bytes(addr.home)),
keccak256(bytes(addr.phone)),
addr.age
));
}
function hash(Person memory person) public pure returns (bytes32) {
return keccak256(abi.encode(
PERSON_TYPEHASH,
keccak256(bytes(person.name)),
person.wallet,
hash(person.addr)
));
}
function hash(Mail memory mail) public pure returns (bytes32) {
return keccak256(abi.encode(
MAIL_TYPEHASH,
hash(mail.from),
hash(mail.to),
keccak256(bytes(mail.contents))
));
}
function verify(Mail memory mail, uint8 v, bytes32 r, bytes32 s) public view returns (address) {
// Note: we need to use `encodePacked` here instead of `encode`.
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
hash(mail)
));
return ecrecover(digest, v, r, s);
}
}
Attached to my javascript code snippet:
import Web3 from "web3";
const EthUtil = require('ethereumjs-util');
import EncodeDataUtil from "./EncodeDataUtil"
const EIP712_MAIL_EXAMPLE_ABI = require("../assets/abi/eip712mail.json")
let chainId = window.ethereum.chainId
if (chainId == null || chainId == undefined) {
chainId = 31337
}
this.typedData = {
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
HomeAddress: [
{ name: 'home', type: 'string' },
{ name: 'phone', type: 'string' },
{ name: 'age', type: 'uint256' },
],
Person: [
{ name: 'name', type: 'string' },
{ name: 'wallet', type: 'address' },
{ name: 'addr', type: 'HomeAddress' }
],
Mail: [
{ name: 'from', type: 'Person' },
{ name: 'to', type: 'Person' },
{ name: 'contents', type: 'string' }
]
},
primaryType: 'Mail',
domain: {
name: 'Ether Mail',
version: '1',
chainId: chainId,
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
},
message: {
from: {
name: 'Cow',
wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
addr: {
home: "The USA",
phone: "123456",
age: 20
}
},
to: {
name: 'Bob',
wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
addr: {
home: "United Kindom",
phone: "098765",
age: 22
}
},
contents: 'Hello, Bob!',
},
}
// this.$privateKey = EthUtil.keccakFromString('cow', 256); // to generate a new private key
const _privateKey = "Your private key to paste it here"
this.privateKey = EthUtil.toBuffer(_privateKey)
this.from = EthUtil.bufferToHex(EthUtil.privateToAddress(this.privateKey));
console.log("from=", this.from)
const encodeUtil = new EncodeDataUtil(this.typedData.types)
const msgHash = encodeUtil.signHash(this.typedData.domain, this.typedData.primaryType, this.typedData.message)
const hexMsgHash = JSON.stringify(this.typedData)
const signature_ = window.ethereum.request({ method: 'eth_signTypedData_v4', params: [this.from, hexMsgHash]})
const sig = EthUtil.fromRpcSig(signature_)
const web3 = new Web3("http://localhost:8545")
const contractAddress = "0x1291Be112d480055DaFd8a610b7d1e203891C274"
const myContract = new web3.eth.Contract(EIP712_MAIL_EXAMPLE_ABI, contractAddress);
const mail = [
['Cow', '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', ["The USA","123456",20]],
['Bob', '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', ["United Kindom","098765",22]],
"Hello, Bob!"
]
const result = await myContract.methods.verify(mail, this.signature.v, this.signature.r, this.signature.s).call();
console.log("1. The signer address is ", result)
console.log(result.toLowerCase() == this.from.toLowerCase() ? "1. Verifying Success!" : "Failed to verify")
const sign_ = EthUtil.toRpcSig(this.signature.v, this.signature.r, this.signature.s)
const result1 = await myContract.methods.verifySignature(mail, sign_).call();
console.log("2. The signer address is ", result1)
console.log(result1.toLowerCase() == this.from.toLowerCase() ? "2. Verifying Success!" : "Failed to verify")
The source code of object EncodeDataUtil
is derived from the linke https://github.com/ethereum/EIPs/blob/master/assets/eip-712/Example.js
Relevant log output
To output the recover public key that is equal to the signer address
This is the wrong repo. Please open an issue at https://github.com/ethereum/solidity/issues
I run into similar issue and I've figure it's probably best for the EIP-712 author to provide a good implementation reference. #5433
EIP-712 was just finalized. It is too late for that.
@xinbenlv Yep, Aug. 8 2022 was the Last Call and Finalized. But I think the solidty code snippets is same as that examples, doesn't it? Or so what is the big change to the solidity code?