Smart Contract Security Guidelines #2: Strategies for Secure Access Controls

This workshop was recorded on August 15th 2021 and led by Martin Abbatemarco, Security Researcher at OpenZeppelin. It is the second workshop of a series that covers multiple topics around secure development of smart contracts.

In this workshop, we covered the following:

  • The importance of access controls in an adversarial environment
  • Reliable patterns for ownership, role-based schemes, multi-sigs and governance systems
  • The role of timelocks and administrators for emergency response
  • Must-have and nice-to-have considerations for secure access controls

You can watch the video, view the slides, and learn about strategies to consider when designing access controls in your system. The workshop focuses on showcasing specific battle-tested practices with real examples, which should allow projects to implement different types and levels of authorization mechanisms in the journey of building secure decentralized applications.


Access controls in an adversarial environment

Smart contracts are public, anyone can see and call the code. So it becomes fundamental that contracts implement secure and reliable access controls. These controls must be robust enough to prevent unauthorized accounts from executing sensitive functions, while at the same time, flexible enough to mutate and evolve as new features are added. With this in mind, in this workshop we cover multiple strategies to implement access controls securely from classic ownership patterns to modern governance implementations.

As a primer for the workshop, we use the following code example to learn what kindz of questions and considerations should be taken into account when reviewing a contract that implements a simple access control scheme.

Some questions you should be asking:

  • Does this code snippet compile?
  • What Solidity version should we use? Could there be unsafe arithmetic operations based on the version?
  • There are no docstrings. What is this contract supposed to do in the first place?
  • What does the “Ownable” contract look like? Can we safely assume it behaves as expected?
  • No visibility on state variables? No events? No error messages? To what extent would this hinder off-chain monitoring, debugging and testing?
  • There’s a low-level call, shouldn’t it be checking the returned value?
  • Can the owner authorize itself? Would this open rug-pulling scenarios?
  • Who controls the privileged account? A single private key? Multiple keys with a multisig? A governance module?
  • Does the privileged account hold any other role in the system? If so, could that introduce any risk worth analyzing?
  • How are the private keys stored? Are there backups? Who’s got access to them? Are access operations logged, monitored and audited?
  • How are the private keys generated? Do they sign transactions in standard ways or is the system using some custom implementation?

Many of these questions are well beyond the scope of a code review. Still, they can serve as triggers for development teams to build a more comprehensive threat model that considers other components aside from smart contracts.


Ownership

Years after it was first implemented, the “ownable pattern” still plays an important role. While the de-facto standard implementation is the Ownable contract available in OpenZeppelin Contracts, there are other similar implementations of this pattern (see for example the DSAuth contract by Dapphub). Overall, it is a reliable ownership strategy used across a huge variety of contracts. 

A few simple examples:

Ownership can be assigned to a simple EOA, multiple EOAs, multisig accounts, or more complex modules involving timelocks and governance (covered later in the workshop). The private keys for these accounts can be held in wallets such as MetaMask, hardware wallets, or more advanced setups using secure vaults with Defender Relay.

If multisigs are used, they can be managed through a user-friendly UI with Defender Admin.

Role-based access controls

Roles allow for a variety of sensitive actions and levels of authorization. Moreover, make it easier to reason about the system and its mechanics while favoring the readability of contracts. The number and privileges of roles will heavily depend on each particular application and the teams behind them.

As a quick exercise, let’s consider a mintable token that can be paused and upgraded. In this scenario, we could think of different strategies to manage these sensitive actions. For example:

  1. A single owner that can do everything.
  2. Two roles. One for a team in charge of token operations that can mint and pause, and the other one for the team in charge of token upgrades that can.
  3. Three roles. One role for the accounting team, in charge of minting. Another role for the security team, in charge of pausing. And the third role for a community-controlled module, in charge of upgrades.

Regardless of the final choice, these roles can be simply implemented using the AccessControl contract available in OpenZeppelin Contracts. Out of the box, this contract can provide functionality to restrict actions to specific roles, manage them, and monitor their actions with events.

Building secure contracts with role-based access controls might seem daunting at first. Luckily, you can simply use Contracts Wizard to include multiple features without having to write a single line of Solidity code.

