NexusFactory

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/NexusFactory.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Nexus} from "../src/Nexus.sol";
import {INexusFactory} from "../src/Interfaces/INexusFactory.sol";
import {IAzimuth} from "../src/Interfaces/IAzimuth.sol";
import {IPlanetDispenser} from "../src/Interfaces/IPlanetDispenser.sol";
import {ReentrancyGuard} from "../lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol";

contract NexusFactory is INexusFactory, ReentrancyGuard {
    ///////////////
    // Errors ////
    //////////////

    error UnauthorizedOwnerOrProxy();
    error UnauthorizedNexusContract();
    error DeployerMustControlOwnershipPoint();
    error CallerNotOwnerOfUrbitId();
    error UrbitIdNotActive();
    error CallerNotAuthorized();

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

    ////////////////////
    //// Immutables ////
    ////////////////////

    /// @notice The immutable address of azimuth
    /// @dev provided as an immutable instead of a constant to support deploying to different chains/instances
    IAzimuth private immutable i_azimuth;

    /// @notice The $URBIT ERC20 token used for invites
    address private immutable i_urbitToken;

    /////////////////////////////////
    //// Regular State Variables ////
    /////////////////////////////////

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

    bool private _allowManagementProxy;

    //////////////
    // Mappings //
    //////////////

    mapping(address => bool) private _nexusContract;

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

    // Owner required for executing transactions that update the planetMarket address.
    modifier onlyOwnerID() {
        if (!i_azimuth.isOwner(_ownerId, msg.sender) &&
            !i_azimuth.isManagementProxy(_ownerId, msg.sender)) {
            revert UnauthorizedOwnerOrProxy();
        }
        _;
    }

    // Possible function parameters here are controlled by the sending contract; this means to remain secure we must ensure that anything enabled to modify the _nexusContract mapping must only do so to add contract addresses which have been audited to properly control their external calls.
    modifier onlyNexus() {
        if (!_nexusContract[msg.sender]) {
            revert UnauthorizedNexusContract();
        }
        _;
    }

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

    constructor(
        address azimuthAddress,
        uint32 ownerId,
        address urbitToken,
        address planetMarket
    ) {
        _ownerId = ownerId;
        _planetMarket = planetMarket;
        i_azimuth = IAzimuth(azimuthAddress);
        i_urbitToken = urbitToken;

        if (!i_azimuth.isOwner(ownerId, msg.sender)) {
            revert DeployerMustControlOwnershipPoint();
        }
    }

    ///////////////
    // Functions //
    ///////////////

    ////////////////////////////
    //// External Functions ////
    ////////////////////////////

    function createNexus(
        uint32 urbitId,
        address nexusInviter,
        uint8 freeMonths,
        uint256 price,
        string memory nexusId
    ) external nonReentrant returns (address nexusContract) {
        // check that caller owns the target Urbit ID and that it is an active point
        if (!i_azimuth.isOwner(urbitId, msg.sender)) {
            revert CallerNotOwnerOfUrbitId();
        }
        if (!i_azimuth.isActive(urbitId)) revert UrbitIdNotActive();

        Nexus nexus = new Nexus(
            address(i_azimuth),
            address(this),
            urbitId,
            nexusInviter,
            freeMonths,
            price,
            i_urbitToken,
            nexusId
        );

        _nexusContract[address(nexus)] = true; // mark as trusted

        emit NexusCreated({
            leaderUrbitId: urbitId,
            nexusAddress: address(nexus),
            inviteKey: nexusInviter,
            nexusId: nexusId
        });

        return address(nexus);
    }

    function destroyNexus() external onlyNexus returns (bool success) {
        _nexusContract[msg.sender] = false;
        emit NexusDestroyed({
            nexusAddress: msg.sender
        });
        return true;
    }

    /////////////////////////
    //// Admin Functions ////
    /////////////////////////

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

    function revokeOwnershipProposal()
        external
        onlyOwnerID
        returns (uint32 _proposedOwnerIdRet)
    {
        _proposedOwnerId = _ownerId; // Just use current ownerId
        emit OwnershipProposalRevoked({
            currentOwnerId: _ownerId
        });
        return _proposedOwnerId;
    }

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

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

    function changePlanetMarket(
        address newMarket
    ) external onlyOwnerID returns (bool success) {
        _planetMarket = newMarket;
        success = true;
        emit PlanetMarketUpdated({
            newMarket: newMarket
        });
        return success;
    }

    ///////////////////////////////
    //// Nexus Event Functions ////
    ///////////////////////////////

    function updateInviteKeys(
        address nexusContract,
        address newPermissionedInviter,
        uint32 urbitId
    ) external onlyNexus returns (bool success) {
        emit NexusInviteKeysUpdated({
            nexusContract: nexusContract,
            urbitId: urbitId,
            newPermissionedInviter: newPermissionedInviter
        });
        return true;
    }

    function urbitJoined(
        address nexusContract,
        uint32 leader,
        uint32 participant
    ) external onlyNexus returns (bool success) {
        emit NexusUrbitJoined({
            nexusContract: nexusContract,
            leader: leader,
            participant: participant
        });
        return true;
    }

    function urbitDeparted(
        address nexusContract,
        uint32 leader,
        uint32 participant
    ) external onlyNexus returns (bool success) {
        emit NexusUrbitDeparted({
            nexusContract: nexusContract,
            leader: leader,
            participant: participant
        });
        return true;
    }

    //////////////////////////
    //// Getter Functions ////
    //////////////////////////

    function getOwnerId() external view returns (uint32 ownerId) {
        return _ownerId;
    }

    function getUrbitToken() external view returns (address) {
        return i_urbitToken;
    }

    function getProposedOwnerId()
        external
        view
        returns (uint32 proposedOwnerId)
    {
        return _proposedOwnerId;
    }

    function isNexusContract(address candidate) external view returns (bool) {
        return _nexusContract[candidate];
    }

    function getPlanetMarketContract()
        external
        view
        returns (address planetMarketContract)
    {
        planetMarketContract = _planetMarket;
        return planetMarketContract;
    }

    function getAzimuth() external view returns (address) {
        return address(i_azimuth);
    }

    function isManagementProxyAllowed() external view returns (bool) {
        return _allowManagementProxy;
    }

    ////////////////////////
    // Internal Functions //
    ////////////////////////

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

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

    //////////////////
    //// Fallback ////
    //////////////////

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

import {INexus} from "../src/Interfaces/INexus.sol";
import {INexusFactory} from "../src/Interfaces/INexusFactory.sol";
import {IAzimuth} from "../src/Interfaces/IAzimuth.sol";
import {ECDSA} from "../lib/openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol";
import {MessageHashUtils} from "../lib/openzeppelin-contracts/contracts/utils/cryptography/MessageHashUtils.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 {ReentrancyGuard} from "../lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol";
import {IPlanetDispenser} from "../src/Interfaces/IPlanetDispenser.sol";
import {SlugLib} from "../src/libraries/SlugLib.sol";

contract Nexus is INexus, ReentrancyGuard {
    using SafeERC20 for IERC20;

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

    error PointNotOwnedByCaller();
    error PointNotControlledByCaller();
    error UnauthorizedAccess();
    error NoInvitesRemaining();
    error BadRecipient();
    error InsufficientUrbit();
    error UrbitIdAlreadyBanned(uint32 urbitId);
    error RecipientNotOwner();
    error SignerMismatch();
    error InvalidSigner();
    error NotAMember(uint32 urbitId);
    error CallerNotAuthorized();
    error InvalidCount();
    error InsufficientInvites();
    error InsufficientUrbitBalance();
    error BadWithdrawalAddress();
    error InvitesRemain();
    error NoUrbitToRetrieve();
    error BadInviter();
    error FactoryDeregistrationFailed();
    error EthTransferFailed();

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

    ////////////////////
    //// Immutables ////
    ////////////////////

    INexusFactory private immutable i_nexusFactory;
    IAzimuth private immutable i_azimuth;
    uint32 private immutable i_urbitId;
    address private immutable i_urbitToken;

    /////////////////////////////////
    //// Regular State Variables ////
    /////////////////////////////////

    NexusNode private _nexusNode;
    bool private _allowManagementProxy;
    uint32 private _totalInvitesIssued;

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

    //////////////
    // Mappings //
    //////////////

    mapping(uint32 => bool) private _isMember;
    mapping(uint32 => bool) private _isBanned;
    mapping(uint32 => bool) private _hasQuit;

    // Anti-replay nonce for each point
    mapping(uint32 => uint256) private _onboardNonce;
    
    // Nonce for quitting
    mapping(uint32 => uint256) private _quitNonce;

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

    // All `urbitId` values are known to be active due to a check in the factory contract on creation of the Nexus
    /// @notice Access controls for functions only callable by the contract owner, who is the owner of the azimuth point
    modifier onlyOwnerID() {
        if (!i_azimuth.isOwner(i_urbitId, msg.sender)) {
            revert PointNotOwnedByCaller();
        }
        _;
    }

    modifier onlyController() {
        if (!i_azimuth.isOwner(i_urbitId, msg.sender) &&
            !(_allowManagementProxy == true &&
                i_azimuth.isManagementProxy(i_urbitId, msg.sender))) {
            revert PointNotControlledByCaller();
        }
        _;
    }

    modifier permissionedAccess() {
        bool mgmtOk = _allowManagementProxy &&
            i_azimuth.isManagementProxy(i_urbitId, msg.sender);

        if (msg.sender != _nexusNode.nexusInviter &&
            !i_azimuth.isOwner(i_urbitId, msg.sender) &&
            !mgmtOk) {
            revert UnauthorizedAccess();
        }
        _;
    }

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

    constructor(
        address azimuthAddress,
        address nexusFactory,
        uint32 urbitId,
        address nexusInviter,
        uint8 freeMonths,
        uint256 price,
        address urbitToken,
        string memory nexusId
    ) {

        SlugLib.assertValidSlug(nexusId);       
        i_azimuth = IAzimuth(azimuthAddress);
        i_nexusFactory = INexusFactory(nexusFactory);
        i_urbitId = urbitId;
        i_urbitToken = urbitToken;
        _nexusNode.ownerId = urbitId;
        _nexusNode.inviteCount = 0;
        _nexusNode.nexusInviter = nexusInviter;
        _nexusNode.freeMonths = freeMonths;
        _nexusNode.price = price;
        _nexusNode.nexusId = nexusId;
        _nexusNode.isDestroyed = false;
    }

    ///////////////
    // Functions //
    ///////////////

    ////////////////////////////
    //// External Functions ////
    ////////////////////////////

    function onboardNewUrbitID(
        address recipient,
        uint32 newUrbitId
    ) external nonReentrant permissionedAccess returns (bool success) {
        if (_nexusNode.inviteCount == 0) revert NoInvitesRemaining();
        if (recipient == address(0)) revert BadRecipient();

        address dispenser = i_nexusFactory.getPlanetMarketContract();
        IPlanetDispenser d = IPlanetDispenser(dispenser);

        // Make sure this Nexus has enough allowance & balance in $URBIT
        (uint256 price, uint256 allowance, uint256 balance) = d
            .priceAndAllowance(address(this));
        if (balance < price) revert InsufficientUrbit();
        if (allowance < price) {
            // Approve dispenser to pull $URBIT from THIS contract (approve max to avoid repeat approvals)
            IERC20(i_urbitToken).forceApprove(dispenser, type(uint256).max);
        }

        d.redeemPlanet(newUrbitId, recipient);

        _nexusNode.inviteCount--;
        _isMember[newUrbitId] = true;

        i_nexusFactory.urbitJoined(address(this), i_urbitId, newUrbitId);
        emit InviteClaimed({
            inviterUrbitId: i_urbitId,
            invitedUrbitId: newUrbitId
        });

        return true;
    }

    // Match INexus signature (bytes memory)
    function onboardExistingUrbitID(
        address recipient,
        uint32 existingUrbitId,
        bytes memory signature
    ) external nonReentrant permissionedAccess returns (bool success) {
        if (_nexusNode.inviteCount == 0) revert NoInvitesRemaining();
        if (_isBanned[existingUrbitId]) revert UrbitIdAlreadyBanned(existingUrbitId);
        if (recipient != i_azimuth.getOwner(existingUrbitId)) {
            revert RecipientNotOwner();
        }

        // Reconstruct signed message  (EIP-191 personal-sign)
        bytes32 structHash = keccak256(
            abi.encode(
                ONBOARD_TYPEHASH,
                i_urbitId,
                existingUrbitId,
                recipient,
                _onboardNonce[existingUrbitId]
            )
        );
        bytes32 digest = MessageHashUtils.toEthSignedMessageHash(structHash);

        // Recover and check signer
        address signer = ECDSA.recover(digest, signature);
        if (signer != i_azimuth.getOwner(existingUrbitId) &&
            !i_azimuth.isManagementProxy(existingUrbitId, signer)) {
            revert InvalidSigner();
        }

        // Bump nonce to invalidate signature forever
        unchecked {
            _onboardNonce[existingUrbitId]++;
        }

        _nexusNode.inviteCount--;
        _isMember[existingUrbitId] = true;

        i_nexusFactory.urbitJoined(address(this), i_urbitId, existingUrbitId);
        emit InviteClaimed({
            inviterUrbitId: i_urbitId,
            invitedUrbitId: existingUrbitId
        });
        return true;
    }

    function permissionedQuitNexus(
        uint32 quittingUrbitId,
        bytes memory signature
    ) external nonReentrant permissionedAccess returns (bool success) {
        if (!_isMember[quittingUrbitId]) revert NotAMember(quittingUrbitId);
       
        bytes32 structHash = keccak256(
            abi.encode(
                QUIT_TYPEHASH,
                i_urbitId,                      
                quittingUrbitId,
                _quitNonce[quittingUrbitId],     
                address(this),                   
                block.chainid                    
            )
        );
        // EIP-191 personal_sign digest
        bytes32 digest = MessageHashUtils.toEthSignedMessageHash(structHash);

        // Signer must currently control the quitting ID
        address signer = ECDSA.recover(digest, signature);
        if (
            signer != i_azimuth.getOwner(quittingUrbitId) &&
            !i_azimuth.isManagementProxy(quittingUrbitId, signer)
        ) {
            revert InvalidSigner();
        }

        unchecked { _quitNonce[quittingUrbitId]++; }

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

        i_nexusFactory.urbitDeparted(address(this), i_urbitId, quittingUrbitId);
        emit UrbitIdPermissionedQuit({ quittingUrbitId: quittingUrbitId });
        return true;
    }

    function rageQuitNexus(
        uint32 rageQuitUrbitId
    ) external nonReentrant returns (bool success) {
        if (!_isMember[rageQuitUrbitId]) revert NotAMember(rageQuitUrbitId);

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

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

        i_nexusFactory.urbitDeparted(address(this), i_urbitId, rageQuitUrbitId);
        emit UrbitIdRageQuit({ rageQuitUrbitId: rageQuitUrbitId });
        return true;
    }

    /////////////////////////
    //// Admin Functions ////
    /////////////////////////

    function allowManagementProxyAccess(
        bool access
    ) external onlyOwnerID returns (address managementAddress) {
        _allowManagementProxy = access;
        if (access == true) {
            managementAddress = i_azimuth.getManagementProxy(i_urbitId);
        } else {
            managementAddress = address(0);
        }
        emit ManagementProxyAccessSet(access, managementAddress);
        return managementAddress;
    }

    // Match INexus signature (includes newNexusInviter)
    function banUrbitId(
        uint32 urbitId
    ) external onlyController returns (bool success) {
        _isBanned[urbitId] = true;
        _isMember[urbitId] = false;
        emit UrbitIdBanned(urbitId);

        return true;
    }

    function unbanUrbitId(
        uint32 urbitId
    ) external onlyController returns (bool success) {
        _isBanned[urbitId] = false;
        emit UrbitIdUnbanned({
            unbannedUrbitId: urbitId
        });
        return true;
    }

    function addInvites(
        uint32 count
    ) external nonReentrant permissionedAccess returns (bool success) {
        if (count == 0) revert InvalidCount();

        // Get dispenser + $URBIT token address
        address dispenser = i_nexusFactory.getPlanetMarketContract();
        IPlanetDispenser d = IPlanetDispenser(dispenser);
        (uint256 price, , ) = d.priceAndAllowance(address(this));
        uint256 amount = price * uint256(count);

        // Pull $URBIT from the controller into THIS contract
        IERC20(i_urbitToken).safeTransferFrom(
            msg.sender,
            address(this),
            amount
        );

        _nexusNode.inviteCount += count;
        _totalInvitesIssued += count;
        emit InviteTokenDeposited(msg.sender, count);
        return true;
    }

    function removeInvites(
        uint32 count
    ) external nonReentrant permissionedAccess returns (bool success) {
        if (count == 0) revert InvalidCount();
        if (_nexusNode.inviteCount < count) revert InsufficientInvites();

        // Get dispenser + URBIT price
        address dispenser = i_nexusFactory.getPlanetMarketContract();
        IPlanetDispenser d = IPlanetDispenser(dispenser);
        (uint256 price, , ) = d.priceAndAllowance(address(this));
        uint256 amount = price * uint256(count);

        // Ensure this contract actually holds enough $URBIT to refund
        uint256 bal = IERC20(i_urbitToken).balanceOf(address(this));
        if (bal < amount) revert InsufficientUrbitBalance();

        // Effects
        _nexusNode.inviteCount -= count;

        // Interaction: return $URBIT to the controller
        IERC20(i_urbitToken).safeTransfer(msg.sender, amount);

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

    function retrieveUrbitTokens(
        address withdrawalAddress
    ) external nonReentrant onlyController returns (bool success) {
        if (withdrawalAddress == address(0)) revert BadWithdrawalAddress();
        // Only when there are no outstanding invites
        if (_nexusNode.inviteCount != 0) revert InvitesRemain();

        uint256 bal = IERC20(i_urbitToken).balanceOf(address(this));
        if (bal == 0) revert NoUrbitToRetrieve();

        IERC20(i_urbitToken).safeTransfer(withdrawalAddress, bal);

        emit UrbitTokensRetrieved(withdrawalAddress, bal);

        return true;
    }

    function changeNexusInviter(
        address newNexusInviter
    ) external onlyController returns (bool success) {
        // Emits Changed; also emit Added/Removed for richer indexing
        address old = _nexusNode.nexusInviter;
        success = _changeNexusInviter(newNexusInviter);
        if (newNexusInviter == address(0) && old != address(0)) {
            emit NexusInviterRemoved(old);
        } else if (newNexusInviter != address(0)) {
            emit NexusInviterAdded(newNexusInviter);
        }
        return success;
    }

    function removeNexusInviter()
        external
        onlyController
        returns (bool success)
    {
        address old = _nexusNode.nexusInviter;
        success = _changeNexusInviter(address(0));
        if (old != address(0)) {
            emit NexusInviterRemoved(old);
        }
        return success;
    }

    function addNexusInviter(
        address newInviter
    ) external onlyController returns (bool success) {
        if (newInviter == address(0)) revert BadInviter();
        success = _changeNexusInviter(newInviter);
        emit NexusInviterAdded(newInviter);
        return success;
    }


    function destroyNexus() external nonReentrant onlyOwnerID returns (bool success) {
        // Close onboarding
        address old = _nexusNode.nexusInviter;
        _changeNexusInviter(address(0));
        if (old != address(0)) {
            emit NexusInviterRemoved(old);
        }

        // Withdraw all URBIT to the owner (caller)
        uint256 bal = IERC20(i_urbitToken).balanceOf(address(this));
        if (bal > 0) {
            IERC20(i_urbitToken).safeTransfer(msg.sender, bal);
        }

        // Reset invites and mark destroyed
        _nexusNode.inviteCount = 0;
        _nexusNode.isDestroyed = true;

        // Tell the factory to de-register this Nexus and emit destruction
        bool dereg = i_nexusFactory.destroyNexus();
        if (!dereg) revert FactoryDeregistrationFailed();

        // Emit destruction event
        emit NexusDestroyed(address(this), i_urbitId);

        // Transfer any remaining ETH to the owner
        if (address(this).balance > 0) {
            (bool ethSuccess, ) = payable(msg.sender).call{
                value: address(this).balance
            }("");
            if (!ethSuccess) revert EthTransferFailed();
        }

        return true;
    }

    //////////////////////////
    //// Getter Functions ////
    //////////////////////////

    /// @notice Returns the entire NexusNode struct
    function getNexusNode() external view returns (NexusNode memory) {
        return _nexusNode;
    }

    /// @notice Returns core config fields individually
    function getNexusInviter() external view returns (address) {
        return _nexusNode.nexusInviter;
    }
    function getInviteCount() external view returns (uint32) {
        return _nexusNode.inviteCount;
    }
    function getTotalInvitesIssued() external view returns (uint32) {
        return _totalInvitesIssued;
    }
    function getFreeMonths() external view returns (uint8) {
        return _nexusNode.freeMonths;
    }
    function isDestroyed() external view returns (bool) {
        return _nexusNode.isDestroyed;
    }
    function getPrice() external view returns (uint256) {
        return _nexusNode.price;
    }
    function getNexusId() external view returns (string memory) {
        return _nexusNode.nexusId;
    }

    /// @notice Returns factory and protocol addresses
    function getAzimuth() external view returns (address) {
        return address(i_azimuth);
    }
    function getNexusFactory() external view returns (address) {
        return address(i_nexusFactory);
    }
    function getNexusOwnerId() external view returns (uint32) {
        return i_urbitId;
    }

    /// @notice Returns whether mgmt proxy is allowed and the current proxy address in Azimuth (if any)
    function isManagementProxyAllowed() external view returns (bool) {
        return _allowManagementProxy;
    }
    function getManagementProxy() external view returns (address) {
        return i_azimuth.getManagementProxy(i_urbitId);
    }

    /// @notice Convenience getters related to dispenser/$URBIT
    function getDispenser() public view returns (address) {
        return i_nexusFactory.getPlanetMarketContract();
    }
    function getUrbitToken() public view returns (address) {
        return i_urbitToken;
    }
    function urbitTokenBalance() external view returns (uint256) {
        return IERC20(i_urbitToken).balanceOf(address(this));
    }
    function urbitTokenAllowance(
        address spender
    ) external view returns (uint256) {
        return IERC20(i_urbitToken).allowance(address(this), spender);
    }

    /// @notice Returns status flags for a given urbitId
    function isMember(uint32 urbitId) external view returns (bool) {
        return _isMember[urbitId];
    }
    function isBanned(uint32 urbitId) external view returns (bool) {
        return _isBanned[urbitId];
    }
    function hasQuit(uint32 urbitId) external view returns (bool) {
        return _hasQuit[urbitId];
    }
    function onboardNonce(uint32 urbitId) external view returns (uint256) {
        return _onboardNonce[urbitId];
    }
    function rageQuitNonce(uint32 urbitId) external view returns (uint256) {
        return _quitNonce[urbitId];
    }

    ////////////////////////
    // Internal Functions //
    ////////////////////////

    function _changeNexusInviter(
        address newNexusInviter
    ) internal returns (bool success) {
        address oldNexusInviter = _nexusNode.nexusInviter;
        _nexusNode.nexusInviter = newNexusInviter;
        emit NexusInviterChanged({
            oldNexusInviter: oldNexusInviter,
            newNexusInviter: newNexusInviter
        });
        return true;
    }

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

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

    //////////////////
    //// Fallback ////
    //////////////////

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

interface INexusFactory {
    ////////////
    // Events //
    ////////////

    // Lifecycle of Nexus contracts
    event NexusCreated(uint32 indexed leaderUrbitId, address indexed nexusAddress, address indexed inviteKey, string nexusId);
    event NexusDestroyed(address indexed nexusAddress);

    // 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);

    // Nexus-sourced signals centralized through factory (for indexing)
    event NexusInviteKeysUpdated(address indexed nexusContract, uint32 indexed urbitId, address indexed newPermissionedInviter);
    event NexusUrbitInvited(address indexed nexusContract, uint32 indexed inviter, uint32 indexed invitee);
    event NexusUrbitDeparted(address indexed nexusContract, uint32 indexed leader, uint32 indexed participant);
    event NexusUrbitJoined(address indexed nexusContract, uint32 indexed leader, uint32 indexed participant);

    ////////////////////////
    // External Functions //
    ////////////////////////

    function changePlanetMarket(
        address newMarket
    ) external returns (bool success);

    function createNexus(
        uint32 urbitId,
        address nexusInviter,
        uint8 freeMonths,
        uint256 price,
        string memory nexusId
    ) external returns (address nexusContract);

    function destroyNexus() external returns (bool success);

    function getOwnerId() external view returns (uint32 owner);

    function getPlanetMarketContract()
        external
        view
        returns (address planetMarketContract);

    function getUrbitToken() external view returns (address);

    function updateInviteKeys(
        address nexusContract,
        address newPermissionedInviter,
        uint32 urbitId
    ) external returns (bool success);

    function urbitDeparted(
        address nexusContract,
        uint32 leader,
        uint32 participant
    ) external returns (bool success);

    function urbitJoined(
        address nexusContract,
        uint32 leader,
        uint32 participant
    ) external returns (bool success);

    function proposeOwner(uint32 newOwnerId) external returns (uint32 proposedOwner);
    function revokeOwnershipProposal() external returns (uint32 proposedOwner);
    function acceptOwnership() external returns (uint32 owner);
    function rejectOwnership() external returns (uint32 proposedOwner);
}
"
    },
    "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/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 actual work.
     */
    modifier nonReentrant() {
        _nonReentrantBefore();
        _;
        _nonReentrantAfter();
    }

    function _nonReentrantBefore() private {
        // On the first call to nonReentrant, _status will be NOT_ENTERED
        if (_status == ENTERED) {
            revert ReentrancyGuardReentrantCall();
        }

        // Any calls to nonReentrant after this point will fail
        _status = ENTERED;
    }

    function _nonReentrantAfter() private {
        // By storing the original value once again, a refund is triggered (see
        // https://eips.ethereum.org/EIPS/eip-2200)
        _status = NOT_ENTERED;
    }

    /**
     * @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
     * `nonReentrant` function in the call stack.
     */
    function _reentrancyGuardEntered() internal view returns (bool) {
        return _status == ENTERED;
    }
}
"
    },
    "src/Interfaces/INexus.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;


