Skip to content

Whitelist Contract

The Whitelist contract provides a robust Merkle tree-based whitelist system that supports multiple independent sections with role-based management. It implements UUPS upgradeable pattern for future improvements while maintaining strict access control.

Overview

Contract Address: contracts/src/core/Whitelist.sol
License: GPL-3.0-only
Solidity Version: 0.8.19
Pattern: UUPS Upgradeable + Role-based Access Control

Key Features

  • Merkle Tree Verification: Gas-efficient whitelist verification using cryptographic proofs
  • Multiple Sections: Independent whitelist sections for different use cases
  • Batch Operations: Efficient verification of multiple addresses in a single transaction
  • Role-based Management: Integration with Roles contract for secure permission management
  • UUPS Upgradeable: Secure upgrade mechanism with admin-only authorization
  • Comprehensive Validation: Built-in safety checks and error handling

Architecture

The contract inherits from three key components:

  • UUPSUpgradeable: Provides secure upgrade functionality
  • Initializable: Ensures proper proxy initialization
  • Role: Provides role-based access control integration

Access Control Integration

The Whitelist contract integrates with the Roles contract to implement secure permission management:

  • Admin Role: Can upgrade contract and update role helper
  • Manager Role: Can update Merkle roots for sections
  • Role Helper: External Roles contract that manages all role operations

Core Functions

Initialization

function initialize(address roleHelper) external initializer

Purpose: Initializes the proxy contract with role helper integration.

Parameters:

  • roleHelper: Address of the Roles contract for access control

Security Features:

  • One-time initialization protection via initializer modifier
  • Validates role helper implements IAccessControl interface
  • Prevents zero address assignment

Access: Can only be called once during proxy deployment

Merkle Root Management

function updateRoot(uint256 section, bytes32 newRoot) public virtual onlyManager

Purpose: Updates the Merkle root for a specific whitelist section.

Parameters:

  • section: The section ID to update (can be any uint256 value)
  • newRoot: The new Merkle tree root hash

Security Features:

  • Restricted to Manager role holders
  • Prevents zero hash assignment
  • Emits MerkleRootUpdated event for transparency

Access: Only addresses with MANAGER_ROLE

Verification Functions

Single Address Verification

function verify(uint256 section, bytes32[] calldata proof, bytes32 leaf) 
    public view returns (bool)

Purpose: Verifies a Merkle proof against a section's root.

Parameters:

  • section: The section ID to verify against
  • proof: Array of Merkle proof hashes
  • leaf: The leaf hash to verify

Returns: true if proof is valid and section exists, false otherwise

Gas Efficiency: Uses Solady's optimized MerkleProofLib.verify() function

Address Verification

function verifyAddress(uint256 section, bytes32[] calldata proof, address account) 
    public view returns (bool)

Purpose: Convenience function to verify an address is whitelisted.

Parameters:

  • section: The section ID to verify against
  • proof: Array of Merkle proof hashes
  • account: The address to verify

Returns: true if address is whitelisted in the section

Implementation: Automatically generates leaf hash using generateLeaf(account)

Batch Verification

function verifyBatch(
    uint256 section,
    bytes32[][] calldata proofs,
    address[] calldata accounts
) public view returns (bool[] memory)

Purpose: Efficiently verifies multiple addresses in a single transaction.

Parameters:

  • section: The section ID to verify against
  • proofs: Array of Merkle proof arrays (one per address)
  • accounts: Array of addresses to verify

Returns: Boolean array indicating verification result for each address

Gas Optimization: More efficient than multiple individual verification calls

Validation: Ensures proof and account arrays have matching lengths

Utility Functions

Leaf Generation

function generateLeaf(address account) public pure returns (bytes32)

Purpose: Generates a standardized leaf hash for an address.

Parameters:

  • account: The address to generate a leaf for

Returns: keccak256(abi.encodePacked(account))

Usage: Consistent leaf generation for Merkle tree construction

Section Management

function sectionExists(uint256 section) public view returns (bool)

Purpose: Checks if a section has been initialized with a Merkle root.

Parameters:

  • section: The section ID to check

Returns: true if section has a non-zero root, false otherwise

Role Helper Management

function updateRoleHelper(address newRoleHelper) public virtual onlyAdmin

Purpose: Updates the role helper contract address.

Parameters:

  • newRoleHelper: Address of the new Roles contract

Security Features:

  • Restricted to Admin role holders
  • Validates new role helper implements IAccessControl
  • Emits RoleHelperUpdated event

Access: Only addresses with ADMIN_ROLE

UUPS Upgrade Authorization

function _authorizeUpgrade(address newImplementation) internal virtual override onlyAdmin

Purpose: Authorizes contract upgrades through UUPS pattern.

Parameters:

  • newImplementation: Address of the new implementation contract

Security: Only Admin role holders can authorize upgrades

Access: Internal function called during upgradeToAndCall()

Usage Examples

Deploying the Contract

// Deploy implementation
Whitelist implementation = new Whitelist();
 
// Deploy Roles contract for access control
Roles roles = new Roles();
 
