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.
128 lines
5.1 KiB
Solidity
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();
|
|
}
|
|
}
|