Introduction
We express our gratitude to the Europeum team for the collaborative engagement that enabled the execution of this Smart Contract Security Assessment.
The in-scope contracts form the Policy Registry module of the EBSI (European Blockchain Services Infrastructure) on-chain identity and access stack, an EVM-based Solidity system. The registry stores named policy definitions and assigns them as attributes to user addresses, allowing other contracts to query whether a given user holds a given policy via checkPolicy.
Document | |
|---|---|
| Name | Smart Contract Code Review and Security Analysis Report for Europeum |
| Audited By | Kornel Światłowski |
| Approved By | Ivan Bondar |
| Website | https://europeum.eu/→ |
| Changelog | 28/07/2026 - Preliminary Report |
| 03/08/2026 - Final Report | |
| 13/08/2026 - Updated Final Report | |
| Platform | Private Chain |
| Language | Solidity |
| Tags | Registry, Storage, Upgradable |
| Methodology | https://docs.hacken.io/methodologies/smart-contracts→ |
Document
- Name
- Smart Contract Code Review and Security Analysis Report for Europeum
- Audited By
- Kornel Światłowski
- Approved By
- Ivan Bondar
- Website
- https://europeum.eu/→
- Changelog
- 28/07/2026 - Preliminary Report
- 03/08/2026 - Final Report
- 13/08/2026 - Updated Final Report
- Platform
- Private Chain
- Language
- Solidity
- Tags
- Registry, Storage, Upgradable
Review Scope | |
|---|---|
| Repository | https://gitlab.com/europeum/public/core-services→ |
| Commit | 89bb63b74e88c7e3b1602f4801d682e634b6a521 |
| Final Commit | a64e3139c3a139da5bc3ff81c4d25fb35212c7f2 |
| Updated Final Commit | d1535bacb0ee03fc987339c58324b34c3db14687 |
Review Scope
- Commit
- 89bb63b74e88c7e3b1602f4801d682e634b6a521
- Final Commit
- a64e3139c3a139da5bc3ff81c4d25fb35212c7f2
- Updated Final Commit
- d1535bacb0ee03fc987339c58324b34c3db14687
Audit Summary
The system users should acknowledge all the risks summed up in the risks section of the report
Documentation quality
Functional requirements are present, but only at a high level
PolicyRegistryfunction NatSpec@noticedescribes behavior, both scopes (global vs. per-target), and roles.Access-control intent (
onlyOperatorOrProxyOwner,checkPolicyfallback) explained inline.No whitepaper, no functional-requirements doc.
No use cases, user flows, or interaction descriptions.
Subtle
checkPolicyscope logic (target =msg.sender) not documented for integrators.
Technical description is detailed.
Test suite present (
policy,userattr,scoped-delegationspecs) plus coverage script.PolicyRegistryNatSpec has@param/@returnand@devvalidation rules.No run instructions in README.
No standalone technical spec (architecture, deployment, storage layout).
Code quality
Modern Solidity version 0.8.26 (not outdated).
Correct OZ primitives,
_disableInitializers(),__gapfor upgrades.Consistent custom errors and events on all state changes.
Dev environment configured (Hardhat 3, Solidity 0.8.26, optimizer,
solhint, eslint).Runs locally, no external keys (default test mnemonic).
Test coverage
Code coverage of the project is 94.03% (branch coverage).
Deployment and basic user interactions are covered with tests.
Negative case coverage is missing.
System Overview
The module centers on a single upgradeable contract, PolicyRegistry, deployed behind a versioned beacon proxy. It inherits AccessControlUpgradeable for role management, Initializable for proxy-based initialization, and ImplementationInitSelector from the external @ebsiint-sc/bootstrap package to expose the initializer selector consumed by the beacon deployment machinery. State is held in OpenZeppelin EnumerableMap and EnumerableSet structures: policy definitions are keyed by an incremental policyCount id and by a keccak256 hash of the policy name, while user assignments are tracked as composite-key sets over the tuple (user, targetContract, policyName).
Two conceptual entities are managed. Policy definitions are created and mutated by operators through insertPolicy, updatePolicy, activatePolicy, and deactivatePolicy, each policy carrying a name, a description, and an active status flag. User attribute assignments bind a user to one or more policy names within a scope: a scope of address(0) denotes an EBSI global policy, while a non-zero targetContract denotes a policy that applies only when that specific contract queries the registry. Assignments are created and removed through insertUserAttributes, insertScopedUserAttributes, deleteUserAttributes, and deleteScopedUserAttributes.
Access enforcement uses two scopes. Policy definition management and global attribute assignment are gated by OPERATOR_ROLE. Scoped attribute assignment is additionally permitted to the proxy owner of the target contract: the onlyOperatorOrProxyOwner modifier calls proxyOwner on the target through the IVersionedBeaconProxy interface and grants access when it returns the caller. The read path checkPolicy resolves a policy by id or name, then treats msg.sender as the target contract and returns true when the user holds the policy either for that specific target scope or for the global scope, giving global policies precedence-free fallback behavior. Read functions that enumerate policies, policy names, and users are paginated through the CustomPagination library, which wraps the external Pagination library from the bootstrap package and returns 1-based id slices with page metadata. The contract reserves a __gap storage slot array for upgrade-safe layout extension.
Files in Scope
PolicyRegistry.sol — Upgradeable registry that defines, mutates, and queries named policies and binds them to users as global or per-target-contract attributes. Exposes operator-gated write functions for policy lifecycle and attribute assignment, an
onlyOperatorOrProxyOwnerpath for scoped assignment, and paginated read functions including thecheckPolicyauthorization query used by other contracts.CustomPagination.sol — Library that paginates a range of incremental 1-based ids given a total count, page number, and page size. Delegates page-boundary math to the external
Pagination.getPaginationParametersfunction and returns the id slice together with total, page-count, previous, and next metadata.
Privileged roles
PolicyRegistrysol
DEFAULT_ADMIN_ROLE: Admin role for all roles in the contract; granted to the initializer caller. Can grant and revoke OPERATOR_ROLE and DEFAULT_ADMIN_ROLE via the inherited
grantRoleandrevokeRole(inherited from AccessControlUpgradeable).OPERATOR_ROLE: Manages the policy catalog and global user attribute assignments; granted to the initializer caller.
Can call
insertPolicyto create a new named policy definition.Can call
updatePolicy(by name or id) to change a policy description.Can call
activatePolicyanddeactivatePolicy(by name or id) to toggle a policy's active status.Can call
insertUserAttributesanddeleteUserAttributesto assign or remove global-scope policy attributes for a user.Can call
insertScopedUserAttributesanddeleteScopedUserAttributesfor any target contract (includingaddress(0)).
Target contract proxy owner: Any address returned by
proxyOwneron a target contract that implements IVersionedBeaconProxy, as enforced by theonlyOperatorOrProxyOwnermodifier.Can call
insertScopedUserAttributesanddeleteScopedUserAttributesfor the specific target contract it owns.
Potential Risks
Scope limited to two of many repository contracts: The audit scope covers only PolicyRegistry and CustomPagination, whereas the repository contains numerous additional deployable contracts, including the beacon proxy and beacon implementation that host and upgrade this registry. Vulnerabilities in those out-of-scope contracts, particularly in the proxy and beacon that control the registry's storage and upgrade path, could compromise the registry despite its own correctness.
Broad operator authority over policy and attribute state: A single OPERATOR_ROLE controls the entire policy lifecycle and all global and scoped user attribute assignments through insertPolicy, updatePolicy, activatePolicy, deactivatePolicy, insertUserAttributes, insertScopedUserAttributes, deleteUserAttributes, and deleteScopedUserAttributes. A compromised operator key could deactivate policies relied upon by downstream contracts or assign and revoke arbitrary user attributes, directly altering authorization decisions returned by checkPolicy.
No timelock on authorization-critical operations: Policy activation, deactivation, and attribute assignment execute immediately with no delay or review window. Because checkPolicy is consumed by other contracts for access decisions, an immediate deactivatePolicy or deleteScopedUserAttributes call can revoke access in the same block without any buffer for review or reversal.
Concentration of initial roles in the deployer: initialize grants both DEFAULT_ADMIN_ROLE and OPERATOR_ROLE to msg.sender, concentrating role administration and operational control in the initializing account. The safety of the private key backing this account cannot be verified during a smart contract audit, and the code contains no on-chain requirement that these roles be held by a multi-signature wallet or governance contract.
Flexibility and Risk in Contract Upgrades: The project's contracts are upgradable, allowing the administrator to update the contract logic at any time. While this provides flexibility in addressing issues and evolving the project, it also introduces risks if upgrade processes are not properly managed or secured, potentially allowing for unauthorized changes that could compromise the project's integrity and security.
Absence of Upgrade Window Constraints: The contract suite allows for immediate upgrades without a mandatory review or waiting period, increasing the risk of rapid deployment of malicious or flawed code, potentially compromising the system's integrity and user assets.
Authorization outcome depends on the calling contract's identity: checkPolicy derives the target scope from msg.sender rather than an explicit parameter, so target-scoped policy checks are correct only when the querying contract is itself the intended target of the assignment. If a policy is assigned for one target contract but a different contract performs the checkPolicy call, the target-specific assignment is not matched and only the global-scope assignment can satisfy the check, which can cause access decisions to diverge from operator intent.
Report Modification: This report was modified on 13/08/2026 at the client’s request to update its original content by changing the commit hash from a64e3139c3a139da5bc3ff81c4d25fb35212c7f2 to d1535bacb0ee03fc987339c58324b34c3db14687. The files in scope were compared between these two commits and found to be identical. While these changes aim to align the report with the most current information provided by the client, it is important to note that modifying previously published content may affect the integrity and continuity of the original audit findings. Hacken has reviewed the modifications to confirm they reflect only the requested updates, but any future changes involving substantial updates or new code commits should be accompanied by a re-assessment to ensure no new risks compromise the security posture.
Findings
Code ― | Title | Status | Severity | |
|---|---|---|---|---|
| F-2026-1871 | getUserAttributes Returns Duplicate Names Across Scopes Despite Documented Uniqueness | fixed | Observation | |
| F-2026-1871 | Paginated Getters Return Clamped prev and next Values That Contradict the Documented Zero Sentinel | fixed | Observation | |
| F-2026-1871 | Repeated Validation and Hashing Logic Duplicated Across Functions Instead of Shared Helpers or Modifiers | fixed | Observation | |
| F-2026-1870 | Unconditional globalKey Hash Computation in _checkPolicy | fixed | Observation | |
| F-2026-1870 | Loop Counters Use Post-Increment and Uncached Array Length | fixed | Observation | |
| F-2026-1870 | Storage policyCount Read Every Iteration in getPolicyNamesByIds | fixed | Observation | |
| F-2026-1870 | Initializer Grants Admin and Operator Roles to a Single Account and Removes Separation of Duties | fixed | Observation | |
| F-2026-1869 | initialize Can Be Declared external | fixed | Observation | |
| F-2026-1869 | Unconditional Set Insertion In _insertUserAttribute | fixed | Observation | |
| F-2026-1869 | Redundant Keccak256 Computation In _insertUserAttribute | fixed | Observation |
Appendix 1. Definitions
Severities
When auditing smart contracts, Hacken is using a risk-based approach that considers Likelihood, Impact, Exploitability and Complexity metrics to evaluate findings and score severities.
Reference on how risk scoring is done is available through the repository in our Github organization:
Severity | Description |
|---|---|
Critical | Critical vulnerabilities are usually straightforward to exploit and can lead to the loss of user funds or contract state manipulation. |
High | High vulnerabilities are usually harder to exploit, requiring specific conditions, or have a more limited scope, but can still lead to the loss of user funds or contract state manipulation. |
Medium | Medium vulnerabilities are usually limited to state manipulations and, in most cases, cannot lead to asset loss. Contradictions and requirements violations. Major deviations from best practices are also in this category. |
Low | Major deviations from best practices or major Gas inefficiency. These issues will not have a significant impact on code execution. |
Severity
- Critical
Description
- Critical vulnerabilities are usually straightforward to exploit and can lead to the loss of user funds or contract state manipulation.
Severity
- High
Description
- High vulnerabilities are usually harder to exploit, requiring specific conditions, or have a more limited scope, but can still lead to the loss of user funds or contract state manipulation.
Severity
- Medium
Description
- Medium vulnerabilities are usually limited to state manipulations and, in most cases, cannot lead to asset loss. Contradictions and requirements violations. Major deviations from best practices are also in this category.
Severity
- Low
Description
- Major deviations from best practices or major Gas inefficiency. These issues will not have a significant impact on code execution.
Potential Risks
The "Potential Risks" section identifies issues that are not direct security vulnerabilities but could still affect the project’s performance, reliability, or user trust. These risks arise from design choices, architectural decisions, or operational practices that, while not immediately exploitable, may lead to problems under certain conditions. Additionally, potential risks can impact the quality of the audit itself, as they may involve external factors or components beyond the scope of the audit, leading to incomplete assessments or oversight of key areas. This section aims to provide a broader perspective on factors that could affect the project's long-term security, functionality, and the comprehensiveness of the audit findings.
Appendix 2. Scope
The scope of the project includes the following smart contracts from the provided repository:
Scope Details | |
|---|---|
| Repository | https://gitlab.com/europeum/public/core-services→ |
| Commit | 89bb63b74e88c7e3b1602f4801d682e634b6a521 |
| Final Commit | a64e3139c3a139da5bc3ff81c4d25fb35212c7f2 |
| Updated Final Commit | d1535bacb0ee03fc987339c58324b34c3db14687 |
| Whitepaper | - |
| Requirements | NatSpec |
| Technical Requirements | NatSpec |
Scope Details
- Commit
- 89bb63b74e88c7e3b1602f4801d682e634b6a521
- Final Commit
- a64e3139c3a139da5bc3ff81c4d25fb35212c7f2
- Updated Final Commit
- d1535bacb0ee03fc987339c58324b34c3db14687
- Whitepaper
- -
- Requirements
- NatSpec
- Technical Requirements
- NatSpec
Assets in Scope
Appendix 3. Additional Valuables
Additional Recommendations
The smart contracts in the scope of this audit could benefit from the introduction of automatic emergency actions for critical activities, such as unauthorized operations like ownership changes or proxy upgrades, as well as unexpected fund manipulations, including large withdrawals or minting events. Adding such mechanisms would enable the protocol to react automatically to unusual activity, ensuring that the contract remains secure and functions as intended.
To improve functionality, these emergency actions could be designed to trigger under specific conditions, such as:
Detecting changes to ownership or critical permissions.
Monitoring large or unexpected transactions and minting events.
Pausing operations when irregularities are identified.
These enhancements would provide an added layer of security, making the contract more robust and better equipped to handle unexpected situations while maintaining smooth operations.
Frameworks and Methodologies
This security assessment was conducted in alignment with recognised penetration testing standards, methodologies and guidelines, including the NIST SP 800-115 – Technical Guide to Information Security Testing and Assessment →, and the Penetration Testing Execution Standard (PTES) →, These assets provide a structured foundation for planning, executing, and documenting technical evaluations such as vulnerability assessments, exploitation activities, and security code reviews. Hacken’s internal penetration testing methodology extends these principles to Web2 and Web3 environments to ensure consistency, repeatability, and verifiable outcomes.