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.
174 lines
7.9 KiB
Solidity
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);
|
|
}
|
|
}
|