Fungible Token Covenant Specification

KCC 20: Fungible Token Covenant Specification

Authors
Sivan Helfer <[email protected]>, Michael Sutton <[email protected]>, Romain Billot <[email protected]>
Type / category
Application, Covenant
Created
2026-07-15
Updated
2026-08-21
Status
Draft

Fungible Token Covenant Specification

KCC20 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.

KCC1 provides the underlying covenant terminology, value types, entrypoint and invocation rules, and state and template conventions used by KCC20.

1. State

A KCC20 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]
}
  • amount is the token quantity held and must be non-negative.
  • owner identifies or commits to the token owner, as defined by KCC2.
  • owner_scheme selects the scheme used to interpret owner.
  • borrow_scheme selects the rule for borrowing this state as defined in Section 5, Borrowed Receive.
  • borrow_guard contains the scheme-specific 32-byte guard: an amount threshold, a Schnorr public key, the current hash-chain value, or unused bytes when borrowing is disabled.
  • extension_commitment is a 32-byte digest of the token-specific extended state. The extended state itself is not stored directly in the KCC20 state. See Section 4, State Extendability and KCC1 Section 10 [1].

2. Transfer Interface

The default KCC20 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 KCC2 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 KCC2 byte values, and maximum token input and output counts.

The transfer and delegator entrypoints must follow the KCC1 leader and delegator rules in Section 9.1 [1].

transfer is invoked by the leader, as defined in KCC1 Section 9.1 [1]. next_states contains the states created by the KCC20 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 KCC1.

Note: A KCC20 "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 KCC20 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 receive

The 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 invalid.

For the default configuration, the owner-authentication bytes are:

Owner schemeOwner-authentication bytes
p2pk-schnorr/v165-byte transaction signature
p2pkh-schnorr/v132-byte public key, then 65-byte transaction signature
p2pkh-ecdsa/v133-byte compressed public key, then 65-byte transaction signature
p2sh/v1One unsigned byte containing the authority input index
covenant-id/v1Empty

Each remaining input in the KCC20 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 KCC20 states consumed and created by the transfer.

Extended state is opaque to the transfer: extension_commitment must be preserved unchanged. When multiple inputs are consolidated into one successor state, they must have the same extension_commitment.

These layouts apply KCC2'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 KCC20 covenant must publish a program artifact exposing the information required to decode its state and construct a transfer.

The program artifact represents a KCC1 Program ABI extended with KCC2 authority scheme information and KCC20 owner-role and token configuration.

InformationDefined by
ABI types and encoding; state locations; entrypoints; templatesKCC1
Authority scheme bytes and interpretationKCC2
Owner role and field locations; transfer boundsKCC20
Extended-state commitmentKCC20, following KCC1 Section 10
Borrow schemes and borrowed-receive rulesKCC20

The default KCC20 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: 3

The KCC20 borrow-scheme selector mapping is fixed:

borrow schemes:
    0x00: disabled/v1
    0x01: amount-threshold/v1
    0x02: schnorr-signature/v1
    0x03: hash-chain/v1

A conforming KCC20 program must recognize the disabled scheme and support all three enabled borrow schemes.

4. State Extendability

A KCC20 covenant may extend its base state through extension_commitment. This field stores a digest of the token-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 KCC1 Section 10 [1].

The standard transfer treats extended state as opaque and preserves extension_commitment unchanged in successor states. A KCC20 covenant may expose a separate program-specific entrypoint that updates extended state.

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 KCC20 Borrowed Receive Authorization.

A borrowed receive is available only to the leader, so there can be at most one per KCC20 covenant family. The borrowed successor is next_states[0], at covenant-family output position zero.

The borrow schemes are:

ValueSchemeMeaning of borrow_guardBorrow witness
0x00disabled/v1UnusedBorrowing is rejected
0x01amount-threshold/v1The first eight bytes contain the threshold that the token increase must exceedEmpty
0x02schnorr-signature/v1A 32-byte Schnorr public keyA 65-byte Schnorr transaction signature
0x03hash-chain/v1The current 32-byte hashIts 32-byte preimage

For a borrowed receive, borrow_witness = witness[1..]. For the amount-threshold/v1 scheme, the first eight bytes of borrow_guard are a non-negative KCC1 int payload; only those eight bytes are used. The selected scheme sets the amount threshold and the expected next borrow guard. Let leader_state be the state of the active leader input:

amount_threshold = 0
next_borrow_guard = borrow_guard

if borrow_scheme == disabled/v1:
    reject
else if borrow_scheme == amount-threshold/v1:
    amount_threshold = threshold from borrow_guard
else if borrow_scheme == schnorr-signature/v1:
    verify borrow_witness with the public key in borrow_guard
else if borrow_scheme == hash-chain/v1:
    require(Hash(borrow_witness) == borrow_guard)
    next_borrow_guard = borrow_witness
else:
    reject

amount_difference = next_states[0].amount - leader_state.amount
require(amount_difference > amount_threshold)
require(next_states[0].borrow_guard == next_borrow_guard)

Hash is the KCC1 Hash Function. 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 configured 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 KCC20 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.

6. References