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
- On Asgard
- the same template in the catalogue
Edit the draft
Set the constants the template marks for editing, and name a deployer to see where it would land.
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.
- Save the draft. Download it above, or start from the catalogue text with
strblk contract template faucet > faucet.aresand edit the marked constants yourself. - 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. - Find your fork's RPC. It is the
rpc (Iris, wallets)line of the fork's banner, and therpcUrlof each node in<data root>/fork.json. - 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 point | Gas |
|---|---|
| claim | 4,326 |
| sweep | 2,703 |
- Declare at least
- 43,260 ten times the most expensive call
Functions
| Kind | Name | Inputs | Outputs | Mutability |
|---|---|---|---|---|
| function | drip | uint | view | |
| function | cooldown | uint | view | |
| function | lifetimeCap | uint | view | |
| function | held | uint | view | |
| function | totalPaid | uint | view | |
| function | totalClaims | uint | view | |
| function | claimedBy | account: address | uint | view |
| function | nextClaimAt | account: address | uint | view |
| function | claimable | account: address | bool | view |
| function | owner | address | view | |
| function | paused | bool | view | |
| function | pendingOwnerIs | address | view | |
| function | claim | uint | nonpayable | |
| function | sweep | to: address, amount: uint | uint | nonpayable |
| function | setPaused | next: bool | nonpayable | |
| function | proposeOwner | next: address | nonpayable | |
| function | acceptOwnership | nonpayable | ||
| 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 |
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 = "";
}
}