Most large smart contract losses do not need a clever exploit: they need one function that trusts the wrong caller, or one admin key that the wrong person can use. OWASP ranks Access Control Vulnerabilities first in the Smart Contract Top 10:2026, as it did in 2025, and its own incident list for the category runs from a missing check on a hook callback to a stolen deployer key that upgraded a vault. This guide goes deeper than our overview of contract security on one question: who is allowed to do what, through which key, and how that is enforced, observed and recovered. It is for protocol engineers, security reviewers and the people who hold or operate privileged keys for a protocol, treasury or token. See the Smart Contract Security Guide for the wider development lifecycle.
Key takeaways
- Access control is two problems, not one. The code must check the right caller on every privileged path, and the keys behind those callers must be held, used and rotated like the most privileged non-human identities in any organisation.
- Model every power as a separate role. Minting, pausing, upgrading, parameter changes and fund movement should never sit behind one owner address; OpenZeppelin AccessControl or AccessManager make this cheap.
- No externally owned account should hold a privileged role in production. Put roles behind a multisig, and put irreversible powers such as upgrades behind a timelock that the multisig controls.
- Delays and two-step transfers are controls, not friction. Ownable2Step, AccessControlDefaultAdminRules and TimelockController give users and monitors time to see a hostile change before it lands.
- Signers must verify what they sign. The Bybit theft used genuine signatures on a manipulated transaction; independent decoding of calldata on a second tool is part of access control.
What OWASP SC01:2026 covers
OWASP describes the category as failing to rigorously enforce who can invoke privileged behaviour, under which conditions and with which parameters. In current DeFi that reaches well beyond a single onlyOwner check to governance contracts, multisigs, guardians, proxy admins and cross-chain routers, which together control minting and burning, reserve movement, pool reconfiguration, pausing and upgrades. OWASP groups the risk into four focus areas, which make a useful review scope.
| OWASP focus area | Typical privileged functions | What to review |
|---|---|---|
| Ownership and admin controls | transferOwnership, grantRole, setRoleAdmin, governor proposals | Who holds each role today, whether transfers are two-step, whether the admin role is its own admin |
| Upgrade and pause mechanisms | upgradeTo or upgradeToAndCall, pause, unpause, proxy admin changes | Whether upgrades are timelocked, who can pause, whether a pauser can also move funds |
| Fund movement and accounting | mint, burn, emergencyWithdraw, fee recipient changes, pool reconfiguration | Caller checks on every path, caps and rate limits, events that record the caller |
| Cross-chain or cross-module trust | Bridge message handlers, vault routers, L2 messenger callbacks, hooks | Whether the contract verifies the real caller and the original sender, not a value supplied in calldata |
The full ranking and how access control interacts with the other nine risks are covered in the OWASP Smart Contract Top 10 Guide.
How access control fails in code
OWASP lists four vulnerable patterns. Each has a recognisable shape in review.
- Missing modifier or role check: a function intended for the owner or a guardian has no
onlyOwner,onlyRoleorrestrictedguard. OWASP's own example has an unguardedtransferOwnershipand anemergencyWithdrawwith no caller check, so any address can take the contract or drain it. These usually appear in functions added late, such as emergency or migration helpers. - Wrong assumption about the caller: code that authorises on
tx.origin, on an address passed in as a parameter, or onmsg.senderin a context where it is not who the developer thinks (inside adelegatecall, through a meta-transaction forwarder, or in a hook called by a pool manager). The Solidity documentation is blunt: "Never use tx.origin for authorization." The Balancer V2 exploit in November 2025, about 128 million US dollars by OWASP's figure, combined a check ofmsg.senderagainst a user-supplied sender field with a rounding flaw. - Unprotected initialisation: an upgradeable contract whose
initializefunction can be called by anyone, or called again, lets the caller set themselves as owner. Use theinitializerandreinitializerguards, and disable initialisers on the implementation contract itself. See the Smart Contract Upgradeability and Proxy Security Guide. - Privilege confusion across modules: one contract trusts another module, router or hook without checking that the call came from the expected contract. In the Cork Protocol incident of May 2025 (11 to 12 million US dollars per OWASP), Uniswap v4 hook callbacks such as
beforeSwaplacked anonlyPoolManagercheck, so attackers called the hook directly with arbitrary parameters.
A practical review rule follows from these: list every external and public function that changes state, and for each one write down who should be able to call it and why. Any function where the answer is "only X" must show the check for X in code, and a test must prove an unprivileged caller is reverted.
Choosing an access control pattern
OWASP recommends battle-tested primitives over bespoke role systems. OpenZeppelin Contracts 5.x offers four building blocks, each suited to a different size of system.
| Pattern | How it works | Good for | Watch out for |
|---|---|---|---|
| Ownable | One owner set at deployment; onlyOwner guards functions; transferOwnership and renounceOwnership | Small contracts with one administrative power, owned by a multisig | One address holds every power; a typo in transferOwnership loses control; renouncing makes owner functions uncallable for ever |
| Ownable2Step | Transfer sets pendingOwner; the new owner must call acceptOwnership | Any Ownable contract: there is little reason to use plain Ownable | Still a single owner; pending transfers should be monitored |
| AccessControl (with AccessControlDefaultAdminRules) | bytes32 roles, onlyRole, grantRole and revokeRole by the role's admin; DEFAULT_ADMIN_ROLE is its own admin; the DefaultAdminRules extension limits the admin to one account with a delayed two-step transfer | Contracts with several distinct powers (minter, pauser, upgrader, oracle setter) | The default admin can grant itself anything; role membership is not enumerable without AccessControlEnumerable, so rely on RoleGranted and RoleRevoked events |
| AccessManager | One manager contract holds uint64 roles for many targets; setTargetFunctionRole maps a contract address and function selector to a role; managed contracts use the restricted modifier; roles can carry execution delays and guardians | Protocols with many contracts that need one consistent permission model | The manager becomes the single most critical contract; ADMIN_ROLE (0) must itself sit behind a multisig and delay |
Decision guidance
- One contract, one power: Ownable2Step owned by a multisig is enough. Adding roles you never use creates review burden without benefit.
- One contract, several powers: AccessControl with AccessControlDefaultAdminRules. Give each power its own role and its own holder.
- Several contracts that share operators: AccessManager, so one inventory and one set of delays covers the whole system, and
setTargetClosedcan shut every restricted function on a target during an incident. - Immutable by design: if a contract should never change, do not add an owner at all. A renounced owner is safer than an unused one, but no owner is clearer still.
Designing roles and separating powers
Treat the role map as a privileged access design, the same exercise an identity team performs for production cloud accounts. The principles are those of least privilege and separation of duties; only the enforcement point is different.
| Power | Suggested holder | Delay | Why |
|---|---|---|---|
| Upgrade implementation or change proxy admin | TimelockController whose proposers are a governance multisig | Long (days) | Irreversible and total: an upgrade can replace every rule |
| Grant and revoke roles (default admin) | Same timelock, via AccessControlDefaultAdminRules | Long, with delayed two-step admin transfer | Whoever holds this can become anything else |
| Change risk parameters (caps, fees, oracle sources) | Operations multisig, optionally timelocked | Short to medium | Frequent but able to cause loss if abused |
| Pause | Guardian multisig with a lower threshold | None | Must act in minutes during an incident; cannot move funds |
| Cancel scheduled operations | Guardian (AccessManager guardian or timelock CANCELLER_ROLE) | None | Lets defenders stop a hostile change during its delay |
| Mint or move treasury funds | Treasury multisig with caps and rate limits in code | Medium | Limits blast radius even if signers are compromised |
| Routine automation (keepers, rebalancers) | Dedicated bot address with one narrow role | None | Hot key: give it only what it needs and nothing that moves value without bounds |
- Pause should never imply power to move funds: emergency roles can bypass delays precisely because they cannot upgrade code or withdraw. If an emergency role can drain, it is an upgrade role without the timelock.
- Use the timelock correctly: OpenZeppelin notes that granting the executor role to
address(0)lets anyone execute a ready operation, and that if the deployer renounces admin so the timelock governs itself, losing the proposer or executor accounts can lock the system permanently. Keep at least two independent proposer paths. - Remove deployer and test holders after launch: the Zoth loss in March 2025 (8.4 million US dollars per OWASP) came from a compromised deployer wallet, a single key that still controlled admin functions and was used to upgrade a vault proxy to a malicious implementation.
Admin keys are non-human identities
Every role holder is ultimately a key, or a set of keys, that a person or a machine uses to sign. That makes protocol access control an identity problem as much as a code problem, and the controls are familiar to anyone who runs privileged access for cloud workloads. See the Non-Human Identity (NHI) Guide and the Privileged Access Management Guide.
| Custody option | Strengths | Weaknesses | Use for |
|---|---|---|---|
| Single EOA (software or hardware wallet) | Simple, cheap | One stolen key is total compromise, as at BitMart | Nothing privileged in production; narrow bot roles only |
| Multisig smart account (for example Safe) | M-of-N approval visible on chain; signers can be rotated; modules and guards add policy | Signers still sign what their screen shows; modules can bypass signatures | Governance, operations, guardian and treasury roles |
| MPC or threshold signing | No full key exists in one place; policy engines can restrict destinations | Policy lives off chain with the provider or operator; on chain it looks like one address | Exchange and custody operations, hot wallets with limits |
| Timelock in front of a multisig | Delay gives users and monitors time to react; cancel role can stop hostile changes | Slower response for legitimate urgent fixes | Upgrades and default admin |
- Signer hygiene: each signer uses a dedicated hardware wallet and a dedicated device for signing, separate from daily browsing and development. Thresholds should survive the loss of one signer and resist the compromise of one organisation: do not let one company or one person hold a majority of keys.
- Verify, do not trust the interface: in the Bybit Hack 2025, modified JavaScript in the Safe{Wallet} web interface showed signers a routine transfer while the transaction did something else, and roughly 1.5 billion US dollars left the cold wallet. Decode calldata and check the target, value, operation type (call or delegatecall) and nonce on a second, independent tool before signing.
- Safe modules and guards are part of the role map: Safe's documentation warns that modules can execute arbitrary transactions through
execTransactionFromModuleand that a malicious module can take over a Safe. Guards can add pre and post execution checks, but a broken guard can cause a denial of service. Inventory both like role holders. - Rotation and leavers: when a signer leaves, rotate them out of every Safe and role the same week, and record it. Stale signers are the web3 equivalent of an orphaned service account.
- Ceremonies for creation and rotation: generating or rotating high-value keys should follow a written, witnessed procedure. See the Cryptographic Key Ceremony Template and the Multisig Treasury Operations Guide.
Implementing and hardening access control
- Inventory: for every deployed contract, list each privileged function, the role that guards it, and the current holder of that role from on-chain state and events. Include proxy admins, Safe modules and guards, timelock proposers, executors and cancellers.
- Write the intended model: a one-page role matrix stating who should hold each power and with what delay. Differences between intended and actual are your first findings.
- Fix the code: add missing guards, replace
tx.originchecks, protect initialisers, check the real caller in hooks and message handlers, and migrate plain Ownable to Ownable2Step or AccessControl where powers need separating. - Test it as a property: OWASP recommends encoding access policy in tests, fuzzing properties and formal specifications. Minimum: for every privileged function, a test that an arbitrary address is reverted; an invariant that no unprivileged sequence can change the owner, the admin or the implementation, or reduce reserves beyond expected flows.
- Rehearse on a fork or testnet: run the full migration (grant new holders, revoke old ones, transfer admin to the timelock) on a fork of mainnet first, and confirm the old holders are really powerless afterwards.
- Move holders in order: first move every role off EOAs to multisigs, then move upgrade and default admin powers behind a timelock the multisig controls, then revoke the deployer. Keep a documented rollback: until the deployer is revoked, a failed step can be reversed; after it, only the new holders can act, so check twice.
- Publish the model: document role holders, thresholds and delays for users and auditors, and keep it current. Transparency lets outside monitors spot unexpected changes.
Detection and measurement
Every privileged action on chain emits evidence. OWASP advises emitting events for every privilege change or upgrade, and the OpenZeppelin contracts already do for roles and ownership.
- Events to alert on:
RoleGranted,RoleRevoked,RoleAdminChanged,OwnershipTransferStartedandOwnershipTransferred,UpgradedandAdminChangedon proxies, timelockCallScheduledandCallExecuted, AccessManagerTargetFunctionRoleUpdated, Safe owner, threshold, module and guard changes. - Behaviour to alert on: privileged calls from addresses not in the role matrix, transactions from a multisig that use delegatecall, large or unusual outflows, and any interaction with a newly deployed implementation.
- Who watches: alerts must reach someone able to cancel or pause within the timelock delay. A seven day delay is worthless if nobody reads the alert for eight.
| Metric | Target |
|---|---|
| Privileged roles held by an EOA in production | Zero, except documented narrow bot roles |
| Privileged functions with a negative (unauthorised caller) test | 100 per cent |
| Role matrix drift (on-chain holders not matching the documented model) | Zero, checked at least monthly |
| Time from a privilege change event to human review | Well inside the shortest timelock delay |
| Signer rotation after a leaver | Within 7 days |
| Upgrades executed without a timelock delay | Zero outside a documented emergency process |
Framework mapping
Smart contracts are not named in mainstream control frameworks, but the controls translate directly. These mappings help a regulated firm or a protocol with corporate operators place contract access control inside its existing programme.
| Control area | NIST SP 800-53 Rev 5 | NIST CSF 2.0 | ISO/IEC 27001:2022 Annex A |
|---|---|---|---|
| Role design and least privilege | AC-6 Least Privilege, AC-5 Separation of Duties | PR.AA-05 | A.5.15 Access control, A.8.2 Privileged access rights |
| Role holder inventory and review | AC-2 Account Management, AC-6(7) Review of User Privileges | PR.AA-01 | A.5.18 Access rights |
| Key custody and signing | SC-12 Cryptographic Key Establishment and Management, IA-5 Authenticator Management | PR.AA-01 | A.8.24 Use of cryptography |
| Upgrades and parameter changes | CM-3 Configuration Change Control | PR.PS-01 | A.8.32 Change management |
| Access policy testing | SA-11 Developer Testing and Evaluation | PR.PS-01 | A.8.29 Security testing in development and acceptance |
| Monitoring privilege changes | AU-6 Audit Record Review, Analysis, and Reporting | DE.CM-09 | A.8.16 Monitoring activities |
| Pause, cancel and key compromise response | IR-4 Incident Handling | RS.MA | A.5.26 Response to information security incidents |
Common failure patterns
- The god-mode owner: one address can upgrade, mint, pause and withdraw. Whoever steals that key owns the protocol, and every user must trust one person's operational security.
- Deployer never removed: the key that deployed the contracts keeps a role "just in case", sits on a developer laptop, and becomes the attack path, as in the Zoth case.
- Single hot key with no limits: the BitMart Hot Wallet Hack 2021 lost about 150 million US dollars by the exchange's own estimate through one stolen private key, with no contract flaw involved. Caps, rate limits and destination controls outside the key would have bounded it.
- Signers trust the screen: a multisig is only as strong as each signer's ability to see what they approve. Bybit shows a compromised supplier interface defeating several genuine signers at once.
- Hooks and callbacks trusted by default: functions meant to be called only by a pool manager, bridge or router that do not check the caller, as at Cork Protocol.
- Timelock that nobody watches, or that locks you out: a delay without monitoring adds nothing, and a self-governing timelock whose proposers are lost cannot be fixed.
Practitioner checklist
- Produce a role matrix for every contract: function, role, holder, threshold, delay.
- Confirm no production privileged role is held by an EOA or by the deployer.
- Replace plain Ownable with Ownable2Step or AccessControl with AccessControlDefaultAdminRules.
- Remove every
tx.originauthorisation and check the real caller in hooks, callbacks and message handlers. - Protect and disable initialisers on all upgradeable implementations.
- Put upgrades and default admin behind a TimelockController controlled by a multisig, with a separate guardian able to cancel and pause.
- Add negative tests and fuzz invariants for every privileged function.
- Require signers to decode calldata on an independent tool and to use dedicated signing devices.
- Inventory Safe modules and guards alongside role holders and review them on every change.
- Alert on every role, ownership, upgrade and Safe configuration event, routed to people who can act within the delay.
Standards and references
- OWASP: SC01:2026 Access Control Vulnerabilities
- OWASP: Smart Contract Top 10
- OpenZeppelin: Access Control (Contracts 5.x)
- OpenZeppelin: Access API reference
- Solidity: Security Considerations
- Safe: Safe Modules
- Safe: Safe Guards
Related NHI Mgmt Group resources: Smart Contract Security Guide · OWASP Smart Contract Top 10 Guide · Multisig Treasury Operations Guide · MPC Wallet and Threshold Signing Security Guide · Web3 Incident Response Playbook