Build · deep dive
Read a real contract
Tutorials teach the mechanism; production code teaches the paranoia. This is a guided read of
Escrow_buy_token.sil —
the limit-order escrow published by the KaspaRocket venue, parse-checked against current
Silverscript daily by this site's CI. Every snippet below is verbatim.
The idea in one breath
A buyer locks KAS in the covenant Covenant A lock on coins that dictates how they are allowed to move next. Not a promise or a bookkeeper's note: consensus rejects any spend that breaks the rule. with a fixed price per token. Anyone holding the right KCC-20 tokens can fill the order — fully or partially — and the covenant itself pays them the KAS, sends the tokens to the buyer, takes the platform's 0.3% fee, and persists with the remaining order size. No operator in the loop, ever.
1 · Parameters are baked, then guarded
// Immutability guard: economic parameters must never change.
function checkImmutable(State prev, State next) {
require(next.creator_pk == prev.creator_pk);
require(next.buyer_pk == prev.buyer_pk);
require(next.kas_per_token == prev.kas_per_token);
require(next.token_id == prev.token_id);
require(next.fee_recipient == prev.fee_recipient);
} A partial fill creates a new escrow UTXO carrying the remaining order — a transition Transition One allowed state change of a covenant: old state, plus your request, plus the transaction's facts, to a permitted new state. Everything else is rejected. . This guard makes every transition re-prove that the economics didn't move: nobody can "fill" an order while quietly rewriting its price. Remember the hole in the tutorial contract? This is what closing holes looks like.
2 · The fee is law, not policy
// Platform fee: 3 / 1000 = 0.3% of the KAS flow per fill.
int constant FEE_NUM = 3;
int constant FEE_DEN = 1000;
function checkFeeOutput(State prev, int feeOutIndex, int kas_amount) {
int fee = (kas_amount * FEE_NUM) / FEE_DEN;
require(fee > 0);
require(tx.outputs[feeOutIndex].value >= fee);
require(tx.outputs[feeOutIndex].scriptPubKey == byte[](prev.fee_recipient));
}
The venue's revenue isn't collected by a server — the covenant refuses any fill that doesn't
pay the fee output. A business model enforced by consensus. Note require(fee > 0):
integer division means a tiny fill could round the fee to zero, so dust-sized fee-dodging fills
are simply not transactions.
3 · The attack they block — in their own words
// ── Protection against "Double-Spend via LP_AMM" attack ──
//
// Attack scenario blocked:
// An attacker performs an LP_AMM buy swap that sends tokens to buyer_pk
// in the same transaction, then claims this satisfies the escrow —
// without bringing any tokens themselves.
//
// Defense:
// The contract scans ALL KCC20 inputs and only counts those with
// identifierType ≠ 0x02 (non-covenant). Protocol vaults (LP_AMM, other
// escrows) are type 0x02 and are excluded from the tally.
Kaspa transactions are atomic across many contracts — which cuts both ways. An attacker could
bundle an AMM swap and an escrow fill into one transaction and try to count the same
tokens twice. The defense uses covenant identity Covenant ID (KIP-20) A stable identity for a covenant lineage, so one contract can own things, be referenced by other contracts, and be found again across many transactions. : token UTXOs
owned by other covenants (type 0x02) don't count as "your" tokens. Composability
preserved, double-counting dead.
4 · One output slot per escrow
// Canonical constraint: buyer_kcc20_out_idx == escrow_out_idx + 1
// Tying the buyer output to escrow_out_idx + 1 makes it unique per escrow,
// preventing aliasing attacks where multiple escrows share the same buyer
// output (which would allow draining multiple escrows for the price of one fill). If two escrows in one transaction could both point at the same "tokens to buyer" output, one delivery could drain them both. Pinning each escrow's buyer output to its own slot kills the aliasing. This is the N:M N:M pattern Several covenant inputs and several covenant outputs of the same contract handled in a single transaction — Silverscript's model for batched state. world's version of double-entry bookkeeping.
What to take away
- Production covenants are mostly guards. Overflow bounds, immutability checks, canonical indices, fee floors — the happy path is a fraction of the code.
- Atomicity is a feature and an attack surface. Anything that can happen in your transaction will be used against your contract.
- The chain enforces exactly what's written. Same lesson as breaking the tutorial contract — here you see what it costs to write it right.
Honesty note: this is the venue's own published code with a pinned compiler revision; no external audit of it is named that this site knows of. Read it to learn, verify before you trust. Full file, tests and the LP AMM it defends against: the repository.