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

174 lines
7.9 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/interfaces/IERC2981.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title AereNFTMarketplace
* @notice On-chain NFT marketplace for AERE Network with EIP-2981 royalty support.
* @dev Sellers list owned ERC-721 tokens for a fixed price in native AERE.
* Buyers purchase atomically: protocol fee → fee recipient, royalty → creator,
* remainder → seller. Buyers can also place expiring offers on any token.
* Marketplace must be approved (setApprovalForAll or approve) before listing.
*/
contract AereNFTMarketplace is Ownable, ReentrancyGuard {
uint96 public constant MAX_FEE_BPS = 1000; // 10%
uint96 public protocolFeeBps = 250; // 2.5%
address public feeRecipient;
struct Listing {
address seller;
uint256 price;
uint64 expiresAt; // 0 = never
}
struct Offer {
address buyer;
uint256 price;
uint64 expiresAt;
}
// collection => tokenId => Listing
mapping(address => mapping(uint256 => Listing)) public listings;
// collection => tokenId => buyer => Offer
mapping(address => mapping(uint256 => mapping(address => Offer))) public offers;
// escrowed offer balances per buyer (refundable)
mapping(address => uint256) public pendingWithdrawals;
event Listed(address indexed collection, uint256 indexed tokenId, address indexed seller, uint256 price, uint64 expiresAt);
event ListingCancelled(address indexed collection, uint256 indexed tokenId, address indexed seller);
event Sold(address indexed collection, uint256 indexed tokenId, address seller, address indexed buyer, uint256 price);
event OfferPlaced(address indexed collection, uint256 indexed tokenId, address indexed buyer, uint256 price, uint64 expiresAt);
event OfferCancelled(address indexed collection, uint256 indexed tokenId, address indexed buyer);
event OfferAccepted(address indexed collection, uint256 indexed tokenId, address seller, address indexed buyer, uint256 price);
event ProtocolFeeUpdated(uint96 newFeeBps);
event FeeRecipientUpdated(address newRecipient);
constructor(address _feeRecipient) {
require(_feeRecipient != address(0), "zero");
feeRecipient = _feeRecipient;
}
// ── Listings ────────────────────────────────────────────────
function list(address collection, uint256 tokenId, uint256 price, uint64 expiresAt) external {
require(price > 0, "price=0");
IERC721 nft = IERC721(collection);
require(nft.ownerOf(tokenId) == msg.sender, "not owner");
require(
nft.getApproved(tokenId) == address(this) || nft.isApprovedForAll(msg.sender, address(this)),
"not approved"
);
listings[collection][tokenId] = Listing(msg.sender, price, expiresAt);
emit Listed(collection, tokenId, msg.sender, price, expiresAt);
}
function cancelListing(address collection, uint256 tokenId) external {
Listing memory l = listings[collection][tokenId];
require(l.seller == msg.sender, "not seller");
delete listings[collection][tokenId];
emit ListingCancelled(collection, tokenId, msg.sender);
}
function buy(address collection, uint256 tokenId) external payable nonReentrant {
Listing memory l = listings[collection][tokenId];
require(l.seller != address(0), "not listed");
require(l.expiresAt == 0 || block.timestamp <= l.expiresAt, "expired");
require(msg.value == l.price, "bad value");
delete listings[collection][tokenId];
_settleSale(collection, tokenId, l.seller, msg.sender, l.price);
emit Sold(collection, tokenId, l.seller, msg.sender, l.price);
}
// ── Offers ──────────────────────────────────────────────────
function placeOffer(address collection, uint256 tokenId, uint64 expiresAt) external payable nonReentrant {
require(msg.value > 0, "price=0");
require(expiresAt > block.timestamp, "expired");
Offer memory existing = offers[collection][tokenId][msg.sender];
if (existing.price > 0) {
// refund prior offer to escrow
pendingWithdrawals[msg.sender] += existing.price;
}
offers[collection][tokenId][msg.sender] = Offer(msg.sender, msg.value, expiresAt);
emit OfferPlaced(collection, tokenId, msg.sender, msg.value, expiresAt);
}
function cancelOffer(address collection, uint256 tokenId) external nonReentrant {
Offer memory o = offers[collection][tokenId][msg.sender];
require(o.price > 0, "no offer");
delete offers[collection][tokenId][msg.sender];
pendingWithdrawals[msg.sender] += o.price;
emit OfferCancelled(collection, tokenId, msg.sender);
}
function acceptOffer(address collection, uint256 tokenId, address buyer) external nonReentrant {
IERC721 nft = IERC721(collection);
require(nft.ownerOf(tokenId) == msg.sender, "not owner");
Offer memory o = offers[collection][tokenId][buyer];
require(o.price > 0, "no offer");
require(block.timestamp <= o.expiresAt, "expired");
delete offers[collection][tokenId][buyer];
// active listing (if any) becomes invalid after transfer; clear it
if (listings[collection][tokenId].seller == msg.sender) {
delete listings[collection][tokenId];
}
_settleSale(collection, tokenId, msg.sender, buyer, o.price);
emit OfferAccepted(collection, tokenId, msg.sender, buyer, o.price);
}
function withdraw() external nonReentrant {
uint256 amount = pendingWithdrawals[msg.sender];
require(amount > 0, "nothing");
pendingWithdrawals[msg.sender] = 0;
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "transfer");
}
// ── Internal settlement ─────────────────────────────────────
function _settleSale(address collection, uint256 tokenId, address seller, address buyer, uint256 price) internal {
IERC721(collection).safeTransferFrom(seller, buyer, tokenId);
uint256 fee = (price * protocolFeeBps) / 10000;
uint256 royalty = 0;
address royaltyReceiver = address(0);
try IERC2981(collection).royaltyInfo(tokenId, price) returns (address r, uint256 amt) {
if (r != address(0) && amt > 0 && amt + fee <= price) {
royaltyReceiver = r;
royalty = amt;
}
} catch { /* collection doesn't implement EIP-2981 */ }
uint256 sellerProceeds = price - fee - royalty;
if (fee > 0) _send(feeRecipient, fee);
if (royalty > 0) _send(royaltyReceiver, royalty);
_send(seller, sellerProceeds);
}
function _send(address to, uint256 amount) internal {
(bool ok, ) = to.call{value: amount}("");
if (!ok) {
// fall back to escrow so a malicious receiver can't block sales
pendingWithdrawals[to] += amount;
}
}
// ── Admin ───────────────────────────────────────────────────
function setProtocolFee(uint96 newFeeBps) external onlyOwner {
require(newFeeBps <= MAX_FEE_BPS, "too high");
protocolFeeBps = newFeeBps;
emit ProtocolFeeUpdated(newFeeBps);
}
function setFeeRecipient(address newRecipient) external onlyOwner {
require(newRecipient != address(0), "zero");
feeRecipient = newRecipient;
emit FeeRecipientUpdated(newRecipient);
}
}