Zerochord
How it works

Transaction tags

The CIP-20 message every Zerochord transaction carries, why the project name is enforced, and how to read a tag back.

Every transaction Zerochord emits carries a CIP-20 transaction message. That message is the only public attribution there is: an auditor with nothing but chain data identifies a Zerochord transaction by the project name in its metadata, and by nothing else.

CIP-20 in brief

CIP-20 is Active. The schema is:

{ "674": { "msg": ["...", "..."] } }

msg is an array of at least one string, and each string is at most 64 bytes when encoded as UTF-8. Sixty four bytes, not characters: a multi byte character costs more than one.

The label is a configuration value under tagging.label rather than a constant in code.

The lines Zerochord writes

buildSwapTagLines produces three lines:

<projectName> <action>
<assetIn>-><assetOut>
route:<routeId>

The actions in use are swap, order, cancel, and the settlement actions. The route identifier ties a transaction back to the quote that produced it, and the applications build it from the block height the quote was read at and the minimum output it carried.

Every line is length checked, and a line over the limit throws rather than being truncated. A truncated message on chain is invisible; a failed build is not.

splitIntoCip20Lines exists for a long message, and splits on UTF-8 boundaries so it never cuts a character in half.

Two rules, enforced where every builder passes

attachProductTag in packages/tx/src/tagging.ts is the only path a builder uses to attach metadata. It enforces both rules:

The message is valid CIP-20

At least one line, and every line at most 64 UTF-8 bytes. buildCip20Metadata throws Cip20Error otherwise.

Some line carries the project name

if (!tagLines.some((line) => line.startsWith(projectName))) {
  throw new ProductTagError(
    `no CIP-20 tag line starts with the project name ...`,
  );
}

A tag without the project name is attached, valid, and useless. An earlier build omitted the name and the audit found zero swaps over a chain that held several. The check lives here, where every builder passes, rather than at each call site.

Reading a tag back

The tag is only worth anything if it survives serialisation, so it is read back out of the finished transaction bytes rather than trusted from the builder.

readProductTagFromTxCbor(cborHex, label): string[] | null
requireProductTag(cborHex, network): string[]

readProductTagFromTxCbor decodes the transaction's auxiliary data and returns the message lines, or null when there is none under the label. It is the same thing an auditor does from the chain, run against the transaction before it is signed.

requireProductTag asserts and throws. Call it on anything about to be signed: it is the last point at which an untagged transaction can still be stopped. The command line prints its result next to the built transaction.

The project name is immutable on chain

tagging.projectName is cardano-swap-router in both shipped configurations.

Every transaction already recorded carries that exact string. cardano-swap audit, the footprint declaration and the Dune queries all select on it, so changing it would orphan every transaction the project has made.

One historical transaction proves why the rule exists

Preprod transaction 93fc0ff3ca526534d355950c21e33cbedc7229e75f76c9d05a525c1143a88173 carries label 674 with the message ["fUSDM->fUSDA", "min:5906126"] and no line naming the project.

It is a real product swap: it spends a UTxO at the declared pool script, and its other input and its change belong to the declared operator wallet. So it is tagged, the tag is valid CIP-20, and the tag is useless, because attribution from public data is the project name and nothing else.

A settled transaction's metadata is immutable, so it cannot be repaired. It cannot recur, because attachProductTag now refuses a tag whose lines do not name the project.

cardano-swap audit reports it in a section of its own, under tagged, not attributable, rather than dropping it. See Reproducing the saving claim.

Surfacing tagged activity

dune/ holds SQL queries over cardano.transaction_metadata and related tables that surface the tagged activity. Their totals are gross.

Dune publishes a single cardano schema and it is mainnet, so those queries return rows for mainnet activity only.

On this page