height 23745 of 23745 on the network. 9 peer(s) connected, 77 in its peer directory; 1041 block(s) followed and 865 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 for your fork
- Status
- a draft, not deployed nothing here is on any chain; never send value to a draft
- 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
- 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
Multisig- Compiler
areslang-1.3.0- Bytecode SHA-256
6e38251a4c314186ed6908d41e364bd5f1c2a7f0b4b1d3de3d352277008d8f9c- Bytecode
- 2,338 bytes
- Source SHA-256
00d42edfd017a21b48e08f152993f49ed8dfbaeceb85a34949cab1417ae02e0b- 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 multisig > multisig.aresand edit the marked constants yourself. - Dry-run it on your machine.
strblk wallet deploy multisig.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 multisig.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 |
|---|---|
| execute | 3,380 |
| propose | 1,400 |
| approve | 938 |
| addOwner | 919 |
| seal | 736 |
- Declare at least
- 33,800 ten times the most expensive call
Functions
| 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 |
Your draft's source
templates/multisig.ares, as shipped
// =========================================================================================
// 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];
}
}