SourceLessExplorer · SourceLess AsgardSourceLess Asgard · chain 1313
LiveHeight23,717Head age32 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.

StakingToken for your fork

Status
a draft, not deployed nothing here is on any chain; never send value to a draft
Profile
staking token
Holds
its own token ledger
The property that matters
a staker cannot withdraw more than they staked, and unstake is not pausable

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 Staking Token"

string, shipped as "EST"

uint, shipped as 13

uint, shipped as 0

uint, shipped as 500

uint, shipped as 63072000000

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
StakingToken
Compiler
areslang-1.3.0
Bytecode SHA-256
06b5aaec3d858b84dc1fc6f77964cc78a62bb477f14a6e833bda304e2396dc7c
Bytecode
5,720 bytes
Source SHA-256
dc0ea23b463f9c8c5a6756512e5e957311a11dc64b08628b785d247f1782bd01
The catalogue text?
Yes, byte for byte: matches the published vector
Ready to deploy?
not yet a deployment refuses while these remain: tokenName = "Example Staking Token", tokenSymbol = "EST"
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 staking-token > staking-token.ares and edit the marked constants yourself.
  2. Dry-run it on your machine. strblk wallet deploy staking-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 staking-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
unstake3,603
stake(top-up)2,560
unstake(paused)2,509
stake2,276
claim2,200
fundRewards1,272
mint1,144
transfer835
burn774
Declare at least
36,030 ten times the most expensive call

Functions

KindNameInputsOutputsMutability
functiontransferto: address, amount: uintboolnonpayable
functionburnamount: uintboolnonpayable
functionmintto: address, amount: uintboolnonpayable
functionfundRewardsamount: uintuintnonpayable
functionstakeamount: uintuintnonpayable
functionclaimuintnonpayable
functionunstakeamount: uintuintnonpayable
functionproposeOwnernext: addressnonpayable
functionacceptOwnershipnonpayable
functionsetPausednext: boolnonpayable
functioncloseMintingnonpayable
functionnamestringview
functionsymbolstringview
functiondecimalsuintview
functiontotalSupplyuintview
functionmaxSupplyuintview
functionlifetimeIssueduintview
functionbalanceOfaccount: addressuintview
functionowneraddressview
functionminteraddressview
functionpausedboolview
functionstakeOfaccount: addressuintview
functionstakedSinceOfaccount: addressuintview
functionstakinguintview
functionpooluintview
functionrateBpsuintview
functionrewardOwedOfaccount: addressuintview
functionpendingRewardaccount: addressuintview
eventTransferfrom: address, to: address, amount: uint
eventOwnerProposedcurrent: address, pending: address
eventOwnerChangedprevious: address, next: address
eventPausedChangedpaused: bool, by: address
eventMinterChangedprevious: address, next: address
eventRewardsFundedby: address, amount: uint, pool: uint
eventStakedwho: address, amount: uint, position: uint
eventUnstakedwho: address, amount: uint, position: uint
eventRewardPaidwho: address, amount: uint, pool: uint

Your draft's source

templates/staking-token.ares, as shipped

