Skip to content

Security model

What the platform can do, what it cannot, and where the trust actually sits. Read custody first for the fund-safety guarantees; this page covers the wider surface.

Trust boundaries

Threat: prompt injection

Contained by architecture, not by prompt engineering.

bz-agent has no RPC provider, no private key, no relayer ABI and no HTTP client pointed back at bz-backend. The worst a malicious prompt can produce is a wrong intent object.

Then:

LayerWhat it re-validates
agent.service handlersToken symbols against the Token collection, chain names against Network
Quote fetchReal provider quotes for the resolved pair
Autopilot draftingThe LLM emits symbols and numbers only; the backend resolves addresses and runs validatePolicy
UserSigns, or does not

An LLM cannot produce a signable Autopilot policy

That is the strongest statement in this section and it is structural: buildPolicyDraft constrains the model's output shape, and validatePolicy runs afterwards. A hallucinated token resolves to nothing.

Threat: forged mutation requests

Most /defi routes are unauthenticated — the wallet address in the body is trusted. That is acceptable where funds only move via user-signed transactions (DCA: claim and cancel are client-side, the backend only flips status).

It is not acceptable for Autopilot, where a forged "raise my spend cap" would change what the executor does with escrowed funds. Hence middleware/walletAuth.js.

The wallet-auth contract

body       = { ...payload, wallet, timestamp, signature }
signedBody = body minus `signature`
digest     = sha256(utf8(JSON.stringify(sortDeep(signedBody))))   // keys sorted recursively
message    = `Blazpay <action> ${digest} ${timestamp}`
signature  = personal_sign(message)                               // EIP-191

Four guarantees:

GuaranteeMechanism
No field tamperingThe digest covers the whole body
Action-scopedaction is in the message, so a signed pause cannot replay as create
Time-bounded±5 minutes (WINDOW_MS)
Single-useRedis replay guard, TTL 600 s — more than 2× the window, so an expired signature cannot come back

On success the middleware sets req.authWallet (lowercased, verified). Handlers must use that, never req.body.wallet.

Applied to: autopilot/create, autopilot/update/:id, autopilot/pause/:id, autopilot/resume/:id.

The frontend mirror must match exactly

defi-dex/src/utils/autopilotPolicy.ts mirrors this alongside the policy canonicalisation. A whitespace or key-order difference produces a digest the backend rejects.

Threat: leaked TX_SIGNER

An attacker with the key can make the relayer .call an arbitrary target — but funds are pulled from msg.sender by transferFrom, so they spend their own money. The realistic damage is griefing and reputational, not theft of user balances.

Mitigation: setSigner(address) for hot rotation. Do it immediately on suspicion; there is no reason to wait for confirmation.

Threat: leaked PRIVATE_KEY_EXECUTOR

Bounded by Solidity:

CannotBecause
Withdraw from a CSIP positionwithdrawWithPositionId checks the caller is the owner
Withdraw from an Autopilot vaultwithdraw/withdrawAllAndClose are owner-gated; _externalPayout targets the agent owner
Trade an unlisted tokenallowedTokens per agent
Exceed spending capsmaxPerTrade, maxPerDay, maxTradesPerDay, rolling window
Trade past expiryPolicyExpired
Call an arbitrary contractallowedRouter allow-list
Redirect outputVaults credit their own measured balance delta

Residual: forced bad-slippage churn on an agent's own positions through allow-listed routers. Bounded by liquidity; funds return to the agent. An oracle-based minOut floor would close it.

PRIVATE_KEY_EXECUTOR is also the DCA contract owner

Which means a compromised executor there has UUPS upgrade authority over the DCA vault. Separating the DCA owner from the executor is straightforward hardening (setExecutor already exists) and has not been done.

Threat: leaked owner EOA

This is the serious one. 0x2Ed05570214f6C0F7612B580aB37C163076e0162 can _authorizeUpgrade the Autopilot vault — replace the implementation entirely and bypass every guarantee on this page.

Moving the owner to a multisig is a launch blocker

It is item 2 on the Autopilot open blockers list. Until then, Autopilot's trust model is "a single EOA is honest and uncompromised".

The relayer's owner (0x75a8b522FC3195e3a3570F11f111AC89c0D35975, same as its signer) has the same power over 22 chains.

Threat: malicious or broken provider

