Skip to main content
Create your own

Mapping Addresses to Investor Tiers

Welcome to our eighth lesson. In our previous session, we took a deep dive into the architecture of ERC-3643, a sophisticated, professional-grade standard for tokenized securities. We saw how its modular design separates identity, compliance rules, and the token itself into a suite of interoperable smart contracts. This provides a powerful framework for on-chain regulation, but it can also be quite complex.

Today, we will take the first practical step in building our own compliance infrastructure, drawing inspiration from that professional standard. The goal of this lesson is to design a simplified on-chain identity system using a mapping of addresses to investor tiers. We will move from the theory of ERC-3643 to the practice of writing a standalone InvestorRegistry contract. This contract will serve as a single source of truth for verifying an investor's status, much like the IdentityRegistry component we analyzed previously. For someone with your background in designing data systems, you can think of this as creating a specialized, on-chain dimension table that maps a primary key—the investor's wallet address—to a critical attribute: their compliance tier.

From Whitelists to Tiered Access

The simplest form of on-chain access control is a whitelist. A contract would store a list of approved addresses and check against it before allowing an action. While straightforward, this binary "yes/no" approach often lacks the nuance required for financial assets, which may have different rules for different types of investors.

How to Implement Smart Contract Whitelisting and Tiered Access

This article from ChainScore Labs provides a clear distinction between a simple whitelist and a more granular tiered access system.

Please read the first two paragraphs under the FAQ section, starting from the definitions. This will clarify why a tiered system is more powerful and flexible for our use case.

As the reading explains, a tiered system allows us to assign different permission levels. For a security token, these tiers could represent qualifications like:

  • Tier 0: None (not verified)
  • Tier 1: Retail (verified for basic participation)
  • Tier 2: Accredited (verified as an accredited investor, eligible for specific offerings)

Implementing the Core Data Structure

To implement this tiered system, we'll use one of Solidity's most powerful features: the mapping. Instead of a mapping(address => bool) for a simple whitelist, we will use a mapping that links an address to a number representing their tier.

How to Implement Smart Contract Whitelisting and Tiered Access

This guide demonstrates the practical implementation of tiered access using mappings.

First, read the paragraph under "Implementing Tiered Access with Mappings" which introduces the userTier mapping. Then, review the next section which shows the same pattern in a slightly different context, again using a mapping for user tiers.

We can make this even more readable and robust by using an enum, a feature you've encountered before. An enum restricts a variable to one of a few predefined values, preventing errors from using arbitrary numbers for tiers. We will also use uint8 for the tier instead of the default uint256, since we only have a few tiers. This is a gas optimization best practice.

Here is the basic structure of our InvestorRegistry contract:

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

contract InvestorRegistry {
    // Defines the possible investor tiers
    enum Tier { None, Retail, Accredited }

    // Maps an investor's address to their assigned tier
    mapping(address => Tier) public investorTiers;

    // We will add management and query functions here
}

This simple structure forms the heart of our on-chain identity system. It's a clear, efficient way to store the compliance status of every potential investor.

Managing the Registry: Access Control

This registry is useless if we can't securely update it. We need functions to add, modify, and remove investors, and these functions must be protected. Only an authorized administrator, like a compliance officer, should be able to change an investor's tier.

While a simple onlyOwner pattern works, a more flexible approach is Role-Based Access Control (RBAC). This allows you to define specific roles (e.g., a COMPLIANCE_ROLE) and assign them to one or more addresses. This is much better for team-based management and aligns with how enterprise systems operate.

We will implement a simplified RBAC system from scratch to manage our registry. The following video demonstrates how to build the necessary components: a mapping to store roles, functions to grant and revoke them, and a modifier to protect functions.

Access Control | Solidity 0.8

This video from Smart Contract Programmer provides a clear, step-by-step guide to building an access control contract from the ground up. We will adapt this pattern for our registry.

Please watch the following segments: Data Structure: The mapping used to store roles. Defining Roles: How to define roles as bytes32 constants. Granting Roles: The grantRole function for assigning roles. Constructor Setup: Granting the initial admin role to the contract deployer. Revoking Roles: The revokeRole function for removing access.

Now, let's integrate this RBAC pattern into our InvestorRegistry to protect its administrative functions.

