_hashTypedDataV4 recovers the wrong address when the struct contains bytes/string — dynamic members must be keccak256-hashed
Symptom: an EIP-712 order signed with ethers signTypedData and verified with OpenZeppelin EIP712._hashTypedDataV4 + ECDSA.recover returns a garbage signer. Nothing reverts; the recovered address is simply not the signer. Removing the bytes params member from the type makes it work, which is the clue.
Cause: EIP-712 encodeData does not abi-encode dynamic members inline. Every bytes or string member must be replaced by keccak256(value), every array by the keccak of its concatenated encoded items, and every nested struct by its own hashStruct. abi.encode(..., order.params, ...) emits offset+length+padded data instead of a single 32-byte word, so your struct hash differs from the one the wallet computed, and ecrecover on a different digest returns an arbitrary address.
Fix inside the contract: pass keccak256(order.params) into abi.encode. Additionally, the type string must match exactly what the signer used, field names and order included. Flattening a nested struct (assetForSaleAddress, assetForSaleId, ...) into one Permit(...) typehash only works if the client signs that same flat type; if the client declares a nested AssetKey struct, the contract must use the nested encoding with the sub-struct hash and the concatenated type strings.
Debug method: have both sides print the domain separator, the type hash, and the struct hash, then compare. Domain mismatches (name, version, chainId, verifyingContract) produce the identical silent-wrong-signer symptom, so check those before rewriting the struct.
Source: adapted from a Stack Exchange answer by Mila A (question by RitzyDevUk), https://ethereum.stackexchange.com/questions/164955/hashtypeddatav4-does-not-verify-properly — CC BY-SA 4.0