Fungible Token Covenant Specification (KCC20)

Nice fix for the KIP-9 output cost, and the additive only envelope (owner fixed, amount up, KAS preserved) looks safe. One second order thing from running covenant tokens in production is borrowed receive lets anyone change your UTXO’s outpoint, which griefs pre-signed, outpoint bound transactions. On our side a sell is built and signed against the token UTXO’s outpoint, so a stranger borrowing a token into it before the tx lands invalidates the order and forces a rebuild. It’s additive only so not theft, purely availability, but maybe worth a security note on the pre-signed-tx interaction, and a per UTXO opt out for holders who don’t want their balance mutable by strangers. Genuinely useful for airdrops and streaming, just worth pinning the griefing surface down in the spec.

This is a property of the in-place UTXO model, it gives the most freedom to application developers as they have the choice to partition the state(s) according to app needs. If the state require frequent parallel reads on the same storage unit, a common pattern is to replicate the state among multiple UTXOs. If your app can’t afford temporary state drift among replicated units, then you can think of other solutions such as authenticated state provider (and pass as witness).

2 Likes

I suppose one can also sign over cov_id (as far as I understand), so if there’s a temporary activity spike, your tx will simply be slightly delayed instead or failing.

2 Likes

That is a nice solution. I am unsure though if would this be enough control for a stable coin issuers , or would they demand a stricter “global state like” policy, as you suggested in your next comment.
I wish we could sit down and discuses about this with real relevant organizations.

It’s an interesting solution, to use a proxy covenant, the issuer then could create a pool of such covenants and update them to represent its policy.

1 Like

You would need some nonce mechanism for that, but it is a legit solution for concurrency.

1 Like

I flagged and removed this comment.

Its a long text which mixes unrelated and non-relevant concerns, and a project promotion.

I also removed other comments which are not centered around the protocol discussion.

I really appreciate the eagerness to build and I am all up for more ecosystem projects. But this thread aims to converge on a lean interface, let’s keep it focused on this please.

3 Likes

For the record, my kUSD references were not in relation to any real product or project. I am not a business, I am simply a researcher. The names of any tokens are generic names I used in my code repo to help understand the capabilities and limitations of the SilverScript process.

Most of the concerns and comments raised here were regarding specific KCC20 SilverScript implementations, which are not part of this discussion.

I want to reiterate the goal of this proposal: Get feedback about what I argue to be the minimal abstract shape of a fungible-token-covenant-standard.

As we gather more clarity about covenant interaction, I become more assured in the proposal, and I intend to finalize and publish it next week under a relevant convention repo.

Below is a short summery of the proposal-

A KCC20 covenant is a covenant which-

1- Carries a state layout which begins with -

    byte[32] ownerIdentifier
    byte identifierType
    int amount

2- Includes a transfer entrypoint which expects these arguments-

    State[] newStates,
    sig[] sigs,
    byte[] witnesses

A token should publish a descriptor artifact which includes at minimum-

  • The contract’s redeem script (prefix and suffix)
  • The transfer entrypoints indexes.

If the covenant extends the KCC20 state, it should also provide the state layout.

3- A token can declare in its descriptor that he supports some known token extensions, which would inform about specific features it implements.

2 Likes

Thanks for the scope clarification — that’s really helpful. We’ve been building on covenants and tried implementing the KCC20 draft to see whether we could get an independent implementation working from the spec alone. Mostly we could, which is a great sign. A few spots tripped us up and we honestly weren’t sure whether it was the spec or just us reading too fast — sharing them here in case they’re useful before you finalize, all at the abstract-shape level. Please take or leave any of it.

1. State[] newStates and the array rule. We first read §5.5/5.6 (array of records needs fixed-width leaves) as conflicting with amount being int under §5.3’s PushMinimal, and thought we’d hit a wall. It took us a while to notice §5.4 already resolves it — a record-embedded int isn’t “standalone,” so it’s the fixed 8-byte payload. Since we stumbled here, maybe a one-line note or a conformance vector would save the next person the same detour? Entirely possible we just missed it on the first pass.

2. Descriptor serialization. We wanted to reconstruct a covenant’s P2SH address from its descriptor and realized we didn’t know the byte-level encoding — field order, length framing, how ExtendedStateLayout | none / ExtensionId[] are tagged. Is that meant to live elsewhere, or is it still open? If it’d help, we’d be glad to draft a serialization for you to pick apart.

3. identifier_type recognition. For SCRIPT_HASH / COVENANT_ID we weren’t sure what a transfer is supposed to check to accept an owner. We saw your witness sketch (tx.inputs[witness].p2sh == owner_id) and the id = H(tag, data) discussion in the PR — curious where you’re leaning, and whether it’ll end up in the spec text itself.

