Hello,
I am working on a simple EIP712 whitelist member wallet registration/validation scheme. The nutshell goes (sign typed data -> pass to chain -> extract signing address -> compare to signing address stored on chain).
I have been banging my head against this for a while now. I am not able to get the onchain extracted address to match the signing public address offchain. My eyes are way too close to this problem and I need help looking for something I may have missed. By my best ability, I appear to be adhering to standard, but obviously I am doing something wrong.
I have been referring to the EIP712 standard, the 'Mail' EIP reference implementation here (sol) + here (js), and the msfeldstein reference implementation here (sol) + here (ts).
Constraint
- For reasons, I do not wish to use any framework/OpenZeppelin (and I also have tried, but likewise could not get to work.)
Notes
- The code presented below is basically the EIP reference implementation whittled down, and made as painfully explicit as possible to make the troubleshooting/review process as easy as possible. I likewise cut out all the other testing console.logs.
- My approach has been to generate the
v,r,s, and signing public address by running .js and printing to console. I then deploy the .sol to Remix, and manually enter generated values. - I am likewise posting the question on Ethereum Stack Exchange, etc.
- Alternative typed-data signing methods/strategies are verymuch welcome.
If you have the time and knowhow, I would appreciate your review of my implementation of the EIP712 standard below.
Clientside:
// using ethereumjs-util 7.1.3
const ethUtil = require('ethereumjs-util');
// using ethereumjs-abi 0.6.9
const abi = require('ethereumjs-abi');
// The purpose of this script is to be painfully explicit for the sake
// of showing work, to ask for help.
// generate keys
prikey = ethUtil.keccakFromString('cow', 256);
signingAddress = ethUtil.privateToAddress(prikey);
// 0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826
// data
const typedData = {
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
Validation: [
{ name: 'wallet', type: 'address' },
{ name: 'share', type: 'uint256' },
{ name: 'pool', type: 'uint8' }
],
},
primaryType: 'Validation',
domain: {
name: 'Validator',
version: '1',
chainId: 1,
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
},
message: {
wallet: '0xeeBA65D9C7E5832918d1F4277DE0a78b78efEC43',
share: 1000,
pool: 5,
},
};
// create domain struct hash
const encodedDomainType = 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)';
const domainTypeHash = ethUtil.keccakFromString(encodedDomainType, 256);
var encTypes = [];
var encValues = [];
// add typehash
encTypes.push('bytes32');
encValues.push(domainTypeHash);
// add name
encTypes.push('bytes32');
encValues.push(ethUtil.keccakFromString(typedData.domain.name, 256));
// add version
encTypes.push('bytes32');
encValues.push(ethUtil.keccakFromString(typedData.domain.version, 256));
// add chainId
encTypes.push('uint256');
encValues.push(typedData.domain.chainId);
// add chainId
encTypes.push('address');
encValues.push(typedData.domain.verifyingContract);
// computer final hash
domainStructHash = abi.rawEncode(encTypes, encValues);
// create validation struct hash
const encodedValidationType = 'Validation(address wallet,uint256 share,uint256 pool)';
const validationTypeHash = ethUtil.keccakFromString(encodedValidationType, 256);
encTypes = [];
encValues = [];
// add typehash
encTypes.push('bytes32');
encValues.push(validationTypeHash);
// add wallet address
encTypes.push('address');
encValues.push(typedData.message.wallet);
// add share
encTypes.push('uint256');
encValues.push(typedData.message.share);
// add pool
encTypes.push('uint256');
encValues.push(typedData.message.pool);
// computer final hash
validationStructHash = abi.rawEncode(encTypes, encValues);
// now finally create final signature hash
signatureHash = ethUtil.keccak256(
Buffer.concat([
Buffer.from('1901', 'hex'),
domainStructHash,
validationStructHash,
]),
);
// and finally, sign
signature = ethUtil.ecsign(signatureHash, prikey);
// convert r, s, and signingAddress into hex strings to pass to remix
console.log(signature.v);
var r = ''
function pad2(s) {return s.length < 2 ? "0" + s : s};
for(i = 0; i < signature.r.length; i++) {
r += pad2(signature.r[i].toString(16)); }
console.log('0x' + r); // r bytes
var s = ''
function pad2(s) {return s.length < 2 ? "0" + s : s};
for(i = 0; i < signature.s.length; i++) {
s += pad2(signature.s[i].toString(16)); }
console.log('0x' + s); // s bytes
var str = '';
function pad2(s) {return s.length < 2 ? "0" + s : s};
for(i = 0; i < signingAddress.length; i++) {
str += pad2(signingAddress[i].toString(16)); }
console.log('0x' + str); // signingAddress bytes
On chain:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract validateData {
address _validationKey = 0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826;
struct EIP712Domain {
string name;
string version;
uint256 chainId;
address verifyingContract;
}
struct Validation {
address wallet;
uint256 share;
uint256 pool;
}
bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);
bytes32 constant VALIDATION_TYPEHASH = keccak256(
"Validation(address wallet,uint256 share,uint256 pool)"
);
bytes32 DOMAIN_SEPARATOR;
constructor () {
DOMAIN_SEPARATOR = hash(EIP712Domain({
name: "Validator",
version: '1',
chainId: 1,
verifyingContract: 0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC
}));
}
function hash(EIP712Domain memory eip712Domain) internal pure returns (bytes32) {
return keccak256(abi.encode(
EIP712DOMAIN_TYPEHASH,
keccak256(bytes(eip712Domain.name)),
keccak256(bytes(eip712Domain.version)),
eip712Domain.chainId,
eip712Domain.verifyingContract
));
}
function hash(Validation calldata validation) internal pure returns (bytes32) {
return keccak256(abi.encode(
VALIDATION_TYPEHASH,
validation.wallet,
validation.share,
validation.pool
));
}
event compare(address sig, address key);
function verify(Validation calldata validation, uint8 v, bytes32 r, bytes32 s) public {
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
hash(validation)
));
emit compare(ecrecover(digest, v, r, s), _validationKey);
}
}
Thank you for your time and consideration!