Here is the complete InvestorRegistry.sol contract. It includes:

  1. The Tier enum and investorTiers mapping.
  2. A COMPLIANCE_ROLE and the mappings and functions for our RBAC system.
  3. A constructor that assigns the COMPLIANCE_ROLE to the contract deployer.
  4. An onlyRole modifier to protect sensitive functions.
  5. A setInvestorTier function, protected by the modifier, to manage investor status.
  6. Events that log all administrative changes.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract InvestorRegistry {
    // 1. Core Data Structure
    enum Tier { None, Retail, Accredited }
    mapping(address => Tier) public investorTiers;

    // 2. Role-Based Access Control (RBAC)
    bytes32 public constant COMPLIANCE_ROLE = keccak256("COMPLIANCE_ROLE");
    mapping(bytes32 => mapping(address => bool)) private _roles;

    // 6. Events
    event RoleGranted(bytes32 indexed role, address indexed account);
    event RoleRevoked(bytes32 indexed role, address indexed account);
    event InvestorTierSet(address indexed investor, Tier indexed tier);

    // 3. Constructor
    constructor() {
        _grantRole(COMPLIANCE_ROLE, msg.sender);
    }

    // 4. Modifier
    modifier onlyRole(bytes32 role) {
        require(hasRole(role, msg.sender), "Caller does not have required role");
        _;
    }

    // --- RBAC Management Functions ---
    function hasRole(bytes32 role, address account) public view returns (bool) {
        return _roles[role][account];
    }

    function grantRole(bytes32 role, address account) public onlyRole(COMPLIANCE_ROLE) {
        _grantRole(role, account);
    }

    function revokeRole(bytes32 role, address account) public onlyRole(COMPLIANCE_ROLE) {
        _revokeRole(role, account);
    }

    function _grantRole(bytes32 role, address account) internal {
        _roles[role][account] = true;
        emit RoleGranted(role, account);
    }

    function _revokeRole(bytes32 role, address account) internal {
        _roles[role][account] = false;
        emit RoleRevoked(role, account);
    }

    // 5. Investor Tier Management Function
    function setInvestorTier(address investor, Tier tier) public onlyRole(COMPLIANCE_ROLE) {
        investorTiers[investor] = tier;
        emit InvestorTierSet(investor, tier);
    }
}

This contract is now a fully functional, standalone identity registry. It securely stores investor tiers and ensures that only authorized compliance officers can make changes.

Designing the Public Interface

The final piece of the design is to create functions that allow other contracts to read the data. This is the primary purpose of our registry: to act as a queryable, on-chain database for compliance information.

We need a way for a security token contract to ask our InvestorRegistry, "Is this address allowed to receive tokens?" or "What is this investor's status?"

We can add a simple view function to our contract for this purpose.

// Add this function inside the InvestorRegistry contract

function isVerified(address investor) external view returns (bool) {
    return investorTiers[investor] != Tier.None;
}

This function, isVerified, provides a simple boolean response that other contracts can easily use. For example, a security token could call this function before every transfer to ensure the recipient is on the allowlist. This mirrors the modular isVerified check from the ERC-3643 standard we studied.

This diagram illustrates the architectural pattern we've just built. Our `InvestorRegistry` acts as a central "Registry Contract." Other contracts, like a security token, can be pointed to its address to query investor information before executing transactions.

Conclusion

In this lesson, we transitioned from the complex theory of ERC-3643 to the practical design of a simplified, standalone identity system. By focusing on the core requirements, we've built a robust and reusable component that forms the foundation of on-chain compliance.

Key Takeaways:

  • Tiered access is more flexible than a simple whitelist. Using an enum and a mapping(address => Tier) allows us to represent granular investor qualifications on-chain.
  • Role-Based Access Control (RBAC) is essential for managing a registry. By creating a COMPLIANCE_ROLE and protecting administrative functions, we ensure that only authorized parties can update investor data.
  • Modular design is a powerful pattern. By building the InvestorRegistry as a standalone contract, we create a reusable component that many different security tokens can reference, just like the systems in your investment management software reference a central data warehouse.
  • A clean public interface is crucial. Simple view functions like getInvestorTier (which Solidity creates for us automatically) and our custom isVerified allow other smart contracts to easily and efficiently query the registry.

You now have a solid, practical understanding of how to build a core piece of compliance infrastructure. In the next lesson, we will focus on the other side of this interaction: we will learn how a separate security token contract can securely communicate with our registry by defining and using a smart contract interface.

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

Sign up