Manyfest: Can I post it here?
KCC20 is still a WIP draft and has not been finalized or versioned. At this stage, the goal is to converge on the best shape we can, not to preserve compatibility with earlier iterations of the draft. Asking the standard to retain an earlier shape as a conforming profile would effectively constrain the design before it is finished.
Burn-and-reissue is a possible migration path for already deployed tokens, as you say it inevitably creates fragmentation when holders do not migrate.
Borrowing remains the main unresolved issue. As noted, allowing arbitrary parties to spend and recreate another UTXO introduces a UX and availability problem: the owner must track a new outpoint, and pre-signed transactions become invalid.
An optout flag is a naive solution, a stronger model under consideration, inspired by this paper https://people.csail.mit.edu/rivest/pubs/RS96a.pdf. The goal is to preserve permissionless borrowing where intended without allowing unrestricted mutation of UTXOs.
I don’t see a good enough reason for a empty_selector in description . The right shape is to use a smarter and more efficient approach instead of validate in each input the entire state tranition. The spec tried to argue for the correct way to implement fungible tokens, and not tries to support all fungible token implantation possbile.
KCC2 defines representation and discovery of control principals, but explicitly does not define full authorization semantics or entrypoint authentication. That layer belongs to conventions built on top of KCC2, not KCC20 itself.
The profile’s off the table, that’s fine - a WIP draft shouldn’t freeze a shape before it’s done, and my deployed bytes work regardless of what the standard calls them.
No raw pre-signed transaction survives a borrow. The v1 sighash folds the spent input’s own outpoint into the signed message unconditionally. I read the node: it’s the hash_outpoint call in calc_schnorr_signature_hash, and it sits outside every sighash-type branch, so no type or flag drops it. AnyoneCanPay only zeroes the other inputs’ outpoints, never the signed input’s own. A borrow recreates the UTXO under a new txid, so a pre-signed input both names an outpoint that no longer exists and carries a signature that wouldn’t verify against the new one. Committing the continuation to a chain-position instead of an outpoint doesn’t rescue it either - covenant state fields aren’t in the sighash at all, consensus never looks at them when it resolves an input.
Authorization has to move off the transaction signature entirely, onto something outpoint-agnostic that a covenant checks against its own state. A hash chain alone doesn’t get you there because it bounds the borrows without keeping the owner’s pending spend alive across them. I’ve got a shape that does both. Rather than argue it in the abstract I’m building it against our KCC20 covenant and I’ll bring real SU numbers.
I just finished building it.
The fold I proposed has a mint bug
When I suggested folding borrow into the leader as a sig-less branch, I called the minter conservation scope an open question for the spec. It is not. It’s a bug in what I built. A minter-branch leader in borrow mode can mint whatever it wants and keep mint authority afterwards. No signature, and anyone can do it.
The fix: the sig-less borrow branch has to refuse a minter leader. 3 bytes, 14/14 regression green. A minter can still lead through the authenticated path or ride along as an authenticated delegator, same as the current single-entrypoint shape.
My deployed covenant isn’t affected. It authenticates every covenant input instead of exempting a sig-less leader, and I ran the attack against it as a control and it was rejected. None of this spike is deployed anywhere. But if you were going to build on the fold as I described it, use the fixed version.
The part worth putting in the spec: if you exempt one input from authentication to save bytes, that input’s own flags become the whole security boundary.
The compiler can’t do intents yet
The shape I had: the owner signs an intent - recipient, amount, nonce - that never names an outpoint. The covenant verifies it against a digest it rebuilds from its own state. A relayer builds the real spend against whatever outpoint exists after N borrows. The nonce is what makes it borrow-transparent, since borrows leave it alone and transfers bump it.
That needs a signature check over a message the covenant picks rather than over the transaction. The node has the opcode for it. Silverscript can’t currently reach it, and the gap has a security dimension, so I’ve reported the details to the compiler maintainers rather than posting them here. I’ll write it up once it’s resolved.
I’m not publishing the source in compilable form until that’s sorted.
So everything below about the mechanism working is measured against an instrumented copy, where the signature check becomes a digest comparison I can watch. That’s the same thing the real check would decide if Schnorr holds, but it doesn’t test the real opcode. Better I say that than someone find it.
Pinning an intent to one shard works
The obvious hole in a nonce-indexed intent is sharded balances. One owner id can back several live UTXOs, so an intent at (owner, nonce) fires against every shard sitting at that nonce. That part is solved now.
Every balance carries an immutable birth id. The continuation inherits it, so a lineage survives borrows and splits. Every other output has to carry a birth id derived from the spent outpoint and start at nonce zero, so nobody can fund a shard with a victim’s owner id and birth id at a chosen nonce and replay a spent intent. The birth id goes in the signed preimage along with the covenant id, a domain tag, a version, the owner, recipient, amount and nonce. 155 bytes, built on-stack out of things the covenant either reads from its own state or enforces on its own outputs. There’s no message-hash parameter in the ABI - a relayer never hands the covenant a digest.
Cross-shard replay rejected. Cross-token replay rejected. Both against siblings with the same owner, same nonce, same balance. Nothing missing from the compiler for any of it.
The balance is deliberately not in the preimage. Put it in and the first borrow kills every outstanding intent.
Every byte of token state costs about a thousand downstream
Token state width propagates into every covenant that bakes the token template. I measured it: 1,122 bytes added to the token became 2,178 in the order covenant, 24,585 in the pool, and 75,723 in the curve. About 68x. I’ve measured that multiplier three times now, in both directions, so it’s structural.
All ten of my production shapes overflow the 250,000 byte cap with the intent token, between 105% and 115%. The control is the interesting part: drop the shard binding, keep only the nonce, 9 bytes of state, and the worst shape still overflows at 101.5%. 9 bytes.
So it isn’t that the shard binding was too expensive. Any KCC-20 state extension costs roughly a thousand bytes of baked curve per byte of token state. That’s a constraint on the standard, not on this design. If authorization state has to live somewhere, the only fix that hits the cause is keeping it out of token state - a separate authorization UTXO that composing covenants never bake. I haven’t built that.
Redeem sizes and cascade deltas are measured at the pin. Shape totals are projections from those deltas. Script units are analytic from the published pricing constants, since the debugger has no meter. The one that matters is a signature verify at 100,000 SU against about 1,800 for everything else the mechanism does, so intents put a real cost on the transfer path while borrows stay cheap.
Borrow-survivable authorization is buildable and I know what it costs now. It needs a compiler fix to exist at all, and it needs to live somewhere other than token state.
Correction to my last post. I said Silverscript can’t do the signature check the intent design needs. It can, and has since late June - I’m pinned to an older compiler for release reasons and didn’t check master before posting.
I’d reported it to the maintainers first rather than posting details, and Ori confirmed it was already fixed upstream. He also added a security policy to the repo off the back of it. One thing worth passing on if you’re pinned to an older compiler too you should check that whatever signature builtin you’re calling actually lowers to a verify.
checkSigFromStack takes a fixed 32-byte digest, which is exactly the shape the covenant rebuilds from its own state, so the design can be checked against a real signature rather than a stand-in. I’ll rebuild against a current compiler and post what the real verify costs. The composition numbers in my last post were measured at my pin and still stand.
First post here in this topic, so brief context: I run kascov, a public Reader that decodes the live KCC20-style covenants directly from chain, without descriptor artifacts, by proving state against the revealed program bytes. A few field notes from the Reader side of the split:
Quote from @Shawn
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.
Byte-level pinning fixes reading the artifact, and I would add one property on top: a descriptor should be verifiable against chain, not just parseable. Almost everything a TokenDescriptor carries is already implied by any accept reveal of the covenant. Splice the decoded state out of the revealed program and the prefix and suffix fall out, and hashing the spliced program gives a stable template identity. Even the entrypoint selectors can be witnessed from real spends once each path is exercised. A Reader that recomputes these from a reveal can check a published descriptor byte for byte befor trusting it, which turns descriptor distribution from a trust problem into a caching problem, a much better problem to have. If descriptors get a pinned encoding, I will contribute chain-derived test vectors for that check, and Shawn’s numbered strawman sounds like the right table to diff them against.
A measurement on how many descriptors there would actually be: in mid-July, 867,456 spent covenant sigscripts on TN10 reduced to under three thousand template reveals and exactly 3 distinct template programs. Descriptors naturally live at the template level with a per-token binding, not one artifact per covenant id. That also hands the versioning story its natural unit: a restructure like the three-entrypoint shape just shows up as a new template generation beside the old ones. Basically the fragmentation @Shawn describes is already the observed steady state as of right now: one mainnet launchpad’s 74 live tokens span four template generations today, and old shapes do not retire. A Reader is fine with any entrypoint shape the draft lands on, as long as each generation stays identifiable from the bytes it has.
Quote from @Manyfest
Explorers should provide a service that lets users reveal token genesis transactions and expose the pre-compiled Silver logic.
This exists today: kascov reveals genesis transactions, matches deployed covenants against their compiled logic (for one launchpad the shipped source proved byte-for-byte identical to the chain bytes), and follows state transitions through replay. Happy to align its output shapes with whatever descriptor encoding this thread converges on!!
Wish you all the best.
Michiel
An updated spec proposal was merged as DRAFT at kccs/kcc-0020.md at main · kaspanet/kccs · GitHub
Imo this is in good state now, so this will be a good time for reference impls and experimenting with the convention and its nuances.
Coming at this from the borrowed-receive extension — I run a KCC-20 trading bot against the spec.
Context: this follows on from a thread with Michael Sutton on X — [ Michael Sutton on X: "3 KCCs by @iziodev (1, 2) & @manyfest_ (20) were finalized today as DRAFT calls for convention: https://t.co/UU3d6rhHMe Now would be a great time to really challenge these specs and find their possible limitations and tradeoffs" / X ] — so I’ll summarise it here rather than assume everyones read it.
hash-chain/v1 as drafted accepts any positive increase, so a released preimage is a bearer token in the mempool. An observer can race the honest borrow with a dust increase; if the dust confirms first, the honest deposit conflicts on the outpoint and the one-time authorisation is burned for nothing — forcing the offline wallet the scheme exists to protect back online. The fix I put to Michael is to make the scheme length-polymorphic: 32 = plain chain, 40 = chain + an 8-byte committed minimum increase, 64 = chain + a one-time Schnorr pubkey. Hash(preimage) == borrow_guard and next_borrow_guard = preimage[0:32] hold in every tier, and because the guard is Hash(preimage) it commits to the length — so the sender can’t downgrade the tier. That patch is written and applies cleanly against master; it’s the narrow fix.
His reply in that thread opened a wider question, and it’s the reason I’m posting here:
Probably if we allow AND guard logic, we might as well allow for OR as well, to define situations where we allow a selection of allowable guards to live side-by-side.
He’s right that the tiers are already AND-composition, just hardcoded (len-40 is hash-chain AND amount-threshold, len-64 is hash-chain AND schnorr-signature). But the two directions aren’t symmetric, and that’s worth pinning down before anyone builds it.
AND is monotone in the safe direction: a conjunct can only shrink the satisfying set, and the recipient writes the guard, so every conjunct they add strengthens their own position. OR is monotone the other way. An OR set is exactly as strong as its weakest clause, and the spender picks the clause — so the downgrade-resistance argument stops holding. It’s no longer a property of the selector; it’s a property of how the set was constructed, and that needs to be normative text:
A composite guard confers the security of the weakest clause it commits to. Anything a wallet relies on must hold for every clause in the set.
The footgun: OR{hash-chain/v1, schnorr-signature/v1} reads as sensible belt-and-braces and is strictly worse than either branch alone. The chain’s entire value is one-time; schnorr is idempotent. Side by side, the static key borrows forever, and the chain is decoration.
It also compounds the fact that the sender writes the recipient’s borrow_scheme and borrow_guard. With OR sets a sender can staple a branch they control alongside a plausible-looking one, and any wallet that renders “this UTXO supports hash-chain borrows” without enumerating the whole set is showing a reassuring half-truth. I’d make a wallet-side enumeration rule a prerequisite for OR, not a follow-up.
I did spec the general version anyway — composite/v1: depth-2 DNF (OR of AND-clauses), merkle root inside the existing 32-byte guard so there’s no state-layout change, prefix-byte domain separation, an 8-byte floor applying to every branch, and the successor recomputed from the same merkle proof so the untouched clauses are structurally preserved. Happy to post the encoding and the BLAKE3 conformance vectors if there’s appetite.
One finding from building it that I’d flag regardless of whether OR happens: “rebuild the tree canonically” and “replace the spent clause in place” produce different roots. Over 20,000 random sets, they disagree on 0.0% of steps with two clauses, 33.5% with three, and 71.3% with nine. The zero is the problem — a single lexicographic branch is order-independent, so the smallest OR set anyone would build to cross-check two implementations agrees perfectly, every time, and the divergence only starts at three. A consensus bug that passes its own minimal test case. Tree order has to be positional after creation, stated as a MUST.
Recommendation: keep the hash-chain length fix separate from composition. One is a bug fix against a spec that’s already merged; the other has real open questions — starting with whether OR is wanted at the covenant layer at all, given the recipient is online when they prepare the guard and could rotate it on an owner-signed transfer instead.
I guess I will write out my thoughts here more clearly. My main point was actually just that I believe we can easily and cheaply define the scheme where instead of opting for one borrow scheme with 1 byte identification prefix, we could easily and cheaply use the 1 byte of borrow-scheme to specify a combination of schemes via bit-flags, I guess anyone familiar with bit flags will get the idea already, but I will still write out the idea in full for completeness.
instead of defining a scheme as a fixed byte, we can define the scheme as a flipped bit within the byte i.e.
0b00000000 → disabled/v1
0b00000001 → amount-threshold/v1
0b00000010 → schnorr-signature/v1
0b00000100 → hash-chain/v1
0bxxxxx000 → reserved for future schemes
now a scheme can be defined by xor’ing the required bytes / flags together, and we get 1 byte that holds the information of all schemes that one wants. This allows us to combine multiple schemes into our 1 byte borrow scheme.
I guess, the following:
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)
then becomes, something akin to:
amount_threshold = 0
next_borrow_guard = borrow_guard
if borrow_scheme == disabled/v1:
reject
if borrow_scheme.includes(amount-threshold/v1):
amount_threshold = threshold from borrow_guard
if borrow_scheme.includes(schnorr-signature/v1):
verify borrow_witness with the public key in borrow_guard
if borrow_scheme.includes(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)
this is mostly independent of the original hash-chain discussion. imo it simply allows us to define a much more flexible scheme by compounding them. I guess it has the draw-back that 32 bytes borrow_guard won’t suffice, not sure how bad it is if this is encoded as variable len? also the ceiling on the amount of independent schemes we can define within 1 byte drastically goes down from 256 to 8, which might be relevant in some future where more guard schemes are defined.
The next thing I wanted to say is that for the hash-chain we can pass the borrow-scheme as input for it to know how to interpret the borrow guard it gets. potentially, i guess, it even simply inherits the borrow_scheme for free, right.. i.e. it just uses the one it has?
the point about not just combining schemes, but also allowing for defining a selection of different combinations / schemes to be valid is just some off-the-cuff food for thought, but I think if it gets hard to reason about it, and has the tendency to increase complexity it probably isn’t anything worth pursuing. The point about expanding op codes with low level bit ops is mostly just about making bit-flags more efficient in kaspa script in general, as i do believe that it could find usage for this case.
in all honesty though I am not deep into the specs, and do not purport to be. This might all be complete none-sense or unrealistic implementation wise, or break something somewhere. Just throwing it out there.
In response to the issue raised about extended state in relation to the standard transfer entrypoint (KCC-0020: updating extended state conflicts with the consolidation rule · Issue #14 · kaspanet/kccs · GitHub)
I opened a PR with additional context and clarification. Clarify KCC20 extended state fungibility semantics by Manyfestation · Pull Request #16 · kaspanet/kccs · GitHub
TL;DR- I argue that a standard fungible-token transfer entrypoint should treat all inputs and outputs as fungible tokens and verify only the amount and ownership transitions. This implies that the extended state must remain identical across the transfer.
I’ll dedicate more time to address and respond to the borrow scheme suggestions tommorow.
Good find, and the front-running is worse than a mempool race - it’s structural, and it’s the same root cause as the pre-signed-transaction problem earlier in this thread.
Grounding it from the consensus side: the honest borrow and the griefing borrow are two spends of the same recipient UTXO. Once the preimage is public anyone can build the second one, and the v1 sighash commits the spent input’s own outpoint, so the honest tx can’t follow to the recreated UTXO after the griefer’s confirms - it names an outpoint that no longer exists. The chain guard has also already advanced to the revealed step, so the honest depositor can’t just rebuild against the new outpoint either; that step is spent and only the recipient can reveal the next one. That’s exactly why it forces the offline wallet back online, which is the property the scheme was supposed to buy.
So the min-increase tier is a price, not a fix. It makes the griefer pay the floor instead of dust, which is a real improvement against spam, but a griefer willing to pay the floor still burns the step and still forces the recipient online. The race itself is untouched.
The thing the whole class comes down to: the attack needs the authorization to be one-time AND bearer AND gate-only - it checks a predicate over the increase rather than committing to the specific deposit. Break any one of those and it’s gone, and your tiers already move along that axis. The floor prices it. A binding that ties the use to something the griefer can’t reproduce - a one-time key, a committed depositor, a committed exact increase - takes it out entirely, at the cost of the recipient committing more up front. Schnorr on its own drops one-time and borrows forever, which is your OR footgun exactly. So the tier that keeps one-time and adds a binding is the one I’d expect to survive contact. I think it’s worth the spec saying out loud that a gate-only borrow scheme is front-runnable and a binding one isn’t, because that’s the property that actually matters here and right now it’s implicit.
Agree on keeping the length fix separate from composition, and on the weakest-clause rule for OR. The “passes its own minimal test case” divergence is the kind of thing that ships, and positional-after-creation as a MUST is the right call.
I guess my question then is how does the functionality originally laid out in the twitter thread (as posted by @GoonBoyCrypto) bypass the fixed length records, I understand that it uses length decoding of the pre-image, so wouldn’t this inherently run into the same wall regarding kcc1 specs? Just want to discern if this is something that my design inherently breaks, or an issue in the original twitter suggestion as well.
that being said we could layout borrow_guards into something like:
BorrowGuards {
threshold: byte[8]
public_key: byte[32]
hash_chain: byte[32]
}
which obviously comes at a higher cost regarding size, but allows us to have a state that is more explicitly defined.
reading over the docs, is the original twitter idea based on an extended state, as this seems to not require a fixed array? Also, I suppose I don’t understand how hashing the pk or threshold into the hash-chain actually achieves anything, at minimum, for me, it seems like it offers a false sense of security, if the threshold, or pk, is not also in the kcc-20 state? pks tend to be public (at minimum not using an unseen pk is a security issue), the threshold can probably be guessed as well, within some likely low bounds on a u64? besides this, knowing about a threshold doesn’t commit to actually spending above the threshold as per the actual kcc-20 state (which cannot even be defined currently together with a HC)?
I guess regarding these points I am more posting for educational enlightenment, and - maybe prematurely - arguing that more state for this kind of extended HC is unavoidable in any case.
I’ll try to add some context and hopefully shed more light and intuition. Borrowing is a feature of the standard token, and the standard transfer entry should be able to verify the borrow. Hence, the borrow-related field should be part of the standard token state. Extended state is meant to allow extending the state beyond the required standard fields, and the standard transfer entry should treat it as an opaque digest.
I spent some time thinking about hardening the hash-chain borrowing scheme against mempool front-running and reviewing the suggestion above.
Some of the solutions are creative and allow for customizability, but I want to remind that the issue is simply front-running and spending the revealed OTP from the hash chain. Therefore, I don’t see a reason to over-engineer a complicated or overly expressive schema.
My suggestion is to modify the scheme to bind each OTP to a one-time public key:
x_1 = Hash(x_0 || pubkey_1)
x_2 = Hash(x_1 || pubkey_2)
...
x_n = Hash(x_(n-1) || pubkey_n)
When providing an OTP to a sender, the recipient provides the corresponding one-time private key as well. The sender then signs its borrow transaction with that key and includes the preimage (which will become the next guard), the public key, and the signature as witnesses.
An observer seeing this tx in in the mempool cannot provide a valid sig for the current pubkey.
My intuition is that if I am authorizing someone to use my UTXO for a one-time borrow, the problem is solved. I don’t see benefit in complicating the borrow logic further by adding further conditions.
IMO this revision completes the set of guards needed against borrow spam:
- Forbid borrowing with
disabled. - Allow anyone to borrow with threshold = 0.
- Allow anyone to borrow above with a configured threshold.
- Allow a someone to borrow repeatedly with
pubkeyscheme. (Owner can always rotate/reset the pubkey or scheme through an owner-authorized transfer) - Allow a sender a single-use borrow authorization with hash-chain OTP bound to a one-time signing key.
Edit - I opened a corresponding PR
I was speaking to @Manyfest about using decimals like is done in EVM and also KRC20 does it, and he said we can just use big integers and that works no need to use fractions, but after going back and fourth with AI one of the main reasons is done like this
“So yes, “big supply and whole units only” works technically. It’s just less familiar for token UX, price charts, orderbooks, CEX/DEX-style trading, and integrations that expect decimal-style assets.”
Maybe
@Manyfest is not aware of how is used, but for simplicity is just like sompi/wei, the reason we use sompi in kas is to keep it big Int and do 0.15 kas → 0.15 kas x 100,000,000 behind the scenes, I think if we should consider this, thoughts? @michaelsutton
Following up on the delegator/fold discussion with numbers, and folding in the security items that came out of building and auditing it.
Framing this as seed material for a Security Considerations section rather than loose feedback: per KCC-0 §4.3, KCC-20 needs both a Security Considerations section and conformance vectors before it can enter Review, and it has neither yet. I’d like to seed both from what we’ve deployed and had independently audited. Vectors to follow; the security content is where the deployed experience is most useful, so I’m leading with that.
1. Composition headroom is why the folded shape matters.
Backing the two-entrypoint shape in the merged draft (borrow folded into the leader, plus transfer_delegator) against any later pressure to split borrow into its own entrypoint for readability. The constraint is composition, not standalone transfers. Standalone, our token redeem is 2,732 B with room to spare. But the covenants we compose tokens into already run tight — our heaviest composed shape is 234,446 B against the 250,000 B serialized cap, only 15,554 B under it (93.8% full). A token shape where each entrypoint carries its own copy of the state-transition validator grows the token redeem; standalone transfers don’t care, but every composed shape I compile does, and at 94% of cap there is no headroom to absorb it. One shared validator across the leader (transfer plus the folded borrow branch) is what keeps composition under the cap. Worth recording in the Rationale so the folded shape isn’t regressed later.
2. A borrow-mode leader that can change supply mints without owner authorization.
The borrow branch (witness[0]=0x01) authorizes a borrow, not a supply change or an owner action. The amount-threshold scheme takes no witness at all; the schnorr and hash-chain schemes authorize with a borrow-specific key, never the owner’s. Base KCC-20 transfer is conservation-only, so the base spec is safe. But §4 permits covenant-specific entrypoints and extended state that can change supply. If an implementer folds a supply-changing branch into the same leader that carries a borrow path, a leader in borrow mode can change supply with no owner authorization under any scheme, and for amount-threshold with no signature at all. We hit exactly this building the fold against our own covenant, which has a minter role. The guard is that a borrow-mode leader must reject any path able to alter total supply — a small guard, and one we regression-cover in our own build. It is a base-spec-safe foot-gun for the whole class of covenants that extend transfer, so it belongs in Security Considerations as a normative implementer rule.
3. Co-presence is not authorization.
Authorization semantics may formally belong to the layer above KCC-2 rather than to KCC-20 itself, but the consequence lands on KCC-20 token balances, so its Security Considerations should at least name it. For the signature-backed owner modes, an authorizing input that does not commit to the whole transaction lets a signature the owner made for something unrelated be replayed to wave a token move through. And the script-hash owner mode carries no signature at all, so whoever can spend that P2SH for any unrelated reason releases every balance keyed to it. Both are acceptable designs if the spec states them out loud; left silent they are a replay bug and an authority-confusion bug waiting for the first implementer to trip them.
4. The one-time-key binding closes theft, not availability.
#18 closes the front-running theft vector, and it is the right fix for that. It does not close the availability half, and that half still needs an answer. A borrow recreates the recipient UTXO under a new outpoint, and the v1 sighash commits the spent input’s own outpoint unconditionally the hash_outpoint call in calc_schnorr_signature_hash sits outside every sighash-type branch, and AnyoneCanPay only zeroes the other inputs’ outpoints, never the signed input’s own. So a borrow the owner never authorized permanently invalidates every pre-signed transaction that names that UTXO. Per-UTXO opt-out was one proposal, and I take the point that it is blunt, but the hardened hash-chain still does not keep a pending spend alive across a borrow, so something has to. I would make naming this, and deciding the availability mechanism, a v1 item rather than an optional extension: as an extension it will not get implemented.
Offer. I can turn 2–4 into a PR adding a Security Considerations section, and follow with machine-readable conformance vectors generated from our deployed covenants (the descriptor round-trip and the borrow ratchet), which §4.3 also gates Review on. Our independent audit is scoped to this exact covenant set; I’ll bring anything from it that’s relevant once it’s in hand. Happy to align scope with @Manyfest before I open anything.
Quick follow-up on the conformance-vectors offer. I’ve built a first cut and self-validated it against KCC-1 §11 — the dispatch tag, the state encoding, and the three template-hash rows all reproduce. Before I open a PR under kcc-0020/vectors/, three things I’d like to confirm so the vectors are authoritative:
-
transfer dispatch tag preimage. I build it from the record-expanded signature per KCC-1 §6.1 (record and field names omitted): transfer({int,byte[32],byte,byte,byte[32],byte[32]},byte), which gives dispatch_tag = 79c71c23 (and transfer_delegator(byte) → fd3ef14a). Is that the intended canonical signature? A different preimage changes the tag.
-
KCC20State field order and types as {int, byte[32], byte, byte, byte[32], byte[32]} (amount, owner, owner_scheme, borrow_scheme, borrow_guard, extension_commitment) — confirm that’s frozen.
-
amount state payload as the 8-byte LE signed-magnitude int (KCC-1 §5.3) — confirm.
Once those are settled I’ll PR the JSON plus the generator (~40 lines, self-checks against §11). @Manyfest happy to align on anything else you’d want in the first vector set.
It seems that EVM tokens use uint256 for token amounts. Note though that decimals is only a notation, and the actual amount is still an integer.
I agree that there is value in using 256-bit amounts for symmetry. The issue is that the script engine does not support 256bit arithmetic.
i64 allows 9 quintillion token units which is plenty any use case . With 8 decimals, that still allows a supply of roughly 92 billion whole tokens.
IMO we should stick with 64bit amounts. When we get to migrate existing tokens, we can reduce their resolution to fit this range.
Also worth noting that decimals is a metadata field and I don’t see a reason to store it in every token UTXO state. Ofc this could fit in extended state if anyone wishes to do so, but it should not be part of the standard.
I’m not sure this is a good thing. We might want some state extensions to be available onchain for some indexers more sophisticated than kcc20 indexers.
I would define the standard to have a known prefix (or some continuous view of the) state, so contracts will have freedom in defining what state they want to expose onchain.
The standard supports the idea of a known prefix. The identity of a token should be its cov id + template hash (defined over any stable prefix+suffix). See template views in kcc1