SourceLessExplorer · SourceLess AsgardSourceLess Asgard · chain 1313
LiveHeight23,744Head age4 minLast interval3 minFinality23,733Behind−11Pending0 / 8,192Tx in head0
XPlorer following the chain as its own read-only node

height 23744 of 23744 on the network. 9 peer(s) connected, 77 in its peer directory; 1040 block(s) followed and 864 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.

Faucet for your fork

Status
a draft, not deployed nothing here is on any chain; never send value to a draft
Profile
native STR faucet
Holds
native STR
The property that matters
one drip per address per cooldown, and a lifetime cap per address

Edit the draft

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

uint, shipped as 10000000000000

uint, shipped as 86400000

uint, shipped as 100000000000000

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
Faucet
Compiler
areslang-1.3.0
Bytecode SHA-256
2b4b7174445e64b3b7a4008c44734d00d74a495e864687c427d38e4a9164d03e
Bytecode
1,954 bytes
Source SHA-256
011e7baa07285a9682f936d6f3d23ab20f8cf05f6d3fc0db7e03c23debd37904
The catalogue text?
Yes, byte for byte: matches the published vector
Ready to deploy?
no catalogue examples left
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 faucet > faucet.ares and edit the marked constants yourself.
  2. Dry-run it on your machine. strblk wallet deploy faucet.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 faucet.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
claim4,326
sweep2,703
Declare at least
43,260 ten times the most expensive call

Functions

KindNameInputsOutputsMutability
functiondripuintview
functioncooldownuintview
functionlifetimeCapuintview
functionhelduintview
functiontotalPaiduintview
functiontotalClaimsuintview
functionclaimedByaccount: addressuintview
functionnextClaimAtaccount: addressuintview
functionclaimableaccount: addressboolview
functionowneraddressview
functionpausedboolview
functionpendingOwnerIsaddressview
functionclaimuintnonpayable
functionsweepto: address, amount: uintuintnonpayable
functionsetPausednext: boolnonpayable
functionproposeOwnernext: addressnonpayable
functionacceptOwnershipnonpayable
eventClaimedwho: address, amount: uint, atMs: uint
eventSweptto: address, amount: uint, by: address
eventPausedChangedpaused: bool, by: address
eventOwnerProposedcurrent: address, pending: address
eventOwnerChangedprevious: address, next: address

Your draft's source

templates/faucet.ares, as shipped

// =========================================================================================
// Faucet — a fixed drip of native STR, one per caller per cooldown, with a declared sweep.
//
// WHAT IT IS FOR
//   Handing out small amounts of STR to whoever asks: a testnet tap, an onboarding allowance,
//   a demo wallet top-up. It is also the catalogue's worked example of PER-CALLER RATE
//   LIMITING, which is a pattern every contract that gives something away needs and which no
//   other template here shows.
//
// THE RATE LIMIT, AND WHAT IT IS AND IS NOT WORTH
//   The limit is per ADDRESS: one drip per `cooldown()` milliseconds, from the sealed block's
//   timestamp. **It is not a limit per person, and cannot be.** Addresses are free to create,
//   so anyone willing to make n addresses can draw n drips — and the only reason this faucet is
//   not drained in one block is a fact about the CHAIN and not about this contract: a contract
//   call requires a proved STR.domain, which costs 1000 STR locked. That is the sybil cost, it
//   is enforced by `contractCallerGate` before this bytecode runs, and it is what makes the
//   per-address limit mean something. If that gate ever loosens, this faucet's protection
//   loosens with it.
//
//   The second limit is a lifetime cap per address, which bounds the total one address can ever
//   take however long it waits. Set it to 0 only if you mean "forever, without limit".
//
// WHAT IT GUARANTEES
//   * A caller cannot take twice inside the cooldown. The timestamp is written BEFORE the
//     transfer, from the block being executed, so no caller argument and no node clock is
//     involved anywhere.
//   * A caller cannot take more than the lifetime cap in total, across any number of calls.
//   * Nobody can take another caller's drip or make a call on their behalf: `claim()` has no
//     arguments and pays `caller`.
//   * The drip amount and the cooldown are fixed at deployment. There is no setter for either;
//     a faucet whose owner can change the terms between two calls is not a rate limit, it is
//     a preference.
//   * A revert leaves the books and the balance byte-identical. Storage and value commit
//     together, only on success.
//
// THE POWER IT DELIBERATELY HAS, DECLARED
//   **`sweep(to, amount)` lets the owner take the remaining balance back.** A faucet is funded
//   as a gift and usually needs winding up, so the power is here — but it is stated rather than
//   hidden, it is in the ABI, and every use of it emits `Swept`. What it CANNOT do is take back
//   a drip somebody has already claimed; it moves only what is still in this contract.
//
//   An operator who wants a faucet with no such power deletes `sweep` before deploying. The
//   address changes with it, which is correct, and `owner()` then names a key whose only
//   remaining power is the pause.
//
// HOW IT IS TYPICALLY MISUSED
//   * Deploying it on a network where the sybil cost is low and calling the per-address limit
//     protection. See above — it is the domain gate doing that work, not this contract.
//   * Funding it before deploying. A contract address cannot receive until the contract
//     exists: the receive gate answers from the deployed set. Deploy, then fund.
//   * Setting the drip near the balance. There is no queue and no fairness rule: the fastest
//     caller takes the last drip, and every later call reverts with `faucet is dry` until
//     somebody refills it.
//   * Expecting the faucet to know who funded it. It cannot: a call record carries no value
//     and there is no deposit hook, so any number of parties may pay it and none of them is
//     recorded. The sweep pays whoever the OWNER names, not whoever put the money in.
//   * 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: claim 4,326 — of which
//     2,100 is the `TRANSFER` and 400 the balance read — sweep 2,703, setPaused 363,
//     proposeOwner 412, acceptOwnership 612. Declare a gas limit of at least 44,000.
//
// TO USE IT
//   Edit the three constants below, deploy, fund the address by an ordinary transfer, and tell
//   people to call `claim()`.
// =========================================================================================

