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 initializerPurpose: 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
initializermodifier - Validates role helper implements
IAccessControlinterface - Prevents zero address assignment
Access: Can only be called once during proxy deployment
Merkle Root Management
function updateRoot(uint256 section, bytes32 newRoot) public virtual onlyManagerPurpose: 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
MerkleRootUpdatedevent 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 againstproof: Array of Merkle proof hashesleaf: 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 againstproof: Array of Merkle proof hashesaccount: 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 againstproofs: 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 onlyAdminPurpose: 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
RoleHelperUpdatedevent
Access: Only addresses with ADMIN_ROLE
UUPS Upgrade Authorization
function _authorizeUpgrade(address newImplementation) internal virtual override onlyAdminPurpose: 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
- Role Segregation: Clear separation between Admin (upgrades) and Manager (operations) roles
- Role Helper Validation: Ensures role helper implements proper interface
- Upgrade Authorization: Only admins can authorize contract upgrades
Merkle Tree Security
- Proof Validation: Uses battle-tested Solady MerkleProofLib for verification
- Section Isolation: Different sections are completely independent
- Zero Hash Prevention: Prevents setting empty roots that would validate any proof
Gas Optimization
- Batch Operations: More efficient than multiple individual calls
- Optimized Library: Uses Solady's gas-optimized Merkle proof verification
- Minimal Storage: Only stores section roots, proofs are provided by users
Common Pitfalls
- Proof Order: Ensure consistent proof generation (use
sortPairs: true) - Leaf Format: Use the contract's
generateLeaf()format for consistency - 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 updatednewRoot: 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 addressnewRoleHelper: 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
- Batch Operations:
verifyBatch()is more gas-efficient than multipleverifyAddress()calls - Optimized Library: Solady's MerkleProofLib provides gas-optimized verification
- 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
- Consistent Leaf Generation: Always use the contract's
generateLeaf()function format - Proof Validation: Validate proofs off-chain before submission
- Section Management: Use descriptive constants for section IDs
- Batch Operations: Prefer batch operations for multiple verifications
Security
- Role Management: Follow the principle of least privilege
- Upgrade Planning: Test upgrades thoroughly on testnets
- Merkle Tree Construction: Use established libraries with proper sorting
- Input Validation: Validate all inputs before contract interaction
Gas Optimization
- Batch Verification: Use
verifyBatch()for multiple addresses - Proof Length: Minimize Merkle tree depth when possible
- 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.