Fungible Token Covenant Specification (KCC20)

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_identifier
  • identifier_type
  • amount

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:

  1. Identifies the covenant input leader, by convention the first covenant input.
  2. Extracts the declared raw next_states byte array from the leader input sigscript.
  3. Decodes raw next_states bytes into an array of states using state_layout.
  4. Verifies each decoded next state against the matching transaction output.
  5. 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:

  1. Fetches relevant owner token UTXOs from the Reader.
  2. Verifies the Reader’s indexed decoded state by matching each input spk against P2SH(prefix || encoded_state || suffix).
  3. Calculates the state transition and produces prev_states and next_states.
  4. 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_states
  • authorization_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:

  1. maintain the standard KCC20 state header
  2. define a transfer method following the KCC20 transfer convention
  3. 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.
    }
}
19 Likes

Do you see these as something similar to ERC20 extensions?

Thinking from a stablecoin perspective. The following uses the classic EVM/ERC20 mental model. Which may not work here.

From issuer point of view, setting aside technicals for a moment. I think I require the following features. Some may be optional per issuer base on their requirements.

  • Mint/burn (maybe there is a distinction between mint/burn allowed by issuer only vs. end-user initiated. not entirely sure). With global mint/burn maximums, per end-user maximums, etc.
  • Whitelist/blacklist - Prevent holders from transferring to unapproved parties (via either whitelist or blacklist). Ability to update the whitelist/blacklist.
  • Pause - stop everyone from transferring
  • Freeze - a certain address from transferring
  • Force transfer - from a specific (blacklisted) address (I think Circle requires this)
  • Force burn - from a specific (blacklisted) address
  • See balances of all holders
  • See all current stablecoin UTXOs
  • See full tx history of all holders

I think some of these are required by upcoming regulation in US. Not that US is the only concern :slight_smile:

Then from a stablecoin holder perspective, I think the following is needed:

  • Transfer
  • Allowance - maybe a nice to have. Grant a dapp or another party the ability to spend on my behalf. Up to a specified amount or my entire balance.
  • Remove/update allowance
  • See my full tx history
  • See all of my current UTXOs

From a “dapp” perspective:

  • The ability to spend on behalf of a user (who granted me some allowance)

It’s probably missing a lot.

Also I would guess any hypothetical stablecoin issue will want/need to run their kcc20 indexer service.

I think this largely makes sense as a minimal/core spec for KCC20. With the understanding that other features are separate roles/specs (minter, extension features, etc).

This is outside the scope of this spec, but guidelines for wallet developers may be beneficial as part of this effort. I think there are unintuitive things wallet devs will need to do to support kcc20.

Also, would be remiss not to mention - I feel that the current level at which this conversation exists is challenging. Comprehending and reasoning here requires pre-requisite knowledge, as well as thinking from a new mental model. Which is OK, this is the start of something new :slight_smile: Just comparing to ERC20 which is quite easy to read a grasp. Feels like ERC20 is at a higher abstraction level, enabled by the underlying layers. This is probably where your point on “How should we define SilverScript artifact and SDK conventions to support covenant interoperability?” comes into play. Again, just remiss not to mention that this could move up to higher levels as it’s built out. I will think a bit on SDK.

EDIT: some edits to reduce verbosity

1 Like

would it make sense to build a DeFi node software that people would run alongside their Kaspa node, and use to store, advertise, maintain and exchange token descriptors among peers? (and in the future, liquidity pool descriptors and similar DeFi constructs)

1 Like

Thinking about the higher-level abstractions will likely also catch some unknown unknowns that are hard to see from the spec side (e.g. what should a wallet dev’s code look like?). Perhaps we could sketch out PoC examples in the SDKs while the spec is still being drafted. It might also lower the barrier to this conversation. As smartgoo noted, participating currently requires a lot of prerequisite knowledge. That’s expected this early, examples are just the fastest way to build the mental model.

I also think it would make sense to include a conformance suite / canonical test vectors published with the spec (similar to how BIP-32/39 ship test vectors).

2 Likes

I support the direction of KCC20, but I don’t think it should replace or invalidate KRC-20. The key issue is continuity, not reset.

KRC-20 already represents real deployed tokens and liquidity. Any new covenant-based standard should therefore act as a unified interface layer, not a competing system.

Proposed path forward :

  • KCC20 defines the canonical covenant token interface
  • KRC-20 is preserved through compatibility/mapping in indexers and wallets
  • migration is optional, not forced
  • both standards remain readable under a unified model

If KCC20 becomes a “new ecosystem” that sidelines KRC-20, it risks fragmenting liquidity and slowing adoption. If instead it standardizes and unifies token interaction, it strengthens Kaspa’s ecosystem.

