Skip to content

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

NetworkSignerUse for
arbitrumBACKEND_PRIVATE_KEY0x2Ed0…0162Autopilot (its owner)
arbitrum_dca_ownerDCA_OWNER_KEY0x5935…0459DCA 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.js

Records 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 state

Shared 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 preview

Records 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

RuleWhy
Append only. Never reorder, never remove, never change a typeSlots are positional
Keep deprecated variables as placeholdersThe relayer keeps executedTransactions purely for layout compatibility on chains upgraded from v1
Use a gap for future additionsThe relayer reserves uint256[50] __gap
Comment the boundaryDCA 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 REPOS

Step 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.json

Upgrading 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.json

The 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:

ContractSlitherAdversarial auditTests
BlazpayAutopilotV1Clean (false positives only)Yes — found C-1 critical61
DCAInvestmentV2NoNone in repo
BlazpayRelayer v2No — hardening pass onlyNone 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

LeverContractAccess
pause()All threeOwner
setSigner(address)RelayerOwner — use immediately on suspected key leak
setExecutor(address)DCA, AutopilotOwner
setRouters([...], false)AutopilotOwner — revokes all routing, stopping every agent
setActive(agentId, false)AutopilotAgent owner, not platform
withdrawEth / withdrawTokenAll threeOwner — 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.

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