interface INexus {
    /////////////
    // Structs //
    /////////////

    // parameter ordering done to allow efficient bit packing
    struct NexusNode {
        // @ud of owner's Urbit ID
        uint32 ownerId; // 4 bytes
        // public address of permissioned ticket
        address nexusInviter; // 20 bytes
        // count of available invites
        uint32 inviteCount; // 4 bytes
        // number of months before subscription cost kicks in
        uint8 freeMonths; // 1 byte
        // is the node deactivated?
        bool isDestroyed; // 1 byte
        // price of monthly subscription
        uint256 price; // 32 bytes
        // URL-safe identifier for this nexus
        string nexusId;
    }

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

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

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

    // Membership lifecycle
    event InviteClaimed(
        uint32 indexed inviterUrbitId,
        uint32 indexed invitedUrbitId
    );
    event UrbitIdBanned(uint32 indexed bannedUrbitId);
    event UrbitIdUnbanned(uint32 indexed unbannedUrbitId);
    event UrbitIdRageQuit(uint32 indexed rageQuitUrbitId);
    event UrbitIdPermissionedQuit(
        uint32 indexed quittingUrbitId
    );

    // Admin toggles / lifecycle
    event ManagementProxyAccessSet(
        bool allowed,
        address indexed proxy
    );
    event NexusDestroyed(
        address indexed nexus,
        uint32 ownerId
    );