4. Issuer controls as an extension? More of a question than a proposal. If someone wants a stablecoin with a freeze/seize path (say, for x402-style payments), would that fit as a named kcc20_extensions category the way Borrowed Receive does? The global-state-utxo and spending-branch ideas already raised here seem to point that way. Not suggesting any layout — just wondering whether it’s worth a reserved slot so those implementations stay recognizable to each other.

Thanks again for driving this glad to help with conformance vectors or a serialization draft if it’s ever useful.

I’ve been working on adapting KCC20 to build upon @IzioDev’s KCC1 & KCC2. This raises a few challenges.

Authenticating different principal types requires variable-length parameters. KCC20 currently defines the leader transfer entrypoint as the one responsible for authenticating every input. This raises a technical issue: the entrypoint needs to receive an array of bytes which might have a different length for each input.

A possible solution is to move the authentication responsibility to each of the delegating inputs, and away from the leader.

This results in another issue with borrowing. The borrowed input needs to validate the borrow itself, or make sure that the leader will validate it on its behalf.

I propose a solution which restricts borrowing to only the leading input, possibly as a new entrypoint. This would result in 3 entrypoints for each token:

// Delegation entrypoint receives a variable-length owner pk preimage
// and validates its own authentication
transfer_delegator(sig signature, byte[] owner_opening)

// Borrow entrypoint, which expects the first output in the state transition to be
// the borrow-continuation with an increased amount, and validates the state
// transition and amount preservation
borrow(State[] next_states)

// A transfer entrypoint which both validates the leader input authentication
// and validates the state transition and amount preservation
transfer(State[] next_states, sig signature, byte[] owner_opening)

cc: @michaelsutton @IzioDev

Hey all, Sione from KaspaCom.
We are from KaspaCom, and we are aligning our existing KCC20V2 implementation with the draft KCC conventions. We want to clarify a
few compatibility points before deciding whether a future standard-native artifact is required.

KaspaCom context:

  • KaspaCom has an existing KCC20V2 implementation already used across our covenant tooling, backend builders, wallet flows, and
    indexer/projection stack.
  • The deployed KCC20V2 artifact is not being changed for this alignment pass.
  • We are currently adding descriptor/metadata compatibility around the existing artifact, not claiming full byte-for-byte
    conformance.

Existing KCC20V2 state fields:

  • ownerIdentifier
  • identifierType
  • amount

Draft references:

Open questions:

  1. KCC-0002 p2pk-schnorr/v1

Can a byte[32] field containing an x-only Schnorr public key conform to p2pk-schnorr/v1, or must the selected field use the
logical pubkey type?

  1. KCC-0002 P2SH ownership

Should existing Kaspa P2SH ownership using Blake2b-256(redeemScript) receive its own principal profile?

The current draft program-hash/v1 appears to use the KCC-0001 BLAKE3 template hash, which is not the same identity as a normal
Kaspa P2SH redeem-script hash.

  1. KCC-0020 transfer compatibility

May a contract retain specialized transfer entrypoints while also adding generic transfer and transfer_delegator entrypoints?

For example, KCC20V2 currently uses specialized flows like transferByOwnerSig and transferByWitness.

If generic entrypoints are added later, must they use KCC-0001 dispatch tags, or can a descriptor map legacy selectors to the draft
transfer model?

  1. KCC-0021 metadata binding

When one deploy transaction creates both a minter output and a premint holder output, does one metadata object identify the shared
token lineage, or only the lowest-indexed covenant output?

This matters for deploy transactions where the token identity is shared across the minter and initial holder state.

Our current position:

  • The existing KCC20V2 artifact should be described as partially aligned, not fully conforming.
  • Descriptor metadata should not upgrade claimed templates into verified classification.
  • A future standard-native artifact can be separately named/versioned once the identity, transfer, and metadata-binding rules are
    clear.

@IzioDev @Manyfest

2 Likes

Hello and welcome :slight_smile:

First and foremost, note the KCC20 is still an evolving draft which has no versioning (What do you mean by KCCV1/2?) .

Second, you question touches many different KCCs , but none of them are directly related to the KCC20 standard.

