Seed phrases (or seed words) began as a smart usability improvement: instead of writing down long, error-prone private keys, users could back up their wallets with a set of human-readable words. It was a clever abstraction — until wallet developers took shortcuts.
The problem arose when wallets started supporting multiple blockchains — Bitcoin, Ethereum, Solana, and others — from a single seed phrase.
Rather than generate a new, unique phrase per chain (as security would demand), wallet developers reused the same seed across all networks. This made onboarding easier, but introduced a serious flaw:
If your seed phrase is ever compromised, the attacker doesn’t just get your Bitcoin. They get your Ethereum, Solana, Avalanche — everything.
The impact of compromise multiplies across every supported chain.
Now, to be clear: the odds of brute-forcing a valid 24-word seed phrase remain 1 in 2²⁵⁶, which is astronomically small. But when the same phrase secures assets across multiple chains, it increases the number of attack surfaces — and more importantly, the statistical observability of successful guesses.
Even if the chance is still nearly zero, an attacker who checks a single seed guess across ten chains has more chances to detect if they hit anything valuable.
That’s why Contractless blocks seed phrase usage altogether.
How Contractless Blocks Seed Words
We enforce a simple but powerful rule:
All public keys must start with a specific prefix byte (239), baked directly into the key generation logic.
Here’s the core of our keypair generator:
pub fn generate_keypair(
network_byte: u8
) -> (Vec<u8>, String) {
let prefix_byte = 239;
let secp = Secp256k1::new();
let mut rng = OsRng;
let mut secret_key_bytes: Vec<u8> = vec![0; 32];
rng.fill_bytes(&mut secret_key_bytes);
let mut secret_key = SecretKey::from_slice(&secret_key_bytes).expect("Failed to create SecretKey from random bytes");
let mut public_key = PublicKey::from_secret_key(&secp, &secret_key);
while public_key.serialize()[1] != prefix_byte {
rng.fill_bytes(&mut secret_key_bytes);
secret_key = SecretKey::from_slice(&secret_key_bytes).expect("Failed to create SecretKey from random bytes");
public_key = PublicKey::from_secret_key(&secp, &secret_key);
}
let mut public_key_bytes: Vec<u8> = Vec::new();
public_key_bytes.push(network_byte);
public_key_bytes.extend(public_key.serialize().to_vec());
let private_key_hex = encode(secret_key_bytes);
(public_key_bytes, private_key_hex)
}
Why This Breaks Seed Word Compatibility
BIP-39 seed phrases are deterministic — they produce fixed private keys, which then produce fixed public keys. But there's no way to force a BIP-39-derived key to produce a public key that starts with a specific byte like239
without looping or brute-forcing — which defeats the whole point of seed determinism.
In other words:
- BIP-39 phrases can’t generate our keys.
- Our keys can’t be generated from standard seed words.
- Result: seed phrases are effectively disabled.
This hardens Contractless against seed phrase leakage and cross-chain compromise. Even if users try to import a BIP-39 phrase, it will be rejected — because the resulting public key simply won’t conform to our prefix rule.
