Appearance
Contracts & upgrades
All three Blazpay vault families are UUPS-upgradeable, which means a bad upgrade can corrupt live user positions irrecoverably. Storage-layout validation is not optional.
Working directory: bz-backend/contract-deployment/.
Hardhat networks
26 networks defined in hardhat.config.js, covering every chain with a relayer plus a few without.
Arbitrum has two definitions with different signers
| Network | Signer | Use for |
|---|---|---|
arbitrum | BACKEND_PRIVATE_KEY — 0x2Ed0…0162 | Autopilot (its owner) |
arbitrum_dca_owner | DCA_OWNER_KEY — 0x5935…0459 | DCA vault (its owner is the executor) |
Same chain id, different signer. An onlyOwner call from the wrong one simply reverts, so pick deliberately.
Deploying
BlazpayRelayer
bash
npx hardhat run scripts/deployBlazpayRelayer.js --network mychain
# or, for many chains:
npx hardhat run scripts/deployAllRelayers.jsRecords into scripts/blazpayRelayer.deployments.json. Then configure:
bash
node ../scripts/enableRelayerFees.js # sets inPercentFee = 10 (0.10%)
node ../scripts/checkRelayerDeployed.js # verify presence
node ../scripts/check-contract-owner.js # verify signer == TX_SIGNER
node ../scripts/check-contract-state.js # fee config, pause stateShared proxy addresses across chains are expected
Twelve chains share 0x7d98E5… because the same TX_SIGNER EOA deployed at the same nonce and deterministic CREATE produces the same address. Abstract differs (zkSync-stack CREATE), Unichain differs (the deployer's nonce had drifted). See contract addresses.
bz-backend/scripts/RELAYER_DEPLOYMENT.md documents the process in more detail.
Autopilot vault
bash
npx hardhat run scripts/deploy-autopilot.cjs --network mychain
npx hardhat run scripts/estimate-autopilot-gas.cjs --network mychain # cost previewRecords into scripts/autopilot.deployments.json. Deploy cost on Arbitrum was 0.0000659 ETH.
A freshly deployed Autopilot vault cannot trade
It is fail-safe: with an empty allowedRouter map, every trade reverts RouterNotAllowed. Two steps are mandatory:
bash
node scripts/probe-autopilot-routers.cjs # which routers deliver IN-TX output?
node scripts/seed-autopilot-routers.cjs # setRouters([...], true)The probe exists because many aggregators on newer chains are solver/intent based and deliver no in-transaction output. Robinhood Chain (4663) is deployed and still has no routers allow-listed.
Upgrading
The one rule
Validate the storage layout before every upgrade
bash
npx hardhat run scripts/validate-dca-upgrade.cjs --network arbitrum_dca_owner
npx hardhat run scripts/validate-autopilot-upgrade.cjs --network arbitrum
npx hardhat run scripts/simulate-upgrade.js --network <net>These wrap OpenZeppelin's upgrades.validateUpgrade(V1 → V2). A layout break on a live vault does not fail loudly — it reinterprets existing storage slots, silently corrupting every user's position with no way back.
The DCA V1 → V2 upgrade was formally validated this way and state was preserved on mainnet. Do the same every time.
Layout rules
| Rule | Why |
|---|---|
| Append only. Never reorder, never remove, never change a type | Slots are positional |
| Keep deprecated variables as placeholders | The relayer keeps executedTransactions purely for layout compatibility on chains upgraded from v1 |
| Use a gap for future additions | The relayer reserves uint256[50] __gap |
| Comment the boundary | DCA V2's source marks // ───── V1 storage (do not touch, slots 0–7) ───── |
Procedure
bash
cd bz-backend/contract-deployment
# 1. compile
npx hardhat compile
# 2. test — Autopilot must stay at 61 passing
npx hardhat test test/autopilot.test.cjs
# 3. VALIDATE THE LAYOUT
npx hardhat run scripts/validate-autopilot-upgrade.cjs --network arbitrum
# 4. simulate
npx hardhat run scripts/simulate-upgrade.js --network arbitrum
# 5. upgrade
npx hardhat run scripts/upgrade-autopilot.cjs --network arbitrum
# DCA: scripts/upgrade-dca-v2.cjs --network arbitrum_dca_owner
# Relayer: scripts/upgradeBlazpayRelayer.js / upgradeBlazpayRelayerRaw.js
# 6. verify on the explorer
npx hardhat verify --network arbitrum <newImplAddress>
# 7. RE-EXPORT THE ABIs TO BOTH REPOSStep 7 is the one people forget
The DCA and Autopilot ABIs are duplicated by hand in two repos
bz-backend/utils/abi/autopilotAbi.json → defi-dex/src/constants/abis/autopilot.json
bz-backend/utils/abi/dcaAbi.json → defi-dex/src/constants/abis/dca.jsonUpgrading without refreshing both produces a silent mismatch: a new function is simply invisible, and a changed signature reverts with no useful error. The Autopilot ABI should have 39 functions — check the count after exporting.
bash
npx hardhat compile
node -e "
const a=require('./artifacts/contracts/autopilot/BlazpayAutopilotV1.sol/BlazpayAutopilotV1.json');
require('fs').writeFileSync('../utils/abi/autopilotAbi.json', JSON.stringify(a.abi,null,2));
console.log('functions:', a.abi.filter(x=>x.type==='function').length);
"
# then copy the same JSON into defi-dex/src/constants/abis/autopilot.jsonThe relayer ABI lives only in swap-sdk
Because both consumers reach the relayer through RelayerFactory, there is one copy (swap-sdk/src/utils/jsons/relayerAbi.ts). A relayer ABI change is therefore an SDK release, not a two-repo copy.
Upgrade history
Worth knowing, because it explains the current shape of the code.
BlazpayRelayer v1 → v2
All 22 chains upgraded in place, proxy addresses unchanged. Nine changes, all storage-compatible: non-zero signer requirement, setSigner, SafeERC20/forceApprove, allowance reset, partial-fill token refund, native overpayment refund, exact nativeValue forwarding, executedTransactions retired, __gap appended. See relayer.
DCA V1 → V2 (2026-07-03)
New implementation 0xC804e3adA5d25FF4555f3B9f79fC035357f6bDF1.
V1's source was not in the repo
It had to be fetched from Arbiscan and is now vendored at contracts/dca/BlazpayDcaV1.sol. Vendor the source of anything you deploy — an upgrade cannot be validated against a contract you do not have.
Fixed the phantom-credit bug (credit the measured delta, not the executor-supplied number), added auto-claim, and fixed four V1 bugs. Layout formally validated; state preserved.
DCA V2 → V2.1
Implementation 0x930a2b7F1B123B11d402bEfb6E1100f465082aD9. CreatePositionAutoBuy treats excess msg.value as auto-claim escrow — create-plus-opt-in in one signature.
Autopilot V1 → V1 hardened (2026-07-25)
This upgrade fixed a critical drain vector
An adversarial audit found C-1: the executor supplied an arbitrary targetContract plus calldata with no allow-list, so a compromised executor could make the vault call anyToken.transfer(attacker, all) and drain every agent's pooled funds. The data.length > 68 guard was padding-bypassable.
The fix added the allowedRouter allow-list, plus M-1 (debit measured spend) and M-2 (resilient close). Deployed to the same proxy with state preserved; new implementation 0x7bF6f14c53BB18BC5D5cd34c9A81c8fb658bB168, Arbiscan-verified. Tests went to 61, including an explicit drain-block case.
Verification
bash
npx hardhat verify --network <net> <address>Keys: ETHERSCAN_API_KEY (v2 unified, covers the majors), plus ARBISCAN_API_KEY, BSCSCAN_API_KEY, POLYGONSCAN_API_KEY, BASESCAN_API_KEY, OPSCAN_API_KEY.
Verify both the proxy and the implementation. DCA V2 and V2.1 are on Arbiscan and Sourcify.
Security review
What has and has not been reviewed:
| Contract | Slither | Adversarial audit | Tests |
|---|---|---|---|
BlazpayAutopilotV1 | Clean (false positives only) | Yes — found C-1 critical | 61 |
DCAInvestmentV2 | — | No | None in repo |
BlazpayRelayer v2 | — | No — hardening pass only | None in repo |
The relayer is the largest unreviewed surface
It handles every interactive trade on 22 chains, has no test suite in the repo, and has not had an adversarial review. Autopilot's review found a critical issue; there is no reason to assume this one is cleaner. This is the highest-value security work available.
Post-deploy checklist
□ Proxy and implementation verified on the explorer
□ Deployment record updated (blazpayRelayer.deployments.json / autopilot.deployments.json)
□ Storage layout validated (before, not after)
□ Tests passing at the expected count
□ ABIs re-exported to BOTH repos; function count checked
□ Relayer: signer == TX_SIGNER, inPercentFee == 10, enableFees == true
□ Autopilot: setRouters called, feeBps == 20, executor set
□ DCA: executor set, autoClaimFeePerClaim set
□ swap-sdk RELAYER_ADDRESSES + RELAYER_DEPLOYED_CHAINS updated, released, both consumers bumped
□ A small real transaction succeeds end-to-end
□ Fees accruing (/defi/fees/summary?force=1)Emergency levers
| Lever | Contract | Access |
|---|---|---|
pause() | All three | Owner |
setSigner(address) | Relayer | Owner — use immediately on suspected key leak |
setExecutor(address) | DCA, Autopilot | Owner |
setRouters([...], false) | Autopilot | Owner — revokes all routing, stopping every agent |
setActive(agentId, false) | Autopilot | Agent owner, not platform |
withdrawEth / withdrawToken | All three | Owner — used for the CSIP stranded-funds recovery |
Owner keys are EOAs, not multisigs
0x75a8b522… (relayer, 22 chains) and 0x2Ed05570… (Autopilot) both hold _authorizeUpgrade. Moving them to multisigs is an open launch blocker. See secrets.