Skip to content
LogoLogo

Access control

Ownable

std/access/ownable.lithe. One owner. Set it with init, usually from deploy, and guard owner-only entrypoints with checkOwner.

Function
owner, transferOwnership, renounceOwnershipthe externals of OpenZeppelin's Ownable, IOwnable: only the owner transfers, never to the zero address, or renounces, which leaves no owner
checkOwner()raises OwnableUnauthorizedAccount unless the caller is the owner
init(initialOwner)sets the first owner; raises OwnableInvalidOwner for the zero address
setOwner(newOwner)moves ownership with no checks, for the contract's own rules

Ownable2Step

std/access/ownable_two_step.lithe, on top of Ownable. The owner proposes a new owner, who must accept. An address that can't accept, because it was mistyped, never takes ownership. Its externals are all of OpenZeppelin's Ownable2Step, owner included, as IOwnable2Step, so a contract exposes them from Ownable2Step and only uses Ownable.

import "std/access/ownable";
import "std/evm";
import "std/access/ownable_two_step";
 
contract Vault is {
  use Ownable;
 
  use Ownable2Step exposing IOwnable2Step;
 
  storage is record {
    limit : uint;
  }
 
  deploy()
    raises OwnableInvalidOwner
  is {
    Ownable.init(Evm.sender());
  }
 
  external function setLimit(const limit : uint)
    raises OwnableUnauthorizedAccount
  is {
    Ownable.checkOwner();
    storage.limit := limit;
  }
}

OwnableRoles

std/access/ownable_roles.lithe, on top of Ownable, as Solady's OwnableRoles. Up to 256 roles, kept as one Roles bitmask per account in one slot, so checking or granting several roles at once costs the same as one. A roles is a nominal type over uint, a uint256 in the ABI: role(n) is Solady's _ROLE_n, and masks combine with |. The owner grants and revokes, and an account can renounce its own roles. Every change logs Solady's RolesUpdated, and a failed role check raises its Unauthorized. Grants and revokes that need the owner raise Ownable's OwnableUnauthorizedAccount.

import "std/access/ownable_roles";
import "std/access/ownable";
import "std/evm";
 
contract Minter is {
  use Ownable exposing IOwnable;
  use OwnableRoles exposing IOwnableRoles;
 
  storage is record {
    minted : map(address, uint);
    paused : bool;
  }
 
  pure function minter() : Roles is OwnableRoles.role(0)
  pure function pauser() : Roles is OwnableRoles.role(1)
 
  deploy()
    raises OwnableInvalidOwner
  is {
    Ownable.init(Evm.sender());
    OwnableRoles.grant(Evm.sender(), minter() | pauser());
  }
 
  external function mint(const to : address; const amount : uint)
    raises Unauthorized
  is {
    OwnableRoles.checkRoles(minter());
    storage.minted[to] := storage.minted[to] + amount;
  }
 
  external function pause()
    raises Unauthorized
  is {
    OwnableRoles.checkOwnerOrRoles(pauser());
    storage.paused := true;
  }
}
Function
grantRoles, revokeRoles, renounceRoles, rolesOf, hasAnyRole, hasAllRolesthe role externals of Solady's OwnableRoles, IOwnableRoles: only the owner grants and revokes, and a caller renounces its own roles
role(ordinal : uint8) : Rolesthe one role numbered ordinal
checkRoles(r)raises Unauthorized unless the caller holds one of r
checkOwnerOrRoles(r), checkRolesOrOwner(r)the same, but the owner passes too; they differ only in which is read first
grant(user, r), revoke(user, r), setRoles(user, r)unchecked, for deploy and the contract's own rules; setRoles replaces the whole mask
rolesFromOrdinals(ordinals : list(uint8)) : Rolesthe roles numbered by ordinals, together: Solady's _rolesFromOrdinals
ordinalsFromRoles(r) : list(uint8)the ordinals of the roles in r, ascending: Solady's _ordinalsFromRoles

ordinalsFromRoles visits only the set bits, so it costs 1,199 gas for a mask of four roles, highest 255, where Solady's, which visits every bit up to the highest, costs 21,914. rolesFromOrdinals costs 1,059 for four ordinals, against Solady's 1,093.

Solady marks grantRoles, revokeRoles, and renounceRoles payable, which saves about 15 gas of the check that rejects ether. Here they are not payable, as Ownable's externals and OpenZeppelin's are not: ether sent with a role change would be stuck in the contract, and the check runs once per call of a function the owner rarely calls. A contract that wants Solady's ABI exactly writes its own external payable function around grantRoles, revokeRoles, or renounceRoles.

OwnableRoles suits a fixed set of roles the owner hands out. AccessControl suits roles named by hash, each administered by another role.

AccessControl

std/access/access_control.lithe. Roles, each with an admin role whose members grant and revoke it. A role is a Role, a nominal type over bytes32, so a stray hash can't be passed where a role is meant. In the ABI, a role is a bytes32, so the file's IAccessControl is OpenZeppelin's, and a contract that exposes it can announce it with supports.

import "std/crypto";
import "std/evm";
import "std/access/access_control";
 
contract Minter is {
  use AccessControl exposing {
    view function hasRole(role : Role; account : address) : bool;
    function grantRole(role : Role; account : address)
      raises AccessControlUnauthorizedAccount;
    function revokeRole(role : Role; account : address)
      raises AccessControlUnauthorizedAccount;
    function renounceRole(role : Role; callerConfirmation : address)
      raises AccessControlBadConfirmation;
  }
 
  storage is record {
    minted : map(address, uint);
  }
 
  pure function minter() : Role is
    Role(Crypto.keccak256("MINTER_ROLE"))
 
  deploy() is {
    AccessControl.grant(AccessControl.defaultAdminRole(), Evm.sender());
  }
 
  external function mint(const to : address; const amount : uint)
    raises AccessControlUnauthorizedAccount
  is {
    AccessControl.checkRole(minter());
    storage.minted[to] := storage.minted[to] + amount;
  }
}
Function
hasRole, getRoleAdmin, grantRole, revokeRole, renounceRolethe externals of OpenZeppelin's AccessControl, IAccessControl: a role's admin is the default admin role until setRoleAdmin changes it, only a role's admin grants and revokes it, and a caller renounces its own role, naming itself as the confirmation
checkRole(r), checkRoleOf(r, account)raise AccessControlUnauthorizedAccount
grant(r, account), revoke(r, account), setRoleAdmin(r, admin)unchecked, for deploy and the contract's own rules

Pausable

std/security/pausable.lithe. An emergency stop. Pausing is policy, so the contract decides who may pause, and which entrypoints check requireNotPaused. A token pauses every transfer, mint, and burn, as OpenZeppelin's ERC20Pausable and its siblings do, from its module's beforeTransfer hook: hook ERC20.beforeTransfer is Pausable.requireNotPaused;.

Function
pausedthe external, IPausable; pausing and unpausing are the contract's own entrypoints
requireNotPaused(), requirePaused()raise EnforcedPause and ExpectedPause
pause(), unpause()no access check of their own