2 Likes

Great work on this spec. The descriptor model is exactly what’s needed for tokens to be readable across the ecosystem without each issuer shipping their own indexer code.

We (KRON, kron.technology) run a launchpad/DEX on native-L1 covenants, and our fungible token follows the same design this spec describes: state header ownerIdentifier (byte[32]) / identifierType (byte) / amount (int), with one extension field appended (isMinter (bool)), and a verification-mode transfer entrypoint where the writer supplies next states in the sigscript. We intend to publish a spec-shape TokenDescriptor for every token on the platform so any compliant Reader can index them.

One question before we do. Our covenant has a single entrypoint, so silverc compiles it with without_selector: true, there are no entrypoint selectors in the script at all. As a side effect of the state sitting at the very front of the script, the descriptor’s prefix is also empty (output script = P2SH(encoded_state ‖ suffix)).

How should a descriptor express this case?

  • Should leader_entrypoint_selector / delegator_entrypoint_selector be empty byte strings, null/absent, or something else?
  • For Writers: is the intended sigscript for a no-selector covenant simply redeem_script, next_states, authorization_data for the leader (selector omitted entirely rather than pushed as an empty item), and bare redeem_script for delegators?
  • Is an empty prefix valid as-is, or should descriptors flag it explicitly?

Happy to test whichever convention you settle on against our live deployments and report back , we’d rather have the single-entrypoint case pinned down in the spec than have every Reader handle it differently.

3 Likes

Hi! Welocme and thanks for the reply.

I did not know about ERC20 extensions, I only assumed they exists informally.

Regarding issuer policies, the challenge is that there is no global token state. Tokens are distinct utxos, and hence it’s tricky to allow global lists, freezes and such.
The solution might be creating a token which forces the existence of a global state utxo, which would hold those global states. The global state utxo could be a pool of utxos, which each token transfer would had to spend along side with. In these pool, the state could be updated by the issuer and force their policy.

^ This could be implemented by giving your token to a script/covenant which is has a multisig logic or any other custom one. This is a neat feature of ownership in this model.

^ Those would be enabled when an indexer implement the Reader part of the spec, and persist it readings.

^ Again, the power of ownership could allow you to transfer the token to any game/app/contract, which could implement some kind of restrictive spending conditions.

Thank you for taking the time to reply.

Makes sense to me. I imagine this as a generalization of the Reader I suggested , a general-purpose covenant tracker that maintains its own UTXO set of the specific covenants it tracks.
Semantically I see it more as a covenant-state-tracker and less as a DeFi node.

1 Like

Defiantly, I am in between stuff, but later this week I’ll post an update regarding with more practical Silver examples and refs.

1 Like

I agree there should be an effort for porting KRC tokens in. The issue is, there are not on L1, so a bridge would require a trusted party (it just can not happen without trust assumptions unfortunatlly)
I would be happy to push on this direction, but would need some help from KRC guys/communities. If you can connect me with any, I’d be happy to coordinate on this.

@here
I’ve edited in the original post two drafts. If you guys could have a look that would be great.
Is the SilverScript interface is clear enough? Do you feel like anything is missing?

How do you feel about the optional-extensions?

I imaged something like this -
optional_extensions=["kcc20_borrowed_receive_v1", ..]

1 Like

In the last few days I made significant progress in the Argent compiler, and I believe some of the features that evolved may be important to this spec discussion.

It boils down to a term I call open ICC.

ICC stands for Inter-Covenant Communication, or more specifically the idea of two or more covenants being co-spent in the same transaction in some interlinked way. Open ICC means that the interacting contracts do not have to fully know each other at compile time.

Starting from the simple case

In the context of assets, the most basic form of interaction is an atomic swap between two asset UTXOs:

Alice's asset A + Bob's asset B
    ->
asset A to Bob + asset B to Alice

Both users sign the complete transaction, so each signer authorizes the full transition after inspecting the transfer amounts on both assets.

This already gives atomicity, but the relation is mostly mediated by signatures. Neither asset covenant needs to understand the other transition.

A more interesting ICC pattern is asymmetric:

Asset A requires Controller C to be co-spent.

Controller C observes and validates A's transition.

A treats the presence of C as its authorization condition. C is compiled against A, understands its exact state layout and surrounding contract template, and validates the relevant A inputs and outputs.

I call this closed ICC, because C is compiled against a specific A implementation.

Open ICC

Open, or dynamic, ICC targets the case where A is authorized by C, but C was not compiled against the concrete A implementation.

It currently relies on two main features.

First, C can observe an actor following some fixed state layout, call it Capsule, without knowing the concrete contract at compile time.

