// ========================================================================================= // 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; // 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; state payAmount: map; state approvals: map; state executed: map; state approvedBy: map>; 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]; } }