Appearance
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
| Mode | Used by | Who holds the funds | What the platform can do |
|---|---|---|---|
| User-signed, atomic | Swap, bridge, limit orders, approvals, manual claims | The user's wallet, until the moment of the trade | Nothing without the user's signature on that exact transaction |
| User-owned vault, bot-executed | CSIP, Autopilot | A vault contract, credited to the user's address | Execute trades within on-chain limits. Cannot withdraw to itself |
| Third-party custody | Fiat on/off-ramp, perpetuals | The 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
targetContractand 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 bytransferFrom. 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:
| Change | Why |
|---|---|
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 forceApprove | Non-conforming tokens (USDT and friends) fail loudly instead of silently |
Allowance reset to 0 after the external call | No residual approval to drain later |
| Token refund of anything the recipient did not pull | Covers partial fills |
| Native overpayment refund | Covers a slippage cushion the router did not need |
Always forwards exactly nativeValue | Predictable, no accidental sweep of msg.value |
executedTransactions retired in favour of nonces alone | One replay guard, not two divergent ones |
uint256[50] __gap | Room 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).
| Guarantee | Enforcement |
|---|---|
| Only the position owner can withdraw or cancel | withdrawWithPositionId, removePosition check the caller |
| The executor cannot redirect output | executeAutoBuy credits the position's own balance |
| Credits are the measured balance delta, not a supplied number | V2 change; V1 credited returnTokenValue, which stranded surplus in the contract |
| Auto-claim pays the user directly | autoClaimFor is onlyOwnerOrExecutor but the payout address is the position owner |
| Auto-claim escrow is refundable | disableAutoClaim |
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:
allowedTokensper agent — the executor cannot buy something the user did not list.active— an owner-only pause switch the executor must respect.policyHash—keccak256of 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
| Cannot | Why |
|---|---|
| Move a user's wallet balance | No approval exists outside a specific trade, and the relayer resets allowances after each call |
| Withdraw from a CSIP position | withdrawWithPositionId checks the caller is the owner |
| Withdraw from an Autopilot vault to itself | withdraw and withdrawAllAndClose are owner-gated; _externalPayout only ever targets the agent owner |
| Trade a token the user did not allow-list | isTokenAllowed 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 sign | On-chain policyHash versus DB policy hash comparison |
| Read a user's private key | Never transmitted; wallet signing happens in the browser extension or app |
Trusted components
Being honest about the trust surface:
| Component | Trust required | Mitigation |
|---|---|---|
TX_SIGNER key | Attests swap routes | Rotatable via setSigner; cannot spend other users' funds |
PRIVATE_KEY_EXECUTOR | Executes CSIP and Autopilot trades | Bounded by on-chain limits, allow-lists and the router allow-list |
Contract owner EOA 0x2Ed05570214f6C0F7612B580aB37C163076e0162 | Can UUPS-upgrade the vaults, set fees, curate routers | Should be moved to a multisig — this is an open item, see below |
bz-backend route-building | Produces the calldata users sign | Backend gas-simulates every candidate route and drops reverting ones |
| Aggregator routers | Actually perform the swap | 21 independent providers; a bad route reverts rather than losing funds, because minOut/OutputBelowMinimum guards exist in the vaults |
| LLM output | Suggests intents and policy drafts | The 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
- Owner EOA → multisig.
0x2Ed0…0162owns the Autopilot proxy and can upgrade it. Transferring ownership to a multisig is a launch blocker for promoting Autopilot to users. setRoutersmust 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.- Copy-trading secrets. The external
copytrade-agentrepo has committed live secrets inconfig.json(Telegram token, Helius key, Hyperliquid API-wallet private key). Rotating and blanking those is step zero of that workstream.