Multi-sig wallet
Three owners, two confirmations required. The owners are a fixed-size list(address, 3), so the loop that records them has a length known when the contract is compiled.
multisig.lithe
(* A 2-of-3 wallet for ether payments. Owners submit a payment, confirm it, and
anyone can execute it once it has enough confirmations. *)
import "std/evm";
error NotOwner;
error AlreadyInitialized;
error NoSuchTx of uint;
error AlreadyExecuted of uint;
error AlreadyConfirmed of uint;
error NotConfirmed of uint;
error NotEnoughConfirmations of record {
id : uint;
confirmations : uint;
}
event Submitted of record {
indexed id : uint;
indexed to : address;
value : wei;
}
event Confirmed of record {
indexed id : uint;
indexed owner : address;
}
event Revoked of record {
indexed id : uint;
indexed owner : address;
}
event Executed of record {
indexed id : uint;
}
type Payment is record {
to : address;
value : wei;
executed : bool;
confirmations : uint;
}
storage is record {
isOwner : map(address, bool);
threshold : uint;
count : uint;
payments : map(uint, Payment);
confirmed : map(uint, map(address, bool));
}
external function init(const owners : list(address, 3))
raises AlreadyInitialized
is {
assert(storage.threshold = 0, AlreadyInitialized);
for owner : address in owners do {
storage.isOwner[owner] := true;
}
storage.threshold := 2;
}
external payable function deposit() is {
skip
}
view function onlyOwner() is {
assert(storage.isOwner[Evm.sender()], NotOwner);
}
view function pending(const id : uint) is {
assert(id < storage.count, NoSuchTx(id));
assert(not storage.payments[id].executed, AlreadyExecuted(id));
}
external function submit(const to : address; const value : wei) : uint
raises NotOwner
is {
onlyOwner();
const id : uint := storage.count;
storage.payments[id] := { to = to; value = value; executed = false; confirmations = 0; };
storage.count := id + 1;
emit Submitted { id = id; to = to; value = value; };
} with id
external function confirm(const id : uint)
raises NotOwner | NoSuchTx | AlreadyExecuted | AlreadyConfirmed
is {
onlyOwner();
pending(id);
assert(not storage.confirmed[id][Evm.sender()], AlreadyConfirmed(id));
storage.confirmed[id][Evm.sender()] := true;
storage.payments[id].confirmations := storage.payments[id].confirmations + 1;
emit Confirmed { id = id; owner = Evm.sender(); };
}
external function revoke(const id : uint)
raises NotOwner | NoSuchTx | AlreadyExecuted | NotConfirmed
is {
onlyOwner();
pending(id);
assert(storage.confirmed[id][Evm.sender()], NotConfirmed(id));
storage.confirmed[id][Evm.sender()] := false;
storage.payments[id].confirmations := storage.payments[id].confirmations - 1;
emit Revoked { id = id; owner = Evm.sender(); };
}
external nonreentrant function execute(const id : uint)
raises NoSuchTx | AlreadyExecuted | NotEnoughConfirmations
is {
pending(id);
const p : Payment := storage.payments[id];
assert(
p.confirmations >= storage.threshold,
NotEnoughConfirmations { id = id; confirmations = p.confirmations; }
);
storage.payments[id].executed := true;
emit Executed { id = id; };
defer Evm.transfer(p.to, p.value);
}
external view function paymentOf(const id : uint) : Payment is
storage.payments[id]executemarks the payment executed before the deferred transfer runs, andnonreentrantstops the recipient from callingexecuteagain.- This wallet sends ether only. Lithe has no arbitrary-calldata calls, so a general-purpose multisig would dispatch typed calls through interfaces.

