Introduction
We express our gratitude to the Europeum team for the collaborative engagement that enabled the execution of this Smart Contract Security Assessment.
The EBSI Trusted Schemas Registry (TSR) is an on-chain component of the European Blockchain Services Infrastructure that stores, versions, and serves JSON schema definitions and their associated metadata for the EBSI ecosystem. Deployed as an upgradeable Solidity contract on an EBSI EVM-compatible ledger, it maintains an append-only history of schema revisions keyed by SHA2-256 content hashes, while delegating all write authorization to an external policy registry.
Document | |
|---|---|
| Name | Smart Contract Code Review and Security Analysis Report for Europeum |
| Audited By | Kornel Światłowski |
| Approved By | Kerem Solmaz |
| Website | https://europeum.eu/→ |
| Changelog | 24/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
- Kerem Solmaz
- Website
- https://europeum.eu/→
- Changelog
- 24/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 not detailed.
Each external function in both files carries a short
@devone-liner describing what it does (e.g., "insertSchema enables to register new schema").No in-scope documentation describes the roles model, despite authorization being delegated via the
TSR:insertSchema/TSR:updateSchema/TSR:updateMetadataattribute checks.Content-addressing (
sha256\-derived IDs) and "latest revision" semantics inSchemaLib.solare non-obvious design choices left undocumented.Use cases, edge cases, and end-to-end flows between
SchemaRegistryandSchemaLibare not described.The
@devNatSpec provides a high-level functional picture, which lifts the score but stops at surface behavior.
Technical description is not detailed.
Neither file documents validation rules, invariants, or upgrade/storage constraints (
__gap,_version) as a technical specification.NatSpec has no
@paramor@returntags on any function in either file.Revert conditions and the pagination return tuple
(items, total, howMany, prev, next)are undocumented.The existing NatSpec covers the code at a surface level and is beneficial, but is insufficient to raise the technical score.
Code quality
The code duplicates commonly known contracts instead of reusing them.
The development environment is properly configured and runs locally with no external keys.
Formatting, naming, and the upgradeable-contract hygiene in
SchemaRegistry.sol(_disableInitializers(),initializerguard, reserved__gap) reflect solid engineering discipline.
Test coverage
Code coverage of the project is 99.07 % (statement coverage).
Deployment and basic user interactions are covered with tests.
Negative case coverage is present.
System Overview
The system is composed of the SchemaRegistry contract and its supporting SchemaLib library, operating over a shared Schemas storage struct whose layout is declared in the out-of-scope ISchemaRegistry interface. SchemaRegistry is written as an upgradeable implementation intended for a beacon-style proxy deployment: it inherits OpenZeppelin Initializable, disables initializers in its constructor through _disableInitializers() (annotated with /// @custom:oz-upgrades-unsafe-allow constructor), exposes an initialize function guarded by the initializer modifier, and reserves a uint256[50] __gap for future storage additions. Introspection support for proxy tooling is provided through ImplementationInitSelector, whose getImplementationInitSelector is overridden to return the initialize selector.
Access control is not implemented locally but is delegated to an external IPolicyRegistry contract, whose address is fixed at initialization through the _tprAddress parameter and stored in policyRegistryContract. Every state-mutating entry point (insertSchema, updateSchema, updateMetadata) gates execution on a checkPolicy call carrying a TSR-scoped attribute string (for example "TSR:insertSchema") and the calling msg.sender, reverting when the caller lacks the required attribute. Read entry points are unrestricted and, where they return lists, are paginated: getSchemaIds, getSchemaRevisionIds, and getSchemaRevisionMetadataIds enforce page and page-size bounds (page size capped at 50) and compute slices through the Pagination library's paginate helper.
Business logic and all storage mutation are factored into SchemaLib, attached to the Schemas struct via using SchemaLib for Schemas, keeping the registry contract a thin authorization-and-pagination layer over the library. Schemas and metadata are content-addressed: revision identifiers and metadata identifiers are derived as SHA2-256 hashes of their respective byte payloads, with revision and metadata byte content stored once per hash in schemaRevisionStore and revisionMetadataStore, and ordered identifier lists tracked in schemaIdToRevisionIds and schemaIdRevisionIdToMetadataIds. The library emits SchemaInserted, SchemaUpdated, and MetadataUpdated events on each mutation and provides the corresponding read helpers consumed by the registry. Cross-contract dependencies comprise the external PolicyRegistry (via IPolicyRegistry) for authorization and shared bootstrap utilities (Pagination, ImplementationInitSelector) from the monorepo.
Files in Scope
SchemaRegistry.sol — The upgradeable registry entry-point contract that initializes the external policy registry reference, enforces per-action authorization through
checkPolicy, and exposes schema lifecycle operations (insertSchema,updateSchema,updateMetadata) alongside paginated and direct read functions (getSchemaIds,getLatestSchemaRevision,getSchemaRevisionIds,getSchemaRevision,getLatestSchemaRevisionMetadataByRevisionId,getSchemaRevisionMetadataIds,getSchemaRevisionMetadataByMetadataId), delegating storage logic to SchemaLib.SchemaLib.sol — A library operating on the
Schemasstorage struct that implements content-addressed schema and metadata storage using SHA2-256 hashing, handles registration and versioning throughinsertSchema,updateSchema, andupdateMetadata, provides read helpers such asgetLatestSchemaRevision,getSchemaRevision,getLatestSchemaRevisionMetadataByRevisionId, andgetSchemaRevisionMetadataByMetadataId, and emits theSchemaInserted,SchemaUpdated, andMetadataUpdatedevents.
Privileged roles
SchemaRegistrysol
Deployer / Initializer: Bootstraps the proxy by setting the policy registry dependency; gated by the
initializermodifier so it can execute only once.Can call
initializeto setpolicyRegistryContractto the provided non-zero_tprAddress, thereby defining which external policy registry governs all subsequent access checks.
TSR:insertSchema attribute holder: Entity that passes
policyRegistryContract.checkPolicy("TSR:insertSchema", msg.sender).Can call
insertSchemato register a new schema together with its first revision and metadata.
TSR:updateSchema attribute holder: Entity that passes
policyRegistryContract.checkPolicy("TSR:updateSchema", msg.sender).Can call
updateSchemato append a new schema revision and associated metadata to an already-registered schema.
TSR:updateMetadata attribute holder: Entity that passes
policyRegistryContract.checkPolicy("TSR:updateMetadata", msg.sender).Can call
updateMetadatato append new metadata to an existing schema revision.
Note: all get* view functions (getSchemaIds, getLatestSchemaRevision, getSchemaRevisionIds, getSchemaRevision, getLatestSchemaRevisionMetadataByRevisionId, getSchemaRevisionMetadataIds, getSchemaRevisionMetadataByMetadataId) and getImplementationInitSelector are unrestricted and callable by any address.
SchemaLibsol
No independently-enforced access control is present. The library functions are consumed via
using SchemaLib for Schemasand operate on the caller contract's storage; the only guards are input/state validationrequirechecks (non-empty inputs, registration existence), with all authorization enforced upstream by the policy gates in SchemaRegistry.sol.
Potential Risks
Dependency on External Logic and Availability: Core logic in SchemaRegistry relies on external contracts and utilities that are not in scope and are not standard well-audited libraries, specifically IPolicyRegistry (imported from @ebsiint-sc/policies-registry), the Pagination library, and the ImplementationInitSelector base (both from @ebsiint-sc/bootstrap). The initialize function stores the policy registry address in policyRegistryContract, and every mutating path (insertSchema, updateSchema, updateMetadata) begins with a require on policyRegistryContract.checkPolicy and reverts otherwise, while getSchemaIds, getSchemaRevisionIds, and getSchemaRevisionMetadataIds all route through Pagination.paginate. A defect or compromise in any of these unaudited dependencies would propagate directly into the registry's behavior. Furthermore, no in-scope function can change policyRegistryContract after initialize, so if the configured policy registry address is wrong, self-destructed, paused, or returns unexpected values, all schema registration and update functionality becomes permanently unusable.
Single Entity Upgrade Authority: SchemaRegistry is an upgradeable implementation (it defines initialize guarded by initializer, disables initializers in the constructor, reserves a __gap storage slot, and exposes getImplementationInitSelector via ImplementationInitSelector) intended to sit behind an out-of-scope beacon proxy. Whichever entity controls that beacon can replace the logic of every proxy pointing to it in a single action, and the in-scope code contains no on-chain multi-signature, governance, or timelock constraint on that authority. Compromise of the beacon controller would allow arbitrary rewriting of all schema-registry behavior and storage interpretation.
Fully Delegated Authorization: SchemaRegistry implements no local access-control model (no owner, roles, pause, or allowlist); instead every mutating function delegates authorization to the external policy registry via string-keyed attribute checks — checkPolicy("TSR:insertSchema", msg.sender), checkPolicy("TSR:updateSchema", msg.sender), and checkPolicy("TSR:updateMetadata", msg.sender). The correctness of who may register or mutate schemas is therefore entirely a function of out-of-scope policy configuration and the referenced attribute strings, which are not validated on-chain. Misconfiguration of these attributes, or a permissive or compromised policy registry, would allow unauthorized parties to insert or overwrite schema revisions and metadata, and no in-scope mechanism can restrict or halt this.
Hash-Derived Identifiers and Silent Deduplication: Revision and metadata identifiers are content-addressed as schemaRevisionId = sha256(schema) and metadataId = sha256(metadata) in both insertSchema and updateSchema, and the backing stores are written only when empty (ss.schemaRevisionStore[schemaRevisionId].length 0 and the equivalent guard for revisionMetadataStore). As a result, identical schema or metadata bytes submitted under different schemaId values collapse to a single shared storage slot, and getSchemaRevision and getSchemaRevisionMetadataByMetadataId resolve bytes from a store keyed only by hash rather than scoped per schema. Any true SHA2-256 collision, or a re-submission whose first-stored bytes differ from a caller's assumption, would cause the registry to serve the first-written content while the revision-id list keeps appending, since updateSchema pushes the computed schemaRevisionId to schemaIdToRevisionIds without deduplicating the list itself.
Full Payload Storage Without Bounds or Deletion: insertSchema, updateSchema, and updateMetadata accept arbitrary-length bytes calldata schema and metadata and persist the entire payloads on-chain in schemaRevisionStore and revisionMetadataStore, with no upper size limit enforced beyond non-emptiness (require(schema.length > 0), require(metadata.length > 0)). Each updateSchema and updateMetadata call also unconditionally appends to schemaIdToRevisionIds and schemaIdRevisionIdToMetadataIds, and the contract exposes no delete or prune function, so per-schema revision and metadata lists grow monotonically. Large payloads impose high and potentially prohibitive gas costs on writers, and unbounded list growth increases the total pages returned through the 50-item-capped pagination while permanently consuming state.
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-1815 | Duplicate Revision And Metadata IDs Appended Without List-Level Deduplication | fixed | Low | |
| F-2026-1813 | Missing Membership Check In getSchemaRevisionMetadataByMetadataId Returns Cross-Provenance Metadata | fixed | Low | |
| F-2026-1817 | initialize Can Be Declared external | fixed | Observation | |
| F-2026-1817 | Repeated Validation And Store Logic Should Be Extracted Into Shared Modifiers And Internal Functions | fixed | Observation | |
| F-2026-1817 | Misleading schemaIdHash Event Field Name | fixed | Observation | |
| F-2026-1815 | Custom Errors Can Be Used for Gas Efficiency | fixed | Observation | |
| F-2026-1815 | Floating Pragma | fixed | Observation | |
| F-2026-1815 | Registry Lists And Blob Stores Grow Unbounded With No Pruning Mechanism | accepted | Observation | |
| F-2026-1814 | Inconsistent Input Validation Across Schema Revision Getters | 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.