AttackDefence
Returns an approve() step as swap calldataCalldata guard: reject tx.to == fromToken or data.length <= 68
Route delivers nothing in-transactionRoute probing with estimateGas
Route reverts on-chainsortQuotes gas simulation; per-item batch isolation
Router pulls less than approvedRelayer refunds the remainder; vaults debit measured spend
Router leaves an allowanceRelayer and vault both forceApprove(spender, 0) after the call
Provider API returns garbageErrors swallowed per aggregator; the user sees fewer routes
Router is malicious outrightAutopilot: allowedRouter. Relayer: requires a platform signature on the route

Threat: replay

SurfaceGuard
Relayer transactionnonces[msg.sender], incremented before execution
Relayer stalenessdeadline (100 s from the SDK)
Wallet-auth requestRedis single-use guard + ±5-minute window
Policy signatureagentId and chainId are inside the signed message
DCA fill recordUnique index on (txHash, positionId)

Secret handling

SecretWhere it livesNotes
TX_SIGNER, POL_TX_SIGNERVM .envNever in a bundle
PRIVATE_KEY_EXECUTORVM .envAlso DCA owner
BACKEND_PRIVATE_KEY / DCA_OWNER_KEYVM .env / local for upgradesOwner authority
Provider API keysVM .env, used only by proxy controllersNever reach the browser
Orderly ed25519 private keysMongo, KMS-encrypted (utils/kms.js)Scoped to Orderly's API
ZeroDev session keysManaged accountsUser-revocable
Model-provider credentials (see env)VM .env

swap-sdk ships with zero embedded keys

Everything key-bearing is either injected via configure() or proxied through the backend. That is what makes it safe to ship the SDK to the browser.

copytrade-agent's config.json has committed live secrets

A Telegram bot token, a Helius API key and a Hyperliquid API-wallet private key. Rotating and blanking them is step zero of that workstream. See copy trading.

Deployment risk

The backend deploy is rsync, not git

deploy.gcp.sh uses rsync because the VM's bz-backend directory is not a git repo. It copies your entire local working tree — excluding only .git/, node_modules/, .env, dist/, build/, logs/, *.log — and without --delete.

Consequences: uncommitted local work reaches production; scratch scripts and secrets in untracked files reach production; deleted files persist on the VM. Making the VM directory a git repo switches the script to git-pull mode and removes the whole class of problem.

.env on the VM is never overwritten, so new environment variables must be added there manually.

CORS

bz-backend/index.js uses an explicit allowlist with credentials: true. Socket.io CORS is configured separately (services/socket.js) — that was the one real gap the AI domain split had to close.

app.set('trust proxy', 1) and express.json({ limit: '20mb' }). The large body limit exists for pasted Solidity and generated dApp HTML; it is also a mild DoS surface.

Contract audit history

ContractReviewOutcome
BlazpayRelayer v2Hardening passNine changes, all storage-compatible. See relayer
BlazpayDcaV2Bug-fix passMeasured delta, .call payout, executor role, frequency = 0. Storage validated with upgrades.validateUpgrade
BlazpayAutopilotV1Slither + adversarialC-1 critical fixed (router allow-list), M-1 and M-2 fixed, H-1 accepted. 61 tests

Slither ran clean on Autopilot (false positives only). The relayer and DCA contracts have not had an equivalent adversarial review — worth noting given the relayer handles every interactive trade on 22 chains.

Known gaps

Stated plainly:

  1. Owner EOAs are not multisigs on any of the three contract families.
  2. PRIVATE_KEY_EXECUTOR doubles as DCA owner.
  3. Autopilot H-1 — non-budgetToken trades are not USD-metered (oracle-free by design).
  4. Autopilot residual — executor-forced slippage churn on an agent's own positions.
  5. Legacy /defi routes are unauthenticated. Fine for DCA's current shape; audit before adding any route that moves escrowed value.
  6. Chat history is readable by wallet address with no auth. Addresses are pseudonymous, but this is a real privacy property.
  7. The rsync deploy ships the whole working tree.
  8. No adversarial audit of the relayer or DCA contracts.
  9. copytrade-agent secrets are committed and live.
  10. No rate limiting on most routes. express-rate-limit is a dependency; verify where it is actually applied before assuming coverage.

Internal + developer documentation. Usage figures are point-in-time snapshots — re-verify before publishing them.