height 23774 of 23774 on the network. 9 peer(s) connected, 77 in its peer directory; 1070 block(s) followed and 893 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.
Multisig
- 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/multisig.ares- Profile
- native STR multisig
- Holds
- native STR
- The property that matters
- one owner is one approval; M cannot be cleared by one key calling M times
- Compiler in force
areslang-1.3.0- Bytecode
- 2,338 bytes
- Bytecode SHA-256
6e38251a4c314186ed6908d41e364bd5f1c2a7f0b4b1d3de3d352277008d8f9c- Source SHA-256
00d42edfd017a21b48e08f152993f49ed8dfbaeceb85a34949cab1417ae02e0b- Published vector
- matches the published vector spec/conformance-vectors.json pins 6e38251a4c314186… 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 |
|---|---|---|
maxOwners | uint | 20 |
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 |
|---|---|
| execute | 3,380 |
| propose | 1,400 |
| approve | 938 |
| addOwner | 919 |
| seal | 736 |
- Declare at least
- 33,800 ten times the most expensive call; post-mining covers only a tenth of the declared limit
ABI
| Kind | Name | Inputs | Outputs | Mutability |
|---|---|---|---|---|
| function | addOwner | who: address | uint | nonpayable |
| function | seal | requiredApprovals: uint | uint | nonpayable |
| function | propose | to: address, amount: uint | uint | nonpayable |
| function | approve | id: uint | uint | nonpayable |
| function | execute | id: uint | uint | nonpayable |
| function | ownerTotal | uint | view | |
| function | approvalsNeeded | uint | view | |
| function | isSealed | bool | view | |
| function | owns | who: address | bool | view |
| function | held | uint | view | |
| function | proposals | uint | view | |
| function | recipientOf | id: uint | address | view |
| function | amountOf | id: uint | uint | view |
| function | approvalsOf | id: uint | uint | view |
| function | hasApproved | id: uint, who: address | bool | view |
| function | wasExecuted | id: uint | bool | view |
| event | OwnerAdded | owner: address | ||
| event | Sealed | owners: uint, threshold: uint | ||
| event | Proposed | id: uint, by: address, to: address, amount: uint | ||
| event | Approved | id: uint, by: address, approvals: uint | ||
| event | Executed | id: uint, to: address, amount: uint |
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 multisig > multisig.ares strblk contract template multisig --info strblk wallet deploy multisig.ares --dry-run --address <your address> strblk wallet deploy multisig.ares --node <your node's RPC>
Source
templates/multisig.ares, as embedded in this build
// =========================================================================================
// Multisig — M of N owners must approve before native STR leaves this contract.
//
// WHAT IT IS FOR
// Shared custody of STR. The contract holds a balance; any owner can propose a payment;
// the payment only happens once `threshold` distinct owners have approved that exact
// proposal.
//
// WHAT IT CAN DO, STATED PRECISELY — READ THIS BEFORE YOU CHOOSE IT
// AresLang has NO cross-contract calls. This multisig therefore CANNOT execute an
// arbitrary call on another contract, cannot move a ZK13 token held elsewhere, and cannot
// act as a general-purpose "wallet that can call anything". Its entire authority is: pay
// native STR out of its own balance, to an address named in a proposal. If you need more
// than that, this template is not a smaller version of what you need — it is a different
// thing, and the thing you need does not exist yet.
//
// WHAT IT GUARANTEES
// * A non-owner cannot propose, approve or execute. Every entry point checks `isOwner`.
// * One owner is one approval. A second `approve` from the same owner on the same proposal
// reverts rather than incrementing the count, so M-of-N cannot be cleared by one key
// calling M times.
// * An approval binds the exact action. `to` and `amount` are written once, at `propose`,
// and there is no function anywhere in this contract that edits them — so approving one
// payment and executing another is not a state this contract can reach.
// * A proposal executes at most once. `executed` is set before the transfer, and only an
// OWNER may trigger it — the opposite of the DAO treasury template, and deliberate: there
// the roll is public and execution carries no authority, whereas here the owner set is the
// whole of the contract's identity and nothing outside it should touch the balance at all.
// * The owner set and the threshold are frozen at `seal`. There is no function that adds an
// owner, removes one, or changes M afterwards, so "M-of-N can lower M to 1" — which makes
// an M-of-N really a 1-of-N over time — cannot happen here.
//
// THE PRICE OF THAT LAST GUARANTEE, AND IT IS A REAL ONE
// A LOST KEY IS PERMANENT. There is no rotation. If you seal 3-of-5 and two owners lose
// their keys, you still have 3 and you are fine; lose a third and the balance is
// unreachable forever, because the code is immutable. Choose N with generous margin, or
// accept that this contract holds an amount you can afford to lose access to.
//
// THE IDENTITY RULE — CORRECTED 2026-09-08
// This block described a rule that no longer exists. `ValueJournal.receiveRefusal` used to
// require a payment recipient to hold an ALLOCATED STR.domain or be a deployed contract; it
// now refuses only Hnitbjorg reserved accounts. The gate became MORE permissive, so nothing
// built against the old text is unsafe — but it instructed issuers to pre-flight a check that
// no longer applies, and told them a payout would revert when it will not.
//
// Today: any well-formed, non-reserved address, or a deployed contract, may be paid. What a
// recipient with no proved domain still cannot do is SPEND what it receives — a fact about
// that holder, not about this contract.
//
// HOW IT IS FUNDED
// By plain STR transfers to its address. A call record carries no value, and the contract
// cannot see who funded it, so there is no per-depositor accounting and there is no
// withdrawal right — everything here belongs to the owner set collectively.
//
// HOW IT IS TYPICALLY MISUSED
// * Sealing with `threshold` = 1. That is not a multisig; it is N single keys, any one of
// which can drain it. The contract permits it because 1-of-N is a legitimate shape for a
// hot spending account, but it must be a decision and not an accident.
// * Counting approvals by iterating an owner list. This contract keeps a counter instead:
// at 55 gas per map read plus 20 per loop iteration, a few thousand owners is a contract
// that becomes permanently uncallable with the funds behind the frozen function. That is
// also why N is bounded at 20 — edit `maxOwners` before you deploy if you need more, and
// understand that you are paying for it in nothing at all, since nothing iterates.
// * Forgetting that anyone with a proved STR.domain can read every proposal. Nothing here
// is private; a pending payout is public from the moment it is proposed.
// * Under-declaring the gas limit. MEASURED: addOwner 919, seal 736, propose 1,400,
// approve 938, execute 3,380. Declare at least 34,000.
// =========================================================================================
contract Multisig {
// ---- edit before deploying -----------------------------------------------------------
// A hard bound on N. Nothing in this contract iterates over owners, so this is not a gas
// limit — it is a limit on how far a setup mistake can go before `seal` closes it.
state maxOwners: uint = 20;
// -------------------------------------------------------------------------------------
// The deployer runs setup and then has no special power at all: after `seal` there is no
// function that checks `installer`.
state installer: address = caller;
state sealed: bool = false;
state ownerCount: uint = 0;
state threshold: uint = 0;
state proposalCount: uint = 0;
state isOwner: map<address, bool>;
// One proposal is a row across these five maps, keyed by id. AresLang has no structs; this
// is what a struct looks like without them, and the maps must be written together.
state payTo: map<uint, address>;
state payAmount: map<uint, uint>;
state approvals: map<uint, uint>;
state executed: map<uint, bool>;
state approvedBy: map<uint, map<address, bool>>;
event OwnerAdded(owner: address);
event Sealed(owners: uint, threshold: uint);
event Proposed(id: uint, by: address, to: address, amount: uint);
event Approved(id: uint, by: address, approvals: uint);
event Executed(id: uint, to: address, amount: uint);
// ---- setup, which happens once and then can never happen again -------------------------
fn addOwner(who: address) -> uint {
require(caller == installer, "not the installer");
require(sealed == false, "already sealed");
require(isOwner[who] == false, "already an owner");
require(ownerCount < maxOwners, "too many owners");
isOwner[who] = true;
ownerCount = ownerCount + 1;
emit OwnerAdded(who);
return ownerCount;
}
// Fixes M and closes the owner set forever. Fund the contract AFTER this, and verify
// `ownerTotal()` and `approvalsNeeded()` before you do.
fn seal(requiredApprovals: uint) -> uint {
require(caller == installer, "not the installer");
require(sealed == false, "already sealed");
require(requiredApprovals > 0, "threshold must be positive");
require(requiredApprovals <= ownerCount, "threshold above the owner count");
threshold = requiredApprovals;
sealed = true;
emit Sealed(ownerCount, requiredApprovals);
return requiredApprovals;
}
// ---- the only thing this contract can do ------------------------------------------------
fn propose(to: address, amount: uint) -> uint {
require(sealed == true, "not sealed yet");
require(isOwner[caller] == true, "not an owner");
require(amount > 0, "amount must be positive");
// A proposal to pay this contract can collect its M approvals and then revert on every
// execution, because the value ledger refuses a contract that pays itself. Refused
// where it is cheap, rather than after M owners have each spent a signature on it.
require(to != self, "self is not payable");
let id = proposalCount + 1;
proposalCount = id;
payTo[id] = to;
payAmount[id] = amount;
// The proposer does NOT approve automatically. Approving is a separate, deliberate act,
// so "M owners approved" always means M owners each sent an approval for this id.
approvals[id] = 0;
executed[id] = false;
emit Proposed(id, caller, to, amount);
return id;
}
fn approve(id: uint) -> uint {
require(sealed == true, "not sealed yet");
require(isOwner[caller] == true, "not an owner");
require(id > 0 && id <= proposalCount, "no such proposal");
require(executed[id] == false, "already executed");
// The line that makes M mean M.
require(approvedBy[id][caller] == false, "already approved");
approvedBy[id][caller] = true;
let count = approvals[id] + 1;
approvals[id] = count;
emit Approved(id, caller, count);
return count;
}
fn execute(id: uint) -> uint {
require(sealed == true, "not sealed yet");
require(isOwner[caller] == true, "not an owner");
require(id > 0 && id <= proposalCount, "no such proposal");
require(executed[id] == false, "already executed");
require(approvals[id] >= threshold, "not enough approvals");
let amount = payAmount[id];
require(balance(self) >= amount, "insufficient balance");
// Effects before the payment.
executed[id] = true;
transfer(payTo[id], amount);
emit Executed(id, payTo[id], amount);
return amount;
}
// ---- reads, never paused, never gated ---------------------------------------------------
view fn ownerTotal() -> uint {
return ownerCount;
}
view fn approvalsNeeded() -> uint {
return threshold;
}
view fn isSealed() -> bool {
return sealed;
}
view fn owns(who: address) -> bool {
return isOwner[who];
}
view fn held() -> uint {
return balance(self);
}
view fn proposals() -> uint {
return proposalCount;
}
view fn recipientOf(id: uint) -> address {
return payTo[id];
}
view fn amountOf(id: uint) -> uint {
return payAmount[id];
}
view fn approvalsOf(id: uint) -> uint {
return approvals[id];
}
view fn hasApproved(id: uint, who: address) -> bool {
return approvedBy[id][who];
}
view fn wasExecuted(id: uint) -> bool {
return executed[id];
}
}