Kaspalytics Learn is a work in progress.
Updated: Aug 2, 2026

KCC20 Fungible Tokens

The KCC20 fungible token covenant — required state prefix, the transfer and delegator interface, the published descriptor, extended state, and the Borrowed Receive extension.

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
FieldRole
owner_identifierIdentifies the owner of this token quantity
identifier_typeSays how to interpret owner_identifier
amountThe token quantity held by this state
extended_state_digestCommits 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_typeowner_identifier holdsSpending requires
0x00a Schnorr public keya matching signature
0x01a P2SH script hashan input in this transaction whose scriptPubKey matches
0x02a Covenant IDan 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 index
  • signatures — authorization signatures corresponding to consumed states
  • witnesses — 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 transpositionnext_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

Inputs (token A)
Input 0 — leader 1000
invokes transfer(...)
owner: alice (pubkey)
transfer
Outputs (token A)
Output 0 400
covenant binding → A
owner: bob
Output 1 600
covenant binding → A
owner: carol

Supply is conserved: 1000 in, 400 + 600 out. Token amount is covenant state and is entirely independent of the outputs' KAS values.

A split. One input, two continuations, one leader doing all validation.
Inputs (token A)
Input 0 — leader 400
invokes transfer(...)
owner: bob
Input 1 — delegator 600
invokes transfer_delegator()
owner: carol
transfer
Outputs (token A)
Output 0 1000
covenant binding → A
owner: dave

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.

A merge. The delegator carries no arguments of its own.

Real encoded state

Here is the state region of an actual KCC20-style covenant compiled with SilverScript, holding 1000 units owned by a pubkey:

KCC20 encoded_state — 46 bytes 46 bytes
push 20 @0
ownerIdentifier 1111111111111111111111111111111111111111111111111111111111111111 @1 · byte[32]
push 01 @33
identifierType 00 @34 · IDENTIFIER_PUBKEY
push 08 @35
amount e803000000000000 @36 · 1000, 8-byte LE
push 01 @44
isMinter 00 @45 · bool — see below
The whole state region sits inside a 1526-byte program. state.start = 1, because this contract has two entrypoints and needs one byte of dispatch-preservation bytecode first.

Spec vs. example. This artifact is the SilverScript KCC20 example contract, which carries a plain isMinter boolean as its fourth field. The KCC20 specification instead requires extended_state_digest: bytes32 in 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 transfer entrypoint 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[]
}
  • prefix and suffix are 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_layout describes the shape of what extended_state_digest commits to.
  • kcc20_extensions lists 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
Inputs
Input 0 — sender 1000
authorized normally
owner: alice
witnesses[0] = 0
Input 1 — borrowed 50
NOT authorized by its owner
owner: bob
witnesses[1] = 0xFF
transfer with borrowed receive
Outputs
Output 0 950
sender change
owner: alice
Output 1 — paired with input 1 100
same owner, more tokens
owner: bob (unchanged)
KAS value ≥ input 1’s

Total tokens preserved: 1050 in, 1050 out. Bob never signed, and could not have been harmed — his balance only went up.

Borrowed receive. Strict positional pairing: input i maps to successor i, always.

For that pair, transfer must enforce every one of:

  • next_states[i] exists;
  • owner_identifier is unchanged;
  • identifier_type is unchanged;
  • extended_state_digest is 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.

References

Kaspalytics strives to provide accurate data - our highest level of effort is given to data validation and maintenance. However, we cannot guarantee 100% accuracy. Data is subject to inaccuracies and change.

Contact | © 2026 Kaspalytics