Milestone 2 Completion Post — [2510] SSI Sandbox Rootstock Integration
Hello team! We are pleased to announce that Milestone 2 is complete.
As you know we have been posting regular updates with each of the tasks completed, now we present each deliverable, what we did, our results, and the supporting evidence.
After discussion, we’ll move to milestone 3 voting.
All feedback is welcome.
Summary
| # | Deliverable | Status | Highlights |
|---|---|---|---|
| 1 | Security Audit of Rootstock smart contracts and tests | Complete | CoinFabrik audit: 0 critical/high/medium findings. 1 low + 2 enhancements — all resolved. |
| 2 | Apply fixes and recommendations from the audit | Complete | Deletion mechanism added, Ownable2Step adopted, gas optimizations applied. Full test suite passes (18 tests). |
| 3 | Gas profiling | Complete | hardhat-gas-reporter integrated. Per-function gas benchmarks documented. |
| 4 | Symbolic execution (time-boxed) | Complete | Mythril analysis executed. 3 findings reported — all confirmed false positives (compiler-generated ABI decoder code). |
| 5 | Fuzz testing / property testing | Complete | Foundry fuzz + invariant testing. 14 tests (9 fuzz, 5 invariant), ~59,000 total fuzzer executions. All pass. 1 notable edge case surfaced. |
| 6 | Monitoring + alerting | Complete | Tenderly alerts configured for contract events. Successfully receiving alerts on new contract transactions. |
1. Security Audit
Auditor: CoinFabrik — specialized Web3 security firm with 500+ decentralization projects audited.
Scope: DidManifestRegistry.sol — our on-chain registry that maps DID keys (bytes32) to IPFS manifest CIDs.
Methodology: Manual code review covering arithmetic errors, reentrancy, DoS, gas abuse, function qualifier misuse, centralization, upgradeability, and more. Severity classification based on Immunefi v2.3.
Results
| Severity | Count |
|---|---|
| Critical | 0 |
| High | 0 |
| Medium | 0 |
| Low | 1 |
| Enhancements | 2 |
Finding LO-01 — No Manifest Deletion Mechanism (Low): The contract had no way to delete a mapping once set, forcing the owner to overwrite with a placeholder string.
Enhancement EN-01 — Two-step Ownership: Recommended adopting Ownable2Step to prevent accidental ownership transfers to uncontrolled addresses.
Enhancement EN-02 — Gas Optimizations: Recommended replacing string-based require with custom errors, using unchecked loop increments, and emitting a single batch event instead of per-item events.
Evidence:
- Audit final report (PDF) — Identity Audit 02-2026-final.pdf by CoinFabrik
- Initial report: 2026-02-06 (commit
680ca31) - Re-audit (fixes verified): 2026-03-19 (commit
94c02b1)
2. Apply Fixes and Recommendations from the Audit
All findings were addressed. The fixes were verified by CoinFabrik in their re-audit.
LO-01 Fix: Deletion Mechanism — Resolved
We added two new functions:
deleteManifestCid(bytes32 didKey)— deletes a single mapping; reverts withManifestNotFoundif the entry does not exist.deleteManifestCidsBatch(bytes32[] calldata didKeys)— batch deletion in a single transaction; reverts if any key is not found.
Both emit events (ManifestCidDeleted, ManifestCidsBatchDeleted) for on-chain traceability.
EN-01 Fix: Two-step Ownership — Implemented
Migrated from Ownable to Ownable2Step (OpenZeppelin). Ownership transfer now requires:
- Current owner calls
transferOwnership(newOwner)→ sets pending owner. - New owner calls
acceptOwnership()→ completes transfer.
This prevents accidental transfer to addresses that cannot interact with the contract.
EN-02 Fix: Gas Optimizations — Implemented
- Custom errors: Replaced all string-based
requirestatements with custom errors (InvalidCid,ArrayMismatch,ManifestNotFound), reducing deployment and revert gas costs. - Unchecked loop increments: Loop counters in batch functions use
unchecked { ++i; }since array bounds cannot overflow. - Batch events:
setManifestCidsBatchnow emits a singleManifestCidsBatchSetevent (instead of N individualManifestCidSetevents), saving ~7% gas on a 100-item batch per the auditor’s estimate.
Verification
- All 18 unit tests pass, including new tests for deletion, batch deletion, and two-step ownership.
- The contract was redeployed on Rootstock Testnet after the fixes were applied.
Evidence:
- Fix commit:
94c02b1a892d72367e05c8319ea8d84911262730 - Re-audit confirmation in the final report (page 4: “Resolved” / “Implemented” for all findings)
3. Gas Profiling
Tool: hardhat-gas-reporter v1.x integrated into the Hardhat project.
Configuration: Solidity 0.8.20, optimizer enabled, 200 runs; enabled via REPORT_GAS=1 npx hardhat test.
Results
| Method | Min (gas) | Max (gas) | Avg (gas) | # calls | Notes |
|---|---|---|---|---|---|
setManifestCid |
32,695 | 49,831 | 47,910 | 9 | Min = warm slot; Max = cold/new slot |
setManifestCidsBatch |
78,224 | 78,272 | 78,256 | 3 | Batch of 2 items; ~39k per item |
deleteManifestCid |
– | – | 26,258 | 2 | SSTORE refund on clear |
deleteManifestCidsBatch |
– | – | 31,129 | 2 | Batch of 2 items |
transferOwnership |
– | – | 47,800 | 2 | OpenZeppelin Ownable2Step |
acceptOwnership |
– | – | 28,278 | 1 | Second step of 2-step transfer |
| Deployment | Gas | % of block limit |
|---|---|---|
DidManifestRegistry |
755,359 | 1.3% |
Key Takeaways
- Batching is efficient: 2 mappings via
setManifestCidsBatch(~78k gas) is cheaper than 2 individualsetManifestCidcalls (~96k gas) — saving ~19% on overhead. - Deletes are cheap: ~26k gas per delete thanks to storage refunds.
- Deployment is lightweight: Only 1.3% of the 60M block gas limit.
Evidence:
- hardhat-gas-reporter output captured during test execution
- Config:
gasReporterblock inhardhat.config.ts, scripttest:gasinpackage.json
4. Symbolic Execution (Time-boxed)
Tool: Mythril (Docker image mythril/myth:latest) — symbolic execution engine by ConsenSys.
Configuration: Analysis run via Docker with solc-json remappings for OpenZeppelin imports, targeting DidManifestRegistry.sol.
Results
Mythril reported 3 findings, all classified as SWC-101 (Integer Arithmetic Bugs):
| # | Function | Location | Warning |
|---|---|---|---|
| 1 | setManifestCid |
Line 74: bytes(manifestCid).length == 0 |
“Arithmetic operator can underflow” |
| 2 | getManifestCid |
#utility.yul:92 (compiler helper) |
Same |
| 3 | setManifestCid |
#utility.yul:92 (compiler helper) |
Same |
Analysis: All Three Are False Positives
Root cause: Mythril’s SWC-101 detector operates at raw EVM opcode level. It flags any SUB opcode whose operands can symbolically wrap below zero. The flagged code is in the compiler-generated ABI decoder (#utility.yul), not in our contract logic. The Solidity 0.8+ compiler emits a calldatasize() - offset subtraction for string calldata parameter validation, but every execution path where this subtraction underflows immediately hits a compiler-inserted REVERT — making the underflow unreachable.
Verdict:
- Finding 1 flags a
.length == 0comparison — not arithmetic that can underflow. - Findings 2 & 3 flag internal Yul helpers for ABI encoding (
eq(outOfPlaceEncoding, lt(length, 32))), not user-written code. - This is a known Mythril behaviour on Solidity 0.8+ contracts with
calldatastring parameters.
Conclusion: No vulnerabilities found. No code changes required. The findings are documented inline in the contract source with SWC-101 false positive comments for future reference.
Evidence:
- Mythril output from Docker execution
- Detailed analysis documented in project files
5. Fuzz Testing / Property Testing
Tool: Foundry v1.5.1-stable (forge), installed alongside the existing Hardhat project.
Configuration:
- Fuzz tests: 1,000 runs per test function
- Invariant tests: 200 runs x 50 call depth = up to 10,000 state transitions per invariant
Architecture
We created a Foundry test layer alongside Hardhat:
test/foundry/DidManifestRegistry.fuzz.t.sol— 9 stateless fuzz teststest/foundry/invariant/RegistryHandler.sol— handler contract with ghost state mirrortest/foundry/invariant/DidManifestRegistry.invariant.t.sol— 5 stateful invariant tests
Fuzz Test Results (9/9 PASS)
| Test | Property | Runs | Result |
|---|---|---|---|
F-01 testFuzz_setAndGet |
set then get returns same value, for any key/CID | 1,000 | PASS |
F-02 testFuzz_emptyStringAlwaysReverts |
empty CID always reverts InvalidCid |
1,000 | PASS |
F-03 testFuzz_nonOwnerCannotSet |
any non-owner address is always rejected | 1,000 | PASS |
F-04 testFuzz_setOverwritesValue |
second write to same key replaces first | 1,000 | PASS |
F-05 testFuzz_deleteAfterSet |
delete then get returns empty string | 1,000 | PASS |
F-06 testFuzz_batchArrayMismatch |
mismatched array lengths always revert ArrayMismatch |
1,000 | PASS |
F-07 testFuzz_batchSetAndGet |
all batch entries are correctly retrievable | 1,001 | PASS |
F-08 testFuzz_bytes32ZeroKeyIsValid |
bytes32(0) works as a valid key |
1,000 | PASS |
F-09 testFuzz_nonExistentKeyReturnsEmpty |
unset keys return empty string without reverting | 1,000 | PASS |
Invariant Test Results (5/5 PASS)
| Invariant | Rule | Calls | Result |
|---|---|---|---|
I-01 invariant_ownerIsAlwaysHandler |
ownership never changes unexpectedly | 10,000 | PASS |
I-02 invariant_storedCidIsNeverEmpty |
tracked keys always have non-empty CID on-chain | 10,000 | PASS |
I-03 invariant_deletedKeyReturnsEmpty |
deleted keys always return “” | 10,000 | PASS |
I-04 invariant_ghostMirrorConsistency |
on-chain value matches ghost state for every key | 10,000 | PASS |
I-05 invariant_nonOwnerCannotMutate |
no non-owner call ever succeeds in writing | 10,000 | PASS |
Notable Finding: Batch Duplicate-Key Behaviour
During fuzz test F-07, the fuzzer discovered on run 6 that a batch containing a duplicate key causes the first entry to be silently overwritten by the second. This is not a bug — it is correct and consistent with the contract’s design as a simple mapping (last-write-wins) — but it was behaviour not covered by the existing Hardhat unit tests. This is a direct example of the fuzzer finding an edge case that hand-written tests missed. We documented this as a known behaviour and recommend validating for duplicates at the application layer (NestJS backend) before calling the contract.
Totals
| Category | Tests | Passed | Failed |
|---|---|---|---|
| Fuzz (stateless) | 9 | 9 | 0 |
| Invariant (stateful) | 5 | 5 | 0 |
| Total | 14 | 14 | 0 |
Total fuzzer executions: ~9,000 property checks (fuzz) + ~50,000 state-changing calls (invariant) = ~59,000 executions.
Evidence:
- Full report:
docs/identity/DidManifestRegistry-Fuzz-Invariant-Testing-Report.md - Test files:
identity/contracts/test/foundry/ foundry.tomlconfiguration
6. Monitoring + Alerting
Tool: Tenderly — real-time smart contract monitoring and alerting platform.
We configured Tenderly to monitor our DidManifestRegistry contract deployed on Rootstock Testnet at address 0x64dB8b2ccD86d4A36b7F9B9F8A3eA2F35fA86c2a.
What We Set Up
- Contract verification on Tenderly — uploaded the contract source (Standard JSON Input with all OpenZeppelin dependencies) so Tenderly can decode function calls and events.
- Alert rule — configured an alert that triggers when a
ManifestCidSetevent is emitted by the contract. - Alert destination — alerts are delivered to the project dashboard (extensible to email, Slack, webhook, PagerDuty, etc.).
Live Verification
We performed an end-to-end test by submitting a DID verification request through the citizen app, which was approved by the issuer. The backend (Web3RegistryWorkerService) successfully wrote the DID-CID mapping to the new contract on Rootstock Testnet.
Transaction: 0x5852b83bf4cd0adf0e38e14505feecf1790224bfd2b40cb4ad786cbaf9ddf5bb
On-chain result:
ManifestCidSetevent emitted with:didKey: the keccak256 hash of the DID URImanifestCid:QmYihf4squDc5AFCM5PPWeKQQUus7pK94dmUo2APh1812qwriter:0x799f8c5124e8c6C4Ec19b5314be2a214E05f4Be5(contract owner / backend wallet)
Tenderly alert triggered successfully:
- Alert: “Event ManifestCidSet emitted in DID Manifest”
- Tx Hash:
0x5852b83bf4...ddf5bb - When: 01/04/2026 12:48:33
Evidence:
- Tenderly transaction view: https://dashboard.tenderly.co/ManuelRM/project/tx/0x5852b83bf4cd0adf0e38e14505feecf1790224bfd2b40cb4ad786cbaf9ddf5bb
- Tenderly event logs: https://dashboard.tenderly.co/ManuelRM/project/tx/0x5852b83bf4cd0adf0e38e14505feecf1790224bfd2b40cb4ad786cbaf9ddf5bb/logs
- Screenshot of Tenderly Alerting History showing the triggered alert (attached below)
Conclusion
All six deliverables for Milestone 2 have been completed:
- The contract was externally audited by CoinFabrik with a clean result (no critical, high, or medium issues).
- All audit findings and recommendations were implemented and verified in the re-audit.
- Gas profiling confirmed efficient gas usage, with batching providing ~19% savings over individual calls.
- Symbolic execution via Mythril found no real vulnerabilities (3 false positives on compiler-generated code).
- Fuzz and invariant testing via Foundry exercised ~59,000 random inputs and state transitions with zero failures, and surfaced one undocumented edge case (batch duplicate keys) that was documented.
- Monitoring and alerting is live via Tenderly, and was validated end-to-end with a real transaction that triggered the configured alert.
The contract has been redeployed on Rootstock Testnet with all audit fixes applied, the backend and mobile app are configured to use the new contract, and the full monitoring pipeline is operational.
We will upload the complete fuzz/property testing report alongside the milestone deliverables.
Regards!