// ========================================================================================= // FixedSupplyToken — a ZK13 token with one issuance, no owner, and no switches. // // WHAT IT IS FOR // The token you deploy when the supply is the whole point: a fixed number of units, issued // once, after which NOBODY — including the deployer — can mint, pause, freeze, confiscate or // change anything. It conforms to docs/ZK13_TOKEN_STANDARD.md exactly as the reference // `zk13-token.ares` does; it is the profile of that standard with every owner power deleted // rather than merely promised unused. // // HOW IT DIFFERS FROM zk13-token.ares // `zk13-token.ares` keeps an owner who may pause, appoint a minter, and close minting. This // one has no owner after issuance, no pause, no minter role and no `setPaused`, `setMinter`, // `proposeOwner` or `acceptOwnership` ANYWHERE IN THE ABI. A holder does not have to trust // that the owner will not use a power: the power is not there to use, and `owner()` returns // "" to say so. That is the entire difference, and it is a large one. // // The price of it: no emergency stop. If a holder's key is stolen the token cannot be frozen // while it is sorted out, because nothing here can freeze anything. That is the trade, and a // token that claimed both properties would be claiming one it does not have. // // WHY THERE IS AN `issue()` CALL RATHER THAN A BALANCE IN THE INITIALIZER // Because a map cannot be initialized at deployment. A state initializer runs before any // entry exists and AresLang has no map literal — deliberately, since an initializer that // enumerated entries would be unbounded work on a deployment path. So the supply cannot be // placed in anybody's balance by the deployment itself, and a token that wants a fixed supply // must issue it in one call afterwards. `issue()` is that call: it mints the whole declared // supply to the issuer and closes issuance permanently IN THE SAME CALL, so there is no // state in which a partial supply exists alongside the power to mint more. // // WHAT IT GUARANTEES // * `issue()` succeeds at most once, ever. The second call reverts with `mint closed`, and // there is no other function anywhere that writes `supply` upward. // * After `issue()`, `totalSupply() == lifetimeIssued() == maxSupply()` and `minter()` and // `owner()` are both "". Those five readings together are the proof a holder needs: they // can be checked from the ABI, in one block, by anyone. // * `totalSupply()` can fall — `burn` is the only way, and only a holder can burn what is // their own. `maxSupply()` and `lifetimeIssued()` never move after issuance, which is what // makes "how much was destroyed" answerable: `maxSupply() - totalSupply()`. // * No function moves a balance the caller does not hold. There is no `transferFrom`, no // allowance, no owner, no recovery path, and no way to add one: the bytecode is in the // address preimage and there is no `setCode` anywhere in Sindri. // * A revert leaves the ledger byte-identical. Storage commits only on success. // // HOW IT IS TYPICALLY MISUSED // * Deploying and then losing the deployer key before calling `issue()`. The token is then // PERMANENTLY UNISSUABLE — a contract with a supply of zero and no way to create one. // Call `issue()` in the block after you deploy, from the same key, before anything else. // * Believing "no owner" means "fairly distributed". The entire supply lands in the // issuer's own balance and the distribution after that is an ordinary sequence of // transfers that nothing in this contract constrains. What the code proves is the SIZE of // the supply, never who holds it. // * Sending to a contract address. A contract cannot be `caller`, so a balance recorded at a // contract address can never move again. PERMANENT LOSS, with no recovery path and none // possible. The one case this contract can detect — its own address — it refuses. // * 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. // * 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, with 53-character addresses: issue 1,350, // transfer 778, burn 717. Declare a gas limit of at least 14,000. // // TO MAKE THIS YOUR TOKEN // Edit the four constants below and nothing else. `supplyTotal` is in MINOR units: at 13 // decimals, one whole token is 10000000000000, so the value shipped here is one million // whole tokens. Changing a constant changes the bytecode and therefore the address, which is // correct — a different token is a different contract. // ========================================================================================= contract FixedSupplyToken { // ---- edit these four ---------------------------------------------------------------- state tokenName: string = "Example Fixed Supply Token"; state tokenSymbol: string = "EFS"; // 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; // The ENTIRE supply, in minor units, issued once by `issue()`. Not shipped as 0: unlike the // reference token, this template cannot be uncapped — a fixed supply of nothing is not a // token, and there is no mint path that could fill it later. One million whole tokens at 13 // decimals. state supplyTotal: uint = 10000000000000000000; // ------------------------------------------------------------------------------------- // `caller` inside a state initializer is the DEPLOYER: Sindri runs the compiler-generated // initializer with the authenticated deployer as caller. The issuer is therefore fixed at // deployment with no claim window, and `issue()` is the only thing it can ever do. state issuer: address = caller; state supply: uint = 0; state issuedEver: uint = 0; state balances: map; // Transfer is the SINGLE record of every balance movement and there is no second one: the // issuance is Transfer("", issuer, supplyTotal) 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); // The standard's event for a minter change, emitted once, with "" as the next minter: // issuance closing is a minter change to nobody, and a wallet watching for it learns that // this token can never mint again at the moment it becomes true. event MinterChanged(previous: address, next: address); // ---- views: the ZK13 fungible interface, complete ------------------------------------- // Which standard this contract was written against. RECOMMENDED rather than required by the // fungible standard, because tokens deployed before that section existed cannot grow a // function — see ZK13_TOKEN_STANDARD.md §9. It is a self-declaration and proves nothing; the // check is `catalog(address)` against the standard's required surface. view fn standard() -> string { return "zk13-fungible/1+fixed-supply"; } view fn name() -> string { return tokenName; } view fn symbol() -> string { return tokenSymbol; } view fn decimals() -> uint { return tokenDecimals; } view fn totalSupply() -> uint { return supply; } // A real cap, not 0-for-uncapped: it is the declared supply, and `issue()` cannot exceed it // because `issue()` sets exactly this number and can only run once. view fn maxSupply() -> uint { return supplyTotal; } // 0 before issuance, `supplyTotal` after, and never anything else. Compare it with // `totalSupply()` to learn how much has been burned. view fn lifetimeIssued() -> uint { return issuedEver; } view fn balanceOf(account: address) -> uint { return balances[account]; } // "" always, and the word for that is: this token has no owner. There is no function in this // contract that any owner could call, so returning the deployer's address here would name a // party with no powers and imply they had some. view fn owner() -> address { return ""; } // The issuer until `issue()` runs, "" forever after. This is the view a holder reads to // learn whether the supply is still open. view fn minter() -> address { return issuer; } // Always false. Required by the standard even for a token that cannot pause, because a // missing function is indistinguishable, to a wallet, from a token whose answer is // inconvenient — and the answer here is the good one: nothing can stop this token. view fn paused() -> bool { return false; } // Convenience for a buyer doing the check described in the header: true exactly when the // supply is issued, closed, and equal to the declared total. view fn issuanceClosed() -> bool { return len(issuer) == 0; } // ---- the one administrative call this token has ---------------------------------------- // Mints the entire declared supply to the issuer and closes issuance permanently, in one // call. There is no argument: an amount argument would make a partial issuance possible, and // a partial issuance is a supply that is not fixed. fn issue() -> bool { require(len(issuer) > 0, "mint closed"); require(caller == issuer, "not minter"); issuedEver = supplyTotal; supply = supplyTotal; balances[caller] = supplyTotal; // Closed in the same call that opens it. The order matters only to a reader — there is no // reentrancy on this chain, because there is no call opcode and nothing inside this // function hands control anywhere — but it is written the way it would have to be written // on a chain where there is. emit Transfer("", caller, supplyTotal); emit MinterChanged(caller, ""); issuer = ""; return true; } // ---- movement --------------------------------------------------------------------------- fn transfer(to: address, amount: uint) -> bool { // Unreachable through a call record — `validateArgs` checksum-validates every `address` // argument before the first opcode — and kept because it is the standard's rule and // because a future caller path that does not run it would need the guard. require(len(to) > 0, "empty recipient"); // The two-statement debit-then-credit below is already safe when `to == caller` on this // VM, because MSTORE writes through and the credit reads the already-debited value. The // inflation appears the moment somebody caches both balances in locals before writing // either. This guard costs a few gas and makes that future edit harmless. Keep it. require(to != caller, "self transfer"); // A balance at this contract's own address could never move again: a contract cannot be // `caller`. It is the one permanent-loss case an address check can detect. 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. // This is the only function in the contract that lowers `totalSupply`, and it can never // raise `lifetimeIssued`, so burning is permanent and provable. fn burn(amount: uint) -> bool { require(balances[caller] >= amount, "insufficient balance"); balances[caller] = balances[caller] - amount; supply = supply - amount; emit Transfer(caller, "", amount); return true; } }