In general (and in relation to question #3) - these standards and conventions exist to enable composability across the ecosystem. They are not intended to be restrictive. If you find value in exposing a non-KCC1 standard ABI alongside the standard one, that is entirely your decision (though I personally find that a bit odd).

1 Like

Yes, V2 is just label since is an extended version of normal kcc20. Thanks for the response, I think ill just change our kcc20 to use your standard and not do any work arounds even if it means changing / refactor.

4 Likes

@cryptosione 1. and 2. are valid questions for kcc-02, let’s engage here: KCC-02: Control Principal References in Program ABIs - #3 by IzioDev

@Manyfest 3. seems to relate to kcc20 directly

about 4. i see no counter indications by kcc-20 as is, maybe it should be its own on-top convention to avoid metadata living at a different place (in state? in another utxo?) across tokens. i suggest to group in: Covenant tokens are the only Kaspa token type still missing metadata - #5 by Manyfest

1 Like

There is also a version that I wrote based on the L1 KCC20 tokens that can be minted in OMA PRO wallet. token-depot-standard-wallet-open-core/docs/KCC20_SPECIFICATION_V1_1_PROPOSAL.md at release/cw283-kcc20-v1-spec · token-depot/token-depot-standard-wallet-open-core · GitHub

To make things clearer- the proposal aim to standardize the minimal surface of a token, if you have a use case which requires more entries with different transfer logic or any other kind of logic, the standard most definitely not forbid or restrict this in any way.

I assumed question number #3 asks about entrypoints which are not compatible with the ABI defined in kcc1, but just wanted to make it clear than extending KCC20 with more entries is legit.

1 Like

This is probably where it ends up. Variable-length openings really do wreck leader validation, and splitting the delegated path out is the honest fix.

Sequencing is what I’d argue about, because everything I’ve deployed is immutable bytecode, and by the time this settles I won’t be the only one who’s shipped against the minimal shape you pinned down earlier in the thread - the standard state header plus a single verification-mode transfer. Deployed code can’t follow a restructure. The only migration that exists is opt-in burn-and-reissue onto a new generation, and you never get 100% of holders to move, so the old shape stays live whatever the paper says. So keep that minimal single-entrypoint shape as a conforming baseline profile and let the descriptor declare the delegated model as a second one. The descriptor needs a versioning story sooner or later anyway - profiles can be its first customer.

I compiled it. The delegator half is a clear win - non-leader inputs get way lighter once they stop carrying the full State columns, and the sig-less borrow ratchet seems genuinely safe. The three-entrypoint packaging has cost: transfer and borrow each carry a full copy of the state-transition validator, my compiler inlines everything, and the token redeem grew ~1.8x. Standalone transfers don’t care. Anything that bakes the token template into composing covenants multiplies it though, and every composed tx shape I run went over the byte cap. The fix keeps the semantics exactly: fold borrow into the leader as an sig-less branch, one shared validator, two entrypoints. That compiles smaller than my current production covenant and every composed shape fits with more headroom than I have today. I’m running my read past Manyfest before I post the full writeup with numbers.

Wherever borrow ends up - its own entrypoint or a branch of the leader - that’s the place to finally fix the consent problem I raised earlier with borrowed receive. A stranger mutating your UTXO still invalidates your pre-signed txs no matter how safe the ratchet is, so per-UTXO opt-out belongs in the borrow rules themselves and I’d make it a v1 blocker. As an optional extension it’ll never get implemented.

One more reason I care about a frozen baseline. We’ve commissioned an independent third-party audit of our full covenant set, scope frozen at a specific commit, and that code conforms to the minimal shape. If that shape survives as a profile, the spec gets a security review it didn’t have to pay for. I’ll post whatever comes back that’s relevant here.

And bumping my earlier question since it’s still open - an empty byte string is ambiguous with a zero-length push, so for covenants compiled without_selector I’d have the descriptor carry an explicit empty-selector flag. It’s what I ship today, round-trips indexers fine. Any objection to just specifying that?

This is close to the exact list I hit before shipping, Ross. Including the 5.5/5.6 vs 5.3 array thing and we ended up resolving it the way 5.4 implies. A cross-reference there would’ve saved me some time.

The serialization gap is the one that bites. Until field order, length framing and the tagging of the ExtendedStateLayout/ExtensionId arrays are pinned at byte level, two implementations can both be “conforming” and still fail to read each other’s descriptors. I can write up what we deployed as a numbered strawman people can diff against, descriptor encoding end to end, plus the SCRIPT_HASH vs COVENANT_ID question.

The edge case I think belongs in the spec rather than implementer folklore is the co-present-input check proves an input carrying the owner’s identity exists in the tx. Not that it consented to this transfer. Co-presence is not authorization. For the signature-backed owner modes the spec should say the authorizing input must commit to the whole transaction, otherwise a signature the owner made for something unrelated can be replayed to wave a token move through. And script-hash mode has no signature at all so whoever can spend that P2SH for any unrelated reason also releases every balance keyed to it. Both are fine if the spec states them out loud.

2 Likes