Skip to content

BORG Auth and BorgAuthACL (auth.sol)

BorgAuth is the central access control contract in the BORG system, providing multi-level permission management for all BORG modules. In simple terms, it acts as a security hierarchy: Owner, Admin, and Privileged roles (represented by numeric levels 99, 98, 97 respectively) are assigned to addresses, defining what they can do. This ensures that BORG members operate under oversight – typically the DAO or a designated “Authority BORG” holds the Owner role (level 99), staying one level above regular BORG members (often Admin at 98). By structuring control in tiers, BorgAuth makes sure no single actor can exceed their authority, and that a higher authority can always intervene if needed. It’s an adaptable system as well: BorgAuth allows plugging in external auth adapters so that other contracts or DAO logic can grant permissions dynamically (for example, letting a DAO’s vote outcomes authorize certain actions). Every BORG contract (the core and each implant) inherits these rules by linking to a BorgAuth instance, creating a unified permissions framework across the entire BORG.

Role Hierarchy and Purpose

BorgAuth defines three built-in roles with a clear hierarchy: Owner (99) as the highest level, Admin (98) as the next, and Privileged (97) as a base level. Higher numeric value means higher authority – an address with Owner role can do anything an Admin can (and more), and an Admin likewise outranks Privileged users. In practice, you might assign BORG safe owners as Admins, while the DAO’s multi-sig or an oversight contract is the sole Owner. This way, BORG members can perform day-to-day admin operations, but truly critical actions require the top-level role. The contract’s design enforces this by checking that a caller’s role is greater than or equal to the required level for an action. For a non-technical analogy, imagine a company: Privileged users are staff, Admins are managers, and the Owner is the executive – each level can do its job, but some decisions are reserved for higher-ups.

Key Functions in BorgAuth (Access Control Logic)

BorgAuth’s functions manage roles and enforce permissions. Each function contributes to the security and flexibility of the BORG system’s access control:

  • updateRole(address user, uint256 role) – Assign or change a user’s role. Only an address with Owner privileges can call this. It’s commonly used to add new BORG members or elevate/demote their permissions. For example, after deploying a BORG, the deployer (initial Owner) might call updateRole to make certain addresses Admins (role 98). Notably, the contract forbids an Owner from demoting their own role through this function (to prevent accidentally removing the only Owner). A new Owner must be appointed first before the original can relinquish control.
  • initTransferOwnership(address newOwner) – Begins a two-step process to transfer the Owner role to a new address. Only the current Owner can initiate this. Calling this sets a pendingOwner. This function by itself doesn’t change any roles; it simply declares an intended new Owner. The requirement that newOwner cannot be the caller or the zero address ensures a valid handoff is being set up.
  • acceptOwnership() – Finalizes the ownership transfer. The pendingOwner (and only that address) should call this to officially become the new Owner. Upon a correct call, the contract assigns role 99 (Owner) to the pending address and clears the pending slot. This two-step handshake (initiate then accept) protects against accidentally losing ownership: the transfer only completes if the designated new owner confirms it. Under the hood, acceptOwnership uses _updateRole to grant Owner status and emits a RoleUpdated event for transparency.
  • zeroOwner() – Revokes the caller’s Owner status, irreversibly dropping them to no role (0). Only an Owner can call this, and doing so when they are the sole Owner will leave the contract with no address holding Owner privileges. This function is like a deliberate self-destruct of admin privileges and is used to renounce direct control. In practice, this might be done once a BORG is fully configured and handed off to autonomous governance – for example, the Owner could call zeroOwner after setting up all roles and thereby prevent any future manual tampering. Warning: After zeroOwner, no one can call updateRole anymore, so it effectively locks the role assignments permanently (which may be the desired security in a production phase).
  • setRoleAdapter(uint256 role, address adapter) – Links an external authorization contract (adapter) to a specific role level. Only the Owner can configure this. This is a powerful extension point: when an adapter is set, BorgAuth will defer to that adapter’s logic to decide if a user is authorized for that role. The adapter must implement the IAuthAdapter interface (specifically, an isAuthorized(address user) returns (uint256)). During permission checks, if a user’s direct role is insufficient, BorgAuth calls adapter.isAuthorized(user) – if that returns a role >= the required level, the check will pass. In essence, role adapters allow dynamic or external criteria to grant roles on the fly. For example, you could attach a DAO governance contract as an adapter for the Owner role: instead of a single hardcoded Owner address, the DAO’s decisions (via the adapter contract logic) could determine who is treated as an Owner or when certain owner-level actions are allowed. This makes the ACL system modular and able to integrate with off-chain or cross-contract permissions. The AdapterUpdated event logs any changes to adapters for transparency.
  • onlyRole(uint256 role, address user) – The internal authorization check used throughout the system. This function (marked view) verifies that user has at least the given role, otherwise it throws an error. Here’s how it works: it looks up the user’s assigned role in the userRoles mapping. If the user’s role number is lower than the required level, it then checks if an adapter is defined for that role. If a roleAdapter exists, BorgAuth queries the adapter’s isAuthorized(user) result. Should the adapter report a role >= the required level, onlyRole considers the user authorized and returns without error. If neither the direct role nor the adapter’s authorization meet the criteria, it reverts with BorgAuth_NotAuthorized, stopping the calling operation. This function is the backbone of all the modifiers like onlyOwner/onlyAdmin; it centralizes the logic for role enforcement.
  • _updateRole(address user, uint256 role) (internal) – A private helper that actually writes the role into the userRoles mapping and emits the RoleUpdated event. Both updateRole and the ownership transfer functions use _updateRole under the hood. Whenever someone’s role changes (be it granting Owner to a new address or revoking an Admin’s access), a RoleUpdated(user, newRole) event is emitted for off-chain monitoring.

