Simplileap logo

// Technical whitepaper

A Non-Custodial Invoicing Protocol on zkSync Era

Counterfactual per-invoice addresses, exchange-aware reconciliation, and paymaster-sponsored settlement

Document: Technical whitepaper

Version: 1.0

Status: Draft for review

Target chain: zkSync Era (ZK Stack / EraVM)

Scope: Protocol design, on-chain contracts, off-chain reconciliation, security model

← All whitepapers

// Abstract

Abstract

A bare blockchain transfer carries no order ID, no invoice reference, and no business context; the chain records value moving between addresses and nothing else. Every crypto billing system therefore has to solve the same problem — deciding which payment satisfied which invoice — and most solve it badly, with shared addresses and manual reconciliation that collapse under volume and under payments originating from centralized exchanges.

This paper specifies a non-custodial invoicing protocol with three load-bearing components. First, counterfactual per-invoice receiving addresses derived by CREATE2 rather than from an HD-wallet seed: each invoice maps deterministically to a contract address that can be displayed and funded before any contract exists, so unpaid invoices incur zero on-chain cost and matching reduces to an address lookup. Second, an exchange-aware reconciliation engine modelled as an explicit state machine, with a tolerance band that absorbs exchange withdrawal-fee shortfalls, partial/over-payment handling, multi-token normalization with rate locking, and confirmation policy tied to zkSync batch finality. Third, the protocol targets zkSync Era because per-invoice contract deployment is only economically rational on a low-fee rollup, and because zkSync's native account abstraction lets a paymaster sponsor the gas for deploying and sweeping each forwarder — including charging the customer's gas in the token being paid.

We give the exact zkSync CREATE2 derivation (including the keccak256("zksyncCreate2") domain prefix), the IPaymaster and IAccount interface surface with their magic return values, production-oriented contract listings for the forwarder and factory, the off-chain data and API model, a threat model with enumerated mitigations, and a sourced reference list.

TermMeaning
InvoiceAn off-chain record of an amount owed, denominated in a settlement currency, with accepted-token set, FX lock, and expiry.
Deposit addressThe unique, per-invoice receiving address a customer pays into.
ForwarderThe minimal contract that will occupy the deposit address; its sole job is to sweep received funds to the merchant treasury.
CounterfactualA contract address fully determined and usable before deployment, because the address is a pure function of (deployer, salt, code).
TreasuryThe merchant-controlled destination for swept funds.
SweepDeploying the forwarder (if not already deployed) and moving its balance to the treasury.
PaymasterA contract that pays a transaction's gas on behalf of the sender.
EraVMzkSync Era's native virtual machine (not byte-for-byte the EVM).

// Section 1

Introduction and motivation

Blockchains settle transfers between addresses but model none of the surrounding commerce: orders, prices, expirations, refunds, partial payments. A crypto invoicing platform supplies that missing layer — it turns an invoice into a set of constraints a future transaction must satisfy, watches the chain for transactions that satisfy them, and reports the outcome to the merchant's systems. The chain cannot reject an incorrect payment; only the platform can interpret it.

The naive design hands every customer the same address and reconciles by eye. It fails as soon as two open invoices have similar amounts, or one customer pays from an exchange instead of a self-custody wallet. Attribution becomes ambiguous, partial payments have no home, and reconciliation cost grows with volume.

The mature design assigns a unique receiving address per invoice. Any transfer arriving at that address provably belongs to that invoice irrespective of who sent it, whether a memo was attached, or how the amount was rounded. This converts a fuzzy classification problem into a deterministic lookup. The remaining engineering questions — and the subject of this paper — are how those addresses should be generated, how the messy reality of exchange-originated payments is reconciled afterward, and what execution layer makes the economics and UX work.

// Section 2

System model, actors, and assumptions

Actors

  • Merchant: issues invoices; controls the treasury address; receives webhooks.
  • Customer (payer): pays from a self-custody wallet or a centralized exchange.
  • Platform: runs the invoicing API, address engine, chain indexer, reconciliation engine, sweeper, and webhook service. It holds an operational signer that can trigger sweeps but cannot redirect funds away from the immutable treasury.
  • zkSync Era: the settlement environment (sequencer, bootloader, system contracts, L1 settlement).

