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

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

/// @title PartnershipBondV3Safe
/// @notice Partnership bond with capped deposits, typed mutual
///         settlement, and time-safe principal-only exits.
/// @dev No deployment path in this contract uses generic consensus or
///      discretionary dispute conclusions to move custody.
contract PartnershipBondV3Safe is EIP712BaseV3, PullPaymentsV3 {
    string public constant FLOURISHING_MISSION =
        "Cooperation, accountable growth, freedom, and human/agent flourishing";
    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;
    /// @notice Deprecated legacy ABI value. It is never read by any custody path.
    bytes32 public constant SETTLEMENT_OUTCOME = keccak256("V3_PARTNERSHIP_SETTLEMENT");
    bytes32 public constant PROTOCOL_VERSION_HASH = keccak256("3.0.0");
    uint64 private constant MAX_CHALLENGE_WINDOW = 30 days;

    struct Terms {
        uint256 bondId;
        address partyA;
        address partyB;
        uint256 partyAStake;
        uint256 partyBStake;
        address stakeAsset;
        bytes32 partnershipType;
        /// @dev Legacy opaque field. Must be zero: only the closed rule class
        /// and schema version below can select a settlement predicate.
        bytes32 breachConditionsHash;
        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 flourishRewardConditionsHash;
        uint256 rewardProgramId;
        uint64 proposalDeadline;
        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 partyAFunded;
        uint256 partyBFunded;
        /// @dev Stores only mutually authorized split values.
        uint16 settlementHumanBps;
        uint16 settlementAiBps;
        uint16 settlementPartnershipBps;
        /// @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;
    /// @notice One mutually signed custody instruction may be consumed once per bond.
    mapping(uint256 => mapping(uint96 => bool)) public 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 Always address(0) in the initial pilot.
    /// @dev Kept for ABI compatibility only. Partnership bond principal never
    ///      calls, funds, commits to, or issues from RewardProgramV3.
    address public immutable rewardProgram;
    /// @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 is updated
    ///         in lockstep with native liability, including pull withdrawals.
    IPilotAccessControllerV3 public immutable pilotController;
    uint256 public immutable maxPartyNativePrincipal;
    uint256 public immutable maxBondNativePrincipal;
    uint256 public immutable maxAggregateNativeLiability;
    bool public paused;

    error NotOwner();
    error ZeroAddress();
    error Paused();
    error BadTerms();
    error BadSignature();
    error SelfBondForbidden();
    error NonceUsed();
    error BadChain();
    error BadState(BondStateV3.State current);
    error DeadlineNotReached();
    error DeadlinePassed();
    error NotParticipant();
    error Overfund();
    error InsufficientSurplus();
    error PrincipalCapExceeded();
    error AggregateCapExceeded();
    /// @notice Initial-pilot bonds cannot bind a reward-program contract.
    error RewardProgramDisabled();
    /// @notice Initial-pilot signed terms cannot represent reward behavior.
    error RewardTermsDisabled();
    /// @notice Initial-pilot principal settlement has no third-party share.
    error PartnershipShareDisabled();
    /// @notice A controller-bound pilot child must keep its registered design
    ///         partner in the signed participant pair.
    error DesignPartnerNotParticipant();
    /// @notice Base pilot custody may only move under both participants' typed authorization.
    error PilotMutualConsentOnly();
    error AuthorizationExpired();
    error AuthorizationUsed();
    error InvalidMutualSettlement();

    event BondCreatedV3(uint256 indexed bondId, address indexed partyA, address indexed partyB, bytes32 termsHash);
    event BondFundedV3(uint256 indexed bondId, address indexed party, 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 partyAAmount, uint256 partyBAmount, uint256 partnershipAmount);
    event BondCancelledV3(uint256 indexed bondId, address indexed by, string reason);
    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 partyAAmount,
        uint256 partyBAmount,
        uint96 indexed authorizationNonce,
        bytes32 authorizationHash
    );

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

    constructor(
        address _owner,
        address _rewardProgram,
        address _consensus,
        address _disputeRegistry,
        uint256 _maxPartyNativePrincipal,
        uint256 _maxBondNativePrincipal,
        uint256 _maxAggregateNativeLiability,
        address _pilotController
    ) EIP712BaseV3("3.0.0") {
        if (_owner == address(0) || _consensus == address(0)) revert ZeroAddress();
        if (_rewardProgram != address(0)) revert RewardProgramDisabled();
        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 (
            _maxPartyNativePrincipal == 0 || _maxBondNativePrincipal == 0 || _maxAggregateNativeLiability == 0
                || _maxPartyNativePrincipal > _maxBondNativePrincipal
                || _maxBondNativePrincipal > _maxAggregateNativeLiability
        ) revert BadTerms();
        owner = _owner;
        rewardProgram = _rewardProgram;
        consensus = _consensus;
        // Generic disputes are retired from the supported Base closure.
        _disputeRegistry;
        disputeRegistry = address(0);
        pilotController = IPilotAccessControllerV3(_pilotController);
        maxPartyNativePrincipal = _maxPartyNativePrincipal;
        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 public constant TERMS_TYPEHASH = keccak256(
        "Terms(uint256 bondId,address partyA,address partyB,uint256 partyAStake,uint256 partyBStake,address stakeAsset,bytes32 partnershipType,bytes32 breachConditionsHash,uint8 objectiveRuleClass,uint16 objectiveRuleSchemaVersion,bytes32 flourishRewardConditionsHash,uint256 rewardProgramId,uint64 proposalDeadline,uint64 activePeriodStart,uint64 activePeriodEnd,uint64 challengeWindow,uint96 nonce,uint256 chainId,string protocolVersion)"
    );
    bytes32 public constant MUTUAL_SETTLEMENT_TYPEHASH = keccak256(
        "MutualSettlement(address bond,uint256 chainId,uint256 bondId,bytes32 termsHash,uint256 partyAAmount,uint256 partyBAmount,uint96 nonce,uint64 deadline)"
    );

    /// @notice Exact, domain-separated instruction signed by both participants.
    struct MutualSettlement {
        address bond;
        uint256 chainId;
        uint256 bondId;
        bytes32 termsHash;
        uint256 partyAAmount;
        uint256 partyBAmount;
        uint96 nonce;
        uint64 deadline;
    }

    function hashTerms(Terms calldata t) public pure returns (bytes32) {
        return keccak256(
            abi.encode(
                TERMS_TYPEHASH,
                t.bondId,
                t.partyA,
                t.partyB,
                t.partyAStake,
                t.partyBStake,
                t.stakeAsset,
                t.partnershipType,
                t.breachConditionsHash,
                t.objectiveRuleClass,
                t.objectiveRuleSchemaVersion,
                t.flourishRewardConditionsHash,
                t.rewardProgramId,
                t.proposalDeadline,
                t.activePeriodStart,
                t.activePeriodEnd,
                t.challengeWindow,
                t.nonce,
                t.chainId,
                keccak256(bytes(t.protocolVersion))
            )
        );
    }

    function createBond(Terms calldata t, bytes calldata sigA, bytes calldata sigB)
        external
        whenNotPaused
        returns (uint256 bondId)
    {
        if (t.chainId != block.chainid) revert BadChain();
        if (_nonceUsed[t.partyA][t.nonce] || _nonceUsed[t.partyB][t.nonce]) revert NonceUsed();
        if (t.bondId != nextBondId) revert BadTerms();
        if (t.partyA == address(0) || t.partyB == address(0)) revert ZeroAddress();
        if (t.partyA == t.partyB) revert SelfBondForbidden();
        _requireRegisteredDesignPartner(t.partyA, t.partyB);
        if (
            t.stakeAsset != address(0) || (t.partyAStake == 0 && t.partyBStake == 0) || t.partnershipType == bytes32(0)
                || t.breachConditionsHash != bytes32(0) || t.objectiveRuleClass != 0
                || t.objectiveRuleSchemaVersion != 0 || t.challengeWindow == 0
                || t.challengeWindow > MAX_CHALLENGE_WINDOW
                || keccak256(bytes(t.protocolVersion)) != PROTOCOL_VERSION_HASH
        ) {
            revert BadTerms();
        }
        if (t.flourishRewardConditionsHash != bytes32(0) || t.rewardProgramId != 0) revert RewardTermsDisabled();
        if (
            t.partyAStake > maxPartyNativePrincipal || t.partyBStake > maxPartyNativePrincipal
                || t.partyAStake > maxBondNativePrincipal - t.partyBStake
        ) {
            revert PrincipalCapExceeded();
        }
        if (
            t.proposalDeadline <= block.timestamp || t.activePeriodStart < t.proposalDeadline
                || t.activePeriodEnd <= t.activePeriodStart
                || uint256(t.activePeriodEnd) + (uint256(t.challengeWindow) * 2) > type(uint64).max
        ) {
            revert BadTerms();
        }
        bytes32 digest = _hashTypedData(hashTerms(t));
        if (!SignatureVerifierV3.verify(t.partyA, digest, sigA) || !SignatureVerifierV3.verify(t.partyB, digest, sigB))
        {
            revert BadSignature();
        }
        _nonceUsed[t.partyA][t.nonce] = true;
        _nonceUsed[t.partyB][t.nonce] = true;
        bondId = nextBondId++;
        BondData storage b = _bonds[bondId];
        b.terms = t;
        b.termsHash = hashTerms(t);
        _transitionTo(bondId, BondStateV3.State.Proposed);
        _transitionTo(bondId, BondStateV3.State.Accepted);
        emit BondCreatedV3(bondId, t.partyA, t.partyB, b.termsHash);
    }

    function fundNative(uint256 bondId) external payable whenNotPaused {
        BondData storage b = _bonds[bondId];
        if (b.terms.stakeAsset != address(0)) revert BadTerms();
        if (b.state != BondStateV3.State.Accepted && b.state != BondStateV3.State.Funded) revert BadState(b.state);
        // Funding is permitted only during [creation, proposalDeadline). At
        // proposalDeadline, expireUnfunded is already available to return any
        // partial principal, so accepting funds would revive expired terms.
        if (block.timestamp >= b.terms.proposalDeadline) revert DeadlinePassed();
        uint256 remaining;
        if (msg.sender == b.terms.partyA) {
            remaining = b.terms.partyAStake - b.partyAFunded;
            if (msg.value == 0 || msg.value > remaining) revert Overfund();
            b.partyAFunded += msg.value;
        } else if (msg.sender == b.terms.partyB) {
            remaining = b.terms.partyBStake - b.partyBFunded;
            if (msg.value == 0 || msg.value > remaining) revert Overfund();
            b.partyBFunded += msg.value;
        } else {
            revert NotParticipant();
        }
        if (msg.value > maxAggregateNativeLiability - nativeLiability) revert AggregateCapExceeded();
        nativeLiability += msg.value;
        if (
            b.state == BondStateV3.State.Accepted && b.partyAFunded == b.terms.partyAStake
                && b.partyBFunded == b.terms.partyBStake
        ) {
            _transitionTo(bondId, BondStateV3.State.Funded);
            b.fundedAt = uint64(block.timestamp);
        }
        emit BondFundedV3(bondId, msg.sender, msg.value);
        if (address(pilotController) != address(0)) {
            pilotController.increasePositionAllocation(address(this), msg.value);
        }
    }

    /// @dev A factory-registered Base pilot child has a permanent controller
    ///      association. The current allowlist is also checked so a rotated or
    ///      revoked partner cannot leave an old child accepting new value.
    ///      Controller-free V3 deployments retain their existing behavior.
    function _requireRegisteredDesignPartner(address partyA, address partyB) private view {
        if (address(pilotController) == address(0)) return;
        address designPartner = pilotController.positionDesignPartner(address(this));
        if (
            designPartner == address(0) || !pilotController.isDesignPartner(designPartner)
                || (partyA != designPartner && partyB != designPartner)
        ) revert DesignPartnerNotParticipant();
    }

    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.partyA && msg.sender != b.terms.partyB) revert NotParticipant();
        if (b.state != BondStateV3.State.Accepted && b.state != BondStateV3.State.Funded) revert BadState(b.state);
        _refundPrincipal(b);
        _transitionTo(bondId, BondStateV3.State.Cancelled);
        emit BondCancelledV3(bondId, msg.sender, "pre-active");
    }

    /// @notice Permissionless expiry for an accepted, partly-funded proposal.
    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.proposalDeadline) revert DeadlineNotReached();
        _refundPrincipal(b);
        _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
    ///      funders 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();
        _refundPrincipal(b);
        _transitionTo(bondId, BondStateV3.State.Expired);
        _transitionTo(bondId, BondStateV3.State.ForceExited);
        emit BondExpiredV3(bondId);
        emit ForceExitedV3(bondId, msg.sender);
    }

    function markExpired(uint256 bondId) external {
        BondData storage b = _bonds[bondId];
        if (b.state != BondStateV3.State.Active) revert BadState(b.state);
        if (block.timestamp < b.terms.activePeriodEnd) revert DeadlineNotReached();
        _transitionTo(bondId, BondStateV3.State.Expired);
        emit BondExpiredV3(bondId);
    }

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

    /// @notice Disabled legacy generic custody entry points retained only for ABI compatibility.
    function requestSettlement(uint256) external pure {
        revert PilotMutualConsentOnly();
    }

    function challengeSettlement(uint256, uint256) external pure {
        revert PilotMutualConsentOnly();
    }

    function advanceToFinalizable(uint256) external pure {
        revert PilotMutualConsentOnly();
    }

    function cancelStalledSettlement(uint256) external pure {
        revert PilotMutualConsentOnly();
    }

    function resolveChallenge(uint256) external pure {
        revert PilotMutualConsentOnly();
    }

    function settlementSnapshot(uint256, BondMathLibV3.Split calldata) external pure returns (bytes32) {
        revert PilotMutualConsentOnly();
    }

    function freezeSettlement(uint256, uint96, BondMathLibV3.Split calldata) external pure {
        revert PilotMutualConsentOnly();
    }

    function settle(uint256) external pure {
        revert PilotMutualConsentOnly();
    }

    function hashMutualSettlement(MutualSettlement calldata authorization) public pure returns (bytes32) {
        return keccak256(
            abi.encode(
                MUTUAL_SETTLEMENT_TYPEHASH,
                authorization.bond,
                authorization.chainId,
                authorization.bondId,
                authorization.termsHash,
                authorization.partyAAmount,
                authorization.partyBAmount,
                authorization.nonce,
                authorization.deadline
            )
        );
    }

    /// @notice Settles an active bond only on matching typed authorization from
    ///         both participants. No score, evidence commitment, threshold vote,
    ///         or adjudicator result can alter this Base-pilot custody outcome.
    function settleByMutualConsent(MutualSettlement calldata authorization, bytes calldata sigA, bytes calldata sigB)
        external
        whenNotPaused
    {
        if (authorization.bond != address(this) || authorization.chainId != block.chainid) revert BadChain();
        if (block.timestamp > authorization.deadline) revert AuthorizationExpired();
        BondData storage b = _bonds[authorization.bondId];
        if (mutualSettlementNonceUsed[authorization.bondId][authorization.nonce]) revert AuthorizationUsed();
        if (b.state != BondStateV3.State.Active) revert BadState(b.state);
        if (block.timestamp >= b.terms.activePeriodEnd) revert DeadlinePassed();
        uint256 pool = b.partyAFunded + b.partyBFunded;
        if (authorization.termsHash != b.termsHash || authorization.partyAAmount + authorization.partyBAmount != pool) {
            revert InvalidMutualSettlement();
        }
        bytes32 digest = _hashTypedData(hashMutualSettlement(authorization));
        if (
            !SignatureVerifierV3.verify(b.terms.partyA, digest, sigA)
                || !SignatureVerifierV3.verify(b.terms.partyB, digest, sigB)
        ) revert BadSignature();

        mutualSettlementNonceUsed[authorization.bondId][authorization.nonce] = true;
        b.settlementSnapshot = digest;
        _transitionTo(authorization.bondId, BondStateV3.State.SettlementRequested);
        _transitionTo(authorization.bondId, BondStateV3.State.Finalizable);
        _transitionTo(authorization.bondId, BondStateV3.State.SettlementFrozen);
        if (authorization.partyAAmount > 0) _credit(b.terms.stakeAsset, b.terms.partyA, authorization.partyAAmount);
        if (authorization.partyBAmount > 0) _credit(b.terms.stakeAsset, b.terms.partyB, authorization.partyBAmount);
        _transitionTo(authorization.bondId, BondStateV3.State.Settled);
        emit MutualSettlementExecutedV3(
            authorization.bondId, authorization.partyAAmount, authorization.partyBAmount, authorization.nonce, digest
        );
        emit BondSettledV3(authorization.bondId, authorization.partyAAmount, authorization.partyBAmount, 0);
    }

    /// @notice Disabled dispute hooks retained only as fail-closed ABI compatibility stubs.
    function disputeSubject(uint256) external pure returns (bytes32) {
        revert PilotMutualConsentOnly();
    }

    function settlementChallengeContext(uint256) external pure returns (bytes32, uint64, uint64) {
        revert PilotMutualConsentOnly();
    }

    function isAuthorizedChallenger(uint256, address) external pure returns (bool) {
        revert PilotMutualConsentOnly();
    }

    function authorizedRespondent(uint256, address) external pure returns (address) {
        revert PilotMutualConsentOnly();
    }

    function disputePayoutSource(uint256) external pure returns (address) {
        revert PilotMutualConsentOnly();
    }

    function disputePayoutCap(uint256) external pure returns (uint256) {
        revert PilotMutualConsentOnly();
    }

    function releaseDisputePayout(uint256, address, uint256) external pure {
        revert PilotMutualConsentOnly();
    }

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

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

    function _refundPrincipal(BondData storage b) internal {
        if (b.partyAFunded > 0) _credit(b.terms.stakeAsset, b.terms.partyA, b.partyAFunded);
        if (b.partyBFunded > 0) _credit(b.terms.stakeAsset, b.terms.partyB, b.partyBFunded);
    }

    function _baseSettlementSnapshot(uint256 bondId, BondData storage b) internal view returns (bytes32) {
        return keccak256(
            abi.encode(address(this), bondId, b.termsHash, b.partyAFunded, b.partyBFunded, b.settlementRequestedAt)
        );
    }

    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);
    }

    function skimSurplus(address asset, address to, uint256 amount) external onlyOwner {
        if (to == address(0)) revert ZeroAddress();
        if (asset == address(0)) {
            uint256 balance = address(this).balance;
            if (balance < nativeLiability || amount > balance - nativeLiability) revert InsufficientSurplus();
            (bool ok,) = payable(to).call{ value: amount }("");
            if (!ok) revert("skim failed");
        } else {
            (bool ok, bytes memory ret) = asset.call(abi.encodeWithSignature("transfer(address,uint256)", to, amount));
            if (!ok || (ret.length != 0 && !abi.decode(ret, (bool)))) revert("skim failed");
        }
    }

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