The Toccata HF enhanced Kaspa’s UTXOs to preserve covenant lineages, creating a path for native assets.
At this early stage of the ecosystem, there is a need to establish shared covenant standards to ensure interoperability, consistency, and long-term compatibility across tools and implementations.
A solid standard should define the minimal identity and interaction surface of a token covenant. It should make it possible to:
- identify token UTXOs
- decode token state transitions
- construct valid transfer transactions
In this draft below, written with the help of Michael, IzioDev, Alex, and Ori, I propose a minimal specification interface that enables recognition, tracking, and interaction with token covenants, while remaining extendable so that additional features can be layered on without breaking compatibility.
KCC20 Specification
KCC20 Interface
A KCC20 token is a covenant instance that implements a minimal fungible-token interface.
To be KCC20-compatible, a covenant must:
- Maintain basic token state:
- who is the owner of a token quantity
- the amount of quantity they own
- Provide a transfer entrypoint that follows the KCC20 transfer convention.
KCC20 interaction can be reduced to two responsibilities:
- determine current KCC20 state
- create valid next-state transactions
This naturally separates into two roles.
Reader
A Reader observes accepted Kaspa transactions and projects KCC20 state.
Given accepted transactions and token descriptors, it identifies KCC20 activity, decodes token state, and maintains the live KCC20 UTXO set.
Writer
A Writer consumes Reader-provided state and user intent.
Given live KCC20 UTXOs, decoded token state, and token descriptors, it constructs valid KCC20 transactions.
Token Descriptor
Each known KCC20 token covenant is described by a descriptor artifact.
The descriptor is defined per token covenant id and provides the minimal information required to identify token UTXOs, decode their state, construct covenant outputs, and build valid transfer input sigscripts.
TokenDescriptor {
prefix
suffix
state_layout
leader_entrypoint_selector
delegator_entrypoint_selector
}
prefix and suffix are the script bytes before and after the encoded token state.
A Reader uses them to verify that decoded states match actual output locking scripts.
A Writer uses them to reconstruct valid token outputs.
state_layout defines how raw state bytes are decoded and encoded.
It must include the standard KCC20 state header fields:
owner_identifieridentifier_typeamount
A Reader uses state_layout to decode accepted state transitions.
A Writer uses state_layout to encode previous and successor states.
leader_entrypoint_selector and delegator_entrypoint_selector are used to build the input sigscripts for a transfer transaction.
Reader Operation
A Reader owns a descriptor artifact for each known KCC20 token covenant.
It tracks accepted Kaspa transactions and, for each transaction with inputs matching registered token descriptors:
- Identifies the covenant input leader, by convention the first covenant input.
- Extracts the declared raw
next_statesbyte array from the leader input sigscript. - Decodes raw
next_statesbytes into an array of states usingstate_layout. - Verifies each decoded next state against the matching transaction output.
- Updates the live KCC20 UTXO set only after verification succeeds.
Note: The transfer convention expects a verification-mode covenant declaration. A transfer entrypoint should have a known next-state shape, where the Writer provides the intended next states in the sigscript, and the entrypoint verifies those states instead of calculating them at runtime.
The Reader must not trust declared next_states, and for each decoded next state, it reconstructs the expected output script and matches it against the actual output script.
decoded_next_states = decode(next_states_raw, state_layout)
for index, next_state in enumerate(decoded_next_states):
encoded_state = encode(next_state, state_layout)
expected_output_p2sh =
P2SH(prefix || encoded_state || suffix)
output_index =
cov_output_index(index)
output_p2sh =
outputs[output_index].spk
assert output_p2sh == expected_output_p2sh
The Reader updates its token UTXO index based on verified state transitions.
Writer Operation
A Writer queries a Reader for up-to-date token state and descriptors, then creates a valid transaction according to user intent.
The Writer:
- Fetches relevant owner token UTXOs from the Reader.
- Verifies the Reader’s indexed decoded state by matching each input
spkagainstP2SH(prefix || encoded_state || suffix). - Calculates the state transition and produces
prev_statesandnext_states. - Creates the sigscript for the leader input.
redeem_script =
prefix || encode(prev_states[0], state_layout) || suffix
builder.append(redeem_script)
builder.append(leader_entrypoint_selector)
for arg in transfer_arguments:
builder.append(arg)
The leader transfer arguments include the declared transition data:
next_statesauthorization_data
The Writer then creates sigscripts for the other delegating inputs:
for prev_state in prev_states[1:]:
redeem_script =
prefix || encode(prev_state, state_layout) || suffix
builder.append(redeem_script)
builder.append(delegator_entrypoint_selector)
The Writer sets output scripts according to next_states:
for index, next_state in enumerate(next_states):
encoded_state =
encode(next_state, state_layout)
outputs[cov_output_index(index)].spk =
P2SH(prefix || encoded_state || suffix)
Finally, the Writer lets the user sign the transaction and broadcasts it to a Kaspa node.
Extension State
A KCC20 token state may extend the standard KCC20 state header with token-specific state.
encoded_state =
encoded_kcc20_state || extension_state_bytes
Generic Readers are only required to understand the standard KCC20 header, and generic Writers are only required to understand the KCC20 transfer convention.
If inputs have different extension state, the Writer must fail instead of deciding how to combine custom state.
Additional Notes
Explorers should provide a service that lets users reveal token genesis transactions and expose the pre-compiled Silver logic. This allows deployed token covenants to be matched against visible rules, verified, and followed through state transitions over time.
A few open questions that would benefit from further discussion:
- How should we SDK conventions to support covenant interoperability?
- How should the standard address stablecoin-oriented features such as mint, burn, freeze, and pause?
Personally, to me this feels like a great moment, the time for the community and ecosystem to meet, collaborate, partner up and together forge the first covenant convention on top of Kaspa.
I would appreciate very much any feedback, ideas, suggestions and comments.
Edit -
I am adding two additional follow up drafts.
The first is a practical SilverScript interface shape, aimed be pragmatic and concise for developers.
The second is an extension proposal for borrowing. Addressing KIP9's storage mass restriction to allow receiving tokens into existing token UTXOs.
Any feedback is welcomed!
KCC20 SilverScript Interface
KCC20 Interface
A KCC20 token is a SilverScript covenant that exposes a standard token state shape and a standard transfer method shape, enabling wallets, apps, SDKs, and indexers to reliably reconstruct, interpret, and interact with token UTXOs.
Specification
A KCC20-compatible SilverScript contract must:
- maintain the standard KCC20 state header
- define a transfer method following the KCC20 transfer convention
- provide a token descriptor artifact
Compatibility is defined by the contract interface shape and descriptor, not by source-level function names.
Token State
Every KCC20 token state begins with the standard KCC20 state header:
contract KCC20Token() {
byte[32] ownerIdentifier
byte identifierType
int amount
}
Token contracts may extend their state beyond the standard header.
ownerIdentifier is interpreted according to identifierType.
IDENTIFIER_PUBKEY = 0x00
IDENTIFIER_SCRIPT_HASH = 0x01
IDENTIFIER_COVENANT_ID = 0x02
These types represent the standard KCC20 ownership forms: direct ownership by public key, ownership by script conditions, or ownership delegated to another covenant.
Transfer Method
A KCC20 token must define a SilverScript transfer method with the following logical shape:
#[covenant(binding = cov, from = maxCovIns, to = maxCovOuts)]
function transfer(
State[] prevStates,
State[] newStates,
sig[] sigs,
byte[] witnesses
)
prevStates are the token states consumed by the transaction.
newStates are the token states created by the transaction.
sigs are per-input authorization signatures.
witnesses are per-input authorization metadata used by the transfer method to resolve how each consumed state is authorized.
Token Descriptor Artifact
Each KCC20 token must provide a descriptor artifact.
The descriptor is defined per token covenant id and provides the minimal information required for tooling to decode token state, reconstruct token outputs, and build valid transfer transactions.
TokenDescriptor {
prefix
suffix
state_layout
leader_entrypoint_selector
delegator_entrypoint_selector
optional_extensions
}
prefix and suffix are the script bytes before and after the encoded token state.
state_layout defines how raw state bytes are encoded and decoded, including any extended state beyond the standard KCC20 header.
leader_entrypoint_selector and delegator_entrypoint_selector identify the compiled transfer paths used to create a token transfer.
Note- In SilverScript, a #[covenant] declaration defines a covenant state transition that is implemented through leader and delegator transfer paths. For more details, see the SilverScript covenant declaration documentation.
optional_extensions is an array of strings, where each string is the identifier of a standard KCC20 extension supported by the token.
Optional Extensions
A KCC20 token may support standard optional extensions.
Extensions must be declared in optional_extensions.
Generic tooling may ignore unsupported extensions, but must not construct transactions that depend on an extension it does not understand.
This keeps the base KCC20 interface small while still allowing tokens to support richer behavior through explicit extensions.
KCC20 Borrowed Receive Extension v1
KCC20 Borrowed Receive Extension v1
kcc20_borrowed_receive_v1 enables additive receive for KCC20 tokens.
An existing recipient token UTXO may be consumed without normal owner authorization only if it is recreated with a higher token amount.
Motivation
KIP9 requires each new UTXO to include KAS for storage.
For asset transfers, this means the sender must fund every new recipient token UTXO, or the recipient must co-sign and provide their own KAS.
Borrowed receive allows a sender to use an existing recipient token UTXO as the receive target.
Instead of creating a fresh recipient token UTXO, the sender consumes the recipient’s existing token UTXO and recreates it in place with a larger token amount.
Specification
This extension reserves the witness value:
BORROWED_RECEIVE = 0xFF
The transfer witness contains one witness byte per token input.
If:
witnesses[i] == BORROWED_RECEIVE
then token input i is treated as a borrowed receive input.
Borrowed receive uses strict positional pairing:
borrowed input i -> token output i
The borrowed input at position i must be recreated by the token output at position i.
This avoids explicit indexing and prevents multiple borrowed inputs from being merged into one output.
Required Conditions
For every borrowed receive pair, the transfer method must enforce:
- paired output exists at the same covenant output position
- owner identifier unchanged
- identifier type unchanged
- token amount strictly increases
- KAS value preserved or increased
- all non-amount token state unchanged
The only allowed token-state change is:
newStates[i].amount > prevStates[i].amount
Composition With Normal Transfer
The following sketch shows the intended shape inside the regular transfer authorization loop:
for(i, 0, prevStates.length, maxCovIns) {
if (witnesses[i] == BORROWED_RECEIVE) {
require(prevStates[i].identifierType == newStates[i].identifierType);
require(prevStates[i].ownerIdentifier == newStates[i].ownerIdentifier);
require(newStates[i].amount > prevStates[i].amount);
byte[32] covId = OpInputCovenantId(this.activeInputIndex);
require(
tx.outputs[OpCovOutputIdx(covId, i)].value >=
tx.inputs[OpCovInputIdx(covId, i)].value
);
// Any token state outside `amount` must remain unchanged.
// Exact comparison depends on the token's state_layout.
}
}