Abstract
KCC-20 specifies a convention for fungible-token covenants. It standardizes the token state, transfer interface, borrowed-receive rules, and program information exposed through each covenant's Program ABI.
KCC-1 provides the underlying covenant terminology, value types, entrypoint and invocation rules, and state and template conventions used by KCC-20.
Specification
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.
1. State
A KCC-20 covenant state describes a token amount, its owner, its borrowing settings, and its extended-state digest.
The Program ABI exposes its state as a KCC20State record with the following fields, in order:
KCC20State {
amount: int
owner: byte[32]
owner_scheme: byte
borrow_scheme: byte
borrow_guard: byte[32]
extension_commitment: byte[32]
}amountis the token quantity held and MUST be non-negative.owneridentifies or commits to the token owner, as defined by KCC-2.owner_schemeselects the scheme used to interpretowner.borrow_schemeselects the rule for borrowing this state as defined in Section 5, Borrowed Receive.borrow_guardcontains the scheme-specific 32-byte guard: an amount threshold, a Schnorr public key, the current hash-chain commitment, or unused bytes when borrowing is disabled.extension_commitmentis a 32-byte digest of the application-specific extended state. The extended state itself is not stored directly in the KCC-20 state. See Section 4, State Extendability and KCC-1 Section 3.9 [1].
2. Transfer Interface
The default KCC-20 configuration uses the following transfer entrypoints:
transfer(
KCC20State[] next_states,
byte[] witness
)
transfer_delegator(
byte[] witness
)A non-default configuration MAY override:
- transfer and delegator entrypoints;
- the supported subset of KCC-2 owner schemes; and
- maximum token inputs and outputs.
The program artifact MUST describe the configured transfer entrypoint, any delegator entrypoint, supported owner schemes using their canonical KCC-2 byte values, and maximum token input and output counts.
The transfer and delegator entrypoints MUST follow the KCC-1 leader and delegator rules in Section 3.8.1 [1].
transfer is invoked by the leader, as defined in KCC-1 Section 3.8.1 [1]. next_states contains the states created by the KCC-20 covenant family, ordered by covenant-family output position. Its length MUST equal the number of continuation outputs in that family, and each element MUST equal the state in the corresponding output. The leader MUST validate each continuation's program, state, and covenant binding according to KCC-1.
Note: A KCC-20 "covenant family" is the transaction's inputs and continuation outputs sharing the same KIP-20
covenant_id[3].
The transfer entrypoint MUST validate that every next_states[i].owner_scheme is supported by the KCC-20 token configuration. This prevents a transfer from creating an unspendable token state.
The first byte of witness selects the leader's path:
0x00: normal owner-authorized transfer
0x01: borrowed receiveThe remaining bytes contain either owner-authentication data or borrow-authorization data, depending on the selected path. A normal transfer uses them to authenticate the leader's owner according to its owner scheme. A borrowed receive uses them according to its borrow scheme as defined in Section 5. Any other first byte is reserved for future KCCs and MUST be treated as invalid.
For the default configuration, the owner-authentication bytes are:
| Owner scheme | Owner-authentication bytes |
|---|---|
p2pk-schnorr/v1 | 65-byte transaction signature |
p2pkh-schnorr/v1 | 32-byte public key, then 65-byte transaction signature |
p2pkh-ecdsa/v1 | 33-byte compressed public key, then 65-byte transaction signature |
p2sh/v1 | One unsigned byte containing the authority input index |
covenant-id/v1 | Empty |
Each remaining input in the KCC-20 covenant family invokes transfer_delegator. Its complete witness contains its owner-authorization data and has no normal-or-borrowed prefix. A delegator validates only its local owner. The leader validates the complete transition, including amount preservation and all successor states.
The transfer is bounded by max_token_inputs and max_token_outputs, which limit the number of KCC-20 states consumed and created by the transfer.
An implementation MAY limit the total input token amount in a transfer to 2^63 - 1, even when each state's amount is individually within the int range.
Extended state is opaque to the transfer. All inputs and continuation outputs participating in a standard transfer MUST have the same extension_commitment, which MUST be preserved unchanged.
These layouts apply KCC-2's minimum approval checks. A non-default configuration MAY override them, in which case the Program ABI MUST describe how each supported owner scheme uses witness.
3. Program Artifact
Each KCC-20 covenant MUST publish a program artifact exposing the information required to decode its state and construct a transfer.
The program artifact represents a KCC-1 Program ABI extended with KCC-2 authority scheme information and KCC-20 owner-role and token configuration.
| Information | Defined by |
|---|---|
| ABI types and encoding; state locations; entrypoints; templates | KCC-1 |
| Authority scheme bytes and interpretation | KCC-2 |
| Owner role and field locations; transfer bounds | KCC-20 |
| Extended-state commitment | KCC-20, following KCC-1 Section 3.9 |
| Borrow schemes and borrowed-receive rules | KCC-20 |
The default KCC-20 configuration is logically:
transfer entrypoint: transfer(KCC20State[],byte[])
delegator entrypoint: transfer_delegator(byte[])
owner schemes:
0x00: p2pk-schnorr/v1 | pubkey
0x01: p2pkh-schnorr/v1 | byte[32]: P2PKHHash(pubkey)
0x02: p2pkh-ecdsa/v1 | byte[32]: P2PKHHash(ecdsa_pubkey)
0x03: p2sh/v1 | byte[32]: Blake2b(R)
0x04: covenant-id/v1 | byte[32]: KIP-20 Covenant ID
max_token_inputs: 3
max_token_outputs: 3The KCC-20 borrow-scheme selector mapping is fixed:
borrow schemes:
0x00: disabled/v1
0x01: amount-threshold/v1
0x02: schnorr-signature/v1
0x03: hash-chain/v1A conforming KCC-20 program MUST recognize the disabled scheme and support all three enabled borrow schemes. Unassigned borrow_scheme values are reserved for future KCCs and MUST be treated as invalid.
4. State Extendability
A KCC-20 covenant MAY extend its base state through extension_commitment. This field stores a digest of application-specific extended state rather than the extended state itself. The artifact MUST describe the extended state's type and encoding, and how its digest is calculated and verified, following KCC-1 Section 3.9 [1].
Extended state allows a covenant to extend token identity beyond the base KCC-20 state. It may represent sub-tokens, types, variants, status flags, restrictions, or other properties defined by the covenant.
For the standard transfer, extended state forms part of the token identity relevant to fungibility. Within a KCC-20 covenant family, states are considered fungible with one another only when their extension_commitment values are identical. The transfer does not interpret what the extended state represents; it treats the commitment as opaque and preserves it unchanged.
In addition to the standard transfer entrypoints, a KCC-20 covenant MAY expose covenant-specific entrypoints that interpret or update extended state according to the covenant's own rules.
5. Borrowed Receive
Receiving tokens normally means creating a new UTXO and funding it with KAS, even when the recipient already has a compatible token UTXO. Borrowed Receive lets the sender use that existing UTXO as the destination instead. It is effectively borrowed for the transaction and recreated with a higher token amount without the recipient's authorization. Its ownership and extended state stay unchanged, and its KAS value cannot be reduced.
For an explanation of the problem and borrow schemes, see KCC-20 Borrowed Receive Authorization.
A borrowed receive is available only to the leader, so there can be at most one per KCC-20 covenant family. The borrowed successor is next_states[0], at covenant-family output position zero.
The borrow schemes are:
| Value | Scheme | Meaning of borrow_guard | Borrow witness |
|---|---|---|---|
0x00 | disabled/v1 | Unused | Borrowing is rejected |
0x01 | amount-threshold/v1 | Signed threshold in the first eight bytes; negative values behave as zero | Empty |
0x02 | schnorr-signature/v1 | A 32-byte Schnorr public key | A 65-byte Schnorr transaction signature |
0x03 | hash-chain/v1 | The current 32-byte hash-chain commitment | A 32-byte preimage, 32-byte one-time Schnorr public key, and 65-byte Schnorr transaction signature |
For a borrowed receive, borrow_witness = witness[1..]. For the amount-threshold/v1 scheme, the first eight bytes of borrow_guard are a KCC-1 eight-byte little-endian signed-magnitude integer payload. The effective threshold is max(decoded_threshold, 0): negative values behave as zero, and both encodings of zero have the same behavior. A contract MAY require the decoded threshold to be non-negative. Only those eight bytes affect the threshold; the complete borrow_guard MUST be preserved by a borrowed receive using this scheme.
For hash-chain/v1, borrow_witness is exactly 129 bytes, laid out as:
next_borrow_guard: byte[32]
one_time_pubkey: pubkey
borrow_signature: sigThe pseudocode below is normative; where it and the prose of this section state the same rule, the prose is authoritative. It assumes any optional threshold-payload restrictions are satisfied.
The selected scheme sets the amount threshold and the expected next borrow guard. Let leader_state be the state of the active leader input, and let Hash be the KCC-1 hash function (unkeyed BLAKE3):
amount_threshold = 0
next_borrow_guard = borrow_guard
if borrow_scheme == disabled/v1:
reject
else if borrow_scheme == amount-threshold/v1:
decoded_threshold = signed-magnitude integer from borrow_guard[0..8]
amount_threshold = max(decoded_threshold, 0)
else if borrow_scheme == schnorr-signature/v1:
verify borrow_witness with the public key in borrow_guard
else if borrow_scheme == hash-chain/v1:
parse revealed_guard, one_time_pubkey, borrow_signature
from borrow_witness
require(Hash(revealed_guard || one_time_pubkey) == borrow_guard)
next_borrow_guard = revealed_guard
verify borrow_signature with one_time_pubkey
else:
reject
amount_difference = next_states[0].amount - leader_state.amount
require(amount_difference > amount_threshold)
require(next_states[0].borrow_scheme == leader_state.borrow_scheme)
require(next_states[0].borrow_guard == next_borrow_guard)Every borrowed receive MUST increase the borrowed state's token amount. For the schnorr-signature/v1 and hash-chain/v1 schemes, any positive increase is sufficient. For the amount-threshold/v1 scheme, the increase MUST be strictly greater than the effective threshold.
The borrowed successor MUST preserve owner, owner_scheme, borrow_scheme, and extension_commitment. It MUST use the expected next_borrow_guard. The actual covenant-family output corresponding to next_states[0] MUST have at least the KAS value of the borrowed input.
The leader MUST preserve the total token amount across the KCC-20 covenant family. All other inputs in that family use transfer_delegator and authenticate their own owners.
A normal owner-authorized transfer MAY choose new borrow_scheme and borrow_guard values for its output states.
Rationale
KCC20State uses fixed-width fields so that next_states can be encoded as an array of records under KCC-1 Section 3.4.6. The shared fixed-width layout also enables streamlined on-chain composition by letting other covenants reuse the same decoding logic when inspecting KCC-20 states. A single 32-byte borrow_guard interpreted according to borrow_scheme gives every scheme the same state layout and template. extension_commitment is a digest so that the state layout and the transfer dispatch tag do not depend on the application-defined extended state. Fungibility is limited to equal commitments because the transfer does not interpret extended state.
The owner scheme is selected per state so that keys, key hashes, scripts, and covenants can hold the same token. The transfer validates the owner_scheme of every successor because an unsupported scheme makes the state unspendable and cannot be corrected afterwards. One witness with a path selector, rather than a separate borrowed-receive entrypoint, keeps one dispatch tag and one validation path for both cases.
Borrowed receive is limited to the leader and to next_states[0] so that a covenant family contains at most one borrowed input and the leader, which validates the complete transition, is the only participant that handles it. The borrow schemes provide, respectively, no borrowing, borrowing by any party above a price, borrowing by a designated key, and single-use borrowing released by the owner. The conditions of a valid borrow are defined in Section 5.
The effective threshold keeps the positive-increase requirement for borrowing separate from optional restrictions on stored threshold payloads.
Conformance Vectors
The conformance vectors are the file kcc-0020/vectors/conformance.json, which is authoritative; they are listed below in abbreviated form. Byte strings are lowercase hexadecimal, || is concatenation, and bb^n is the byte bb repeated n times. sig and pubkey values are placeholders, and no vector asserts a signature verification. All vectors use the default configuration of Section 3. Vectors with negative threshold payloads apply only to contracts that permit those payloads.
dispatch tags
transfer({int,byte[32],byte,byte,byte[32],byte[32]}[],byte[]) 79c71c23
transfer_delegator(byte[]) fd3ef14a
EncodeState of (1000, 11^32, 01, 01, f401000000000000 || 00^24, 00^32), 112 bytes
08 || e803000000000000 || 20 || 11^32 || 01 || 01 || 01 || 01 ||
20 || f401000000000000 || 00^24 || 20 || 00^32
transfer arguments for next_states (600, 22^32, 00, 00, 00^32, 00^32) and
(400, 33^32, 01, 01, 00^32, 00^32): one PushMinimal per grouped field,
then the witness argument, then the dispatch-tag push
10 || 5802000000000000 || 9001000000000000
40 || 22^32 || 33^32
02 || 00 || 01
02 || 00 || 01
40 || 00^64
40 || 00^64
42 || 00 || 44^65
04 || 79c71c23
owner-authentication witness, normal path
p2pk-schnorr/v1 00 || 44^65
p2pkh-schnorr/v1 00 || 22^32 || 44^65
p2pkh-ecdsa/v1 00 || 02 || 55^32 || 44^65
p2sh/v1 00 || 02
covenant-id/v1 00
P2PKHHash(22^32) = 7caa514a05535fd97887e483ce6f1fc36ef7026435c47521b04549d9af4837bf
P2PKHHash(02 || 55^32) = ed887ad164d313eeeac970912504a81156274732ab09c0c0559ff59160abddf9
borrow witness, borrowed path
amount-threshold/v1 01
schnorr-signature/v1 01 || 44^65
hash-chain/v1 01 || x_2 || b3^32 || 44^65
hash chain, x_i = Hash(x_(i-1) || pubkey_i)
x_1 = Hash(a0^32 || b1^32) = 7c39893345dd20342a5b0b5dd3459276e4fee0cfab947e720b18be5af5500f99
x_2 = Hash(x_1 || b2^32) = c1addceaf72a21a322c888e10e2f34445b063c3ca61fe7a1614f2d382961f0ee
x_3 = Hash(x_2 || b3^32) = 46530c9d75f7a5a1ce01b4e04cd8833f5f30c2482f99d855e3841279a8bcb8cc initial borrow_guard
borrowed receive; leader amount 1000, fields not shown preserved
guard_500 = f401000000000000 || 00^24
guard_zero = 00^32
guard_neg_500 = f401000000000080 || 00^24
guard_neg_zero = 0000000000000080 || 00^24
W_chain = 01 || x_2 || b3^32 || 44^65
case scheme leader guard successor witness result
threshold-exceeded 01 guard_500 1501, guard_500 01 accept
threshold-equal 01 guard_500 1500, guard_500 01 reject
threshold-zero 01 guard_zero 1001, guard_zero 01 accept
threshold-negative 01 guard_neg_500 1001, guard_neg_500 01 accept
threshold-negative-unchanged 01 guard_neg_500 1000, guard_neg_500 01 reject
threshold-negative-decrease 01 guard_neg_500 999, guard_neg_500 01 reject
threshold-negative-zero 01 guard_neg_zero 1001, guard_neg_zero 01 accept
threshold-negative-zero-unchanged 01 guard_neg_zero 1000, guard_neg_zero 01 reject
threshold-guard-normalized 01 guard_neg_500 1001, guard_zero 01 reject
disabled 00 00^32 1500, 00^32 01 reject
hash-chain-valid 03 x_3 1001, x_2 W_chain accept if the signature verifies
hash-chain-guard-not-advanced 03 x_3 1001, x_3 W_chain reject
unassigned-scheme 7f 00^32 1500, 00^32 01 reject
successor-changes-owner 01 guard_500 1501, guard_500 01 reject successor owner 22^32
successor-reduces-kas-value 01 guard_500 1501, guard_500 01 reject output 99999999 sompi, input 100000000 sompi
hash-chain-link-mismatch 03 x_3 1001, x_1 W_x1 reject
W_x1 = 01 || x_1 || b3^32 || 44^65
Hash(x_1 || b3^32) = 54b9c2f64c32236324a93ad2d57da6c73d5ff733ef133b69e0da978f5ba00a00
standard transfer; leader (1000, 11^32, 00, 00, 00^32, 00^32), the next_states
and witness above, max_token_inputs 3; each case changes only what is listed
case change result
negative-amount successor amounts -600, 1600 reject
unsupported-owner-scheme second successor owner_scheme 7f reject
invalid-witness-path-byte witness 02 || 44^65 reject
extension-commitment-not-preserved second successor extension_commitment ee^32 reject
amount-not-preserved successor amounts 600, 500 reject
over-max-token-inputs three delegator inputs of 100; successors 600, 700 rejectReference Implementation
An Argent reference implementation with generated Silverscript contracts and Rust conformance tests is available at kcc20-reference.
Security Considerations
The threat model, trust assumptions, and malleability analysis in the Security Considerations of KCC-1 apply to every KCC-20 covenant and are not repeated here. This section covers the considerations specific to KCC-20.
A borrowed receive spends a UTXO without the authorization of its owner. Section 5 requires the borrowed successor to preserve owner, owner_scheme, borrow_scheme, and extension_commitment, to increase amount, and to carry at least the KAS value of the borrowed input. A borrower therefore cannot move the tokens to another owner, reduce their amount, change their extended state, or reduce the KAS value of the UTXO. A borrower can change the outpoint of the UTXO.
A borrowed receive invalidates every unconfirmed transaction that spends the previous outpoint, and a wallet that holds the UTXO has to discover the successor before spending it. Repeated borrowing denies the owner a stable outpoint. Under amount-threshold/v1, any party can borrow by increasing the token amount by more than the effective threshold. A zero effective threshold permits an increase of one token unit. Under schnorr-signature/v1, the holder of the borrow key can borrow without limit until the owner replaces borrow_guard. Under hash-chain/v1, each released link permits one borrow. A link consists of a revealed value and a one-time private key, and any party that obtains both can use it. A released link that is not used keeps the outpoint unstable until it is used or the owner replaces borrow_guard.
The hash-chain/v1 scheme relies on the preimage and second-preimage resistance of the KCC-1 Hash Function. A preimage of borrow_guard whose last 32 bytes are a public key with a private key known to the attacker authorizes a borrow.
extension_commitment determines fungibility (Section 4). Its security depends on the collision resistance of the commitment scheme declared in the program artifact. The standard transfer does not interpret extended state; a rule that extended state represents is enforced only by covenant-specific entrypoints.
A successor state whose owner_scheme is not supported by the covenant cannot be spent; Section 2 requires the transfer to reject it. Unassigned borrow_scheme and owner_scheme values are reserved. A state that carries one cannot be borrowed, and an implementation that assigns local meaning to one is not interoperable.
The path selector in witness is part of the signature script and is not covered by the transaction ID. A third party that changes it causes the invocation to be evaluated under the other path, and the transaction is rejected unless the remaining bytes satisfy that path. This document does not restrict the sighash type of a signature in witness; whether a signature can be reused in another transaction depends on that type.
hash-chain/v1 provides the owner with the strongest per-borrow control: the owner can release an authorization only when ready to permit a single outpoint change. Like a one-time password (OTP), each link authorizes at most one borrow and is bound to a one-time signing key. Using the link advances borrow_guard, so that authorization cannot be reused for a later borrow.
References
- [1] KCC-1, Covenant Concepts, Byte Layouts, and ABI.
- [2] KCC-2, Authority Schemes.
- [3] KIP-20, Covenant IDs.
Copyright
Copyright and related rights waived via CC0.