// 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 { IRewardProgramV3 } from "./IRewardProgramV3.sol"; import { RewardProgramV3 } from "./RewardProgramV3.sol"; import { AccountabilityBondValidationV3 } from "./AccountabilityBondValidationV3.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 for a /// reward-free bond. A flourishing reward binds the exact metric /// schema hash and the sponsor program that funds it; it can /// never debit protected principal or the remedy reserve. bytes32 flourishRewardRulesHash; uint256 rewardProgramId; /// @dev Maximum flourishing reward this bond can ever receive. The /// amount is committed from the program reserve at creation. uint256 flourishRewardCap; 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 Protected principal actually deposited. Never debited by /// enforcement, metrics or reputation. uint256 principalFunded; /// @dev Separately funded objective remedy reserve. A proven breach /// pays the counterparty only from this bucket. uint256 remedyFunded; /// @dev Flourishing rewards already issued against the signed cap. uint256 flourishRewardIssued; /// @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; /// @notice Sponsor-funded flourishing reward program bound at deployment. /// @dev Zero disables reward terms; bonds then stay reward-free. IRewardProgramV3 public immutable rewardProgram; /// @notice Independent verifier that signs exact reward authorizations. /// @dev Governance-controlled. It receives no custody authority. address public flourishRewardIssuer; /// @dev Replay protection for verifier-signed reward authorizations. mapping(uint256 => mapping(uint96 => bool)) flourishRewardAuthorizationNonceUsed; 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 RewardProgramMismatch(); error UnauthorizedRewardIssuer(); error RewardCapExceeded(); /// @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 FlourishRewardIssuerSetV3(address indexed issuer); event FlourishRewardReleasedV3(uint256 indexed bondId, uint256 amount); event FlourishRewardReleaseFailedV3(uint256 indexed bondId, uint256 amount); event FlourishRewardIssuedV3(uint256 indexed bondId, address indexed beneficiary, uint256 amount); 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 _rewardProgram, address _consensus, address _disputeRegistry, uint256 _maxBondNativePrincipal, uint256 _maxAggregateNativeLiability, address _pilotController ) EIP712BaseV3("3.0.0") { if (_owner == address(0) || _consensus == address(0)) revert ZeroAddress(); if (_rewardProgram != address(0) && _rewardProgram.code.length == 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; rewardProgram = IRewardProgramV3(_rewardProgram); } /// @notice Binds the independent flourishing-reward verifier. /// @dev The verifier signs exact reward authorizations inside the signed /// cap and window. It never gains custody, principal or remedy /// authority, and rotating it never blocks value recovery. function setFlourishRewardIssuer(address issuer) external onlyOwner { if (issuer == address(0)) revert ZeroAddress(); flourishRewardIssuer = issuer; emit FlourishRewardIssuerSetV3(issuer); } /// @notice Exact, domain-separated flourishing-reward authorization signed /// by the independent verifier bound to the program policy. struct FlourishRewardAuthorization { address bond; uint256 chainId; uint256 bondId; address beneficiary; uint256 amount; uint96 nonce; uint64 deadline; } function hashFlourishRewardAuthorization(FlourishRewardAuthorization calldata a) public pure returns (bytes32) { return AccountabilityBondValidationV3.hashRewardAuthorization(a, FLOURISH_REWARD_AUTHORIZATION_TYPEHASH); } 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,uint256 flourishRewardCap,uint64 activePeriodStart,uint64 activePeriodEnd,uint64 challengeWindow,uint96 nonce,uint256 chainId,string protocolVersion)" ); bytes32 private constant FLOURISH_REWARD_AUTHORIZATION_TYPEHASH = keccak256( "FlourishRewardAuthorization(address bond,uint256 chainId,uint256 bondId,address beneficiary,uint256 amount,uint96 nonce,uint64 deadline)" ); 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 AccountabilityBondValidationV3.hashTerms(t, TERMS_TYPEHASH); } function hashEnforcementConfig(EnforcementConfig calldata c) public pure returns (bytes32) { return AccountabilityBondValidationV3.hashEnforcementConfig(c); } /// @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); AccountabilityBondValidationV3.validateTerms(t, enforcement, signers, rewardProgram); 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); if (t.rewardProgramId != 0) { // Commit the signed reward cap from the sponsor reserve so this // bond can never overbook the program. Failure reverts creation. rewardProgram.commit(t.rewardProgramId, bondId, t.flourishRewardCap, b.termsHash); } emit BondCreatedV3(bondId, t.aiCompany, t.counterparty, b.termsHash); } /// @notice Releases any unissued flourishing reward commitment when a bond /// reaches a terminal state without issuing it. function _releaseUnusedFlourishReward(uint256 bondId, BondData storage b) private { // Invoke only after custody effects and state transitions are complete. // A failing/reentrant sponsor callback must not repeat a principal exit. if (b.terms.rewardProgramId == 0 || address(rewardProgram) == address(0)) return; uint256 remaining = b.terms.flourishRewardCap - b.flourishRewardIssued; if (remaining != 0) { // A rotated or paused program must never block recovery of the // participants' funded value. A failed release only strands the // sponsor's reserve decision, never the bond's value. try rewardProgram.release{ gas: 100_000 }(b.terms.rewardProgramId, bondId, remaining) { emit FlourishRewardReleasedV3(bondId, remaining); } catch { emit FlourishRewardReleaseFailedV3(bondId, remaining); } } } /// @notice Executes a verifier-signed flourishing reward inside the signed /// cap and the signed active window. /// @dev The independent verifier signs the exact authorization; anyone may /// relay it. Issuance is replay-protected, deadline-bounded, capped /// and can never touch protected principal or the remedy reserve. function issueFlourishReward( uint256 bondId, address beneficiary, uint256 amount, FlourishRewardAuthorization calldata authorization, bytes calldata signature ) external { BondData storage b = _bonds[bondId]; if (b.terms.rewardProgramId == 0) revert BadTerms(); if (flourishRewardIssuer == address(0)) revert UnauthorizedRewardIssuer(); if (b.state != BondStateV3.State.Active) revert BadState(b.state); if (block.timestamp >= b.terms.activePeriodEnd) revert DeadlinePassed(); if (flourishRewardAuthorizationNonceUsed[bondId][authorization.nonce]) revert NonceUsed(); if ( authorization.bond != address(this) || authorization.chainId != block.chainid || authorization.bondId != bondId || authorization.beneficiary != beneficiary || authorization.amount != amount || authorization.deadline < block.timestamp ) revert InvalidEvidence(); bytes32 digest = _hashTypedData(hashFlourishRewardAuthorization(authorization)); if (!SignatureVerifierV3.verify(flourishRewardIssuer, digest, signature)) revert UnauthorizedRewardIssuer(); flourishRewardAuthorizationNonceUsed[bondId][authorization.nonce] = true; uint256 remaining = b.terms.flourishRewardCap - b.flourishRewardIssued; if (amount > remaining) revert RewardCapExceeded(); b.flourishRewardIssued += amount; rewardProgram.issue(b.terms.rewardProgramId, bondId, beneficiary, amount); emit FlourishRewardIssuedV3(bondId, beneficiary, amount); } 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(); // VF-27: the staker deposits protected principal and the separately // funded objective remedy reserve. A bond is Funded only when both are // complete; enforcement can never debit the principal bucket. uint256 requiredTotal = b.terms.companyStake + _enforcement[bondId].maximumSlash - b.fundedAmount; if (msg.value == 0 || msg.value > requiredTotal) revert Overfund(); if (msg.value > maxAggregateNativeLiability - nativeLiability) revert AggregateCapExceeded(); uint256 principalRemaining = b.terms.companyStake - b.principalFunded; uint256 toPrincipal = msg.value < principalRemaining ? msg.value : principalRemaining; b.principalFunded += toPrincipal; b.remedyFunded += msg.value - toPrincipal; b.fundedAmount += msg.value; nativeLiability += msg.value; if ( b.state == BondStateV3.State.Accepted && b.principalFunded == b.terms.companyStake && b.remedyFunded == _enforcement[bondId].maximumSlash ) { _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); _releaseUnusedFlourishReward(bondId, b); } 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); _releaseUnusedFlourishReward(bondId, b); } /// @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); _releaseUnusedFlourishReward(bondId, b); } function hashObjectiveEvidence(ObjectiveEvidence calldata evidence) public pure returns (bytes32) { return AccountabilityBondValidationV3.hashObjectiveEvidence(evidence, OBJECTIVE_EVIDENCE_TYPEHASH); } /// @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 { BondData storage b = _bonds[evidence.bondId]; EnforcementConfig storage c = _enforcement[evidence.bondId]; // Submission remains callable while paused for an already-active // position. The canonical source/codehash checks and immutable signed // response deadline still apply, so an owner pause cannot erase a // counterparty's existing bounded claim and cannot extend liability. // 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. (uint64 responseDeadline, uint64 challengeDeadline, uint64 decisionDeadline) = AccountabilityBondValidationV3 .validateObjectiveEvidence( evidence, b, c, evidenceNonceUsed[evidence.bondId][evidence.nonce], signers.length, signatures.length ); 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; if (b.remedyFunded < slash) revert BadState(b.state); uint256 refund = b.principalFunded; b.slashedAmount = slash; b.settlementSnapshot = finalSnapshot; // A proven breach pays the counterparty only from the separately // funded remedy reserve; protected principal refunds in full. _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); _releaseUnusedFlourishReward(bondId, b); } 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); _releaseUnusedFlourishReward(bondId, b); } function _canonicalBreachStillExists(EnforcementConfig storage c) private view returns (bool) { if (c.canonicalSource.codehash != c.canonicalSourceCodehash) return false; // An unavailable dependency is not proof of breach and must not trap // protected principal at finite expiry. Only a successful exact source // observation can settle a remedy; otherwise expiry enables forceExit. try IAccountabilityCanonicalSourceV3(c.canonicalSource).recordedAt(c.canonicalSourceId, c.obligationId) returns (uint64 timestamp) { return timestamp == 0 || timestamp > c.requiredBy; } catch { return false; } } 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); _releaseUnusedFlourishReward(bondId, b); } 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); _releaseUnusedFlourishReward(bondId, b); } 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); } }