Replies: 1 comment
-
Ok I figured out where I was going wrong. First, all strings need to be encoded when hashing. Here is a working solution: // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
contract DelegateVoter is EIP712 {
constructor() EIP712("DelegateVoter", "0.0.1") {}
struct QuorumConfig {
uint256 minQuorum;
uint256 minAcceptanceQuorum;
}
struct ProposalConfig {
string name;
QuorumConfig quorumConfig;
}
bytes32 public constant QUORUM_TYPE_HASH = keccak256("QuorumConfig(uint256 minQuorum,uint256 minAcceptanceQuorum)");
// typehash for proposal must also include the quorum typehash
bytes32 public constant PROPOSAL_TYPE_HASH =
keccak256("ProposalConfig(string name,QuorumConfig quorumConfig)QuorumConfig(uint256 minQuorum,uint256 minAcceptanceQuorum)");
function hashString(string calldata source) private pure returns (bytes32) {
return keccak256(bytes(source));
}
function hashQuorumConfig(QuorumConfig calldata quorumConfig) private pure returns (bytes32) {
return keccak256(abi.encode(QUORUM_TYPE_HASH, quorumConfig.minQuorum, quorumConfig.minAcceptanceQuorum));
}
function hashProposal(ProposalConfig calldata proposalConfig) private pure returns (bytes32) {
return keccak256(abi.encode(PROPOSAL_TYPE_HASH, hashString(proposalConfig.name), hashQuorumConfig(proposalConfig.quorumConfig)));
}
function delegateProposeHashData(ProposalConfig calldata proposalConfig) public view returns (bytes32) {
bytes32 encoded = hashProposal(proposalConfig);
// build the struct hash to be signed using eip712 library
return _hashTypedDataV4(encoded);
}
function recoverAddress(ProposalConfig calldata proposalConfig, bytes calldata signature) public view returns (address) {
// encode the data and recover the signature
bytes32 encoded = delegateProposeHashData(proposalConfig);
return ECDSA.recover(encoded, signature);
}
} And working test: import { http, createWalletClient, createPublicClient } from 'viem'
import { foundry } from 'viem/chains'
import { describe, it, expect } from 'vitest';
import * as delegateVoterContract from '../contracts/out/DelegateVoter.sol/DelegateVoter.json';
import { delegateVoterABI } from './generated';
import { AbiParametersToPrimitiveTypes, ExtractAbiFunction, } from 'abitype';
const walletClient = createWalletClient({
chain: foundry,
transport: http()
});
const [account] = await walletClient.getAddresses();
const publicClient = createPublicClient({
chain: foundry,
transport: http()
})
// JSON-RPC Account
type DelegateProposeHashDataInputs = AbiParametersToPrimitiveTypes<ExtractAbiFunction<typeof delegateVoterABI, 'delegateProposeHashData'>['inputs']>;
describe("DelegateVoter", () => {
it("can sign a digest", async () => {
const hash = await walletClient.deployContract({
abi: delegateVoterContract.abi,
bytecode: delegateVoterContract.bytecode.object as `0x${string}`,
account,
})
const receipt = await publicClient.waitForTransactionReceipt({ hash })
const contractAddress = receipt.contractAddress!;
const proposalConfig: DelegateProposeHashDataInputs[0] = {
name: 'test proposal',
quorumConfig: {
minAcceptanceQuorum: 2n,
minQuorum: 3n
},
};
const signature = await walletClient.signTypedData({
domain: {
chainId: foundry.id,
name: 'DelegateVoter',
version: '0.0.1',
verifyingContract: contractAddress,
},
types: {
ProposalConfig: [
{ name: 'name', type: 'string' },
{ name: 'quorumConfig', type: 'QuorumConfig' }
],
QuorumConfig: [
{ name: 'minQuorum', type: 'uint256' },
{ name: 'minAcceptanceQuorum', type: 'uint256' },
],
},
message: proposalConfig,
primaryType: 'ProposalConfig',
account
})
const recovered = await publicClient.readContract({
abi: delegateVoterABI,
address: contractAddress,
functionName: 'recoverAddress',
args: [proposalConfig, signature]
});
expect(recovered).toBe(account);
});
}) |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
I'm having difficulty getting viem's signedTypeData to work properly with nested structs as arguments; I'm using openzeppelin's standard eip712 contract for typed data generateion. I'm trying to simulate signing and recovering a signature in a test using vitest.
Given this example contract:
I have a simple test that can sign/recover the signature in solidity:
In javascript, ideally, I'd be able to fetch via the rpc the return value of
delegateProposeHashData
and then sign it. I can't seem to see how to do that with viem.sh. So I attempt to follow the instructions of walletClient.signTypedData.The above contract results in an abi containing:
Here's the test I end up writing:
The resulting recovered address differs than the expected account address and the test fails.
Some qs:
types
forsignTypedData
instead of manually having to recreate the types needed? If I try to pass them directly, the fact that they have a propertyinternalType
breaks signedTypeData.delegateProposeHashData
?Beta Was this translation helpful? Give feedback.
All reactions