SourceLessExplorer · SourceLess AsgardSourceLess Asgard · chain 1313
LiveHeight23,717Head age59 sLast interval1 minFinality23,706Behind−11Pending0 / 8,192Tx in head0
XPlorer following the chain as its own read-only node

height 23717 of 23717 on the network. 9 peer(s) connected, 77 in its peer directory; 1013 block(s) followed and 842 pulled in by sync since this follower started. Caught up: this follower holds every block any peer has claimed, so every panel below is the whole chain. Genesis 1cf8655125cc1bbb…, producers genesis.

What this follower is, locally

Following 7 seed(s) — their addresses are not published here, for the reason the peer table gives. Its store is held under its own Cerberus lease — no other node’s store is opened, touched or stopped.

VestingToken for your fork

Status
a draft, not deployed nothing here is on any chain; never send value to a draft
Profile
ZK13 fungible +vesting
Holds
its own token ledger
The property that matters
a grant is immutable and spends the cap at once; release mints vested minus released on the block clock

Edit the draft

Set the constants the template marks for editing, and name a deployer to see where it would land.

string, shipped as "Example Vesting Token"

string, shipped as "EVEST"

uint, shipped as 13

uint, shipped as 0

The form asks this explorer to compile your draft and show it back. It signs nothing, stores nothing and reaches no node.

The draft as shipped

Contract
VestingToken
Compiler
areslang-1.3.0
Bytecode SHA-256
84fa4c79f435ce668c9bbc1c28ef2961105500e0cda468e6921d9af31dac7942
Bytecode
3,782 bytes
Source SHA-256
bf4f8fe4908701b6fae8324e8f4c324e4cf979c7374048377888949eddc5d292
The catalogue text?
Yes, byte for byte: matches the published vector
Ready to deploy?
not yet a deployment refuses while these remain: tokenName = "Example Vesting Token", tokenSymbol = "EVEST"
Would land at
name a deployer to see it

Deploy it to your fork

Only the last step signs, and only it reaches a node.

  1. Save the draft. Download it above, or start from the catalogue text with strblk contract template vesting-token > vesting-token.ares and edit the marked constants yourself.
  2. Dry-run it on your machine. strblk wallet deploy vesting-token.ares --dry-run --address <your address on the fork> compiles it locally and prints its digest and address. They must match this page; if they do not, the file is not this draft.
  3. Find your fork's RPC. It is the rpc (Iris, wallets) line of the fork's banner, and the rpcUrl of each node in <data root>/fork.json.
  4. Deploy it. strblk wallet deploy vesting-token.ares --node <your fork's RPC>, from a wallet whose account holds a proved STR.domain on your fork. Before it signs, the wallet prints the network: the realm reads unregistered and the genesis is your fork's hash. Check that hash; it is what the signature binds.

Measured gas

Charged by the STARW VM under areslang-1.3.0, the same on your fork. Measured, not estimated.

Entry pointGas
grant1,368
release1,220
mint1,144
transfer835
burn774
acceptOwnership612
setMinter471
closeMinting470
proposeOwner412
setPaused363
Declare at least
13,680 ten times the most expensive call

Functions

KindNameInputsOutputsMutability
functionstandardstringview
functionnamestringview
functionsymbolstringview
functiondecimalsuintview
functiontotalSupplyuintview
functionmaxSupplyuintview
functionlifetimeIssueduintview
functionbalanceOfaccount: addressuintview
functiongrantedaccount: addressuintview
functionreleasedaccount: addressuintview
functionvestingStartaccount: addressuintview
functionvestingDurationaccount: addressuintview
functionvestedaccount: addressuintview
functionowneraddressview
functionminteraddressview
functionpausedboolview
functionpendingOwnerIsaddressview
functiontransferto: address, amount: uintboolnonpayable
functionburnamount: uintboolnonpayable
functionmintto: address, amount: uintboolnonpayable
functiongrantbeneficiary: address, total: uint, startMs: uint, durationMs: uintnonpayable
functionreleaseuintnonpayable
functionproposeOwnernext: addressnonpayable
functionacceptOwnershipnonpayable
functionsetPausednext: boolnonpayable
functionsetMinternext: addressnonpayable
functioncloseMintingnonpayable
eventTransferfrom: address, to: address, amount: uint
eventGrantedbeneficiary: address, total: uint, startMs: uint, durationMs: uint
eventOwnerProposedcurrent: address, pending: address
eventOwnerChangedprevious: address, next: address
eventPausedChangedpaused: bool, by: address
eventMinterChangedprevious: address, next: address

