Access control is a core component of smart contract security, ensuring that only authorized users can perform specific operations. Solidity provides multiple ways to implement access control.
1. Basic Access Control: Only Owner Pattern
The simplest access control pattern that only allows the contract deployer to perform sensitive operations.
soliditycontract Ownable { address public owner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); constructor() { owner = msg.sender; emit OwnershipTransferred(address(0), msg.sender); } modifier onlyOwner() { require(msg.sender == owner, "Not the owner"); _; } function transferOwnership(address newOwner) public onlyOwner { require(newOwner != address(0), "Invalid address"); emit OwnershipTransferred(owner, newOwner); owner = newOwner; } // Functions only callable by owner function withdraw() public onlyOwner { payable(owner).transfer(address(this).balance); } }
2. OpenZeppelin AccessControl: Role-Based Access Control
A more flexible and secure access control solution that supports multi-role management.
solidityimport "@openzeppelin/contracts/access/AccessControl.sol"; contract RoleBasedAccess is AccessControl { // Define roles bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); constructor() { // Deployer gets default admin role _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _grantRole(ADMIN_ROLE, msg.sender); _grantRole(MINTER_ROLE, msg.sender); } // Only MINTER_ROLE can mint function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) { // Minting logic } // Only PAUSER_ROLE can pause function pause() public onlyRole(PAUSER_ROLE) { // Pause logic } // Only ADMIN_ROLE can set parameters function setParameter(uint256 param) public onlyRole(ADMIN_ROLE) { // Set parameter logic } }
3. Multi-Signature Access Control
For high-value contracts, using multi-signature wallets for access control is more secure.
soliditycontract MultiSigControl { address[] public owners; mapping(address => bool) public isOwner; uint256 public requiredConfirmations; struct Transaction { address to; uint256 value; bytes data; bool executed; uint256 confirmations; } Transaction[] public transactions; mapping(uint256 => mapping(address => bool)) public confirmed; modifier onlyOwner() { require(isOwner[msg.sender], "Not an owner"); _; } constructor(address[] memory _owners, uint256 _required) { require(_owners.length > 0, "Owners required"); require(_required > 0 && _required <= _owners.length, "Invalid required"); for (uint i = 0; i < _owners.length; i++) { address owner = _owners[i]; require(owner != address(0), "Invalid owner"); require(!isOwner[owner], "Owner not unique"); isOwner[owner] = true; owners.push(owner); } requiredConfirmations = _required; } function submitTransaction(address _to, uint256 _value, bytes memory _data) public onlyOwner returns (uint256) { uint256 txId = transactions.length; transactions.push(Transaction({ to: _to, value: _value, data: _data, executed: false, confirmations: 0 })); return txId; } function confirmTransaction(uint256 _txId) public onlyOwner { require(_txId < transactions.length, "Invalid tx"); require(!confirmed[_txId][msg.sender], "Already confirmed"); confirmed[_txId][msg.sender] = true; transactions[_txId].confirmations++; if (transactions[_txId].confirmations >= requiredConfirmations) { executeTransaction(_txId); } } function executeTransaction(uint256 _txId) internal { Transaction storage transaction = transactions[_txId]; require(!transaction.executed, "Already executed"); transaction.executed = true; (bool success, ) = transaction.to.call{value: transaction.value}(transaction.data); require(success, "Transaction failed"); } }
4. Timelock Access Control
Add time delays to sensitive operations to give users reaction time.
soliditycontract TimelockControl { uint256 public constant DELAY = 2 days; struct PendingAction { bytes32 actionHash; uint256 executeTime; bool executed; } mapping(bytes32 => PendingAction) public pendingActions; event ActionScheduled(bytes32 indexed actionHash, uint256 executeTime); event ActionExecuted(bytes32 indexed actionHash); function scheduleAction(bytes32 actionHash) public onlyOwner { require(pendingActions[actionHash].executeTime == 0, "Already scheduled"); uint256 executeTime = block.timestamp + DELAY; pendingActions[actionHash] = PendingAction({ actionHash: actionHash, executeTime: executeTime, executed: false }); emit ActionScheduled(actionHash, executeTime); } function executeAction(bytes32 actionHash) public { PendingAction storage action = pendingActions[actionHash]; require(action.executeTime > 0, "Not scheduled"); require(block.timestamp >= action.executeTime, "Too early"); require(!action.executed, "Already executed"); action.executed = true; // Execute specific operation emit ActionExecuted(actionHash); } function cancelAction(bytes32 actionHash) public onlyOwner { require(!pendingActions[actionHash].executed, "Already executed"); delete pendingActions[actionHash]; } }
5. Token-Based Access Control
Use token holdings to control access permissions, commonly used in DAO governance.
soliditycontract TokenBasedAccess { IERC20 public governanceToken; uint256 public minTokensRequired; constructor(address _token, uint256 _minTokens) { governanceToken = IERC20(_token); minTokensRequired = _minTokens; } modifier onlyTokenHolder() { require(governanceToken.balanceOf(msg.sender) >= minTokensRequired, "Insufficient tokens"); _; } function propose(bytes memory proposal) public onlyTokenHolder { // Proposal logic } function vote(uint256 proposalId, bool support) public onlyTokenHolder { // Voting logic } }
6. Combined Access Control Pattern
Real-world projects usually require combining multiple access control patterns.
solidityimport "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/security/Pausable.sol"; contract ComprehensiveAccess is AccessControl, Pausable { bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); bytes32 public constant EMERGENCY_ROLE = keccak256("EMERGENCY_ROLE"); mapping(address => bool) public whitelist; bool public whitelistEnabled; constructor() { _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _grantRole(ADMIN_ROLE, msg.sender); _grantRole(OPERATOR_ROLE, msg.sender); _grantRole(EMERGENCY_ROLE, msg.sender); } // Whitelist check modifier onlyWhitelisted() { require(!whitelistEnabled || whitelist[msg.sender], "Not whitelisted"); _; } // Admin function: manage whitelist function addToWhitelist(address user) public onlyRole(ADMIN_ROLE) { whitelist[user] = true; } function removeFromWhitelist(address user) public onlyRole(ADMIN_ROLE) { whitelist[user] = false; } function toggleWhitelist(bool enabled) public onlyRole(ADMIN_ROLE) { whitelistEnabled = enabled; } // Operator function: daily operations function processTransaction(address user, uint256 amount) public onlyRole(OPERATOR_ROLE) onlyWhitelisted whenNotPaused { // Process transaction logic } // Emergency function: pause contract function emergencyPause() public onlyRole(EMERGENCY_ROLE) { _pause(); } function emergencyUnpause() public onlyRole(ADMIN_ROLE) { _unpause(); } // Upgrade function: admin only function upgrade(address newImplementation) public onlyRole(ADMIN_ROLE) { // Upgrade logic } }
Best Practices Summary
| Scenario | Recommended Solution | Description |
|---|---|---|
| Simple contracts | Ownable | Single owner pattern, simple and effective |
| Complex contracts | AccessControl | Multi-role management, flexible and secure |
| High-value contracts | Multi-sig + Timelock | Risk distribution, added security buffer |
| DAO governance | Token-based | Decentralized governance |
| Production environment | Combined pattern | Multiple mechanisms combined |
Security Considerations
- Never use tx.origin for permission checks:
solidity// Dangerous! modifier onlyOwner() { require(tx.origin == owner, "Not owner"); // Wrong! _; } // Correct modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _; }
- Verify addresses when transferring permissions:
solidityfunction transferOwnership(address newOwner) public onlyOwner { require(newOwner != address(0), "Invalid address"); // Must verify owner = newOwner; }
- Consider using OpenZeppelin libraries: Audited standard implementations that reduce security risks.
- Regularly audit permission settings: Check for unnecessary permissions or overly privileged accounts.
- Implement principle of least privilege: Only grant the minimum permissions needed to complete the work.