One clear example of roles-based access controls being used in mainnet is the Compound Protocol. In the following code snippet you can see how its Comptroller contract implements multiple roles for different operations.

A final consideration when introducing roles in your system: too many roles might be difficult to manage and coordinate actions, yet too few roles can be dangerously powerful.

Timelocks and their use cases

Timelocks are a common mechanism for allowing time-delayed opt-out changes in a system. Thanks to timelocks, users can have time to react to sensitive changes that may significantly affect them. Therefore, you’ll usually find timelocks sitting between a module with powers to execute sensitive actions, and the contract where those actions take place.

For a reliable implementation of a timelock, you can checkout the TimelockController contract available in OpenZeppelin Contracts. As seen in the diagram below, it implements a role-based access control scheme, with additional features like an adjustable delay.

Other popular timelocks are the Timelock contract used in the Compound protocol, the Timelock contract used in Uniswap, and the DSPause contract used in the Maker protocol.

A final remark for timelocks: they can sometimes be implemented along other modules that are allowed to bypass delays. This strategy is often used in scenarios where privileged accounts have special powers to trigger emergency actions, like instantly pausing a contract in response to a security incident.

Governance

Smart-contract-based governance modules are becoming increasingly common in decentralized applications. In simple terms, they can be understood as a set of contracts given access powers to execute sensitive actions on others. Users can propose and vote for different actions, which can only be executed if enough votes in favor are submitted. Usually there’s some kind of governance token involved to represent the voting power of participants. Examples of this kind of token are UNI, COMP, MKR, and many others.

The GovernorAlpha is one of the most well-known examples of governance contracts, popularized by the Compound protocol. A simplified version of the GovernorAlpha contract can be found implemented in Uniswap. It can easily be wired to a timelock contract in which the original version includes a “guardian” role with powers to cancel proposals, transfer rights, and abdicate.

The guardian’s powers can be easily detected reading through the contract’s source code, as seen below.

Recently, the GovernorAlpha contract evolved into a more sophisticated version: the GovernorBravo. Because it is upgradeable, the module’s logic is split between the GovernorBravoDelegator and GovernorBravoDelegate contracts.

The GovernorBravo contract has an “admin” role, which contains the power to upgrade and change sensitive parameters of the governance system. Interestingly, the strategy implemented by the Compound protocol consists of granting this role to the Timelock contract itself. This means that changes to the governance module are timelocked.

Some additional considerations when implementing governance mechanisms in access controls schemes:

  • Guardians (privileged accounts with special powers) can help in at least two ways:
    • Push for proposals if there’s no active community participation. 
    • Stop proposals if there’s malicious intent.
  • Delegation mechanisms of voting power may also allow interested actors to participate on behalf of others that may not be always interested or available.
  • If governance has full control over a system, and there are no privileged accounts involved, setting up off-chain validations, checklists and documentation on procedures becomes crucial to ensure the system is always modified to follow standard procedures.

Finally, as mentioned before, it is possible governance tokens are involved to represent voting power. Therefore, you should mitigate attack vectors that attempt to subvert access controls leveraging flash-loans of these tokens. One way to achieve this is by not measuring voting power at the current block. You can see this in action in the governance contracts mentioned before. In the snippets below, we see the function “getPriorVotes” of the tokens being used, which allows the caller to query the balances in previous blocks.

If you’re searching for a token with these capabilities, look no longer! Use the ERC20Votes token contract available in OpenZeppelin Contracts.


Closing thoughts

  • Keep in mind the importance of having consistent and reliable access controls.
  • The progressive decentralization approach can help balance compromises between fast iterations and security.
  • It is fundamental to document all roles in a system, clearly stating their powers. You want to be transparent with your community.
  • Take advantage of battle-tested and secure building blocks available in the ecosystem when devising your access control strategy.
  • Many seasoned developers have already gone through the challenges you’re trying to overcome. Learn from them!

Additional Resources

The Ethereum security community is quite active and collects many of these behaviors in different checklists and repositories. These are some of them:


Video

Slides

To see the slides from the webinar click here.

Learn more

Join the next installment of the security series on August 26th!

Learn more about OpenZeppelin Contracts and sign up for a free Defender account.

See the documentation: 

OpenZeppelin Defender Documentation

OpenZeppelin Contracts Documentation


Be part of the community