contract Faucet {
  // ---- edit these three ----------------------------------------------------------------
  // One drip, in MINOR units. At 13 decimals, 10000000000000 is one whole STR.
  state dripAmount: uint = 10000000000000;
  // The wait between two claims by the same address, in milliseconds. 86400000 is 24 hours.
  state cooldownMs: uint = 86400000;
  // The most one address may ever take in total, in minor units. 0 means no lifetime limit,
  // which is a decision and not a default — say it deliberately.
  state lifetimeCapMinor: uint = 100000000000000;
  // -------------------------------------------------------------------------------------

  // `caller` inside a state initializer is the DEPLOYER: Sindri runs the compiler-generated
  // initializer with the authenticated deployer as caller, so there is no claim race.
  state contractOwner: address = caller;
  state pendingOwner: address = "";
  state frozen: bool = false;

  state paidTotal: uint = 0;
  state claimCount: uint = 0;

  // 0 means this address has never claimed. The cooldown arithmetic below relies on that:
  // `0 + cooldown` is smaller than any real block time, so a first claim is never blocked.
  state lastClaimAt: map<address, uint>;
  state claimedByOf: map<address, uint>;

  event Claimed(who: address, amount: uint, atMs: uint);
  event Swept(to: address, amount: uint, by: address);
  event PausedChanged(paused: bool, by: address);
  event OwnerProposed(current: address, pending: address);
  event OwnerChanged(previous: address, next: address);

  // ---- views ----------------------------------------------------------------------------

  view fn drip() -> uint {
    return dripAmount;
  }

  view fn cooldown() -> uint {
    return cooldownMs;
  }

  view fn lifetimeCap() -> uint {
    return lifetimeCapMinor;
  }

  // What the faucet holds right now, read from the native ledger rather than from a mirror in
  // state: a cached balance is a second copy of the truth and diverges the moment anybody pays
  // this contract without calling it. Divide by `drip()` for how many claims are left.
  view fn held() -> uint {
    return balance(self);
  }

  view fn totalPaid() -> uint {
    return paidTotal;
  }

  view fn totalClaims() -> uint {
    return claimCount;
  }

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

  // The earliest block time at which this address may claim again, or 0 if it never has.
  view fn nextClaimAt(account: address) -> uint {
    if (lastClaimAt[account] == 0) {
      return 0;
    }
    return lastClaimAt[account] + cooldownMs;
  }

  // Everything `claim()` checks, as one answer, so a wallet can grey out a button instead of
  // spending gas on a call that reverts.
  view fn claimable(account: address) -> bool {
    if (frozen) {
      return false;
    }
    if (timestamp < lastClaimAt[account] + cooldownMs) {
      return false;
    }
    if (lifetimeCapMinor > 0 && claimedByOf[account] + dripAmount > lifetimeCapMinor) {
      return false;
    }
    return balance(self) >= dripAmount;
  }

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

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

  view fn pendingOwnerIs() -> address {
    return pendingOwner;
  }

  // ---- the drip ---------------------------------------------------------------------------

  // Pays the CALLER one drip. No arguments, because an argument naming the recipient would
  // make one address able to farm the faucet on behalf of many.
  fn claim() -> uint {
    require(frozen == false, "paused");
    // `lastClaimAt` is 0 for an address that has never claimed, and `0 + cooldown` is below any
    // real block time, so this permits a first claim without a special case for it.
    require(timestamp >= lastClaimAt[caller] + cooldownMs, "cooldown");
    if (lifetimeCapMinor > 0) {
      require(claimedByOf[caller] + dripAmount <= lifetimeCapMinor, "lifetime cap");
    }
    require(balance(self) >= dripAmount, "faucet is dry");
    // Effects before the interaction. The cooldown is stamped, the totals are written, and only
    // then does value move — so on a chain where a recipient could call back in (this one
    // cannot: there is no call opcode) the second claim would already be blocked.
    lastClaimAt[caller] = timestamp;
    claimedByOf[caller] = claimedByOf[caller] + dripAmount;
    paidTotal = paidTotal + dripAmount;
    claimCount = claimCount + 1;
    emit Claimed(caller, dripAmount, timestamp);
    transfer(caller, dripAmount);
    return dripAmount;
  }

  // ---- the owner's powers, both of them declared -------------------------------------------

  // Takes unclaimed balance back out. It cannot reach a drip already claimed — that is in the
  // claimant's own account — and every use of it is on the record as `Swept`.
  fn sweep(to: address, amount: uint) -> uint {
    require(caller == contractOwner, "not owner");
    require(len(to) > 0, "empty recipient");
    require(amount > 0, "amount must be positive");
    require(to != self, "self is not payable");
    require(balance(self) >= amount, "insufficient balance");
    emit Swept(to, amount, caller);
    transfer(to, amount);
    return amount;
  }

  // Stops `claim` and no view, so a stopped faucet stays readable. It does not stop `sweep`:
  // winding a faucet up is precisely the case where both are wanted at once.
  fn setPaused(next: bool) {
    require(caller == contractOwner, "not owner");
    frozen = next;
    emit PausedChanged(next, caller);
  }

  // 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 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 = "";
  }
}