    ////////////////////////
    // External Functions //
    ////////////////////////

    function addInvites(uint32 count) external returns (bool success);

    function addNexusInviter(
        address newInviter
    ) external returns (bool success);

    function banUrbitId(uint32 urbitId) external returns (bool success);

    function changeNexusInviter(
        address newNexusInviter
    ) external returns (bool success);

    function destroyNexus() external returns (bool success);

    function onboardExistingUrbitID(
        address recipient,
        uint32 existingUrbitId,
        bytes memory signature
    ) external returns (bool success);

    function onboardNewUrbitID(
        address recipient,
        uint32 newUrbitId
    ) external returns (bool success);

    function permissionedQuitNexus(
        uint32 quittingUrbitId,
        bytes memory signature
    ) external returns (bool success);

    function rageQuitNexus(
        uint32 rageQuitUrbitId
    ) external returns (bool success);

    function removeInvites(uint32 count) external returns (bool success);

    function removeNexusInviter() external returns (bool success);

    function retrieveUrbitTokens(
        address withdrawalAddress
    ) external returns (bool success);

    function unbanUrbitId(uint32 urbitId) external returns (bool success);

    function allowManagementProxyAccess(
        bool access
    ) external returns (address managementAddress);

    //////////////////////
    // Getter Functions //
    //////////////////////

