// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import { BondStateV3 } from "./BondStateV3.sol";
import { PullPaymentsV3 } from "./PullPaymentsV3.sol";
import { EIP712BaseV3 } from "../signatures/EIP712BaseV3.sol";
import { SignatureVerifierV3 } from "../signatures/SignatureVerifierV3.sol";
import { IPilotAccessControllerV3 } from "../pilot/IPilotAccessControllerV3.sol";
import { IAccountabilityCanonicalSourceV3 } from "./IAccountabilityCanonicalSourceV3.sol";
import { AccountabilityCanonicalSourceV3 } from "./AccountabilityCanonicalSourceV3.sol";

/// @title AccountabilityBondV3Safe
/// @notice Accountability bond with a closed, pre-agreed missed-deadline rule,
///         typed threshold attestations, and permissionless timeout recovery.
/// @dev The only enforcement path accepts the exact v1 canonical
///      record schema below. Signers attest facts; this contract computes the
///      outcome. No score, profile, generic evidence, or discretionary remedy
///      is accepted.
contract AccountabilityBondV3Safe is EIP712BaseV3, PullPaymentsV3 {
    bytes32 public constant FLOURISHING_NON_CONTROL_BOUNDARY =
        keccak256("FLOURISHING_IS_A_MISSION_OUTCOME_NOT_A_SCORE_OR_PROTECTED_DECISION_INPUT");
    using BondStateV3 for BondStateV3.State;

    uint256 private constant BASE_CHAIN_ID = 8453;
    uint256 private constant BASE_SEPOLIA_CHAIN_ID = 84532;
    bytes32 private constant PROTOCOL_VERSION_HASH = keccak256("3.0.0");
    uint64 private constant MAX_CHALLENGE_WINDOW = 30 days;
    uint8 private constant OBJECTIVE_RULE_CLASS = 1;
    uint16 private constant OBJECTIVE_RULE_SCHEMA_VERSION = 1;
    bytes32 private constant RULE_SCHEMA_HASH = keccak256("Vaultfire.AccountabilityRule.MissedCanonicalDeadline.v1");
    bytes32 private constant PERMITTED_EVIDENCE_SCHEMA_HASH =
        keccak256("Vaultfire.ObjectiveEvidence.CanonicalRecordedAt.v1");
    bytes32 private constant PRIVACY_EXCLUSIONS_HASH = keccak256(
        "NO_BEHAVIORAL_SCORING|NO_REPUTATION|NO_LOYALTY|NO_FLOURISHING|NO_QUALITY|NO_BELIEF|NO_BIOMETRIC|NO_PROFILING|NO_SURVEILLANCE"
    );

    /// @notice Closed v1 rule configuration committed by both participants.
    /// @dev A breach exists iff the canonical record was absent (`recordedAt=0`)
    ///      or recorded strictly after `requiredBy`. A proven breach always
    ///      applies exactly `maximumSlash`; no signer chooses a remedy.
    struct EnforcementConfig {
        address canonicalSource;
        bytes32 canonicalSourceCodehash;
        bytes32 canonicalSourceId;
        bytes32 obligationId;
        uint64 requiredBy;
        uint64 responseWindow;
        uint64 challengeWindow;
        uint64 decisionWindow;
        uint256 maximumSlash;
        bytes32 evidenceSchemaHash;
        bytes32 privacyExclusionsHash;
        bytes32 attesterSetHash;
        bytes32 adjudicatorSetHash;
        uint8 attesterThreshold;
        uint8 adjudicatorThreshold;
    }

    struct SignerConfig {
        address[] attesters;
        address[] adjudicators;
    }

    /// @notice Exact EIP-712 factual payload signed by the pre-agreed attesters.
    struct ObjectiveEvidence {
        address bond;
        uint256 chainId;
        uint256 bondId;
        bytes32 termsHash;
        bytes32 ruleHash;
        address canonicalSource;
        bytes32 canonicalSourceId;
        bytes32 obligationId;
        uint64 requiredBy;
        uint64 recordedAt;
        uint64 observedAt;
        uint96 nonce;
        uint64 signatureDeadline;
    }

    struct EnforcementState {
        bytes32 evidenceHash;
        bytes32 challengeHash;
        uint64 requestedAt;
        uint64 responseDeadline;
        uint64 respondedAt;
        uint64 challengeDeadline;
        uint64 challengedAt;
        uint64 decisionDeadline;
        bool resolved;
    }

    struct Terms {
        uint256 bondId;
        address aiCompany;
        address counterparty;
        uint256 companyStake;
        address stakeAsset;
        /// @dev Legacy opaque field. Must be zero: arbitrary mission content
        /// may not be a custody-affecting term.
        bytes32 missionHash;
        /// @dev Legacy opaque field. Must be zero: only the closed rule class
        /// and schema version below can select a settlement predicate.
        bytes32 objectiveBreachRulesHash;
        uint8 objectiveRuleClass;
        uint16 objectiveRuleSchemaVersion;
        /// @dev Reserved compatibility fields. Both must be zero in the
        ///      initial pilot because no reward program is wired to a bond.
        bytes32 flourishRewardRulesHash;
        uint256 rewardProgramId;
        uint64 activePeriodStart;
        uint64 activePeriodEnd;
        uint64 challengeWindow;
        uint96 nonce;
        uint256 chainId;
        string protocolVersion;
    }

    struct BondData {
        Terms terms;
        bytes32 termsHash;
        BondStateV3.State state;
        uint64 fundedAt;
        /// @dev Deprecated compatibility slot. Legacy settlement entry points
        ///      always revert and this slot is never written.
        uint64 settlementRequestedAt;
        uint256 fundedAmount;
        /// @dev Deprecated compatibility slot. Never written or read by custody.
        bool upheldBreachRecorded;
        /// @dev Stores only a mutually authorized counterparty payout.
        uint256 slashedAmount;
        /// @dev Stores only a mutual-settlement EIP-712 digest.
        bytes32 settlementSnapshot;
        /// @dev Deprecated compatibility slots. Disputes cannot affect custody
        ///      and none of these slots are written or read.
        uint256 challengeDisputeId;
        address challengeChallenger;
        bytes32 challengeSettlementSnapshot;
        uint64 challengeSettlementRequestedAt;
        uint64 challengeFiledAt;
    }

    mapping(uint256 => BondData) internal _bonds;
    mapping(address => mapping(uint96 => bool)) internal _nonceUsed;
    /// @dev Deprecated storage slot retained for layout compatibility.
    mapping(uint256 => mapping(uint96 => bool)) internal mutualSettlementNonceUsed;
    uint256 public nextBondId = 1;
    /// @notice Native principal still held by the contract or owed as a pull-payment credit.
    /// @dev This is the aggregate-cap occupancy. It intentionally does not fall
    ///      when principal is merely converted into a withdrawal credit.
    uint256 public nativeLiability;
    address public owner;
    /// @notice Legacy constructor bindings retained for address-level compatibility only; they have no custody authority.
    address public immutable consensus;
    address public immutable disputeRegistry;
    /// @notice Base pilot controller. It is required on Base and follows the
    ///         same outstanding-principal lifecycle as nativeLiability.
    IPilotAccessControllerV3 public immutable pilotController;
    uint256 public immutable maxBondNativePrincipal;
    uint256 public immutable maxAggregateNativeLiability;
    bool public paused;
    /// @dev New enforcement storage is appended after every legacy top-level
    ///      variable and kept outside BondData so `bondOf` preserves its V3 ABI.
    mapping(uint256 => mapping(uint96 => bool)) internal evidenceNonceUsed;
    mapping(uint256 => EnforcementConfig) internal _enforcement;
    mapping(uint256 => EnforcementState) internal _enforcementState;

    error NotOwner();
    error ZeroAddress();
    error Paused();
    error BadTerms();
    error BadSignature();
    error NonceUsed();
    error BadChain();
    error BadState(BondStateV3.State current);
    error NotParticipant();
    error DeadlineNotReached();
    error DeadlinePassed();
    error Overfund();
    error PrincipalCapExceeded();
    error AggregateCapExceeded();
    /// @notice Initial-pilot signed terms cannot represent reward behavior.
    error RewardTermsDisabled();
    /// @notice A controller-bound pilot child must bind the registered design
    ///         partner as the value-bearing company.
    error DesignPartnerNotCompany();
    error EnforcementRequired();
    error InvalidRule();
    error InvalidEvidence();
    error CanonicalSourceMismatch();

    event BondCreatedV3(
        uint256 indexed bondId, address indexed aiCompany, address indexed counterparty, bytes32 termsHash
    );
    event BondFundedV3(uint256 indexed bondId, uint256 amount);
    event BondActivatedV3(uint256 indexed bondId);
    event BondStateChangedV3(uint256 indexed bondId, BondStateV3.State from, BondStateV3.State to, address indexed by);
    /// @notice Emitted only by a valid mutual-consent settlement.
    event BondSettledV3(uint256 indexed bondId, uint256 refundedToCompany, uint256 paidToCounterparty);
    event BondExpiredV3(uint256 indexed bondId);
    event ForceExitedV3(uint256 indexed bondId, address indexed by);
    event PausedV3(address indexed by);
    event UnpausedV3(address indexed by);
    event MutualSettlementExecutedV3(
        uint256 indexed bondId,
        uint256 companyRefund,
        uint256 counterpartySlash,
        uint96 indexed authorizationNonce,
        bytes32 authorizationHash
    );
    event ObjectiveEvidenceAcceptedV3(
        uint256 indexed bondId,
        bytes32 indexed evidenceHash,
        uint64 responseDeadline,
        uint64 challengeDeadline,
        uint64 decisionDeadline
    );

    modifier onlyOwner() {
        if (msg.sender != owner) revert NotOwner();
        _;
    }
    modifier whenNotPaused() {
        if (paused) revert Paused();
        _;
    }

    constructor(
        address _owner,
        address _consensus,
        address _disputeRegistry,
        uint256 _maxBondNativePrincipal,
        uint256 _maxAggregateNativeLiability,
        address _pilotController
    ) EIP712BaseV3("3.0.0") {
        if (_owner == address(0) || _consensus == address(0)) revert ZeroAddress();
        if (
            (block.chainid == BASE_CHAIN_ID || block.chainid == BASE_SEPOLIA_CHAIN_ID) && _pilotController == address(0)
        ) {
            revert ZeroAddress();
        }
        if (_pilotController != address(0) && _pilotController.code.length == 0) revert ZeroAddress();
        if (
            _pilotController != address(0)
                && IPilotAccessControllerV3(_pilotController).requiredChainId() != block.chainid
        ) {
            revert BadTerms();
        }
        if (
            _maxBondNativePrincipal == 0 || _maxAggregateNativeLiability == 0
                || _maxBondNativePrincipal > _maxAggregateNativeLiability
        ) revert BadTerms();
        owner = _owner;
        consensus = _consensus;
        // Generic disputes are retired from the supported Base closure.
        _disputeRegistry;
        disputeRegistry = address(0);
        pilotController = IPilotAccessControllerV3(_pilotController);
        maxBondNativePrincipal = _maxBondNativePrincipal;
        maxAggregateNativeLiability = _maxAggregateNativeLiability;
    }

    function pause() external onlyOwner {
        paused = true;
        emit PausedV3(msg.sender);
    }

    function unpause() external onlyOwner {
        paused = false;
        emit UnpausedV3(msg.sender);
    }

    bytes32 private constant TERMS_TYPEHASH = keccak256(
        "Terms(uint256 bondId,address aiCompany,address counterparty,uint256 companyStake,address stakeAsset,bytes32 missionHash,bytes32 objectiveBreachRulesHash,uint8 objectiveRuleClass,uint16 objectiveRuleSchemaVersion,bytes32 flourishRewardRulesHash,uint256 rewardProgramId,uint64 activePeriodStart,uint64 activePeriodEnd,uint64 challengeWindow,uint96 nonce,uint256 chainId,string protocolVersion)"
    );
    bytes32 private constant OBJECTIVE_EVIDENCE_TYPEHASH = keccak256(
        "ObjectiveEvidence(address bond,uint256 chainId,uint256 bondId,bytes32 termsHash,bytes32 ruleHash,address canonicalSource,bytes32 canonicalSourceId,bytes32 obligationId,uint64 requiredBy,uint64 recordedAt,uint64 observedAt,uint96 nonce,uint64 signatureDeadline)"
    );

    function hashTerms(Terms calldata t) public pure returns (bytes32) {
        return keccak256(
            abi.encode(
                TERMS_TYPEHASH,
                t.bondId,
                t.aiCompany,
                t.counterparty,
                t.companyStake,
                t.stakeAsset,
                t.missionHash,
                t.objectiveBreachRulesHash,
                t.objectiveRuleClass,
                t.objectiveRuleSchemaVersion,
                t.flourishRewardRulesHash,
                t.rewardProgramId,
                t.activePeriodStart,
                t.activePeriodEnd,
                t.challengeWindow,
                t.nonce,
                t.chainId,
                keccak256(bytes(t.protocolVersion))
            )
        );
    }

    function hashEnforcementConfig(EnforcementConfig calldata c) public pure returns (bytes32) {
        return keccak256(
            abi.encode(
                RULE_SCHEMA_HASH,
                c.canonicalSource,
                c.canonicalSourceCodehash,
                c.canonicalSourceId,
                c.obligationId,
                c.requiredBy,
                c.responseWindow,
                c.challengeWindow,
                c.decisionWindow,
                c.maximumSlash,
                c.evidenceSchemaHash,
                c.privacyExclusionsHash,
                c.attesterSetHash,
                c.adjudicatorSetHash,
                c.attesterThreshold,
                c.adjudicatorThreshold
            )
        );
    }

    /// @notice Legacy mutual-only creation shape is fail-closed. Accountability
    ///         bonds must explicitly bind a complete enforcement configuration.
    function createBond(Terms calldata, bytes calldata, bytes calldata) external pure returns (uint256) {
        revert EnforcementRequired();
    }

    function createBond(
        Terms calldata t,
        EnforcementConfig calldata enforcement,
        SignerConfig calldata signers,
        bytes calldata sigCompany,
        bytes calldata sigCounterparty
    ) external whenNotPaused returns (uint256 bondId) {
        if (t.chainId != block.chainid) revert BadChain();
        if (_nonceUsed[t.aiCompany][t.nonce] || _nonceUsed[t.counterparty][t.nonce]) revert NonceUsed();
        if (t.bondId != nextBondId) revert BadTerms();
        if (t.aiCompany == address(0) || t.counterparty == address(0)) revert ZeroAddress();
        _requireRegisteredDesignPartner(t.aiCompany);
        if (
            t.stakeAsset != address(0) || t.aiCompany == t.counterparty || t.companyStake == 0
                || t.missionHash != bytes32(0) || t.objectiveBreachRulesHash != hashEnforcementConfig(enforcement)
                || t.objectiveRuleClass != OBJECTIVE_RULE_CLASS
                || t.objectiveRuleSchemaVersion != OBJECTIVE_RULE_SCHEMA_VERSION
                || t.activePeriodStart <= block.timestamp || t.activePeriodEnd <= t.activePeriodStart
                || t.challengeWindow == 0 || t.challengeWindow > MAX_CHALLENGE_WINDOW
                || keccak256(bytes(t.protocolVersion)) != PROTOCOL_VERSION_HASH
        ) revert BadTerms();
        _validateEnforcement(t, enforcement, signers);
        if (t.flourishRewardRulesHash != bytes32(0) || t.rewardProgramId != 0) revert RewardTermsDisabled();
        if (t.companyStake > maxBondNativePrincipal) revert PrincipalCapExceeded();
        bytes32 digest = _hashTypedData(hashTerms(t));
        if (
            !SignatureVerifierV3.verify(t.aiCompany, digest, sigCompany)
                || !SignatureVerifierV3.verify(t.counterparty, digest, sigCounterparty)
        ) revert BadSignature();
        _nonceUsed[t.aiCompany][t.nonce] = true;
        _nonceUsed[t.counterparty][t.nonce] = true;
        bondId = nextBondId++;
        BondData storage b = _bonds[bondId];
        b.terms = t;
        b.termsHash = hashTerms(t);
        _enforcement[bondId] = enforcement;
        _transitionTo(bondId, BondStateV3.State.Proposed);
        _transitionTo(bondId, BondStateV3.State.Accepted);
        emit BondCreatedV3(bondId, t.aiCompany, t.counterparty, b.termsHash);
    }

    function _validateEnforcement(Terms calldata t, EnforcementConfig calldata c, SignerConfig calldata signers)
        private
        view
    {
        if (
            c.canonicalSource == address(0) || c.canonicalSourceId == bytes32(0) || c.obligationId == bytes32(0)
                || c.canonicalSource.code.length == 0 || c.canonicalSource.codehash != c.canonicalSourceCodehash
                || c.canonicalSourceCodehash != keccak256(type(AccountabilityCanonicalSourceV3).runtimeCode)
                || c.obligationId != keccak256(abi.encode(t.aiCompany, c.canonicalSourceId))
                || c.requiredBy < t.activePeriodStart || c.requiredBy >= t.activePeriodEnd || c.responseWindow == 0
                || c.responseWindow > MAX_CHALLENGE_WINDOW || c.challengeWindow != t.challengeWindow
                || c.decisionWindow == 0 || c.decisionWindow > MAX_CHALLENGE_WINDOW || c.maximumSlash == 0
                || c.maximumSlash > t.companyStake || c.evidenceSchemaHash != PERMITTED_EVIDENCE_SCHEMA_HASH
                || c.privacyExclusionsHash != PRIVACY_EXCLUSIONS_HASH
        ) revert InvalidRule();
        uint256 totalWindows = uint256(c.responseWindow) + c.challengeWindow + c.decisionWindow;
        // The complete process must end strictly before activePeriodEnd so the
        // half-open enforcement interval and expiry interval never overlap.
        if (uint256(c.requiredBy) + totalWindows >= t.activePeriodEnd) revert InvalidRule();
        if (
            signers.attesters.length != 0 || signers.adjudicators.length != 0 || c.attesterSetHash != bytes32(0)
                || c.adjudicatorSetHash != bytes32(0) || c.attesterThreshold != 0 || c.adjudicatorThreshold != 0
                || IAccountabilityCanonicalSourceV3(c.canonicalSource).SOURCE_SCHEMA() != c.evidenceSchemaHash
        ) revert InvalidRule();
    }

    function fundNative(uint256 bondId) external payable whenNotPaused {
        BondData storage b = _bonds[bondId];
        if (b.terms.stakeAsset != address(0)) revert BadTerms();
        if (msg.sender != b.terms.aiCompany) revert NotParticipant();
        if (b.state != BondStateV3.State.Accepted && b.state != BondStateV3.State.Funded) revert BadState(b.state);
        // Funding is permitted only before activePeriodStart. At that exact
        // boundary expireUnfunded can return partial principal, so terms
        // cannot be revived after their signed funding window closes.
        if (block.timestamp >= b.terms.activePeriodStart) revert DeadlinePassed();
        if (msg.value == 0 || msg.value > b.terms.companyStake - b.fundedAmount) revert Overfund();
        if (msg.value > maxAggregateNativeLiability - nativeLiability) revert AggregateCapExceeded();
        b.fundedAmount += msg.value;
        nativeLiability += msg.value;
        if (b.state == BondStateV3.State.Accepted && b.fundedAmount == b.terms.companyStake) {
            _transitionTo(bondId, BondStateV3.State.Funded);
            b.fundedAt = uint64(block.timestamp);
        }
        emit BondFundedV3(bondId, msg.value);
        if (address(pilotController) != address(0)) {
            pilotController.increasePositionAllocation(address(this), msg.value);
        }
    }

    /// @dev Accountability principal is supplied only by `aiCompany`, so the
    ///      registered pilot partner is bound to that role. The immutable
    ///      position association and current allowlist are both required.
    ///      Controller-free V3 deployments retain their existing behavior.
    function _requireRegisteredDesignPartner(address aiCompany) private view {
        if (address(pilotController) == address(0)) return;
        address designPartner = pilotController.positionDesignPartner(address(this));
        if (
            designPartner == address(0) || !pilotController.isDesignPartner(designPartner) || aiCompany != designPartner
        ) revert DesignPartnerNotCompany();
    }

    function activate(uint256 bondId) external whenNotPaused {
        BondData storage b = _bonds[bondId];
        if (b.state != BondStateV3.State.Funded) revert BadState(b.state);
        if (block.timestamp < b.terms.activePeriodStart) revert DeadlineNotReached();
        // The active period is half-open: [activePeriodStart, activePeriodEnd).
        // At activePeriodEnd an active bond may markExpired, while a funded
        // bond that never activated becomes permissionlessly recoverable.
        // Neither path may be bypassed by reviving an expired agreement.
        if (block.timestamp >= b.terms.activePeriodEnd) revert DeadlinePassed();
        _transitionTo(bondId, BondStateV3.State.Active);
        emit BondActivatedV3(bondId);
    }

    function cancelBeforeActivation(uint256 bondId) external {
        BondData storage b = _bonds[bondId];
        if (msg.sender != b.terms.aiCompany && msg.sender != b.terms.counterparty) revert NotParticipant();
        if (b.state != BondStateV3.State.Accepted && b.state != BondStateV3.State.Funded) revert BadState(b.state);
        if (b.fundedAmount > 0) _credit(b.terms.stakeAsset, b.terms.aiCompany, b.fundedAmount);
        _transitionTo(bondId, BondStateV3.State.Cancelled);
    }

    function expireUnfunded(uint256 bondId) external {
        BondData storage b = _bonds[bondId];
        if (b.state != BondStateV3.State.Accepted) revert BadState(b.state);
        if (block.timestamp < b.terms.activePeriodStart) revert DeadlineNotReached();
        if (b.fundedAmount > 0) _credit(b.terms.stakeAsset, b.terms.aiCompany, b.fundedAmount);
        _transitionTo(bondId, BondStateV3.State.Expired);
        _transitionTo(bondId, BondStateV3.State.ForceExited);
        emit BondExpiredV3(bondId);
        emit ForceExitedV3(bondId, msg.sender);
    }

    /// @notice Permissionless recovery for a fully funded bond that was never
    ///         activated before the signed active-period window closed.
    /// @dev Callable while paused. Principal is credited to the original
    ///      funder and remains subject to pull-payment withdrawal.
    function expireUnactivated(uint256 bondId) external {
        BondData storage b = _bonds[bondId];
        if (b.state != BondStateV3.State.Funded) revert BadState(b.state);
        if (block.timestamp < b.terms.activePeriodEnd) revert DeadlineNotReached();
        if (b.fundedAmount > 0) _credit(b.terms.stakeAsset, b.terms.aiCompany, b.fundedAmount);
        _transitionTo(bondId, BondStateV3.State.Expired);
        _transitionTo(bondId, BondStateV3.State.ForceExited);
        emit BondExpiredV3(bondId);
        emit ForceExitedV3(bondId, msg.sender);
    }

    function hashObjectiveEvidence(ObjectiveEvidence calldata evidence) public pure returns (bytes32) {
        return keccak256(
            abi.encode(
                OBJECTIVE_EVIDENCE_TYPEHASH,
                evidence.bond,
                evidence.chainId,
                evidence.bondId,
                evidence.termsHash,
                evidence.ruleHash,
                evidence.canonicalSource,
                evidence.canonicalSourceId,
                evidence.obligationId,
                evidence.requiredBy,
                evidence.recordedAt,
                evidence.observedAt,
                evidence.nonce,
                evidence.signatureDeadline
            )
        );
    }

    /// @notice Executes the sole unilateral enforcement path after reading the
    ///         exact codehash-pinned canonical source on-chain. The empty array
    ///         arguments are retained only to make old clients fail explicitly;
    ///         threshold signatures have no authority over the result.
    function submitObjectiveEvidence(
        ObjectiveEvidence calldata evidence,
        address[] calldata signers,
        bytes[] calldata signatures
    ) external whenNotPaused {
        BondData storage b = _bonds[evidence.bondId];
        EnforcementConfig storage c = _enforcement[evidence.bondId];
        if (signers.length != 0 || signatures.length != 0) revert InvalidEvidence();
        if (b.state != BondStateV3.State.Active) revert BadState(b.state);
        if (evidenceNonceUsed[evidence.bondId][evidence.nonce]) revert NonceUsed();
        if (block.timestamp < c.requiredBy) revert DeadlineNotReached();
        uint64 responseDeadline = c.requiredBy + c.responseWindow;
        uint64 challengeDeadline = responseDeadline + c.challengeWindow;
        uint64 decisionDeadline = challengeDeadline + c.decisionWindow;
        // Enforcement owns [requiredBy, activePeriodEnd); expiry exclusively
        // owns [activePeriodEnd, infinity). Evidence must enter during its
        // signed response window, so no transaction ordering can change the
        // economic outcome at the active-period boundary.
        if (block.timestamp >= responseDeadline || block.timestamp >= b.terms.activePeriodEnd) revert DeadlinePassed();
        if (
            evidence.bond != address(this) || evidence.chainId != block.chainid || evidence.termsHash != b.termsHash
                || evidence.ruleHash != b.terms.objectiveBreachRulesHash
        ) revert InvalidEvidence();
        if (
            evidence.canonicalSource != c.canonicalSource || evidence.canonicalSourceId != c.canonicalSourceId
                || evidence.obligationId != c.obligationId || evidence.requiredBy != c.requiredBy
                || evidence.observedAt < c.requiredBy || evidence.observedAt > block.timestamp
                || (evidence.recordedAt != 0 && evidence.recordedAt <= c.requiredBy)
                || evidence.signatureDeadline < block.timestamp || evidence.signatureDeadline > responseDeadline
        ) revert InvalidEvidence();
        if (c.canonicalSource.codehash != c.canonicalSourceCodehash) revert CanonicalSourceMismatch();
        uint64 canonicalRecordedAt =
            IAccountabilityCanonicalSourceV3(c.canonicalSource).recordedAt(c.canonicalSourceId, c.obligationId);
        if (canonicalRecordedAt != evidence.recordedAt) revert CanonicalSourceMismatch();
        if (canonicalRecordedAt != 0 && canonicalRecordedAt <= c.requiredBy) revert InvalidEvidence();

        bytes32 evidenceHash = _hashTypedData(hashObjectiveEvidence(evidence));
        evidenceNonceUsed[evidence.bondId][evidence.nonce] = true;

        EnforcementState storage e = _enforcementState[evidence.bondId];
        e.evidenceHash = evidenceHash;
        e.requestedAt = uint64(block.timestamp);
        e.responseDeadline = responseDeadline;
        e.challengeDeadline = challengeDeadline;
        e.decisionDeadline = decisionDeadline;
        b.settlementRequestedAt = uint64(block.timestamp);
        b.settlementSnapshot = evidenceHash;
        _transitionTo(evidence.bondId, BondStateV3.State.SettlementRequested);
        emit ObjectiveEvidenceAcceptedV3(
            evidence.bondId, evidenceHash, responseDeadline, challengeDeadline, decisionDeadline
        );
    }

    /// @notice Permissionlessly advances exactly one signed enforcement stage.
    /// @dev SettlementRequested closes at responseDeadline, Finalizable closes
    ///      at challengeDeadline, and SettlementFrozen settles at decisionDeadline.
    function advanceObjectiveEvidence(uint256 bondId) external whenNotPaused {
        BondData storage b = _bonds[bondId];
        EnforcementState storage e = _enforcementState[bondId];
        if (block.timestamp >= b.terms.activePeriodEnd) revert DeadlinePassed();
        if (b.state == BondStateV3.State.SettlementRequested) {
            if (block.timestamp < e.responseDeadline) revert DeadlineNotReached();
            if (!_canonicalBreachStillExists(_enforcement[bondId])) {
                _dismissObjectiveEvidence(bondId, b, e);
                return;
            }
            _transitionTo(bondId, BondStateV3.State.Finalizable);
            return;
        }
        if (b.state == BondStateV3.State.Finalizable) {
            if (block.timestamp < e.challengeDeadline) revert DeadlineNotReached();
            if (!_canonicalBreachStillExists(_enforcement[bondId])) {
                _dismissObjectiveEvidence(bondId, b, e);
                return;
            }
            _transitionTo(bondId, BondStateV3.State.SettlementFrozen);
            return;
        }
        if (b.state != BondStateV3.State.SettlementFrozen) revert BadState(b.state);
        if (block.timestamp < e.decisionDeadline) revert DeadlineNotReached();
        if (!_canonicalBreachStillExists(_enforcement[bondId])) {
            _dismissObjectiveEvidence(bondId, b, e);
            return;
        }
        e.resolved = true;
        _settleEnforcement(bondId, b, e.evidenceHash);
    }

    function _settleEnforcement(uint256 bondId, BondData storage b, bytes32 finalSnapshot) private {
        uint256 slash = _enforcement[bondId].maximumSlash;
        uint256 refund = b.fundedAmount - slash;
        b.slashedAmount = slash;
        b.settlementSnapshot = finalSnapshot;
        if (slash > 0) _credit(b.terms.stakeAsset, b.terms.counterparty, slash);
        if (refund > 0) _credit(b.terms.stakeAsset, b.terms.aiCompany, refund);
        _transitionTo(bondId, BondStateV3.State.Settled);
        emit BondSettledV3(bondId, refund, slash);
    }

    function markExpired(uint256 bondId) external {
        BondData storage b = _bonds[bondId];
        if (
            b.state != BondStateV3.State.Active && b.state != BondStateV3.State.SettlementRequested
                && b.state != BondStateV3.State.Finalizable && b.state != BondStateV3.State.SettlementFrozen
        ) revert BadState(b.state);
        if (block.timestamp < b.terms.activePeriodEnd) revert DeadlineNotReached();
        if (b.state != BondStateV3.State.Active) {
            EnforcementState storage e = _enforcementState[bondId];
            if (_canonicalBreachStillExists(_enforcement[bondId])) {
                if (b.state == BondStateV3.State.SettlementRequested) {
                    _transitionTo(bondId, BondStateV3.State.Finalizable);
                }
                if (b.state == BondStateV3.State.Finalizable) {
                    _transitionTo(bondId, BondStateV3.State.SettlementFrozen);
                }
                e.resolved = true;
                _settleEnforcement(bondId, b, e.evidenceHash);
                return;
            }
        }
        _transitionTo(bondId, BondStateV3.State.Expired);
        emit BondExpiredV3(bondId);
    }

    function _canonicalBreachStillExists(EnforcementConfig storage c) private view returns (bool) {
        if (c.canonicalSource.codehash != c.canonicalSourceCodehash) return false;
        uint64 canonicalRecordedAt =
            IAccountabilityCanonicalSourceV3(c.canonicalSource).recordedAt(c.canonicalSourceId, c.obligationId);
        return canonicalRecordedAt == 0 || canonicalRecordedAt > c.requiredBy;
    }

    function _dismissObjectiveEvidence(uint256 bondId, BondData storage b, EnforcementState storage e) private {
        e.resolved = true;
        if (b.fundedAmount > 0) _credit(b.terms.stakeAsset, b.terms.aiCompany, b.fundedAmount);
        _transitionTo(bondId, BondStateV3.State.Cancelled);
    }

    function forceExit(uint256 bondId) external {
        BondData storage b = _bonds[bondId];
        if (b.state != BondStateV3.State.Expired) revert BadState(b.state);
        if (b.fundedAmount > 0) _credit(b.terms.stakeAsset, b.terms.aiCompany, b.fundedAmount);
        _transitionTo(bondId, BondStateV3.State.ForceExited);
        emit ForceExitedV3(bondId, msg.sender);
    }

    function nonceUsed(address party, uint96 nonce) external view returns (bool) {
        return _nonceUsed[party][nonce];
    }

    function bondOf(uint256 bondId) external view returns (BondData memory) {
        return _bonds[bondId];
    }

    function enforcementOf(uint256 bondId) external view returns (EnforcementConfig memory) {
        return _enforcement[bondId];
    }

    function enforcementStateOf(uint256 bondId) external view returns (EnforcementState memory) {
        return _enforcementState[bondId];
    }

    function _afterCreditWithdrawal(address asset, uint256 amount) internal override {
        if (asset == address(0)) {
            nativeLiability -= amount;
            if (address(pilotController) != address(0)) {
                pilotController.releasePositionAllocation(address(this), amount);
            }
        }
    }

    function _transitionTo(uint256 bondId, BondStateV3.State to) internal {
        BondStateV3.State from = _bonds[bondId].state;
        BondStateV3.requireTransition(from, to);
        _bonds[bondId].state = to;
        emit BondStateChangedV3(bondId, from, to, msg.sender);
    }
}
