Permissions
Who may do what is the most important question in any contract. Language version 2 makes the answer explicit in the code and visible in the interface.
Roles
role manager
role shipper
init():
grant manager to caller
action hire_shipper(who: address) only manager:
grant shipper to who
action fire_shipper(who: address) only manager:
revoke shipper from who
action ship(id: int) only shipper:
orders[id].status = Status.Shipped
role namedeclares a set of addresses. A contract may declare several roles; a module may declare its own.grant name to <address>adds an address;revoke name from <address>removes it. Both are statements and emitRoleGranted(role, account, by)orRoleRevoked(role, account, by), so the history of permissions is in the receipts.name.has(address)returns whether the address holds the role.- A role is not a value: it cannot be assigned, passed or returned.
- Nobody holds a role until the contract grants it. Grant the first administrator in
init().
only
state owner: address
state treasurer: address
role auditor
action withdraw(to: address, amount: int) only owner, treasurer:
send(to, amount)
fn audit_log(note: text) only auditor:
emit Audited(caller, note)
only a, b, …goes at the end of the function header. The call continues ifcallerholds any listed role, or equals any listed address state variable. Otherwise it fails withrequirement failed: only owner or treasurer can call 'withdraw'and nothing changes.- It works on actions,
fnhelpers andupgrade(). It is not allowed on views (free queries have no verified caller) or oninit()(no role exists yet). - The check runs before the first statement of the function, so no work is done for an unauthorized caller.
tccl check,tccl abiand the playground list the rules next to each function, e.g.action withdraw(to: address, amount: int) only owner, treasurer.
only owner is exactly require caller == owner, "only owner can call '…'" written in a way tools can read. Version 1 contracts use the require form.
Patterns
Transferable ownership
state owner: address
state pending_owner: address
action offer_ownership(to: address) only owner:
pending_owner = to
action accept_ownership() only pending_owner:
owner = pending_owner
pending_owner = zero_address()
The two steps prevent handing the contract to a mistyped address.
Never lock yourself out. Revoking the last administrator is permanent. Keep a count if it matters:
role admin
state admins: int
action add_admin(who: address) only admin:
require not admin.has(who), "already an admin"
grant admin to who
admins += 1
action remove_admin(who: address) only admin:
require admin.has(who), "not an admin"
require admins > 1, "keep at least one admin"
revoke admin from who
admins -= 1
Roles for contracts. A role can be granted to a contract address. Inside a call from that contract, caller is the contract, so only operator works for contracts too — for example a game contract allowed to mint items.
Multiple approvals. For decisions that need M of N people, store approvals per proposal and act when the count reaches M — see treasury.tccl.
Mistakes to avoid
- Authorizing with
origin. Always usecaller(oronly).originis the signer of the transaction even when an unknown contract is in between. - Unprotected initialization. Set owners and grant roles in
init(), which runs once at deployment. Asetupaction anyone can call first is a takeover. - Forgetting that views are public.
onlydoes not hide data: everything stored on the chain can be read by anyone. - Trusting the upgrade authority blindly. An upgradeable contract's authority can replace its code, including its permission checks. See Upgrades.