// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; /** * @title AerePaymentChannels * @notice Bidirectional payment channels (L2-style state channels) for AERE Network. * @dev Two parties open a channel by depositing AERE. Off-chain they exchange signed * states (a, b, nonce) representing the current split of the channel balance. * Either party closes by submitting the latest co-signed state on-chain. A * challenge window allows the counterparty to override with a higher-nonce state. * After the challenge window expires, funds are settled. * * Whitepaper alignment: "L2 layers — state channels for instant micropayments" */ contract AerePaymentChannels is ReentrancyGuard { using ECDSA for bytes32; enum Status { None, Open, Closing, Closed } struct Channel { address partyA; address partyB; uint256 balanceA; uint256 balanceB; uint256 nonce; uint64 challengeDeadline; // 0 unless Closing Status status; } uint64 public constant CHALLENGE_PERIOD = 1 days; uint256 public nextChannelId = 1; mapping(uint256 => Channel) public channels; event ChannelOpened(uint256 indexed id, address indexed partyA, address indexed partyB, uint256 deposit); event ChannelFunded(uint256 indexed id, address indexed party, uint256 amount); event ChannelClosing(uint256 indexed id, uint256 nonce, uint256 balanceA, uint256 balanceB, uint64 deadline); event ChannelChallenged(uint256 indexed id, uint256 nonce, uint256 balanceA, uint256 balanceB); event ChannelClosed(uint256 indexed id, uint256 finalA, uint256 finalB); function open(address counterparty) external payable returns (uint256 id) { require(counterparty != address(0) && counterparty != msg.sender, "bad cp"); require(msg.value > 0, "deposit"); id = nextChannelId++; channels[id] = Channel({ partyA: msg.sender, partyB: counterparty, balanceA: msg.value, balanceB: 0, nonce: 0, challengeDeadline: 0, status: Status.Open }); emit ChannelOpened(id, msg.sender, counterparty, msg.value); } function fund(uint256 id) external payable { Channel storage c = channels[id]; require(c.status == Status.Open, "not open"); require(msg.value > 0, "amount"); if (msg.sender == c.partyA) c.balanceA += msg.value; else if (msg.sender == c.partyB) c.balanceB += msg.value; else revert("not party"); emit ChannelFunded(id, msg.sender, msg.value); } /** * @notice Initiate cooperative or unilateral close with a co-signed state. * @param id channel id * @param balanceA proposed final balance for partyA * @param balanceB proposed final balance for partyB * @param nonce state nonce (higher overrides lower during challenge) * @param sigA partyA signature over (this, id, balanceA, balanceB, nonce) * @param sigB partyB signature over the same digest */ function closeWithState( uint256 id, uint256 balanceA, uint256 balanceB, uint256 nonce, bytes calldata sigA, bytes calldata sigB ) external nonReentrant { Channel storage c = channels[id]; require(c.status == Status.Open || c.status == Status.Closing, "bad status"); require(balanceA + balanceB <= c.balanceA + c.balanceB, "overspend"); if (c.status == Status.Closing) require(nonce > c.nonce, "stale nonce"); bytes32 digest = _stateDigest(id, balanceA, balanceB, nonce); require(digest.recover(sigA) == c.partyA, "sigA"); require(digest.recover(sigB) == c.partyB, "sigB"); c.balanceA = balanceA; c.balanceB = balanceB; c.nonce = nonce; if (c.status == Status.Open) { c.status = Status.Closing; c.challengeDeadline = uint64(block.timestamp) + CHALLENGE_PERIOD; emit ChannelClosing(id, nonce, balanceA, balanceB, c.challengeDeadline); } else { emit ChannelChallenged(id, nonce, balanceA, balanceB); } } function settle(uint256 id) external nonReentrant { Channel storage c = channels[id]; require(c.status == Status.Closing, "not closing"); require(block.timestamp >= c.challengeDeadline, "challenge"); c.status = Status.Closed; uint256 a = c.balanceA; uint256 b = c.balanceB; c.balanceA = 0; c.balanceB = 0; if (a > 0) { (bool ok, ) = c.partyA.call{value: a}(""); require(ok, "A"); } if (b > 0) { (bool ok, ) = c.partyB.call{value: b}(""); require(ok, "B"); } emit ChannelClosed(id, a, b); } function _stateDigest(uint256 id, uint256 balanceA, uint256 balanceB, uint256 nonce) internal view returns (bytes32) { return keccak256( abi.encodePacked("AereChannel", address(this), block.chainid, id, balanceA, balanceB, nonce) ).toEthSignedMessageHash(); } }