// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IArbitrableV2, IArbitratorV2} from "@kleros/kleros-v2-contracts/interfaces/IArbitrableV2.sol";
import {IDisputeTemplateRegistry} from "@kleros/kleros-v2-contracts/interfaces/IDisputeTemplateRegistry.sol";
contract SimpleEscrow is IArbitrableV2 {
enum Status { Created, Reclaimed, Disputed, Resolved }
struct Transaction {
address payable sender;
address payable receiver;
uint256 amount;
uint256 disputeID;
Status status;
}
IArbitratorV2 public immutable arbitrator;
bytes public arbitratorExtraData;
uint256 public immutable feeTimeout;
uint256 public templateId;
mapping(uint256 => Transaction) public transactions;
mapping(uint256 => uint256) public disputeIDtoTxID;
uint256 public txCount;
// Ruling options: 0 = Refuse, 1 = Pay Sender, 2 = Pay Receiver
uint256 constant SENDER_WINS = 1;
uint256 constant RECEIVER_WINS = 2;
constructor(
IArbitratorV2 _arbitrator,
bytes memory _arbitratorExtraData,
uint256 _feeTimeout,
IDisputeTemplateRegistry _templateRegistry,
string memory _templateData,
string memory _templateDataMappings
) {
arbitrator = _arbitrator;
arbitratorExtraData = _arbitratorExtraData;
feeTimeout = _feeTimeout;
templateId = _templateRegistry.setDisputeTemplate(
"", _templateData, _templateDataMappings
);
}
/// @dev Create an escrow transaction.
function createTransaction(address payable _receiver) external payable returns (uint256 txID) {
require(msg.value > 0, "Must send ETH");
txID = txCount++;
transactions[txID] = Transaction({
sender: payable(msg.sender),
receiver: _receiver,
amount: msg.value,
disputeID: 0,
status: Status.Created
});
}
/// @dev Sender releases payment to receiver (both parties agree).
function pay(uint256 _txID) external {
Transaction storage tx_ = transactions[_txID];
require(msg.sender == tx_.sender, "Only sender");
require(tx_.status == Status.Created, "Wrong status");
tx_.status = Status.Resolved;
// Use .call() not .transfer() — .transfer() can fail with smart contract recipients
(bool ok,) = tx_.receiver.call{value: tx_.amount}("");
require(ok, "Transfer failed");
}
/// @dev Raise a dispute with Kleros Court.
function raiseDispute(uint256 _txID) external payable {
Transaction storage tx_ = transactions[_txID];
require(tx_.status == Status.Created, "Wrong status");
require(
msg.sender == tx_.sender || msg.sender == tx_.receiver,
"Only parties"
);
uint256 cost = arbitrator.arbitrationCost(arbitratorExtraData);
require(msg.value >= cost, "Insufficient fee");
tx_.status = Status.Disputed;
tx_.disputeID = arbitrator.createDispute{value: msg.value}(
2, // number of ruling options
arbitratorExtraData
);
disputeIDtoTxID[tx_.disputeID] = _txID;
emit DisputeRequest(
arbitrator,
tx_.disputeID,
_txID,
templateId,
""
);
}
/// @dev Called by the arbitrator when a ruling is final.
function rule(uint256 _disputeID, uint256 _ruling) external override {
require(msg.sender == address(arbitrator), "Only arbitrator");
uint256 txID = disputeIDtoTxID[_disputeID];
Transaction storage tx_ = transactions[txID];
require(tx_.status == Status.Disputed, "Not disputed");
tx_.status = Status.Resolved;
if (_ruling == SENDER_WINS) {
(bool ok,) = tx_.sender.call{value: tx_.amount}("");
require(ok, "Transfer failed");
} else if (_ruling == RECEIVER_WINS) {
(bool ok,) = tx_.receiver.call{value: tx_.amount}("");
require(ok, "Transfer failed");
} else {
// Ruling 0 = Refuse to Arbitrate — split equally as fallback
uint256 half = tx_.amount / 2;
(bool ok1,) = tx_.sender.call{value: half}("");
(bool ok2,) = tx_.receiver.call{value: tx_.amount - half}("");
require(ok1 && ok2, "Transfer failed");
}
emit Ruling(arbitrator, _disputeID, _ruling);
}
}