Trust assumptions

  • The platform is non-custodial: it never holds customer private keys. The only on-chain authority it needs is the ability to deploy forwarders and call their sweep function, which by construction can only move funds to the pre-committed treasury.
  • zkSync Era's sequencer is liveness-trusted but not safety-trusted: validity proofs settled to Ethereum guarantee state correctness; the protocol must distinguish soft (sequencer-acknowledged) confirmation from hard (L1-settled) finality.
  • keccak256 is collision-resistant; address derivation is non-malleable.

Out of scope (deferred to deployment): AML/KYC and Travel-Rule compliance, fiat on/off-ramps, dispute/chargeback policy, and the merchant's downstream accounting. These are organizational, jurisdiction-specific, and orthogonal to the protocol mechanics.

// Section 3

Architecture overview

Components and their responsibilities:

  • Invoicing API — mints invoices; stores settlement currency, accepted tokens, locked FX rate, tolerance, and expiry; returns the deposit address.
  • Address engine — computes counterfactual CREATE2 addresses using the zkSync derivation and, at sweep time, deploys forwarders.
  • Chain indexer — subscribes to the chain and emits an event whenever any known deposit address receives value. Missing an event does not lose funds but loses truth, so the indexer must reconcile against canonical state on restart and run redundantly.
  • Reconciliation engine — drives the invoice state machine.
  • Sweeper — deploys forwarders and forwards balances to treasury, with gas sponsored by a paymaster.
  • Webhook service — signed, idempotent, retried delivery of state changes.
ZKSYNC INVOICING PROTOCOL

Architecture flow

Control plane
Value in motion
Settled
ISSUE · CONTROL PLANESETTLE · VALUE PLANEcreate invoicederivecounterfactualwebhookpaydetectclassifysweepforwardMerchant backendcontrolInvoicing APIcontrolAddress enginecontrolDeposit addresscounterfactualCustomer / ExchangevalueChain indexervalueReconciliation enginevalueSweeper + PaymastervalueTreasuryvalue
Hover any node for its role, or press Run a payment to watch a unit of value travel the settle path.

// Section 4

Per-invoice address derivation

4.1 Baseline: HD-wallet addresses and why we move past them

  • Key custody surface. A single seed (or a hot signer plus xpub) controls every derived address; one compromise is total.
  • Monitoring sprawl. The indexer must track an unbounded, growing set of unrelated addresses.
  • Sweep orchestration. Each derived address is an EOA holding funds independently; consolidating to treasury means one signed transaction per address, and each address must first be funded with the gas token before it can move anything.
  • No programmability. A plain EOA cannot enforce minimums, restrict tokens, auto-forward, or be gas-sponsored.

4.2 Counterfactual CREATE2 forwarders

We assign each invoice a deterministic contract address computed with CREATE2. Funds can be sent to it while no code exists there; the contract is deployed only when we choose to sweep. Per-invoice uniqueness comes entirely from the salt; the implementation bytecode is shared, registered once, and reused.

Salt construction
salt = H( DOMAIN_TAG ‖ chainId ‖ merchantId ‖ invoiceId ‖ schemaVersion )

4.3 zkSync CREATE2 derivation — exact formula

EraVM does not use Ethereum's 0xff-prefixed CREATE2 formula. On EraVM, deployment is keyed on the hash of the bytecode (with the bytecode supplied in the transaction's factoryDeps), and the address is derived with a zkSync-specific domain prefix:

EraVM address derivation
prefix = keccak256("zksyncCreate2")
       = 0x2020dba91b30cc0006188af794c2fb30dd8520db7e2c088b7fc7c103c00ca494

address = keccak256(
              prefix
            ‖ pad32(senderAddress)        // the deployer (factory)
            ‖ salt                         // 32 bytes
            ‖ bytecodeHash                 // zkSync bytecode hash of the contract
            ‖ keccak256(constructorInput)  // hash of constructor calldata
          )[12:]
TypeScript — off-chain prediction (ethers v5)
import { ethers } from "ethers";

export function zkCreate2Address(
  sender: string,        // factory address
  bytecodeHash: string,  // zkSync bytecode hash (NOT keccak256 of EVM initcode)
  salt: string,          // 32-byte hex
  constructorInput: string = "0x"
): string {
  const prefix = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("zksyncCreate2"));
  const inputHash = ethers.utils.keccak256(constructorInput);
  const hash = ethers.utils.keccak256(
    ethers.utils.concat([
      prefix,
      ethers.utils.zeroPad(sender, 32),
      salt,
      bytecodeHash,
      inputHash,
    ])
  );
  return ethers.utils.getAddress("0x" + hash.slice(26)); // low 20 bytes
}