state Capsule {
    ...
}

state ControllerState {
    byte[32] agent_covid;
    actor<Capsule> agent_type;
    ...
}

actor Controller owns ControllerState {
    entry advance()
    observes remote by self.agent_covid {
        inputs  { agent: self.agent_type; }
        outputs { agent: self.agent_type; }
    }
    emits { controller: Controller; } {
        Capsule next_state = ...;

        require remote.outputs become {
            agent <- self.agent_type(next_state);
        };

        ...
    }
}

The controller receives an actor<Capsule> representing the concrete contract template.

Although it was not compiled against that contract, it can:

  • decode the foreign input using the known Capsule layout

  • verify that the non-state parts match the supplied contract template hash

  • inspect and constrain the state transition

  • require an output using the same concrete contract plus the updated state

This already gives dynamic onchain composition, but it limits every compatible implementation to exactly the same state layout. That is highly limiting and leads to the second feature.

Virtual state

A capsule can contain an opaque virtual field:

state Capsule {
    ...
    virtual memory;
    ...
}

Each concrete implementation can expand it into a different typed object:

state AgentMemory {
    int hunger;
    ...
}

state AgentState expands Capsule {
    memory: AgentMemory;
}

actor Agent owns AgentState {
    entry step(...) emits { agent: Agent; } {
        AgentState next_state = {
            ...
            memory: AgentMemory {
                hunger: memory.hunger + 1,
                ...
            },
            ...
        };

        become agent <- Agent(next_state);
    }
}

The base Capsule contains only a digest for memory.

The open observer can inspect and constrain the shared Capsule fields while allowing the virtual field to change without understanding its contents. The concrete Agent contract receives the AgentMemory preimage, verifies it against the digest, exposes its typed fields, and repacks the updated object into the next digest.

So both contracts constrain the same output:

Controller:
    understands and constrains the shared Capsule state

Agent:
    understands and constrains its expanded Memory state

Neither has to understand the other’s internal logic.

A working example of this model is here:

The generated contracts are ordinary Silverscript. Argent also packages the contracts, state layouts, entrypoints, template commitments, routing information, hidden witness recipes and attached-app fingerprints into portable artifacts consumed by argent-runtime.

Back to KCC20

In the context of KCC20, this means we may now have tools for defining onchain composability standards, rather than only offchain wallet and indexer standards.

For example, the stable KCC20 state could contain the common asset fields plus a virtual extension:

state KCC20State {
    byte[32] owner_identifier;
    byte identifier_type;
    int amount;

    virtual extension;
}

The exact fields and types are of course part of the spec discussion.

A concrete asset could then expand the extension into any state it needs:

state AssetMemory {
    covid controller_id;
    bool is_minter;
    ...
}

state AssetState expands KCC20State {
    extension: AssetMemory;
}

The common KCC20 fields remain directly visible onchain, while controller logic, minting state, borrower policy or any other implementation-specific state lives behind the extension digest.

Any asset implementing the standard is still usable by another covenant as:

actor<KCC20State>

The composing covenant understands the standard asset state, while the asset implementation retains arbitrary internal state and independently validates it.

A DEX as an example of dynamic composition

A DEX is a useful example because the composing covenant can be written once, while the concrete assets it interacts with may only be chosen much later.

A pool actor could open-observe two KCC20 transitions belonging to two different asset covenants in the same transaction. It would compile only against KCC20State, not against the concrete implementation of either asset.

For example:

trader asset A
pool reserve A
pool reserve B
pool A/B actor
        |
        v
updated reserve A
updated reserve B
asset B to trader
updated pool actor

The pool could verify onchain that:

reserve A increased by dx
reserve B decreased by dy
the configured ratio/invariant holds
both reserves remain owned by the pool

At the same time:

  • asset A independently validates its own authorization, conservation and extension-state transition

  • asset B independently validates its own authorization, conservation and extension-state transition

  • the pool does not need to understand either asset’s concrete controller or policy state

This is different from an offchain SDK merely knowing how to construct transactions for several asset implementations. A DEX compiled against KCC20State could directly inspect and constrain future compatible asset implementations onchain.

For actual reserve ownership, one possible design is a DEX root controlling a family of pair-specific pool covenants:

                    DEX root / registry
                    /                 \
             Pool A/B covid        Pool A/C covid
                /      \              /      \
          A reserve   B reserve  A reserve   C reserve

The reserve UTXOs are owned by the pair-specific covenant id, giving narrowly scoped co-spend authorization. The root can register or control the family of pools without directly being the broad owner of every reserve.

There are probably other DEX designs involving another level of indirection or more elaborate authorization chaining. That part can remain a problem for the DEX designers. The important point for this discussion is that the KCC20 state itself can be made generically inspectable and composable onchain.


