Foundational patterns for managing permissions and authorization in decentralized systems.
Comparing Onchain vs Offchain Access Control Models
Core Access Control Models
Ownable
The single-owner model centralizes administrative power to one address.
- Uses a simple
ownerstate variable andonlyOwnermodifier. - Common in early smart contracts for upgradeability and fee management.
- Creates a single point of failure but is straightforward to implement and audit.
Role-Based Access Control (RBAC)
Assigns permissions based on defined roles like ADMIN, MINTER, or PAUSER.
- Uses mappings and modifiers like
onlyRole(MINTER_ROLE). - Implemented in libraries like OpenZeppelin's AccessControl.
- Provides granularity and multi-signer security, essential for DAOs and complex protocols.
Access Control Lists (ACL)
Manages permissions via a list mapping specific addresses to allowed actions.
- More granular than RBAC, specifying
address -> functionpermissions. - Used in systems like Compound's Comptroller for market permissions.
- Offers high precision but can become complex and gas-intensive to manage onchain.
Multi-Signature Wallets
Requires multiple signatures from a set of approvers to execute a transaction.
- Implemented via smart contracts like Gnosis Safe or simple
n-of-mlogic. - Decentralizes control for treasury management or protocol upgrades.
- Increases security through redundancy but adds operational latency for approvals.
Token-Gated Access
Restricts access based on ownership of a specific NFT or token.
- Checks token balance or membership via
IERC721.balanceOf(msg.sender) > 0. - Enables gated communities, premium features, and voting rights.
- Aligns access with economic stake or community participation directly onchain.
Attribute-Based Access Control (ABAC)
Grants access based on dynamic attributes of the user, resource, or environment.
- Evaluates complex policies using attributes like token age, reputation score, or time.
- Often implemented offchain due to computational complexity.
- Enables sophisticated, context-aware permissions for DeFi risk management or gaming.
Onchain vs Offchain: Key Differences
Comparison of core technical and operational characteristics between onchain and offchain access control implementations.
| Feature | Onchain Access Control | Offchain Access Control | Hybrid Model |
|---|---|---|---|
Authorization Logic Execution | Executed by EVM on the blockchain | Executed by a centralized server or decentralized oracle network | Critical logic onchain, complex rules offchain |
State Finality & Consistency | Globally consistent, immutable state | Eventual consistency, dependent on external system | Onchain state is source of truth, offchain state is ephemeral |
Transaction Cost per Auth Check | ~50,000 - 200,000 gas ($1-$10 at 50 Gwei) | ~$0.001 - $0.01 (server/API cost) | Variable; onchain component incurs gas, offchain is low-cost |
Latency for Permission Update | ~12 sec (Ethereum block time) to ~5 min (finality) | ~100 ms - 2 sec (API response time) | Onchain delay for root changes, near-instant for delegated updates |
Data Privacy for User Roles | Fully transparent; roles are public onchain | Can be fully private; encrypted or held offchain | Public onchain identifiers, private attributes stored offchain |
Censorship Resistance | High; governed by protocol rules, not operators | Low; dependent on operator availability and policies | Medium; core access is resilient, auxiliary features are not |
Development & Upgrade Complexity | High; requires smart contract deployment and audits | Low; standard web2 backend development practices | Medium; requires integration between onchain/offchain systems |
Trust Assumptions | Trustless; security derives from blockchain consensus | Requires trust in the offchain authority or service | Minimized trust; onchain verifies critical claims from offchain |
Implementing Onchain Access Control
Process overview for deploying and managing permission logic directly on the blockchain.
Design the Access Control Logic
Define the rules and roles for your smart contract system.
Detailed Instructions
First, map out the permissioned actions and the entities that can perform them. Common patterns include role-based access control (RBAC) where functions are gated by roles like MINTER_ROLE or ADMIN_ROLE. Determine if you need a single owner, a multi-signature wallet for administrative actions, or a more complex hierarchy. Consider using established libraries like OpenZeppelin's AccessControl for gas-efficient, audited implementations. Define the initial role assignments and any role-administration roles (e.g., who can grant the ADMIN_ROLE). This upfront design prevents security flaws and simplifies future upgrades.
- Sub-step 1: List all contract functions that require protection (e.g.,
mint,pause,updateFee). - Sub-step 2: Group these functions into logical roles (e.g., Minter, Pauser, Upgrader).
- Sub-step 3: Decide on the governance model for assigning and revoking these roles.
solidity// Example role definitions using keccak256 hashes bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
Tip: Use descriptive, unique role hash names to avoid collisions with other systems.
Integrate an Access Control Library
Inherit from and configure a battle-tested access control contract.
Detailed Instructions
Import and inherit from a library such as OpenZeppelin's AccessControl or Ownable. For most projects, AccessControl is preferred due to its flexibility with multiple roles. The contract will manage role storage and provide modifiers like onlyRole. Initialize roles in the constructor, typically granting the DEFAULT_ADMIN_ROLE to the deployer address (msg.sender). This admin can then grant and revoke other roles. Ensure you understand the role admin concept, where a role can be granted the permission to manage another role, creating a permission tree.
- Sub-step 1: Install the OpenZeppelin Contracts package:
npm install @openzeppelin/contracts. - Sub-step 2: Import the library:
import "@openzeppelin/contracts/access/AccessControl.sol";. - Sub-step 3: Inherit from
AccessControland call_grantRolein the constructor.
solidityimport "@openzeppelin/contracts/access/AccessControl.sol"; contract MyToken is AccessControl { constructor() { _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); } }
Tip: For simple ownership,
Ownableis sufficient, butAccessControlis more future-proof.
Apply Access Modifiers to Functions
Protect sensitive functions by restricting execution to authorized roles.
Detailed Instructions
Use the onlyRole modifier provided by the access control library to guard your function logic. Apply the modifier directly in the function signature. This ensures the transaction reverts unless the caller (msg.sender) holds the required role. For critical functions like upgrading a proxy contract or withdrawing funds, consider requiring a timelock or multi-signature approval in addition to the role check, which adds a delay for security. Always verify that the modifier is placed correctly and that there are no unprotected backdoor functions.
- Sub-step 1: Add the
onlyRolemodifier to a function:function mint(address to) public onlyRole(MINTER_ROLE). - Sub-step 2: Test the restriction by calling the function from an unauthorized address (expect revert).
- Sub-step 3: For admin functions, consider emitting events (e.g.,
RoleGranted) for transparency.
solidityfunction safeMint(address to, uint256 tokenId) public onlyRole(MINTER_ROLE) { _safeMint(to, tokenId); } function setBaseURI(string memory newUri) public onlyRole(DEFAULT_ADMIN_ROLE) { _setBaseURI(newUri); }
Tip: Avoid using
tx.originfor authorization; always usemsg.sender.
Deploy and Initialize Roles
Deploy the contract and set up the initial role permissions onchain.
Detailed Instructions
Deploy the contract to your target network (e.g., Ethereum Mainnet, Arbitrum). After deployment, the constructor logic will have set the initial admin. You must now execute transactions to grant roles to the appropriate addresses, which could be EOAs or smart contracts like a DAO treasury. Use the grantRole function, which can only be called by an account with the role's admin role. For decentralized governance, you may write the admin role to a DAO's voting contract address (e.g., 0x323A76393544d5ecca80cd6ef2A560C6a395b7E3) upon deployment.
- Sub-step 1: Deploy the contract using a tool like Hardhat or Foundry.
- Sub-step 2: Call
grantRole(MINTER_ROLE, 0x...)from the admin account to authorize a minter. - Sub-step 3: Verify the role assignment by calling
hasRole(MINTER_ROLE, 0x...)– it should returntrue.
solidity// Example transaction data for granting a role (using ethers.js) const tx = await contract.grantRole(MINTER_ROLE, minterAddress); await tx.wait();
Tip: Consider using a deployment script to atomically set up all roles, avoiding manual steps.
Manage Roles and Monitor Events
Establish processes for ongoing role administration and transparency.
Detailed Instructions
Onchain access control requires active management. Implement off-chain monitoring for the RoleGranted and RoleRevoked events to maintain an audit trail. Plan for role rotation and key loss scenarios; using a multi-signature wallet as the default admin adds resilience. To revoke a compromised key, call revokeRole. For complex systems, consider implementing a role proposal and voting mechanism within a DAO framework. Regularly review and prune unnecessary permissions to adhere to the principle of least privilege.
- Sub-step 1: Set up an indexer or event listener to log all role changes from your contract.
- Sub-step 2: Schedule periodic reviews of active roles and their associated addresses.
- Sub-step 3: Test the
renounceRolefunction flow if users should be able to voluntarily give up a role.
solidity// Example: A function allowing a role member to renounce their own role function renounceMinterRole() public { renounceRole(MINTER_ROLE, msg.sender); }
Tip: Keep a secure, off-chain record of all admin keys and their associated roles.
Implementing Offchain Access Control
Process overview for setting up a secure, scalable offchain authorization system.
Design the Authorization Schema
Define the roles, permissions, and data structures for your access control system.
Detailed Instructions
Define the authorization schema that maps user identifiers to permissions. This typically involves creating a data model, often using a JSON structure, that specifies roles (e.g., admin, editor, viewer) and their associated capabilities for specific resources. The schema should be versioned to allow for future updates.
- Sub-step 1: Identify all resources (e.g., API endpoints, data fields) that require protection.
- Sub-step 2: Enumerate the distinct actions (e.g., read, write, delete) possible on each resource.
- Sub-step 3: Group actions into logical roles and assign them to user identifiers like public keys or decentralized identifiers (DIDs).
json{ "schemaVersion": "1.0", "roles": { "admin": { "permissions": ["api:write", "user:manage"] }, "user": { "permissions": ["api:read"] } } }
Tip: Keep the schema simple initially; you can introduce hierarchical roles (RBAC) or attribute-based access control (ABAC) as complexity grows.
Set Up a Secure Signing Service
Create a backend service to issue and verify signed authorization tokens.
Detailed Instructions
Implement a secure, non-custodial signing service (like a backend API) that holds a private key. This service's sole responsibility is to validate user requests against your authorization schema and issue signed tokens, such as JWTs or custom EIP-712 typed data signatures. The service must never expose the signing key.
- Sub-step 1: Generate a dedicated Ethereum private key (e.g.,
0x...) for the service and store it securely using a secrets manager or HSM. - Sub-step 2: Expose an authenticated endpoint (e.g.,
POST /auth/token) where users can request a token for a specific action. - Sub-step 3: In the endpoint logic, verify the requesting user's identity (e.g., via a session or a signature) and check their permissions in your database against the requested resource.
javascript// Example of signing an EIP-712 permit for an offchain action const domain = { name: 'MyApp', version: '1', chainId: 1 }; const types = { Permit: [{ name: 'action', type: 'string' }, { name: 'resource', type: 'string' }] }; const value = { action: 'api:write', resource: 'userProfile:123' }; const signature = await signer._signTypedData(domain, types, value);
Tip: Use short-lived tokens (e.g., 5-15 minutes) to limit the impact of a compromised token and implement a revocation list.
Integrate Token Verification in Gateway
Add middleware to your application gateway or API to validate tokens before processing requests.
Detailed Instructions
Protect your application's entry points by adding a verification middleware. This component intercepts incoming requests, extracts the authorization token from the Authorization header, and validates its signature and claims before allowing the request to proceed to business logic.
- Sub-step 1: In your API gateway (e.g., Express.js middleware, NGINX auth_request), parse the token. For a JWT, verify its signature using the service's public key. For an EIP-712 signature, recover the signer address using
ecrecover. - Sub-step 2: Validate the token's claims, ensuring the
actionandresourcematch the current HTTP request's method and path. Check the token's expiry timestamp. - Sub-step 3: If validation fails, return a
403 ForbiddenHTTP status. On success, attach the user's identifier and permissions to the request object for downstream use.
javascript// Express.js middleware example for EIP-712 const { recoverTypedSignature } = require('@metamask/eth-sig-util'); async function verifyAuth(req, res, next) { const sig = req.headers['authorization']?.split('Bearer ')[1]; const recoveredAddr = recoverTypedSignature({ data: typedData, signature: sig, version: 'V4' }); if (recoveredAddr !== trustedSignerAddr) return res.sendStatus(403); // Further validate claims... next(); }
Tip: Cache successful verifications for a short period (e.g., 1 second) to reduce computational overhead on high-traffic endpoints.
Implement Permission Revocation and Updates
Establish mechanisms to instantly revoke access or update user permissions.
Detailed Instructions
Because offchain systems lack the automatic state finality of a blockchain, you need explicit processes for permission revocation and updates. This requires maintaining a real-time source of truth for user status, such as a database with a revoked_tokens table or a fast key-value store for blocklists.
- Sub-step 1: Create an admin endpoint to revoke a user's permissions. This should add the user's identifier or the specific token's JTI (JWT ID) to a revocation list.
- Sub-step 2: Modify your verification middleware (from Step 3) to query this revocation list on every request. A Redis store with TTL is ideal for this.
- Sub-step 3: Design a listener for onchain events (e.g., a
RoleRevokedevent from a related smart contract) that triggers an update to your offchain permission database, ensuring cross-system consistency.
sql-- Example schema for a token revocation table CREATE TABLE revoked_tokens ( id SERIAL PRIMARY KEY, user_address CHAR(42) NOT NULL, token_id UUID, -- For JWT revocation revoked_at TIMESTAMP NOT NULL DEFAULT NOW() ); CREATE INDEX idx_user ON revoked_tokens(user_address);
Tip: For immediate global revocation, consider using a short token expiry combined with a high-frequency check against a centralized but authenticated revocation endpoint.
Audit and Monitor Access Logs
Implement logging and monitoring to detect unauthorized access attempts and audit permission usage.
Detailed Instructions
Establish comprehensive audit logging for all authorization events. Logs should be immutable (e.g., written to a separate, append-only service) and include sufficient detail to reconstruct events and identify policy violations or attack patterns.
- Sub-step 1: Instrument your signing service and API gateway to log key events: token issuance (including requester ID and granted permissions), successful verifications, and all failed authorization attempts.
- Sub-step 2: Structure log data with consistent fields: timestamp, user identifier (or recovered address), resource accessed, action attempted, and decision (allow/deny).
- Sub-step 3: Set up real-time alerts for anomalous patterns, such as a high rate of
403errors from a single IP address or repeated attempts to access admin endpoints.
javascript// Example audit log entry structure const auditLog = { timestamp: new Date().toISOString(), event: 'API_ACCESS', user: '0x742d35Cc6634C0532925a3b844Bc9e...', resource: '/api/v1/admin/users', action: 'DELETE', decision: 'DENY', reason: 'Insufficient permissions', ip: req.ip }; // Send to logging service
Tip: Consider forwarding critical audit logs to an onchain log, like emitting an event to a low-cost L2, to create a tamper-resistant record for compliance.
Use Case Analysis
Technical Implementation
Onchain access control is implemented directly within smart contracts, typically using modifiers and role-based systems. For example, OpenZeppelin's AccessControl library provides a standard for managing permissions with functions like grantRole and hasRole. This model ensures that authorization logic is transparent and immutable, enforced by the blockchain's consensus. Offchain access control delegates permission checks to external services, such as a centralized API or a decentralized oracle network like Chainlink Functions. The smart contract makes an external call, and the transaction proceeds only upon receiving a verified success response. This separation allows for more complex, updatable logic without costly contract redeployments.
Key Considerations
- Gas Costs: Onchain checks add gas overhead for every transaction, while offchain models shift computational cost to the verifier.
- Upgradability: Offchain logic can be modified without changing the core contract, whereas onchain roles require a governance proposal or a proxy upgrade.
- Security Model: Onchain control inherits blockchain finality; offchain models introduce a trust assumption in the external verifier's correctness and liveness.
Example: Hybrid Approach
A common pattern is to use offchain checks for complex KYC/AML validation via an oracle, while retaining onchain roles for core administrative functions like adjusting fees or pausing the contract.
Security Considerations and Risks
The core risk is centralization of trust. The system's security is only as strong as the offchain server, API, or database managing permissions. This creates a single point of failure vulnerable to DDoS attacks, server downtime, or malicious admin actions. For example, if an NFT project's mint whitelist is stored offchain, a compromised admin key could alter the list, allowing unauthorized mints. Furthermore, users must trust the operator to correctly implement and enforce the rules, as there is no cryptographic proof onchain. This model often fails under high load, with API latency causing transaction failures for legitimate users.