Standard helpers — Clones.predictDeterministicAddress, any hand-rolled 0xff computation, CREATE3 libraries — return the wrong address on native EraVM, and funds sent there can never be reached.

4.4 EraVM deployment model — the clone caveat

  • Native EraVM, single registered implementation (recommended): deploy the forwarder implementation once; per-invoice clones are deployments of the same bytecode via ContractDeployer.create2, distinguished by salt.
  • EVM-interpreter path: zkSync Era's EVM bytecode interpreter reproduces Ethereum-identical create/create2 derivation; standard OpenZeppelin Clones/EIP-1167 work at higher gas cost.
  • Pick one path explicitly and make the address engine's derivation match it byte-for-byte. Mixing the two is exactly how the displayed address silently diverges from where the contract lands.

4.5 Forwarder and factory (reference listings)

Solidity — InvoiceForwarder
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract InvoiceForwarder {
    using SafeERC20 for IERC20;

    address public immutable treasury;
    address public immutable sweeper;

    event Swept(address indexed token, uint256 amount);
    error NotAuthorized();

    constructor(address _treasury, address _sweeper) {
        treasury = _treasury;
        sweeper  = _sweeper;
    }

    modifier onlySweeper() {
        if (msg.sender != sweeper) revert NotAuthorized();
        _;
    }

    function sweep(address[] calldata tokens) external onlySweeper {
        uint256 nativeBal = address(this).balance;
        if (nativeBal > 0) {
            (bool ok, ) = treasury.call{value: nativeBal}("");
            require(ok, "native sweep failed");
            emit Swept(address(0), nativeBal);
        }
        for (uint256 i; i < tokens.length; ++i) {
            uint256 bal = IERC20(tokens[i]).balanceOf(address(this));
            if (bal > 0) {
                IERC20(tokens[i]).safeTransfer(treasury, bal);
                emit Swept(tokens[i], bal);
            }
        }
    }

    receive() external payable {}
}
Solidity — ForwarderFactory
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract ForwarderFactory {
    address public immutable owner;

    event Deployed(bytes32 indexed salt, address forwarder);

    constructor(address _owner) { owner = _owner; }

    function deploy(bytes32 salt, address treasury, address sweeper)
        external
        returns (address forwarder)
    {
        require(msg.sender == owner, "not owner");
        forwarder = address(new InvoiceForwarder{salt: salt}(treasury, sweeper));
        emit Deployed(salt, forwarder);
    }
}

4.6 Why this is strictly better here

Matching keys on the destination address, which is unique per invoice, so attribution is exact regardless of sender or memo — neutralizing the exchange-sender problem at the structural level. Counterfactual addressing means unpaid and expired invoices cost nothing on-chain. No per-invoice private keys exist, shrinking the custody surface to a single immutable treasury. And because the forwarder is a contract, the sweep can be gas-sponsored by a paymaster, eliminating the fund-every-ephemeral-address orchestration that plagues the HD-wallet model.

// Section 5

Reconciliation engine

5.1 The exchange-withdrawal problem, precisely

  • Wrong sender. Funds arrive from the exchange's pooled hot wallet, not the customer's address. Mitigated structurally by per-invoice addresses.
  • Wrong amount. Many exchanges deduct a flat network/withdrawal fee from the requested amount. Mitigated by the tolerance band.
  • Batched withdrawals. Exchanges aggregate many users' withdrawals into one transaction with multiple outputs. Mitigated structurally: each output targets a distinct deposit address.

5.2 Invoice state machine

The engine is an explicit finite-state machine. State is authoritative off-chain; on-chain events are inputs. States include CREATED, PENDING, DETECTED, CONFIRMED, PARTIAL, OVERPAID, SETTLED, EXPIRED, and REFUNDED. Transitions are guarded for exactly-once settlement: re-detection of the same transfer at a deeper confirmation updates depth, never payment count.

5.3 Classification and scoring

Tolerance and classification
τ = max( τ_fixed , τ_pct · E )

classify(transfer, invoice):
    if t ∉ invoice.acceptedTokens:        return WRONG_TOKEN
    if |r − E| ≤ τ:                       return PAID
    if r < E − τ:                         return PARTIAL
    else:                                 return OVERPAID

5.4 FX lock and multi-token

