From b34870f4fe550bad3603a5ca95bca928f6e36b1a Mon Sep 17 00:00:00 2001 From: Adedoyinjr <110180947+Adedoyinjr@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:23:01 +0100 Subject: [PATCH 01/23] feat(events): add canonical versioned event interface --- contracts/interfaces/ITruthBountyEvents.sol | 137 ++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 contracts/interfaces/ITruthBountyEvents.sol diff --git a/contracts/interfaces/ITruthBountyEvents.sol b/contracts/interfaces/ITruthBountyEvents.sol new file mode 100644 index 0000000..37b0114 --- /dev/null +++ b/contracts/interfaces/ITruthBountyEvents.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/// @title ITruthBountyEvents +/// @notice Canonical, versioned event surface for TruthBounty protocol indexers. +/// @dev Events are public protocol APIs. Implementations should emit events only +/// after the corresponding state transition succeeds. +interface ITruthBountyEvents { + uint16 constant EVENT_SCHEMA_VERSION = 1; + + // Claims + event ClaimCreated( + uint256 indexed claimId, + address indexed actor, + bytes32 indexed metadataHash, + uint64 timestamp, + uint16 version + ); + event ClaimUpdated( + uint256 indexed claimId, + address indexed actor, + bytes32 indexed metadataHash, + uint64 timestamp, + uint16 version + ); + event ClaimResolved( + uint256 indexed claimId, + address indexed actor, + bool outcome, + uint64 timestamp, + uint16 version + ); + event ClaimFinalized( + uint256 indexed claimId, + address indexed actor, + uint64 timestamp, + uint16 version + ); + + // Verification + event VerificationSubmitted( + uint256 indexed claimId, + address indexed verifier, + bool support, + uint256 stakeAmount, + uint64 timestamp, + uint16 version + ); + event VerificationChallenged( + uint256 indexed claimId, + address indexed challenger, + bytes32 indexed reasonHash, + uint64 timestamp, + uint16 version + ); + + // Staking and slashing + event StakeDeposited( + address indexed verifier, + uint256 amount, + uint256 newBalance, + uint64 timestamp, + uint16 version + ); + event StakeWithdrawn( + address indexed verifier, + uint256 amount, + uint256 newBalance, + uint64 timestamp, + uint16 version + ); + event SlashExecuted( + uint256 indexed claimId, + address indexed verifier, + bytes32 indexed reason, + uint256 amount, + uint64 timestamp, + uint16 version + ); + + // Rewards and treasury + event RewardCalculated( + uint256 indexed claimId, + address indexed recipient, + uint256 amount, + uint64 timestamp, + uint16 version + ); + event RewardEscrowed( + uint256 indexed claimId, + address indexed recipient, + uint256 amount, + uint64 timestamp, + uint16 version + ); + event RewardClaimed( + uint256 indexed claimId, + address indexed recipient, + uint256 amount, + uint64 timestamp, + uint16 version + ); + event TreasuryTransfer( + bytes32 indexed operationId, + address indexed token, + address indexed recipient, + uint256 amount, + uint64 timestamp, + uint16 version + ); + + // Governance and emergency controls + event GovernanceProposalCreated( + uint256 indexed proposalId, + address indexed proposer, + bytes32 indexed metadataHash, + uint64 timestamp, + uint16 version + ); + event GovernanceProposalExecuted( + uint256 indexed proposalId, + address indexed executor, + uint64 timestamp, + uint16 version + ); + event EmergencyPauseActivated( + address indexed actor, + bytes32 indexed reason, + uint64 timestamp, + uint16 version + ); + event EmergencyPauseRecovered( + address indexed actor, + uint64 timestamp, + uint16 version + ); +} From 15a535ee801d8f1fea71dee56bc837a51efe3ab0 Mon Sep 17 00:00:00 2001 From: Adedoyinjr <110180947+Adedoyinjr@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:23:44 +0100 Subject: [PATCH 02/23] docs(events): define indexing and versioning standard --- docs/event-architecture.md | 82 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 docs/event-architecture.md diff --git a/docs/event-architecture.md b/docs/event-architecture.md new file mode 100644 index 0000000..9572139 --- /dev/null +++ b/docs/event-architecture.md @@ -0,0 +1,82 @@ +# TruthBounty On-Chain Event Architecture + +## Status + +Schema version: `1` + +This document defines the canonical event contract between TruthBounty smart contracts and off-chain consumers such as indexers, explorers, APIs, analytics services, notification systems, and frontends. + +The Solidity source of truth is `contracts/interfaces/ITruthBountyEvents.sol`. + +## Design rules + +1. Events are emitted only after the corresponding state transition succeeds. +2. Event names use past-tense domain actions such as `ClaimCreated` and `RewardClaimed`. +3. Entity identifiers, actors, and stable lookup keys are indexed where practical. +4. Dynamic metadata is referenced by a deterministic `bytes32` hash rather than duplicated in logs. +5. Every canonical event includes a block-context timestamp and schema version. +6. A single state transition emits one canonical event unless the transition intentionally spans multiple protocol domains. +7. Failed or reverted transactions emit no canonical events. +8. New schema versions must be additive whenever possible. Existing event signatures are immutable once consumed in production. + +## Common fields + +- `timestamp`: `uint64(block.timestamp)` at emission time. +- `version`: `EVENT_SCHEMA_VERSION` from `ITruthBountyEvents`. +- `actor`: the address responsible for the state transition. +- `metadataHash`: a deterministic hash of off-chain or dynamic metadata. +- `reason`: a stable `bytes32` identifier, not free-form text. + +## Event ordering + +Within one transaction, consumers may rely on log order. Across transactions, consumers must order by block number, transaction index, and log index. + +Business logic must not depend on event ordering. Events reflect completed state changes; they do not authorize or trigger on-chain state transitions. + +## Event catalogue + +### Claims + +- `ClaimCreated`: a new claim is persisted. +- `ClaimUpdated`: claim metadata changes. +- `ClaimResolved`: the canonical outcome is determined. +- `ClaimFinalized`: settlement and all required accounting are complete. + +### Verification + +- `VerificationSubmitted`: a verifier records a position and stake. +- `VerificationChallenged`: a verification or claim is challenged. + +### Staking and slashing + +- `StakeDeposited`: verifier collateral increases. +- `StakeWithdrawn`: verifier collateral decreases through a valid withdrawal. +- `SlashExecuted`: locked collateral is confiscated for a unique offence. + +### Rewards and treasury + +- `RewardCalculated`: a deterministic reward amount is established. +- `RewardEscrowed`: funds become reserved for a recipient. +- `RewardClaimed`: reserved funds are transferred or marked paid. +- `TreasuryTransfer`: treasury-controlled value moves for a uniquely identified operation. + +### Governance and emergency controls + +- `GovernanceProposalCreated`: a governance proposal is registered. +- `GovernanceProposalExecuted`: an approved proposal is executed. +- `EmergencyPauseActivated`: protocol emergency controls are activated. +- `EmergencyPauseRecovered`: normal operation is restored. + +## Indexing guidance + +Indexers should persist the tuple `(chainId, contractAddress, transactionHash, logIndex)` as the unique event identity. Reorg handling must roll back events from orphaned blocks before replaying the canonical chain. + +Consumers should filter by indexed entity IDs and actor addresses, then validate the `version` field before decoding version-specific semantics. + +## Versioning policy + +Version 1 consumers must ignore unknown event signatures and reject unsupported versions only for events they recognize. A new incompatible payload requires a new event signature and a new schema version; existing signatures must not be silently repurposed. + +## Gas guidance + +Only fields required for filtering should be indexed. Large strings and byte arrays should not be emitted when a content hash or stable identifier is sufficient. Gas benchmarks should compare state-changing functions before and after canonical event adoption. From 83924c00e0ac76223cda59b997679bb8719fe447 Mon Sep 17 00:00:00 2001 From: Adedoyinjr <110180947+Adedoyinjr@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:24:40 +0100 Subject: [PATCH 03/23] fix(events): keep schema version interface-compatible --- contracts/interfaces/ITruthBountyEvents.sol | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/contracts/interfaces/ITruthBountyEvents.sol b/contracts/interfaces/ITruthBountyEvents.sol index 37b0114..69759b3 100644 --- a/contracts/interfaces/ITruthBountyEvents.sol +++ b/contracts/interfaces/ITruthBountyEvents.sol @@ -4,10 +4,9 @@ pragma solidity ^0.8.20; /// @title ITruthBountyEvents /// @notice Canonical, versioned event surface for TruthBounty protocol indexers. /// @dev Events are public protocol APIs. Implementations should emit events only -/// after the corresponding state transition succeeds. +/// after the corresponding state transition succeeds. Schema version 1 is +/// represented by the literal value `1` in each event's `version` field. interface ITruthBountyEvents { - uint16 constant EVENT_SCHEMA_VERSION = 1; - // Claims event ClaimCreated( uint256 indexed claimId, From f81f744504344f9482afc023e823f1ed8b9668a1 Mon Sep 17 00:00:00 2001 From: Adedoyinjr <110180947+Adedoyinjr@users.noreply.github.com> Date: Tue, 28 Jul 2026 23:03:06 +0000 Subject: [PATCH 04/23] fix(build): resolve compile blockers --- contracts/TruthBounty.sol | 1 + contracts/bootstrap/BootstrapController.sol | 4 +++- contracts/deployment/MigrationManager.sol | 1 + hardhat.config.ts | 19 ++++++++++--------- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/contracts/TruthBounty.sol b/contracts/TruthBounty.sol index 114b352..d89793a 100644 --- a/contracts/TruthBounty.sol +++ b/contracts/TruthBounty.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; import "./utils/ResolverRoleTimelock.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "@openzeppelin/contracts/utils/Pausable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; diff --git a/contracts/bootstrap/BootstrapController.sol b/contracts/bootstrap/BootstrapController.sol index 99a9cd1..8a7e2c5 100644 --- a/contracts/bootstrap/BootstrapController.sol +++ b/contracts/bootstrap/BootstrapController.sol @@ -19,6 +19,7 @@ interface ITruthBountyWeighted { function settlementThresholdPercent() external view returns (uint256); function rewardPercent() external view returns (uint256); function slashPercent() external view returns (uint256); + function GOVERNANCE_ROLE() external view returns (bytes32); } interface IStaking { @@ -43,6 +44,7 @@ contract BootstrapController is ReentrancyGuard, Pausable, GovernanceOwnable { bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); bytes32 public constant DEPLOYER_ROLE = keccak256("DEPLOYER_ROLE"); + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); // ============ Constants ============ @@ -308,7 +310,7 @@ contract BootstrapController is ReentrancyGuard, Pausable, GovernanceOwnable { } } - function _validateConfiguration() internal view { + function _validateConfiguration() internal { BootstrapConfig memory cfg = config; if (cfg.verificationWindowDuration < 1 days || cfg.verificationWindowDuration > 30 days) { diff --git a/contracts/deployment/MigrationManager.sol b/contracts/deployment/MigrationManager.sol index 0ff286b..b52d4f5 100644 --- a/contracts/deployment/MigrationManager.sol +++ b/contracts/deployment/MigrationManager.sol @@ -8,6 +8,7 @@ import "../governance/GovernanceOwnable.sol"; contract MigrationManager is ReentrancyGuard, Pausable, GovernanceOwnable { bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); bytes32 public constant MIGRATOR_ROLE = keccak256("MIGRATOR_ROLE"); bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); diff --git a/hardhat.config.ts b/hardhat.config.ts index f46f70f..6d35f45 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -8,16 +8,17 @@ import "@openzeppelin/hardhat-upgrades"; dotenv.config(); const config: HardhatUserConfig = { - solidity: { - version: "0.8.28", - settings: { - evmVersion: "cancun", - optimizer: { - enabled: true, - runs: 200 - } - } + solidity: { + version: "0.8.28", + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + viaIR: true, + evmVersion: "cancun", }, +}, networks: { optimismSepolia: { url: From 1b3bbd22830ef8b99f0d26a3cb0a2f39aa14fa08 Mon Sep 17 00:00:00 2001 From: Adedoyinjr <110180947+Adedoyinjr@users.noreply.github.com> Date: Wed, 29 Jul 2026 00:04:59 +0100 Subject: [PATCH 05/23] test(events): add event architecture harness --- contracts/mocks/EventArchitectureHarness.sol | 54 ++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 contracts/mocks/EventArchitectureHarness.sol diff --git a/contracts/mocks/EventArchitectureHarness.sol b/contracts/mocks/EventArchitectureHarness.sol new file mode 100644 index 0000000..a4c1d0b --- /dev/null +++ b/contracts/mocks/EventArchitectureHarness.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "../interfaces/ITruthBountyEvents.sol"; + +contract EventArchitectureHarness is ITruthBountyEvents { + uint16 public constant EVENT_SCHEMA_VERSION = 1; + + function emitClaimCreated( + uint256 claimId, + address actor, + bytes32 metadataHash + ) external { + emit ClaimCreated( + claimId, + actor, + metadataHash, + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); + } + + function emitVerificationSubmitted( + uint256 claimId, + address verifier, + bool support, + uint256 stakeAmount + ) external { + emit VerificationSubmitted( + claimId, + verifier, + support, + stakeAmount, + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); + } + + function emitSlashExecuted( + uint256 claimId, + address verifier, + bytes32 reason, + uint256 amount + ) external { + emit SlashExecuted( + claimId, + verifier, + reason, + amount, + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); + } +} From b711ef40db6f5a6258062b1f8a6303b31cb50dcd Mon Sep 17 00:00:00 2001 From: Adedoyinjr <110180947+Adedoyinjr@users.noreply.github.com> Date: Wed, 29 Jul 2026 00:05:17 +0100 Subject: [PATCH 06/23] test(events): cover indexed fields and schema version --- test/EventArchitecture.test.ts | 55 ++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 test/EventArchitecture.test.ts diff --git a/test/EventArchitecture.test.ts b/test/EventArchitecture.test.ts new file mode 100644 index 0000000..7754e07 --- /dev/null +++ b/test/EventArchitecture.test.ts @@ -0,0 +1,55 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; + +const VERSION = 1n; + +describe("Event Architecture", function () { + async function deployFixture() { + const [actor, verifier] = await ethers.getSigners(); + const factory = await ethers.getContractFactory("EventArchitectureHarness"); + const harness = await factory.deploy(); + await harness.waitForDeployment(); + return { harness, actor, verifier }; + } + + it("emits a versioned ClaimCreated event with indexed identifiers", async function () { + const { harness, actor } = await deployFixture(); + const metadataHash = ethers.keccak256(ethers.toUtf8Bytes("claim-metadata")); + + const tx = await harness.emitClaimCreated(7, actor.address, metadataHash); + const receipt = await tx.wait(); + const block = await ethers.provider.getBlock(receipt!.blockNumber); + + await expect(tx) + .to.emit(harness, "ClaimCreated") + .withArgs(7, actor.address, metadataHash, BigInt(block!.timestamp), VERSION); + + const log = receipt!.logs.find((entry) => { + try { + return harness.interface.parseLog(entry)?.name === "ClaimCreated"; + } catch { + return false; + } + }); + + expect(log).to.not.equal(undefined); + expect(log!.topics).to.have.length(4); + }); + + it("emits deterministic verification and slashing payloads", async function () { + const { harness, verifier } = await deployFixture(); + const reason = ethers.keccak256(ethers.toUtf8Bytes("DOUBLE_VOTE")); + + await expect(harness.emitVerificationSubmitted(9, verifier.address, true, 1000)) + .to.emit(harness, "VerificationSubmitted") + .withArgs(9, verifier.address, true, 1000, anyUint64, VERSION); + + await expect(harness.emitSlashExecuted(9, verifier.address, reason, 250)) + .to.emit(harness, "SlashExecuted") + .withArgs(9, verifier.address, reason, 250, anyUint64, VERSION); + }); +}); + +function anyUint64(value: unknown): boolean { + return typeof value === "bigint" && value >= 0n && value <= (1n << 64n) - 1n; +} From 550a9c2dba9b44d7e390614e65622bdc42c78886 Mon Sep 17 00:00:00 2001 From: Adedoyinjr <110180947+Adedoyinjr@users.noreply.github.com> Date: Wed, 29 Jul 2026 00:14:47 +0100 Subject: [PATCH 07/23] docs(events): add event gas benchmark methodology --- docs/event-gas-benchmarks.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 docs/event-gas-benchmarks.md diff --git a/docs/event-gas-benchmarks.md b/docs/event-gas-benchmarks.md new file mode 100644 index 0000000..94c0853 --- /dev/null +++ b/docs/event-gas-benchmarks.md @@ -0,0 +1,26 @@ +# Event gas benchmarks + +This document defines the repeatable benchmark method for the SC-022 event schema. + +## Method + +Run the focused Hardhat suite with gas reporting enabled for the event harness. Measure transaction gas for each isolated event emission and compare it with a no-op baseline compiled with the same Solidity version, optimizer settings, IR pipeline, and Cancun EVM target. + +```bash +REPORT_GAS=true npx hardhat test test/EventArchitecture.test.ts +``` + +Record the compiler version, optimizer runs, EVM target, event signature, indexed topic count, encoded data size, total transaction gas, and baseline-adjusted event overhead. + +## Required cases + +- `ClaimCreated`: three indexed fields plus timestamp and schema version. +- `VerificationSubmitted`: claim and verifier filters plus vote payload. +- `SlashExecuted`: claim, verifier, and reason filters plus amount payload. +- Governance and emergency events when their protocol modules adopt the canonical interface. + +## Interpretation + +Indexed parameters improve query performance but consume additional topic gas. Dynamic metadata is represented by a `bytes32` reference rather than duplicated strings. Benchmarks must be rerun whenever an event signature, indexed field, compiler configuration, or protocol schema version changes. + +The canonical schema version for the initial SC-022 event surface is `1`. \ No newline at end of file From a66245601a2d9b0fa09549fd80a6ae55c75c848d Mon Sep 17 00:00:00 2001 From: Adedoyinjr <110180947+Adedoyinjr@users.noreply.github.com> Date: Wed, 29 Jul 2026 00:15:06 +0100 Subject: [PATCH 08/23] docs(events): add consumer compatibility checklist --- docs/event-consumer-checklist.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 docs/event-consumer-checklist.md diff --git a/docs/event-consumer-checklist.md b/docs/event-consumer-checklist.md new file mode 100644 index 0000000..d42bde9 --- /dev/null +++ b/docs/event-consumer-checklist.md @@ -0,0 +1,16 @@ +# Event consumer compatibility checklist + +Indexer, backend, frontend, explorer, and analytics consumers should apply the following rules to the SC-022 event surface. + +- Filter by contract address and canonical event signature before decoding. +- Treat indexed entity identifiers and actors as the primary query keys. +- Persist block number, block hash, transaction hash, transaction index, and log index with every decoded event. +- Deduplicate by chain ID, transaction hash, and log index. +- Delay irreversible processing until the consumer's configured confirmation depth is reached. +- Roll back events whose block hash is no longer canonical after a reorganization. +- Reject unsupported schema versions without corrupting previously indexed state. +- Process logs in block number, transaction index, then log index order. +- Never infer a successful state transition from a reverted transaction; reverted transactions produce no logs. +- Treat metadata hashes as references and verify fetched metadata independently. + +Schema version `1` is the initial compatibility target. \ No newline at end of file From 17ec7fae09eb894a8eb61400ad6a62aeb5f3f632 Mon Sep 17 00:00:00 2001 From: Adedoyinjr <110180947+Adedoyinjr@users.noreply.github.com> Date: Wed, 29 Jul 2026 00:15:24 +0100 Subject: [PATCH 09/23] docs(events): catalog schema v1 event surface --- docs/event-schema-v1.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 docs/event-schema-v1.md diff --git a/docs/event-schema-v1.md b/docs/event-schema-v1.md new file mode 100644 index 0000000..af858cd --- /dev/null +++ b/docs/event-schema-v1.md @@ -0,0 +1,37 @@ +# TruthBounty event schema v1 + +The canonical event declarations are defined in `contracts/interfaces/ITruthBountyEvents.sol`. Every event carries a `uint16 version` value of `1` and a block-context `uint64 timestamp`. + +## Claims + +- `ClaimCreated`: claim ID, actor, metadata hash, timestamp, version. +- `ClaimUpdated`: claim ID, actor, metadata hash, timestamp, version. +- `ClaimResolved`: claim ID, actor, outcome, timestamp, version. +- `ClaimFinalized`: claim ID, actor, timestamp, version. + +## Verification + +- `VerificationSubmitted`: claim ID, verifier, support flag, stake amount, timestamp, version. +- `VerificationChallenged`: claim ID, challenger, reason hash, timestamp, version. + +## Staking and slashing + +- `StakeDeposited`: verifier, amount, resulting balance, timestamp, version. +- `StakeWithdrawn`: verifier, amount, resulting balance, timestamp, version. +- `SlashExecuted`: claim ID, verifier, reason hash, amount, timestamp, version. + +## Rewards and treasury + +- `RewardCalculated`: claim ID, recipient, amount, timestamp, version. +- `RewardEscrowed`: claim ID, recipient, amount, timestamp, version. +- `RewardClaimed`: claim ID, recipient, amount, timestamp, version. +- `TreasuryTransfer`: operation ID, token, recipient, amount, timestamp, version. + +## Governance and emergency + +- `GovernanceProposalCreated`: proposal ID, proposer, metadata hash, timestamp, version. +- `GovernanceProposalExecuted`: proposal ID, executor, timestamp, version. +- `EmergencyPauseActivated`: actor, reason hash, timestamp, version. +- `EmergencyPauseRecovered`: actor, timestamp, version. + +The first three indexed parameters are chosen for entity, actor, and reason or metadata filtering where applicable. Consumers must use the complete event signature because event names may coexist with legacy signatures during migration. \ No newline at end of file From fb5b24261b1aa3de8c8366c6f42476751bb363e4 Mon Sep 17 00:00:00 2001 From: Adedoyinjr <110180947+Adedoyinjr@users.noreply.github.com> Date: Wed, 29 Jul 2026 00:15:40 +0100 Subject: [PATCH 10/23] docs(events): define deterministic log ordering --- docs/indexer-ordering.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docs/indexer-ordering.md diff --git a/docs/indexer-ordering.md b/docs/indexer-ordering.md new file mode 100644 index 0000000..e160029 --- /dev/null +++ b/docs/indexer-ordering.md @@ -0,0 +1,15 @@ +# Deterministic event ordering + +TruthBounty event consumers must process canonical logs using the following total order: + +1. block number +2. transaction index +3. log index + +Within one successful transaction, events are emitted only after the state mutation they describe. A consumer must not assume ordering across separate transactions beyond the canonical block ordering above. + +Consumers should store the block hash with each event. When a reorganization replaces a block, all events associated with the orphaned block hash must be rolled back before events from the replacement branch are applied. + +Duplicate delivery is expected in distributed systems. Consumers must make ingestion idempotent by using chain ID, transaction hash, and log index as the event identity. + +Version upgrades do not change the ordering rule. Unsupported versions should be quarantined for later decoding rather than interpreted using an older schema. \ No newline at end of file From 59274d4d22734508e321055d53287724956ef1cd Mon Sep 17 00:00:00 2001 From: Adedoyinjr <110180947+Adedoyinjr@users.noreply.github.com> Date: Tue, 28 Jul 2026 23:58:51 +0000 Subject: [PATCH 11/23] feat(events): integrate versioned protocol events --- contracts/TruthBountyWeighted.sol | 91 +++++++++++++++++++- contracts/interfaces/ITruthBountyEvents.sol | 34 ++++---- contracts/mocks/EventArchitectureHarness.sol | 12 +-- docs/event-architecture.md | 36 ++++---- docs/event-gas-benchmarks.md | 6 +- docs/event-schema-v1.md | 34 ++++---- test/EventArchitecture.test.ts | 16 ++-- 7 files changed, 159 insertions(+), 70 deletions(-) diff --git a/contracts/TruthBountyWeighted.sol b/contracts/TruthBountyWeighted.sol index 2bd6a36..0a677a7 100644 --- a/contracts/TruthBountyWeighted.sol +++ b/contracts/TruthBountyWeighted.sol @@ -8,6 +8,7 @@ import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "@openzeppelin/contracts/utils/Pausable.sol"; import "./IReputationOracle.sol"; import "./governance/GovernanceOwnable.sol"; +import "./interfaces/ITruthBountyEvents.sol"; /** * @title TruthBountyWeighted @@ -20,7 +21,7 @@ import "./governance/GovernanceOwnable.sol"; * - Prevents low-reputation dominance * - Maintains backward compatibility with equal-weight fallback */ -contract TruthBountyWeighted is ResolverRoleTimelock, ReentrancyGuard, Pausable, GovernanceOwnable { +contract TruthBountyWeighted is ResolverRoleTimelock, ReentrancyGuard, Pausable, GovernanceOwnable, ITruthBountyEvents { // ============ Roles ============ bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); @@ -79,6 +80,9 @@ contract TruthBountyWeighted is ResolverRoleTimelock, ReentrancyGuard, Pausable, uint256 public constant MAX_REPUTATION_STALENESS = 1 hours; /// @notice Threshold above which a withdrawal is considered "large" and requires a 2-day cooldown (#152) uint256 public constant LARGE_WITHDRAWAL_THRESHOLD = 10_000 * TOKEN_DECIMALS_MULTIPLIER; + + /// @notice Canonical indexing schema emitted by this module. + uint16 public constant EVENT_SCHEMA_VERSION = 1; // ============ State Variables ============ /// @notice Token contract for staking and rewards @@ -303,6 +307,13 @@ contract TruthBountyWeighted is ResolverRoleTimelock, ReentrancyGuard, Pausable, }); emit ClaimCreated(claimId, msg.sender, content, verificationWindowEnd); + emit ClaimCreatedV1( + claimId, + msg.sender, + keccak256(bytes(content)), + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); return claimId; } @@ -323,6 +334,13 @@ contract TruthBountyWeighted is ResolverRoleTimelock, ReentrancyGuard, Pausable, verifierStakes[msg.sender].totalStaked += amount; emit StakeDeposited(msg.sender, amount); + emit StakeDepositedV1( + msg.sender, + amount, + verifierStakes[msg.sender].totalStaked, + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); } /** @@ -430,6 +448,14 @@ contract TruthBountyWeighted is ResolverRoleTimelock, ReentrancyGuard, Pausable, claim.totalStakeAmount += stakeAmount; // Still track raw stake total emit VoteCast(claimId, msg.sender, support, stakeAmount, effectiveStake, reputationScore); + emit VerificationSubmittedV1( + claimId, + msg.sender, + support, + stakeAmount, + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); } /** @@ -465,6 +491,19 @@ contract TruthBountyWeighted is ResolverRoleTimelock, ReentrancyGuard, Pausable, rewardAmount, slashedAmount ); + emit ClaimResolvedV1( + claimId, + msg.sender, + passed, + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); + emit ClaimFinalizedV1( + claimId, + msg.sender, + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); } /** @@ -491,6 +530,13 @@ contract TruthBountyWeighted is ResolverRoleTimelock, ReentrancyGuard, Pausable, verifierStakes[msg.sender].activeStakes -= vote.stakeAmount; require(bountyToken.transfer(msg.sender, vote.stakeAmount), "Stake transfer failed"); emit StakeWithdrawn(msg.sender, vote.stakeAmount); + emit StakeWithdrawnV1( + msg.sender, + vote.stakeAmount, + verifierStakes[msg.sender].totalStaked, + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); return; } @@ -518,6 +564,13 @@ contract TruthBountyWeighted is ResolverRoleTimelock, ReentrancyGuard, Pausable, if (reward > 0) { require(bountyToken.transfer(msg.sender, reward), "Reward transfer failed"); emit RewardsDistributed(claimId, msg.sender, reward); + emit RewardClaimedV1( + claimId, + msg.sender, + reward, + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); } // Return stake (winners get full RAW stake back) @@ -526,6 +579,13 @@ contract TruthBountyWeighted is ResolverRoleTimelock, ReentrancyGuard, Pausable, verifierStakes[msg.sender].activeStakes -= vote.stakeAmount; require(bountyToken.transfer(msg.sender, vote.stakeAmount), "Stake transfer failed"); emit StakeWithdrawn(msg.sender, vote.stakeAmount); + emit StakeWithdrawnV1( + msg.sender, + vote.stakeAmount, + verifierStakes[msg.sender].totalStaked, + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); } } @@ -551,6 +611,13 @@ contract TruthBountyWeighted is ResolverRoleTimelock, ReentrancyGuard, Pausable, verifierStakes[msg.sender].activeStakes -= vote.stakeAmount; require(bountyToken.transfer(msg.sender, vote.stakeAmount), "Stake transfer failed"); emit StakeWithdrawn(msg.sender, vote.stakeAmount); + emit StakeWithdrawnV1( + msg.sender, + vote.stakeAmount, + verifierStakes[msg.sender].totalStaked, + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); return; } @@ -566,6 +633,14 @@ contract TruthBountyWeighted is ResolverRoleTimelock, ReentrancyGuard, Pausable, stakeToReturn = vote.stakeAmount - slashAmount; emit StakeSlashed(claimId, msg.sender, slashAmount); + emit SlashExecutedV1( + claimId, + msg.sender, + keccak256("LOSING_VERIFICATION_POSITION"), + slashAmount, + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); } vote.stakeReturned = true; @@ -578,6 +653,13 @@ contract TruthBountyWeighted is ResolverRoleTimelock, ReentrancyGuard, Pausable, if (stakeToReturn > 0) { require(bountyToken.transfer(msg.sender, stakeToReturn), "Stake transfer failed"); emit StakeWithdrawn(msg.sender, stakeToReturn); + emit StakeWithdrawnV1( + msg.sender, + stakeToReturn, + verifierStakes[msg.sender].totalStaked, + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); } } @@ -610,6 +692,13 @@ contract TruthBountyWeighted is ResolverRoleTimelock, ReentrancyGuard, Pausable, require(bountyToken.transfer(msg.sender, amount), "Transfer failed"); emit StakeWithdrawn(msg.sender, amount); + emit StakeWithdrawnV1( + msg.sender, + amount, + verifierStakes[msg.sender].totalStaked, + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); } diff --git a/contracts/interfaces/ITruthBountyEvents.sol b/contracts/interfaces/ITruthBountyEvents.sol index 69759b3..35f7b3f 100644 --- a/contracts/interfaces/ITruthBountyEvents.sol +++ b/contracts/interfaces/ITruthBountyEvents.sol @@ -8,28 +8,28 @@ pragma solidity ^0.8.20; /// represented by the literal value `1` in each event's `version` field. interface ITruthBountyEvents { // Claims - event ClaimCreated( + event ClaimCreatedV1( uint256 indexed claimId, address indexed actor, bytes32 indexed metadataHash, uint64 timestamp, uint16 version ); - event ClaimUpdated( + event ClaimUpdatedV1( uint256 indexed claimId, address indexed actor, bytes32 indexed metadataHash, uint64 timestamp, uint16 version ); - event ClaimResolved( + event ClaimResolvedV1( uint256 indexed claimId, address indexed actor, bool outcome, uint64 timestamp, uint16 version ); - event ClaimFinalized( + event ClaimFinalizedV1( uint256 indexed claimId, address indexed actor, uint64 timestamp, @@ -37,7 +37,7 @@ interface ITruthBountyEvents { ); // Verification - event VerificationSubmitted( + event VerificationSubmittedV1( uint256 indexed claimId, address indexed verifier, bool support, @@ -45,7 +45,7 @@ interface ITruthBountyEvents { uint64 timestamp, uint16 version ); - event VerificationChallenged( + event VerificationChallengedV1( uint256 indexed claimId, address indexed challenger, bytes32 indexed reasonHash, @@ -54,21 +54,21 @@ interface ITruthBountyEvents { ); // Staking and slashing - event StakeDeposited( + event StakeDepositedV1( address indexed verifier, uint256 amount, uint256 newBalance, uint64 timestamp, uint16 version ); - event StakeWithdrawn( + event StakeWithdrawnV1( address indexed verifier, uint256 amount, uint256 newBalance, uint64 timestamp, uint16 version ); - event SlashExecuted( + event SlashExecutedV1( uint256 indexed claimId, address indexed verifier, bytes32 indexed reason, @@ -78,28 +78,28 @@ interface ITruthBountyEvents { ); // Rewards and treasury - event RewardCalculated( + event RewardCalculatedV1( uint256 indexed claimId, address indexed recipient, uint256 amount, uint64 timestamp, uint16 version ); - event RewardEscrowed( + event RewardEscrowedV1( uint256 indexed claimId, address indexed recipient, uint256 amount, uint64 timestamp, uint16 version ); - event RewardClaimed( + event RewardClaimedV1( uint256 indexed claimId, address indexed recipient, uint256 amount, uint64 timestamp, uint16 version ); - event TreasuryTransfer( + event TreasuryTransferV1( bytes32 indexed operationId, address indexed token, address indexed recipient, @@ -109,26 +109,26 @@ interface ITruthBountyEvents { ); // Governance and emergency controls - event GovernanceProposalCreated( + event GovernanceProposalCreatedV1( uint256 indexed proposalId, address indexed proposer, bytes32 indexed metadataHash, uint64 timestamp, uint16 version ); - event GovernanceProposalExecuted( + event GovernanceProposalExecutedV1( uint256 indexed proposalId, address indexed executor, uint64 timestamp, uint16 version ); - event EmergencyPauseActivated( + event EmergencyPauseActivatedV1( address indexed actor, bytes32 indexed reason, uint64 timestamp, uint16 version ); - event EmergencyPauseRecovered( + event EmergencyPauseRecoveredV1( address indexed actor, uint64 timestamp, uint16 version diff --git a/contracts/mocks/EventArchitectureHarness.sol b/contracts/mocks/EventArchitectureHarness.sol index a4c1d0b..82cbee6 100644 --- a/contracts/mocks/EventArchitectureHarness.sol +++ b/contracts/mocks/EventArchitectureHarness.sol @@ -6,12 +6,12 @@ import "../interfaces/ITruthBountyEvents.sol"; contract EventArchitectureHarness is ITruthBountyEvents { uint16 public constant EVENT_SCHEMA_VERSION = 1; - function emitClaimCreated( + function emitClaimCreatedV1( uint256 claimId, address actor, bytes32 metadataHash ) external { - emit ClaimCreated( + emit ClaimCreatedV1( claimId, actor, metadataHash, @@ -20,13 +20,13 @@ contract EventArchitectureHarness is ITruthBountyEvents { ); } - function emitVerificationSubmitted( + function emitVerificationSubmittedV1( uint256 claimId, address verifier, bool support, uint256 stakeAmount ) external { - emit VerificationSubmitted( + emit VerificationSubmittedV1( claimId, verifier, support, @@ -36,13 +36,13 @@ contract EventArchitectureHarness is ITruthBountyEvents { ); } - function emitSlashExecuted( + function emitSlashExecutedV1( uint256 claimId, address verifier, bytes32 reason, uint256 amount ) external { - emit SlashExecuted( + emit SlashExecutedV1( claimId, verifier, reason, diff --git a/docs/event-architecture.md b/docs/event-architecture.md index 9572139..a3c37bd 100644 --- a/docs/event-architecture.md +++ b/docs/event-architecture.md @@ -11,7 +11,7 @@ The Solidity source of truth is `contracts/interfaces/ITruthBountyEvents.sol`. ## Design rules 1. Events are emitted only after the corresponding state transition succeeds. -2. Event names use past-tense domain actions such as `ClaimCreated` and `RewardClaimed`. +2. Event names use past-tense domain actions such as `ClaimCreatedV1` and `RewardClaimedV1`. 3. Entity identifiers, actors, and stable lookup keys are indexed where practical. 4. Dynamic metadata is referenced by a deterministic `bytes32` hash rather than duplicated in logs. 5. Every canonical event includes a block-context timestamp and schema version. @@ -37,35 +37,35 @@ Business logic must not depend on event ordering. Events reflect completed state ### Claims -- `ClaimCreated`: a new claim is persisted. -- `ClaimUpdated`: claim metadata changes. -- `ClaimResolved`: the canonical outcome is determined. -- `ClaimFinalized`: settlement and all required accounting are complete. +- `ClaimCreatedV1`: a new claim is persisted. +- `ClaimUpdatedV1`: claim metadata changes. +- `ClaimResolvedV1`: the canonical outcome is determined. +- `ClaimFinalizedV1`: settlement and all required accounting are complete. ### Verification -- `VerificationSubmitted`: a verifier records a position and stake. -- `VerificationChallenged`: a verification or claim is challenged. +- `VerificationSubmittedV1`: a verifier records a position and stake. +- `VerificationChallengedV1`: a verification or claim is challenged. ### Staking and slashing -- `StakeDeposited`: verifier collateral increases. -- `StakeWithdrawn`: verifier collateral decreases through a valid withdrawal. -- `SlashExecuted`: locked collateral is confiscated for a unique offence. +- `StakeDepositedV1`: verifier collateral increases. +- `StakeWithdrawnV1`: verifier collateral decreases through a valid withdrawal. +- `SlashExecutedV1`: locked collateral is confiscated for a unique offence. ### Rewards and treasury -- `RewardCalculated`: a deterministic reward amount is established. -- `RewardEscrowed`: funds become reserved for a recipient. -- `RewardClaimed`: reserved funds are transferred or marked paid. -- `TreasuryTransfer`: treasury-controlled value moves for a uniquely identified operation. +- `RewardCalculatedV1`: a deterministic reward amount is established. +- `RewardEscrowedV1`: funds become reserved for a recipient. +- `RewardClaimedV1`: reserved funds are transferred or marked paid. +- `TreasuryTransferV1`: treasury-controlled value moves for a uniquely identified operation. ### Governance and emergency controls -- `GovernanceProposalCreated`: a governance proposal is registered. -- `GovernanceProposalExecuted`: an approved proposal is executed. -- `EmergencyPauseActivated`: protocol emergency controls are activated. -- `EmergencyPauseRecovered`: normal operation is restored. +- `GovernanceProposalCreatedV1`: a governance proposal is registered. +- `GovernanceProposalExecutedV1`: an approved proposal is executed. +- `EmergencyPauseActivatedV1`: protocol emergency controls are activated. +- `EmergencyPauseRecoveredV1`: normal operation is restored. ## Indexing guidance diff --git a/docs/event-gas-benchmarks.md b/docs/event-gas-benchmarks.md index 94c0853..5ee24a1 100644 --- a/docs/event-gas-benchmarks.md +++ b/docs/event-gas-benchmarks.md @@ -14,9 +14,9 @@ Record the compiler version, optimizer runs, EVM target, event signature, indexe ## Required cases -- `ClaimCreated`: three indexed fields plus timestamp and schema version. -- `VerificationSubmitted`: claim and verifier filters plus vote payload. -- `SlashExecuted`: claim, verifier, and reason filters plus amount payload. +- `ClaimCreatedV1`: three indexed fields plus timestamp and schema version. +- `VerificationSubmittedV1`: claim and verifier filters plus vote payload. +- `SlashExecutedV1`: claim, verifier, and reason filters plus amount payload. - Governance and emergency events when their protocol modules adopt the canonical interface. ## Interpretation diff --git a/docs/event-schema-v1.md b/docs/event-schema-v1.md index af858cd..a37e2d3 100644 --- a/docs/event-schema-v1.md +++ b/docs/event-schema-v1.md @@ -4,34 +4,34 @@ The canonical event declarations are defined in `contracts/interfaces/ITruthBoun ## Claims -- `ClaimCreated`: claim ID, actor, metadata hash, timestamp, version. -- `ClaimUpdated`: claim ID, actor, metadata hash, timestamp, version. -- `ClaimResolved`: claim ID, actor, outcome, timestamp, version. -- `ClaimFinalized`: claim ID, actor, timestamp, version. +- `ClaimCreatedV1`: claim ID, actor, metadata hash, timestamp, version. +- `ClaimUpdatedV1`: claim ID, actor, metadata hash, timestamp, version. +- `ClaimResolvedV1`: claim ID, actor, outcome, timestamp, version. +- `ClaimFinalizedV1`: claim ID, actor, timestamp, version. ## Verification -- `VerificationSubmitted`: claim ID, verifier, support flag, stake amount, timestamp, version. -- `VerificationChallenged`: claim ID, challenger, reason hash, timestamp, version. +- `VerificationSubmittedV1`: claim ID, verifier, support flag, stake amount, timestamp, version. +- `VerificationChallengedV1`: claim ID, challenger, reason hash, timestamp, version. ## Staking and slashing -- `StakeDeposited`: verifier, amount, resulting balance, timestamp, version. -- `StakeWithdrawn`: verifier, amount, resulting balance, timestamp, version. -- `SlashExecuted`: claim ID, verifier, reason hash, amount, timestamp, version. +- `StakeDepositedV1`: verifier, amount, resulting balance, timestamp, version. +- `StakeWithdrawnV1`: verifier, amount, resulting balance, timestamp, version. +- `SlashExecutedV1`: claim ID, verifier, reason hash, amount, timestamp, version. ## Rewards and treasury -- `RewardCalculated`: claim ID, recipient, amount, timestamp, version. -- `RewardEscrowed`: claim ID, recipient, amount, timestamp, version. -- `RewardClaimed`: claim ID, recipient, amount, timestamp, version. -- `TreasuryTransfer`: operation ID, token, recipient, amount, timestamp, version. +- `RewardCalculatedV1`: claim ID, recipient, amount, timestamp, version. +- `RewardEscrowedV1`: claim ID, recipient, amount, timestamp, version. +- `RewardClaimedV1`: claim ID, recipient, amount, timestamp, version. +- `TreasuryTransferV1`: operation ID, token, recipient, amount, timestamp, version. ## Governance and emergency -- `GovernanceProposalCreated`: proposal ID, proposer, metadata hash, timestamp, version. -- `GovernanceProposalExecuted`: proposal ID, executor, timestamp, version. -- `EmergencyPauseActivated`: actor, reason hash, timestamp, version. -- `EmergencyPauseRecovered`: actor, timestamp, version. +- `GovernanceProposalCreatedV1`: proposal ID, proposer, metadata hash, timestamp, version. +- `GovernanceProposalExecutedV1`: proposal ID, executor, timestamp, version. +- `EmergencyPauseActivatedV1`: actor, reason hash, timestamp, version. +- `EmergencyPauseRecoveredV1`: actor, timestamp, version. The first three indexed parameters are chosen for entity, actor, and reason or metadata filtering where applicable. Consumers must use the complete event signature because event names may coexist with legacy signatures during migration. \ No newline at end of file diff --git a/test/EventArchitecture.test.ts b/test/EventArchitecture.test.ts index 7754e07..5382fa1 100644 --- a/test/EventArchitecture.test.ts +++ b/test/EventArchitecture.test.ts @@ -12,21 +12,21 @@ describe("Event Architecture", function () { return { harness, actor, verifier }; } - it("emits a versioned ClaimCreated event with indexed identifiers", async function () { + it("emits a versioned ClaimCreatedV1 event with indexed identifiers", async function () { const { harness, actor } = await deployFixture(); const metadataHash = ethers.keccak256(ethers.toUtf8Bytes("claim-metadata")); - const tx = await harness.emitClaimCreated(7, actor.address, metadataHash); + const tx = await harness.emitClaimCreatedV1(7, actor.address, metadataHash); const receipt = await tx.wait(); const block = await ethers.provider.getBlock(receipt!.blockNumber); await expect(tx) - .to.emit(harness, "ClaimCreated") + .to.emit(harness, "ClaimCreatedV1") .withArgs(7, actor.address, metadataHash, BigInt(block!.timestamp), VERSION); const log = receipt!.logs.find((entry) => { try { - return harness.interface.parseLog(entry)?.name === "ClaimCreated"; + return harness.interface.parseLog(entry)?.name === "ClaimCreatedV1"; } catch { return false; } @@ -40,12 +40,12 @@ describe("Event Architecture", function () { const { harness, verifier } = await deployFixture(); const reason = ethers.keccak256(ethers.toUtf8Bytes("DOUBLE_VOTE")); - await expect(harness.emitVerificationSubmitted(9, verifier.address, true, 1000)) - .to.emit(harness, "VerificationSubmitted") + await expect(harness.emitVerificationSubmittedV1(9, verifier.address, true, 1000)) + .to.emit(harness, "VerificationSubmittedV1") .withArgs(9, verifier.address, true, 1000, anyUint64, VERSION); - await expect(harness.emitSlashExecuted(9, verifier.address, reason, 250)) - .to.emit(harness, "SlashExecuted") + await expect(harness.emitSlashExecutedV1(9, verifier.address, reason, 250)) + .to.emit(harness, "SlashExecutedV1") .withArgs(9, verifier.address, reason, 250, anyUint64, VERSION); }); }); From 85f7a6ea7ec60b9928b11e3c8b82727d7738dff1 Mon Sep 17 00:00:00 2001 From: Adedoyinjr <110180947+Adedoyinjr@users.noreply.github.com> Date: Wed, 29 Jul 2026 00:09:18 +0000 Subject: [PATCH 12/23] feat(events): emit canonical slashing events --- contracts/VerifierSlashing.sol | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/contracts/VerifierSlashing.sol b/contracts/VerifierSlashing.sol index 983843e..b635410 100644 --- a/contracts/VerifierSlashing.sol +++ b/contracts/VerifierSlashing.sol @@ -7,6 +7,7 @@ import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "@openzeppelin/contracts/utils/Pausable.sol"; import "./governance/GovernanceOwnable.sol"; import "./governance/GovernanceHooks.sol"; +import "./interfaces/ITruthBountyEvents.sol"; /** * @title VerifierSlashing @@ -20,7 +21,7 @@ interface IStaking { function forceSlash(address user, uint256 amount) external; } -contract VerifierSlashing is ResolverRoleTimelock, ReentrancyGuard, Pausable, GovernanceOwnable { +contract VerifierSlashing is ResolverRoleTimelock, ReentrancyGuard, Pausable, GovernanceOwnable, ITruthBountyEvents { // Role definitions bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); @@ -40,6 +41,7 @@ contract VerifierSlashing is ResolverRoleTimelock, ReentrancyGuard, Pausable, Go // making it significantly heavier per item. Bounds gas to avoid // out-of-gas / block-gas-limit DoS in batchSlash. (Audit #156) uint256 public constant MAX_BATCH_SIZE = 50; + uint16 public constant EVENT_SCHEMA_VERSION = 1; // Default maximum slashing percentage per incident uint256 public maxSlashPercentage = 50; // 50% max per slash @@ -225,6 +227,14 @@ contract VerifierSlashing is ResolverRoleTimelock, ReentrancyGuard, Pausable, Go reason, msg.sender ); + emit SlashExecutedV1( + 0, + verifier, + keccak256(bytes(reason)), + slashAmount, + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); } /** @@ -317,6 +327,14 @@ contract VerifierSlashing is ResolverRoleTimelock, ReentrancyGuard, Pausable, Go stakingContract.forceSlash(verifier, slashAmount); emit CriticalSlashed(verifier, slashAmount, percentage, reason, msg.sender); + emit SlashExecutedV1( + 0, + verifier, + keccak256(bytes(reason)), + slashAmount, + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); } /** @@ -382,6 +400,14 @@ contract VerifierSlashing is ResolverRoleTimelock, ReentrancyGuard, Pausable, Go reason, msg.sender ); + emit SlashExecutedV1( + 0, + verifier, + keccak256(bytes(reason)), + slashAmount, + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); } // === VIEW FUNCTIONS === From 65fbb79adfb6eafc95ba9c9d8ccd59c29ec15a39 Mon Sep 17 00:00:00 2001 From: Adedoyinjr <110180947+Adedoyinjr@users.noreply.github.com> Date: Wed, 29 Jul 2026 00:30:27 +0000 Subject: [PATCH 13/23] feat(events): emit canonical reward calculation event --- contracts/interfaces/ITruthBountyEvents.sol | 2 +- contracts/reward/RewardEngine.sol | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/contracts/interfaces/ITruthBountyEvents.sol b/contracts/interfaces/ITruthBountyEvents.sol index 35f7b3f..f9a9753 100644 --- a/contracts/interfaces/ITruthBountyEvents.sol +++ b/contracts/interfaces/ITruthBountyEvents.sol @@ -79,7 +79,7 @@ interface ITruthBountyEvents { // Rewards and treasury event RewardCalculatedV1( - uint256 indexed claimId, + bytes32 indexed calculationId, address indexed recipient, uint256 amount, uint64 timestamp, diff --git a/contracts/reward/RewardEngine.sol b/contracts/reward/RewardEngine.sol index d2885b1..f7a756b 100644 --- a/contracts/reward/RewardEngine.sol +++ b/contracts/reward/RewardEngine.sol @@ -6,9 +6,10 @@ import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "@openzeppelin/contracts/utils/Pausable.sol"; import "../governance/GovernanceOwnable.sol"; import "../governance/GovernanceHooks.sol"; +import "../interfaces/ITruthBountyEvents.sol"; import "../IReputationOracle.sol"; -contract RewardEngine is ReentrancyGuard, Pausable, GovernanceOwnable { +contract RewardEngine is ReentrancyGuard, Pausable, GovernanceOwnable, ITruthBountyEvents { // ============ Roles ============ bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); @@ -17,6 +18,7 @@ contract RewardEngine is ReentrancyGuard, Pausable, GovernanceOwnable { // ============ Constants ============ uint256 public constant BASE_MULTIPLIER = 1e18; + uint16 public constant EVENT_SCHEMA_VERSION = 1; uint256 public constant PERCENT_DENOMINATOR = 100; uint256 public constant MAX_MULTIPLIER = 10e18; uint256 public constant MIN_MULTIPLIER = 0; @@ -237,6 +239,13 @@ contract RewardEngine is ReentrancyGuard, Pausable, GovernanceOwnable { dailyVerifierRewards[dayKey][verifier] += finalAmount; emit RewardCalculated(verifier, finalAmount, calculationId); + emit RewardCalculatedV1( + calculationId, + verifier, + finalAmount, + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); return (finalAmount, calculationId); } From 9f4f91a90f89ca5003951ab07d30c1ca9dd057e5 Mon Sep 17 00:00:00 2001 From: Adedoyinjr <110180947+Adedoyinjr@users.noreply.github.com> Date: Wed, 29 Jul 2026 00:36:11 +0000 Subject: [PATCH 14/23] feat(events): emit canonical governance events --- contracts/governance/GovernanceController.sol | 24 ++++++++++++++++++- contracts/interfaces/ITruthBountyEvents.sol | 4 ++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/contracts/governance/GovernanceController.sol b/contracts/governance/GovernanceController.sol index 7cdc224..f0ae259 100644 --- a/contracts/governance/GovernanceController.sol +++ b/contracts/governance/GovernanceController.sol @@ -5,6 +5,7 @@ import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "./GovernanceHooks.sol"; import "./GovernorAccess.sol"; +import "../interfaces/ITruthBountyEvents.sol"; /** * @title GovernanceController @@ -17,11 +18,12 @@ import "./GovernorAccess.sol"; * 3. Once approved, proposal can be executed here * 4. Execution updates the target contract parameters */ -contract GovernanceController is GovernorAccessControl, ReentrancyGuard, GovernanceHooks { +contract GovernanceController is GovernorAccessControl, ReentrancyGuard, GovernanceHooks, ITruthBountyEvents { // ============ Constants ============ uint256 public constant MIN_PROPOSAL_DELAY = 1 hours; uint256 public constant MAX_PROPOSAL_DELAY = 30 days; + uint16 public constant EVENT_SCHEMA_VERSION = 1; // ============ State Variables ============ @@ -201,6 +203,12 @@ contract GovernanceController is GovernorAccessControl, ReentrancyGuard, Governa oldValue, proposal.newValue ); + emit GovernanceProposalExecutedV1( + proposalId, + msg.sender, + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); } /** @@ -319,6 +327,12 @@ contract GovernanceController is GovernorAccessControl, ReentrancyGuard, Governa proposalId, proposal.newAddress ); + emit GovernanceProposalExecutedV1( + proposalId, + msg.sender, + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); } // ============ View Functions ============ @@ -420,6 +434,14 @@ contract GovernanceController is GovernorAccessControl, ReentrancyGuard, Governa pendingProposalIds.push(proposalId); isProposalPendingSet[proposalId] = true; } + + emit GovernanceProposalCreatedV1( + proposalId, + msg.sender, + keccak256(abi.encode(paramType, oldValue, newValue, newAddress)), + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); } // ============ Admin Functions ============ diff --git a/contracts/interfaces/ITruthBountyEvents.sol b/contracts/interfaces/ITruthBountyEvents.sol index f9a9753..221fc4c 100644 --- a/contracts/interfaces/ITruthBountyEvents.sol +++ b/contracts/interfaces/ITruthBountyEvents.sol @@ -110,14 +110,14 @@ interface ITruthBountyEvents { // Governance and emergency controls event GovernanceProposalCreatedV1( - uint256 indexed proposalId, + bytes32 indexed proposalId, address indexed proposer, bytes32 indexed metadataHash, uint64 timestamp, uint16 version ); event GovernanceProposalExecutedV1( - uint256 indexed proposalId, + bytes32 indexed proposalId, address indexed executor, uint64 timestamp, uint16 version From 2e6e290979e66ee4985071aac9139e479b5aa460 Mon Sep 17 00:00:00 2001 From: Adedoyinjr <110180947+Adedoyinjr@users.noreply.github.com> Date: Wed, 29 Jul 2026 00:44:46 +0000 Subject: [PATCH 15/23] feat(events): emit canonical emergency lifecycle events --- contracts/TruthBounty.sol | 17 ++++++++++++--- contracts/TruthBountyWeighted.sol | 20 +++++++++++++++++ contracts/VerifierSlashing.sol | 20 +++++++++++++++++ contracts/bootstrap/BootstrapController.sol | 24 ++++++++++++++++++++- contracts/deployment/MigrationManager.sol | 17 ++++++++++++--- contracts/reward/RewardEngine.sol | 20 +++++++++++++++++ 6 files changed, 111 insertions(+), 7 deletions(-) diff --git a/contracts/TruthBounty.sol b/contracts/TruthBounty.sol index d89793a..a2abffe 100644 --- a/contracts/TruthBounty.sol +++ b/contracts/TruthBounty.sol @@ -10,6 +10,7 @@ import "@openzeppelin/contracts/utils/Pausable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; import "./governance/GovernanceOwnable.sol"; +import "./interfaces/ITruthBountyEvents.sol"; import "./governance/GovernanceHooks.sol"; /** @@ -147,7 +148,8 @@ contract TruthBountyToken is ERC20, ResolverRoleTimelock, Initializable, UUPSUpg * unchanged. * ──────────────────────────────────────────────────────────────────────────── */ -contract TruthBounty is AccessControl, ReentrancyGuard, Pausable, GovernanceOwnable { +contract TruthBounty is AccessControl, ReentrancyGuard, Pausable, GovernanceOwnable, ITruthBountyEvents { + uint16 public constant EVENT_SCHEMA_VERSION = 1; // ── Roles ────────────────────────────────────────────────────────────── @@ -472,6 +474,15 @@ contract TruthBounty is AccessControl, ReentrancyGuard, Pausable, GovernanceOwna // ── Pauser ───────────────────────────────────────────────────────────── - function pause() external onlyRole(PAUSER_ROLE) { _pause(); } - function unpause() external onlyRole(PAUSER_ROLE) { _unpause(); } + function pause() external onlyRole(PAUSER_ROLE) { _pause(); emit EmergencyPauseActivatedV1( + msg.sender, + keccak256("MANUAL_PAUSE"), + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); } + function unpause() external onlyRole(PAUSER_ROLE) { _unpause(); emit EmergencyPauseRecoveredV1( + msg.sender, + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); } } diff --git a/contracts/TruthBountyWeighted.sol b/contracts/TruthBountyWeighted.sol index 0a677a7..781eb02 100644 --- a/contracts/TruthBountyWeighted.sol +++ b/contracts/TruthBountyWeighted.sol @@ -1240,10 +1240,30 @@ contract TruthBountyWeighted is ResolverRoleTimelock, ReentrancyGuard, Pausable, function pause() external onlyRole(PAUSER_ROLE) { _pause(); + emit EmergencyPauseActivatedV1( + + msg.sender, + + keccak256("MANUAL_PAUSE"), + + uint64(block.timestamp), + + EVENT_SCHEMA_VERSION + + ); } function unpause() external onlyRole(PAUSER_ROLE) { _unpause(); + emit EmergencyPauseRecoveredV1( + + msg.sender, + + uint64(block.timestamp), + + EVENT_SCHEMA_VERSION + + ); } /** diff --git a/contracts/VerifierSlashing.sol b/contracts/VerifierSlashing.sol index b635410..7e95e5f 100644 --- a/contracts/VerifierSlashing.sol +++ b/contracts/VerifierSlashing.sol @@ -593,6 +593,17 @@ contract VerifierSlashing is ResolverRoleTimelock, ReentrancyGuard, Pausable, Go */ function pause() external onlyRole(PAUSER_ROLE) { _pause(); + emit EmergencyPauseActivatedV1( + + msg.sender, + + keccak256("MANUAL_PAUSE"), + + uint64(block.timestamp), + + EVENT_SCHEMA_VERSION + + ); } /** @@ -600,5 +611,14 @@ contract VerifierSlashing is ResolverRoleTimelock, ReentrancyGuard, Pausable, Go */ function unpause() external onlyRole(PAUSER_ROLE) { _unpause(); + emit EmergencyPauseRecoveredV1( + + msg.sender, + + uint64(block.timestamp), + + EVENT_SCHEMA_VERSION + + ); } } \ No newline at end of file diff --git a/contracts/bootstrap/BootstrapController.sol b/contracts/bootstrap/BootstrapController.sol index 8a7e2c5..5f39bb8 100644 --- a/contracts/bootstrap/BootstrapController.sol +++ b/contracts/bootstrap/BootstrapController.sol @@ -5,6 +5,7 @@ import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "@openzeppelin/contracts/utils/Pausable.sol"; import "../governance/GovernanceOwnable.sol"; +import "../interfaces/ITruthBountyEvents.sol"; import "../governance/GovernanceHooks.sol"; import "../governance/GovernanceController.sol"; import "../IReputationOracle.sol"; @@ -39,7 +40,8 @@ interface ITruthBountyClaims { function bountyToken() external view returns (address); } -contract BootstrapController is ReentrancyGuard, Pausable, GovernanceOwnable { +contract BootstrapController is ReentrancyGuard, Pausable, GovernanceOwnable, ITruthBountyEvents { + uint16 public constant EVENT_SCHEMA_VERSION = 1; // ============ Roles ============ bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); @@ -465,9 +467,29 @@ contract BootstrapController is ReentrancyGuard, Pausable, GovernanceOwnable { function pause() external onlyRole(PAUSER_ROLE) { _pause(); + emit EmergencyPauseActivatedV1( + + msg.sender, + + keccak256("MANUAL_PAUSE"), + + uint64(block.timestamp), + + EVENT_SCHEMA_VERSION + + ); } function unpause() external onlyRole(PAUSER_ROLE) { _unpause(); + emit EmergencyPauseRecoveredV1( + + msg.sender, + + uint64(block.timestamp), + + EVENT_SCHEMA_VERSION + + ); } } \ No newline at end of file diff --git a/contracts/deployment/MigrationManager.sol b/contracts/deployment/MigrationManager.sol index b52d4f5..4982a5f 100644 --- a/contracts/deployment/MigrationManager.sol +++ b/contracts/deployment/MigrationManager.sol @@ -5,8 +5,10 @@ import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "@openzeppelin/contracts/utils/Pausable.sol"; import "../governance/GovernanceOwnable.sol"; +import "../interfaces/ITruthBountyEvents.sol"; -contract MigrationManager is ReentrancyGuard, Pausable, GovernanceOwnable { +contract MigrationManager is ReentrancyGuard, Pausable, GovernanceOwnable, ITruthBountyEvents { + uint16 public constant EVENT_SCHEMA_VERSION = 1; bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); bytes32 public constant MIGRATOR_ROLE = keccak256("MIGRATOR_ROLE"); @@ -166,8 +168,17 @@ contract MigrationManager is ReentrancyGuard, Pausable, GovernanceOwnable { emit DeploymentVerified(_ownershipTransferred, _allModulesInitialized); } - function pause() external onlyRole(PAUSER_ROLE) { _pause(); } - function unpause() external onlyRole(PAUSER_ROLE) { _unpause(); } + function pause() external onlyRole(PAUSER_ROLE) { _pause(); emit EmergencyPauseActivatedV1( + msg.sender, + keccak256("MANUAL_PAUSE"), + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); } + function unpause() external onlyRole(PAUSER_ROLE) { _unpause(); emit EmergencyPauseRecoveredV1( + msg.sender, + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); } function getModuleAddress(bytes32 moduleId) external view returns (address) { if (!registeredModules[moduleId]) revert ModuleNotRegistered(moduleId); diff --git a/contracts/reward/RewardEngine.sol b/contracts/reward/RewardEngine.sol index f7a756b..1b7d557 100644 --- a/contracts/reward/RewardEngine.sol +++ b/contracts/reward/RewardEngine.sol @@ -508,9 +508,29 @@ contract RewardEngine is ReentrancyGuard, Pausable, GovernanceOwnable, ITruthBou function pause() external onlyRole(PAUSER_ROLE) { _pause(); + emit EmergencyPauseActivatedV1( + + msg.sender, + + keccak256("MANUAL_PAUSE"), + + uint64(block.timestamp), + + EVENT_SCHEMA_VERSION + + ); } function unpause() external onlyRole(PAUSER_ROLE) { _unpause(); + emit EmergencyPauseRecoveredV1( + + msg.sender, + + uint64(block.timestamp), + + EVENT_SCHEMA_VERSION + + ); } } \ No newline at end of file From ab5dad7176705e4d824ac9cf8bba1fa45fc14bd2 Mon Sep 17 00:00:00 2001 From: Adedoyinjr <110180947+Adedoyinjr@users.noreply.github.com> Date: Thu, 30 Jul 2026 02:16:00 +0100 Subject: [PATCH 16/23] chore: run one-time SC-022 merge repair --- .github/workflows/fix-sc022-merge.yml | 72 +++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 .github/workflows/fix-sc022-merge.yml diff --git a/.github/workflows/fix-sc022-merge.yml b/.github/workflows/fix-sc022-merge.yml new file mode 100644 index 0000000..9cf99be --- /dev/null +++ b/.github/workflows/fix-sc022-merge.yml @@ -0,0 +1,72 @@ +name: One-time SC-022 merge repair + +on: + push: + branches: + - feature/sc-022-event-architecture + +permissions: + contents: write + +jobs: + repair: + if: ${{ !contains(github.event.head_commit.message, '[sc022-repair]') }} + runs-on: ubuntu-latest + steps: + - name: Checkout branch + uses: actions/checkout@v4 + with: + ref: feature/sc-022-event-architecture + fetch-depth: 0 + + - name: Apply deterministic repairs + shell: bash + run: | + python3 - <<'PY' + from pathlib import Path + + truth = Path('contracts/TruthBounty.sol') + text = truth.read_text() + + start = text.find(' function _resolverRole() internal pure override returns (bytes32) {', text.find('contract TruthBounty is ')) + end_marker = ' // ── Core Functions' + end = text.find(end_marker, start) + if start == -1 or end == -1: + raise SystemExit('Could not locate stale TruthBounty override block') + + stale = text[start:end] + if 'ResolverRoleTimelock.grantRole' not in stale: + raise SystemExit('Unexpected TruthBounty block; refusing unsafe edit') + + percent_fn = ''' function _percentOf(uint256 value, uint256 percent) internal pure returns (uint256) { + return Math.mulDiv(value, percent, 100); + } + +''' + text = text[:start] + percent_fn + text[end:] + truth.write_text(text) + + simulation = Path('contracts/simulation/EconomicSimulation.sol') + sim = simulation.read_text() + old = 'uint256 totalStakedEnd = stakers * config.minStakeAmount;' + new = 'uint256 totalStakedEnd = stakers * gp.minStakeAmount;' + if old not in sim: + raise SystemExit('Could not locate remaining SimulationConfig minStakeAmount reference') + simulation.write_text(sim.replace(old, new, 1)) + PY + + - name: Install dependencies + run: npm ci + + - name: Compile + run: npx hardhat compile --force + + - name: Commit validated repair + shell: bash + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git rm .github/workflows/fix-sc022-merge.yml + git add contracts/TruthBounty.sol contracts/simulation/EconomicSimulation.sol + git commit -m "fix: complete upstream merge repairs [sc022-repair]" + git push origin HEAD:feature/sc-022-event-architecture From 6d8710b5db9eaa70316661efbb261c9e405f5073 Mon Sep 17 00:00:00 2001 From: Adedoyinjr <110180947+Adedoyinjr@users.noreply.github.com> Date: Thu, 30 Jul 2026 01:19:23 +0000 Subject: [PATCH 17/23] chore: trigger SC-022 repair From 638abd17ef64e7fdc65fe3221cd5f24cbacf54c1 Mon Sep 17 00:00:00 2001 From: Adedoyinjr <110180947+Adedoyinjr@users.noreply.github.com> Date: Thu, 30 Jul 2026 01:21:34 +0000 Subject: [PATCH 18/23] chore: trigger SC-022 repair From f2c7b0b276ed28c465ec653eb50567a8d02a1fc3 Mon Sep 17 00:00:00 2001 From: Adedoyinjr <110180947+Adedoyinjr@users.noreply.github.com> Date: Thu, 30 Jul 2026 02:25:18 +0100 Subject: [PATCH 19/23] fix: make SC-022 repair workflow push before validation --- .github/workflows/fix-sc022-merge.yml | 41 ++++++++++++++++++--------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/.github/workflows/fix-sc022-merge.yml b/.github/workflows/fix-sc022-merge.yml index 9cf99be..002c3f4 100644 --- a/.github/workflows/fix-sc022-merge.yml +++ b/.github/workflows/fix-sc022-merge.yml @@ -27,8 +27,8 @@ jobs: truth = Path('contracts/TruthBounty.sol') text = truth.read_text() - - start = text.find(' function _resolverRole() internal pure override returns (bytes32) {', text.find('contract TruthBounty is ')) + contract_start = text.find('contract TruthBounty is ') + start = text.find(' function _resolverRole() internal pure override returns (bytes32) {', contract_start) end_marker = ' // ── Core Functions' end = text.find(end_marker, start) if start == -1 or end == -1: @@ -42,9 +42,8 @@ jobs: return Math.mulDiv(value, percent, 100); } -''' - text = text[:start] + percent_fn + text[end:] - truth.write_text(text) + ''' + truth.write_text(text[:start] + percent_fn + text[end:]) simulation = Path('contracts/simulation/EconomicSimulation.sol') sim = simulation.read_text() @@ -55,18 +54,34 @@ jobs: simulation.write_text(sim.replace(old, new, 1)) PY - - name: Install dependencies - run: npm ci - - - name: Compile - run: npx hardhat compile --force - - - name: Commit validated repair + - name: Commit and push repairs shell: bash run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git rm .github/workflows/fix-sc022-merge.yml git add contracts/TruthBounty.sol contracts/simulation/EconomicSimulation.sol git commit -m "fix: complete upstream merge repairs [sc022-repair]" git push origin HEAD:feature/sc-022-event-architecture + + - name: Install dependencies + run: npm ci + + - name: Compile + id: compile + continue-on-error: true + shell: bash + run: | + set -o pipefail + npx hardhat compile --force 2>&1 | tee compile-output.txt + + - name: Upload compiler output + if: always() + uses: actions/upload-artifact@v4 + with: + name: sc022-compile-output + path: compile-output.txt + if-no-files-found: ignore + + - name: Report compile failure + if: steps.compile.outcome == 'failure' + run: exit 1 From 65ac6e037e724b952a82674faea5511970a005ab Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2026 01:25:33 +0000 Subject: [PATCH 20/23] fix: complete upstream merge repairs [sc022-repair] --- contracts/TruthBounty.sol | 12 ++---------- contracts/simulation/EconomicSimulation.sol | 2 +- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/contracts/TruthBounty.sol b/contracts/TruthBounty.sol index 9f85470..9ee5f0d 100644 --- a/contracts/TruthBounty.sol +++ b/contracts/TruthBounty.sol @@ -278,17 +278,9 @@ contract TruthBounty is AccessControl, ReentrancyGuard, Pausable, GovernanceOwna ); } - function _resolverRole() internal pure override returns (bytes32) { - return RESOLVER_ROLE; - } - function _percentOf(uint256 value, uint256 percent) internal pure returns (uint256) { - return Math.mulDiv(value, percent, 100); - } - - function grantRole(bytes32 role, address account) public override(AccessControl, ResolverRoleTimelock) { - ResolverRoleTimelock.grantRole(role, account); - } + return Math.mulDiv(value, percent, 100); +} // ── Core Functions ───────────────────────────────────────────────────── diff --git a/contracts/simulation/EconomicSimulation.sol b/contracts/simulation/EconomicSimulation.sol index 1a705ac..4f8c05f 100644 --- a/contracts/simulation/EconomicSimulation.sol +++ b/contracts/simulation/EconomicSimulation.sol @@ -250,7 +250,7 @@ contract EconomicSimulation is // ── Compute final metrics ────────────────────────────────────── metrics.treasurySolvency = treasury; - uint256 totalStakedEnd = stakers * config.minStakeAmount; + uint256 totalStakedEnd = stakers * gp.minStakeAmount; metrics.totalRewardEmissions = totalRewards; metrics.protocolRevenue = totalRevenue; From be2d79b39d729f24dbf37ec4177331b6632da010 Mon Sep 17 00:00:00 2001 From: Adedoyinjr <110180947+Adedoyinjr@users.noreply.github.com> Date: Thu, 30 Jul 2026 01:26:19 +0000 Subject: [PATCH 21/23] chore: trigger repaired SC-022 workflow From 9b41e862969e1b6004ac03bb7694673abc741e35 Mon Sep 17 00:00:00 2001 From: Adedoyinjr <110180947+Adedoyinjr@users.noreply.github.com> Date: Thu, 30 Jul 2026 02:28:28 +0100 Subject: [PATCH 22/23] chore: update SC-022 repair workflow for remaining compile errors --- .github/workflows/fix-sc022-merge.yml | 59 +++++++++++++++------------ 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/.github/workflows/fix-sc022-merge.yml b/.github/workflows/fix-sc022-merge.yml index 002c3f4..0735bc7 100644 --- a/.github/workflows/fix-sc022-merge.yml +++ b/.github/workflows/fix-sc022-merge.yml @@ -25,33 +25,34 @@ jobs: python3 - <<'PY' from pathlib import Path - truth = Path('contracts/TruthBounty.sol') - text = truth.read_text() - contract_start = text.find('contract TruthBounty is ') - start = text.find(' function _resolverRole() internal pure override returns (bytes32) {', contract_start) - end_marker = ' // ── Core Functions' - end = text.find(end_marker, start) - if start == -1 or end == -1: - raise SystemExit('Could not locate stale TruthBounty override block') + treasury = Path('contracts/treasury/TreasuryAccounting.sol') + text = treasury.read_text() - stale = text[start:end] - if 'ResolverRoleTimelock.grantRole' not in stale: - raise SystemExit('Unexpected TruthBounty block; refusing unsafe edit') + old_enum = ''' ECOSYSTEM_FUND, // Future ecosystem development fund + SLASHED_TREASURY // Tokens slashed from verifiers + }''' + new_enum = ''' ECOSYSTEM_FUND, // Future ecosystem development fund + SLASHED_TREASURY, // Tokens slashed from verifiers + EXTERNAL // Off-ledger source or destination marker + }''' - percent_fn = ''' function _percentOf(uint256 value, uint256 percent) internal pure returns (uint256) { - return Math.mulDiv(value, percent, 100); - } + if 'TreasuryAccount(99)' in text: + if old_enum not in text: + raise SystemExit('Could not locate TreasuryAccount enum for EXTERNAL marker') + text = text.replace(old_enum, new_enum, 1) + text = text.replace('TreasuryAccount(99)', 'TreasuryAccount.EXTERNAL') + treasury.write_text(text) + elif 'EXTERNAL' not in text: + raise SystemExit('Treasury external marker is neither patched nor patchable') - ''' - truth.write_text(text[:start] + percent_fn + text[end:]) - - simulation = Path('contracts/simulation/EconomicSimulation.sol') - sim = simulation.read_text() - old = 'uint256 totalStakedEnd = stakers * config.minStakeAmount;' - new = 'uint256 totalStakedEnd = stakers * gp.minStakeAmount;' - if old not in sim: - raise SystemExit('Could not locate remaining SimulationConfig minStakeAmount reference') - simulation.write_text(sim.replace(old, new, 1)) + registry = Path('contracts/upgrade/VersionRegistry.sol') + text = registry.read_text() + old = 'function getVersion(string calldata contractName, string calldata semanticVersion)' + new = 'function getVersion(string memory contractName, string memory semanticVersion)' + if old in text: + registry.write_text(text.replace(old, new, 1)) + elif new not in text: + raise SystemExit('Could not locate VersionRegistry.getVersion signature') PY - name: Commit and push repairs @@ -59,9 +60,13 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add contracts/TruthBounty.sol contracts/simulation/EconomicSimulation.sol - git commit -m "fix: complete upstream merge repairs [sc022-repair]" - git push origin HEAD:feature/sc-022-event-architecture + git add contracts/treasury/TreasuryAccounting.sol contracts/upgrade/VersionRegistry.sol + if git diff --cached --quiet; then + echo "No source changes required" + else + git commit -m "fix: resolve remaining merge compile errors [sc022-repair]" + git push origin HEAD:feature/sc-022-event-architecture + fi - name: Install dependencies run: npm ci From 6eb7997c5b04c9dc3bed1aa6ecb5bd95f0dc7b69 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2026 01:28:35 +0000 Subject: [PATCH 23/23] fix: resolve remaining merge compile errors [sc022-repair] --- contracts/treasury/TreasuryAccounting.sol | 13 +++++++------ contracts/upgrade/VersionRegistry.sol | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/contracts/treasury/TreasuryAccounting.sol b/contracts/treasury/TreasuryAccounting.sol index e5bc3a4..f885878 100644 --- a/contracts/treasury/TreasuryAccounting.sol +++ b/contracts/treasury/TreasuryAccounting.sol @@ -33,7 +33,8 @@ contract TreasuryAccounting is AccessControl, ReentrancyGuard, GovernanceOwnable PROTOCOL_FEES, // Accumulated protocol fees GOVERNANCE_RESERVES, // Governance-controlled reserve fund ECOSYSTEM_FUND, // Future ecosystem development fund - SLASHED_TREASURY // Tokens slashed from verifiers + SLASHED_TREASURY, // Tokens slashed from verifiers + EXTERNAL // Off-ledger source or destination marker } // ============ STRUCTS ============ @@ -192,7 +193,7 @@ contract TreasuryAccounting is AccessControl, ReentrancyGuard, GovernanceOwnable msg.sender, address(this), actualReceived, - TreasuryAccount(99), // External source marker + TreasuryAccount.EXTERNAL, // External source marker targetAccount, "external_deposit" ); @@ -294,7 +295,7 @@ contract TreasuryAccounting is AccessControl, ReentrancyGuard, GovernanceOwnable recipient, amount, sourceAccount, - TreasuryAccount(99), // External destination + TreasuryAccount.EXTERNAL, // External destination "external_withdrawal" ); @@ -320,7 +321,7 @@ contract TreasuryAccounting is AccessControl, ReentrancyGuard, GovernanceOwnable user, address(this), amount, - TreasuryAccount(99), + TreasuryAccount.EXTERNAL, TreasuryAccount.STAKING_RESERVE, "user_stake" ); @@ -348,7 +349,7 @@ contract TreasuryAccounting is AccessControl, ReentrancyGuard, GovernanceOwnable user, amount, TreasuryAccount.STAKING_RESERVE, - TreasuryAccount(99), + TreasuryAccount.EXTERNAL, "user_unstake" ); @@ -407,7 +408,7 @@ contract TreasuryAccounting is AccessControl, ReentrancyGuard, GovernanceOwnable recipient, amount, TreasuryAccount.REWARDS_POOL, - TreasuryAccount(99), + TreasuryAccount.EXTERNAL, "reward_distribution" ); diff --git a/contracts/upgrade/VersionRegistry.sol b/contracts/upgrade/VersionRegistry.sol index 78d84b4..a056334 100644 --- a/contracts/upgrade/VersionRegistry.sol +++ b/contracts/upgrade/VersionRegistry.sol @@ -116,7 +116,7 @@ contract VersionRegistry is IVersionRegistry, AccessControl { return versionList.entries[versionList.entries.length - 1]; } - function getVersion(string calldata contractName, string calldata semanticVersion) + function getVersion(string memory contractName, string memory semanticVersion) public view override returns (VersionEntry memory) { if (!_contractRegistered[contractName]) revert ContractNotRegistered(contractName);