// Deploy proxy with initialization
bytes memory initData = abi.encodeWithSelector(
    Whitelist.initialize.selector,
    address(roles)
);
 
ERC1967Proxy proxy = new ERC1967Proxy(
    address(implementation),
    initData
);
 
Whitelist whitelist = Whitelist(address(proxy));

Setting Up Whitelist Sections

// Grant manager role for root updates
roles.grantRole(roles.MANAGER_ROLE(), managerAddress);
 
// Manager updates section 1 with new Merkle root
vm.prank(managerAddress);
whitelist.updateRoot(1, merkleRoot1);
 
// Manager sets up multiple sections
vm.prank(managerAddress);
whitelist.updateRoot(2, merkleRoot2);
whitelist.updateRoot(3, merkleRoot3);

Verifying Addresses

// Single address verification
bytes32[] memory proof = getMerkleProof(userAddress);
bool isWhitelisted = whitelist.verifyAddress(1, proof, userAddress);
 
// Batch verification for multiple addresses
address[] memory users = [user1, user2, user3];
bytes32[][] memory proofs = [proof1, proof2, proof3];
bool[] memory results = whitelist.verifyBatch(1, proofs, users);
 
// Check which users are whitelisted
for (uint i = 0; i < results.length; i++) {
    if (results[i]) {
        // users[i] is whitelisted
    }
}

Managing Multiple Sections

// Different sections for different purposes
uint256 EARLY_ACCESS = 1;
uint256 PUBLIC_SALE = 2;
uint256 VIP_TIER = 3;
 
// Set up different whitelists
vm.prank(manager);
whitelist.updateRoot(EARLY_ACCESS, earlyAccessRoot);
whitelist.updateRoot(PUBLIC_SALE, publicSaleRoot);
whitelist.updateRoot(VIP_TIER, vipTierRoot);
 
// Verify user for different sections
bool earlyAccess = whitelist.verifyAddress(EARLY_ACCESS, proof, user);
bool publicSale = whitelist.verifyAddress(PUBLIC_SALE, proof, user);
bool vipTier = whitelist.verifyAddress(VIP_TIER, proof, user);

Upgrading the Contract

// Deploy new implementation
Whitelist newImplementation = new Whitelist();
 
// Admin authorizes upgrade
whitelist.upgradeToAndCall(
    address(newImplementation), 
    "" // No additional initialization data
);

Merkle Tree Integration

Building Merkle Trees

The contract expects Merkle trees to be constructed with leaf hashes generated using the generateLeaf() function:

// JavaScript example for tree construction
const { MerkleTree } = require('merkletreejs');
const { keccak256 } = require('ethers/lib/utils');
 
// Generate leaves (same as contract's generateLeaf function)
const addresses = ['0x...', '0x...', '0x...'];
const leaves = addresses.map(addr => 
    keccak256(ethers.utils.solidityPack(['address'], [addr]))
);
 
// Build tree
const tree = new MerkleTree(leaves, keccak256, { sortPairs: true });
const root = tree.getRoot();
 
// Generate proof for an address
const leaf = keccak256(ethers.utils.solidityPack(['address'], [userAddress]));
const proof = tree.getProof(leaf);

Off-chain Proof Generation

// Generate proof for verification
function generateProof(merkleTree, address) {
    const leaf = keccak256(
        ethers.utils.solidityPack(['address'], [address])
    );
    return merkleTree.getProof(leaf).map(p => p.data);
}
 
// Verify on-chain
const proof = generateProof(tree, userAddress);
const isValid = await whitelist.verifyAddress(sectionId, proof, userAddress);

Security Considerations

Access Control

  1. Role Segregation: Clear separation between Admin (upgrades) and Manager (operations) roles
  2. Role Helper Validation: Ensures role helper implements proper interface
  3. Upgrade Authorization: Only admins can authorize contract upgrades

Merkle Tree Security

  1. Proof Validation: Uses battle-tested Solady MerkleProofLib for verification
  2. Section Isolation: Different sections are completely independent
  3. Zero Hash Prevention: Prevents setting empty roots that would validate any proof

Gas Optimization

  1. Batch Operations: More efficient than multiple individual calls
  2. Optimized Library: Uses Solady's gas-optimized Merkle proof verification
  3. Minimal Storage: Only stores section roots, proofs are provided by users

Common Pitfalls

  1. Proof Order: Ensure consistent proof generation (use sortPairs: true)
  2. Leaf Format: Use the contract's generateLeaf() format for consistency
  3. Section Management: Verify section exists before generating proofs

Events

MerkleRootUpdated

event MerkleRootUpdated(uint256 section, bytes32 newRoot);

Emitted: When a section's Merkle root is updated Parameters:

  • section: The section ID that was updated
  • newRoot: The new Merkle root hash

RoleHelperUpdated

event RoleHelperUpdated(address indexed oldRoleHelper, address indexed newRoleHelper);

Emitted: When the role helper contract address is updated Parameters:

  • oldRoleHelper: Previous role helper address
  • newRoleHelper: New role helper address

