Skip to main content
Create your own

ERC-20 Security Tokens with AccessControl

Welcome back. In our last lesson, we constructed the InvestorRegistry, a concrete on-chain allowlist that acts as a single source of truth for investor verification. This registry is a crucial piece of our compliance infrastructure, but it's just one part of the puzzle.

Today, we shift our focus to the asset itself. This lesson's goal is to build a security token contract that inherits from ERC-20 and uses AccessControl. We will construct the foundational SecurityToken contract, which will serve as the core of our tokenized asset. Think of this as laying the keel of a ship: we are creating the fundamental structure upon which all other functionalities—compliance, governance, and financial operations—will be built.

We will leverage two powerful, industry-standard components from OpenZeppelin:

  1. ERC-20: To provide the standard interface for a fungible token (e.g., transfer, balanceOf).
  2. AccessControl: To create a granular system of roles and permissions, which is essential for managing a regulated asset.

The ERC-20 Foundation

First, any fungible token on an EVM-compatible chain needs to conform to the ERC-20 standard. This ensures it can be held in wallets, traded on exchanges, and integrated into the broader DeFi ecosystem. Instead of implementing the standard from scratch, we will build upon OpenZeppelin's battle-tested ERC20 contract.

Tools like the OpenZeppelin Contracts Wizard make it incredibly easy to generate the boilerplate for a standard token, allowing developers to focus on custom business logic.

The OpenZeppelin Contracts Wizard provides an interactive interface for generating standard smart contracts, such as this basic ERC-20 token. It helps scaffold the contract with a name, symbol, and optional features.

Our SecurityToken will inherit directly from OpenZeppelin's ERC20 contract, instantly giving it all the necessary functions and properties of a standard token.

Why AccessControl is Essential for Security Tokens

In the InvestorRegistry we built, we used a simple onlyOwner modifier for access control. This works when there's a single administrative entity. However, a security token mirrors complex real-world financial instruments and requires a more sophisticated permissions model. Different parties have different rights:

  • An issuer might have the right to mint new tokens.
  • A compliance officer might have the right to freeze accounts or force transfers.
  • A treasurer might have the right to deposit dividends.

A single "owner" cannot effectively and securely manage these distinct responsibilities. This is where role-based access control becomes critical. The following video from OpenZeppelin excellently contrasts the simple Ownable pattern with the more flexible AccessControl contract.

Setting Up Access Control for Smart Contracts

This official OpenZeppelin video explains the core access control patterns they provide. It's ideal for understanding why a simple "owner" model is insufficient for sophisticated systems like our security token.

Please watch the segment on granular access control. The speaker uses a stablecoin example to illustrate how different functions (e.g., setting fees vs. emergency shutdown) require different actors and permissions, making a single-owner model unsuitable. This logic applies directly to the needs of our security token.

As the video demonstrates, segregating duties into distinct roles is a fundamental security practice. OpenZeppelin's AccessControl contract provides a standard and secure way to implement this pattern.

Implementing Role-Based Access

The AccessControl contract allows us to define specific roles and protect functions so that only addresses assigned to a particular role can execute them. Let's see how this is implemented in practice.

The core mechanics involve:

  1. Inheriting from the AccessControl contract.
  2. Defining a unique bytes32 identifier for each role.
  3. Protecting functions with the onlyRole modifier.
  4. Granting roles to specific addresses, typically during contract deployment.

The following resource provides a clear and concise code example of this exact process.

The ERC20 Token

This guide offers a straightforward example of integrating OpenZeppelin's AccessControl into an ERC-20 token.

Please read the section on Access Control and Roles. Following that, carefully review the first code example, which combines ERC20 and AccessControl to protect a mint function with a newly defined MINTER_ROLE.

The Initial SecurityToken.sol Contract

Now, let's combine these concepts to create the first version of our SecurityToken.sol. This contract will inherit from both ERC20 and AccessControl. We'll define a MINTER_ROLE and ensure that only addresses with this role can create new tokens. In the constructor, we will assign the deployer of the contract both the MINTER_ROLE and the DEFAULT_ADMIN_ROLE. The DEFAULT_ADMIN_ROLE is a special role within AccessControl that has the power to grant and revoke other roles.

Here is the initial implementation:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";

// We will import and use this interface in the next lesson.
// import "./IInvestorRegistry.sol";

/**
 * @title SecurityToken
 * @dev An ERC-20 token with role-based access control for security token features.
 */
contract SecurityToken is ERC20, AccessControl {
    // Role identifier for accounts that are allowed to mint new tokens.
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    
    // We will define more roles later for compliance and other functions.

    // This state variable will hold the address of our investor registry.
    // We will initialize and use it in the next lesson.
    // address public investorRegistry;

    /**
     * @dev Sets up the token name, symbol, and initial roles.
     * @param name The name of the token.
     * @param symbol The symbol of the token.
     */
    constructor(
        string memory name,
        string memory symbol
    ) ERC20(name, symbol) {
        // Grant the contract deployer the default admin role, which allows
        // it to grant and revoke other roles.
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        
        // Grant the minter role to the deployer.
        _grantRole(MINTER_ROLE, msg.sender);
    }

    /**
     * @notice Creates `amount` new tokens and assigns them to `to`.
     * @dev Can only be called by an account with the MINTER_ROLE.
     * @param to The address that will receive the minted tokens.
     * @param amount The amount of tokens to mint.
     */
    function mint(address to, uint256 amount) public virtual onlyRole(MINTER_ROLE) {
        _mint(to, amount);
    }

    // In the next lesson, we will override the `_beforeTokenTransfer` hook here
    // to add our compliance checks.
}

Notice the commented-out lines. They are placeholders for the InvestorRegistry integration, which is the logical next step. This prepares our contract structure for the compliance logic we'll add soon.

The Broader Context: Security Token Standards

While we are building our compliance logic on top of the generic ERC-20 standard, it's worth noting that the ecosystem is developing specialized standards for security tokens. One such example is ERC-1404, the "Simple Restricted Token Standard".

ERC-1404 is an open standard designed specifically for security tokens. It extends ERC-20 with functions to detect transfer restrictions and provide human-readable reasons for failed transfers, which is crucial for compliance.

ERC-1404 and more comprehensive standards like ERC-3643 (T-REX) show that the problems we are solving—transfer restrictions, identity management, and granular permissions—are central to the future of tokenized securities. Our hands-on approach of building these features from first principles provides a deep understanding of how these standards work under the hood.

Conclusion

In this lesson, we successfully constructed the skeleton of our SecurityToken. We established a solid foundation by inheriting from OpenZeppelin's standard ERC20 contract and integrated the AccessControl contract to create a flexible, role-based permission system essential for a regulated asset.

Key Takeaways:

  • Security tokens are built on the ERC-20 standard but require significant additional functionality for compliance and governance.
  • OpenZeppelin's AccessControl contract is the industry standard for implementing granular, role-based permissions, which is a superior model to a simple Ownable pattern for complex applications.
  • We defined a MINTER_ROLE and protected the mint function, establishing our first administrative capability.
  • The deployer of the contract was granted the DEFAULT_ADMIN_ROLE, giving it the authority to manage other roles, and the initial MINTER_ROLE.

We now have a token contract with a robust administrative framework. In our next lesson, we will bring our system to life by connecting this token to the InvestorRegistry. We'll override the _beforeTokenTransfer hook to enforce our on-chain allowlist, ensuring that only verified investors can send or receive the token.

Can't find a good explanation? Sign up and we'll make it for you

Sign up