// =========================================================================================
// StakingToken — a ZK13 token whose holders can lock their balance and accrue a reward.
//
// WHY THE TOKEN AND THE STAKING ARE ONE CONTRACT, AND NOT TWO
//   Because they have to be. A ZK13 token's ledger lives in its own contract storage, and
//   AresLang has no cross-contract calls, so a separate staking contract could never move a
//   holder's token balance — and a balance recorded at a staking contract's address could
//   never move again at all, because a contract cannot be `caller`. Staking a ZK13 token is
//   therefore only writable INSIDE the token contract. This is that.
//
//   The same reasoning is why this template does not stake native STR. A staker would have
//   to send STR with a plain transfer, and THE CONTRACT CANNOT SEE WHO PAID IT: a call
//   record carries no value and there is no deposit hook. Every per-depositor native pattern
//   — staking, a token sale, a shared vault — is unwritable for that one reason, and no
//   amount of contract code fixes it.
//
// WHAT IT GUARANTEES
//   * A staker can never withdraw more principal than they staked. `unstake` is a checked
//     subtraction against that staker's own `staked` slot, keyed by `caller`.
//   * A staker can never touch another staker's position. Every position is keyed by
//     `caller` and there is no argument anywhere that names whose position to move.
//   * Reward is accrued from the BLOCK's timestamp. There is no time argument in the ABI.
//   * Reward is paid only out of the declared reward pool, and never more than the pool
//     holds, so rewards can never mint supply out of nothing.
//   * A reward that cannot be paid is CARRIED, never destroyed. When the pool is short, or
//     while the contract is paused, the settlement pays what it can and the remainder stays in
//     `rewardOwed` — readable as `rewardOwedOf(account)` — to be paid by a later call once the
//     pool is refilled. `claim` accepts a caller with a debt and no position, so a debt
//     outlives the stake that earned it.
//   * Principal is ALWAYS recoverable. Not "usually": `unstake` is the one mutating function
//     in this contract that the pause does not gate, so an exhausted pool cannot block a
//     withdrawal and neither can the owner. The reward is held by a pause; the stake is not.
//   * The supply cap bounds LIFETIME ISSUANCE (`lifetimeIssued()`), not the circulating
//     `totalSupply` that `burn` lowers. A cap checked against a burnable number is defeated by
//     mint, burn, mint — measured on this template before the fix: 4,000 issued against a cap
//     of 1,000. `maxSupply() == 0` means uncapped, which is what this ships as.
//   * totalSupply is unchanged by staking. Staked tokens are moved to this contract's own
//     address (`self`), which is a balance slot like any other; the sum of every balance plus
//     nothing else still equals `totalSupply`.
//
// THE ACCRUAL BLOCK APPEARS THREE TIMES AND MUST STAY IDENTICAL
//   AresLang has no user-defined functions, so the reward calculation cannot be factored out.
//   It is copied verbatim into `stake`, `claim` and `unstake` between the ACCRUAL BEGIN and
//   ACCRUAL END markers. IF YOU CHANGE ONE, CHANGE ALL THREE. A reward formula that differs
//   between claim and unstake is a hole that pays out on whichever path is more generous.
//   This is not left to the reader: `tests/integration/template-staking.integration.test.ts`
//   extracts all three blocks from this file and fails if they are not character-for-character
//   the same apart from the copy number, so a drifted copy cannot ship.
//
// HOW IT IS TYPICALLY MISUSED
//   * Expecting guaranteed yield. Rewards are first come, first served out of a finite pool
//     anyone can fund with `fundRewards`, and a DEBT IS NOT A GUARANTEE: it records what a
//     staker is owed and nothing in this contract mints to cover it. A refill is taken by
//     whoever settles first — one large position can absorb an entire refill and leave every
//     other debt untouched — so the ordering is a race, and the only fix is to fund the pool
//     for the maximum accrual of the maximum stake, or to say plainly that yield is
//     best-effort. What the debt DOES guarantee is that nobody's earned reward is destroyed by
//     their own claim, which is what happened before it existed: a claim against a short pool
//     paid the clamped amount and restarted the clock, and the difference was gone.
//   * Reading the pause as covering everything. It does not, deliberately: `unstake` runs
//     while paused. An owner can stop minting, transfers, staking, funding and every reward
//     payment; the one thing an owner cannot do is keep a staker from their own principal.
//   * Forgetting who holds the pause and that there is no `renounceOwnership`. `transfer` IS
//     gated by it, so an owner who pauses and then loses the key leaves a token whose balances
//     nobody can ever move again — stakes excepted. That is not confiscation and nothing is
//     destroyed, but it is a dead token, and the code is immutable. Read `owner()` before you
//     hold this, and hand ownership to a key that will still exist in five years.
//   * Believing this confers consensus weight. It does not. Validator admission is declared
//     in the genesis and is not chain state; a staking contract confers no consensus power of
//     any kind, and a template implying otherwise is claiming something the chain does not
//     implement.
//   * Adding to a stake and expecting the clock not to move. `stake` settles the accrued
//     reward first and then restarts the clock on the whole position. That is the correct
//     order — computing a reward from a `lastUpdate` the same call has already overwritten is
//     the classic staking bug — but it means topping up resets nothing you have already been
//     paid and everything you have not yet earned.
//   * Under-declaring the gas limit. MEASURED: mint 1,144, transfer 835, burn 774,
//     fundRewards 1,272, stake 2,276 on a fresh position and 2,560 on a top-up that settles a
//     reward, claim 2,200, unstake 3,603 and 2,509 while paused. Declare at least 38,000.
// =========================================================================================

