Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

EVMAuth Core

GitHub Actions Workflow Status GitHub Repo stars

Overview

EVMAuth is an authorization state management system for APIs, MCP servers, and AI agents.

Purpose

EVMAuth was created to provide a simple, standardized way to manage persistent authorization without the overhead of developing and maintaining auth servers, databases, subscription management services, and payment systems.

Use Cases

  • Token-Gated Access Control: Grant access to digital services, APIs, and AI agents based on token ownership.
  • Subscription Management: Issue time-limited access tokens for subscription-based services.
  • Metered Usage: Issue tokens that can be redeemed for usage credits, allowing for pay-as-you-go models.
  • Licensing: Manage software licenses and entitlements through tokens.
  • Access Revocation: Instantly revoke access by burning tokens or freezing accounts.
  • Decentralized Identity: Leverage EVM networks for secure and verifiable identity management.

Features

Each EVMAuth token type can be configured for direct purchase, with any of the following:

  • Native currencies like ETH
  • Stablecoins like USDC and USDT
  • Any other ERC-20 tokens

Each accepted payment method can be priced independently. If no prices are set (i.e. all prices are zero), direct purchases are disabled for that token type.

EVMAuth tokens can also be issued to users programmatically and redeemed by the issuer at any time. This works well for metered usage models, where tokens are used as credits.

Each EVMAuth token type can be configured with a time-to-live (TTL). If the TTL is set (i.e. greater than zero), tokens of that type will automatically expire. This is ideal for subscription models.

All EVMAuth tokens are transferable by default, but each token type can be made non-transferable. This is useful for non-transferable licenses, identity tokens, or other situations where transfers are undesirable.

Compatibility

EVMAuth comes in two flavors: ERC-1155 and ERC-6909. Each version is fully compliant with its token standard and, as such, is fully compatible with existing wallets, apps, marketplaces, and other tools that support those standards.

EVMAuth contracts are upgradable by default, using OpenZeppelin's UUPSUpgradeable and ERC-7201 namespaced storage layout, and can be deployed to any EVM-compatible network, including Ethereum, Base, Tron, Polygon, Monad, and Radius.

Deployment

EVMAuth can be deployed on any EVM-compatible network (e.g. Ethereum, Base, Radius) using the scripts provided in the scripts/ directory.

When deploying the contract, you will need to specify the following parameters:

  • Initial transfer delay for the default admin role
  • Initial default admin address, for role management
  • Initial treasury address, for receiving revenue from direct purchases
  • Role grants, to set up access control during initialization (optional, can be done later)
  • Base token metadata URI (for ERC-1155) or contract URI (for ERC-6909) (optional, can be updated later)

Access Control

If you did not grant roles during initialization, only the default admin can assign roles after deployment, by calling:

  • grantRole(TOKEN_MANAGER_ROLE, address) for accounts that can configure tokens and token metadata
  • grantRole(ACCESS_MANAGER_ROLE, address) for accounts that can pause/unpause the contract and freeze accounts
  • grantRole(TREASURER_ROLE, address) for accounts that can modify the treasury address where funds are collected
  • grantRole(MINTER_ROLE, address)for accounts that can issue tokens to addresses
  • grantRole(BURNER_ROLE, address)for accounts that can deduct tokens from addresses

Token Configuration

An account with the TOKEN_MANAGER_ROLE should then create one or more new tokens by calling createToken with the desired configuration:

  • uint256 price: The cost to purchase one unit of the token; 0 means the token cannot be purchased directly; set to 0 to disable native currency purchases
  • PaymentToken[] erc20Prices: Array of PaymentToken structs, each containing ERC-20 token address and price; pass an empty array to disable ERC-20 token purchases
  • uint256 ttl: Time-to-live in seconds; 0 means the token never expires; set to 0 for non-expiring tokens
  • bool transferable: Whether the token can be transferred between accounts

An account with the TOKEN_MANAGER_ROLE can modify an existing token by calling updateToken(id, EVMAuthTokenConfig).

Token Standards

ERC-1155 vs ERC-6909

FeatureERC-1155ERC-6909
CallbacksRequired for each transfer to contract accounts; must return specific valuesRemoved entirely; no callbacks required
Batch OperationsIncluded in specification (batch transfers)Excluded from specification to allow custom implementations
Permission SystemSingle operator scheme: operators get unlimited allowance on all token IDsHybrid scheme: allowances for specific token IDs + operators for all tokens
Transfer MethodsBoth transferFrom and safeTransferFrom required; no opt-out for callbacksSimplified transfers without mandatory recipient validation
Transfer SemanticsSafe transfers with data parameter and receiver hooksSimple transfers without hooks
Interface ComplexityIncludes multiple features (callbacks, batching, etc.)Minimized to bare essentials for multi-token management
Recipient RequirementsContract recipients must implement callback functions with return valuesNo special requirements for contract recipients
Approval GranularityOperators only (all-or-nothing for entire contract)Granular allowances per token ID + full operators
Metadata HandlingURI-based metadata (typically off-chain JSON)On-chain name/symbol/decimals per token ID