All these functions work together to maintain a robust permission structure. Events RoleUpdated and AdapterUpdated are emitted on changes, which helps with auditing and UI feedback (for example, front-ends could listen to know when a new admin is added or an adapter is set).

Using BorgAuth: Typical Scenarios and Patterns

When a new BORG is deployed, the BorgAuth contract is one of the first things created, and the deploying account is automatically given the Owner role (99). This initial Owner can then configure the rest of the system: for instance, they might call updateRole to assign Admin role (98) to the addresses that will serve as the BORG’s multisig members. They could also set up a higher authority: for example, if a DAO’s address should oversee the BORG, the Owner might use updateRole(daoAddress, 99) to add a DAO-controlled account as an Owner too. Once setup is complete, ownership can be handed off securely – either using the safe two-step initTransferOwnership/acceptOwnership process, or by directly assigning a new Owner and then removing oneself. In one deployment script example, the deployer adds an executor address as a new Owner (auth.updateRole(executor, 99)) and then calls auth.zeroOwner() to renounce their own role. This effectively transfers full control to the executor address in a single sequence.

Another scenario is integrating a DAO voting contract via role adapters. Suppose you want the community (via a governance contract) to approve certain admin actions without hard-coding individual Admin addresses. You could write a custom IAuthAdapter that checks if a user’s address is whitelisted by the DAO or if a proposal passed, and have BorgAuth use it. By calling auth.setRoleAdapter(auth.ADMIN_ROLE(), address(myAdapter)), the BorgAuth will consult myAdapter.isAuthorized(user) whenever an Admin-level action is attempted by user. If, say, the adapter returns 98 for users holding a certain governance token, then those users would effectively have Admin rights in the BORG system without being explicitly added in userRoles. This pattern is powerful for linking on-chain governance with BORG permissions.

BorgAuthACL – Inherited Access Control in Every Module