Error Handling

Custom Errors

The contract uses custom errors for gas-efficient error reporting:

  • OnlyAdminAuthorized(): Function requires Admin role
  • OnlyManagerAuthorized(): Function requires Manager role
  • ZeroAddress(): Invalid zero address provided
  • ZeroHash(): Invalid zero hash provided
  • InvalidRoleHelperInterface(): Role helper doesn't implement IAccessControl

Standard Errors

  • Array length mismatch: Batch operations with mismatched array lengths
  • Low-level delegate call failed: UUPS upgrade or initialization failures

Testing

The contract includes comprehensive test coverage:

Test Coverage (100%)

  • Lines: 33/33 (100%)
  • Statements: 32/32 (100%)
  • Branches: 4/4 (100%)
  • Functions: 9/9 (100%)

Test Categories

Initialization Tests

  • Proper initialization with valid role helper
  • Prevention of double initialization
  • Zero address validation
  • Invalid interface detection

Access Control Tests

  • Role-based function restrictions
  • Admin and Manager permission verification
  • Role helper integration validation

Merkle Verification Tests

  • Single address verification
  • Batch verification operations
  • Non-existent section handling
  • Array length validation

Upgrade Tests

  • UUPS upgrade authorization
  • Admin-only upgrade restrictions

Security Tests

  • Zero hash prevention
  • Gas efficiency comparisons
  • Error condition handling

Gas Considerations

Optimization Strategies

  1. Batch Operations: verifyBatch() is more gas-efficient than multiple verifyAddress() calls
  2. Optimized Library: Solady's MerkleProofLib provides gas-optimized verification
  3. Minimal Storage: Only Merkle roots are stored on-chain

Gas Estimates

  • Single verification: ~3,000-5,000 gas (depending on proof length)
  • Batch verification: ~2,000-3,000 gas per address (more efficient than individual calls)
  • Root update: ~25,000-30,000 gas
  • Contract upgrade: ~50,000-70,000 gas

Integration Examples

Token Sale Integration

contract TokenSale {
    Whitelist public immutable whitelist;
    uint256 public constant SALE_SECTION = 1;
    
    constructor(address _whitelist) {
        whitelist = Whitelist(_whitelist);
    }
    
    function purchaseTokens(bytes32[] calldata proof) external payable {
        require(
            whitelist.verifyAddress(SALE_SECTION, proof, msg.sender),
            "Not whitelisted"
        );
        
        // Token sale logic
    }
    
    function batchPurchase(
        bytes32[][] calldata proofs,
        address[] calldata buyers
    ) external {
        bool[] memory verified = whitelist.verifyBatch(
            SALE_SECTION, 
            proofs, 
            buyers
        );
        
        for (uint i = 0; i < verified.length; i++) {
            require(verified[i], "Buyer not whitelisted");
            // Process purchase for buyers[i]
        }
    }
}

Multi-tier Access Control

contract TieredAccess {
    Whitelist public immutable whitelist;
    
    uint256 public constant BASIC_TIER = 1;
    uint256 public constant PREMIUM_TIER = 2;
    uint256 public constant VIP_TIER = 3;
    
    modifier onlyTier(uint256 tier, bytes32[] calldata proof) {
        require(
            whitelist.verifyAddress(tier, proof, msg.sender),
            "Insufficient tier access"
        );
        _;
    }
    
    function basicFunction(bytes32[] calldata proof) 
        external 
        onlyTier(BASIC_TIER, proof) 
    {
        // Basic tier functionality
    }
    
    function premiumFunction(bytes32[] calldata proof) 
        external 
        onlyTier(PREMIUM_TIER, proof) 
    {
        // Premium tier functionality
    }
    
    function vipFunction(bytes32[] calldata proof) 
        external 
        onlyTier(VIP_TIER, proof) 
    {
        // VIP tier functionality
    }
}

Best Practices

Development

  1. Consistent Leaf Generation: Always use the contract's generateLeaf() function format
  2. Proof Validation: Validate proofs off-chain before submission
  3. Section Management: Use descriptive constants for section IDs
  4. Batch Operations: Prefer batch operations for multiple verifications

Security

  1. Role Management: Follow the principle of least privilege
  2. Upgrade Planning: Test upgrades thoroughly on testnets
  3. Merkle Tree Construction: Use established libraries with proper sorting
  4. Input Validation: Validate all inputs before contract interaction

Gas Optimization

  1. Batch Verification: Use verifyBatch() for multiple addresses
  2. Proof Length: Minimize Merkle tree depth when possible
  3. Section Reuse: Reuse sections when appropriate to avoid redundant storage

Future Considerations

The UUPS upgradeable pattern allows for future enhancements while maintaining state and address consistency:

  • Additional Verification Methods: Support for different proof formats
  • Enhanced Batch Operations: More efficient batch processing
  • Cross-section Operations: Functions that work across multiple sections
  • Integration Features: Built-in integration with common DeFi protocols

The contract's modular design and comprehensive testing provide a solid foundation for evolving requirements while maintaining security and efficiency.