Skip to main content
Create your own

Implementing an On-Chain Allowlist

Welcome back! In our previous lesson, we designed the "blueprint" for our compliance system by creating the IInvestorRegistry interface. This defined what functions our security token would expect from an identity contract, establishing a clear API for communication.

Today, we transition from blueprint to construction. The goal of this lesson is to implement a basic, on-chain allowlist as a simplified identity registry. We will build the first version of our InvestorRegistry contract, bringing the interface we designed to life. For someone with your background in data systems, this is like moving from defining a table schema to actually creating the table and writing the stored procedures to manage its data.

The On-Chain Allowlist: A Core Compliance Tool

Before we write any code, let's solidify the concept. In the world of regulated assets, not just anyone can hold or trade a security. An "allowlist" (often called a whitelist) is a fundamental on-chain mechanism to enforce these rules. It's a list of addresses that have been pre-approved—perhaps after an off-chain KYC/AML check—to interact with the token.

This pattern is a cornerstone of compliance for Real World Assets (RWAs) on the blockchain. To understand its importance, please read the introductory section of the following guide.

How to Implement Smart Contract Whitelisting and Tiered Access

This guide from ChainScore Labs provides an excellent overview of on-chain access control for RWAs, which is directly relevant to our security token.

Please read the section titled Introduction to On-Chain Access Control. Focus on why smart contract-based controls are superior to off-chain methods for enforcing rules like KYC/AML compliance.

As the article highlights, an on-chain allowlist provides transparent and autonomous enforcement, which is crucial for building trust in a tokenized security.

Choosing the Right Data Structure

Our InvestorRegistry needs to store a list of approved addresses and efficiently answer the question: "Is this address verified?" In your work with data warehouses, you know that the choice of data structure is critical for performance. The same is true in Solidity, but here the "performance" cost is measured in gas.

The most common and gas-efficient data structure for an allowlist is a mapping. Specifically, we will use a mapping(address => bool). This acts like a hash table or a dictionary, directly mapping an investor's address to a boolean value (true if they are verified, false otherwise). Looking up an address in a mapping is an extremely fast (and cheap) operation, regardless of how many investors are on the list.

The guide we just looked at has a section that compares different data structures. Let's examine its take on using a mapping.

How to Implement Smart Contract Whitelisting and Tiered Access

This part of the guide explains the trade-offs between different data structures for implementing whitelists.

Find the section Choosing a Whitelist Data Structure and read the paragraph that begins For basic, non-iterable whitelists. This explains why a mapping(address => bool) is the ideal choice for our current needs.

Implementing the InvestorRegistry

Now we have all the pieces: the interface from the last lesson, the concept of an allowlist, and the right data structure. Let's build the InvestorRegistry.sol contract.

Our contract needs to do three main things:

  1. Implement the IInvestorRegistry interface.
  2. Provide a secure way for an administrator to add or remove investors from the allowlist.
  3. Store the allowlist data efficiently.

To handle administrative access, we'll use a simple and effective pattern: designate the address that deploys the contract as the "owner" and restrict sensitive functions to that owner. This is done with a custom modifier.

The following video provides a clear walkthrough of creating this exact access control pattern from scratch.

Access Control with Solidity & OpenZeppelin | Authorization, RBAC (Role Based Access Control)

This video from EatTheBlocks demonstrates how to implement basic access control by creating an admin and an onlyAdmin modifier.

Please watch the section from this demonstration. Pay close attention to how the admin address is set in the constructor and how the onlyAdmin modifier uses a require statement to protect a function.

With that pattern in mind, here is the complete code for our InvestorRegistry.sol. It combines the mapping, the onlyOwner modifier, and the functions required by our IInvestorRegistry interface.

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

// First, we import the interface we created in the previous lesson.
import "./IInvestorRegistry.sol";

/**
 * @title InvestorRegistry
 * @dev A basic on-chain allowlist implementation for managing verified investors.
 * This contract implements the IInvestorRegistry interface.
 */
