Files
smom-dbis-138/contracts/channels/GenericStateChannelManager.sol
2026-03-02 12:14:09 -08:00

247 lines
9.0 KiB
Solidity

// 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 {}
}