diff --git a/hyperdrive/packages/homepage/ui/package-lock.json b/hyperdrive/packages/homepage/ui/package-lock.json index 63cdee8e1..0d76e3e8d 100644 --- a/hyperdrive/packages/homepage/ui/package-lock.json +++ b/hyperdrive/packages/homepage/ui/package-lock.json @@ -431,6 +431,278 @@ "node": ">=6.9.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", + "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz", + "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz", + "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz", + "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz", + "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz", + "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz", + "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz", + "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz", + "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz", + "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz", + "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz", + "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz", + "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz", + "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz", + "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz", + "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/linux-x64": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz", @@ -448,6 +720,142 @@ "node": ">=18" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz", + "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz", + "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz", + "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz", + "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz", + "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz", + "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz", + "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz", + "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", diff --git a/hyperdrive/src/register-ui/src/pages/SetPassword.tsx b/hyperdrive/src/register-ui/src/pages/SetPassword.tsx index 7556ed524..f86e59bc3 100644 --- a/hyperdrive/src/register-ui/src/pages/SetPassword.tsx +++ b/hyperdrive/src/register-ui/src/pages/SetPassword.tsx @@ -2,9 +2,10 @@ import React, { useState, useEffect, FormEvent, useCallback } from "react"; import Loader from "../components/Loader"; import { downloadKeyfile } from "../utils/download-keyfile"; import { Tooltip } from "../components/Tooltip"; -import { useSignTypedData, useAccount, useChainId } from 'wagmi' +import { useSignTypedData, useAccount, useChainId, usePublicClient } from 'wagmi' import { HYPERMAP } from "../abis"; import { redirectToHomepage } from "../utils/redirect-to-homepage"; +import { getWalletType, WalletType } from "../utils/wallet-detection"; type SetPasswordProps = { direct: boolean; @@ -26,10 +27,13 @@ function SetPassword({ const [pw2, setPw2] = useState(""); const [error, setError] = useState(""); const [loading, setLoading] = useState(false); + const [walletType, setWalletType] = useState('EOA'); + const [loadingMessage, setLoadingMessage] = useState("Please sign the structured message in your wallet to set your password."); const { signTypedDataAsync } = useSignTypedData(); const { address } = useAccount(); const chainId = useChainId(); + const publicClient = usePublicClient(); useEffect(() => { document.title = "Set Password"; @@ -39,6 +43,26 @@ function SetPassword({ setError(""); }, [pw, pw2]); + // Detect wallet type when address changes + useEffect(() => { + async function detectWallet() { + if (address && publicClient) { + const type = await getWalletType(address, publicClient); + setWalletType(type); + + // Update loading message based on wallet type + if (type === 'SAFE') { + setLoadingMessage("Please approve the message in your Safe app. All required owners must sign."); + } else if (type === 'UNKNOWN_CONTRACT') { + setLoadingMessage("Please sign the message in your smart contract wallet."); + } else { + setLoadingMessage("Please sign the structured message in your wallet to set your password."); + } + } + } + detectWallet(); + }, [address, publicClient]); + const handleSubmit = useCallback( async (e: FormEvent) => { e.preventDefault(); @@ -137,7 +161,7 @@ function SetPassword({ return ( <> {loading ? ( - + ) : (
diff --git a/hyperdrive/src/register-ui/src/utils/wallet-detection.ts b/hyperdrive/src/register-ui/src/utils/wallet-detection.ts new file mode 100644 index 000000000..df842be2f --- /dev/null +++ b/hyperdrive/src/register-ui/src/utils/wallet-detection.ts @@ -0,0 +1,109 @@ +import { Address } from 'viem'; +import { PublicClient } from 'wagmi'; + +export type WalletType = 'EOA' | 'SAFE' | 'UNKNOWN_CONTRACT'; + +/** + * Detects the type of wallet connected + * @param address - The wallet address to check + * @param client - The public client from wagmi + * @returns The type of wallet + */ +export async function getWalletType( + address: Address, + client: PublicClient +): Promise { + try { + // Get the bytecode at the address + const code = await client.getBytecode({ address }); + + // If no code, it's an EOA + if (!code || code === '0x') { + return 'EOA'; + } + + // Check if it's a Safe wallet + if (await isSafeWallet(address, client)) { + return 'SAFE'; + } + + // Otherwise it's some other smart contract wallet + return 'UNKNOWN_CONTRACT'; + } catch (error) { + console.error('Error detecting wallet type:', error); + // Default to EOA on error + return 'EOA'; + } +} + +/** + * Checks if the address is a Gnosis Safe wallet + * @param address - The address to check + * @param client - The public client from wagmi + * @returns True if it's a Safe wallet + */ +async function isSafeWallet( + address: Address, + client: PublicClient +): Promise { + try { + // Safe wallets implement the VERSION method + // Try to call VERSION() on the contract + const versionAbi = [{ + name: 'VERSION', + type: 'function', + stateMutability: 'view', + inputs: [], + outputs: [{ type: 'string' }], + }] as const; + + const version = await client.readContract({ + address, + abi: versionAbi, + functionName: 'VERSION', + }); + + // Safe wallets return version strings like "1.3.0", "1.4.1" etc + return typeof version === 'string' && version.includes('.'); + } catch { + // If VERSION call fails, check for other Safe-specific methods + try { + // Try calling getOwners() which is specific to Safe + const getOwnersAbi = [{ + name: 'getOwners', + type: 'function', + stateMutability: 'view', + inputs: [], + outputs: [{ type: 'address[]' }], + }] as const; + + await client.readContract({ + address, + abi: getOwnersAbi, + functionName: 'getOwners', + }); + + return true; + } catch { + return false; + } + } +} + +/** + * Helper to check if an address is a contract + * @param address - The address to check + * @param client - The public client from wagmi + * @returns True if the address has contract code + */ +export async function isContract( + address: Address, + client: PublicClient +): Promise { + try { + const code = await client.getBytecode({ address }); + return code !== undefined && code !== '0x'; + } catch { + return false; + } +} diff --git a/hyperdrive/src/register.rs b/hyperdrive/src/register.rs index db7b504b6..b7225f7a1 100644 --- a/hyperdrive/src/register.rs +++ b/hyperdrive/src/register.rs @@ -7,6 +7,9 @@ use alloy::rpc::types::eth::{TransactionInput, TransactionRequest}; use alloy::signers::Signature; use alloy_primitives::{Address as EthAddress, Bytes, FixedBytes, U256}; use alloy_sol_types::{eip712_domain, SolCall, SolStruct}; + +// EIP-1271 magic value for valid signatures +const EIP1271_MAGIC_VALUE: [u8; 4] = [0x16, 0x26, 0xba, 0x7e]; use base64::{engine::general_purpose::STANDARD as base64_standard, Engine}; use lib::types::core::{ BootInfo, Identity, ImportKeyfileInfo, Keyfile, LoginInfo, NodeRouting, UnencryptedIdentity, @@ -500,15 +503,32 @@ async fn handle_boot( }; let hash = boot.eip712_signing_hash(&domain); - let sig = Signature::from_str(&info.signature).map_err(|_| warp::reject())?; - let recovered_address = sig - .recover_address_from_prehash(&hash) - .map_err(|_| warp::reject())?; + // First try EOA signature verification + let mut signature_valid = false; + if let Ok(sig) = Signature::from_str(&info.signature) { + if let Ok(recovered_address) = sig.recover_address_from_prehash(&hash) { + if recovered_address == owner { + signature_valid = true; + } + } + } - if recovered_address != owner { - println!("recovered_address: {}\r", recovered_address); - println!("owner: {}\r", owner); + // If EOA verification failed, try EIP-1271 contract signature verification + if !signature_valid && is_contract(&owner, &provider).await { + match verify_eip1271_signature(&owner, &hash, &info.signature, &provider).await + { + Ok(is_valid) => { + signature_valid = is_valid; + } + Err(e) => { + println!("EIP-1271 verification error: {}\r", e); + } + } + } + + if !signature_valid { + println!("Signature verification failed for owner: {}\r", owner); attempts += 1; tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; continue; @@ -790,6 +810,55 @@ pub async fn assign_routing( Ok(()) } +/// Check if an address is a contract by checking if it has code +async fn is_contract(address: &EthAddress, provider: &RootProvider) -> bool { + match provider.get_code_at(*address).await { + Ok(code) => !code.is_empty(), + Err(_) => false, + } +} + +/// Verify a signature using EIP-1271 standard for contract wallets +async fn verify_eip1271_signature( + wallet_address: &EthAddress, + message_hash: &FixedBytes<32>, + signature: &str, + provider: &RootProvider, +) -> anyhow::Result { + // Convert signature string to bytes + let sig_bytes = if signature.starts_with("0x") { + hex::decode(&signature[2..]).map_err(|e| anyhow::anyhow!("Invalid signature hex: {}", e))? + } else { + hex::decode(signature).map_err(|e| anyhow::anyhow!("Invalid signature hex: {}", e))? + }; + + // Prepare the isValidSignature call + let call_data = isValidSignatureCall { + hash: *message_hash, + signature: Bytes::from(sig_bytes), + } + .abi_encode(); + + let tx = TransactionRequest::default() + .to(*wallet_address) + .input(TransactionInput::new(Bytes::from(call_data))); + + // Call the contract + match provider.call(&tx).await { + Ok(result) => { + // Check if the result is the magic value + if result.len() >= 4 { + let mut magic = [0u8; 4]; + magic.copy_from_slice(&result[0..4]); + Ok(magic == EIP1271_MAGIC_VALUE) + } else { + Ok(false) + } + } + Err(_) => Ok(false), + } +} + async fn success_response( sender: Arc, our: Identity, diff --git a/hyperdrive/src/sol.rs b/hyperdrive/src/sol.rs index 16521c501..fc5ad5fd7 100644 --- a/hyperdrive/src/sol.rs +++ b/hyperdrive/src/sol.rs @@ -61,4 +61,10 @@ sol! { ) external payable returns (uint256 blockNumber, bytes[] memory returnData); function token() external view returns (uint256,address,uint256); + + // EIP-1271: Standard Signature Validation Method for Contracts + function isValidSignature( + bytes32 hash, + bytes calldata signature + ) external view returns (bytes4 magicValue); }