Invoices are priced in a settlement currency. At creation the platform locks a rate with an expiry window, so an in-window payment delivers the billed value and the merchant does not carry intra-window price risk. The accepted-token set lets a customer pay in any supported asset; the received token is normalized to the settlement currency at the locked rate before the band test.

5.5 Confirmation and finality on zkSync

  • Soft (sequencer-acknowledged): fast, sufficient for low-value or reversible fulfilment.
  • Committed / proven / executed on L1: progressively stronger; executed is hard finality backed by a settled validity proof.
  • The engine marks DETECTED on soft acknowledgement and only advances to CONFIRMED/SETTLED per the configured stage.

5.6 Idempotency, webhooks, and delivery

  • Exactly-once: state transitions are guarded; an idempotency key per invoice event lets the merchant dedupe on their side.
  • Signed webhooks: every webhook carries an HMAC signature over the body and a timestamp.
  • Retries: delivery uses bounded retries with exponential backoff; the merchant endpoint must be idempotent.

5.7 Reconciliation loop (pseudocode)

Python
def on_inbound_transfer(ev):
    inv = invoices.by_deposit_address(ev.to)
    if inv is None:
        return quarantine(ev)
    if ev not in inv.seen_transfers:
        inv.seen_transfers.add(ev)
    r = fx.normalize(ev.amount, ev.token, inv.locked_rate)
    grade = classify(ev, inv, r)
    inv.apply(grade, residual_or_surplus(r, inv))
    if inv.state in (CONFIRMED, OVERPAID) and depth(ev) >= inv.policy.depth:
        sweeper.schedule(inv)
    webhooks.emit(inv.signed_event())

// Section 6

Execution layer: zkSync Era

6.1 Why an L2, and why this one

Contract-per-invoice is only rational where deploying and sweeping a contract costs far less than the invoice it serves. On Ethereum L1 a deploy-plus-sweep can exceed a small invoice's value. On a ZK rollup the marginal cost falls to the order of cents. zkSync Era settles validity proofs to Ethereum, inheriting Ethereum security while pushing fees down.

6.2 Native account abstraction

Solidity — IAccount
interface IAccount {
    function validateTransaction(bytes32 txHash, bytes32 suggestedSignedHash, Transaction calldata tx)
        external payable returns (bytes4 magic);
    function executeTransaction(bytes32 txHash, bytes32 suggestedSignedHash, Transaction calldata tx)
        external payable;
    function executeTransactionFromOutside(Transaction calldata tx) external payable;
    function payForTransaction(bytes32 txHash, bytes32 suggestedSignedHash, Transaction calldata tx)
        external payable;
    function prepareForPaymaster(bytes32 txHash, bytes32 possibleSignedHash, Transaction calldata tx)
        external payable;
}

6.3 Paymasters: sponsoring deploy + sweep

Solidity — IPaymaster
interface IPaymaster {
    function validateAndPayForPaymasterTransaction(
        bytes32 txHash,
        bytes32 suggestedSignedHash,
        Transaction calldata tx
    ) external payable returns (bytes4 magic, bytes memory context);

    function postTransaction(
        bytes calldata context,
        Transaction calldata tx,
        bytes32 txHash,
        bytes32 suggestedSignedHash,
        ExecutionResult txResult,
        uint256 maxRefundedGas
    ) external payable;
}

Two standardized input encodings select the flow via the first 4 bytes of paymasterInput: general flow for fully-sponsored sweeps, and approval-based flow to let a payer settle gas in the same stablecoin they are paying with.

6.4 Relevant system contracts

System contractAddress
ContractDeployer0x0000000000000000000000000000000000008006
NonceHolder0x0000000000000000000000000000000000008003
L1Messenger0x0000000000000000000000000000000000008008

// Section 7

Data model and API surface

7.1 Invoice object (off-chain)

JSON
{
  "invoice_id": "inv_01HZX...",
  "merchant_id": "mer_42",
  "settlement_currency": "USD",
  "amount": "129.00",
  "accepted_tokens": ["USDC", "USDT", "ETH"],
  "fx": { "rate": "1.00", "source": "...", "locked_at": "2026-06-17T09:00:00Z", "expires_at": "2026-06-17T09:30:00Z" },
  "tolerance": { "fixed": "1.00", "pct": "0.01" },
  "chain_id": 324,
  "deposit_address": "0x...",
  "state": "PENDING",
  "confirmation_policy": { "stage": "executed" },
  "expires_at": "2026-06-17T10:00:00Z"
}

