# Access Control Vulnerabilities — Protect Your Admin Functions
Smart contracts में access control सबसे fundamental security mechanisms में से एक है। Improper access control से unauthorized users critical functions को execute कर सकते हैं — tokens mint कर सकते हैं, funds withdraw कर सकते हैं, या contracts को pause कर सकते हैं।
DeFi में कई major hacks खराब access control की वजह से हुए हैं। इस guide में, हम common vulnerabilities, best practices, और OpenZeppelin के battle-tested patterns सीखेंगे।
Access Control क्या है?
Access control यह determine करता है कि कौन कौन से functions को call कर सकता है और कब। यह typically involve करता है:
- Authentication — caller की identity verify करना
- Authorization — यह check करना कि caller को action perform करने की permission है या नहीं
- Role management — different levels of access define करना
Common Vulnerabilities
1. Missing Access Control
सबसे basic mistake — critical functions पर कोई protection नहीं।
// VULNERABLE: Anyone can mint tokens!
contract VulnerableToken {
mapping(address => uint256) public balances;
uint256 public totalSupply;
function mint(address to, uint256 amount) external {
balances[to] += amount;
totalSupply += amount;
}
}
Attack: कोई भी unlimited tokens mint कर सकता है।
2. Weak tx.origin Check
tx.origin को authorization के लिए use करना dangerous है।
// VULNERABLE: tx.origin is not reliable
contract WeakAuth {
address public owner;
constructor() {
owner = msg.sender;
}
function withdraw() external {
require(tx.origin == owner, "Not owner");
payable(owner).transfer(address(this).balance);
}
}
Attack: Malicious contract owner को trick कर सकता है कि वह उसके through withdraw() call करे — tx.origin अभी भी owner होगा, लेकिन attacker funds receive करेगा।
Fix: tx.origin के बजाय msg.sender use करें।
3. Uninitialized Owner
Constructor में owner को set करना भूल जाना।
// VULNERABLE: Owner never set
contract UninitializedOwner {
address public owner;
function setOwner(address newOwner) external {
require(msg.sender == owner, "Not owner");
owner = newOwner;
}
}
owner address(0) है, तो कोई भी पहली बार setOwner() call कर सकता है और control ले सकता है।
4. Front-Running Owner Changes
Owner changes predictable हैं और front-run किए जा सकते हैं।
// VULNERABLE: Single-step ownership transfer
function transferOwnership(address newOwner) external {
require(msg.sender == owner);
owner = newOwner;
}
अगर owner address गलत है, तो contract permanently locked हो सकता है।
5. Incorrect Modifier Logic
Custom modifiers में logic errors।
// VULNERABLE: Logic error in modifier
modifier onlyOwner() {
require(msg.sender != owner, "Not owner"); // Wrong operator!
_;
}
!= को == होना चाहिए। यह modifier किसी को भी allow करता है owner को छोड़कर!
OpenZeppelin का Ownable Pattern
सबसे simple और widely-used access control pattern।
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyToken is Ownable {
mapping(address => uint256) public balances;
// Only owner can mint
function mint(address to, uint256 amount) external onlyOwner {
balances[to] += amount;
}
// Ownership transfer is two-step
// 1. Current owner calls transferOwnership(newOwner)
// 2. New owner calls acceptOwnership()
}
Ownable Features
- ✅ Automatically sets deployer as owner
- ✅
onlyOwnermodifier for protected functions
- ✅ Two-step ownership transfer (
transferOwnership+acceptOwnership)
- ✅
renounceOwnershipfor removing owner entirely
Ownable Implementation (Simplified)
abstract contract Ownable {
address private _owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
constructor() {
_transferOwnership(msg.sender);
}
function owner() public view returns (address) {
return _owner;
}
modifier onlyOwner() {
require(owner() == msg.sender, "Ownable: caller is not the owner");
_;
}
function renounceOwnership() public onlyOwner {
_transferOwnership(address(0));
}
function transferOwnership(address newOwner) public onlyOwner {
require(newOwner != address(0), "Ownable: new owner is the zero address");
_transferOwnership(newOwner);
}
function _transferOwnership(address newOwner) internal {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
}
Role-Based Access Control (RBAC)
Multiple roles के साथ complex permissions के लिए, OpenZeppelin का AccessControl use करें।
import "@openzeppelin/contracts/access/AccessControl.sol";
contract MultiRoleToken is AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
mapping(address => uint256) public balances;
bool public paused;
constructor() {
// Grant deployer the default admin role
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
}
function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
require(!paused, "Contract paused");
balances[to] += amount;
}
function burn(address from, uint256 amount) external onlyRole(BURNER_ROLE) {
require(balances[from] >= amount, "Insufficient balance");
balances[from] -= amount;
}
function pause() external onlyRole(PAUSER_ROLE) {
paused = true;
}
function unpause() external onlyRole(PAUSER_ROLE) {
paused = false;
}
// Admin can grant/revoke roles
function grantMinter(address account) external onlyRole(DEFAULT_ADMIN_ROLE) {
grantRole(MINTER_ROLE, account);
}
}
RBAC Benefits
- ✅ Multiple independent roles
- ✅ Role hierarchy (admin roles can manage other roles)
- ✅ Fine-grained permissions
- ✅ Easy to audit और manage
- ✅ Separation of concerns
AccessControl Implementation (Simplified)
abstract contract AccessControl {
struct RoleData {
mapping(address => bool) members;
bytes32 adminRole;
}
mapping(bytes32 => RoleData) private _roles;
bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;
event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);
event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);
modifier onlyRole(bytes32 role) {
require(hasRole(role, msg.sender), "AccessControl: account missing role");
_;
}
function hasRole(bytes32 role, address account) public view returns (bool) {
return _roles[role].members[account];
}
function grantRole(bytes32 role, address account) public onlyRole(getRoleAdmin(role)) {
_grantRole(role, account);
}
function revokeRole(bytes32 role, address account) public onlyRole(getRoleAdmin(role)) {
_revokeRole(role, account);
}
function getRoleAdmin(bytes32 role) public view returns (bytes32) {
return _roles[role].adminRole;
}
function _grantRole(bytes32 role, address account) internal {
if (!hasRole(role, account)) {
_roles[role].members[account] = true;
emit RoleGranted(role, account, msg.sender);
}
}
function _revokeRole(bytes32 role, address account) internal {
if (hasRole(role, account)) {
_roles[role].members[account] = false;
emit RoleRevoked(role, account, msg.sender);
}
}
}
Time-Locked Admin Functions
Critical changes के लिए, time delay add करें ताकि users को react करने का time मिले।
import "@openzeppelin/contracts/governance/TimelockController.sol";
contract TimeLocked is Ownable {
TimelockController public timelock;
constructor(uint256 minDelay, address[] memory proposers, address[] memory executors) {
timelock = new TimelockController(minDelay, proposers, executors, address(0));
}
function criticalFunction() external {
require(msg.sender == address(timelock), "Must go through timelock");
// ... critical logic
}
}
Workflow:
minDelay (e.g., 2 days) wait करना होता हैMulti-Signature Wallets
Single owner से बचने के लिए, multi-sig wallets use करें।
contract SimpleMultiSig {
address[] public owners;
uint256 public required;
mapping(uint256 => mapping(address => bool)) public confirmations;
struct Transaction {
address to;
uint256 value;
bytes data;
bool executed;
}
Transaction[] public transactions;
modifier onlyOwner() {
bool isOwner = false;
for (uint i = 0; i < owners.length; i++) {
if (owners[i] == msg.sender) {
isOwner = true;
break;
}
}
require(isOwner, "Not owner");
_;
}
constructor(address[] memory _owners, uint256 _required) {
require(_owners.length >= _required && _required > 0, "Invalid requirements");
owners = _owners;
required = _required;
}
function submitTransaction(address to, uint256 value, bytes memory data)
external
onlyOwner
returns (uint256)
{
uint256 txId = transactions.length;
transactions.push(Transaction({
to: to,
value: value,
data: data,
executed: false
}));
return txId;
}
function confirmTransaction(uint256 txId) external onlyOwner {
require(txId < transactions.length, "Transaction does not exist");
require(!confirmations[txId][msg.sender], "Already confirmed");
confirmations[txId][msg.sender] = true;
}
function executeTransaction(uint256 txId) external onlyOwner {
require(txId < transactions.length, "Transaction does not exist");
require(!transactions[txId].executed, "Already executed");
uint256 count = 0;
for (uint i = 0; i < owners.length; i++) {
if (confirmations[txId][owners[i]]) {
count++;
}
}
require(count >= required, "Not enough confirmations");
Transaction storage txn = transactions[txId];
txn.executed = true;
(bool success, ) = txn.to.call{value: txn.value}(txn.data);
require(success, "Transaction failed");
}
}
Production-ready multi-sigs: Gnosis Safe use करें।
Testing Access Control
Foundry Test Example
contract AccessControlTest is Test {
MyToken token;
address owner = address(1);
address alice = address(2);
address bob = address(3);
function setUp() public {
vm.prank(owner);
token = new MyToken();
}
function testOnlyOwnerCanMint() public {
// Owner can mint
vm.prank(owner);
token.mint(alice, 100);
assertEq(token.balances(alice), 100);
// Non-owner cannot mint
vm.prank(bob);
vm.expectRevert("Ownable: caller is not the owner");
token.mint(alice, 100);
}
function testOwnershipTransfer() public {
vm.prank(owner);
token.transferOwnership(alice);
// New owner can now mint
vm.prank(alice);
token.mint(bob, 50);
assertEq(token.balances(bob), 50);
// Old owner cannot mint
vm.prank(owner);
vm.expectRevert("Ownable: caller is not the owner");
token.mint(bob, 50);
}
}
Best Practices Checklist
- ✅ OpenZeppelin के battle-tested contracts use करें (
Ownable,AccessControl)
- ✅
msg.senderuse करें,tx.originnever
- ✅ Critical functions के लिए two-step ownership transfers implement करें
- ✅ Complex permissions के लिए role-based access control use करें
- ✅ Time locks add करें critical admin functions में
- ✅ Production में multi-sig wallets prefer करें
- ✅ हर modifier और access check को thoroughly test करें
- ✅ Events emit करें हर permission change पर (auditability के लिए)
- ✅ Zero address checks add करें ownership/role assignments में
- ✅ Documentation में clearly define करें कि किसे क्या permissions हैं
Real-World Example: Poly Network Hack (2021)
August 2021 में, Poly Network से $600+ million drain हुए थे, जो तब तक का सबसे बड़ा DeFi hack था। Root cause एक access control vulnerability थी:
- Contract ने
_executeCrossChainTx()function को expose किया था
- यह function किसी को भी arbitrary data के साथ call करने की permission देता था
- Attacker ने इसे exploit करके खुद को keeper role assign कर लिया
- Keeper permissions के साथ, attacker ने सारे funds drain कर दिए
Lesson: सभी privileged functions को properly protect करें और extensive testing करें।
Conclusion
Access control smart contract security का backbone है। Simple ownership mistakes से लेकर complex role hierarchies तक, हर vulnerability devastating consequences ले सकता है।
Key takeaways:
- OpenZeppelin के proven patterns use करें
msg.senderuse करें,tx.originनहीं
- Complex systems के लिए role-based access control implement करें
- Critical changes के लिए time locks add करें
- Multi-sig wallets से single points of failure eliminate करें
- Extensively test करें सभी permission logic को
Solingo पर, आप real-world scenarios के साथ access control vulnerabilities को identify और fix करना सीखेंगे। Production-grade authorization patterns master करें interactive challenges के साथ।
अभी शुरू करें: solingo.io/security/access-control