While BorgAuth is the standalone ACL contract, BorgAuthACL is an abstract contract (also defined in auth.sol) that BORG modules inherit to easily use these permissions. BorgAuthACL stores an immutable reference AUTH to the BorgAuth instance and provides common modifiers: onlyOwner, onlyAdmin, onlyPriv, and a generic onlyRole(_role). These modifiers are convenience wrappers that internally call AUTH.onlyRole(requiredRole, msg.sender) before executing the rest of a function. This means any function in a BORG contract tagged with onlyOwner will automatically enforce that the caller has role 99 in the shared BorgAuth, or the call will be rejected. For example, in the borgCore guard contract, critical configuration methods like adding a whitelisted recipient are defined as function addRecipient(address _recipient, uint256 _limit) external onlyOwner – only a caller with Owner role can execute that. By contrast, some routine updates in borgCore (such as setting the BORG’s human-readable identifier or adding a legal agreement URI) are marked onlyAdmin, so any Admin-level user can perform them. Similarly, implants inherit BorgAuthACL via a base class, so their functions can require appropriate roles. Most implants restrict dangerous operations (like changing limits or toggling settings) to the Owner, whereas actions meant for regular BORG members might allow Admin or Privileged access depending on the design. All these checks route back to the single BorgAuth source of truth, ensuring consistency.

Because each BORG module references the same BorgAuth contract for roles, updating someone’s role immediately affects permissions across the board. This central design avoids having to configure roles in multiple places – you manage roles in one contract and all guards, modules, and adapters respect those settings. It also enhances security by reducing complexity: the logic for authorization is centralized and auditable in BorgAuth, rather than spread throughout the code. In terms of modularity, if MetaLex introduces new implants or adapters, they can all hook into the existing BorgAuth system without modification. And if a particular BORG needs a custom role logic, the setRoleAdapter mechanism allows extending or overriding the basic role-checks in a clean way.

Summary

In summary, auth.sol (BorgAuth and BorgAuthACL) is the linchpin of BORG contract security and governance. It establishes a clear chain of command (Owner > Admin > Privileged) and provides the tools to manage that chain – assigning roles, transferring ownership, removing superuser access, and delegating authority to external logic. BORGs and their associated components constantly interact with these functions: every restricted action in the BORG goes through a BorgAuth check to ensure the caller is allowed. For developers, BorgAuth offers an extendable, event-rich ACL pattern that can be tailored to complex governance setups. For general users or organizations, it means the BORG operates with checks and balances: the core can automate operations, yet always under the watchful eye of the roles defined in BorgAuth. This design keeps the BORG’s cybernetic autonomy aligned with the governance rules of its community or parent DAO, balancing flexibility with security in the MetaLex ecosystem.

Code Example: Using BorgAuth in a BORG setup

// 1. Deploy the BorgAuth contract – the deployer becomes the initial Owner (role 99).
BorgAuth auth = new BorgAuth();
address deployer = msg.sender;
// auth.userRoles(deployer) == 99 (Owner by constructor)
 
// 2. Assign roles to BORG members and possibly a DAO oversight:
auth.updateRole(borgMember1, auth.ADMIN_ROLE());   // Make borgMember1 an Admin (98)
auth.updateRole(borgMember2, auth.ADMIN_ROLE());   // borgMember2 is also Admin.
auth.updateRole(daoMultisig, auth.OWNER_ROLE());   // Give the DAO’s multisig full Owner rights (99)
 
// (At this point, deployer, as Owner, has set up two Admins and added the DAO as co-Owner.)
 
// 3. Use the two-step ownership transfer if handing off control:
auth.initTransferOwnership(newOwner);    // Propose newOwner as the next Owner
// ... later, from newOwner’s account:
auth.acceptOwnership();                  // newOwner officially becomes Owner
 
// (Alternatively, the deployer could transfer by adding newOwner as Owner and then removing itself:)
auth.updateRole(newOwner, auth.OWNER_ROLE());  // grant Owner to newOwner
auth.zeroOwner();                              // deployer renounces its own Owner role
 
// 4. (Optional) Set up an auth adapter, e.g., to integrate DAO voting for Admin role:
auth.setRoleAdapter(auth.ADMIN_ROLE(), address(daoAdapterContract));
// Now, even if someone isn’t in userRoles with 98, the daoAdapterContract’s logic can authorize them.