When to Choose Which

Choose ERC-1155 when you:

  • Need NFT marketplace compatibility
  • Batch operations are important
  • Want receiver hook notifications
  • Prefer URI-based metadata

Choose ERC-6909 when you:

  • Need ERC-20-like semantics per token
  • Want granular approval control
  • Need on-chain token metadata
  • Prefer a simpler token transfer model
  • Want to extend the contract with custom features

Both versions of EVMAuth are fairly large. EVMAuth1155 is right near the limit, while EVMAuth6909 has a bit more headroom for expansion.

╭------------------------------+------------------+-------------------+--------------------+---------------------╮
| Contract                     | Runtime Size (B) | Initcode Size (B) | Runtime Margin (B) | Initcode Margin (B) |
+================================================================================================================+
| EVMAuth1155                  | 24,516           | 24,575            | 60                 | 24,577              |
|------------------------------+------------------+-------------------+--------------------+---------------------|
| EVMAuth6909                  | 22,247           | 22,306            | 2,329              | 26,846              |
╰------------------------------+------------------+-------------------+--------------------+---------------------╯

EVMAuth Contract Architecture

Overview

The EVMAuth contract system is designed with a modular, composable architecture that separates concerns into focused base contracts. This approach provides flexibility while maintaining a clean inheritance structure.

The architecture consists of:

  • Base Contracts: Modular components that handle specific functionality (e.g. access control, purchasing, token expiry)
  • Base EVMAuth Contract: Combines all base contracts into a unified authorization state management system
  • Token Standard Implementations: EVMAuth1155 and EVMAuth6909 extend EVMAuth with their respective token standards

Base Contracts Hierarchy

classDiagram
    class TokenAccessControl {
        <<abstract>>
        +DEFAULT_ADMIN_ROLE
        +UPGRADE_MANAGER_ROLE
        +ACCESS_MANAGER_ROLE
        +TOKEN_MANAGER_ROLE
        +MINTER_ROLE
        +BURNER_ROLE
        +TREASURER_ROLE
        +freezeAccount(account)
        +unfreezeAccount(account)
        +pause()
        +unpause()
        <<inherited from Pausable>>
        +paused()
        <<inherited from AccessControl>>
        +hasRole(role, account)
        +grantRole(role, account)
        +revokeRole(role, account)
        +renounceRole(role, account)
    }
    
    class AccountFreezable {
        <<abstract>>
        -frozenAccounts mapping
        +isFrozen(address)
        +frozenAccounts()
        #_freezeAccount(address)
        #_unfreezeAccount(address)
    }
    
    class TokenEnumerable {
        <<abstract>>
        -nextTokenId
        -tokenExists mapping
        +nextTokenID()
        +exists(id)
        #_claimNextTokenID()
    }
    
    class TokenTransferable {
        <<abstract>>
        -transferable mapping
        +isTransferable(id)
        #_setTransferable(id, bool)
    }
    
    class TokenEphemeral {
        <<abstract>>
        -ttl mapping
        -balanceRecords mapping
        +balanceOf(account, id)
        +balanceRecordsOf(account, id)
        +tokenTTL(id)
        +pruneBalanceRecords(account, id)
        #_setTTL(id, ttl)
        #_expiresAt(id)
        #_maxBalanceRecords()
        #_updateBalanceRecords(from, to, id, amount)
        #_addToBalanceRecords(account, id, amount, expiresAt)
        #_deductFromBalanceRecords(account, id, amount)
        #_transferBalanceRecords(from, to, id, amount)
    }
    
    class TokenPurchasable {
        <<abstract>>
        -treasury address
        -prices mapping
        -erc20Prices mapping
        +treasury()
        +tokenPrice(id)
        +tokenERC20Prices(id)
        +isAcceptedERC20PaymentToken(id, token)
        +purchase(id, amount)
        +purchaseFor(receiver, id, amount)
        +purchaseWithERC20(token, id, amount)
        +purchaseWithERC20For(receiver, token, id, amount)
        #_setPrice(id, price)
        #_setERC20Price(id, token, price)
        #_setERC20Prices(id, prices[])
        #_mintPurchasedTokens(to, id, amount)
    }
    
    class EVMAuth {
        <<abstract>>
        +EVMAuthTokenConfig struct
        +createToken(config)
        +updateToken(id, config)
        +tokenConfig(id)
        +setTreasury(address payable newTreasury)
        #_authorizeUpgrade(newImplementation)
    }
    
    AccessControlDefaultAdminRulesUpgradeable <|-- TokenAccessControl
    PausableUpgradeable <|-- TokenAccessControl
    AccountFreezable <|-- TokenAccessControl
    TokenAccessControl <|-- EVMAuth
    TokenEnumerable <|-- EVMAuth
    TokenTransferable <|-- EVMAuth
    TokenEphemeral <|-- EVMAuth
    TokenPurchasable <|-- EVMAuth
    UUPSUpgradeable <|-- EVMAuth

