NexusHub

Description:

Multi-signature wallet contract requiring multiple confirmations for transaction execution.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

{{
  "language": "Solidity",
  "sources": {
    "src/NexusHub.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {IAzimuth} from "../src/Interfaces/IAzimuth.sol";
import {IPlanetDispenser} from "../src/Interfaces/IPlanetDispenser.sol";
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import {ECDSA} from "../lib/openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol";
import {MessageHashUtils} from "../lib/openzeppelin-contracts/contracts/utils/cryptography/MessageHashUtils.sol";
import {ReentrancyGuard} from "../lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol";
import {SlugLib} from "../src/libraries/SlugLib.sol";

/**
 * @title NexusHub
 * @notice Centralized registry for Nexus instances
 */
contract NexusHub is ReentrancyGuard {
    using SafeERC20 for IERC20;

    ///////////////
    // Errors ////
    //////////////

    error UnauthorizedOwnerOrProxy();        
    error DeployerMustControlOwnershipPoint();
    error CallerNotOwnerOfUrbitId();
    error UrbitIdNotActive();
    error CallerNotAuthorized();
    error PointNotOwnedByCaller();           
    error PointNotControlledByCaller();      
    error UnauthorizedAccess();              
    error NoInvitesRemaining();
    error BadRecipient();
    error InsufficientUrbit();
    error UrbitIdAlreadyBanned(uint32 urbitId);
    error RecipientNotOwner();
    error InvalidSigner();
    error NotAMember(uint32 urbitId);
    error AlreadyMember(uint32 urbitId);
    error InvalidCount();
    error InsufficientInvites();
    error InsufficientUrbitBalance();
    error BadWithdrawalAddress();
    error InvitesRemain();
    error NoUrbitToRetrieve();
    error BadInviter();
    error NexusAlreadyExists();
    error NexusDoesNotExist();
    error NexusDestroyed();
    error SlugAlreadyUsed();
    error EthTransferFailed();

    ///////////////////////
    // Storage Variables //
    ///////////////////////

    IAzimuth private immutable i_azimuth;
    address  private immutable i_urbitToken;

    uint32 private _ownerId;
    uint32 private _proposedOwnerId;
    address private _planetMarket;       

    //////////////////////////////
    // Per-nexus state & ledgers//
    //////////////////////////////

    struct NexusNode {
        uint32 ownerId;
        address nexusInviter;
        uint32 inviteCount;
        uint8  freeMonths;
        bool   isDestroyed;
        uint256 price;       
        string nexusId;      
        bool   allowManagementProxy;
        uint32 totalInvitesIssued;
        uint256 escrowUrbit; // $URBIT allocated to a nexus
        bool   exists;       
    }

    // key: keccak256(ownerId, slug)
    mapping(bytes32 => NexusNode) private _nodes;

    // per-nexus membership flags
    mapping(bytes32 => mapping(uint32 => bool)) private _isMember;
    mapping(bytes32 => mapping(uint32 => bool)) private _isBanned;
    mapping(bytes32 => mapping(uint32 => bool)) private _hasQuit;

    // anti-replay nonces, scoped per (nexusKey, urbitId)
    mapping(bytes32 => mapping(uint32 => uint256)) private _onboardNonce;
    mapping(bytes32 => mapping(uint32 => uint256)) private _quitNonce;

    // prevent duplicate slugs per (ownerId, slug) pair via key; 
    mapping(bytes32 => bool) private _slugKeyUsed;

    //////////////////////
    // EIP-191 typehash //
    //////////////////////

    /// @dev keccak256("Onboard(bytes32 nexusKey,uint32 nexusOwnerId,uint32 urbitId,address recipient,uint256 nonce,address verifyingContract,uint256 chainId)")
    bytes32 private constant ONBOARD_TYPEHASH =
        keccak256("Onboard(bytes32 nexusKey,uint32 nexusOwnerId,uint32 urbitId,address recipient,uint256 nonce,address verifyingContract,uint256 chainId)");

    /// @dev keccak256("Quit(bytes32 nexusKey,uint32 nexusOwnerId,uint32 quittingUrbitId,uint256 nonce,address verifyingContract,uint256 chainId)")
    bytes32 private constant QUIT_TYPEHASH =
        keccak256(
            "Quit(bytes32 nexusKey,uint32 nexusOwnerId,uint32 quittingUrbitId,uint256 nonce,address verifyingContract,uint256 chainId)"
        );

    //////////////
    // Events  ///
    //////////////

    // Lifecycle of per-nexus records
    event NexusOpened(uint32 indexed leaderUrbitId, bytes32 indexed nexusKey, string nexusId, address indexed inviteKey);
    event NexusClosed(bytes32 indexed nexusKey, uint32 indexed leaderUrbitId);

    // Ownership transfer flow
    event OwnerProposed(uint32 indexed currentOwnerId, uint32 indexed proposedOwnerId);
    event OwnershipProposalRevoked(uint32 indexed currentOwnerId);
    event OwnershipAccepted(uint32 indexed newOwnerId);
    event OwnershipRejected(uint32 indexed proposedOwnerId);

    // Market / external integration 
    event PlanetMarketUpdated(address indexed newMarket);

    // Invite escrow/accounting
    event InviteTokenDeposited(bytes32 indexed nexusKey, address indexed depositor, uint256 count, uint256 amount);
    event InviteTokenWithdrawn(bytes32 indexed nexusKey, address indexed withdrawer, uint256 count, uint256 amount);
    event UrbitTokensRetrieved(bytes32 indexed nexusKey, address indexed to, uint256 amount);

    // Inviter configuration
    event NexusInviterAdded(bytes32 indexed nexusKey, address indexed nexusInviter);
    event NexusInviterChanged(bytes32 indexed nexusKey, address indexed oldNexusInviter, address indexed newNexusInviter);
    event NexusInviterRemoved(bytes32 indexed nexusKey, address indexed nexusInviter);

    // Membership lifecycle
    event InviteClaimed(bytes32 indexed nexusKey, uint32 indexed nexusOwnerUrbitId, uint32 indexed invitedUrbitId, address recipient, uint256 stipend);
    event UrbitIdBanned(bytes32 indexed nexusKey, uint32 indexed bannedUrbitId);
    event UrbitIdUnbanned(bytes32 indexed nexusKey, uint32 indexed unbannedUrbitId);
    event UrbitIdQuit(bytes32 indexed nexusKey, uint32 indexed quittingUrbitId);

    // Admin toggles / lifecycle
    event ManagementProxyAccessSet(bytes32 indexed nexusKey, bool allowed, address indexed proxy);

    ///////////////
    // Modifiers //
    ///////////////

    modifier onlyHubOwner() {
        if (!i_azimuth.isOwner(_ownerId, msg.sender) &&
            !i_azimuth.isManagementProxy(_ownerId, msg.sender)) {
            revert UnauthorizedOwnerOrProxy();
        }
        _;
    }

    modifier onlyExisting(bytes32 nexusKey) {
        if (!_nodes[nexusKey].exists) revert NexusDoesNotExist();
        if (_nodes[nexusKey].isDestroyed) revert NexusDestroyed();
        _;
    }

    modifier onlyOwnerID(bytes32 nexusKey) {
        uint32 nid = _nodes[nexusKey].ownerId;
        if (!i_azimuth.isOwner(nid, msg.sender)) revert PointNotOwnedByCaller();
        _;
    }

    modifier onlyController(bytes32 nexusKey) {
        NexusNode storage n = _nodes[nexusKey];
        bool asOwner = i_azimuth.isOwner(n.ownerId, msg.sender);
        bool asMgmt  = n.allowManagementProxy && i_azimuth.isManagementProxy(n.ownerId, msg.sender);
        if (!(asOwner || asMgmt)) revert PointNotControlledByCaller();
        _;
    }

    modifier permissionedAccess(bytes32 nexusKey) {
        NexusNode storage n = _nodes[nexusKey];
        bool mgmtOk = n.allowManagementProxy && i_azimuth.isManagementProxy(n.ownerId, msg.sender);
        if (msg.sender != n.nexusInviter &&
            !i_azimuth.isOwner(n.ownerId, msg.sender) &&
            !mgmtOk) {
            revert UnauthorizedAccess();
        }
        _;
    }

    /////////////////
    // Constructor //
    /////////////////

    constructor(address azimuthAddress, uint32 ownerId, address urbitToken, address planetMarket) {
        i_azimuth    = IAzimuth(azimuthAddress);
        i_urbitToken = urbitToken;
        _ownerId     = ownerId;
        _planetMarket = planetMarket;
        if (!i_azimuth.isOwner(ownerId, msg.sender)) {
            revert DeployerMustControlOwnershipPoint();
        }
    }

    ////////////////////////////
    //// Nexus CRUD (public) ////
    ////////////////////////////

    function createNexus(
        uint32 leaderUrbitId,
        address nexusInviter,
        uint8 freeMonths,
        uint256 price,
        string memory nexusId
    ) external nonReentrant returns (bytes32 nexusKey) {
        if (!i_azimuth.isOwner(leaderUrbitId, msg.sender)) revert CallerNotOwnerOfUrbitId();
        if (!i_azimuth.isActive(leaderUrbitId)) revert UrbitIdNotActive();

        SlugLib.assertValidSlug(nexusId);

        nexusKey = keccak256(abi.encodePacked(leaderUrbitId, nexusId));
        if (_nodes[nexusKey].exists) revert NexusAlreadyExists();

        if (_slugKeyUsed[nexusKey]) revert SlugAlreadyUsed();
        _slugKeyUsed[nexusKey] = true;

        NexusNode storage n = _nodes[nexusKey];
        n.ownerId = leaderUrbitId;
        n.nexusInviter = nexusInviter;
        n.inviteCount = 0;
        n.freeMonths = freeMonths;
        n.price = price;
        n.nexusId = nexusId;
        n.isDestroyed = false;
        n.allowManagementProxy = false;
        n.totalInvitesIssued = 0;
        n.escrowUrbit = 0;
        n.exists = true;

        emit NexusOpened(leaderUrbitId, nexusKey, nexusId, nexusInviter);
    }

    function destroyNexus(bytes32 nexusKey)
        external
        nonReentrant
        onlyExisting(nexusKey)
        onlyOwnerID(nexusKey)
        returns (bool)
    {
        NexusNode storage n = _nodes[nexusKey];

        // Close onboarding
        address oldInviter = n.nexusInviter;
        n.nexusInviter = address(0);
        if (oldInviter != address(0)) {
            emit NexusInviterRemoved(nexusKey, oldInviter);
            emit NexusInviterChanged(nexusKey, oldInviter, address(0));
        }

        // Refund any remaining escrow for this nexus to the caller (owner)
        uint256 escrow = n.escrowUrbit;
        if (escrow > 0) {
            n.escrowUrbit = 0;
            IERC20(i_urbitToken).safeTransfer(msg.sender, escrow);
            emit UrbitTokensRetrieved(nexusKey, msg.sender, escrow);
        }

        n.inviteCount = 0;
        n.isDestroyed = true;

        emit NexusClosed(nexusKey, n.ownerId);
        return true;
    }

    ///////////////////////////////
    //// Invite Escrow & Flows ////
    ///////////////////////////////

    function addInvites(bytes32 nexusKey, uint32 count)
        external
        nonReentrant
        onlyExisting(nexusKey)
        permissionedAccess(nexusKey)
        returns (bool)
    {
        if (count == 0) revert InvalidCount();

        IPlanetDispenser d = IPlanetDispenser(_planetMarket);
        (uint256 unitPrice,,) = d.priceAndAllowance(address(this));
        uint256 amount = unitPrice * uint256(count);

        // Pull $URBIT into hub & attribute to this nexus
        IERC20(i_urbitToken).safeTransferFrom(msg.sender, address(this), amount);

        NexusNode storage n = _nodes[nexusKey];
        unchecked {
            n.inviteCount += count;
            n.totalInvitesIssued += count;
        }
        n.escrowUrbit += amount;

        emit InviteTokenDeposited(nexusKey, msg.sender, count, amount);
        return true;
    }

    function removeInvites(bytes32 nexusKey, uint32 count)
        external
        nonReentrant
        onlyExisting(nexusKey)
        permissionedAccess(nexusKey)
        returns (bool)
    {
        if (count == 0) revert InvalidCount();

        NexusNode storage n = _nodes[nexusKey];
        if (n.inviteCount < count) revert InsufficientInvites();

        IPlanetDispenser d = IPlanetDispenser(_planetMarket);
        (uint256 unitPrice,,) = d.priceAndAllowance(address(this));
        uint256 amount = unitPrice * uint256(count);

        if (n.escrowUrbit < amount) revert InsufficientUrbitBalance();

        
        n.inviteCount -= count;
        n.escrowUrbit -= amount;

        // Refund $URBIT to caller
        IERC20(i_urbitToken).safeTransfer(msg.sender, amount);

        emit InviteTokenWithdrawn(nexusKey, msg.sender, count, amount);
        return true;
    }

    function retrieveUrbitTokens(bytes32 nexusKey, address withdrawalAddress)
        external
        nonReentrant
        onlyExisting(nexusKey)
        onlyController(nexusKey)
        returns (bool)
    {
        if (withdrawalAddress == address(0)) revert BadWithdrawalAddress();

        NexusNode storage n = _nodes[nexusKey];
        if (n.inviteCount != 0) revert InvitesRemain();

        uint256 bal = n.escrowUrbit;
        if (bal == 0) revert NoUrbitToRetrieve();

        n.escrowUrbit = 0;
        IERC20(i_urbitToken).safeTransfer(withdrawalAddress, bal);

        emit UrbitTokensRetrieved(nexusKey, withdrawalAddress, bal);
        return true;
    }

    /////////////////////////////////////
    //// Membership: onboarding/quit  ////
    /////////////////////////////////////

    function onboardNewUrbitID(bytes32 nexusKey, address recipient, uint32 newUrbitId)
        external
        payable
        nonReentrant
        onlyExisting(nexusKey)
        permissionedAccess(nexusKey)
        returns (bool)
    {
        NexusNode storage n = _nodes[nexusKey];
        if (n.inviteCount == 0) revert NoInvitesRemaining();
        if (_isMember[nexusKey][newUrbitId]) revert AlreadyMember(newUrbitId);
        if (recipient == address(0)) revert BadRecipient();

        IPlanetDispenser d = IPlanetDispenser(_planetMarket);

        // Check hub-level $URBIT escrow for this nexus covers the spend
        (uint256 price,,) = d.priceAndAllowance(address(this));
        if (n.escrowUrbit < price) revert InsufficientUrbit();

        // One-time or top-up approval for dispenser (global allowance)
        uint256 allowance = IERC20(i_urbitToken).allowance(address(this), _planetMarket);
        if (allowance < price) {
            IERC20(i_urbitToken).forceApprove(_planetMarket, type(uint256).max);
        }

        // Spend & effect
        d.redeemPlanet(newUrbitId, recipient);

        unchecked { n.inviteCount -= 1; }
        _isMember[nexusKey][newUrbitId] = true;
        n.escrowUrbit -= price;

        // Send ETH stipend to recipient if provided
        if (msg.value > 0) {
            (bool success, ) = payable(recipient).call{value: msg.value}("");
            if (!success) revert EthTransferFailed();
        }

        emit InviteClaimed(nexusKey, n.ownerId, newUrbitId, recipient, msg.value);

        return true;
    }

    function onboardExistingUrbitID(
        bytes32 nexusKey,
        address recipient,
        uint32 existingUrbitId,
        bytes memory signature
    )
        external
        payable
        nonReentrant
        onlyExisting(nexusKey)
        permissionedAccess(nexusKey)
        returns (bool)
    {
        NexusNode storage n = _nodes[nexusKey];

        if (n.inviteCount == 0) revert NoInvitesRemaining();
        if (_isMember[nexusKey][existingUrbitId]) revert AlreadyMember(existingUrbitId);
        if (_isBanned[nexusKey][existingUrbitId]) revert UrbitIdAlreadyBanned(existingUrbitId);
        if (recipient != i_azimuth.getOwner(existingUrbitId)) revert RecipientNotOwner();

        // Reconstruct the personal_sign payload
        bytes32 structHash = keccak256(
            abi.encode(
                ONBOARD_TYPEHASH,
                nexusKey,
                n.ownerId,
                existingUrbitId,
                recipient,
                _onboardNonce[nexusKey][existingUrbitId],
                address(this),
                block.chainid
            )
        );
        bytes32 digest = MessageHashUtils.toEthSignedMessageHash(structHash);

        address signer = ECDSA.recover(digest, signature);
        bool ok = (signer == i_azimuth.getOwner(existingUrbitId)) ||
                  (i_azimuth.isManagementProxy(existingUrbitId, signer));
        if (!ok) revert InvalidSigner();

        unchecked { _onboardNonce[nexusKey][existingUrbitId]++; }
        unchecked { n.inviteCount -= 1; }

        _isMember[nexusKey][existingUrbitId] = true;

        // Send ETH stipend to recipient if provided
        if (msg.value > 0) {
            (bool success, ) = payable(recipient).call{value: msg.value}("");
            if (!success) revert EthTransferFailed();
        }

        emit InviteClaimed(nexusKey, n.ownerId, existingUrbitId, recipient, msg.value);

        return true;
    }

    function permissionedQuitNexus(
        bytes32 nexusKey,
        uint32 quittingUrbitId,
        bytes memory signature
    )
        external
        nonReentrant
        onlyExisting(nexusKey)
        permissionedAccess(nexusKey)
        returns (bool)
    {
        if (!_isMember[nexusKey][quittingUrbitId]) revert NotAMember(quittingUrbitId);

        NexusNode storage n = _nodes[nexusKey];

        bytes32 structHash = keccak256(
            abi.encode(
                QUIT_TYPEHASH,
                nexusKey,
                n.ownerId,
                quittingUrbitId,
                _quitNonce[nexusKey][quittingUrbitId],
                address(this),
                block.chainid
            )
        );
        bytes32 digest = MessageHashUtils.toEthSignedMessageHash(structHash);

        address signer = ECDSA.recover(digest, signature);
        bool ok = (signer == i_azimuth.getOwner(quittingUrbitId)) ||
                  (i_azimuth.isManagementProxy(quittingUrbitId, signer));
        if (!ok) revert InvalidSigner();

        unchecked { _quitNonce[nexusKey][quittingUrbitId]++; }

        _hasQuit[nexusKey][quittingUrbitId] = true;
        _isMember[nexusKey][quittingUrbitId] = false;

        emit UrbitIdQuit(nexusKey, quittingUrbitId);
        return true;
    }

    function rageQuitNexus(bytes32 nexusKey, uint32 rageQuitUrbitId)
        external
        nonReentrant
        onlyExisting(nexusKey)
        returns (bool)
    {
        if (!_isMember[nexusKey][rageQuitUrbitId]) revert NotAMember(rageQuitUrbitId);

        bool controls =
            i_azimuth.isOwner(rageQuitUrbitId, msg.sender) ||
            i_azimuth.isManagementProxy(rageQuitUrbitId, msg.sender);
        if (!controls) revert CallerNotAuthorized();

        NexusNode storage n = _nodes[nexusKey];

        _hasQuit[nexusKey][rageQuitUrbitId] = true;
        _isMember[nexusKey][rageQuitUrbitId] = false;

        emit UrbitIdQuit(nexusKey, rageQuitUrbitId);
        return true;
    }

    /////////////////////////
    //// Per-nexus Admin ////
    /////////////////////////

    function allowManagementProxyAccess(bytes32 nexusKey, bool access)
        external
        onlyExisting(nexusKey)
        onlyOwnerID(nexusKey)
        returns (address managementAddress)
    {
        NexusNode storage n = _nodes[nexusKey];
        n.allowManagementProxy = access;
        if (access) {
            managementAddress = i_azimuth.getManagementProxy(n.ownerId);
        } else {
            managementAddress = address(0);
        }
        emit ManagementProxyAccessSet(nexusKey, access, managementAddress);
        return managementAddress;
    }

    function banUrbitId(bytes32 nexusKey, uint32 urbitId)
        external
        onlyExisting(nexusKey)
        onlyController(nexusKey)
        returns (bool)
    {
        _isBanned[nexusKey][urbitId] = true;
        _isMember[nexusKey][urbitId] = false;
        emit UrbitIdBanned(nexusKey, urbitId);
        return true;
    }

    function unbanUrbitId(bytes32 nexusKey, uint32 urbitId)
        external
        onlyExisting(nexusKey)
        onlyController(nexusKey)
        returns (bool)
    {
        _isBanned[nexusKey][urbitId] = false;
        emit UrbitIdUnbanned(nexusKey, urbitId);
        return true;
    }

    function changeNexusInviter(bytes32 nexusKey, address newNexusInviter)
        external
        onlyExisting(nexusKey)
        onlyController(nexusKey)
        returns (bool)
    {
        address old = _nodes[nexusKey].nexusInviter;
        _nodes[nexusKey].nexusInviter = newNexusInviter;
        emit NexusInviterChanged(nexusKey, old, newNexusInviter);
        if (newNexusInviter == address(0) && old != address(0)) {
            emit NexusInviterRemoved(nexusKey, old);
        } else if (newNexusInviter != address(0)) {
            emit NexusInviterAdded(nexusKey, newNexusInviter);
        }
        return true;
    }

    function removeNexusInviter(bytes32 nexusKey)
        external
        onlyExisting(nexusKey)
        onlyController(nexusKey)
        returns (bool)
    {
        address old = _nodes[nexusKey].nexusInviter;
        _nodes[nexusKey].nexusInviter = address(0);
        if (old != address(0)) {
            emit NexusInviterRemoved(nexusKey, old);
            emit NexusInviterChanged(nexusKey, old, address(0));
        }
        return true;
    }

    function addNexusInviter(bytes32 nexusKey, address newInviter)
        external
        onlyExisting(nexusKey)
        onlyController(nexusKey)
        returns (bool)
    {
        if (newInviter == address(0)) revert BadInviter();
        address old = _nodes[nexusKey].nexusInviter;
        _nodes[nexusKey].nexusInviter = newInviter;
        emit NexusInviterAdded(nexusKey, newInviter);
        emit NexusInviterChanged(nexusKey, old, newInviter);
        return true;
    }

    /////////////////////////
    //// Hub Admin (owner) //
    /////////////////////////

    function proposeOwner(uint32 proposedOwnerId) external onlyHubOwner returns (uint32) {
        _proposedOwnerId = proposedOwnerId;
        emit OwnerProposed({ currentOwnerId: _ownerId, proposedOwnerId: proposedOwnerId });
        return _proposedOwnerId;
    }

    function revokeOwnershipProposal() external onlyHubOwner returns (uint32) {
        _proposedOwnerId = _ownerId;
        emit OwnershipProposalRevoked({ currentOwnerId: _ownerId });
        return _proposedOwnerId;
    }

    function acceptOwnership() external returns (uint32) {
        if (!i_azimuth.isOwner(_proposedOwnerId, msg.sender)) revert CallerNotAuthorized();
        _ownerId = _proposedOwnerId;
        emit OwnershipAccepted({ newOwnerId: _ownerId });
        return _ownerId;
    }

    function rejectOwnership() external returns (uint32) {
        if (!i_azimuth.isOwner(_proposedOwnerId, msg.sender)) revert CallerNotAuthorized();
        emit OwnershipRejected({ proposedOwnerId: _proposedOwnerId });
        _proposedOwnerId = _ownerId;
        return _ownerId;
    }

    function changePlanetMarket(address newMarket) external onlyHubOwner returns (bool) {
        // Revoke approval from old market
        if (_planetMarket != address(0)) {
            IERC20(i_urbitToken).forceApprove(_planetMarket, 0);
        }

        _planetMarket = newMarket;
        emit PlanetMarketUpdated(newMarket);
        return true;
    }

    //////////////////////////
    //// View / Getters   ////
    //////////////////////////

    function nexusKeyOf(uint32 leaderUrbitId, string memory nexusId) public pure returns (bytes32) {
        return keccak256(abi.encodePacked(leaderUrbitId, nexusId));
    }

    function getNode(bytes32 nexusKey)
        external
        view
        returns (
            uint32 ownerId,
            address nexusInviter,
            uint32 inviteCount,
            uint8  freeMonths,
            bool   isDestroyed,
            uint256 price,
            string memory nexusId,
            bool   allowManagementProxy,
            uint32 totalInvitesIssued,
            uint256 escrowUrbit
        )
    {
        NexusNode storage n = _nodes[nexusKey];
        return (
            n.ownerId,
            n.nexusInviter,
            n.inviteCount,
            n.freeMonths,
            n.isDestroyed,
            n.price,
            n.nexusId,
            n.allowManagementProxy,
            n.totalInvitesIssued,
            n.escrowUrbit
        );
    }

    function exists(bytes32 nexusKey) external view returns (bool) { return _nodes[nexusKey].exists; }
    function isDestroyed(bytes32 nexusKey) external view returns (bool) { return _nodes[nexusKey].isDestroyed; }

    function isMember(bytes32 nexusKey, uint32 urbitId) external view returns (bool) { return _isMember[nexusKey][urbitId]; }
    function isBanned(bytes32 nexusKey, uint32 urbitId) external view returns (bool) { return _isBanned[nexusKey][urbitId]; }
    function hasQuit(bytes32 nexusKey, uint32 urbitId) external view returns (bool) { return _hasQuit[nexusKey][urbitId]; }


    function isValidAddress(
        address userAddress,
        uint32 hostUrbitId,
        string memory nexusId,
        uint32 urbitId
    ) external view returns (bool) {
        bytes32 nexusKey = nexusKeyOf(hostUrbitId, nexusId);
        return i_azimuth.canManage(urbitId, userAddress) && _isMember[nexusKey][urbitId];
    }

    function onboardNonce(bytes32 nexusKey, uint32 urbitId) external view returns (uint256) {
        return _onboardNonce[nexusKey][urbitId];
    }
    function quitNonce(bytes32 nexusKey, uint32 urbitId) external view returns (uint256) {
        return _quitNonce[nexusKey][urbitId];
    }

    function getOwnerId() external view returns (uint32) { return _ownerId; }
    function getProposedOwnerId() external view returns (uint32) { return _proposedOwnerId; }
    function getPlanetMarketContract() external view returns (address) { return _planetMarket; }
    function getUrbitToken() external view returns (address) { return i_urbitToken; }
    function getAzimuth() external view returns (address) { return address(i_azimuth); }

    /////////////////
    //// Receive ////
    /////////////////

    receive() external payable {
        revert("Direct ETH transfers not accepted");
    }

    fallback() external payable {
        revert("Function does not exist");
    }
}
"
    },
    "src/Interfaces/IAzimuth.sol": {
      "content": "// SPDX-License-Identifier: GPLv3
pragma solidity ^0.8.20;

interface IAzimuth {
    event Activated(uint32 indexed point);
    event BrokeContinuity(uint32 indexed point, uint32 number);
    event ChangedDns(string primary, string secondary, string tertiary);
    event ChangedKeys(
        uint32 indexed point,
        bytes32 encryptionKey,
        bytes32 authenticationKey,
        uint32 cryptoSuiteVersion,
        uint32 keyRevisionNumber
    );
    event ChangedManagementProxy(
        uint32 indexed point,
        address indexed managementProxy
    );
    event ChangedSpawnProxy(uint32 indexed point, address indexed spawnProxy);
    event ChangedTransferProxy(
        uint32 indexed point,
        address indexed transferProxy
    );
    event ChangedVotingProxy(uint32 indexed point, address indexed votingProxy);
    event EscapeAccepted(uint32 indexed point, uint32 indexed sponsor);
    event EscapeCanceled(uint32 indexed point, uint32 indexed sponsor);
    event EscapeRequested(uint32 indexed point, uint32 indexed sponsor);
    event LostSponsor(uint32 indexed point, uint32 indexed sponsor);
    event OwnerChanged(uint32 indexed point, address indexed owner);
    event OwnershipRenounced(address indexed previousOwner);
    event OwnershipTransferred(
        address indexed previousOwner,
        address indexed newOwner
    );
    event Spawned(uint32 indexed prefix, uint32 indexed child);

    function activatePoint(uint32 _point) external;

    function canManage(
        uint32 _point,
        address _who
    ) external view returns (bool result);

    function canSpawnAs(
        uint32 _point,
        address _who
    ) external view returns (bool result);

    function canTransfer(
        uint32 _point,
        address _who
    ) external view returns (bool result);

    function canVoteAs(
        uint32 _point,
        address _who
    ) external view returns (bool result);

    function cancelEscape(uint32 _point) external;

    function dnsDomains(uint256) external view returns (string memory);

    function doEscape(uint32 _point) external;

    function escapeRequests(uint32, uint256) external view returns (uint32);

    function escapeRequestsIndexes(
        uint32,
        uint32
    ) external view returns (uint256);

    function getContinuityNumber(
        uint32 _point
    ) external view returns (uint32 continuityNumber);

    function getEscapeRequest(
        uint32 _point
    ) external view returns (uint32 escape);

    function getEscapeRequests(
        uint32 _sponsor
    ) external view returns (uint32[] memory requests);

    function getEscapeRequestsCount(
        uint32 _sponsor
    ) external view returns (uint256 count);

    function getKeyRevisionNumber(
        uint32 _point
    ) external view returns (uint32 revision);

    function getKeys(
        uint32 _point
    )
        external
        view
        returns (bytes32 crypt, bytes32 auth, uint32 suite, uint32 revision);

    function getManagementProxy(
        uint32 _point
    ) external view returns (address manager);

    function getManagerFor(
        address _proxy
    ) external view returns (uint32[] memory mfor);

    function getManagerForCount(
        address _proxy
    ) external view returns (uint256 count);

    function getOwnedPointAtIndex(
        address _whose,
        uint256 _index
    ) external view returns (uint32 point);

    function getOwnedPointCount(
        address _whose
    ) external view returns (uint256 count);

    function getOwnedPoints(
        address _whose
    ) external view returns (uint32[] memory ownedPoints);

    function getOwner(uint32 _point) external view returns (address owner);

    function getPointSize(uint32 _point) external pure returns (uint8 _size);

    function getPrefix(uint32 _point) external pure returns (uint16 prefix);

    function getSpawnCount(
        uint32 _point
    ) external view returns (uint32 spawnCount);

    function getSpawnProxy(
        uint32 _point
    ) external view returns (address spawnProxy);

    function getSpawned(
        uint32 _point
    ) external view returns (uint32[] memory spawned);

    function getSpawningFor(
        address _proxy
    ) external view returns (uint32[] memory sfor);

    function getSpawningForCount(
        address _proxy
    ) external view returns (uint256 count);

    function getSponsor(uint32 _point) external view returns (uint32 sponsor);

    function getSponsoring(
        uint32 _sponsor
    ) external view returns (uint32[] memory sponsees);

    function getSponsoringCount(
        uint32 _sponsor
    ) external view returns (uint256 count);

    function getTransferProxy(
        uint32 _point
    ) external view returns (address transferProxy);

    function getTransferringFor(
        address _proxy
    ) external view returns (uint32[] memory tfor);

    function getTransferringForCount(
        address _proxy
    ) external view returns (uint256 count);

    function getVotingFor(
        address _proxy
    ) external view returns (uint32[] memory vfor);

    function getVotingForCount(
        address _proxy
    ) external view returns (uint256 count);

    function getVotingProxy(
        uint32 _point
    ) external view returns (address voter);

    function hasBeenLinked(uint32 _point) external view returns (bool result);

    function hasSponsor(uint32 _point) external view returns (bool has);

    function incrementContinuityNumber(uint32 _point) external;

    function isActive(uint32 _point) external view returns (bool equals);

    function isEscaping(uint32 _point) external view returns (bool escaping);

    function isLive(uint32 _point) external view returns (bool result);

    function isManagementProxy(
        uint32 _point,
        address _proxy
    ) external view returns (bool result);

    function isOperator(
        address _owner,
        address _operator
    ) external view returns (bool result);

    function isOwner(
        uint32 _point,
        address _address
    ) external view returns (bool result);

    function isRequestingEscapeTo(
        uint32 _point,
        uint32 _sponsor
    ) external view returns (bool equals);

    function isSpawnProxy(
        uint32 _point,
        address _proxy
    ) external view returns (bool result);

    function isSponsor(
        uint32 _point,
        uint32 _sponsor
    ) external view returns (bool result);

    function isTransferProxy(
        uint32 _point,
        address _proxy
    ) external view returns (bool result);

    function isVotingProxy(
        uint32 _point,
        address _proxy
    ) external view returns (bool result);

    function loseSponsor(uint32 _point) external;

    function managerFor(address, uint256) external view returns (uint32);

    function managerForIndexes(address, uint32) external view returns (uint256);

    function operators(address, address) external view returns (bool);

    function owner() external view returns (address);

    function pointOwnerIndexes(address, uint32) external view returns (uint256);

    function points(
        uint32
    )
        external
        view
        returns (
            bytes32 encryptionKey,
            bytes32 authenticationKey,
            bool hasSponsor,
            bool active,
            bool escapeRequested,
            uint32 sponsor,
            uint32 escapeRequestedTo,
            uint32 cryptoSuiteVersion,
            uint32 keyRevisionNumber,
            uint32 continuityNumber
        );

    function pointsOwnedBy(address, uint256) external view returns (uint32);

    function registerSpawned(uint32 _point) external;

    function renounceOwnership() external;

    function rights(
        uint32
    )
        external
        view
        returns (
            address owner,
            address managementProxy,
            address spawnProxy,
            address votingProxy,
            address transferProxy
        );

    function setDnsDomains(
        string memory _primary,
        string memory _secondary,
        string memory _tertiary
    ) external;

    function setEscapeRequest(uint32 _point, uint32 _sponsor) external;

    function setKeys(
        uint32 _point,
        bytes32 _encryptionKey,
        bytes32 _authenticationKey,
        uint32 _cryptoSuiteVersion
    ) external;

    function setManagementProxy(uint32 _point, address _proxy) external;

    function setOperator(
        address _owner,
        address _operator,
        bool _approved
    ) external;

    function setOwner(uint32 _point, address _owner) external;

    function setSpawnProxy(uint32 _point, address _proxy) external;

    function setTransferProxy(uint32 _point, address _proxy) external;

    function setVotingProxy(uint32 _point, address _proxy) external;

    function spawningFor(address, uint256) external view returns (uint32);

    function spawningForIndexes(
        address,
        uint32
    ) external view returns (uint256);

    function sponsoring(uint32, uint256) external view returns (uint32);

    function sponsoringIndexes(uint32, uint32) external view returns (uint256);

    function transferOwnership(address _newOwner) external;

    function transferringFor(address, uint256) external view returns (uint32);

    function transferringForIndexes(
        address,
        uint32
    ) external view returns (uint256);

    function votingFor(address, uint256) external view returns (uint32);

    function votingForIndexes(address, uint32) external view returns (uint256);
}
"
    },
    "src/Interfaces/IPlanetDispenser.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IPlanetDispenser {
    function redeemPlanet(uint32 planetId, address recipient) external;
    function priceAndAllowance(
        address buyer
    ) external view returns (uint256 price, uint256 allowance, uint256 balance);
    function urbitToken() external view returns (address);
}
"
    },
    "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (token/ERC20/IERC20.sol)

