// ========================================================================================= // ZK13Token — the reference fungible token for SourceLess. // // WHAT IT IS FOR // A fungible asset whose ledger lives in this contract's own storage, committed through // Sindri into the chain state root. This is the reference implementation of // docs/ZK13_TOKEN_STANDARD.md: every view, every mutating function, every event and every // `require` message in that document is here, spelled exactly as the standard spells it, // so a wallet can recognise a refusal without parsing prose. // // WHAT IT GUARANTEES // * Supply moves only through `mint` and `burn`. `transfer` moves it by exactly zero. // * `maxSupply` is set at deployment and no function raises it. It bounds LIFETIME // ISSUANCE — `lifetimeIssued()`, which only ever goes up — and NOT the circulating // `totalSupply`, which `burn` lowers. Those are different promises and only the first one // is keepable: a cap checked against a burnable number is defeated by mint, burn, mint, // which issues without limit against a cap that never appears to be breached. // `maxSupply() == 0` means UNCAPPED, which is what this template ships as. // * Minting closes permanently: `closeMinting` is one-way and `setMinter` refuses to // reopen it, so "fixed supply" is a promise the code keeps rather than a claim. // * The owner can pause and can hand over ownership in two steps. The owner CANNOT move // anybody else's balance: there is no `setBalance`, no `adjust`, no `transferFrom`. // * A revert leaves the ledger byte-identical. Storage commits only on success. // * A self-transfer is refused, so the classic debit-then-credit inflation bug cannot be // reached at all. // // HOW IT IS TYPICALLY MISUSED // * Treating "holds a balance" as "is a verified user". Receiving a ZK13 token is NOT // gated by STR.domain and cannot be — there is no domain accessor in AresLang. Airdrop // eligibility, voting weight and revenue shares computed from `balanceOf` are computed // over an unauthenticated set. Check the registry off chain. // * Sending to a contract address. A contract cannot be `caller` and there are no // cross-contract calls, so a balance recorded at a contract address can never move. // THAT IS A PERMANENT LOSS. This token has no recovery path and cannot be given one. // * Deploying and then forgetting to `closeMinting`, while telling holders the supply is // fixed. `minter()` is in the ABI; a holder will read it. // * Under-declaring the gas limit on the call record. Post-mining covers only // min(gasUsed, gasLimit / 10), so the network silently absorbs the rest, forever, on // every call. MEASURED, on the v2 gas table: mint 1,144, transfer 835, burn 774, // proposeOwner 412, acceptOwnership 612, setPaused 363, setMinter 471, closeMinting 470. // Declare a gas limit of at least 12,000. // // WHAT IT DELIBERATELY DOES NOT HAVE // No `approve` and no `transferFrom`. An allowance exists so another CONTRACT can spend on // a holder's behalf, and AresLang has no cross-contract calls. An allowance nothing can // spend is a stock of forgotten approvals waiting to become live on the day a call opcode // ships. See ZK13_TOKEN_STANDARD.md §4.2. // No upgrade path and no proxy. Bytecode is in this contract's address preimage; there is // no `setCode` anywhere in Sindri. What you deploy is what holders get, forever. // // TO MAKE THIS YOUR TOKEN // Edit the four constants below and nothing else. Changing them changes the bytecode and // therefore the deployed address, which is correct: a different token is a different // contract. Supply starts at zero; the deployer is owner and minter, so the first call // after deployment is `mint(founderAddress, amount)`. // ========================================================================================= contract ZK13Token { // ---- edit these four ---------------------------------------------------------------- state tokenName: string = "Example ZK13 Token"; state tokenSymbol: string = "EZK"; // 13, like all seven native ledgers. A wallet showing two assets at different precision is // where somebody sends 10^5 times what they meant to. state tokenDecimals: uint = 13; // A ceiling on LIFETIME ISSUANCE, not on circulating supply — see the header. 0 means // uncapped, and must be declared as 0 rather than as a very large number that looks like a // cap. Immutable once deployed. // // SHIPPED AT 0, WHICH IS UNCAPPED. That is deliberate rather than lazy: a template cannot // invent a cap for a token it knows nothing about, and a plausible-looking placeholder is // worse than an honest zero because it reads as a decision somebody made. If your holders // are to be told the supply is bounded, either set this to the real number before you // deploy — the address changes with it, which is correct — or `closeMinting()` once the // whole supply is issued, which proves the bound instead of promising it. state supplyCap: uint = 0; // ------------------------------------------------------------------------------------- state supply: uint = 0; // Everything `mint` has ever created, and it never goes down. This is what `supplyCap` // bounds. `supply` cannot be, because `burn` decrements it: with the cap tested against a // burnable number, mint -> burn -> mint issues without limit against a fixed cap, and the // arithmetic never complains. MEASURED on this template before the fix: 4,000 issued // against a cap of 1,000, in four calls, on a chain that stayed valid. state issuedEver: uint = 0; // `caller` inside a state initializer is the DEPLOYER: Sindri runs the compiler-generated // initializer with the authenticated deployer as caller. That is why this token needs no // `claimOwnership` call and has no window in which a stranger could claim it. state contractOwner: address = caller; state pendingOwner: address = ""; state mintAuthority: address = caller; state frozen: bool = false; state balances: map; // Transfer is the SINGLE record of every balance movement and there is no second one: // a mint is Transfer("", to, amount) and a burn is Transfer(from, "", amount). An indexer // summing Transfer must arrive at exactly the balances in storage. event Transfer(from: address, to: address, amount: uint); event OwnerProposed(current: address, pending: address); event OwnerChanged(previous: address, next: address); event PausedChanged(paused: bool, by: address); event MinterChanged(previous: address, next: address); // ---- views ---------------------------------------------------------------------------- view fn name() -> string { return tokenName; } view fn symbol() -> string { return tokenSymbol; } view fn decimals() -> uint { return tokenDecimals; } view fn totalSupply() -> uint { return supply; } view fn maxSupply() -> uint { return supplyCap; } // Everything ever minted, burned or not. `totalSupply` answers "how much exists"; this // answers "how much was ever issued", and it is the one the cap is enforced against. A // holder comparing the two learns exactly how much has been burned. view fn lifetimeIssued() -> uint { return issuedEver; } view fn balanceOf(account: address) -> uint { return balances[account]; } view fn owner() -> address { return contractOwner; } // "" means minting is closed permanently. Required even though this token does mint: a // holder must be able to ask "who can mint this?" and get an answer from the ABI. view fn minter() -> address { return mintAuthority; } // Required even though a token might never pause. A missing function is indistinguishable, // to a wallet, from a token whose answer is inconvenient. view fn paused() -> bool { return frozen; } view fn pendingOwnerIs() -> address { return pendingOwner; } // ---- movement ------------------------------------------------------------------------- fn transfer(to: address, amount: uint) -> bool { require(frozen == false, "paused"); // Unreachable through a call record — the ABI check refuses a malformed `address` // argument before the first opcode runs — and kept because it is the standard's rule and // because a future caller path that does not run `validateArgs` would need it. require(len(to) > 0, "empty recipient"); // Measured, not assumed. On this VM the two-statement form below is ALREADY safe for // to == caller: MSTORE writes through to the working storage, so the credit reads the // already-debited value and 1000 - 1000 + 1000 is 1000. The inflation appears the moment // somebody caches both balances in locals before writing either — `let from = // balances[caller]; let to = balances[to];` — because then the credit overwrites the // debit and the holder gains `amount` from nothing, callable in a loop, by anyone. // The guard costs 8 gas and makes that edit harmless. Keep it. require(to != caller, "self transfer"); // The header documents that a balance recorded at a contract address can never move, per // standard section 7.3. This is the one case of it the token can cheaply refuse outright. require(to != self, "self is not payable"); require(balances[caller] >= amount, "insufficient balance"); // uint - uint compiles to the CHECKED subtract, which reverts rather than producing a // negative that every later comparison would read as "plenty". balances[caller] = balances[caller] - amount; balances[to] = balances[to] + amount; // A zero-amount transfer is permitted and still emits: refusing it breaks batch payers // that send a list including zeros. emit Transfer(caller, to, amount); return true; } // The caller burns its OWN balance and no other. With no allowance mechanism there is no // authority by which anyone could burn somebody else's, and inventing one is confiscation. fn burn(amount: uint) -> bool { require(frozen == false, "paused"); require(balances[caller] >= amount, "insufficient balance"); balances[caller] = balances[caller] - amount; supply = supply - amount; emit Transfer(caller, "", amount); return true; } fn mint(to: address, amount: uint) -> bool { require(frozen == false, "paused"); require(len(mintAuthority) > 0, "mint closed"); require(caller == mintAuthority, "not minter"); require(len(to) > 0, "empty recipient"); // A position at this contract's own address could never move again: a contract cannot be // `caller`, and the value ledger refuses a contract that pays itself. Every `transfer` in // this catalogue has refused this since it was written; the ISSUANCE path did not, so the // guard sat on the call a holder makes and was absent from the call an operator makes. // MEASURED 2026-10-07: minting here succeeded and produced a position nothing can spend. require(to != self, "self is not payable"); // Refuses at the cap rather than truncating. A truncated mint gives the minter less than // they asked for and tells nobody. // // The cap is tested against LIFETIME ISSUANCE and never against `supply`, which `burn` // can lower. A cap that burning reopens is not a cap: burn what you just minted and the // room comes back, so a "fixed supply of 1,000" mints 1,000 as many times as the minter // has patience for. if (supplyCap > 0) { require(issuedEver + amount <= supplyCap, "supply cap"); } issuedEver = issuedEver + amount; supply = supply + amount; balances[to] = balances[to] + amount; emit Transfer("", to, amount); return true; } // ---- powers the deployer keeps, all of them visible in the ABI -------------------------- fn proposeOwner(next: address) { require(caller == contractOwner, "not owner"); pendingOwner = next; emit OwnerProposed(contractOwner, next); } // Two steps, because a one-step handover to a mistyped address is permanent: there is no // administrator anywhere in SourceLess who can reach into a contract's slots. fn acceptOwnership() { require(caller == pendingOwner, "not the pending owner"); emit OwnerChanged(contractOwner, caller); contractOwner = caller; pendingOwner = ""; } fn setPaused(next: bool) { require(caller == contractOwner, "not owner"); frozen = next; emit PausedChanged(next, caller); } fn setMinter(next: address) { require(caller == contractOwner, "not owner"); // Minting, once closed, stays closed. A fixed supply that can be un-fixed is not one. require(len(mintAuthority) > 0, "mint closed"); emit MinterChanged(mintAuthority, next); mintAuthority = next; } // One-way. This is the function that lets a token PROVE a fixed supply rather than promise // one. There is no argument because an `address` argument cannot be "": the ABI check // refuses it before the first opcode. fn closeMinting() { require(caller == contractOwner, "not owner"); require(len(mintAuthority) > 0, "mint closed"); emit MinterChanged(mintAuthority, ""); mintAuthority = ""; } }