EVMAuth1155 Implementation

classDiagram
    class EVMAuth {
        <<abstract>>
        +DEFAULT_ADMIN_ROLE
        +UPGRADE_MANAGER_ROLE
        +ACCESS_MANAGER_ROLE
        +TOKEN_MANAGER_ROLE
        +MINTER_ROLE
        +BURNER_ROLE
        +TREASURER_ROLE
        +hasRole(role, account)
        +isFrozen(address)
        +frozenAccounts()
        +balanceOf(account, id)
        +balanceRecordsOf(account, id)
        +nextTokenID()
        +exists(id)
        +tokenConfig(id)
        +treasury()
        +tokenPrice(id)
        +tokenERC20Prices(id)
        +isAcceptedERC20PaymentToken(id, token)
        +tokenTTL(id)
        +isTransferable(id)
        +grantRole(role, account)
        +revokeRole(role, account)
        +renounceRole(role, account)
        +pause()
        +unpause()
        +freezeAccount(account)
        +unfreezeAccount(account)
        +createToken(config)
        +updateToken(id, config)
        +setTreasury(address payable newTreasury)
        +purchase(id, amount)
        +purchaseFor(receiver, id, amount)
        +purchaseWithERC20(token, id, amount)
        +purchaseWithERC20For(receiver, token, id, amount)
        +pruneBalanceRecords(account, id)
    }
    
    class ERC1155Upgradeable {
        +balanceOf(account, id)
        +balanceOfBatch(accounts[], ids[])
        +setApprovalForAll(operator, approved)
        +isApprovedForAll(account, operator)
        +safeTransferFrom(from, to, id, amount, data)
        +safeBatchTransferFrom(from, to, ids[], amounts[], data)
    }
    
    class ERC1155URIStorageUpgradeable {
        +uri(id)
        #_setURI(id, uri)
        #_setBaseURI(baseURI)
    }
    
    class EVMAuth1155 {
        +initialize(delay, admin, treasury, uri)
        +mint(to, id, amount, data)
        +mintBatch(to, ids[], amounts[], data)
        +burn(from, id, amount)
        +burnBatch(from, ids[], amounts[])
        +setBaseURI(uri)
        +setTokenURI(id, uri)
        +purchaseFor(recipient, id, amount)
        +purchaseWithERC20For(token, recipient, id, amount)
        +balanceRecordsOf(account, id)
        +pruneBalanceRecords(account, id)
        +exists(id)
        +supportsInterface(interfaceId)
        #_update(from, to, ids[], amounts[])
        #_mintPurchasedTokens(to, id, amount)
    }
    
    ERC1155Upgradeable <|-- ERC1155SupplyUpgradeable
    ERC1155Upgradeable <|-- ERC1155URIStorageUpgradeable
    ERC1155SupplyUpgradeable <|-- EVMAuth1155
    ERC1155URIStorageUpgradeable <|-- EVMAuth1155
    EVMAuth <|-- EVMAuth1155

EVMAuth6909 Implementation