pragma solidity >=0.4.16;

/**
 * @dev Interface of the ERC-20 standard as defined in the ERC.
 */
interface IERC20 {
    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);

    /**
     * @dev Returns the value of tokens in existence.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns the value of tokens owned by `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves a `value` amount of tokens from the caller's account to `to`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address to, uint256 value) external returns (bool);

    /**
     * @dev Returns the remaining number of tokens that `spender` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     *
     * This value changes when {approve} or {transferFrom} are called.
     */
    function allowance(address owner, address spender) external view returns (uint256);

    /**
     * @dev Sets a `value` amount of tokens as the allowance of `spender` over the
     * caller's tokens.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * IMPORTANT: Beware that changing an allowance with this method brings the risk
     * that someone may use both the old and the new allowance by unfortunate
     * transaction ordering. One possible solution to mitigate this race
     * condition is to first reduce the spender's allowance to 0 and set the
     * desired value afterwards:
     * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 value) external returns (bool);

    /**
     * @dev Moves a `value` amount of tokens from `from` to `to` using the
     * allowance mechanism. `value` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(address from, address to, uint256 value) external returns (bool);
}
"
    },
    "lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.3.0) (token/ERC20/utils/SafeERC20.sol)

pragma solidity ^0.8.20;

import {IERC20} from "../IERC20.sol";
import {IERC1363} from "../../../interfaces/IERC1363.sol";

/**
 * @title SafeERC20
 * @dev Wrappers around ERC-20 operations that throw on failure (when the token
 * contract returns false). Tokens that return no value (and instead revert or
 * throw on failure) are also supported, non-reverting calls are assumed to be
 * successful.
 * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
 * which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
 */
