The KCC20Minter Contract
Source: silverscript-lang/tests/examples/kcc20-minter.sil [Link]
Full Source
contract KCC20Minter(pubkey owner, byte[32] initKCC20Covid, int initAmount,
bool initInitialized, int templatePrefixLen, int templateSuffixLen, byte[32] expectedTemplateHash,
byte[] templatePrefix, byte[] templateSuffix) {
byte[32] kcc20Covid = initKCC20Covid;
int amount = initAmount;
bool initialized = initInitialized;
struct KCC20State {
byte[32] ownerIdentifier;
byte identifierType;
int amount;
bool isMinter;
}
byte constant IDENTIFIER_COVENANT_ID = 0x02;
function calcInAmount() : (int) {
KCC20State kcc20PrevState = readInputStateWithTemplate(
OpCovInputIdx(kcc20Covid, 0),
templatePrefixLen,
templateSuffixLen,
expectedTemplateHash
);
return (kcc20PrevState.amount);
}
function checkMinterKcc20NewState(KCC20State minterKcc20NewState){
byte[32] controllerId = OpInputCovenantId(this.activeInputIndex);
require(minterKcc20NewState.ownerIdentifier == controllerId); // We do not allow the minter to delegate minting authority to another party.
require(minterKcc20NewState.identifierType == IDENTIFIER_COVENANT_ID);
require(minterKcc20NewState.isMinter); // The minter cannot stop being a minter.
validateOutputStateWithTemplate(
OpCovOutputIdx(kcc20Covid, 0),
minterKcc20NewState,
templatePrefix,
templateSuffix,
expectedTemplateHash
);
}
function checkRecipientKcc20NewState(KCC20State recipientKcc20NewState){
require(!recipientKcc20NewState.isMinter); // We do not allow the minter to designate another minter.
validateOutputStateWithTemplate(
OpCovOutputIdx(kcc20Covid, 1),
recipientKcc20NewState,
templatePrefix,
templateSuffix,
expectedTemplateHash
);
}
#[covenant.singleton]
function init(State prevState, State newState, sig s) {
require(!initialized);
require(newState.kcc20Covid == OpOutputCovenantId(0));
require(newState.amount == prevState.amount);
require(newState.initialized);
require(checkSig(s, owner));
}
#[covenant.singleton]
function mint(State prevState, State newState, sig s, KCC20State minterKcc20NewState, KCC20State recipientKcc20NewState) {
require(initialized);
require(newState.amount >= 0);
require(newState.initialized);
require(newState.kcc20Covid == prevState.kcc20Covid);
// We focus on the simple case 1-2 minting transfer.
require(OpCovOutputCount(kcc20Covid) == 2);
require(OpCovInputCount(kcc20Covid) == 1);
checkMinterKcc20NewState(minterKcc20NewState);
checkRecipientKcc20NewState(recipientKcc20NewState);
int inAmount = calcInAmount();
int mintedAmount = minterKcc20NewState.amount + recipientKcc20NewState.amount - inAmount;
require(newState.amount == amount - mintedAmount);
require(checkSig(s, owner));
}
}
Purpose
KCC20Minter is the example controller covenant for one KCC20 covenant instance.
The key idea is that issuance policy is not embedded directly into KCC20’s constructor or entrypoint arguments. Instead a separate controller covenant holds:
- which KCC20 covenant it governs
- how much issuance allowance remains
- whether the cross-contract binding has already been initialized
Constructor And State
The constructor takes:
ownerinitKCC20CovidinitAmountinitInitializedtemplatePrefixLentemplateSuffixLenexpectedTemplateHashtemplatePrefixtemplateSuffix
The state fields derived from those constructor args are:
byte[32] kcc20Covid = initKCC20Covid;
int amount = initAmount;
bool initialized = initInitialized;
The template-related constructor fields are not mutable state. They are contract parameters baked into the script instance.
Embedded KCC20State
The minter declares:
struct KCC20State {
byte[32] ownerIdentifier;
byte identifierType;
int amount;
bool isMinter;
}
This local struct gives the minter an explicit schema for reading and validating KCC20 state.
Why Template Metadata Exists
The minter needs to reason about a KCC20 output. It cannot safely trust “some output at index X has the right fields”. It must ensure that the output really belongs to the intended KCC20 template.
That is why the contract stores:
- prefix length
- suffix length
- expected template hash
- the actual prefix bytes
- the actual suffix bytes
These values come from the KCC20 script with its encoded state region removed. Conceptually, they identify the fixed template around the mutable KCC20 state payload.
calcInAmount
function calcInAmount() : (int)
This function reads the previous KCC20 state from the covenant input selected by:
OpCovInputIdx(kcc20Covid, 0)
That means:
- find the first covenant input whose covenant ID equals
kcc20Covid - parse it using the expected template metadata
- return its
amount
This is how the minter learns the old token supply before minting.
checkMinterKcc20NewState
function checkMinterKcc20NewState(KCC20State minterKcc20NewState)
This validates the continuing controller-owned KCC20 minter branch.
It enforces three things:
- the branch must remain owned by the current
KCC20Mintercovenant ID - the branch must remain covenant-ID owned
- the branch must remain marked as a minter
The first check deliberately uses the active input’s covenant ID:
byte[32] controllerId = OpInputCovenantId(this.activeInputIndex);
require(minterKcc20NewState.ownerIdentifier == controllerId);
This separates two identities:
owneris the admin key that signs minter actionscontrollerIdis the covenant ID that owns the KCC20 minter branch
So the admin key authorizes the controller, but the KCC20 branch remains owned by the controller covenant.
Then it validates the actual output with:
validateOutputStateWithTemplate(
OpCovOutputIdx(kcc20Covid, 0),
minterKcc20NewState,
templatePrefix,
templateSuffix,
expectedTemplateHash
);
This does two jobs:
- it selects the first KCC20 output for the governed covenant ID
- it ensures that output matches the expected KCC20 template and state payload
This is much safer than trusting an arbitrary output index or script shape.
checkRecipientKcc20NewState
function checkRecipientKcc20NewState(KCC20State recipientKcc20NewState)
This validates the newly minted recipient output.
It enforces that the recipient output is not itself a minter branch, and then checks that the second KCC20 output in the transaction matches the supplied state.
That means each mint transaction has a fixed shape:
- output 0 is the continuing minter KCC20 branch
- output 1 is the freshly minted recipient KCC20 branch
init
The first entrypoint is:
#[covenant.singleton]
function init(State prevState, State newState, sig s)
This binds a previously uninitialized controller covenant to a freshly created KCC20 covenant.
The controller covenant already has its own covenant ID before this entrypoint runs. In the bootstrap flow, a plain funding UTXO first creates the uninitialized controller covenant C. Then the asset genesis transaction spends C through init, creates the KCC20 asset covenant A, and recreates C as initialized and bound to A.
Its key checks are:
require(!initialized);
require(newState.kcc20Covid == OpOutputCovenantId(0));
require(newState.amount == prevState.amount);
require(newState.initialized);
require(checkSig(s, owner));
Interpretation:
- the minter must not already be initialized
- the new minter state must point at the covenant ID of output 0
- the issuance allowance is preserved during initialization
- the new state flips
initializedto true - the owner authorizes the operation
The critical piece is OpOutputCovenantId(0). That lets the minter learn the covenant ID of the KCC20 output created in the same transaction.
Without this check, this single transaction would not prove that the initialized minter bound itself to the exact KCC20 covenant output created beside it.
Initialization Diagram
plain funding utxo
|
v
[minter genesis tx] -> C covenant id
|
v
[asset genesis/init tx] -> A covenant id + C binds to A
before asset genesis/init:
C.initialized = false
C.kcc20Covid = placeholder
after asset genesis/init:
C.initialized = true
C.kcc20Covid = A
A.ownerIdentifier = C
mint
The second entrypoint is:
#[covenant.singleton]
function mint(State prevState, State newState, sig s, KCC20State minterKcc20NewState, KCC20State recipientKcc20NewState)
This is the transaction-level minting step that enforces the issuance policy.
The checks break down into four groups.
Minter state invariants
require(initialized);
require(newState.amount >= 0);
require(newState.initialized);
require(newState.kcc20Covid == prevState.kcc20Covid);
The minter must stay initialized, cannot go negative, and cannot switch to a different KCC20 covenant.
KCC20 cardinality
require(OpCovOutputCount(kcc20Covid) == 2);
require(OpCovInputCount(kcc20Covid) == 1);
The example only allows minting when exactly one KCC20 covenant input and two KCC20 covenant outputs are involved. That keeps the accounting simple and makes the split between the persistent minter branch and the recipient branch explicit.
KCC20 template validation
checkMinterKcc20NewState(minterKcc20NewState);
checkRecipientKcc20NewState(recipientKcc20NewState);
This ensures both supplied KCC20 successor states match the actual outputs in the transaction.
Issuance accounting
int inAmount = calcInAmount();
int mintedAmount = minterKcc20NewState.amount + recipientKcc20NewState.amount - inAmount;
require(newState.amount == amount - mintedAmount);
This means:
- compute previous KCC20 amount
- compute the total amount in the two new KCC20 outputs
- subtract the old amount to get the newly minted quantity
- decrement the minter’s remaining issuance allowance by exactly that amount
If someone tries to mint more than the issuance allowance permits, the minter state cannot satisfy the final equality and the transaction fails.
Mint Accounting Diagram
mintedAmount
= (new minter-branch amount + new recipient amount)
- previous minter-branch amount
new issuance allowance
= old issuance allowance - mintedAmount
Mint Shape Diagram
before mint:
KCC20 minter branch amount = old amount
KCC20Minter issuance allowance = remaining budget
after mint:
KCC20 minter branch amount = 0
KCC20 recipient branch amount = minted tokens for this transaction
KCC20Minter issuance allowance = reduced by minted amount
Why A Separate Minter Covenant Matters
This design cleanly demonstrates covenant composition.
- KCC20 knows how to authorize token state transitions.
- KCC20Minter knows how to constrain issuance.
KCC20 can be reused with different issuance policies because issuance control is externalized into another covenant rather than welded into the token contract itself.