Your draft's source

templates/vesting-token.ares, as shipped

// =========================================================================================
// VestingToken — a ZK13 fungible token with a built-in per-beneficiary vesting schedule.
//
// WHAT IT IS FOR
//   A fungible asset whose ledger lives in this contract's own storage, with one extra power
//   the base token does not have: the mint authority can GRANT a beneficiary a total that
//   comes into existence gradually, linearly, over a window the grant fixes. It is the
//   `+vesting` profile of docs/ZK13_TOKEN_STANDARD.md — every view, every mutating function,
//   every event and every `require` message the base standard spells is here, spelled exactly,
//   plus `grant` and `release` and the five views that read a schedule back. This is the token
//   a project uses to pay a team or an investor on a cliffless linear release without a
//   separate escrow contract: the schedule is enforced by the code holders can read, not by a
//   spreadsheet somebody promises to honour.
//
// WHAT IT GUARANTEES
//   * Supply moves only through `mint`, `release` and `burn`. `transfer` moves it by exactly
//     zero. A grant does NOT create tokens: it records a commitment, and the tokens are minted
//     by `release` as they vest. So the circulating supply of a granted-but-unreleased schedule
//     is zero until the beneficiary pulls it.
//   * `maxSupply` bounds LIFETIME ISSUANCE and is immutable. The one counter it is checked
//     against is `committedEver` = everything ever MINTED plus everything ever GRANTED, and
//     `lifetimeIssued()` returns exactly that. A vesting grant is a promise to issue, so it
//     spends cap room the moment it is made, not when it is released — otherwise a schedule
//     could be granted against headroom that a direct mint had already taken, and `release`
//     would mint past the cap one claim at a time with nothing complaining. `committedEver`
//     only ever goes up; `burn` lowers circulating `totalSupply` and never touches it.
//     `maxSupply() == 0` means UNCAPPED, which is what this template ships as.
//   * The sum of everything a schedule ever pays is exactly its `total`, never more, however
//     many times `release` is called and in whatever order. Each `release` pays
//     `vestedNow - alreadyReleased`, never `vestedNow` — paying the vested total on every call
//     is the classic vesting bug and it pays the whole schedule repeatedly. The final release
//     trues up to exactly `total` because at or after the end the vested amount is
//     `total * duration / duration`, which is `total` with no remainder.
//   * A schedule is IMMUTABLE once granted. There is no `revoke`, no `cancel`, no `accelerate`,
//     no `setSchedule` and no second `grant` for the same beneficiary — the mint authority
//     keeps NO power over a grant after making it. Read the ABI: there is no such function to
//     call. The one thing the authority can still do is appoint a new authority or close
//     minting, neither of which reaches an existing schedule.
//   * `release` pays the CALLER's own schedule and mints into the caller's own balance. Unlike
//     the native `vesting.ares`, nobody can trigger it on a beneficiary's behalf, because a
//     mint credits `caller` and a contract cannot be `caller`. A beneficiary with no gas is a
//     beneficiary who has not yet released; the schedule waits, it does not expire.
//   * The owner can pause and can hand over ownership in two steps. The owner CANNOT move,
//     grant away, or release anybody else's balance or schedule: there is no `setBalance`, no
//     `adjust`, no `transferFrom`, no `releaseFor`.
//   * A revert leaves the ledger byte-identical. Storage commits only on success, so a refused
//     release rolls the `released` counter back with the mint that did not happen.
//   * A self-transfer is refused, so the classic debit-then-credit inflation bug cannot be
//     reached at all.
//
// THE CLOCK, AND WHAT THE SCHEDULE'S HONESTY DOES AND DOES NOT REST ON
//   `release` reads the BLOCK's timestamp (ms) and nothing else for its clock — there is no
//   time argument on `release`, so there is no unlock moment the beneficiary can supply. The
//   grantor DOES supply `startMs` and `durationMs` to `grant`, which is correct: defining the
//   schedule is the grantor's job and is done once, visibly, at grant time. A `startMs` in the
//   past vests immediately from that point; a `startMs` in the future is a cliff until then.
//   Either way it is fixed when the grant is made and no function changes it afterwards.
//
//   What the vesting enforces is the RELEASE RATE: the contract will not mint faster than the
//   schedule. What it does not and cannot enforce is the BACKING — a grant is the mint
//   authority's commitment that the total it promised is a total it means to have issuable, and
//   `committedEver` is what holds the authority to that against the cap. There is no collateral
//   here and none is claimed.
//
// HOW IT IS TYPICALLY MISUSED
//   * Reading `granted(b)` as "already received". It is the WHOLE schedule; `released(b)` is
//     what has been minted so far and `vested(b)` is what could be released right now. A
//     beneficiary owed 1,000,000 over a year has `granted` 1,000,000 on day one and `balanceOf`
//     zero until they release.
//   * Expecting the grantor to be able to claw a grant back. It cannot, and that is the point.
//     If your agreement needs revocation this is the wrong template, and the beneficiary must
//     be able to see that in the ABI before accepting the grant.
//   * Granting to a contract address. A contract cannot be `caller`, so it can never call
//     `release`, so a grant to a contract address is a schedule nothing can ever draw —
//     PERMANENT and unrecoverable. The one case it 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.
//   * Deploying and forgetting to `closeMinting`, while telling holders the supply is fixed.
//     `minter()` is in the ABI and a holder will read it; so is every outstanding grant's
//     committed total, through `granted()` and `lifetimeIssued()`.
//   * 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: mint 1,144,
//     transfer 835, burn 774, grant 1,368, release 1,220, proposeOwner 412,
//     acceptOwnership 612, setPaused 363, setMinter 471, closeMinting 470. The most expensive
//     entry point is `grant` at 1,368. Declare a gas limit of at least 14,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. See ZK13_TOKEN_STANDARD.md §4.2.
//   No `revoke`, `cancel`, `accelerate` or `releaseFor`, for the reasons above.
//   No upgrade path and no proxy. What you deploy is what holders get, forever.
//
// A NOTE ON THE LINEAR ARITHMETIC
//   `vestedNow = total * min(now - start, duration) / duration`. The multiply happens before
//   the divide, so the integer division truncates exactly once, at the end. AresLang integers
//   are arbitrary precision (thousands of bits), so `total * elapsed` cannot wrap for any total
//   and duration a token will ever carry — a total below 2^128 and a duration below 2^64 give a
//   product below 2^192, orders of magnitude inside the limit. Intermediate releases may each
//   be one unit short of the true continuous value because of the truncation; the FINAL release
//   is exact, so the schedule always pays out to the last unit and the dust is never lost.
//
// 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(founder, amount)` for the circulating part and `grant(beneficiary, total, startMs,
//   durationMs)` for each schedule.
// =========================================================================================

