[2510] Grant Proposal - Self Sovereign Identity (SSI) sandbox rootstock integration, maturity and alpha launch

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 with ManifestNotFound if 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:

  1. Current owner calls transferOwnership(newOwner) → sets pending owner.
  2. New owner calls acceptOwnership() → completes transfer.

This prevents accidental transfer to addresses that cannot interact with the contract.

EN-02 Fix: Gas Optimizations — Implemented

  1. Custom errors: Replaced all string-based require statements with custom errors (InvalidCid, ArrayMismatch, ManifestNotFound), reducing deployment and revert gas costs.
  2. Unchecked loop increments: Loop counters in batch functions use unchecked { ++i; } since array bounds cannot overflow.
  3. Batch events: setManifestCidsBatch now emits a single ManifestCidsBatchSet event (instead of N individual ManifestCidSet events), 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 individual setManifestCid calls (~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: gasReporter block in hardhat.config.ts, script test:gas in package.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 == 0 comparison — 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 calldata string 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 tests
  • test/foundry/invariant/RegistryHandler.sol — handler contract with ghost state mirror
  • test/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.toml configuration

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

  1. Contract verification on Tenderly — uploaded the contract source (Standard JSON Input with all OpenZeppelin dependencies) so Tenderly can decode function calls and events.
  2. Alert rule — configured an alert that triggers when a ManifestCidSet event is emitted by the contract.
  3. 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:

  • ManifestCidSet event emitted with:
    • didKey: the keccak256 hash of the DID URI
    • manifestCid: QmYihf4squDc5AFCM5PPWeKQQUus7pK94dmUo2APh1812q
    • writer: 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:


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!

3 Likes