Skip to content

Custody & trust model

Blazpay never holds user funds in an omnibus account. Every flow is either (a) the user signing a transaction from their own wallet, or (b) a contract the user owns, executed by a platform bot whose authority is bounded on-chain.

Read this page before writing anything that touches automation. The guarantees below are enforced by Solidity, not by convention, and it is easy to weaken them accidentally.

Three custody modes

ModeUsed byWho holds the fundsWhat the platform can do
User-signed, atomicSwap, bridge, limit orders, approvals, manual claimsThe user's wallet, until the moment of the tradeNothing without the user's signature on that exact transaction
User-owned vault, bot-executedCSIP, AutopilotA vault contract, credited to the user's addressExecute trades within on-chain limits. Cannot withdraw to itself
Third-party custodyFiat on/off-ramp, perpetualsThe provider (OnRamp.money, Orderly, …)Nothing — Blazpay is a client of their API

Mode 1 — user-signed atomic settlement

A swap or bridge is one transaction, sent by the user, from the user's wallet.

What the backend signature is and is not

The relayer requires ECDSA.recover(metaTxHash, signature) == signer, where signer is the platform's TX_SIGNER address (0x75a8b522FC3195e3a3570F11f111AC89c0D35975 on all deployed chains).

That means the design is co-signed:

  • The backend attests to the route: that this targetContract and this calldata are a legitimate aggregator route the platform built. Without that attestation, an arbitrary attacker could not use the relayer as a generic call-forwarder.
  • The user attests to the spend: they are msg.sender, they pay gas, and the funds are pulled from their address by transferFrom. Without their transaction, nothing happens.

Neither party can act alone. The platform's signature is useless without a user willing to send it; a user's transaction is useless without a fresh platform signature.

A leaked TX_SIGNER is serious but not a drain

An attacker with TX_SIGNER could get the relayer to .call an arbitrary target — but only while spending their own funds, because transferFrom pulls from msg.sender. The realistic damage is griefing and reputational, not theft of other users' balances. setSigner(address) exists precisely so the key can be hot-rotated. See secrets.

Hardening in relayer v2

Version 2 (deployed on all 22 chains, version: v2.1-raw in the deployment record) added:

ChangeWhy
require(_signer != address(0)) in initialize, and rejection of recovered == address(0)An uninitialised signer slot would otherwise accept a malformed signature
setSigner(address)Hot key rotation
SafeERC20 everywhere plus forceApproveNon-conforming tokens (USDT and friends) fail loudly instead of silently
Allowance reset to 0 after the external callNo residual approval to drain later
Token refund of anything the recipient did not pullCovers partial fills
Native overpayment refundCovers a slippage cushion the router did not need
Always forwards exactly nativeValuePredictable, no accidental sweep of msg.value
executedTransactions retired in favour of nonces aloneOne replay guard, not two divergent ones
uint256[50] __gapRoom for V3 state without a layout break

Mode 2 — user-owned vaults

CSIP and Autopilot both put funds in a contract and hand a narrow execution right to a platform key. The user's protections are on-chain.

CSIP / DCA vault

Contract: 0x998D4f9A36d196c762C6A646733E63d9eD1b980D on Arbitrum (UUPS, currently at V2.1 implementation 0x930a2b7F1B123B11d402bEfb6E1100f465082aD9).

GuaranteeEnforcement
Only the position owner can withdraw or cancelwithdrawWithPositionId, removePosition check the caller
The executor cannot redirect outputexecuteAutoBuy credits the position's own balance
Credits are the measured balance delta, not a supplied numberV2 change; V1 credited returnTokenValue, which stranded surplus in the contract
Auto-claim pays the user directlyautoClaimFor is onlyOwnerOrExecutor but the payout address is the position owner
Auto-claim escrow is refundabledisableAutoClaim

V1 had a real accounting bug — know it before reading old code

V1's executeAutoBuy credited the executor-supplied returnTokenValue rather than the actual swap output. Because the cron set returnTokenValue = 0.995 × quote as revert protection, every buy leaked 0.5% of price into uncredited surplus sitting in the contract. Worse, one provider returned an approve() step as its transaction data, which the contract dutifully "executed" and phantom-credited.

V2 fixed both: it credits the measured delta (so returnTokenValue is only a floor), and the cron gained a calldata guard that rejects any tx.to == fromToken or data.length <= 68.