library SafeERC20 {
    /**
     * @dev An operation with an ERC-20 token failed.
     */
    error SafeERC20FailedOperation(address token);

    /**
     * @dev Indicates a failed `decreaseAllowance` request.
     */
    error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease);

    /**
     * @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value,
     * non-reverting calls are assumed to be successful.
     */
    function safeTransfer(IERC20 token, address to, uint256 value) internal {
        _callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value)));
    }

    /**
     * @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the
     * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.
     */
    function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
        _callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value)));
    }

    /**
     * @dev Variant of {safeTransfer} that returns a bool instead of reverting if the operation is not successful.
     */
    function trySafeTransfer(IERC20 token, address to, uint256 value) internal returns (bool) {
        return _callOptionalReturnBool(token, abi.encodeCall(token.transfer, (to, value)));
    }

    /**
     * @dev Variant of {safeTransferFrom} that returns a bool instead of reverting if the operation is not successful.
     */
    function trySafeTransferFrom(IERC20 token, address from, address to, uint256 value) internal returns (bool) {
        return _callOptionalReturnBool(token, abi.encodeCall(token.transferFrom, (from, to, value)));
    }

    /**
     * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
     * non-reverting calls are assumed to be successful.
     *
     * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client"
     * smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using
     * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
     * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.
     */
    function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {
        uint256 oldAllowance = token.allowance(address(this), spender);
        forceApprove(token, spender, oldAllowance + value);
    }

    /**
     * @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no
     * value, non-reverting calls are assumed to be successful.
     *
     * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client"
     * smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using
     * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
     * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.
     */
    function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal {
        unchecked {
            uint256 currentAllowance = token.allowance(address(this), spender);
            if (currentAllowance < requestedDecrease) {
                revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease);
            }
            forceApprove(token, spender, currentAllowance - requestedDecrease);
        }
    }

    /**
     * @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,
     * non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval
     * to be set to zero before setting it to a non-zero value, such as USDT.
     *
     * NOTE: If the token implements ERC-7674, this function will not modify any temporary allowance. This function
     * only sets the "standard" allowance. Any temporary allowance will remain active, in addition to the value being
     * set here.
     */
    function forceApprove(IERC20 token, address spender, uint256 value) internal {
        bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value));

        if (!_callOptionalReturnBool(token, approvalCall)) {
            _callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0)));
            _callOptionalReturn(token, approvalCall);
        }
    }

    /**
     * @dev Performs an {ERC1363} transferAndCall, with a fallback to the simple {ERC20} transfer if the target has no
     * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
     * targeting contracts.
     *
     * Reverts if the returned value is other than `true`.
     */
    function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {
        if (to.code.length == 0) {
            safeTransfer(token, to, value);
        } else if (!token.transferAndCall(to, value, data)) {
            revert SafeERC20FailedOperation(address(token));
        }
    }

    /**
     * @dev Performs an {ERC1363} transferFromAndCall, with a fallback to the simple {ERC20} transferFrom if the target
     * has no code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
     * targeting contracts.
     *
     * Reverts if the returned value is other than `true`.
     */
    function transferFromAndCallRelaxed(
        IERC1363 token,
        address from,
        address to,
        uint256 value,
        bytes memory data
    ) internal {
        if (to.code.length == 0) {
            safeTransferFrom(token, from, to, value);
        } else if (!token.transferFromAndCall(from, to, value, data)) {
            revert SafeERC20FailedOperation(address(token));
        }
    }

    /**
     * @dev Performs an {ERC1363} approveAndCall, with a fallback to the simple {ERC20} approve if the target has no
     * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
     * targeting contracts.
     *
     * NOTE: When the recipient address (`to`) has no code (i.e. is an EOA), this function behaves as {forceApprove}.
     * Opposedly, when the recipient address (`to`) has code, this function only attempts to call {ERC1363-approveAndCall}
     * once without retrying, and relies on the returned value to be true.
     *
     * Reverts if the returned value is other than `true`.
     */
    function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {
        if (to.code.length == 0) {
            forceApprove(token, to, value);
        } else if (!token.approveAndCall(to, value, data)) {
            revert SafeERC20FailedOperation(address(token));
        }
    }

    /**
     * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
     * on the return value: the return value is optional (but if data is returned, it must not be false).
     * @param token The token targeted by the call.
     * @param data The call data (encoded using abi.encode or one of its variants).
     *
     * This is a variant of {_callOptionalReturnBool} that reverts if call fails to meet the requirements.
     */
    function _callOptionalReturn(IERC20 token, bytes memory data) private {
        uint256 returnSize;
        uint256 returnValue;
        assembly ("memory-safe") {
            let success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
            // bubble errors
            if iszero(success) {
                let ptr := mload(0x40)
                returndatacopy(ptr, 0, returndatasize())
                revert(ptr, returndatasize())
            }
            returnSize := returndatasize()
            returnValue := mload(0)
        }

        if (returnSize == 0 ? address(token).code.length == 0 : returnValue != 1) {
            revert SafeERC20FailedOperation(address(token));
        }
    }

    /**
     * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
     * on the return value: the return value is optional (but if data is returned, it must not be false).
     * @param token The token targeted by the call.
     * @param data The call data (encoded using abi.encode or one of its variants).
     *
     * This is a variant of {_callOptionalReturn} that silently catches all reverts and returns a bool instead.
     */
    function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {
        bool success;
        uint256 returnSize;
        uint256 returnValue;
        assembly ("memory-safe") {
            success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
            returnSize := returndatasize()
            returnValue := mload(0)
        }
        return success && (returnSize == 0 ? address(token).code.length > 0 : returnValue == 1);
    }
}
"
    },
    "lib/openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/cryptography/ECDSA.sol)