7.2 Endpoints (illustrative)

MethodPathPurpose
POST/v1/invoicesCreate an invoice; returns the deposit address and locked FX.
GET/v1/invoices/{id}Fetch current state and received transfers.
POST/v1/invoices/{id}/refundInitiate a refund from treasury (merchant-authorized).
GET/v1/invoices/{id}/addressRe-derive / re-display the deposit address.

7.3 Webhook event

JSON
{
  "type": "invoice.paid",
  "invoice_id": "inv_01HZX...",
  "idempotency_key": "evt_...",
  "state": "SETTLED",
  "received": [{ "token": "USDC", "amount": "129.00", "tx_hash": "0x...", "depth": "executed" }],
  "timestamp": "2026-06-17T09:12:03Z",
  "signature": "hmac-sha256=..."
}

// Section 8

Security analysis and threat model

#ThreatVectorMitigation
T1Fund loss via address mismatchDisplaying an address derived with Ethereum's 0xff formula on native EraVMUse the zksyncCreate2 derivation; unit-test prediction against on-chain ContractDeployer.create2.
T2Sweeper key compromiseStolen operational signer redirects fundsTreasury is immutable in the forwarder; sweep can only ever pay the treasury.
T3Paymaster drainingAdversary submits arbitrary transactions for sponsorshipPolicy-gate the paymaster: only the sweep selector, only known forwarders, rate limits and caps.
T4Premature settlement / reorgActing on soft confirmation that later reversesMap invoice finality to the configured zkSync batch stage.
T5Underpayment abuseExploiting the tolerance band to pay lessSize τ_fixed to realistic exchange fees only; route material shortfalls to PARTIAL.
T6Indexer gapMissed transfer leaves state staleRedundant indexers; reconcile against canonical chain state on restart.
T7Webhook forgery / replaySpoofed payment notifications to the merchantHMAC-signed bodies with timestamps and idempotency keys.
T8Cross-tenant address collisionSame invoiceId reused across merchants/chainsDomain-separated salt binding chainId and merchantId.
T9Sweep front-runningAdversary observes a pending sweepSweeps move funds only to the immutable treasury; onlySweeper prevents nuisance triggering.

Custody posture. The protocol is non-custodial in the strong sense: no per-invoice keys exist, and the platform's signer can only trigger movement to a destination fixed in immutable contract state. Compromise of platform infrastructure cannot, by construction, divert funds away from the merchant treasury.

// Section 9

Economic and fee considerations

The dominant on-chain cost is the per-paid-invoice deploy-plus-sweep. Three levers keep it small: (1) the forwarder is a minimal contract sharing one registered implementation; (2) deployments are batched across invoices that became payable in the same window; (3) gas is paid by a paymaster, optionally recovered from the swept amount or in the paid token via the approval-based flow. Because rollup fees track L1 pubdata cost, the platform should expose a configurable economic floor — invoices below a minimum value can default to soft-confirmation settlement or be aggregated.

// Section 10

Limitations and future work

  • EraVM clone semantics. The native path requires care around bytecode-hash deployment and factoryDeps; teams wanting drop-in EIP-1167 should use the EVM-interpreter path and accept higher gas.
  • FX oracle trust. Rate locking introduces dependence on a price source; production needs redundant feeds and staleness handling.
  • Multi-chain. The forwarder pattern generalizes to other rollups, but each has its own address-derivation quirks.
  • Recurring billing. Subscriptions can reuse a per-customer standing address with running-balance reconciliation.
  • Privacy. Per-invoice addresses leak a payment graph; future work could explore stealth-address-style derivation.

// Section 11

Conclusion

Attribution in crypto invoicing should be structural, not heuristic. Giving every invoice its own address turns matching into a lookup that survives missing memos, exchange-pooled senders, and batched withdrawals. Smart contracts make those addresses better than HD-derived ones: counterfactual CREATE2 forwarders are free until paid, hold no per-invoice keys, and are programmable enough to sweep themselves under paymaster sponsorship. A state-machine reconciliation engine absorbs the messy reality of exchange payments — fee shortfalls, partials, overpayments, FX, and multi-stage finality. And zkSync Era supplies the missing economics and UX, provided its one sharp edge — a CREATE2 derivation distinct from Ethereum's, prefixed by keccak256("zksyncCreate2") — is handled with the precision it demands.