contract StakingToken {
  // ---- edit these before deploying -------------------------------------------------------
  state tokenName: string = "Example Staking Token";
  state tokenSymbol: string = "EST";
  state tokenDecimals: uint = 13;
  // A ceiling on LIFETIME ISSUANCE, not on circulating supply. SHIPPED AT 0, WHICH IS
  // UNCAPPED: a template cannot invent a cap for a token it knows nothing about, and a
  // plausible-looking placeholder reads as a decision somebody made. Set the real number
  // before you deploy, or `closeMinting()` when issuance is done and prove the bound instead
  // of promising it.
  state supplyCap: uint = 0;
  // Reward rate in basis points per year: 500 = 5% a year, simple, not compounded.
  state rewardBpsPerYear: uint = 500;
  // The longest window a single position accrues over, in milliseconds. 730 days. A capped
  // window is what keeps `staked * elapsed * rate` from growing without bound until it
  // reverts at the 4096-bit integer ceiling and strands the position.
  state maxAccrualMs: uint = 63072000000;
  // -------------------------------------------------------------------------------------

  // 10000 bps * 365 days in ms. The denominator of the reward formula, written once here so
  // the three copies of the accrual block cannot drift apart on it.
  state bpsYearMs: uint = 315360000000000;

  state supply: uint = 0;
  // Everything `mint` has ever created, and it never goes down. This — not `supply`, which
  // `burn` lowers — is what `supplyCap` bounds. See the header: a cap tested against a
  // burnable number is defeated by mint, burn, mint.
  state issuedEver: uint = 0;
  state contractOwner: address = caller;
  state pendingOwner: address = "";
  state mintAuthority: address = caller;
  state frozen: bool = false;

  state rewardPool: uint = 0;
  state totalStaked: uint = 0;

  state balances: map<address, uint>;
  state staked: map<address, uint>;
  state stakedSince: map<address, uint>;
  // Reward that was earned and could not be paid, because the pool was short of it or the
  // contract was paused. It is carried, not forgotten. Without this slot a settlement that
  // paid less than it owed still restarted the clock, so the difference was destroyed by the
  // staker's own call — silently, with a RewardPaid event for the smaller number.
  state rewardOwed: map<address, uint>;

  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);
  event RewardsFunded(by: address, amount: uint, pool: uint);
  event Staked(who: address, amount: uint, position: uint);
  event Unstaked(who: address, amount: uint, position: uint);
  event RewardPaid(who: address, amount: uint, pool: uint);

  // ---- the token, exactly as ZK13Token defines it -----------------------------------------

  fn transfer(to: address, amount: uint) -> bool {
    require(frozen == false, "paused");
    require(len(to) > 0, "empty recipient");
    require(to != caller, "self transfer");
    // A transfer to the CONTRACT itself is unrecoverable loss, and here it is worse than the
    // usual kind: `balances[self]` is load-bearing accounting - it is `rewardPool + totalStaked`
    // by construction, and every withdrawal path is bounded by one of those two counters. Value
    // arriving here without a matching counter can never move again by any path. One opcode.
    require(to != self, "self is not payable; use fundRewards");
    require(balances[caller] >= amount, "insufficient balance");
    balances[caller] = balances[caller] - amount;
    balances[to] = balances[to] + amount;
    emit Transfer(caller, to, amount);
    return true;
  }

  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");
    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;
  }

  // ---- the reward pool ---------------------------------------------------------------------

  // Moves the caller's own tokens into the pool. Anybody may fund it; nobody can take from
  // it except a staker accruing a reward, and there is no function that empties it.
  fn fundRewards(amount: uint) -> uint {
    require(frozen == false, "paused");
    require(amount > 0, "amount must be positive");
    require(balances[caller] >= amount, "insufficient balance");
    balances[caller] = balances[caller] - amount;
    balances[self] = balances[self] + amount;
    rewardPool = rewardPool + amount;
    emit Transfer(caller, self, amount);
    emit RewardsFunded(caller, amount, rewardPool);
    return rewardPool;
  }

  // ---- staking -----------------------------------------------------------------------------

  fn stake(amount: uint) -> uint {
    require(frozen == false, "paused");
    require(amount > 0, "amount must be positive");
    require(balances[caller] >= amount, "insufficient balance");

    // ---- ACCRUAL BEGIN (copy 1 of 3) ----
    let earned = 0;
    if (staked[caller] > 0) {
      let elapsed = timestamp - stakedSince[caller];
      if (elapsed > maxAccrualMs) {
        elapsed = maxAccrualMs;
      }
      earned = staked[caller] * elapsed * rewardBpsPerYear / bpsYearMs;
    }
    // What is owed is what was carried plus what this window earned. The clamp below decides
    // how much of it can be PAID now; the remainder stays owed rather than being dropped.
    let owed = rewardOwed[caller] + earned;
    let reward = owed;
    if (rewardPool < owed) {
      reward = rewardPool;
    }
    // A pause holds the POOL, which is the contract's money. `stake` and `claim` can never
    // reach this line — both refuse to run while frozen — and it is here for `unstake`, which
    // deliberately does run while frozen and must return the principal without paying out of a
    // pool the owner has stopped. Nothing is lost by it: the whole amount stays owed.
    if (frozen == true) {
      reward = 0;
    }
    rewardOwed[caller] = owed - reward;
    if (reward > 0) {
      rewardPool = rewardPool - reward;
      balances[self] = balances[self] - reward;
      balances[caller] = balances[caller] + reward;
      emit Transfer(self, caller, reward);
      emit RewardPaid(caller, reward, rewardPool);
    }
    // ---- ACCRUAL END ----

    balances[caller] = balances[caller] - amount;
    balances[self] = balances[self] + amount;
    staked[caller] = staked[caller] + amount;
    stakedSince[caller] = timestamp;
    totalStaked = totalStaked + amount;
    emit Transfer(caller, self, amount);
    emit Staked(caller, amount, staked[caller]);
    return staked[caller];
  }

  fn claim() -> uint {
    require(frozen == false, "paused");
    // A debt outlives the position it was earned by: a staker whose reward could not be paid
    // when they unstaked must still be able to collect it once the pool is refilled.
    require(staked[caller] > 0 || rewardOwed[caller] > 0, "nothing staked and nothing owed");

    // ---- ACCRUAL BEGIN (copy 2 of 3) ----
    let earned = 0;
    if (staked[caller] > 0) {
      let elapsed = timestamp - stakedSince[caller];
      if (elapsed > maxAccrualMs) {
        elapsed = maxAccrualMs;
      }
      earned = staked[caller] * elapsed * rewardBpsPerYear / bpsYearMs;
    }
    // What is owed is what was carried plus what this window earned. The clamp below decides
    // how much of it can be PAID now; the remainder stays owed rather than being dropped.
    let owed = rewardOwed[caller] + earned;
    let reward = owed;
    if (rewardPool < owed) {
      reward = rewardPool;
    }
    // A pause holds the POOL, which is the contract's money. `stake` and `claim` can never
    // reach this line — both refuse to run while frozen — and it is here for `unstake`, which
    // deliberately does run while frozen and must return the principal without paying out of a
    // pool the owner has stopped. Nothing is lost by it: the whole amount stays owed.
    if (frozen == true) {
      reward = 0;
    }
    rewardOwed[caller] = owed - reward;
    if (reward > 0) {
      rewardPool = rewardPool - reward;
      balances[self] = balances[self] - reward;
      balances[caller] = balances[caller] + reward;
      emit Transfer(self, caller, reward);
      emit RewardPaid(caller, reward, rewardPool);
    }
    // ---- ACCRUAL END ----

    // The clock restarts whether or not anything was paid, so the same elapsed time can
    // never be claimed twice.
    stakedSince[caller] = timestamp;
    return reward;
  }

  // NOT gated on the pause, and that is the whole point of it. A pause that holds the
  // PRINCIPAL is a pause that takes it: the owner sets one bool and every staker's own tokens
  // are frozen for as long as the key that set it stays silent, with no renounce and no
  // second key anywhere in this contract that could end it. The reward is a different
  // question and IS held by the pause — the accrual block pays nothing while frozen and
  // carries the whole amount as a debt — so a pause stops the contract paying out of its pool
  // without ever standing between a staker and their own stake.
  fn unstake(amount: uint) -> uint {
    require(amount > 0, "amount must be positive");
    // The checked subtraction that bounds a withdrawal by what this caller actually staked.
    require(staked[caller] >= amount, "insufficient stake");

    // ---- ACCRUAL BEGIN (copy 3 of 3) ----
    let earned = 0;
    if (staked[caller] > 0) {
      let elapsed = timestamp - stakedSince[caller];
      if (elapsed > maxAccrualMs) {
        elapsed = maxAccrualMs;
      }
      earned = staked[caller] * elapsed * rewardBpsPerYear / bpsYearMs;
    }
    // What is owed is what was carried plus what this window earned. The clamp below decides
    // how much of it can be PAID now; the remainder stays owed rather than being dropped.
    let owed = rewardOwed[caller] + earned;
    let reward = owed;
    if (rewardPool < owed) {
      reward = rewardPool;
    }
    // A pause holds the POOL, which is the contract's money. `stake` and `claim` can never
    // reach this line — both refuse to run while frozen — and it is here for `unstake`, which
    // deliberately does run while frozen and must return the principal without paying out of a
    // pool the owner has stopped. Nothing is lost by it: the whole amount stays owed.
    if (frozen == true) {
      reward = 0;
    }
    rewardOwed[caller] = owed - reward;
    if (reward > 0) {
      rewardPool = rewardPool - reward;
      balances[self] = balances[self] - reward;
      balances[caller] = balances[caller] + reward;
      emit Transfer(self, caller, reward);
      emit RewardPaid(caller, reward, rewardPool);
    }
    // ---- ACCRUAL END ----

    staked[caller] = staked[caller] - amount;
    stakedSince[caller] = timestamp;
    totalStaked = totalStaked - amount;
    balances[self] = balances[self] - amount;
    balances[caller] = balances[caller] + amount;
    emit Transfer(self, caller, amount);
    emit Unstaked(caller, amount, staked[caller]);
    return staked[caller];
  }

  // ---- ownership, pause, minting -------------------------------------------------------------

  fn proposeOwner(next: address) {
    require(caller == contractOwner, "not owner");
    pendingOwner = next;
    emit OwnerProposed(contractOwner, next);
  }

  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 closeMinting() {
    require(caller == contractOwner, "not owner");
    require(len(mintAuthority) > 0, "mint closed");
    emit MinterChanged(mintAuthority, "");
    mintAuthority = "";
  }

  // ---- 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, and the number `maxSupply` actually bounds.
  view fn lifetimeIssued() -> uint {
    return issuedEver;
  }

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

  view fn owner() -> address {
    return contractOwner;
  }

  view fn minter() -> address {
    return mintAuthority;
  }

  view fn paused() -> bool {
    return frozen;
  }

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

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

  view fn staking() -> uint {
    return totalStaked;
  }

  view fn pool() -> uint {
    return rewardPool;
  }

  view fn rateBps() -> uint {
    return rewardBpsPerYear;
  }

  // What was earned and could not be paid. It is a claim on the reward pool, not a promise
  // that the pool can ever pay it: nothing in this contract mints to cover a debt.
  view fn rewardOwedOf(account: address) -> uint {
    return rewardOwed[account];
  }

  // What `claim` would pay right now, at this block's time — carried debt included, clamped
  // to the pool exactly as the accrual block clamps it. A wallet can show it without moving
  // anything. While `paused()` is true this is what a claim WOULD pay once the pause ends,
  // because a frozen contract pays no reward at all.
  view fn pendingReward(account: address) -> uint {
    let earned = 0;
    if (staked[account] > 0) {
      let elapsed = timestamp - stakedSince[account];
      if (elapsed > maxAccrualMs) {
        elapsed = maxAccrualMs;
      }
      earned = staked[account] * elapsed * rewardBpsPerYear / bpsYearMs;
    }
    let owed = rewardOwed[account] + earned;
    let reward = owed;
    if (rewardPool < owed) {
      reward = rewardPool;
    }
    return reward;
  }
}