height 23768 of 23768 on the network. 9 peer(s) connected, 77 in its peer directory; 1064 block(s) followed and 887 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
- Status
- template — not deployed A template is a draft, not a deployed contract. Nothing here is on the chain: it has no address until somebody deploys it, and where it lands depends on who deploys it and with which salt. Never send value to a template.
- File
templates/faucet.ares- Profile
- native STR faucet
- Holds
- native STR
- The property that matters
- one drip per address per cooldown, and a lifetime cap per address
- Compiler in force
areslang-1.3.0- Bytecode
- 1,954 bytes
- Bytecode SHA-256
2b4b7174445e64b3b7a4008c44734d00d74a495e864687c427d38e4a9164d03e- Source SHA-256
011e7baa07285a9682f936d6f3d23ab20f8cf05f6d3fc0db7e03c23debd37904- Published vector
- matches the published vector spec/conformance-vectors.json pins 2b4b7174445e64b3… under areslang-1.3.0
- Catalogue
- every template
Before you deploy
The header marks these for editing before you deploy. The values below are the examples it ships with; deployed unedited, they are what every holder sees for the life of the contract.
| Constant | Type | Shipped as |
|---|---|---|
dripAmount | uint | 10000000000000 |
cooldownMs | uint | 86400000 |
lifetimeCapMinor | uint | 100000000000000 |
Where your deployment would land
Name a deployer to see where its deployment of this template, unedited, would land.
The chain derives a contract’s address from its name, its bytecode, the deployer and the salt. This is that function over the bytecode above, so it holds only for the template exactly as shown and only while the chain runs the same compiler. Edit one constant and the address moves, which is correct: different code is a different contract. Nothing is at this address until somebody deploys there.
Measured gas
What the STARW VM charged for each call on a real chain, under areslang-1.3.0, and asserted exactly by the catalogue test: a figure that moved would fail the build. Measured, not estimated. A call that stores a string costs more for a longer one; those were measured with ipfs://QmExampleTokenMetadataDocument01.
| Entry point | Gas |
|---|---|
| claim | 4,326 |
| sweep | 2,703 |
- Declare at least
- 43,260 ten times the most expensive call; post-mining covers only a tenth of the declared limit
ABI
| 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 |
From the command line
Print the source to a file, edit the marked constants, dry-run it to see the digest and your address, then deploy. Only the last command signs, and it is the only one that needs your key and a node.
strblk contract template faucet > faucet.ares strblk contract template faucet --info strblk wallet deploy faucet.ares --dry-run --address <your address> strblk wallet deploy faucet.ares --node <your node's RPC>
Source
templates/faucet.ares, as embedded in this build
// =========================================================================================
// 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 = "";
}
}