Autopilot vault

Contract: 0x689AaEc7E3012d84226e7c5F4DEeFdC01C86d23e on Arbitrum (UUPS, implementation 0x7bF6f14c53BB18BC5D5cd34c9A81c8fb658bB168), plus 0x4dE7CD522f1715b2a48F3ad6612924841d450A0F on Robinhood Chain and 0x7cA5A09E59e8d84e678854184F67D8aF505EfF9f on Arbitrum Sepolia.

The guardrails live in the Limits struct and are checked on every trade:

solidity
struct Limits {
    uint128 maxPerTrade;      // max budgetToken per single buy
    uint128 maxPerDay;        // max budgetToken per rolling 24h
    uint64  expiry;           // executeTrade reverts at/after this
    uint16  maxTradesPerDay;  // 0 = unlimited
}

Plus:

  • allowedTokens per agent — the executor cannot buy something the user did not list.
  • active — an owner-only pause switch the executor must respect.
  • policyHashkeccak256 of the canonical policy JSON the user signed. The engine refuses to act when the DB policy hash differs from the on-chain hash.
  • allowedRouter — an owner-curated allow-list of aggregator routers, fail-safe by construction: with no routers listed, no trade can execute at all.

The router allow-list closed a critical vulnerability

An adversarial audit on 2026-07-25 found C-1 (critical): the executor supplied an arbitrary targetContract plus calldata with no allow-list. A compromised executor could therefore have the vault call anyToken.transfer(attacker, all) and drain every agent's pooled funds. The data.length > 68 guard did not help — it was padding-bypassable.

The fix added setRouter / setRouters, requiring both targetContract and spender to be allow-listed. It was deployed as a UUPS upgrade to the same proxy address, with state preserved.

Residual risk, documented and accepted for launch: with a platform-held executor key, a compromise can still force bad-slippage churn on an agent's own positions through allow-listed routers. The loss is bounded by liquidity, and the funds return to the agent rather than to the attacker. An oracle-based minOut floor would close it and is a candidate for a future upgrade.

What the platform genuinely cannot do

CannotWhy
Move a user's wallet balanceNo approval exists outside a specific trade, and the relayer resets allowances after each call
Withdraw from a CSIP positionwithdrawWithPositionId checks the caller is the owner
Withdraw from an Autopilot vault to itselfwithdraw and withdrawAllAndClose are owner-gated; _externalPayout only ever targets the agent owner
Trade a token the user did not allow-listisTokenAllowed check
Trade past a user's expiry, daily cap or per-trade cap_validateLimits plus the rolling-window counters
Trade against a policy the user did not signOn-chain policyHash versus DB policy hash comparison
Read a user's private keyNever transmitted; wallet signing happens in the browser extension or app

Trusted components

Being honest about the trust surface:

ComponentTrust requiredMitigation
TX_SIGNER keyAttests swap routesRotatable via setSigner; cannot spend other users' funds
PRIVATE_KEY_EXECUTORExecutes CSIP and Autopilot tradesBounded by on-chain limits, allow-lists and the router allow-list
Contract owner EOA 0x2Ed05570214f6C0F7612B580aB37C163076e0162Can UUPS-upgrade the vaults, set fees, curate routersShould be moved to a multisig — this is an open item, see below
bz-backend route-buildingProduces the calldata users signBackend gas-simulates every candidate route and drops reverting ones
Aggregator routersActually perform the swap21 independent providers; a bad route reverts rather than losing funds, because minOut/OutputBelowMinimum guards exist in the vaults
LLM outputSuggests intents and policy draftsThe LLM never originates a trade. Policy drafts are re-validated by validatePolicy server-side, so a hallucination cannot produce a signable policy

Open custody items

  1. Owner EOA → multisig. 0x2Ed0…0162 owns the Autopilot proxy and can upgrade it. Transferring ownership to a multisig is a launch blocker for promoting Autopilot to users.
  2. setRouters must be seeded. The Autopilot vault is fail-safe: until the owner allow-lists the swap-sdk aggregator routers for a chain, no trade can ever execute there. This is true today on Robinhood Chain.
  3. Copy-trading secrets. The external copytrade-agent repo has committed live secrets in config.json (Telegram token, Helius key, Hyperliquid API-wallet private key). Rotating and blanking those is step zero of that workstream.

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