aere-contracts/contracts/AerePaymentChannels.sol
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 01:02:37 +03:00

128 lines
5.1 KiB
Solidity

// 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();
}
}