diff --git a/.github/workflows/fix-sc022-merge.yml b/.github/workflows/fix-sc022-merge.yml new file mode 100644 index 0000000..0735bc7 --- /dev/null +++ b/.github/workflows/fix-sc022-merge.yml @@ -0,0 +1,92 @@ +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 + + treasury = Path('contracts/treasury/TreasuryAccounting.sol') + text = treasury.read_text() + + 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 + }''' + + 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') + + 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 + shell: bash + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + 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 + + - 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 diff --git a/compile-output.txt b/compile-output.txt new file mode 100644 index 0000000..9ee104d --- /dev/null +++ b/compile-output.txt @@ -0,0 +1,118 @@ +Warning: This declaration has the same name as another declaration. + --> contracts/TruthBountyWeighted.sol:548:9: + | +548 | Vote storage vote = votes[claimId][msg.sender]; + | ^^^^^^^^^^^^^^^^^ +Note: The other declaration is here: + --> contracts/TruthBountyWeighted.sol:379:5: + | +379 | function vote( + | ^ (Relevant source part starts here and spans across multiple lines). + + +Warning: This declaration has the same name as another declaration. + --> contracts/TruthBountyWeighted.sol:631:9: + | +631 | Vote storage vote = votes[claimId][msg.sender]; + | ^^^^^^^^^^^^^^^^^ +Note: The other declaration is here: + --> contracts/TruthBountyWeighted.sol:379:5: + | +379 | function vote( + | ^ (Relevant source part starts here and spans across multiple lines). + + +Warning: This declaration has the same name as another declaration. + --> contracts/TruthBountyWeighted.sol:702:9: + | +702 | VerifierStake storage stake = verifierStakes[msg.sender]; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Note: The other declaration is here: + --> contracts/TruthBountyWeighted.sol:354:5: + | +354 | function stake(uint256 amount) external nonReentrant whenNotPaused { + | ^ (Relevant source part starts here and spans across multiple lines). + + +Warning: This declaration has the same name as another declaration. + --> contracts/TruthBountyWeighted.sol:975:13: + | +975 | Vote storage vote = votes[claimId][voters[i]]; + | ^^^^^^^^^^^^^^^^^ +Note: The other declaration is here: + --> contracts/TruthBountyWeighted.sol:379:5: + | +379 | function vote( + | ^ (Relevant source part starts here and spans across multiple lines). + + +Warning: This declaration has the same name as another declaration. + --> contracts/TruthBountyWeighted.sol:1006:13: + | +1006 | Vote storage vote = votes[claimId][voter]; + | ^^^^^^^^^^^^^^^^^ +Note: The other declaration is here: + --> contracts/TruthBountyWeighted.sol:379:5: + | +379 | function vote( + | ^ (Relevant source part starts here and spans across multiple lines). + + +Warning: This declaration shadows an existing declaration. + --> contracts/TruthBountyWeighted.sol:1001:25: + | +1001 | ) internal returns (uint256 totalSlashed) { + | ^^^^^^^^^^^^^^^^^^^^ +Note: The shadowed declaration is here: + --> contracts/TruthBountyWeighted.sol:211:5: + | +211 | uint256 public totalSlashed; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + +TypeError: Function has override specified but does not override anything. + --> contracts/TruthBounty.sol:281:44: + | +281 | function _resolverRole() internal pure override returns (bytes32) { + | ^^^^^^^^ + + +TypeError: Invalid contract specified in override list: "ResolverRoleTimelock". + --> contracts/TruthBounty.sol:289:62: + | +289 | function grantRole(bytes32 role, address account) public override(AccessControl, ResolverRoleTimelock) { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Note: This contract: + --> contracts/utils/ResolverRoleTimelock.sol:12:1: + | +12 | abstract contract ResolverRoleTimelock is AccessControl { + | ^ (Relevant source part starts here and spans across multiple lines). + + +TypeError: Cannot call function via contract type name. + --> contracts/TruthBounty.sol:290:9: + | +290 | ResolverRoleTimelock.grantRole(role, account); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + +TypeError: Member "minStakeAmount" not found or not visible after argument-dependent lookup in struct IEconomicSimulation.SimulationConfig memory. + --> contracts/simulation/EconomicSimulation.sol:253:44: + | +253 | uint256 totalStakedEnd = stakers * config.minStakeAmount; + | ^^^^^^^^^^^^^^^^^^^^^ + + +Error HH600: Compilation failed + +HardhatError: HH600: Compilation failed + at SimpleTaskDefinition.action (/workspaces/truthbounty-contract/node_modules/hardhat/src/builtin-tasks/compile.ts:504:15) + at processTicksAndRejections (node:internal/process/task_queues:104:5) + at async Environment._runTaskDefinition (/workspaces/truthbounty-contract/node_modules/hardhat/src/internal/core/runtime-environment.ts:351:14) + at async OverriddenTaskDefinition._action (/workspaces/truthbounty-contract/node_modules/@typechain/hardhat/src/index.ts:28:30) + at async Environment._runTaskDefinition (/workspaces/truthbounty-contract/node_modules/hardhat/src/internal/core/runtime-environment.ts:351:14) + at async Environment.run (/workspaces/truthbounty-contract/node_modules/hardhat/src/internal/core/runtime-environment.ts:184:14) + at async SimpleTaskDefinition.action (/workspaces/truthbounty-contract/node_modules/hardhat/src/builtin-tasks/compile.ts:1433:63) + at async Environment._runTaskDefinition (/workspaces/truthbounty-contract/node_modules/hardhat/src/internal/core/runtime-environment.ts:351:14) + at async Environment._runTaskDefinition (/workspaces/truthbounty-contract/node_modules/hardhat/src/internal/core/runtime-environment.ts:351:14) + at async Environment.run (/workspaces/truthbounty-contract/node_modules/hardhat/src/internal/core/runtime-environment.ts:184:14) diff --git a/contracts/TruthBounty.sol b/contracts/TruthBounty.sol index c6ceaba..9ee5f0d 100644 --- a/contracts/TruthBounty.sol +++ b/contracts/TruthBounty.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; import "./utils/ResolverRoleTimelock.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; @@ -12,6 +11,7 @@ import "@openzeppelin/contracts/utils/math/Math.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"; /** @@ -149,7 +149,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 ────────────────────────────────────────────────────────────── @@ -268,19 +269,18 @@ contract TruthBounty is AccessControl, ReentrancyGuard, Pausable, GovernanceOwna _setRoleAdmin(RESOLVER_ROLE, ADMIN_ROLE); _setRoleAdmin(TREASURY_ROLE, ADMIN_ROLE); - _setRoleAdmin(PAUSER_ROLE, ADMIN_ROLE); + _setRoleAdmin(PAUSER_ROLE, ADMIN_ROLE); - function _resolverRole() internal pure override returns (bytes32) { - return RESOLVER_ROLE; + _initializeGovernance( + _governanceController, + initialAdmin, + initialAdmin + ); } 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 ───────────────────────────────────────────────────── @@ -422,10 +422,8 @@ contract TruthBounty is AccessControl, ReentrancyGuard, Pausable, GovernanceOwna require(!isWinner, "Winners should use claimSettlementRewards"); // Calculate slashed portion - uint256 slashedAmount = _percentOf(vote.stakeAmount, slashPercent); - uint256 returnAmount = vote.stakeAmount - slashedAmount; - - vote.stakeReturned = true; + uint256 slashedAmount = _percentOf(v.stakeAmount, slashPercent); + uint256 returnAmount = v.stakeAmount - slashedAmount; v.stakeReturned = true; verifierStakes[msg.sender].activeStakes -= v.stakeAmount; @@ -446,32 +444,6 @@ contract TruthBounty is AccessControl, ReentrancyGuard, Pausable, GovernanceOwna // ── Internal Helpers ─────────────────────────────────────────────────── - function _determineOutcome(uint256 stakedFor, uint256 stakedAgainst) internal view returns (bool) { - uint256 total = stakedFor + stakedAgainst; - if (total == 0) return false; - return (stakedFor * 100) / total >= settlementThresholdPercent; - } - - function _calculateSettlement(uint256 claimId, bool passed) internal returns (uint256 rewardAmount, uint256 slashedAmount) { - Claim storage claim = claims[claimId]; - uint256 winnerStake = passed ? claim.totalStakedFor : claim.totalStakedAgainst; - uint256 loserStake = passed ? claim.totalStakedAgainst : claim.totalStakedFor; - - slashedAmount = (loserStake * slashPercent) / 100; - rewardAmount = (slashedAmount * rewardPercent) / 100; - - totalSlashed += slashedAmount; - totalRewarded += rewardAmount; - - settlementResults[claimId] = SettlementResult({ - passed: passed, - totalRewards: rewardAmount, - totalSlashed: slashedAmount, - winnerStake: winnerStake, - loserStake: loserStake - }); - } - // ── View Functions ───────────────────────────────────────────────────── function getClaim(uint256 claimId) external view returns (Claim memory) { return claims[claimId]; } @@ -513,6 +485,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 ee53526..28a71dd 100644 --- a/contracts/TruthBountyWeighted.sol +++ b/contracts/TruthBountyWeighted.sol @@ -10,6 +10,7 @@ import "@openzeppelin/contracts/utils/math/Math.sol"; import "./IReputationOracle.sol"; import "./IReputationUpdateEngine.sol"; import "./governance/GovernanceOwnable.sol"; +import "./interfaces/ITruthBountyEvents.sol"; /** * @title TruthBountyWeighted @@ -22,7 +23,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"); @@ -85,9 +86,11 @@ 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; + /// @notice Epoch duration for reputation snapshot tracking (7 days) (SC-013) uint256 public constant EPOCH_DURATION = 7 days; - // ============ State Variables ============ /// @notice Token contract for staking and rewards @@ -256,7 +259,6 @@ contract TruthBountyWeighted is ResolverRoleTimelock, ReentrancyGuard, Pausable, event DefaultReputationScoreUpdated(uint256 oldScore, uint256 newScore); event ReputationSnapshotRecorded(address indexed user, uint256 reputationScore, uint256 timestamp); event ReputationStalenessValidated(address indexed user, uint256 expectedReputation, uint256 actualReputation, uint256 maxDrift); - event ReputationUpdateGracePeriodUpdated(uint256 newGracePeriod); event ClaimWiped(uint256 indexed claimId, address indexed admin, string reason); event ReputationUpdateEngineUpdated(address indexed oldEngine, address indexed newEngine); @@ -332,6 +334,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; } @@ -352,6 +361,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 + ); } /** @@ -462,6 +478,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 + ); } /** @@ -497,6 +521,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 + ); } /** @@ -523,6 +560,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; } @@ -550,6 +594,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) @@ -558,6 +609,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 + ); } } @@ -583,6 +641,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; } @@ -598,6 +663,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; @@ -610,6 +683,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 + ); } } @@ -642,6 +722,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 + ); } @@ -1327,10 +1414,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 55f508f..ff951af 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"; import "./treasury/ITreasuryAccounting.sol"; import "./IReputationOracle.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -25,7 +26,7 @@ interface IStaking { function stakingToken() external view returns (address); } -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"); @@ -41,6 +42,7 @@ contract VerifierSlashing is ResolverRoleTimelock, ReentrancyGuard, Pausable, Go // Maximum number of verifiers that can be slashed in a single batch. 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 @@ -432,7 +434,11 @@ contract VerifierSlashing is ResolverRoleTimelock, ReentrancyGuard, Pausable, Go amount: slashAmount, percentage: config.slashPercentage, reason: reason, - slashedBy: msg.sender + slashedBy: msg.sender, + claimId: 0, + settlementEpoch: block.timestamp / settlementEpochDuration, + offenceId: offenceId, + reasonHash: keccak256(bytes(reason)) })); if (slashAmount > 0) { @@ -473,7 +479,7 @@ contract VerifierSlashing is ResolverRoleTimelock, ReentrancyGuard, Pausable, Go } if (block.timestamp < lastSlashTime[verifier] + slashCooldown) { - revert o(); // Custom revert or standard cooldown block + revert SlashingTooFrequent(); } if (lastSlashBlock[verifier] == block.number) { @@ -521,6 +527,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 + ); emit StakeSlashed(verifier, slashAmount, bytes32(0)); } @@ -704,7 +718,20 @@ contract VerifierSlashing is ResolverRoleTimelock, ReentrancyGuard, Pausable, Go _routeSlashedTokens(slashAmount); emit CriticalSlashed(verifier, slashAmount, percentage, reason, msg.sender); - emit StakeSlashed(verifier, slashAmount, keccak256("CRITICAL_OFFENCE")); + emit SlashExecutedV1( + 0, + verifier, + keccak256(bytes(reason)), + slashAmount, + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); + + emit StakeSlashed( + verifier, + slashAmount, + keccak256("CRITICAL_OFFENCE") + ); } /** @@ -728,7 +755,7 @@ contract VerifierSlashing is ResolverRoleTimelock, ReentrancyGuard, Pausable, Go } if (block.timestamp < lastSlashTime[verifier] + slashCooldown) { - revert o(); + revert SlashingTooFrequent(); } if (lastSlashBlock[verifier] == block.number) { @@ -776,6 +803,15 @@ contract VerifierSlashing is ResolverRoleTimelock, ReentrancyGuard, Pausable, Go reason, msg.sender ); + emit SlashExecutedV1( + 0, + verifier, + keccak256(bytes(reason)), + slashAmount, + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); + emit StakeSlashed(verifier, slashAmount, bytes32(0)); } @@ -784,12 +820,12 @@ contract VerifierSlashing is ResolverRoleTimelock, ReentrancyGuard, Pausable, Go */ function _routeSlashedTokens(uint256 amount) internal { if (amount == 0) return; - + address tokenAddress = stakingContract.stakingToken(); if (tokenAddress == address(0)) return; IERC20 token = IERC20(tokenAddress); - + uint256 toTreasury = (amount * pctTreasuryReserve) / 100; uint256 toSecurity = (amount * pctSecurityFund) / 100; uint256 toInsurance = (amount * pctProtocolInsurance) / 100; @@ -1072,9 +1108,29 @@ 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 + + ); } function unpause() external onlyRole(PAUSER_ROLE) { _unpause(); + emit EmergencyPauseRecoveredV1( + + msg.sender, + + uint64(block.timestamp), + + EVENT_SCHEMA_VERSION + + ); } } diff --git a/contracts/bootstrap/BootstrapController.sol b/contracts/bootstrap/BootstrapController.sol index 71dd25c..2dcdfde 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"; @@ -20,8 +21,6 @@ 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 { @@ -41,13 +40,13 @@ 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"); bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); bytes32 public constant DEPLOYER_ROLE = keccak256("DEPLOYER_ROLE"); - bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); // ============ Constants ============ @@ -471,9 +470,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/crosschain/CrossChainEndpoint.sol b/contracts/crosschain/CrossChainEndpoint.sol index 41fa322..442ffd4 100644 --- a/contracts/crosschain/CrossChainEndpoint.sol +++ b/contracts/crosschain/CrossChainEndpoint.sol @@ -21,7 +21,7 @@ contract CrossChainEndpoint is ICrossChainEndpoint, Ownable { // Relayer address that is authorized to process incoming messages (simplified for this architecture) mapping(address => bool) public authorizedRelayers; - constructor() Ownable() {} + constructor() Ownable(msg.sender) {} modifier onlySupportedChain(uint256 chainId) { require(supportedChains[chainId], "Unsupported chain"); diff --git a/contracts/deployment/MigrationManager.sol b/contracts/deployment/MigrationManager.sol index 8e13483..1fce593 100644 --- a/contracts/deployment/MigrationManager.sol +++ b/contracts/deployment/MigrationManager.sol @@ -5,14 +5,14 @@ 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"); - bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); - bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); struct Release { string version; @@ -168,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); @@ -214,4 +223,4 @@ contract MigrationManager is ReentrancyGuard, Pausable, GovernanceOwnable { function isModuleRegistered(bytes32 moduleId) external view returns (bool) { return registeredModules[moduleId]; } -} \ No newline at end of file +} 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/insurance/IInsuranceFund.sol b/contracts/insurance/IInsuranceFund.sol index 351314f..f462518 100644 --- a/contracts/insurance/IInsuranceFund.sol +++ b/contracts/insurance/IInsuranceFund.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + /** * @title IInsuranceFund * @notice Interface for the Protocol Insurance Fund & Loss Recovery Framework @@ -155,7 +157,7 @@ interface IInsuranceFund { // ============ View Functions ============ - function reserveToken() external view returns (address); + function reserveToken() external view returns (IERC20); function getReserveBalance() external view returns (uint256); diff --git a/contracts/insurance/InsuranceFund.sol b/contracts/insurance/InsuranceFund.sol index dac996d..1f91910 100644 --- a/contracts/insurance/InsuranceFund.sol +++ b/contracts/insurance/InsuranceFund.sol @@ -114,7 +114,6 @@ contract InsuranceFund is error InsufficientReserves(uint256 requested, uint256 available); error InvalidFundingAmount(); error ClaimNotApproved(uint256 claimId); - error ZeroAddress(); error BatchSizeExceeded(uint256 provided, uint256 maxAllowed); // ============ Constructor ============ diff --git a/contracts/interfaces/ITruthBountyEvents.sol b/contracts/interfaces/ITruthBountyEvents.sol new file mode 100644 index 0000000..221fc4c --- /dev/null +++ b/contracts/interfaces/ITruthBountyEvents.sol @@ -0,0 +1,136 @@ +// 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. Schema version 1 is +/// represented by the literal value `1` in each event's `version` field. +interface ITruthBountyEvents { + // Claims + event ClaimCreatedV1( + uint256 indexed claimId, + address indexed actor, + bytes32 indexed metadataHash, + uint64 timestamp, + uint16 version + ); + event ClaimUpdatedV1( + uint256 indexed claimId, + address indexed actor, + bytes32 indexed metadataHash, + uint64 timestamp, + uint16 version + ); + event ClaimResolvedV1( + uint256 indexed claimId, + address indexed actor, + bool outcome, + uint64 timestamp, + uint16 version + ); + event ClaimFinalizedV1( + uint256 indexed claimId, + address indexed actor, + uint64 timestamp, + uint16 version + ); + + // Verification + event VerificationSubmittedV1( + uint256 indexed claimId, + address indexed verifier, + bool support, + uint256 stakeAmount, + uint64 timestamp, + uint16 version + ); + event VerificationChallengedV1( + uint256 indexed claimId, + address indexed challenger, + bytes32 indexed reasonHash, + uint64 timestamp, + uint16 version + ); + + // Staking and slashing + event StakeDepositedV1( + address indexed verifier, + uint256 amount, + uint256 newBalance, + uint64 timestamp, + uint16 version + ); + event StakeWithdrawnV1( + address indexed verifier, + uint256 amount, + uint256 newBalance, + uint64 timestamp, + uint16 version + ); + event SlashExecutedV1( + uint256 indexed claimId, + address indexed verifier, + bytes32 indexed reason, + uint256 amount, + uint64 timestamp, + uint16 version + ); + + // Rewards and treasury + event RewardCalculatedV1( + bytes32 indexed calculationId, + address indexed recipient, + uint256 amount, + uint64 timestamp, + uint16 version + ); + event RewardEscrowedV1( + uint256 indexed claimId, + address indexed recipient, + uint256 amount, + uint64 timestamp, + uint16 version + ); + event RewardClaimedV1( + uint256 indexed claimId, + address indexed recipient, + uint256 amount, + uint64 timestamp, + uint16 version + ); + event TreasuryTransferV1( + bytes32 indexed operationId, + address indexed token, + address indexed recipient, + uint256 amount, + uint64 timestamp, + uint16 version + ); + + // Governance and emergency controls + event GovernanceProposalCreatedV1( + bytes32 indexed proposalId, + address indexed proposer, + bytes32 indexed metadataHash, + uint64 timestamp, + uint16 version + ); + event GovernanceProposalExecutedV1( + bytes32 indexed proposalId, + address indexed executor, + uint64 timestamp, + uint16 version + ); + event EmergencyPauseActivatedV1( + address indexed actor, + bytes32 indexed reason, + uint64 timestamp, + uint16 version + ); + event EmergencyPauseRecoveredV1( + address indexed actor, + uint64 timestamp, + uint16 version + ); +} diff --git a/contracts/mocks/EventArchitectureHarness.sol b/contracts/mocks/EventArchitectureHarness.sol new file mode 100644 index 0000000..82cbee6 --- /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 emitClaimCreatedV1( + uint256 claimId, + address actor, + bytes32 metadataHash + ) external { + emit ClaimCreatedV1( + claimId, + actor, + metadataHash, + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); + } + + function emitVerificationSubmittedV1( + uint256 claimId, + address verifier, + bool support, + uint256 stakeAmount + ) external { + emit VerificationSubmittedV1( + claimId, + verifier, + support, + stakeAmount, + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); + } + + function emitSlashExecutedV1( + uint256 claimId, + address verifier, + bytes32 reason, + uint256 amount + ) external { + emit SlashExecutedV1( + claimId, + verifier, + reason, + amount, + uint64(block.timestamp), + EVENT_SCHEMA_VERSION + ); + } +} diff --git a/contracts/reward/RewardEngine.sol b/contracts/reward/RewardEngine.sol index 4917be4..95d63e9 100644 --- a/contracts/reward/RewardEngine.sol +++ b/contracts/reward/RewardEngine.sol @@ -8,11 +8,11 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../governance/GovernanceOwnable.sol"; import "../governance/GovernanceHooks.sol"; +import "../interfaces/ITruthBountyEvents.sol"; import "../IReputationOracle.sol"; import "../treasury/ITreasuryAccounting.sol"; -import "../IReputationOracle.sol"; -contract RewardEngine is ReentrancyGuard, Pausable, GovernanceOwnable { +contract RewardEngine is ReentrancyGuard, Pausable, GovernanceOwnable, ITruthBountyEvents { using SafeERC20 for IERC20; // ============ Roles ============ @@ -23,6 +23,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; @@ -339,6 +340,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); } @@ -964,7 +972,7 @@ contract RewardEngine is ReentrancyGuard, Pausable, GovernanceOwnable { * @param recipient Address to receive the reward * @param amount Amount of reward to distribute */ - function distributeReward(address recipient, uint256 amount) external onlyRole(REWARD_ENGINE_ROLE) whenNotPaused { + function distributeReward(address recipient, uint256 amount) external onlyRole(DISTRIBUTOR_ROLE) whenNotPaused { require(recipient != address(0), "Invalid recipient"); require(amount > 0, "Cannot distribute 0"); require(address(treasuryAccounting) != address(0), "Treasury not configured"); @@ -977,9 +985,29 @@ contract RewardEngine 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/simulation/EconomicSimulation.sol b/contracts/simulation/EconomicSimulation.sol index 7abffe0..4f8c05f 100644 --- a/contracts/simulation/EconomicSimulation.sol +++ b/contracts/simulation/EconomicSimulation.sol @@ -205,7 +205,7 @@ contract EconomicSimulation is totalSettlements += settledToday; // ── Staking dynamics ─────────────────────────────────────── - uint256 totalStaked = stakers * config.minStakeAmount * (1e18 + verifierBonus) / 1e18; + uint256 totalStaked = stakers * gp.minStakeAmount * (1e18 + verifierBonus) / 1e18; // ── Settlement outcomes ──────────────────────────────────── // Simulate round-robin: some correct, some incorrect @@ -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; 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/ProtocolUpgradeable.sol b/contracts/upgrade/ProtocolUpgradeable.sol index 503cf8b..a92c6c6 100644 --- a/contracts/upgrade/ProtocolUpgradeable.sol +++ b/contracts/upgrade/ProtocolUpgradeable.sol @@ -27,7 +27,6 @@ abstract contract ProtocolUpgradeable is event ContractUpgraded(address indexed oldImpl, address indexed newImpl, string version); error UpgradeNotAuthorized(); - error ZeroAddress(); function _initializeProtocolUpgradeable( address _admin, @@ -36,7 +35,6 @@ abstract contract ProtocolUpgradeable is ) internal { require(_admin != address(0), ZeroAddress()); - __UUPSUpgradeable_init(); if (_upgradeController != address(0)) { upgradeController = IUpgradeController(_upgradeController); diff --git a/contracts/upgrade/VersionRegistry.sol b/contracts/upgrade/VersionRegistry.sol index 75055b5..a056334 100644 --- a/contracts/upgrade/VersionRegistry.sol +++ b/contracts/upgrade/VersionRegistry.sol @@ -116,8 +116,8 @@ contract VersionRegistry is IVersionRegistry, AccessControl { return versionList.entries[versionList.entries.length - 1]; } - function getVersion(string calldata contractName, string calldata semanticVersion) - external view override returns (VersionEntry memory) + function getVersion(string memory contractName, string memory semanticVersion) + public view override returns (VersionEntry memory) { if (!_contractRegistered[contractName]) revert ContractNotRegistered(contractName); VersionList storage versionList = _versions[contractName]; diff --git a/docs/event-architecture.md b/docs/event-architecture.md new file mode 100644 index 0000000..a3c37bd --- /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 `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. +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 + +- `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 + +- `VerificationSubmittedV1`: a verifier records a position and stake. +- `VerificationChallengedV1`: a verification or claim is challenged. + +### Staking and slashing + +- `StakeDepositedV1`: verifier collateral increases. +- `StakeWithdrawnV1`: verifier collateral decreases through a valid withdrawal. +- `SlashExecutedV1`: locked collateral is confiscated for a unique offence. + +### Rewards and treasury + +- `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 + +- `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 + +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. 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 diff --git a/docs/event-gas-benchmarks.md b/docs/event-gas-benchmarks.md new file mode 100644 index 0000000..5ee24a1 --- /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 + +- `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 + +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 diff --git a/docs/event-schema-v1.md b/docs/event-schema-v1.md new file mode 100644 index 0000000..a37e2d3 --- /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 + +- `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 + +- `VerificationSubmittedV1`: claim ID, verifier, support flag, stake amount, timestamp, version. +- `VerificationChallengedV1`: claim ID, challenger, reason hash, timestamp, version. + +## Staking and slashing + +- `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 + +- `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 + +- `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/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 diff --git a/hardhat.config.ts b/hardhat.config.ts index 15eab08..c300804 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -8,17 +8,16 @@ import "@openzeppelin/hardhat-upgrades"; dotenv.config(); const config: HardhatUserConfig = { - solidity: { +solidity: { version: "0.8.28", settings: { evmVersion: "cancun", viaIR: true, optimizer: { enabled: true, - runs: 200 + runs: 200, }, - viaIR: true - } + }, }, networks: { optimismSepolia: { diff --git a/test/EventArchitecture.test.ts b/test/EventArchitecture.test.ts new file mode 100644 index 0000000..5382fa1 --- /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 ClaimCreatedV1 event with indexed identifiers", async function () { + const { harness, actor } = await deployFixture(); + const metadataHash = ethers.keccak256(ethers.toUtf8Bytes("claim-metadata")); + + 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, "ClaimCreatedV1") + .withArgs(7, actor.address, metadataHash, BigInt(block!.timestamp), VERSION); + + const log = receipt!.logs.find((entry) => { + try { + return harness.interface.parseLog(entry)?.name === "ClaimCreatedV1"; + } 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.emitVerificationSubmittedV1(9, verifier.address, true, 1000)) + .to.emit(harness, "VerificationSubmittedV1") + .withArgs(9, verifier.address, true, 1000, anyUint64, VERSION); + + await expect(harness.emitSlashExecutedV1(9, verifier.address, reason, 250)) + .to.emit(harness, "SlashExecutedV1") + .withArgs(9, verifier.address, reason, 250, anyUint64, VERSION); + }); +}); + +function anyUint64(value: unknown): boolean { + return typeof value === "bigint" && value >= 0n && value <= (1n << 64n) - 1n; +}