{"record":{"id":"007cc301b841ab1e","repo":"datahaven-xyz/datahaven","slug":"invalid-permit","errorCode":null,"errorMessage":"Invalid permit","messagePattern":"Invalid permit","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"operator/precompiles/call-permit/src/lib.rs","lineNumber":205,"sourceCode":"            from,\n            to,\n            value,\n            data.clone(),\n            gas_limit,\n            nonce,\n            deadline,\n        );\n\n        let mut sig = [0u8; 65];\n        sig[0..32].copy_from_slice(&r.as_bytes());\n        sig[32..64].copy_from_slice(&s.as_bytes());\n        sig[64] = v;\n\n        let signer = sp_io::crypto::secp256k1_ecdsa_recover(&sig, &permit)\n            .map_err(|_| revert(\"Invalid permit\"))?;\n        let signer = H160::from(H256::from_slice(keccak_256(&signer).as_slice()));\n\n        ensure!(\n            signer != H160::zero() && signer == from,\n            revert(\"Invalid permit\")\n        );\n\n        NoncesStorage::insert(from, nonce + U256::one());\n\n        // DISPATCH CALL\n        let sub_context = Context {\n            caller: from,\n            address: to.clone(),\n            apparent_value: value,\n        };\n\n        let transfer = if value.is_zero() {\n            None\n        } else {\n            Some(Transfer {\n                source: from,","sourceCodeStart":187,"sourceCodeEnd":223,"githubUrl":"https://github.com/datahaven-xyz/datahaven/blob/edcb13dbbcd3c29489eaa2480a6f60ee4cb1f3ec/operator/precompiles/call-permit/src/lib.rs#L187-L223","documentation":"The call-permit precompile's `dispatch` reverts with \"Invalid permit\" when ECDSA recovery fails or, more commonly, when the recovered signer does not match the claimed `from` address (or is the zero address). The permit signature must recover to exactly the account authorizing the call; any mismatch means the signature is malformed, signed with the wrong key, or covers different data.","triggerScenarios":"Calling `dispatch` with: (1) a signature that fails `secp256k1_ecdsa_recover` (corrupt/truncated sig, wrong v), or (2) a recovered signer != `from` or signer == H160::zero() — e.g. the permit hash was built with fields differing from those used at signing time (nonce, deadline, call data, chain id, contract address).","commonSituations":"Client and precompile hashing different permit structs (field order/types mismatch); signer uses a different key than the `from` address; nonce already consumed so the signed nonce no longer matches `NoncesStorage`; EIP-155 v-value handling bugs; chain id or verifying-contract address mismatch when signing EIP-712.","solutions":["Re-sign the permit with the exact same fields the precompile hashes (address, from, call data hash, nonce from NoncesStorage, deadline, chain id) using the key controlling `from`.","Ensure the `from` argument equals the signer address derived from the signature.","Refresh the nonce: read NoncesStorage (the precompile's nonce query) before signing, since consumed nonces invalidate old signatures.","Verify EIP-712 domain (name, version, chainId, verifyingContract) matches the precompile's expected domain."],"exampleFix":"// before: signed with mismatched fields\nlet sig = sign_eip712(old_domain, stale_nonce, ...);\ncall_permit.dispatch(call, sig)?; // revert(\"Invalid permit\")\n\n// after: sign current nonce with matching domain\nlet nonce = call_permit.nonces(from);\nlet sig = sign_eip712(precompile_domain, nonce, deadline, call);\nassert!(recover(sig) == from);\ncall_permit.dispatch(call, sig)?;","handlingStrategy":"try-catch","validationCode":"const recovered = recoverAddress(permitHash, sig);\nif (recovered.toLowerCase() !== from.toLowerCase()) throw new Error('signer mismatch');","typeGuard":"function isPermitSignatureValid(sig, from) { try { return recoverAddress(sig).toLowerCase() === from.toLowerCase(); } catch { return false; } }","tryCatchPattern":"try { await callPermit.dispatch(call, sig); } catch (e) { if (e.message.includes('Invalid permit')) { const nonce = await callPermit.nonces(from); const fresh = signPermitEip712(domain, { from, nonce, deadline, call }); return callPermit.dispatch(call, fresh); } throw e; }","preventionTips":["Fetch the current nonce immediately before signing","Match EIP-712 domain (name, version, chainId, verifyingContract) exactly","Sign with the key controlling `from`","Keep client permit struct field order identical to the precompile's hashing order"],"tags":["evm","precompile","permit","ecdsa","signature-verification","eip712"],"backgroundTag":"invalid-argument-format","analyzedSha":"edcb13dbbcd3c29489eaa2480a6f60ee4cb1f3ec","analyzedAt":"2026-09-13T19:19:32.206Z","contentChangedAt":"2026-09-13T19:19:32.206Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}