contract InvestorRegistry is IInvestorRegistry {
    // The address of the contract owner, who can manage the allowlist.
    address public owner;

    // Mapping from an investor's address to their verification status.
    mapping(address => bool) private _isVerified;

    // Events to log administrative actions.
    event InvestorAdded(address indexed investor);
    event InvestorRemoved(address indexed investor);

    /**
     * @dev Modifier to restrict function access to the contract owner.
     */
    modifier onlyOwner() {
        require(msg.sender == owner, "InvestorRegistry: Caller is not the owner");
        _;
    }

    /**
     * @dev Sets the contract deployer as the initial owner.
     */
    constructor() {
        owner = msg.sender;
    }

    /**
     * @notice Adds an investor to the allowlist. Can only be called by the owner.
     * @param investor The address of the investor to add.
     */
    function addInvestor(address investor) external onlyOwner {
        require(investor != address(0), "InvestorRegistry: Invalid address");
        _isVerified[investor] = true;
        emit InvestorAdded(investor);
    }

    /**
     * @notice Removes an investor from the allowlist. Can only be called by the owner.
     * @param investor The address of the investor to remove.
     */
    function removeInvestor(address investor) external onlyOwner {
        require(investor != address(0), "InvestorRegistry: Invalid address");
        _isVerified[investor] = false;
        emit InvestorRemoved(investor);
    }

    // --- Implementation of IInvestorRegistry functions ---

    /**
     * @notice Checks if an investor's address is on the allowlist and verified.
     * @param investor The address of the investor to check.
     * @return bool True if the investor is verified, false otherwise.
     */
    function isVerified(address investor) external view override returns (bool) {
        return _isVerified[investor];
    }

    /**
     * @notice Retrieves the compliance tier of a given investor.
     * @dev For this basic implementation, any verified user is considered Tier 1.
     * @param investor The address of the investor.
     * @return uint8 The numerical tier (0 for non-verified, 1 for verified).
     */
    function getInvestorTier(address investor) external view override returns (uint8) {
        return _isVerified[investor] ? 1 : 0;
    }
}

Notice a few key details:

  • We import and then implement the IInvestorRegistry interface. The override keyword is used on the functions we implement from the interface, which is a good practice enforced by the Solidity compiler.
  • The onlyOwner modifier is applied to addInvestor and removeInvestor, ensuring only the administrator can change the allowlist.
  • The isVerified function is a simple, gas-efficient lookup in our _isVerified mapping.
  • For getInvestorTier, we've implemented the simplest possible logic that satisfies the interface: if you're verified, you are "Tier 1", otherwise you're "Tier 0". In a more advanced system, this would likely read from a separate mapping(address => uint8) storing different tier levels.

The Registry Pattern

The contract we just built is an example of a common and powerful architectural concept in smart contract development: the Registry Pattern.

A registry is a contract that acts as an on-chain database or directory, providing a single source of truth that other contracts can query. In our case, the InvestorRegistry is a directory of verified identities.

This diagram illustrates the general Registry Pattern. A central `Registry Contract` stores mappings (e.g., name-to-address, or in our case, address-to-verified-status). Other contracts on the blockchain can then query this registry to get the information they need, decoupling the data from the logic.

This pattern is highly valuable because it separates data from business logic. Our SecurityToken doesn't need to manage the allowlist itself; it just needs to know the address of a registry that it can trust and query. This makes the system more modular, maintainable, and upgradeable.

The full ERC-3643 standard, which we've discussed, is built around this modular, registry-based architecture.

This diagram shows the architecture of the ERC-3643 standard. Our `InvestorRegistry` is a simplified version of the `IDENTITY REGISTRY` component, which works alongside other registries for compliance and claims.

Conclusion

In this lesson, we successfully built a concrete implementation of our IInvestorRegistry interface. We created a simple but secure on-chain allowlist, a critical component for any compliant tokenized asset.

Key Takeaways:

  • An on-chain allowlist is a fundamental pattern for enforcing compliance rules like KYC/AML.
  • The mapping(address => bool) data structure is the most gas-efficient choice for storing a simple, non-iterable allowlist.
  • Access to administrative functions (like adding/removing investors) can be secured using an onlyOwner modifier, a core access control pattern.
  • Our InvestorRegistry is an example of the Registry Pattern, which decouples data storage from business logic, leading to a more modular and robust system.

We now have the core components ready: a token standard (ERC-20), an identity interface (IInvestorRegistry), and a working identity registry (InvestorRegistry). In the next lesson, we will begin the main event: building the SecurityToken contract itself, integrating these components to create a token with built-in compliance checks.

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

Sign up