aere-contracts/contracts/passkey/MultiOwnable.sol
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 01:02:37 +03:00

112 lines
4.4 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @title MultiOwnable — variant ownership with both EOA and WebAuthn passkey owners
/// @notice Adapted from Coinbase Smart Wallet's MultiOwnable.sol (MIT).
/// Owners are stored as raw bytes: 32 bytes (left-padded EOA address) OR
/// 64 bytes (P-256 uncompressed point: x||y). Each owner has an index.
/// Any owner can add or remove other owners. Loss of one owner ≠ loss of account
/// as long as ≥1 other owner remains.
///
/// @dev Source: https://github.com/coinbase/smart-wallet/blob/main/src/MultiOwnable.sol
contract MultiOwnable {
/// @notice Tracks the next owner index to assign. Monotonic — never reused.
uint256 public nextOwnerIndex;
/// @notice Number of currently-registered owners (decreases on remove).
uint256 public ownerCount;
/// @notice Indexed owner storage.
mapping(uint256 => bytes) internal _ownerAtIndex;
/// @notice Reverse lookup: bytes-encoded owner → true if registered.
mapping(bytes => bool) internal _isOwner;
event AddOwner(uint256 indexed index, bytes owner);
event RemoveOwner(uint256 indexed index, bytes owner);
error Unauthorized();
error AlreadyOwner(bytes owner);
error NoOwnerAtIndex(uint256 index);
error InvalidOwnerBytesLength(uint256 length);
error InvalidEthereumAddressOwner(bytes owner);
error ZeroAddressOwner();
error LastOwner();
modifier onlyOwner() {
_checkOwner();
_;
}
/// @notice Self-call only. Used so external callers can never bypass auth — every
/// owner-management op must come through an authenticated execute() that
/// loops back through msg.sender == address(this).
function _checkOwner() internal view virtual {
if (msg.sender != address(this)) revert Unauthorized();
}
/// @notice Add an EOA owner by address (20 bytes left-padded to 32).
function addOwnerAddress(address owner) public virtual onlyOwner {
_addOwner(abi.encode(owner));
}
/// @notice Add a passkey owner by uncompressed P-256 public key (64 bytes: x || y).
function addOwnerPublicKey(bytes32 x, bytes32 y) public virtual onlyOwner {
_addOwner(abi.encode(x, y));
}
function _addOwner(bytes memory owner) internal {
if (_isOwner[owner]) revert AlreadyOwner(owner);
if (owner.length != 32 && owner.length != 64) revert InvalidOwnerBytesLength(owner.length);
if (owner.length == 32) {
// EOA owner — top 12 bytes must be zero (uint160 left-padded to bytes32).
uint256 word;
// solhint-disable-next-line no-inline-assembly
assembly { word := mload(add(owner, 0x20)) }
if (word >> 160 != 0) revert InvalidEthereumAddressOwner(owner);
// Ban address(0) as an owner: paired with an ecrecover-failure returning
// address(0), a zero owner would let a malformed signature authorize.
if (word == 0) revert ZeroAddressOwner();
}
uint256 idx = nextOwnerIndex++;
_ownerAtIndex[idx] = owner;
_isOwner[owner] = true;
ownerCount += 1;
emit AddOwner(idx, owner);
}
/// @notice Remove the owner at a specific index. Reverts if it would zero the set.
function removeOwnerAtIndex(uint256 index) public virtual onlyOwner {
bytes memory owner = _ownerAtIndex[index];
if (owner.length == 0) revert NoOwnerAtIndex(index);
if (ownerCount == 1) revert LastOwner();
delete _isOwner[owner];
delete _ownerAtIndex[index];
ownerCount -= 1;
emit RemoveOwner(index, owner);
}
function isOwnerAddress(address account) public view virtual returns (bool) {
return _isOwner[abi.encode(account)];
}
function isOwnerPublicKey(bytes32 x, bytes32 y) public view virtual returns (bool) {
return _isOwner[abi.encode(x, y)];
}
function isOwnerBytes(bytes memory account) public view virtual returns (bool) {
return _isOwner[account];
}
function ownerAtIndex(uint256 index) public view virtual returns (bytes memory) {
return _ownerAtIndex[index];
}
/// @notice Internal initializer — called by inheriting contract during init.
function _initializeOwners(bytes[] memory owners) internal {
for (uint256 i = 0; i < owners.length; i++) {
_addOwner(owners[i]);
}
}
}