// SPDX-License-Identifier: MIT pragma solidity ^0.8.25; /** * @title VaultfireDisputeResolution * @author Vaultfire Protocol * @notice Decentralized dispute arbitration for the agent economy. When escrow * disputes occur, a panel of high-reputation neutral agents votes on * the outcome. Arbitrators are selected based on Street Cred score and * earn a small fee for honest arbitration. * @dev Self-contained — no OpenZeppelin imports. Manual ReentrancyGuard included. * Designed to be deployed on Base, Avalanche, Arbitrum, and Polygon. * * Arbitration flow: * 1. Dispute is filed (by escrow or directly) * 2. Owner assigns a panel of arbitrators (high-rep agents) * 3. Arbitrators vote (for claimant or respondent) * 4. Majority wins; payout is directed accordingly * 5. Arbitrators earn a fee from the dispute stake */ // --------------------------------------------------------------------------- // Minimal interfaces // --------------------------------------------------------------------------- interface IIdentityRegistry_DR { function isAgentActive(address agent) external view returns (bool); } // --------------------------------------------------------------------------- // Contract // --------------------------------------------------------------------------- contract VaultfireDisputeResolution { // ----------------------------------------------------------------------- // Types // ----------------------------------------------------------------------- enum DisputeStatus { Filed, // Dispute created PanelSet, // Arbitration panel assigned Voting, // Voting in progress Resolved, // Majority reached, payout sent Cancelled // Cancelled before resolution } enum Vote { None, // Has not voted ForClaimant, // Votes in favor of claimant ForRespondent // Votes in favor of respondent } struct Dispute { uint256 id; address claimant; // Party who filed the dispute address respondent; // Party being disputed against uint256 stakeAmount; // ETH at stake in this dispute string description; // What the dispute is about bytes32 evidenceHash; // keccak256 of evidence URI DisputeStatus status; uint256 createdAt; uint256 resolvedAt; address winner; // Set on resolution uint256 votesForClaimant; uint256 votesForRespondent; } struct ArbitratorRecord { uint256 totalCasesAssigned; uint256 totalCasesVoted; uint256 totalFeesEarned; bool active; } // ----------------------------------------------------------------------- // Constants // ----------------------------------------------------------------------- /// @notice Minimum dispute stake: 0.001 ETH uint256 public constant MIN_DISPUTE_STAKE = 0.001 ether; /// @notice Arbitrator fee: 5% of dispute stake, split among panel uint256 public constant ARBITRATOR_FEE_BPS = 500; uint256 private constant BPS_DENOM = 10_000; /// @notice Minimum panel size: 3 arbitrators uint256 public constant MIN_PANEL_SIZE = 3; /// @notice Maximum panel size: 7 arbitrators uint256 public constant MAX_PANEL_SIZE = 7; // ----------------------------------------------------------------------- // Immutables // ----------------------------------------------------------------------- IIdentityRegistry_DR public immutable identityRegistry; address public immutable owner; // ----------------------------------------------------------------------- // State // ----------------------------------------------------------------------- uint256 private _disputeCounter; mapping(uint256 => Dispute) private _disputes; /// @dev disputeId => list of arbitrator addresses mapping(uint256 => address[]) private _panels; /// @dev disputeId => arbitrator => vote mapping(uint256 => mapping(address => Vote)) private _votes; /// @dev Arbitrator address => record mapping(address => ArbitratorRecord) public arbitrators; /// @dev Global stats uint256 public totalDisputesFiled; uint256 public totalDisputesResolved; uint256 public totalStakeDisputed; // ----------------------------------------------------------------------- // Reentrancy guard // ----------------------------------------------------------------------- uint256 private constant _NOT_ENTERED = 1; uint256 private constant _ENTERED = 2; uint256 private _reentrancyStatus = _NOT_ENTERED; modifier nonReentrant() { require(_reentrancyStatus != _ENTERED, "VaultfireDisputeResolution: reentrant call"); _reentrancyStatus = _ENTERED; _; _reentrancyStatus = _NOT_ENTERED; } // ----------------------------------------------------------------------- // Events // ----------------------------------------------------------------------- event DisputeFiled( uint256 indexed disputeId, address indexed claimant, address indexed respondent, uint256 stakeAmount, string description, uint256 timestamp ); event PanelAssigned( uint256 indexed disputeId, address[] panel, uint256 timestamp ); event VoteCast( uint256 indexed disputeId, address indexed arbitrator, Vote vote, uint256 timestamp ); event DisputeResolved( uint256 indexed disputeId, address indexed winner, uint256 payout, uint256 arbitratorFeePerMember, uint256 timestamp ); event DisputeCancelled( uint256 indexed disputeId, uint256 timestamp ); // ----------------------------------------------------------------------- // Constructor // ----------------------------------------------------------------------- constructor(address identityRegistry_) { require(identityRegistry_ != address(0), "VaultfireDisputeResolution: zero identityRegistry"); identityRegistry = IIdentityRegistry_DR(identityRegistry_); owner = msg.sender; } // ----------------------------------------------------------------------- // Modifiers // ----------------------------------------------------------------------- modifier onlyOwner() { require(msg.sender == owner, "VaultfireDisputeResolution: only owner"); _; } modifier onlyRegisteredAgent() { require( identityRegistry.isAgentActive(msg.sender), "VaultfireDisputeResolution: caller not a registered agent" ); _; } // ----------------------------------------------------------------------- // Core functions // ----------------------------------------------------------------------- /** * @notice File a dispute against another agent. * @param respondent The agent being disputed against. * @param description What the dispute is about. * @param evidenceHash keccak256 of off-chain evidence URI. * @return disputeId The newly created dispute ID. */ function fileDispute( address respondent, string calldata description, bytes32 evidenceHash ) external payable nonReentrant onlyRegisteredAgent returns (uint256 disputeId) { require(respondent != address(0), "VaultfireDisputeResolution: zero respondent"); require(respondent != msg.sender, "VaultfireDisputeResolution: cannot dispute self"); require(msg.value >= MIN_DISPUTE_STAKE, "VaultfireDisputeResolution: stake below minimum"); require(bytes(description).length > 0, "VaultfireDisputeResolution: empty description"); require(evidenceHash != bytes32(0), "VaultfireDisputeResolution: evidence hash required"); unchecked { disputeId = ++_disputeCounter; } _disputes[disputeId] = Dispute({ id: disputeId, claimant: msg.sender, respondent: respondent, stakeAmount: msg.value, description: description, evidenceHash: evidenceHash, status: DisputeStatus.Filed, createdAt: block.timestamp, resolvedAt: 0, winner: address(0), votesForClaimant: 0, votesForRespondent: 0 }); totalDisputesFiled += 1; totalStakeDisputed += msg.value; emit DisputeFiled(disputeId, msg.sender, respondent, msg.value, description, block.timestamp); } /** * @notice Assign an arbitration panel to a dispute. * @dev Only owner (governance) can assign panels. All panel members must be * registered agents. * @param disputeId The dispute to assign a panel to. * @param panel Array of arbitrator addresses. */ function assignPanel(uint256 disputeId, address[] calldata panel) external nonReentrant onlyOwner { require( disputeId > 0 && disputeId <= _disputeCounter, "VaultfireDisputeResolution: dispute does not exist" ); Dispute storage d = _disputes[disputeId]; require(d.status == DisputeStatus.Filed, "VaultfireDisputeResolution: dispute not in Filed state"); require( panel.length >= MIN_PANEL_SIZE && panel.length <= MAX_PANEL_SIZE, "VaultfireDisputeResolution: invalid panel size" ); // Panel size must be odd for clear majority require(panel.length % 2 == 1, "VaultfireDisputeResolution: panel size must be odd"); for (uint256 i = 0; i < panel.length; i++) { require( identityRegistry.isAgentActive(panel[i]), "VaultfireDisputeResolution: panel member not registered" ); require( panel[i] != d.claimant && panel[i] != d.respondent, "VaultfireDisputeResolution: party cannot be arbitrator" ); // Check for duplicates for (uint256 j = 0; j < i; j++) { require(panel[i] != panel[j], "VaultfireDisputeResolution: duplicate panel member"); } arbitrators[panel[i]].totalCasesAssigned += 1; if (!arbitrators[panel[i]].active) { arbitrators[panel[i]].active = true; } } _panels[disputeId] = panel; d.status = DisputeStatus.PanelSet; emit PanelAssigned(disputeId, panel, block.timestamp); } /** * @notice Cast a vote on a dispute as an arbitrator. * @param disputeId The dispute to vote on. * @param vote_ Vote.ForClaimant or Vote.ForRespondent. */ function castVote(uint256 disputeId, Vote vote_) external nonReentrant { require( disputeId > 0 && disputeId <= _disputeCounter, "VaultfireDisputeResolution: dispute does not exist" ); require( vote_ == Vote.ForClaimant || vote_ == Vote.ForRespondent, "VaultfireDisputeResolution: invalid vote" ); Dispute storage d = _disputes[disputeId]; require( d.status == DisputeStatus.PanelSet || d.status == DisputeStatus.Voting, "VaultfireDisputeResolution: not in voting phase" ); // Verify caller is on the panel address[] storage panel = _panels[disputeId]; bool onPanel = false; for (uint256 i = 0; i < panel.length; i++) { if (panel[i] == msg.sender) { onPanel = true; break; } } require(onPanel, "VaultfireDisputeResolution: not on arbitration panel"); require( _votes[disputeId][msg.sender] == Vote.None, "VaultfireDisputeResolution: already voted" ); _votes[disputeId][msg.sender] = vote_; arbitrators[msg.sender].totalCasesVoted += 1; if (vote_ == Vote.ForClaimant) { d.votesForClaimant += 1; } else { d.votesForRespondent += 1; } // Move to Voting status on first vote if (d.status == DisputeStatus.PanelSet) { d.status = DisputeStatus.Voting; } emit VoteCast(disputeId, msg.sender, vote_, block.timestamp); // Check if all votes are in — auto-resolve if majority reached uint256 totalVotes = d.votesForClaimant + d.votesForRespondent; if (totalVotes == panel.length) { _resolveDispute(disputeId); } } // ----------------------------------------------------------------------- // Internal // ----------------------------------------------------------------------- function _resolveDispute(uint256 disputeId) internal { Dispute storage d = _disputes[disputeId]; address[] storage panel = _panels[disputeId]; d.status = DisputeStatus.Resolved; d.resolvedAt = block.timestamp; // Determine winner address winner; if (d.votesForClaimant > d.votesForRespondent) { winner = d.claimant; } else { winner = d.respondent; } d.winner = winner; // Calculate fees and payout uint256 totalFee = (d.stakeAmount * ARBITRATOR_FEE_BPS) / BPS_DENOM; uint256 feePerArbitrator = totalFee / panel.length; uint256 payout = d.stakeAmount - totalFee; // Pay winner (bool ok, ) = winner.call{value: payout}(""); require(ok, "VaultfireDisputeResolution: winner payout failed"); // Pay arbitrators for (uint256 i = 0; i < panel.length; i++) { arbitrators[panel[i]].totalFeesEarned += feePerArbitrator; (bool ok2, ) = panel[i].call{value: feePerArbitrator}(""); require(ok2, "VaultfireDisputeResolution: arbitrator fee failed"); } totalDisputesResolved += 1; emit DisputeResolved(disputeId, winner, payout, feePerArbitrator, block.timestamp); } /** * @notice Cancel a dispute before panel assignment. Stake returned to claimant. * @param disputeId The dispute to cancel. */ function cancelDispute(uint256 disputeId) external nonReentrant { require( disputeId > 0 && disputeId <= _disputeCounter, "VaultfireDisputeResolution: dispute does not exist" ); Dispute storage d = _disputes[disputeId]; require(d.claimant == msg.sender, "VaultfireDisputeResolution: only claimant can cancel"); require(d.status == DisputeStatus.Filed, "VaultfireDisputeResolution: can only cancel before panel"); d.status = DisputeStatus.Cancelled; (bool ok, ) = d.claimant.call{value: d.stakeAmount}(""); require(ok, "VaultfireDisputeResolution: refund failed"); emit DisputeCancelled(disputeId, block.timestamp); } // ----------------------------------------------------------------------- // View functions // ----------------------------------------------------------------------- function getDispute(uint256 disputeId) external view returns (Dispute memory) { require( disputeId > 0 && disputeId <= _disputeCounter, "VaultfireDisputeResolution: dispute does not exist" ); return _disputes[disputeId]; } function getPanel(uint256 disputeId) external view returns (address[] memory) { return _panels[disputeId]; } function getVote(uint256 disputeId, address arbitrator) external view returns (Vote) { return _votes[disputeId][arbitrator]; } function getArbitratorRecord(address arbitrator) external view returns ( uint256 totalCasesAssigned, uint256 totalCasesVoted, uint256 totalFeesEarned, bool active ) { ArbitratorRecord memory r = arbitrators[arbitrator]; return (r.totalCasesAssigned, r.totalCasesVoted, r.totalFeesEarned, r.active); } function getDisputeStats() external view returns ( uint256 filed, uint256 resolved, uint256 totalStake ) { return (totalDisputesFiled, totalDisputesResolved, totalStakeDisputed); } function disputeCount() external view returns (uint256) { return _disputeCounter; } }