Argent is still experimental, but this point is relevant to the KCC20 state design now.

If the first standard adopts a closed state layout, adding implementation-owned extension state later may require an incompatible format after wallets, indexers and assets have already converged around the original one.

KCC20 does not need to depend on Argent. But reserving and defining this extension point early may be essential for future onchain composability.

9 Likes

That’s huge. Thank you for going to such depths in the explanation.

I clearly see how allowing extended/virtual state would benefit composability, and DX in general.

One thing I did not understand is what is the benefit of open icc in the DEX example, it seems that the pair-pool is still tied to a specific pair of tokens, what is the advantage of open-icc in that case? If I understand correctly, open icc allows to create one general purpose pool-actor which can enforce the swap validation over any reserve pair? Or do I miss here something?

1 Like

It’s just a very initial sketch, but in short, the pair controller is not meant to be a different contract, only a different covenant id.

Btw this specific design requires ~N^2 controllers for N asset types, so it’s sub-ideal anyway.

1 Like

It depends on design. I think it makes sense to take a look at solana or cardano models.

For example in solana there’s no global state in mints except for mint authority, freeze authority and supply(let’s remove the last one from discussion just because it’s hard to fix the gap efficiently). Authorities may be replaced with covenants that have actual authorities in the state. The way it works in solana - freezer is able to freeze any token account of the same mint. The same property may be implemented as additional spending branch. Not only someone is able to spend but also a fixed authority, so freezing is actually a movement of tokens to another predefined spk, and unfreeze is moving tokens back

1 Like

There are also hacks that require global state less often. For example it’s possible to have a 2 phase transfers: send allows me to send tokens to some address, and they will be owned by some entity. However to spend it may be required to reference global state. So two states: pending transfer and ready to spend. Compounding/transaction may require additional checks

Note: the compiler generates the deserialization logic verifying the supplied preimage against the stored digest, then and decodes according to the concrete layout. Here is the generated .sil:

entrypoint function step(/*...*/, byte[8] gen__memory_agent_memory_preimage) {
  require(blake2b(gen__memory_agent_memory_preimage) == memory);
  int gen__memory_hunger = OpBin2Num(gen__memory_agent_memory_preimage.slice(0, 8));

  /* ... */
}

Correct me if I am wrong; this would allow one covenant to co-spend a second to ‘ingest’ its state (and advance it at the same time). Wouldn’t this be limiting in the sense that reading the covenant state required spending it, making access serialized? Even at 10BPS if two or three TXs try to read the state concurrently, only one will succeed while the x amount of others fail since the covenant state/UTXO is spent. Is there a solution/game theory I am missing? :thinking:

Michael, this closed vs open ICC framing matches our setup almost exactly, we’re a live example of the closed side, and it’s worth naming what that cost in production.

Our DEX pool is a covenant compiled per token. The pool template carries its own copy of the token’s state struct (owner, type, amount, plus the isMinter extension) and validates every token output it touches against that specific token’s baked-in prefix, suffix and template hash. So the pool literally cannot trade an asset it wasn’t compiled against, a new token means a freshly compiled and deployed pool. For a launchpad that mints its own tokens that’s tolerable, but the second cost bites harder because the pool is bound to exact template bytes, any covenant upgrade forks the whole address space, and we’ve had to build a template versioning and pinning system just to keep already deployed tokens on the testnet tradable after we change the covenant.

Your KCC20State example is basically our layout, and our extension would be almost exactly your AssetMemory, controller_id plus is_minter. The reason it maps so cleanly is that our supply rules are already split the way open ICC wants. The token covenant enforces its own conservation and minter policy, and the pool really only cares about reserve amounts and ownership. The pool reading isMinter today is largely redundant defense in depth on a rule the asset already enforces. So under open ICC the pool would constrain only the common Capsule fields and the reserve invariant, and the minter/controller state would sit behind the extension digest where the pool never has to see it. The other half of our coupling, the token template hash we bake in at compile time, is the part actor turns into a dynamically supplied template. Between the two features, that’s essentially the versioning machinery we run today going away.

On the pair-specific pool family, I agree that it doesn’t scale cleanly (you flagged the ~N² count yourself), and we’ve hit the adjacent wall from the throughput side too. A single pool UTXO per token serializes trades under load, which is what pushed us toward batched execution. Probably worth keeping those two axes separate though, open ICC fixes composability without touching throughput, they want different answers.

If it’s useful I’m happy to write up the closed-ICC pool plus template-pinning setup we run as a concrete reference for what the capsule pattern would replace, real bytes rather than a sketch.

3 Likes