Skip to main content

Type a word from a question or an article title.

3 · Under the hood

Access-control patterns in smart contracts - owner, roles and deal parties

How contracts decide who may do what: Ownable and Ownable2Step, roles, checking deal parties, pauses and least privilege. A walkthrough using the escrow.

4 min readUpdated:
In this article

Every contract function answers the question: "who is allowed to do this?" A mistake in the answer is the most frequent cause of lost money: a withdraw function with no check, a forgotten role, a stolen owner key. Here we examine the main patterns and see how the escrow contract applies them. For context on signatures and addresses see public-key cryptography.

How a contract knows who is calling

Inside a function msg.sender is available - the address that directly called the contract. It cannot be forged: the transaction's signature is verified by the network before execution (how it works). All access control is built on comparing msg.sender with an expected address or role.

Pattern 1. A single owner (Ownable)

The simplest scheme: an address owner and functions with the onlyOwner modifier.

solidity
function setTokenAllowed(address token, bool allowed) external onlyOwner {
    allowedToken[token] = allowed;
}

The scheme's problems: a lost key means lost control, a stolen one means a takeover. Multisig and timelock help to secure it.

Pattern 2. Two-step ownership transfer (Ownable2Step)

In plain Ownable the owner is handed over in one call: a typo in the address and control is lost forever. Ownable2Step requires two steps:

  1. The current owner calls transferOwnership(newOwner).
  2. The new owner calls acceptOwnership().

Until the second step is done nothing changes. A typo is harmless because nobody at the wrong address will "accept". The escrow contract uses exactly this scheme.

Pattern 3. Roles (AccessControl)

If a contract has several administrative duties, giving all rights to one address is risky. AccessControl introduces roles - identifiers (bytes32) that can be granted and revoked:

solidity
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");

function pause() external onlyRole(PAUSER_ROLE) { ... }

The least privilege principle: to each role and each address only what it needs. A pause operator does not need the right to withdraw fees.

Pattern 4. The deal's parties

In the escrow the main authorisation is not the owner but the participants of a specific deal. Rights are determined by the deal's data:

ActionWho canHow it is checked
Accept an offerthe counterpartymsg.sender == (funded ? seller : buyer)
Confirm receiptthe buyermsg.sender == d.buyer
Open a disputebuyer or sellermsg.sender equals d.buyer or d.seller
Issue a rulingthe appointed guarantormsg.sender == d.arbiter

Errors are returned as NotAuthorized(). More about the flow in the lifecycle.

Pattern 5. State checking as part of access

Authorisation is not only "who" but also "when". Each deal function requires the right status:

solidity
_requireStatus(d, Status.Active);

You cannot confirm receipt on an offer, and cannot open a dispute on a closed deal. This is a state machine (analysis).

Pattern 6. "Anyone" functions for liveness

Sometimes the best access control is its absence. For example, after an offer's term expires anyone can call cancelOffer. Why? So the money does not depend on the parties' activity: if the buyer lost a key and the seller vanished, an outsider (including the service) can return the funds. The refund still goes only to the buyer, so the function cannot be abused.

This is a design principle: "anyone can trigger it, but the result is predetermined".

Pattern 7. Pause

Pausable lets the owner stop part of the functions during an incident. In the escrow a pause blocks only creating and accepting offers:

solidity
function createOffer(...) external payable whenNotPaused nonReentrant { ... }

Exiting an active deal always works. The owner cannot freeze anyone's money (why this matters).

What the escrow owner can and cannot do

CanCannot
Allow or forbid a token for new dealsTake the funds of active deals
Mark a guarantor "verified"Change a deal's result
Pause new dealsStop payouts
Withdraw accumulated feesTake what is reserved (reserved)
Withdraw "ownerless" tokensChange the contract's code

The guarantee rests on an invariant: the contract's balance is no less than everything it owes (reserved). Administrative functions can touch only the excess. More in the analysis of the state machine.

Typical mistakes

  • A forgotten onlyOwner on an administrative function.
  • tx.origin instead of msg.sender.
  • A public initialisation function that anyone can call (proxies).
  • renounceOwnership called by accident: the contract is left forever without management.
  • Backdoors: hidden roles in the code (audits).
  • Too-broad rights for one role.

Let us talk about attacks that bypass even correct access control: reentrancy and pull payments and the catalogue of vulnerabilities.

Mentioned in

Ready to try it?

Create your first deal in a couple of minutes. The money is locked in a smart contract and paid out by rules you can see in advance.