pragma solidity ^0.8.20;

/**
 * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations.
 *
 * These functions can be used to verify that a message was signed by the holder
 * of the private keys of a given address.
 */
library ECDSA {
    enum RecoverError {
        NoError,
        InvalidSignature,
        InvalidSignatureLength,
        InvalidSignatureS
    }

    /**
     * @dev The signature derives the `address(0)`.
     */
    error ECDSAInvalidSignature();

    /**
     * @dev The signature has an invalid length.
     */
    error ECDSAInvalidSignatureLength(uint256 length);

    /**
     * @dev The signature has an S value that is in the upper half order.
     */
    error ECDSAInvalidSignatureS(bytes32 s);

    /**
     * @dev Returns the address that signed a hashed message (`hash`) with `signature` or an error. This will not
     * return address(0) without also returning an error description. Errors are documented using an enum (error type)
     * and a bytes32 providing additional information about the error.
     *
     * If no error is returned, then the address can be used for verification purposes.
     *
     * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures:
     * this function rejects them by requiring the `s` value to be in the lower
     * half order, and the `v` value to be either 27 or 28.
     *
     * IMPORTANT: `hash` _must_ be the result of a hash operation for the
     * verification to be secure: it is possible to craft signatures that
     * recover to arbitrary addresses for non-hashed data. A safe way to ensure
     * this is by receiving a hash of the original message (which may otherwise
     * be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it.
     *
     * Documentation for signature generation:
     * - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js]
     * - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers]
     */
    function tryRecover(
        bytes32 hash,
        bytes memory signature
    ) internal pure returns (address recovered, RecoverError err, bytes32 errArg) {
        if (signature.length == 65) {
            bytes32 r;
            bytes32 s;
            uint8 v;
            // ecrecover takes the signature parameters, and the only way to get them
            // currently is to use assembly.
            assembly ("memory-safe") {
                r := mload(add(signature, 0x20))
                s := mload(add(signature, 0x40))
                v := byte(0, mload(add(signature, 0x60)))
            }
            return tryRecover(hash, v, r, s);
        } else {
            return (address(0), RecoverError.InvalidSignatureLength, bytes32(signature.length));
        }
    }

    /**
     * @dev Returns the address that signed a hashed message (`hash`) with
     * `signature`. This address can then be used for verification purposes.
     *
     * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures:
     * this function rejects them by requiring the `s` value to be in the lower
     * half order, and the `v` value to be either 27 or 28.
     *
     * IMPORTANT: `hash` _must_ be the result of a hash operation for the
     * verification to be secure: it is possible to craft signatures that
     * recover to arbitrary addresses for non-hashed data. A safe way to ensure
     * this is by receiving a hash of the original message (which may otherwise
     * be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it.
     */
    function recover(bytes32 hash, bytes memory signature) internal pure returns (address) {
        (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, signature);
        _throwError(error, errorArg);
        return recovered;
    }

    /**
     * @dev Overload of {ECDSA-tryRecover} that receives the `r` and `vs` short-signature fields separately.
     *
     * See https://eips.ethereum.org/EIPS/eip-2098[ERC-2098 short signatures]
     */
    function tryRecover(
        bytes32 hash,
        bytes32 r,
        bytes32 vs
    ) internal pure returns (address recovered, RecoverError err, bytes32 errArg) {
        unchecked {
            bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);
            // We do not check for an overflow here since the shift operation results in 0 or 1.
            uint8 v = uint8((uint256(vs) >> 255) + 27);
            return tryRecover(hash, v, r, s);
        }
    }

    /**
     * @dev Overload of {ECDSA-recover} that receives the `r and `vs` short-signature fields separately.
     */
    function recover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address) {
        (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, r, vs);
        _throwError(error, errorArg);
        return recovered;
    }

    /**
     * @dev Overload of {ECDSA-tryRecover} that receives the `v`,
     * `r` and `s` signature fields separately.
     */
    function tryRecover(
        bytes32 hash,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) internal pure returns (address recovered, RecoverError err, bytes32 errArg) {
        // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
        // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
        // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most
        // signatures from current libraries generate a unique signature with an s-value in the lower half order.
        //
        // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
        // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
        // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
        // these malleable signatures as well.
        if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
            return (address(0), RecoverError.InvalidSignatureS, s);
        }

        // If the signature is valid (and not malleable), return the signer address
        address signer = ecrecover(hash, v, r, s);
        if (signer == address(0)) {
            return (address(0), RecoverError.InvalidSignature, bytes32(0));
        }

        return (signer, RecoverError.NoError, bytes32(0));
    }

    /**
     * @dev Overload of {ECDSA-recover} that receives the `v`,
     * `r` and `s` signature fields separately.
     */
    function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) {
        (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, v, r, s);
        _throwError(error, errorArg);
        return recovered;
    }

    /**
     * @dev Optionally reverts with the corresponding custom error according to the `error` argument provided.
     */
    function _throwError(RecoverError error, bytes32 errorArg) private pure {
        if (error == RecoverError.NoError) {
            return; // no error: do nothing
        } else if (error == RecoverError.InvalidSignature) {
            revert ECDSAInvalidSignature();
        } else if (error == RecoverError.InvalidSignatureLength) {
            revert ECDSAInvalidSignatureLength(uint256(errorArg));
        } else if (error == RecoverError.InvalidSignatureS) {
            revert ECDSAInvalidSignatureS(errorArg);
        }
    }
}
"
    },
    "lib/openzeppelin-contracts/contracts/utils/cryptography/MessageHashUtils.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.3.0) (utils/cryptography/MessageHashUtils.sol)

