SourceLessExplorer · SourceLess AsgardSourceLess Asgard · chain 1313
LiveHeight23,717Head age31 sLast interval1 minFinality23,706Behind−11Pending0 / 8,192Tx in head0
XPlorer following the chain as its own read-only node

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

Edit the draft

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

uint, shipped as 20

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
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.

  1. Save the draft. Download it above, or start from the catalogue text with strblk contract template multisig > multisig.ares and edit the marked constants yourself.
  2. 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.
  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 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 pointGas
execute3,380
propose1,400
approve938
addOwner919
seal736
Declare at least
33,800 ten times the most expensive call

Functions

KindNameInputsOutputsMutability
functionaddOwnerwho: addressuintnonpayable
functionsealrequiredApprovals: uintuintnonpayable
functionproposeto: address, amount: uintuintnonpayable
functionapproveid: uintuintnonpayable
functionexecuteid: uintuintnonpayable
functionownerTotaluintview
functionapprovalsNeededuintview
functionisSealedboolview
functionownswho: addressboolview
functionhelduintview
functionproposalsuintview
functionrecipientOfid: uintaddressview
functionamountOfid: uintuintview
functionapprovalsOfid: uintuintview
functionhasApprovedid: uint, who: addressboolview
functionwasExecutedid: uintboolview
eventOwnerAddedowner: address
eventSealedowners: uint, threshold: uint
eventProposedid: uint, by: address, to: address, amount: uint
eventApprovedid: uint, by: address, approvals: uint
eventExecutedid: 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];
  }
}