简体   繁体   中英

Requesting code review for simple EIP712 (Ethereum) validation scheme

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

Use this contract template to recover your address first, then use it into your contract, when you successfull retrieve the address. I will explain the contract too:

pragma solidity ^0.8.0;

contract SignTest {

address owner = msg.sender;

mapping(uint256 => bool) usedNonces;

function test(uint256 amount, uint256 nonce, bytes memory sig, uint tV, bytes32 tR, bytes32 tS, bytes32 tMsg) public view returns(address) {

    bytes32 message = prefixed(keccak256(abi.encodePacked(amount, nonce))); 
    bytes32 messageWithoutPrefix = keccak256(abi.encodePacked(amount, nonce)); 

   
    address signer = recoverSigner(messageWithoutPrefix, sig, tV, tR,tS);

    return signer;
}

// Signature methods

function splitSignature(bytes memory sig)
    public
    view
    returns (uint8, bytes32, bytes32)
{
    require(sig.length == 65, "B");

    bytes32 r;
    bytes32 s;
    uint8 v;

    assembly {
        // first 32 bytes, after the length prefix
        r := mload(add(sig, 32))
        // second 32 bytes
        s := mload(add(sig, 64))
        // final byte (first byte of the next 32 bytes)
        v := byte(0, mload(add(sig, 96)))
    }

    return (v, r, s);
}

function recoverSigner(bytes32 message, bytes memory sig, uint tV, bytes32 tR, bytes32 tS)
    public
    view
    returns (address)
{
    uint8 v;
    bytes32 r;
    bytes32 s;

    (v, r, s) = splitSignature(sig);

    require(v==tV, "V is not correct");
    require(r==tR, "R is not correct");
    require(s==tS, "S is not correct");

    return ecrecover(message, v, r, s);
}

// Builds a prefixed hash to mimic the behavior of eth_sign.
function prefixed(bytes32 inputHash) public pure returns (bytes32) {
    return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", inputHash));
}

}

I use web3 for ecrecover, so, let me provide you a simple example:

    let fnSignature = web3.utils.keccak256("setApprovalForAll(address,bool").substr(0,10)

// encode the function parameters and add them to the call data
let fnParams = web3.eth.abi.encodeParameters(
  ["address","bool"],
  [toAddr,permit]
)

calldata = fnSignature + fnParams.substr(2)

console.log(calldata)

First, substr first 4 bytes of function signature, then encode them into fnParams + fnSignature

Second step is to sign your data:

const data = calldata //Retrieved from above code
const NFTAddress = 'Contract address where you sign'
const newSigner = web3.eth.accounts.privateKeyToAccount("Your Priv Key");
const myAccount = web3.eth.accounts.wallet.add(newSigner);
const signer = myAccount.address;
console.log(signer) // display the target address in console ( for better verification )

Use calldata from first function and add it to data Add the contract address where you want to sign the hash Then, follow these steps:

    let rawData = web3.eth.abi.encodeParameters(
    ['address','bytes'],
    [NFTAddress,data]
  );
  // hash the data.
  let hash = web3.utils.soliditySha3(rawData);
  console.log(hash)
  // sign the hash.
  let signature = web3.eth.sign(hash, signer);
 console.log(signature)

Using "hash", go to solidity contract (posted by me) and add retrieved bytes(hash) into function prefixed(bytes32 hash) Using "signature", go to solidity contract and retrieve v,r,s. Use function splitSignature(bytes32 signature) Using bytes generated by function prefixed(bytes32 hash), use function recoverSigner(message ( bytes from prefixed(bytes32hash), signature ( from web3 javascript), v ( uint8 from splitSignature), r (bytes32 from splitSignature), s ( bytes32 from splitSignature)

You can also play with the contract and script I provided, but, i preffer to post a step by step guide, easy to understand for everyone.

Happy developing: )

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM