KCC20 is where the whole stack shows up at once. It defines the covenant surface a fungible token must expose so it can be recognized, transferred, and composed with other covenants.
Note the framing: KCC20 is not a token implementation. It is the minimum agreement that lets independently written token covenants interoperate with shared tooling. Everything above the line is left to the individual token.
Prerequisites: State, Templates & Continuations and Leaders & Delegators.
Required state prefix
Every KCC20 covenant’s state must begin with these four fields, in this order:
owner_identifier: bytes32
identifier_type: byte
amount: integer
extended_state_digest: bytes32 | Field | Role |
|---|---|
owner_identifier | Identifies the owner of this token quantity |
identifier_type | Says how to interpret owner_identifier |
amount | The token quantity held by this state |
extended_state_digest | Commits to token-specific extended state |
“Must begin with” is the important phrase. A token may add fields after these; it may not reorder or omit them. That fixed prefix is what lets generic tooling decode any KCC20 token’s balance and owner without knowing anything else about it.
The defined identifier types:
IDENTIFIER_PUBKEY = 0x00
IDENTIFIER_SCRIPT_HASH = 0x01
IDENTIFIER_COVENANT_ID = 0x02 This is precisely the selected profile binding pattern — one byte of state choosing how another field is interpreted.
Three ways to own a token
The three modes are what make KCC20 more expressive than a pubkey-only token model:
identifier_type | owner_identifier holds | Spending requires |
|---|---|---|
0x00 | a Schnorr public key | a matching signature |
0x01 | a P2SH script hash | an input in this transaction whose scriptPubKey matches |
0x02 | a Covenant ID | an input in this transaction whose Covenant ID matches |
The second and third are the interesting ones, and they work the way they do because of a hard constraint: Kaspa script has no eval. One covenant cannot execute another’s code by reference.
So “this token is owned by that contract” cannot be an abstract pointer that gets dereferenced. Instead the proof is structural:
The spending transaction must actually spend a UTXO belonging to that owner, and the token covenant checks that one of the supplied witness inputs matches.
Ownership is demonstrated by co-spending, not by evaluation. This is the practical form of inter-covenant communication on Kaspa, and it is why covenant-id ownership matters so much — it gives one covenant a concrete, transaction-level way to exercise control over another’s state.
The transfer interface
A KCC20 covenant must expose two entrypoints:
transfer(
State[] next_states,
sig[] signatures,
byte[] witnesses
)
transfer_delegator() This is the leader/delegator split applied directly. transfer is invoked by the first KCC20 covenant input as the leader entrypoint; every other KCC20 input invokes transfer_delegator with no input data to join the transfer the leader declared.
The leader’s three arguments:
next_states— the states created by the transfer, ordered by covenant output indexsignatures— authorization signatures corresponding to consumed stateswitnesses— per-input authorization metadata required for consumed states
All three are parallel arrays indexed by consumed state position, and each is a single lowered element per the array-of-records transposition — next_states becomes one stack element per State field, not one per record.
witnesses exists so the contract can jump straight to the relevant transaction input for script-hash and covenant-ID ownership, instead of scanning every input to work out which one authorizes each consumed state.
The leader derives the previous token states from the consumed inputs, validates authorization and amount preservation, then verifies the successor outputs.
What a transfer looks like
Supply is conserved: 1000 in, 400 + 600 out. Token amount is covenant state and is entirely independent of the outputs' KAS values.
Both inputs must authorize. Input 1 supplies its signature via the leader's parallel arrays, but its own script only checks that it is not at position 0.
Real encoded state
Here is the state region of an actual KCC20-style covenant compiled with SilverScript, holding 1000 units owned by a pubkey:
Spec vs. example. This artifact is the SilverScript KCC20 example contract, which carries a plain
isMinterboolean as its fourth field. The KCC20 specification instead requiresextended_state_digest: bytes32in that position, with token-specific flags moved behind the digest. The spec’s own illustration of an extended state layout is, fittingly,{ color: byte, is_minter: boolean }. Treat the example as the shape of a KCC20-style token, not as a conformant encoding of the current draft.
Extended state
The fourth field is where KCC20 stays open-ended without becoming un-decodable. A token may extend the base state through extended_state_digest, a commitment to token-specific state:
extended_state_digest = blake2b(encode(extended_state)) The descriptor’s extended_state_layout defines the canonical encoding of what is committed. For example:
ExtendedStateLayout {
color: byte
is_minter: boolean
} The rule that makes this safe:
The KCC20
transferentrypoint treats extended state as opaque and must preserve its digest unchanged.
And when several inputs are consolidated into one successor, they must all have the same extended_state_digest — you cannot merge tokens whose token-specific state disagrees.
Token-specific transitions (minting, recoloring, whatever the token defines) may update the extended state and store the new digest. Those are separate entrypoints; transfer never touches it.
This is the virtual element pattern: a fixed-size commitment in state, the full value supplied as an opening only when an entrypoint actually needs it. Generic transfer tooling never needs the opening at all — it only needs to copy 32 bytes forward.
The descriptor
Because an unspent P2SH output reveals only a hash, a KCC20 token is undiscoverable without published metadata. Every KCC20 covenant must provide:
KCC20Descriptor {
prefix: bytes
suffix: bytes
extended_state_layout: ExtendedStateLayout | none
kcc20_extensions: ExtensionId[]
} prefixandsuffixare the covenant script bytes before and after its mutable state — the template. Together they identify the token: they are the spending rules applied to a KCC20 state.extended_state_layoutdescribes the shape of whatextended_state_digestcommits to.kcc20_extensionslists the standardized behavioral extensions the covenant implements. It may be omitted when there are none.
The descriptor must be published so tooling can identify the covenant, decode its state, reconstruct its outputs, and determine which extensions it supports.
The template is the token’s identity, in the strongest sense available: two UTXOs with the same prefix and suffix are the same token in different states, and a UTXO with a different template is a different token no matter how similar it looks.
Extensions
A KCC20 extension defines optional standardized behavior on top of the base state and transfer interface. It is distinct from token-specific extended state: extensions are standardized and declared by versioned ExtensionId; extended state is private to the token.
Borrowed Receive v1
Extension ID: kcc20_borrowed_receive_v1 The problem it solves is an ordinary UX annoyance. Receiving tokens normally means creating a new UTXO and funding it with KAS — even when the recipient already holds a compatible token UTXO. Over time a recipient accumulates dust-funded UTXOs, and every sender pays to create one.
Borrowed Receive lets the sender use the recipient’s existing UTXO as the destination. It is borrowed for the transaction and recreated with a higher token amount, without the recipient’s authorization.
A borrowed receive input is flagged by a reserved witness value:
BORROWED_RECEIVE = 0xFF When witnesses[i] == BORROWED_RECEIVE, the consumed state at position i is paired with the successor state at the same position:
borrowed input i -> successor state i Total tokens preserved: 1050 in, 1050 out. Bob never signed, and could not have been harmed — his balance only went up.
For that pair, transfer must enforce every one of:
next_states[i]exists;owner_identifieris unchanged;identifier_typeis unchanged;extended_state_digestis unchanged;next_states[i].amount > prev_states[i].amount; and- the successor output’s KAS value is greater than or equal to the consumed input’s KAS value.
Together these are what make an unauthorized spend of someone else’s UTXO safe. Every dimension in which the owner could be harmed is pinned: they keep ownership, they keep their extended state, their token balance must strictly increase, and their KAS cannot be skimmed.
The exemption is narrow and worth stating exactly:
The borrowed input is exempt only from its normal owner authorization.
signatures[i]remains positionally present but is not verified for that input.
Everything else still applies. The increase must be funded by other authorized token inputs, and the transfer must still preserve the total token amount — borrowed receive moves tokens, it never creates them.
Finally, strict positional pairing keeps the recipient state one-to-one: borrowed inputs cannot be merged, and cannot target the same successor state. Without that rule, two borrowed inputs could be collapsed into one output and the difference silently pocketed.
Composition: a controller covenant
The last idea KCC20 enables is that a token’s issuance policy need not live in the token contract at all.
Because a KCC20 branch can be owned by a Covenant ID, a separate controller covenant can hold that ownership and decide when supply may grow. The two contracts stay separate and each validates its own side of the transaction:
- the token contract answers “what counts as a valid token transition?”
- the controller answers “under what policy may new tokens be issued?”
The SilverScript examples work this through end to end: a minter covenant is created first and receives Covenant ID C; an asset genesis transaction creates the token with Covenant ID A while C records A in its own state; later mint transactions spend both together, with the minter validating the token’s template hash against a pinned expectation before allowing issuance and decrementing its remaining allowance.
That template check is the load-bearing part, and it is the authentication requirement from earlier in this series showing up in production form. The minter does not merely inspect a KCC20-looking output — it extracts the template bytes from the co-spent token input, verifies them against its expected template hash, and reuses those authenticated bytes to reconstruct the outputs it will accept.