Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.vela.monolithsystematic.com/llms.txt

Use this file to discover all available pages before exploring further.

Every action that modifies state on Vela — placing an order, cancelling an order, requesting a withdrawal — requires a valid ECDSA signature from the account holder’s Ethereum wallet. This makes Vela’s authentication model identical to Ethereum’s: your private key is your identity, and signatures are your authorization.

Elliptic Curve: secp256k1

Vela uses the secp256k1 elliptic curve, the same curve used by Bitcoin and Ethereum. Signatures are produced using the standard ECDSA algorithm over this curve. Curve parameters (for reference):
  • Field prime: p = 2²⁵⁶ − 2³² − 977
  • Generator point: standard secp256k1 G
  • Order: n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
In Rust, Vela uses the k256 crate (part of the RustCrypto ecosystem) for all signature operations. k256 is a pure-Rust, constant-time implementation with no unsafe code.

EIP-191 Personal Sign

Raw ECDSA signatures over arbitrary data have a replay attack vector: a signature produced for one application could be replayed in another. EIP-191 prevents this by prepending a standard prefix before hashing:
message = "\x19Ethereum Signed Message:\n" + len(data) + data
hash = keccak256(message)
signature = ECDSA.sign(hash, private_key)
The \x19Ethereum Signed Message:\n prefix ensures that a personal signature cannot be mistaken for an Ethereum transaction signature (which uses a different prefix). Vela uses personal_sign (EIP-191 personal sign) for all signatures. This is the standard method exposed by MetaMask, ethers.js (signer.signMessage()), and all major Ethereum wallets.

Order Signing

The signing payload for an order is the canonical JSON encoding of the order parameters:
{"market_id":"ETH-USDC","side":"bid","price":"3200000000","quantity":"1000000","order_type":"limit","time_in_force":"gtc","nonce":42}
Canonical format rules:
  1. No whitespace (no spaces after : or ,)
  2. Fields in this exact order: market_id, side, price, quantity, order_type, time_in_force, nonce
  3. All values are strings or integers — no floats
  4. UTF-8 encoding
The engine re-computes the canonical encoding from the submitted order fields and verifies the signature against that encoding. This means if a single character is different, the signature check fails.

Address Recovery

The engine recovers the signer address from the signature and verifies it matches the claimed address:
1. Reconstruct the prefixed hash:
   hash = keccak256("\x19Ethereum Signed Message:\n" + len(message) + message)

2. Extract (r, s, v) from the 65-byte signature

3. Recover public key:
   pubkey = ECDSA.recover(hash, r, s, v)

4. Derive Ethereum address:
   address = "0x" + keccak256(pubkey[1:])[12:].hex()

5. Compare to claimed address:
   assert address == request.address
Step 2 uses the recovery id (v) — either 27 or 28 — to select which of the two possible public keys corresponds to the signature. The k256 crate handles this via RecoveryId.

Nonce Replay Prevention

Each order includes a nonce field. The engine maintains a nonce high-water mark per account, initialized at 0:
  • When an order with nonce = N is accepted, the high-water mark becomes N
  • Any future order with nonce ≤ N is rejected with DUPLICATE_NONCE
  • Nonces must be strictly increasing, but do not need to be contiguous
This prevents a replayed signature attack: even if an attacker intercepts a valid signed order, they cannot re-submit it because the nonce has already been consumed.
Nonces are per-account and persisted in the state layer. They survive engine restarts. Your local nonce counter must start from nonce_high_water + 1 fetched from the API, not from 0.

Cancel Signing

Cancel requests are also signed to prevent unauthorized order cancellation:
{"order_id":"ord_7f3a91c2","address":"0xYourAddress","nonce":43}
An attacker who knows your order ID cannot cancel your order without your signature.

WebSocket Authentication

WebSocket private channel authentication uses a challenge-response scheme:
  1. Client sends request_challenge with address
  2. Server returns a random nonce (hex string, 24 bytes)
  3. Client signs: "Vela Exchange\nNonce: " + challenge_nonce
  4. Client sends auth with address and signature
  5. Server recovers address, verifies match, grants session
The challenge nonce expires in 5 minutes to prevent offline brute-force attacks. The format (Vela Exchange\nNonce: ...) is distinct from order signing to prevent cross-use of signatures.

Rust Implementation (k256)

use k256::ecdsa::{RecoveryId, Signature, VerifyingKey};
use k256::ecdsa::signature::hazmat::PrehashVerifier;
use sha3::{Digest, Keccak256};

pub fn recover_signer(message: &str, signature_bytes: &[u8; 65]) -> Result<Address> {
    // EIP-191 prefix
    let prefix = format!("\x19Ethereum Signed Message:\n{}", message.len());
    let hash = Keccak256::new()
        .chain_update(prefix.as_bytes())
        .chain_update(message.as_bytes())
        .finalize();

    let (r_s, v) = signature_bytes.split_at(64);
    let sig = Signature::from_bytes(r_s.into())?;
    let recovery_id = RecoveryId::try_from(v[0] - 27)?;

    let verifying_key = VerifyingKey::recover_from_prehash(&hash, &sig, recovery_id)?;
    let pubkey_bytes = verifying_key.to_encoded_point(false);

    // keccak256(pubkey[1:]), take last 20 bytes
    let pubkey_hash = Keccak256::digest(&pubkey_bytes.as_bytes()[1..]);
    let address = Address::from_slice(&pubkey_hash[12..]);

    Ok(address)
}

Security Properties

PropertyMechanism
Authenticationsecp256k1 ECDSA — only private key holder can sign
AuthorizationSignature covers full order details — no partial authorization
IntegrityAny field modification invalidates the signature
Replay preventionPer-account nonce high-water mark
Cross-application isolationEIP-191 prefix prevents signature reuse across apps
Constant-timek256 crate uses constant-time operations