pragma solidity ^0.8.20;

import {Strings} from "../Strings.sol";

/**
 * @dev Signature message hash utilities for producing digests to be consumed by {ECDSA} recovery or signing.
 *
 * The library provides methods for generating a hash of a message that conforms to the
 * https://eips.ethereum.org/EIPS/eip-191[ERC-191] and https://eips.ethereum.org/EIPS/eip-712[EIP 712]
 * specifications.
 */
library MessageHashUtils {
    /**
     * @dev Returns the keccak256 digest of an ERC-191 signed data with version
     * `0x45` (`personal_sign` messages).
     *
     * The digest is calculated by prefixing a bytes32 `messageHash` with
     * `"\x19Ethereum Signed Message:\
32"` and hashing the result. It corresponds with the
     * hash signed when using the https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sign[`eth_sign`] JSON-RPC method.
     *
     * NOTE: The `messageHash` parameter is intended to be the result of hashing a raw message with
     * keccak256, although any bytes32 value can be safely used because the final digest will
     * be re-hashed.
     *
     * See {ECDSA-recover}.
     */
    function toEthSignedMessageHash(bytes32 messageHash) internal pure returns (bytes32 digest) {
        assembly ("memory-safe") {
            mstore(0x00, "\x19Ethereum Signed Message:\
32") // 32 is the bytes-length of messageHash
            mstore(0x1c, messageHash) // 0x1c (28) is the length of the prefix
            digest := keccak256(0x00, 0x3c) // 0x3c is the length of the prefix (0x1c) + messageHash (0x20)
        }
    }

    /**
     * @dev Returns the keccak256 digest of an ERC-191 signed data with version
     * `0x45` (`personal_sign` messages).
     *
     * The digest is calculated by prefixing an arbitrary `message` with
     * `"\x19Ethereum Signed Message:\
" + len(message)` and hashing the result. It corresponds with the
     * hash signed when using the https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sign[`eth_sign`] JSON-RPC method.
     *
     * See {ECDSA-recover}.
     */
    function toEthSignedMessageHash(bytes memory message) internal pure returns (bytes32) {
        return
            keccak256(bytes.concat("\x19Ethereum Signed Message:\
", bytes(Strings.toString(message.length)), message));
    }

    /**
     * @dev Returns the keccak256 digest of an ERC-191 signed data with version
     * `0x00` (data with intended validator).
     *
     * The digest is calculated by prefixing an arbitrary `data` with `"\x19\x00"` and the intended
     * `validator` address. Then hashing the result.
     *
     * See {ECDSA-recover}.
     */
    function toDataWithIntendedValidatorHash(address validator, bytes memory data) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked(hex"19_00", validator, data));
    }

    /**
     * @dev Variant of {toDataWithIntendedValidatorHash-address-bytes} optimized for cases where `data` is a bytes32.
     */
    function toDataWithIntendedValidatorHash(
        address validator,
        bytes32 messageHash
    ) internal pure returns (bytes32 digest) {
        assembly ("memory-safe") {
            mstore(0x00, hex"19_00")
            mstore(0x02, shl(96, validator))
            mstore(0x16, messageHash)
            digest := keccak256(0x00, 0x36)
        }
    }

    /**
     * @dev Returns the keccak256 digest of an EIP-712 typed data (ERC-191 version `0x01`).
     *
     * The digest is calculated from a `domainSeparator` and a `structHash`, by prefixing them with
     * `\x19\x01` and hashing the result. It corresponds to the hash signed by the
     * https://eips.ethereum.org/EIPS/eip-712[`eth_signTypedData`] JSON-RPC method as part of EIP-712.
     *
     * See {ECDSA-recover}.
     */
    function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32 digest) {
        assembly ("memory-safe") {
            let ptr := mload(0x40)
            mstore(ptr, hex"19_01")
            mstore(add(ptr, 0x02), domainSeparator)
            mstore(add(ptr, 0x22), structHash)
            digest := keccak256(ptr, 0x42)
        }
    }
}
"
    },
    "lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/ReentrancyGuard.sol)

