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 successfully 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 prefer to post a step by step guide, easy to understand for everyone.
Happy developing! :)
Have a simple contract in solidity:
contract SellStuff{
address seller;
string name;
string description;
uint256 price;
function sellStuff(string memory _name, string memory _description, uint256 _price) public{
seller = msg.sender;
name = _name;
description = _description;
price = _price;
}
function getStuff() public view returns (
address _seller,
string memory _name,
string memory _description,
uint256 _price){
return(seller, name, description, price);
}
}
And running a javascript test as follows:
var SellStuff= artifacts.require("./SellStuff.sol");
// Testing
contract('SellStuff', function(accounts){
var sellStuffInstance;
var seller = accounts[1];
var stuffName = "stuff 1";
var stuffDescription = "Description for stuff 1";
var stuffPrice = 10;
it("should sell stuff", function(){
return SellStuff.deployed().then(function(instance){
sellStuffInstance= instance;
return sellStuffInstance.sellStuff(stuffName, stuffDescription, web3.utils.toWei(stuffPrice,'ether'), {from: seller});
}).then(function(){
//the state of the block should be updated from the last promise
return sellStuffInstance.getStuff();
}).then(function(data){
assert.equal(data[0], seller, "seller must be " + seller);
assert.equal(data[1], stuffName, "stuff name must be " + stuffName);
assert.equal(data[2], stuffDescription, "stuff description must be " + stuffDescription);
assert.equal(data[3].toNumber(), web3.utils.toWei(stuffPrice,"ether"), "stuff price must be " + web3.utils.toWei(stuffPrice,"ether"));
});
});
});
But I am getting the following error:
Error: Please pass numbers as string or BN objects to avoid precision errors.
This seems to look like it pertains to the return type from the web3.utils.toWei call, so I have tried to cast it to a string:web3.utils.toWei(stuffPrice.toString(),"ether"); but this gives the Error: Number can only safely store up to 53 bits.
Not sure if I need to simply change the var in the class from uint256 or if there is a better way to cast the toWei return variable?
The error is here:
web3.utils.toWei(stuffPrice,'ether')
stuffPrice should be string.
web3.utils.toWei(String(stuffPrice),'ether')
The toWei() method accepts String|BN as the first argument. You're passing it the stuffPrice as a Number.
A quick fix is to define the stuffPrice as String:
var stuffPrice = '10'; // corrected code, String
instead of
var stuffPrice = 10; // original code, Number
Another way is to pass it a BN object.
var stuffPrice = 10; // original code, Number
web3.utils.toWei(
web3.utils.toBN(stuffPrice), // converts Number to BN, which is accepted by `toWei()`
'ether'
);
You need to declare the state variable as a string which will work in this case.
In React:
state = { playerEthervalue: ''};
const accounts = await web3.eth.getAccounts();
// Send the ethers to transaction, initiate the transaction
await lottery.methods.getPlayersAddress().send({ from: accounts[0],
value: web3.utils.toWei(this.state.playerEthervalue, 'ether') });
In solidity(.sol):
function getPlayersAddress() public payable {
require(msg.value >= 0.00000001 ether);
players.push(msg.sender);
}
I am writing a unit test for my smart contract using truffle and when I run the test using "truffle test" the test failed with this error "Error: Returned error: VM Exception while processing transaction: invalid opcode".
smart contract code:
pragma solidity ^0.4.3;
contract owned {
address public owner;
/* Initialise contract creator as owner */
function owned() public {
owner = msg.sender;
}
/* Function to dictate that only the designated owner can call a function */
modifier onlyOwner {
require(owner == msg.sender);
_;
}
/* Transfer ownership of this contract to someone else */
function transferOwnership(address newOwner) public onlyOwner() {
owner = newOwner;
}
}
/*
* #title AsnScRegistry
* Open Vote Network
* A self-talling protocol that supports voter privacy.
*
* Author: Shahrul Sharudin
*/
contract AsnScRegistry is owned{
struct IPAddress {
uint128 ip;
uint8 mask;
}
struct MemberAddresses {
address contractAddress;
address walletAddress;
uint index;
}
mapping (uint => IPAddress[]) managedIps;
mapping (uint => MemberAddresses) ethAddresses;
uint[] private registeredAsn;
function getManagedIpByAsn(uint _asn) view public returns (uint128[] _ip, uint8[] _mask){
IPAddress[] memory addresses = managedIps[_asn];
uint128[] memory ips = new uint128[](addresses.length);
uint8[] memory mask = new uint8[](addresses.length);
for (uint i = 0; i < addresses.length; i++) {
ips[i] = addresses[i].ip;
mask[i] = addresses[i].mask;
}
return (ips, mask);
}
function getMemberAddressesByAsn(uint _asn) view public returns (address _contractAddress, address _walletAddress){
MemberAddresses memory memberAddresses = ethAddresses[_asn];
return (memberAddresses.contractAddress, memberAddresses.walletAddress);
}
function addMember(uint _asn, uint128[] _ip, uint8[] _mask, address _contractAddress, address _walletAddress) public {
MemberAddresses memory member = ethAddresses[_asn];
//throw error if member already exist
assert(member.contractAddress != 0);
for(uint i=0; i<_ip.length; i++){
managedIps[_asn].push(IPAddress({ip:_ip[i], mask:_mask[i]}));
}
uint idx = registeredAsn.push(_asn)-1;
ethAddresses[_asn] = MemberAddresses({contractAddress:_contractAddress, walletAddress:_walletAddress, index:idx});
}
function removeMember(uint _asn) public returns (uint[] _registeredAsn){
//uint memory index = ethAddresses[_asn].index;
if (ethAddresses[_asn].index >= registeredAsn.length) return;
for (uint i = ethAddresses[_asn].index; i<registeredAsn.length - 1; i++){
registeredAsn[i] = registeredAsn[i+1];
}
registeredAsn.length--;
delete managedIps[_asn];
delete ethAddresses[_asn];
return registeredAsn;
}
function getTotalMembers() view public returns (uint _totalMembers) {
return (registeredAsn.length);
}
function getRegisteredAsn() view public returns (uint[] _asn){
return (registeredAsn);
}
}
javascript unit test:
const AsnScRegistry = artifacts.require('./AsnScRegistry.sol')
const assert = require('assert')
const ip = [3524503734,3232235776];
const mask = [255,255];
const contractAddress = '0x1234567890123456789012345678901234567891';
const walletAddress = '0x1234567890123456789012345678901234567891';
const asn = 123;
let registryInstance;
contract("AsnScRegistry", accounts => {
it("Should add a new member", () =>
AsnScRegistry.deployed()
.then(instance => {
registryInstance = instance;
return instance.addMember(asn, ip, mask, contractAddress, walletAddress);
})
.then(() => registryInstance.getTotalMembers())
.then(total => assert.equal(total.toNumber(), 1, "xxxxx"))
);
});
the full source is available here: https://github.com/shaza4061/electioncommissioner
I expect the addMember() function to store the information in the smart contract and getTotalMembers() to return the number of record in registeredAsn[] array and the assertion in the unit test to pass.
The following error message:
"Error: Returned error: VM Exception while processing transaction: invalid opcode"
indicates a failed assert statement in solidity.
In your code, you have an assert which causes this error.
//throw error if member already exist
assert(member.contractAddress != 0);
Here is a manual for Solidity Smart contract debugging using Truffle.
I am new to Parse and Cloud Code, but I have managed to write a few AfterSave Cloud Code functions that work fine. However, I am having a lot of trouble with this one, and I cannot figure out why. Please help...
I have
Two PFObject classes: Message and MessageThread
Message contains chat messages that are associated with a MessageThread
MessageThread contains an array of members (which are all PFUsers)
Upon insert to Message, I want to look up all the members of the related MessageThread and Push notifications to them
class MessageThread: PFObject {
#NSManaged var members: [PFUser]
#NSManaged var lastMessageDate: NSDate?
#NSManaged var messageCount: NSNumber?
override class func query() -> PFQuery? {
let query = PFQuery(className: MessageThread.parseClassName())
query.includeKey("members")
return query
}
init(members: [PFUser], lastMessageDate: NSDate?, messageCount: NSNumber?) {
super.init()
self.members = members
self.lastMessageDate = lastMessageDate
self.messageCount = messageCount
}
override init() {
super.init()
}
}
extension MessageThread: PFSubclassing {
class func parseClassName() -> String {
return "MessageThread"
}
override class func initialize() {
var onceToken: dispatch_once_t = 0
dispatch_once(&onceToken) {
self.registerSubclass()
}
}
}
class Message: PFObject {
#NSManaged var messageThreadParent: MessageThread
#NSManaged var from: PFUser
#NSManaged var message: String
#NSManaged var image: PFFile?
override class func query() -> PFQuery? {
let query = PFQuery(className: Message.parseClassName())
query.includeKey("messageThreadParent")
return query
}
init( messageThreadParent: MessageThread, from: PFUser, message: String, image: PFFile?) {
super.init()
self.messageThreadParent = messageThreadParent
self.from = from
self.message = message
self.image = image
}
override init() {
super.init()
}
}
extension Message: PFSubclassing {
class func parseClassName() -> String {
return "Message"
}
override class func initialize() {
var onceToken: dispatch_once_t = 0
dispatch_once(&onceToken) {
self.registerSubclass()
}
}
}
Approach
From the request object (a Message), get its messageThreadParent
Lookup the members of the parent MessageThread, loop through them, etc.
The problem
When I try to retrieve the MessageThread object, I attempt to query on Id == threadParent.objectId. However, this query always returns all 8 of my current MessageThreads, rather than the single one I need.
Parse.Cloud.afterSave(Parse.Object.extend("Message"), function(request) {
Parse.Cloud.useMasterKey();
var theMsg = request.object;
var threadParent;
var currUsername = request.user.get("username");
var threadUsers;
var usernameArray;
threadParent = request.object.get("messageThreadParent");
// promise
queryM = new Parse.Query(Parse.Object.extend("MessageThread"));
queryM.include("members");
queryM.equalTo("id", threadParent.objectId);
queryM.find().then(function (threadParam) {
console.log(" threads: ");
console.log(threadParam.length); //this returns 8, which is the number of threads I have. I would expect just 1, matching threadParent.objectId...
console.log("thread is: ");
//... additional code follows, which seems to work...
After grappling with a separate problem all day I finally figured out that in Parse's Javascript SDK there is a difference between "id" and "objectId".
Changing this
queryM.equalTo("id", threadParent.objectId); // doesn't work
to
queryM.equalTo("objectId", threadParent.id); // works!
fixed my problem.
is declaring an array and initializing some arbitrary indexes allocate all the array elements in the memory even the undefined ones?
Example:
var users = [];
function addUser(userID, name, address) {
if (typeof (users[userID]) === 'undefined')
users[userID] = new User(userID, name, address)
}
function User (userID, name, address) {
this.userID = userID;
this.name = name;
this.address = address;
}
$(function () {
addUser(63, 'John', 'VA');
addUser(5, 'Kate', 'NY');
addUser(895, 'Yaz', 'DC');
});
So in the above example, will the browser allocate 896 instances of User in the memory (only 3 are defined) or only 3?
Thanks,
Nope
JavaScript doesn't care what you put in the array, and it's not going to auto-populate it with values you didn't give it.
If you add 3 users to the array, you will only have 3 users in memory.
The indices in the gaps will just be undefined
var x = [];
// undefined
x[0] = "user1";
// 'user1'
x[3] = "user2";
// 'user2'
x[10] = "user3";
// 'user3'
x;
// ['user1',,,'user2',,,,,,,'user3']
All of that said, you might be better off using an Object ({})
var users = {};
function addUser(userID, name, address) {
if (!(userID in users)) {
users[userID] = new User(userID, name, address)
}
}
You will have an object that looks like this
{"63": [object User], "5": [object User], "895": [object User]}