contract VestingToken {
  // ---- edit these four ----------------------------------------------------------------
  state tokenName: string = "Example Vesting Token";
  state tokenSymbol: string = "EVEST";
  // 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: a template cannot invent a
  // cap for a token it knows nothing about. If your holders are to be told the supply is
  // bounded, set the real number before you deploy — the address changes with it, which is
  // correct — or `closeMinting()` once the whole supply is minted or granted.
  state supplyCap: uint = 0;
  // -------------------------------------------------------------------------------------

  state supply: uint = 0;
  // The ONE counter the cap is enforced against: everything ever MINTED plus everything ever
  // GRANTED, and it never goes down. `supply` cannot be the cap counter — `burn` lowers it — and
  // neither can a mint-only counter, because a grant is a commitment to mint later and a cap
  // that ignored commitments could be over-granted and then released past itself one claim at a
  // time. `release` does NOT add to this: the grant already counted its whole total here.
  state committedEver: 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<address, uint>;

  // The vesting layer, as four FLAT per-beneficiary maps rather than one map of a record: the
  // whole schedule is `grantTotal[b]`, `startMsOf[b]`, `durationMsOf[b]` and `releasedOf[b]`.
  // `grantTotal[b] == 0` is the single "no schedule" sentinel the whole layer keys off, so a
  // grant refuses a zero total and `release`/`vested` treat a zero total as nothing to pay.
  state grantTotal: map<address, uint>;
  state startMsOf: map<address, uint>;
  state durationMsOf: map<address, uint>;
  state releasedOf: map<address, uint>;

  // Transfer is the SINGLE record of every balance movement and there is no second one: a mint
  // and a release are both Transfer("", to, amount), a burn is Transfer(from, "", amount), and a
  // transfer is Transfer(from, to, amount). An indexer summing Transfer must arrive at exactly
  // the balances in storage, which is why a release emits one too.
  event Transfer(from: address, to: address, amount: uint);
  // Emitted by `grant` when a schedule is recorded. It is evidence of the commitment, not a
  // movement — no tokens exist yet — so it is NOT a Transfer. `release` emits the Transfer.
  event Granted(beneficiary: address, total: uint, startMs: uint, durationMs: 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 ----------------------------------------------------------------------------

  // Which standard this contract was written against, and the profile of it. The `+vesting`
  // suffix is how a wallet learns, in one cheap read, that this token has a schedule layer with
  // a `grant`/`release` path. A self-declaration that proves nothing; the check is
  // `catalog(address)` against the standard's required surface.
  view fn standard() -> string {
    return "zk13-fungible/1+vesting";
  }

  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 or granted, and it never goes down. This is the number the cap is
  // checked against — mint and grant both move it, release and burn do not — so a wallet reads
  // remaining mintable as `maxSupply() - lifetimeIssued()` and it is right whether or not any
  // grant has been released yet.
  view fn lifetimeIssued() -> uint {
    return committedEver;
  }

  view fn balanceOf(account: address) -> uint {
    return balances[account];
  }

  // The WHOLE schedule for a beneficiary — the committed total, not what has been released. 0
  // means no schedule. Read `released` and `vested` alongside it.
  view fn granted(account: address) -> uint {
    return grantTotal[account];
  }

  // How much of a schedule has been minted to the beneficiary so far.
  view fn released(account: address) -> uint {
    return releasedOf[account];
  }

  // The schedule's start, in ms, exactly as the grantor fixed it.
  view fn vestingStart(account: address) -> uint {
    return startMsOf[account];
  }

  // The schedule's duration, in ms, exactly as the grantor fixed it.
  view fn vestingDuration(account: address) -> uint {
    return durationMsOf[account];
  }

  // How much of a schedule has vested as of THIS block's timestamp: the amount a `release`
  // called now would bring the beneficiary's released total up to. `vested - released` is what
  // the next release would pay. Returns 0 for an address with no schedule and before the start.
  view fn vested(account: address) -> uint {
    let total = grantTotal[account];
    if (total == 0) {
      return 0;
    }
    // `grant` guarantees a non-zero total has a non-zero duration, so the divide below is safe.
    let duration = durationMsOf[account];
    let start = startMsOf[account];
    let elapsed = 0;
    // Nothing before the start; `timestamp - start` would be a checked subtract that reverts on
    // a negative, so it is only taken when the clock has passed the start.
    if (timestamp > start) {
      elapsed = timestamp - start;
    }
    if (elapsed > duration) {
      elapsed = duration;
    }
    // Multiply before divide: the truncation happens once, at the end, and at or past the end
    // `elapsed == duration` makes this exactly `total`.
    return total * elapsed / duration;
  }

  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. Closing minting
  // also closes granting — a grant is a mint authority power — so an existing schedule can still
  // be released but no new one can be made.
  view fn minter() -> address {
    return mintAuthority;
  }

  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.
    require(len(to) > 0, "empty recipient");
    // The guard costs 8 gas and makes a future caching edit harmless; see zk13-token.ares §6.6.
    require(to != caller, "self transfer");
    // 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.
    require(to != self, "self is not payable");
    // Refuses at the cap rather than truncating. The cap is tested against `committedEver`,
    // which a grant also moves, so a direct mint and a vesting commitment share one ceiling.
    if (supplyCap > 0) {
      require(committedEver + amount <= supplyCap, "supply cap");
    }
    committedEver = committedEver + amount;
    supply = supply + amount;
    balances[to] = balances[to] + amount;
    emit Transfer("", to, amount);
    return true;
  }

  // ---- the vesting layer ----------------------------------------------------------------

  // Records an IMMUTABLE schedule for a beneficiary. The mint authority commits `total` to be
  // released linearly from `startMs` over `durationMs`; the tokens are NOT minted here — they
  // come into existence as the beneficiary releases them. The commitment spends cap room at
  // once, through `committedEver`, because a grant that did not would let the cap be beaten by
  // granting and minting against the same headroom. There is no revoke, cancel or accelerate,
  // and a beneficiary already holding a schedule cannot be granted a second one — "grant exists"
  // protects the first grant's terms from being overwritten.
  fn grant(beneficiary: address, total: uint, startMs: uint, durationMs: uint) {
    require(frozen == false, "paused");
    require(len(mintAuthority) > 0, "mint closed");
    require(caller == mintAuthority, "not minter");
    require(len(beneficiary) > 0, "empty recipient");
    // A schedule to this contract's own address could never be released — a contract is never
    // `caller` — so it would be a permanent, unrecoverable commitment. Refused at the door.
    require(beneficiary != self, "self is not payable");
    require(total > 0, "nothing to grant");
    require(durationMs > 0, "duration must be positive");
    // Immutable: a second grant would silently rewrite the first one's total, start and clock.
    require(grantTotal[beneficiary] == 0, "grant exists");
    // The commitment respects the same cap a mint does, counted the same way.
    if (supplyCap > 0) {
      require(committedEver + total <= supplyCap, "supply cap");
    }
    committedEver = committedEver + total;
    grantTotal[beneficiary] = total;
    startMsOf[beneficiary] = startMs;
    durationMsOf[beneficiary] = durationMs;
    emit Granted(beneficiary, total, startMs, durationMs);
  }

  // The BENEFICIARY pulls their newly-vested portion. It mints `vested - released` into the
  // caller's own balance and nowhere else, so nobody can release on another's behalf and the
  // mint authority has no `releaseFor`. Pays the DIFFERENCE and never the vested total — paying
  // the total on every call is the classic bug that pays the whole schedule repeatedly. The sum
  // of everything a schedule ever pays is exactly its `total`, because the final release lands
  // when `elapsed == duration` and vests `total` with no remainder.
  fn release() -> uint {
    require(frozen == false, "paused");
    let total = grantTotal[caller];
    require(total > 0, "no grant");
    let duration = durationMsOf[caller];
    let start = startMsOf[caller];
    let elapsed = 0;
    // Nothing before the start; the checked subtract is only taken once the clock has passed it.
    if (timestamp > start) {
      elapsed = timestamp - start;
    }
    if (elapsed > duration) {
      elapsed = duration;
    }
    // Multiply before divide — one truncation, at the end — and `total > 0` guarantees
    // `duration > 0`, so the divide cannot be by zero.
    let vestedNow = total * elapsed / duration;
    let alreadyReleased = releasedOf[caller];
    let claimable = vestedNow - alreadyReleased;
    require(claimable > 0, "nothing to release");
    // Effects before the mint, and the mint is an ordinary credit of this token. A revert rolls
    // the released counter back with the supply that was not created.
    releasedOf[caller] = alreadyReleased + claimable;
    supply = supply + claimable;
    balances[caller] = balances[caller] + claimable;
    emit Transfer("", caller, claimable);
    return claimable;
  }

  // ---- 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 closes both minting and granting permanently. 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 = "";
  }
}