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.
112 lines
4.4 KiB
Solidity
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]);
|
|
}
|
|
}
|
|
}
|