aere-contracts/contracts/depin/AereComputeMarket.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

227 lines
9.1 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
/**
* @title AereComputeMarket — DePIN compute coordination layer
* @notice The ON-CHAIN coordination + settlement layer for a decentralized
* compute marketplace (DePIN-for-AI). Providers stake native AERE and
* advertise capacity; requesters escrow AERE for a job; the provider
* does the work OFF-CHAIN and submits a result hash; the requester
* confirms (or a timeout auto-confirms) and payment is released. A
* dispute window lets the requester challenge, with Foundation
* arbitration and provider-stake slashing.
*
* SCOPE (honest): this contract is the trust/settlement rail. The actual
* GPU/compute supply is off-chain and requires real hardware providers —
* the network is only as real as the providers who join. No new token:
* staking, escrow and payment are all in native AERE.
*/
contract AereComputeMarket is Ownable, ReentrancyGuard {
uint256 public constant MIN_STAKE = 10 ether; // 10 AERE to be a provider
uint64 public constant CONFIRM_WINDOW = 3 days; // requester confirm/dispute window
uint16 public constant SLASH_BPS = 5000; // 50% of stake slashed on lost dispute
address public immutable FOUNDATION; // dispute arbiter
struct Provider {
bool active;
uint256 stake;
uint32 pricePerUnit; // informational, AERE-wei per compute unit
uint32 activeJobs; // outstanding claimed/submitted jobs
string endpoint; // provider API endpoint (off-chain)
}
enum Status { None, Open, Claimed, Submitted, Paid, Refunded, Slashed }
struct Job {
address requester;
address provider;
uint256 payment; // escrowed AERE
bytes32 specHash; // hash of the off-chain job spec
bytes32 resultHash; // hash of the off-chain result
Status status;
uint64 submittedAt;
}
mapping(address => Provider) public providers;
mapping(uint256 => Job) public jobs;
uint256 public jobCount;
uint256 public slashPool; // slashed stake, sweepable by Foundation to the sink/treasury
event ProviderRegistered(address indexed provider, uint256 stake, uint32 pricePerUnit, string endpoint);
event ProviderDeregistered(address indexed provider, uint256 stakeReturned);
event JobPosted(uint256 indexed id, address indexed requester, uint256 payment, bytes32 specHash);
event JobClaimed(uint256 indexed id, address indexed provider);
event JobSubmitted(uint256 indexed id, bytes32 resultHash);
event JobPaid(uint256 indexed id, address indexed provider, uint256 payment);
event JobRefunded(uint256 indexed id, address indexed requester, uint256 payment);
event JobDisputed(uint256 indexed id);
event DisputeResolved(uint256 indexed id, bool providerWon);
error BadStake();
error NotProvider();
error HasActiveJobs();
error ZeroPayment();
error WrongStatus();
error NotRequester();
error NotJobProvider();
error WindowOpen();
error WindowClosed();
error ZeroAddress();
constructor(address foundation) {
if (foundation == address(0)) revert ZeroAddress();
FOUNDATION = foundation;
}
modifier onlyFoundation() {
if (msg.sender != FOUNDATION) revert("not foundation");
_;
}
/* ------------------------------ providers ------------------------------- */
function registerProvider(uint32 pricePerUnit, string calldata endpoint) external payable {
if (msg.value < MIN_STAKE) revert BadStake();
Provider storage p = providers[msg.sender];
p.active = true;
p.stake += msg.value;
p.pricePerUnit = pricePerUnit;
p.endpoint = endpoint;
emit ProviderRegistered(msg.sender, p.stake, pricePerUnit, endpoint);
}
function deregisterProvider() external nonReentrant {
Provider storage p = providers[msg.sender];
if (!p.active) revert NotProvider();
if (p.activeJobs != 0) revert HasActiveJobs();
uint256 amt = p.stake;
p.active = false;
p.stake = 0;
(bool ok, ) = msg.sender.call{value: amt}("");
require(ok, "xfer");
emit ProviderDeregistered(msg.sender, amt);
}
/* -------------------------------- jobs ---------------------------------- */
function postJob(bytes32 specHash) external payable returns (uint256 id) {
if (msg.value == 0) revert ZeroPayment();
id = jobCount++;
jobs[id] = Job({
requester: msg.sender, provider: address(0), payment: msg.value,
specHash: specHash, resultHash: bytes32(0), status: Status.Open, submittedAt: 0
});
emit JobPosted(id, msg.sender, msg.value, specHash);
}
/// @notice Requester cancels an unclaimed job and reclaims the escrow.
function cancelJob(uint256 id) external nonReentrant {
Job storage j = jobs[id];
if (j.requester != msg.sender) revert NotRequester();
if (j.status != Status.Open) revert WrongStatus();
j.status = Status.Refunded;
uint256 amt = j.payment;
j.payment = 0;
(bool ok, ) = msg.sender.call{value: amt}("");
require(ok, "xfer");
emit JobRefunded(id, msg.sender, amt);
}
function claimJob(uint256 id) external {
Job storage j = jobs[id];
if (j.status != Status.Open) revert WrongStatus();
Provider storage p = providers[msg.sender];
if (!p.active || p.stake < MIN_STAKE) revert NotProvider();
j.provider = msg.sender;
j.status = Status.Claimed;
p.activeJobs += 1;
emit JobClaimed(id, msg.sender);
}
function submitResult(uint256 id, bytes32 resultHash) external {
Job storage j = jobs[id];
if (j.provider != msg.sender) revert NotJobProvider();
if (j.status != Status.Claimed) revert WrongStatus();
j.resultHash = resultHash;
j.status = Status.Submitted;
j.submittedAt = uint64(block.timestamp);
emit JobSubmitted(id, resultHash);
}
/// @notice Requester accepts the result → pay the provider immediately.
function confirmJob(uint256 id) external nonReentrant {
Job storage j = jobs[id];
if (j.requester != msg.sender) revert NotRequester();
if (j.status != Status.Submitted) revert WrongStatus();
_pay(id, j);
}
/// @notice After the confirm window with no dispute, anyone can settle → pay provider.
function autoConfirm(uint256 id) external nonReentrant {
Job storage j = jobs[id];
if (j.status != Status.Submitted) revert WrongStatus();
if (block.timestamp < j.submittedAt + CONFIRM_WINDOW) revert WindowOpen();
_pay(id, j);
}
function _pay(uint256 id, Job storage j) internal {
j.status = Status.Paid;
uint256 amt = j.payment;
j.payment = 0;
providers[j.provider].activeJobs -= 1;
(bool ok, ) = j.provider.call{value: amt}("");
require(ok, "xfer");
emit JobPaid(id, j.provider, amt);
}
/* ------------------------------ disputes -------------------------------- */
function disputeJob(uint256 id) external {
Job storage j = jobs[id];
if (j.requester != msg.sender) revert NotRequester();
if (j.status != Status.Submitted) revert WrongStatus();
if (block.timestamp >= j.submittedAt + CONFIRM_WINDOW) revert WindowClosed();
j.status = Status.Claimed; // parked pending arbitration; marker below via event
// Re-mark to Submitted-disputed is avoided; we keep Claimed + emit; Foundation resolves.
emit JobDisputed(id);
}
/// @notice Foundation arbitrates a disputed job. providerWon → provider paid;
/// else requester refunded AND provider stake slashed by SLASH_BPS.
function resolveDispute(uint256 id, bool providerWon) external onlyFoundation nonReentrant {
Job storage j = jobs[id];
if (j.status != Status.Claimed || j.provider == address(0) || j.submittedAt == 0) revert WrongStatus();
Provider storage p = providers[j.provider];
p.activeJobs -= 1;
uint256 amt = j.payment;
j.payment = 0;
if (providerWon) {
j.status = Status.Paid;
(bool ok, ) = j.provider.call{value: amt}("");
require(ok, "xfer");
emit JobPaid(id, j.provider, amt);
} else {
j.status = Status.Slashed;
uint256 slash = (p.stake * SLASH_BPS) / 10_000;
p.stake -= slash;
slashPool += slash;
(bool ok, ) = j.requester.call{value: amt}("");
require(ok, "xfer");
emit JobRefunded(id, j.requester, amt);
}
emit DisputeResolved(id, providerWon);
}
function sweepSlashPool(address to) external onlyFoundation nonReentrant {
uint256 amt = slashPool;
slashPool = 0;
(bool ok, ) = to.call{value: amt}("");
require(ok, "xfer");
}
}