Appearance
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:
| Layer | What it re-validates |
|---|---|
agent.service handlers | Token symbols against the Token collection, chain names against Network |
| Quote fetch | Real provider quotes for the resolved pair |
| Autopilot drafting | The LLM emits symbols and numbers only; the backend resolves addresses and runs validatePolicy |
| User | Signs, 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-191Four guarantees:
| Guarantee | Mechanism |
|---|---|
| No field tampering | The digest covers the whole body |
| Action-scoped | action is in the message, so a signed pause cannot replay as create |
| Time-bounded | ±5 minutes (WINDOW_MS) |
| Single-use | Redis 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:
| Cannot | Because |
|---|---|
| Withdraw from a CSIP position | withdrawWithPositionId checks the caller is the owner |
| Withdraw from an Autopilot vault | withdraw/withdrawAllAndClose are owner-gated; _externalPayout targets the agent owner |
| Trade an unlisted token | allowedTokens per agent |
| Exceed spending caps | maxPerTrade, maxPerDay, maxTradesPerDay, rolling window |
| Trade past expiry | PolicyExpired |
| Call an arbitrary contract | allowedRouter allow-list |
| Redirect output | Vaults 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
| Attack | Defence |
|---|---|
Returns an approve() step as swap calldata | Calldata guard: reject tx.to == fromToken or data.length <= 68 |
| Route delivers nothing in-transaction | Route probing with estimateGas |
| Route reverts on-chain | sortQuotes gas simulation; per-item batch isolation |
| Router pulls less than approved | Relayer refunds the remainder; vaults debit measured spend |
| Router leaves an allowance | Relayer and vault both forceApprove(spender, 0) after the call |
| Provider API returns garbage | Errors swallowed per aggregator; the user sees fewer routes |
| Router is malicious outright | Autopilot: allowedRouter. Relayer: requires a platform signature on the route |
Threat: replay
| Surface | Guard |
|---|---|
| Relayer transaction | nonces[msg.sender], incremented before execution |
| Relayer staleness | deadline (100 s from the SDK) |
| Wallet-auth request | Redis single-use guard + ±5-minute window |
| Policy signature | agentId and chainId are inside the signed message |
| DCA fill record | Unique index on (txHash, positionId) |
Secret handling
| Secret | Where it lives | Notes |
|---|---|---|
TX_SIGNER, POL_TX_SIGNER | VM .env | Never in a bundle |
PRIVATE_KEY_EXECUTOR | VM .env | Also DCA owner |
BACKEND_PRIVATE_KEY / DCA_OWNER_KEY | VM .env / local for upgrades | Owner authority |
| Provider API keys | VM .env, used only by proxy controllers | Never reach the browser |
| Orderly ed25519 private keys | Mongo, KMS-encrypted (utils/kms.js) | Scoped to Orderly's API |
| ZeroDev session keys | Managed accounts | User-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
| Contract | Review | Outcome |
|---|---|---|
BlazpayRelayer v2 | Hardening pass | Nine changes, all storage-compatible. See relayer |
BlazpayDcaV2 | Bug-fix pass | Measured delta, .call payout, executor role, frequency = 0. Storage validated with upgrades.validateUpgrade |
BlazpayAutopilotV1 | Slither + adversarial | C-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:
- Owner EOAs are not multisigs on any of the three contract families.
PRIVATE_KEY_EXECUTORdoubles as DCA owner.- Autopilot H-1 — non-
budgetTokentrades are not USD-metered (oracle-free by design). - Autopilot residual — executor-forced slippage churn on an agent's own positions.
- Legacy
/defiroutes are unauthenticated. Fine for DCA's current shape; audit before adding any route that moves escrowed value. - Chat history is readable by wallet address with no auth. Addresses are pseudonymous, but this is a real privacy property.
- The rsync deploy ships the whole working tree.
- No adversarial audit of the relayer or DCA contracts.
copytrade-agentsecrets are committed and live.- No rate limiting on most routes.
express-rate-limitis a dependency; verify where it is actually applied before assuming coverage.