skills/vulnerability-patterns/access-control/SKILL.md
Access-control exploit patterns and secure authorization approaches for privileged Solidity functions.
npx skillsauth add apegurus/solidity-argus access-controlInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Access control vulnerabilities occur when unauthorized users can execute privileged functions. These are often the simplest bugs but cause catastrophic losses.
Total Losses: $1B+ across DeFi
// VULNERABLE: Anyone can call
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
// SECURE: Access control
function mint(address to, uint256 amount) external onlyMinter {
_mint(to, amount);
}
// VULNERABLE: Anyone can initialize
function initialize(address _admin) external {
admin = _admin;
}
// SECURE: Initializer modifier
function initialize(address _admin) external initializer {
admin = _admin;
}
// VULNERABLE: tx.origin can be manipulated
function withdraw() external {
require(tx.origin == owner, "Not owner");
payable(owner).transfer(address(this).balance);
}
// Attack: Trick owner into calling attacker's contract
contract Attacker {
function attack(VulnerableContract target) external {
target.withdraw(); // tx.origin is still the owner!
}
}
// VULNERABLE: Admin can make anyone admin
function setAdmin(address newAdmin) external onlyAdmin {
admin = newAdmin; // No checks on newAdmin
}
// SECURE: Multi-sig or timelock for admin changes
// VULNERABLE: Same signature works multiple times
function executeWithSig(bytes calldata data, bytes calldata sig) external {
address signer = recover(keccak256(data), sig);
require(signer == admin, "Invalid sig");
// Execute...
}
// SECURE: Include nonce
mapping(address => uint256) public nonces;
function executeWithSig(bytes calldata data, uint256 nonce, bytes calldata sig) external {
require(nonce == nonces[msg.sender]++, "Invalid nonce");
bytes32 hash = keccak256(abi.encode(data, nonce, address(this), block.chainid));
address signer = recover(hash, sig);
require(signer == admin, "Invalid sig");
// Execute...
}
What happened:
Root cause: Insufficient key distribution + social engineering
Lesson: Multi-sig threshold must assume some keys will be compromised.
What happened:
initWallet()kill(), destroyed libraryRoot cause: Unprotected initialize function on library contract
// The fatal function
function initWallet(address[] _owners, uint _required, uint _daylimit) {
// NO ACCESS CONTROL
initMultiowned(_owners, _required);
}
Lesson: ALL contracts need access control, especially libraries.
What happened:
Root cause: Improper signature verification in Solana program
What happened:
Root cause: Insufficient validation of cross-chain calls
onlyOwner/onlyAdmin used consistently?initialize() protected with initializer modifier?tx.origin used anywhere? (Almost always wrong)import "@openzeppelin/contracts/access/AccessControl.sol";
contract Secure is AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
constructor() {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
}
function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
_mint(to, amount);
}
}
address public pendingOwner;
function transferOwnership(address newOwner) external onlyOwner {
pendingOwner = newOwner;
}
function acceptOwnership() external {
require(msg.sender == pendingOwner, "Not pending owner");
owner = pendingOwner;
pendingOwner = address(0);
}
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract MyContract is Initializable {
address public admin;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers(); // Protect implementation
}
function initialize(address _admin) external initializer {
admin = _admin;
}
}
onlyOwner, onlyRole, etc.) or require(msg.sender == ...) checksaddress public owner;
uint256 public feeRate;
// Missing access control — anyone can call
function setFeeRate(uint256 newRate) external {
feeRate = newRate;
}
// Missing access control — anyone can take ownership
function setOwner(address newOwner) external {
owner = newOwner;
}
// Incomplete: role grant is unprotected
function grantRole(address user, bytes32 role) external {
// Missing: require(hasRole(ADMIN_ROLE, msg.sender))
_roles[role][user] = true;
}
external and public functions that modify state variablesonlyOwner, onlyRole, whenNotPaused, etc.) or an inline require(msg.sender == ...) checkgrantRole requires ADMIN_ROLE)initialize() functions in upgradeable contracts — they must have an initializer modifier to prevent re-initializationdeposit(), claim() where anyone should be able to call)Ownable or AccessControl for standardized role managementinitialize() with the initializer modifierimport {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract Secure is Ownable {
function setFeeRate(uint256 newRate) external onlyOwner {
feeRate = newRate;
}
}
testing
Specialist profile for mechanically applying the attack-vector deck and classifying vectors as skip, drop, or investigate.
tools
Specialist profile for libraries, helpers, base contracts, adapters, encoders, wrappers, and integration glue.
testing
Specialist profile for rounding, scale, decimal, downcast, and arithmetic accounting edge cases.
testing
Specialist profile for extracting conservation laws and state couplings, then searching for violating paths.