From 32166c46d91ace2958300aac0641643e4a6ce722 Mon Sep 17 00:00:00 2001 From: Pierre Hay Date: Wed, 13 Nov 2024 19:49:21 +0100 Subject: [PATCH] test: validate EIP-712 signature --- tasks/prepareTests.ts | 4 +- tasks/utils.ts | 125 ++++++++++++++++--------- test/upgradeableOpenfortAccountTest.ts | 113 ++++++++++++++-------- 3 files changed, 157 insertions(+), 85 deletions(-) diff --git a/tasks/prepareTests.ts b/tasks/prepareTests.ts index 8537974..6d3c51f 100644 --- a/tasks/prepareTests.ts +++ b/tasks/prepareTests.ts @@ -14,10 +14,10 @@ task("test") const {factory, implementation} = await hre.run("deploy-factory") // wait for sophon backend service to whitelist the factory in their paymaster factoryAddress = factory - if (chain.name == "Sophon") await sleep(30000) + if (chain.name == "Sophon") await sleep(10000) accountAddress = await hre.run("create-account", { factory, implementation, nonce: args.nonce }) // wait for sophon backend service to whitelist the new account in their paymaster - if (chain.name == "Sophon") await sleep(30000) + if (chain.name == "Sophon") await sleep(10000) } hre.openfortAccountAddress = accountAddress hre.factoryAddress = factoryAddress diff --git a/tasks/utils.ts b/tasks/utils.ts index f6c7be5..2483cf2 100644 --- a/tasks/utils.ts +++ b/tasks/utils.ts @@ -1,55 +1,88 @@ -import { defineChain, WalletClient } from "viem" +import { Address, defineChain, PublicClient, WalletClient } from "viem" import { chainConfig, getGeneralPaymasterInput, zksyncInMemoryNode, zksyncSepoliaTestnet } from "viem/zksync" -export async function writeContract(c: WalletClient, contractParams) { - // add paymaster info for sophon - if (hre.network.config.url.includes("sophon")) { - contractParams.paymaster = process.env.SOPHON_TESTNET_PAYMASTER_ADDRESS - contractParams.paymasterInput = getGeneralPaymasterInput({ innerInput: new Uint8Array() }) - } - return c.writeContract(contractParams) +export async function writeContract(c: WalletClient, contractParams) { + // add paymaster info for sophon + if (hre.network.config.url.includes("sophon")) { + contractParams.paymaster = process.env.SOPHON_TESTNET_PAYMASTER_ADDRESS + contractParams.paymasterInput = getGeneralPaymasterInput({ innerInput: new Uint8Array() }) + } + return c.writeContract(contractParams) } export function getViemChainFromConfig() { - const sophon = defineChain({ - ...chainConfig, - id: 531050104, - name: "Sophon", - network: "sepolia", - nativeCurrency: { - name: "SOPHON", - symbol: "SOPH", - decimals: 18, - }, - rpcUrls: { - default: { - http: ["https://rpc.testnet.sophon.xyz"], - }, - public: { - http: ["https://rpc.testnet.sophon.xyz"], - }, - }, - blockExplorers: { - default: { - name: "Sophon Testnet Explorer", - url: "https://explorer.testnet.sophon.xyz/", - }, - }, - testnet: true, - }) + const sophon = defineChain({ + ...chainConfig, + id: 531050104, + name: "Sophon", + network: "sepolia", + nativeCurrency: { + name: "SOPHON", + symbol: "SOPH", + decimals: 18, + }, + rpcUrls: { + default: { + http: ["https://rpc.testnet.sophon.xyz"], + }, + public: { + http: ["https://rpc.testnet.sophon.xyz"], + }, + }, + blockExplorers: { + default: { + name: "Sophon Testnet Explorer", + url: "https://explorer.testnet.sophon.xyz/", + }, + }, + testnet: true, + }) - switch (hre.network.config.url) { - case "http://127.0.0.1:8011": - return zksyncInMemoryNode - case "https://sepolia.era.zksync.dev": - return zksyncSepoliaTestnet - case "https://rpc.testnet.sophon.xyz": - return sophon - default: - throw new Error("Unkown network") - } + switch (hre.network.config.url) { + case "http://127.0.0.1:8011": + return zksyncInMemoryNode + case "https://sepolia.era.zksync.dev": + return zksyncSepoliaTestnet + case "https://rpc.testnet.sophon.xyz": + return sophon + default: + throw new Error("Unkown network") + } } export function sleep(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); -} \ No newline at end of file + return new Promise(resolve => setTimeout(resolve, ms)); +} + +export async function getEIP712Domain(contractAddress: Address, client: PublicClient) { + const abi = [ + { + "constant": true, + "inputs": [], + "name": "eip712Domain", + "outputs": [ + { "name": "fields", "type": "bytes1" }, + { "name": "name", "type": "string" }, + { "name": "version", "type": "string" }, + { "name": "chainId", "type": "uint256" }, + { "name": "verifyingContract", "type": "address" }, + { "name": "salt", "type": "bytes32" }, + { "name": "extensions", "type": "uint256[]" } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + } + ]; + + try { + const result = await client.readContract({ + address: contractAddress, + abi: abi, + functionName: "eip712Domain", + }); + return result; + } catch (error) { + console.error("Error calling eip712Domain:", error); + } +} diff --git a/test/upgradeableOpenfortAccountTest.ts b/test/upgradeableOpenfortAccountTest.ts index 591ab30..fa1eb20 100644 --- a/test/upgradeableOpenfortAccountTest.ts +++ b/test/upgradeableOpenfortAccountTest.ts @@ -1,9 +1,9 @@ import { expect } from "chai" -import { encodeFunctionData, encodePacked, parseAbi } from "viem" +import { concat, encodeFunctionData, encodePacked, keccak256, pad, parseAbi, toHex } from "viem" import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" import { eip712WalletActions, toSinglesigSmartAccount } from "viem/zksync" import { createWalletClient, createPublicClient, hashTypedData, http } from "viem" -import { getViemChainFromConfig, writeContract } from "../tasks/utils" +import { getEIP712Domain, getViemChainFromConfig, writeContract } from "../tasks/utils" import { getGeneralPaymasterInput, serializeTransaction } from "viem/zksync" import hre from "hardhat"; @@ -209,7 +209,7 @@ describe("ERC20 interactions from Openfort Account", function () { value: 0n, callData: encodeFunctionData({ abi: mintAbi, - functionName: 'mint', + functionName: "mint", args: [openfortAccountAddress, 10n] }) }, @@ -218,7 +218,7 @@ describe("ERC20 interactions from Openfort Account", function () { value: 0n, callData: encodeFunctionData({ abi: mintAbi, - functionName: 'mint', + functionName: "mint", args: [openfortAccountAddress, 20n] }) }, @@ -227,7 +227,7 @@ describe("ERC20 interactions from Openfort Account", function () { value: 0n, callData: encodeFunctionData({ abi: mintAbi, - functionName: 'mint', + functionName: "mint", args: [openfortAccountAddress, 30n] }) }, @@ -236,46 +236,45 @@ describe("ERC20 interactions from Openfort Account", function () { value: 0n, callData: encodeFunctionData({ abi: mintAbi, - functionName: 'mint', + functionName: "mint", args: [openfortAccountAddress, 40n] }) } ]; const abi = [ - { - inputs: [ - { - components: [ - { - internalType: "address", - name: "target", - type: "address" - }, - { - internalType: "uint256", - name: "value", - type: "uint256" - }, - { - internalType: "bytes", - name: "callData", - type: "bytes" - } + { + inputs: [ + { + components: [ + { + internalType: "address", + name: "target", + type: "address" + }, + { + internalType: "uint256", + name: "value", + type: "uint256" + }, + { + internalType: "bytes", + name: "callData", + type: "bytes" + } + ], + internalType: "struct Call[]", + name: "calls", + type: "tuple[]" + } ], - internalType: "struct Call[]", - name: "calls", - type: "tuple[]" - } - ], - name: "batchCall", - outputs: [], - stateMutability: "nonpayable", - type: "function" - } + name: "batchCall", + outputs: [], + stateMutability: "nonpayable", + type: "function" + } ]; - const data = encodeFunctionData({ abi: abi, functionName: "batchCall", @@ -337,4 +336,44 @@ describe("ERC20 interactions from Openfort Account", function () { // Assert that the final balance is the initial balance plus the sum of all minted amounts expect(finalBalance - initialBalance).to.equal(10n + 20n + 30n + 40n); }); -}) \ No newline at end of file + + it("should validate the EIP-712 signature correctly with the given message structure", async function () { + // keccak256("OpenfortMessage(bytes32 message)") + const OF_MSG_TYPEHASH = "0x57159f03b9efda178eab2037b2ec0b51ce11be0051b8a2a9992c29dc260e4a30" + // keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") + const TYPE_HASH = "0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f" + + const messageToSign = "Signed by Owner" + const hash = keccak256(new TextEncoder().encode(messageToSign)) + const structHash = keccak256(encodePacked(["bytes32", "bytes32"], [OF_MSG_TYPEHASH, hash])) + + const [,name, version, chainId, verifyingContract,,] = await getEIP712Domain(openfortAccountAddress, publicClient) + + // Manually calculate domain separator + // to include TYPE_HASH + const domainSeparator = keccak256( + concat([ + TYPE_HASH, + pad(keccak256(Buffer.from(name)), { size: 32 }), + pad(keccak256(Buffer.from(version)), { size: 32 }), + pad(toHex(chainId), { size: 32 }), + pad(verifyingContract, { size: 32 }) + ]) + ); + const hashToSign = keccak256(concat(["0x1901", domainSeparator, structHash])) + // Sign the message + const signature = await owner.sign({ hash: hashToSign }) + const abi = parseAbi(["function isValidSignature(bytes32 _hash, bytes memory _signature) external view returns (bytes4)"]); + const isValid = await publicClient.readContract({ + address: openfortAccountAddress, + abi, + functionName: "isValidSignature", + args: [hash, signature], + }); + // Assert that the signature is valid + expect(isValid).to.equal("0x1626ba7e"); // EIP1271_SUCCESS_RETURN_VALUE + }); +}) + + +