classDiagram
     class EVMAuth {
        <<abstract>>
        +DEFAULT_ADMIN_ROLE
        +UPGRADE_MANAGER_ROLE
        +ACCESS_MANAGER_ROLE
        +TOKEN_MANAGER_ROLE
        +MINTER_ROLE
        +BURNER_ROLE
        +TREASURER_ROLE
        +hasRole(role, account)
        +isFrozen(address)
        +frozenAccounts()
        +balanceOf(account, id)
        +balanceRecordsOf(account, id)
        +nextTokenID()
        +exists(id)
        +tokenConfig(id)
        +treasury()
        +tokenPrice(id)
        +tokenERC20Prices(id)
        +isAcceptedERC20PaymentToken(id, token)
        +tokenTTL(id)
        +isTransferable(id)
        +grantRole(role, account)
        +revokeRole(role, account)
        +renounceRole(role, account)
        +pause()
        +unpause()
        +freezeAccount(account)
        +unfreezeAccount(account)
        +createToken(config)
        +updateToken(id, config)
        +setTreasury(address payable newTreasury)
        +purchase(id, amount)
        +purchaseFor(receiver, id, amount)
        +purchaseWithERC20(token, id, amount)
        +purchaseWithERC20For(receiver, token, id, amount)
        +pruneBalanceRecords(account, id)
    }
    
    class ERC6909Upgradeable {
        +balanceOf(account, id)
        +allowance(owner, spender, id)
        +isOperator(owner, spender)
        +transfer(to, id, amount)
        +transferFrom(from, to, id, amount)
        +approve(spender, id, amount)
        +setOperator(operator, approved)
    }
    
    class ERC6909MetadataUpgradeable {
        +name(id)
        +symbol(id)
        +decimals(id)
        #_setTokenMetadata(id, name, symbol, decimals)
    }
    
    class ERC6909ContentURIUpgradeable {
        +contractURI()
        +tokenURI(id)
        #_setContractURI(uri)
        #_setTokenURI(id, uri)
    }
    
    class EVMAuth6909 {
        +initialize(delay, admin, treasury, uri)
        +mint(to, id, amount)
        +burn(from, id, amount)
        +setContractURI(uri)
        +setTokenURI(id, uri)
        +setTokenMetadata(id, name, symbol, decimals)
        +purchaseFor(recipient, id, amount)
        +purchaseWithERC20For(token, recipient, id, amount)
        +balanceRecordsOf(account, id)
        +pruneBalanceRecords(account, id)
        +exists(id)
        +supportsInterface(interfaceId)
        #_update(from, to, id, amount)
        #_mintPurchasedTokens(to, id, amount)
    }
    
    ERC6909Upgradeable <|-- ERC6909MetadataUpgradeable
    ERC6909Upgradeable <|-- ERC6909ContentURIUpgradeable
    ERC6909MetadataUpgradeable <|-- EVMAuth6909
    ERC6909ContentURIUpgradeable <|-- EVMAuth6909
    EVMAuth <|-- EVMAuth6909

Base Contract Descriptions

TokenAccessControl

Provides role-based access control with six distinct roles, pausable functionality, and account freezing capabilities. Extends OpenZeppelin's AccessControlDefaultAdminRulesUpgradeable for secure admin transfer with time delays.

AccountFreezable

Enables freezing and unfreezing of individual accounts, preventing them from transferring or receiving tokens. Maintains a list of frozen accounts for transparency.

TokenEnumerable

Manages token ID generation and tracks which token IDs have been created. Provides a sequential ID system starting from 1.

TokenTransferable

Controls whether individual token types can be transferred between accounts. Each token ID can be configured as transferable or non-transferable.

TokenEphemeral

Implements time-to-live (TTL) functionality for tokens. Tokens with a TTL expire after the specified duration, with automatic pruning of expired balance records. Uses an efficient time-bucket system for gas optimization.

TokenPurchasable

Handles direct token purchases with both native currency and ERC-20 tokens. Supports per-token pricing in multiple currencies, with revenue sent to a configurable treasury address. Includes reentrancy protection for secure purchases.

EVMAuth

The main abstract contract that combines all base functionality and provides a unified interface for token configuration. Defines the EVMAuthTokenConfig structure that encapsulates price, ERC-20 prices, TTL, and transferability settings for each token type.

Key Architectural Decisions

  1. Upgradability: All contracts use the Universal Upgradeable Proxy Standard (UUPS) pattern for future improvements

  2. Security:

    • Role-based access control with time-delayed admin transfers
    • Pausable operations for emergency situations
    • Account freezing capabilities
    • Reentrancy protection on purchase functions
  3. Gas Optimization:

    • TTL implementation uses bounded arrays and time buckets for balance records
    • Automatic pruning of expired records, with manual pruning methods available
    • Efficient storage patterns for token properties, as defined in ERC-7201
  4. Flexibility:

    • Alternative purchase options (native and/or ERC-20 tokens)
    • Configurable token properties (price, TTL, transferability)
    • Support for both ERC-1155 and ERC-6909 token standards

SDKs & Libraries

EVMAuth provides the following SDKs and libraries for easy integration with applications and frameworks:

To request additional SDKs or libraries, create a new issue with the question label.

You can also use Foundry's cast command line tool to interact with deployed EVMAuth contracts, and reference this Cast Command Cheat Sheet which illustrates how to call each EVMAuth function.

Contributing

To contribute to this open source project, please follow the guidelines in the Contributing Guide.

License

The EVMAuth contract is released under the MIT License. See the LICENSE file for details.