// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import {IGenericStateChannelManager} from "./IGenericStateChannelManager.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; /** * @title GenericStateChannelManager * @notice State channels with committed stateHash: open, fund, close with (stateHash, balanceA, balanceB, nonce). stateHash attests to arbitrary off-chain state (e.g. game, attestation). * @dev Same lifecycle as PaymentChannelManager; close/submit/challenge include stateHash so settlement commits to agreed state. */ contract GenericStateChannelManager is IGenericStateChannelManager, ReentrancyGuard { address public admin; bool public paused; uint256 public challengeWindowSeconds; uint256 private _nextChannelId; mapping(uint256 => Channel) private _channels; uint256[] private _channelIds; modifier onlyAdmin() { require(msg.sender == admin, "only admin"); _; } modifier whenNotPaused() { require(!paused, "paused"); _; } constructor(address _admin, uint256 _challengeWindowSeconds) { require(_admin != address(0), "zero admin"); require(_challengeWindowSeconds > 0, "zero challenge window"); admin = _admin; challengeWindowSeconds = _challengeWindowSeconds; } function openChannel(address participantB) external payable whenNotPaused returns (uint256 channelId) { require(participantB != address(0), "zero participant"); require(participantB != msg.sender, "self channel"); require(msg.value > 0, "zero deposit"); channelId = _nextChannelId++; _channels[channelId] = Channel({ participantA: msg.sender, participantB: participantB, depositA: msg.value, depositB: 0, status: ChannelStatus.Open, disputeNonce: 0, disputeStateHash: bytes32(0), disputeBalanceA: 0, disputeBalanceB: 0, disputeDeadline: 0 }); _channelIds.push(channelId); emit ChannelOpened(channelId, msg.sender, participantB, msg.value, 0); return channelId; } function fundChannel(uint256 channelId) external payable whenNotPaused { Channel storage ch = _channels[channelId]; require(ch.status == ChannelStatus.Open, "not open"); require(ch.depositB == 0, "already funded"); require(msg.sender == ch.participantB, "not participant B"); require(msg.value > 0, "zero deposit"); ch.depositB = msg.value; emit ChannelOpened(channelId, ch.participantA, ch.participantB, ch.depositA, ch.depositB); } function closeChannelCooperative( uint256 channelId, bytes32 stateHash, uint256 nonce, uint256 balanceA, uint256 balanceB, uint8 vA, bytes32 rA, bytes32 sA, uint8 vB, bytes32 rB, bytes32 sB ) external nonReentrant { Channel storage ch = _channels[channelId]; require(ch.status == ChannelStatus.Open, "not open"); uint256 total = ch.depositA + ch.depositB; require(balanceA + balanceB == total, "balance sum"); _verifySignatures(channelId, stateHash, ch.participantA, ch.participantB, nonce, balanceA, balanceB, vA, rA, sA, vB, rB, sB); ch.status = ChannelStatus.Closed; _transfer(ch.participantA, balanceA); _transfer(ch.participantB, balanceB); emit ChannelClosed(channelId, stateHash, balanceA, balanceB, true); } function submitClose( uint256 channelId, bytes32 stateHash, uint256 nonce, uint256 balanceA, uint256 balanceB, uint8 vA, bytes32 rA, bytes32 sA, uint8 vB, bytes32 rB, bytes32 sB ) external { Channel storage ch = _channels[channelId]; require(ch.status == ChannelStatus.Open || ch.status == ChannelStatus.Dispute, "wrong status"); uint256 total = ch.depositA + ch.depositB; require(balanceA + balanceB == total, "balance sum"); _verifySignatures(channelId, stateHash, ch.participantA, ch.participantB, nonce, balanceA, balanceB, vA, rA, sA, vB, rB, sB); if (ch.status == ChannelStatus.Open) { ch.status = ChannelStatus.Dispute; } else { require(nonce > ch.disputeNonce, "older state"); } ch.disputeNonce = nonce; ch.disputeStateHash = stateHash; ch.disputeBalanceA = balanceA; ch.disputeBalanceB = balanceB; ch.disputeDeadline = block.timestamp + challengeWindowSeconds; emit ChallengeSubmitted(channelId, nonce, stateHash, balanceA, balanceB, ch.disputeDeadline); } function challengeClose( uint256 channelId, bytes32 stateHash, uint256 nonce, uint256 balanceA, uint256 balanceB, uint8 vA, bytes32 rA, bytes32 sA, uint8 vB, bytes32 rB, bytes32 sB ) external { Channel storage ch = _channels[channelId]; require(ch.status == ChannelStatus.Dispute, "not in dispute"); uint256 total = ch.depositA + ch.depositB; require(balanceA + balanceB == total, "balance sum"); require(nonce > ch.disputeNonce, "not newer"); _verifySignatures(channelId, stateHash, ch.participantA, ch.participantB, nonce, balanceA, balanceB, vA, rA, sA, vB, rB, sB); ch.disputeNonce = nonce; ch.disputeStateHash = stateHash; ch.disputeBalanceA = balanceA; ch.disputeBalanceB = balanceB; ch.disputeDeadline = block.timestamp + challengeWindowSeconds; emit ChallengeSubmitted(channelId, nonce, stateHash, balanceA, balanceB, ch.disputeDeadline); } function finalizeClose(uint256 channelId) external nonReentrant { Channel storage ch = _channels[channelId]; require(ch.status == ChannelStatus.Dispute, "not in dispute"); require(block.timestamp >= ch.disputeDeadline, "window open"); ch.status = ChannelStatus.Closed; _transfer(ch.participantA, ch.disputeBalanceA); _transfer(ch.participantB, ch.disputeBalanceB); emit ChannelClosed(channelId, ch.disputeStateHash, ch.disputeBalanceA, ch.disputeBalanceB, false); } function getChannel(uint256 channelId) external view returns (Channel memory) { return _channels[channelId]; } function getChannelCount() external view returns (uint256) { return _channelIds.length; } function getChannelId(address participantA, address participantB) external view returns (uint256) { for (uint256 i = 0; i < _channelIds.length; i++) { Channel storage ch = _channels[_channelIds[i]]; if ( (ch.participantA == participantA && ch.participantB == participantB) || (ch.participantA == participantB && ch.participantB == participantA) ) { if (ch.status == ChannelStatus.Open || ch.status == ChannelStatus.Dispute) { return _channelIds[i]; } } } return 0; } function getChannelIdByIndex(uint256 index) external view returns (uint256) { require(index < _channelIds.length, "out of bounds"); return _channelIds[index]; } function setAdmin(address newAdmin) external onlyAdmin { require(newAdmin != address(0), "zero admin"); admin = newAdmin; emit AdminChanged(newAdmin); } function setChallengeWindow(uint256 newWindow) external onlyAdmin { require(newWindow > 0, "zero window"); uint256 old = challengeWindowSeconds; challengeWindowSeconds = newWindow; emit ChallengeWindowUpdated(old, newWindow); } function pause() external onlyAdmin { paused = true; emit Paused(); } function unpause() external onlyAdmin { paused = false; emit Unpaused(); } function _verifySignatures( uint256 channelId, bytes32 stateHash, address participantA, address participantB, uint256 nonce, uint256 balanceA, uint256 balanceB, uint8 vA, bytes32 rA, bytes32 sA, uint8 vB, bytes32 rB, bytes32 sB ) internal pure { bytes32 stateHashInner = keccak256(abi.encodePacked(channelId, stateHash, nonce, balanceA, balanceB)); bytes32 ethSigned = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", stateHashInner)); address signerA = ECDSA.recover(ethSigned, vA, rA, sA); address signerB = ECDSA.recover(ethSigned, vB, rB, sB); require(signerA == participantA && signerB == participantB, "invalid sigs"); } function _transfer(address to, uint256 amount) internal { if (amount > 0) { (bool ok,) = payable(to).call{value: amount}(""); require(ok, "transfer failed"); } } receive() external payable {} }