datahaven-xyz/datahaven · error

Invalid permit

Error message

Invalid permit

What it means

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.

Solutions

  1. 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`.
  2. Ensure the `from` argument equals the signer address derived from the signature.
  3. Refresh the nonce: read NoncesStorage (the precompile's nonce query) before signing, since consumed nonces invalidate old signatures.
  4. Verify EIP-712 domain (name, version, chainId, verifyingContract) matches the precompile's expected domain.

Example fix

// before: signed with mismatched fields
let sig = sign_eip712(old_domain, stale_nonce, ...);
call_permit.dispatch(call, sig)?; // revert("Invalid permit")

// after: sign current nonce with matching domain
let nonce = call_permit.nonces(from);
let sig = sign_eip712(precompile_domain, nonce, deadline, call);
assert!(recover(sig) == from);
call_permit.dispatch(call, sig)?;
Defensive patterns

Strategy: try-catch

Validate before calling

const recovered = recoverAddress(permitHash, sig);
if (recovered.toLowerCase() !== from.toLowerCase()) throw new Error('signer mismatch');

Type guard

function isPermitSignatureValid(sig, from) { try { return recoverAddress(sig).toLowerCase() === from.toLowerCase(); } catch { return false; } }

Try / catch

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; }

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of datahaven-xyz/datahaven@edcb13dbbc (2026-09-13). Data as JSON: /api/errors/007cc301b841ab1e. Report an issue: GitHub.

Appendix: source

Thrown at operator/precompiles/call-permit/src/lib.rs:205

            from,
            to,
            value,
            data.clone(),
            gas_limit,
            nonce,
            deadline,
        );

        let mut sig = [0u8; 65];
        sig[0..32].copy_from_slice(&r.as_bytes());
        sig[32..64].copy_from_slice(&s.as_bytes());
        sig[64] = v;

        let signer = sp_io::crypto::secp256k1_ecdsa_recover(&sig, &permit)
            .map_err(|_| revert("Invalid permit"))?;
        let signer = H160::from(H256::from_slice(keccak_256(&signer).as_slice()));

        ensure!(
            signer != H160::zero() && signer == from,
            revert("Invalid permit")
        );

        NoncesStorage::insert(from, nonce + U256::one());

        // DISPATCH CALL
        let sub_context = Context {
            caller: from,
            address: to.clone(),
            apparent_value: value,
        };

        let transfer = if value.is_zero() {
            None
        } else {
            Some(Transfer {
                source: from,

View on GitHub (pinned to edcb13dbbc)