From grimoire
Restricts privileged Solidity functions using OpenZeppelin's Ownable2Step and AccessControl with role-based permissions and two-step ownership transfer to prevent unauthorized execution and fund loss.
How this skill is triggered — by the user, by Claude, or both
Slash command
/grimoire:apply-smart-contract-access-controlThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Restrict privileged contract functions with OpenZeppelin's Ownable and AccessControl — using two-step ownership transfer, role-based permissions, and timelocks — preventing unauthorized fund withdrawal, parameter manipulation, and upgrade hijacking.
Restrict privileged contract functions with OpenZeppelin's Ownable and AccessControl — using two-step ownership transfer, role-based permissions, and timelocks — preventing unauthorized fund withdrawal, parameter manipulation, and upgrade hijacking.
Adopted by: OWASP Smart Contract Top 10 SC01 (Access Control) and SC06 (Vulnerable Access Control). SWC-105 (Unprotected Ether Withdrawal) and SWC-106 (Unprotected SELFDESTRUCT Instruction) are the canonical CWEs. OpenZeppelin's Contracts (used by Uniswap, Compound, Aave) provides Ownable2Step and AccessControl as the standard implementations. Ethereum Foundation security documentation mandates access control for all privileged state changes.
Impact: SWC-105 is the most commonly exploited smart contract vulnerability class. The 2022 Nomad Bridge hack ($190M) exploited an access control bug that allowed any address to call an initialization function. The 2021 Poly Network hack ($611M) exploited an access control flaw in the EthCrossChainManager contract. The 2022 Wormhole hack ($320M) involved an unguarded complete_wrapped function. Missing onlyOwner on a withdrawal function typically results in complete fund loss.
Why best: Solidity's default function visibility is public — all functions are callable by any address unless explicitly restricted. Adding require(msg.sender == owner) manually is error-prone and doesn't handle role hierarchies. OpenZeppelin's AccessControl provides audited, battle-tested RBAC with events for all role grants/revocations — the audit trail is essential for forensic analysis after incidents.
Sources: OWASP Smart Contract Top 10 SC01, SC06; SWC-105, SWC-106; OpenZeppelin Contracts documentation; Ethereum Smart Contract Best Practices
Use Ownable2Step — two-step ownership transfer prevents accidental loss:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/access/Ownable2Step.sol";
contract Protocol is Ownable2Step {
uint256 public fee;
constructor(address initialOwner) Ownable(initialOwner) {}
// Restricted to owner only
function setFee(uint256 newFee) external onlyOwner {
require(newFee <= 100, "Fee cannot exceed 100 basis points");
fee = newFee;
}
function emergencyWithdraw() external onlyOwner {
(bool ok, ) = owner().call{value: address(this).balance}("");
require(ok, "Transfer failed");
}
}
// Ownable2Step: transferOwnership sets pending owner, new owner must call acceptOwnership()
// Prevents transferring to wrong address (which would permanently lock admin functions)
Use AccessControl for multi-role systems:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/access/AccessControl.sol";
contract DeFiVault is AccessControl {
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
constructor(address admin) {
_grantRole(DEFAULT_ADMIN_ROLE, admin); // can grant/revoke other roles
_grantRole(ADMIN_ROLE, admin);
}
function setFeeRecipient(address recipient) external onlyRole(ADMIN_ROLE) {
// only ADMIN_ROLE can change fee recipient
}
function processWithdrawal(address user) external onlyRole(OPERATOR_ROLE) {
// operators can process but cannot change protocol parameters
}
function pause() external onlyRole(PAUSER_ROLE) {
// separate pause role for emergency response
}
}
Protect initialization functions — prevent re-initialization:
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
contract UpgradeableProtocol is Initializable, AccessControl {
uint256 public fee;
// initializer modifier ensures this can only be called once
function initialize(address admin, uint256 initialFee) external initializer {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
fee = initialFee;
}
// For upgradeable contracts: add reinitializer version guard
function initializeV2(uint256 newParam) external reinitializer(2) {
// only runs on upgrade to V2, not callable again after
}
}
Use a Timelock for high-impact admin operations:
import "@openzeppelin/contracts/governance/TimelockController.sol";
// Deployer: create timelock with 48-hour delay for all critical ops
address[] memory proposers = new address[](1);
address[] memory executors = new address[](1);
proposers[0] = multiSigAddress;
executors[0] = multiSigAddress;
TimelockController timelock = new TimelockController(
48 hours, // minDelay — operations wait 48 hours before execution
proposers,
executors,
address(0) // no admin — timelock is self-administered
);
// Then set the timelock as the owner of your protocol contracts
protocol.transferOwnership(address(timelock));
Emit events on all role changes for auditability:
// OpenZeppelin AccessControl emits RoleGranted/RoleRevoked automatically
// For custom access control, always emit:
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
event RoleGranted(bytes32 indexed role, address indexed account);
event RoleRevoked(bytes32 indexed role, address indexed account);
// Monitor these events off-chain for unauthorized role changes
Use multi-sig for protocol admin keys:
For production protocols:
- Deploy Gnosis Safe (multi-sig) as the contract owner
- Require M-of-N signers (e.g., 3-of-5) for admin transactions
- Never use a single EOA as the sole owner of a protocol with TVL > $100k
- Hardware wallets for all multi-sig signers
initialize() functions callable after deployment — use initializer modifier or _disableInitializers() in the constructor of implementation contracts.DEFAULT_ADMIN_ROLE in OpenZeppelin AccessControl can grant any role — restrict it to a multi-sig, not a developer EOA.onlyRole/onlyOwner function.Ownable single-step transfer — transferOwnership(wrongAddress) permanently loses admin control; use Ownable2Step.msg.sender which is a deploy script — constructor ownership goes to the deployer account; immediately transfer to a multi-sig post-deployment.onlyOwner on functions that should be callable by users — access control bugs go both ways: too permissive (no guard) and too restrictive (wrong guard).vm.prank(attacker) to call owner-only functions and verify revert.npx claudepluginhub jeffreytse/grimoire --plugin grimoirePrevents Solidity smart contract vulnerabilities like reentrancy, overflows, and access control using secure patterns, Checks-Effects-Interactions, and ReentrancyGuard. For writing and auditing contracts.
Develops secure smart contracts by integrating OpenZeppelin libraries for ERC tokens, access control, pausability, governance, and accounts. Supports Solidity, Cairo, Stylus, Stellar.
Provides Solidity smart contract security best practices, vulnerability prevention, and secure patterns for writing, auditing, DeFi protocols, and preventing reentrancy, overflows, access issues.