pragma solidity ^0.8.20;

/**
 * @dev Contract module that helps prevent reentrant calls to a function.
 *
 * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
 * available, which can be applied to functions to make sure there are no nested
 * (reentrant) calls to them.
 *
 * Note that because there is a single `nonReentrant` guard, functions marked as
 * `nonReentrant` may not call one another. This can be worked around by making
 * those functions `private`, and then adding `external` `nonReentrant` entry
 * points to them.
 *
 * TIP: If EIP-1153 (transient storage) is available on the chain you're deploying at,
 * consider using {ReentrancyGuardTransient} instead.
 *
 * TIP: If you would like to learn more about reentrancy and alternative ways
 * to protect against it, check out our blog post
 * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
 */
abstract contract ReentrancyGuard {
    // Booleans are more expensive than uint256 or any type that takes up a full
    // word because each write operation emits an extra SLOAD to first read the
    // slot's contents, replace the bits taken up by the boolean, and then write
    // back. This is the compiler's defense against contract upgrades and
    // pointer aliasing, and it cannot be disabled.

    // The values being non-zero value makes deployment a bit more expensive,
    // but in exchange the refund on every call to nonReentrant will be lower in
    // amount. Since refunds are capped to a percentage of the total
    // transaction's gas, it is best to keep them low in cases like this one, to
    // increase the likelihood of the full refund coming into effect.
    uint256 private constant NOT_ENTERED = 1;
    uint256 private constant ENTERED = 2;

    uint256 private _status;

    /**
     * @dev Unauthorized reentrant call.
     */
    error ReentrancyGuardReentrantCall();

    constructor() {
        _status = NOT_ENTERED;
    }

    /**
     * @dev Prevents a contract from calling itself, directly or indirectly.
     * Calling a `nonReentrant` function from another `nonReentrant`
     * function is not supported. It is possible to prevent this from happening
     * by making the `nonReentrant` function external, and making it call a
     * `private` function that does the 

Tags:
ERC20, ERC165, Multisig, Voting, Upgradeable, Multi-Signature, Factory|addr:0x39f317a7e1c20b1f786b4b9a12a40c1e41de1d10|verified:true|block:23586543|tx:0xa4543e64aa7cec8f0c3f0631c68dfd73490a379b674a1190f88f3e8892dd3729|first_check:1760601436

Submitted on: 2025-10-16 09:57:19

Comments

Log in to comment.

No comments yet.