    function getNexusNode() external view returns (NexusNode memory);

    function getNexusInviter() external view returns (address);

    function getInviteCount() external view returns (uint32);

    function getTotalInvitesIssued() external view returns (uint32);

    function getFreeMonths() external view returns (uint8);

    function isDestroyed() external view returns (bool);

    function getPrice() external view returns (uint256);

    function getNexusId() external view returns (string memory);

    function getAzimuth() external view returns (address);

    function getNexusFactory() external view returns (address);

    function getNexusOwnerId() external view returns (uint32);

    function isManagementProxyAllowed() external view returns (bool);

    function getManagementProxy() external view returns (address);

    function urbitTokenBalance() external view returns (uint256);

    function isMember(uint32 urbitId) external view returns (bool);

    function isBanned(uint32 urbitId) external view returns (bool);

    function hasQuit(uint32 urbitId) external view returns (bool);

    function onboardNonce(uint32 urbitId) external view returns (uint256);
    
    function rageQuitNonce(uint32 urbitId) external view returns (uint256);
}
"
    },
    "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/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

Tags:
ERC20, ERC165, Multisig, Voting, Upgradeable, Multi-Signature, Factory|addr:0x86c5c98c905cb01d7828eb36a4b2a1696e732c1a|verified:true|block:23448712|tx:0xcc0308f7a845be08a1c18c1e8393ccdc82b05d203accf5ae401a892fe4fbfde2|first_check:1758912845

Submitted on: 2025-09-26 20:54:05

Comments

Log in to comment.

No comments yet.