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, renounceOwnership | the 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, hasAllRoles | the role externals of Solady's OwnableRoles, IOwnableRoles: only the owner grants and revokes, and a caller renounces its own roles |
role(ordinal : uint8) : Roles | the 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)) : Roles | the 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, renounceRole | the 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 | |
|---|---|
paused | the 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 |

